feat(chat): ordinary authorized workspace conversations without implicit Goals - #5540
huangruiteng wants to merge 3 commits into
Conversation
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
Signed-off-by: huangruiteng <huangrt01@163.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | reasoning_effort=xhigh
评审精确 head:1e0bedc160a5db421741cf26ff6cc8830e8fd51c;独立基础版本:99839aeb8fed5fae38a5d319391cd050672a6508。
[P1] 迟到的会话刷新会把新工作区输入发到旧工作区。 具体位置:apps/presentation/dashboard/src/features/personal-workspace/project-conversation-panel.tsx:39–44,下一次发送在第62行使用被覆盖的 session_id。打开 A 并发一条消息,点击“刷新会话状态”,在响应返回前选择 B,再让 A 响应返回;页面显示 B,却恢复 A 的记录。随后发送 B 的消息,真实 HTTP POST 和持久 Turn 都落到 A Session。这是打包前端与真实本地后端的独立复现,外部模型是协议 fixture。最小修复是按项目、Session 和请求代次过滤/取消过时刷新,并在发送前核对 session.project_ref;增加延迟 A 响应、A→B→A、关闭/新会话的回归用例。
动机
只想讨论一个已授权项目的用户,需要普通聊天和连续上下文。
此前用户必须先建 Goal 或进入管家对话;当前版本提供工作区聊天入口,但切换到 B 后可能被迟到的 A 刷新响应带回旧会话。
后端能连续两轮并恢复同一原生会话,伪造和撤销授权会拒绝;打包界面已复现选择 B 却向 A 会话发送的回归。
不验收 Lark 私聊、跨主机附着会话、写权限、团队委派或长期 Goal。
改动思路
普通项目对话复用当前 Chat 原生 Session/Turn、read-only adapter 和结果流。host 只提供已配置根目录的 filesystem observation,用户选择稳定 project_ref;TS context owner 将其与当前 root grant、local-owner 受众和只读权限逐项匹配。HTTP 不接受用户用路径或文字造 grant。保存的上下文必须在每次 Turn 前重新确认,Root 消失、symlink 改指或权限撤销不能沿用旧授权。
我检查了基础和 head 的 Goal/manager/native Chat 创建、恢复、peer context、Session store 和 Settings companion 调用方。新 native_chat/project_context.py 是 host 观察 adapter,通用决定仍在 TS 的 conversation_scope/project_conversation;不另造 Goal、portfolio identity、任务账本或模型 runner。secondary Settings 的最短有效路径是选择当前工作区后直接发送:缺 Session 时自动打开/恢复,不强制用户额外确认已经给出的选择。打开/新建/关闭为可选会话管理。
具体改动
完整26文件 diff +589/−24,另有三张公开合成 UI 图片:共享 typed scope/resolver 与 effect 注册;原生 Session 创建/恢复/store/turn 前的权限复核及只读 adapter 处理;HTTP projects/create 路由;frontend typed summary 的 nullable Goal 和 activity projection 伴随调整;Settings panel134行和CSS26行;三项 native HTTP 测试、两项 TS 测试;双语 RFC及既有 IO manifest 行位置刷新。所有表面都在本次完整 diff 内评估,问题不是缺少 frontend 入口,而是入口的异步身份处理有缺陷。
关键代码讲解
ChatProjectContexts(project_context.py:25):只从 host 配置 roots 生成观察,保存初始 canonical target 防 symlink retarget;session_context校验 Goal 必须为 null、当前完整 context 和 channel 必须匹配,objective 明确排除写入、Goal、委派、调度和 portfolio。resolveProjectConversation(project_conversation.ts:14)和复用的normalizeProjectContext:要求唯一 exact authorized ref、合法绝对根目录、local_owner/workspace_read;保存 context 不同就拒绝。peer context 的私有访问资格只接受既有 Goal/portfolio scope,普通 project variant 不变成 registered peer 或全局管家。ChatRuntimeController.open_session(chat_runtime.py:553):显式 project_ref 沿既有 Session 生命周期创建或恢复,工作目录/objective 从 fresh grant 派生;跳过 Goal 附着选择。enqueue/preparation 均复核,外部 Lark origin 和 LoopX execution 不能借普通对话扩大执行权。真实 backend fixture 验证两轮和 controller 重启接续同一 upstream thread。ProjectConversationPanel.refresh(:39):只检查组件仍 mounted,然后无条件写session/messages/turnId。工作区 select 可在刷新期间变更;第62行send直接沿用这个 session。mounted 不能说明响应仍属于当前工作区。这是开头 P1 的根因;单靠后端合法 grant 不会发现“两个都已授权、但选择的是另一个”的错误。
依据 docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,不可变版本 99839aeb8fed5fae38a5d319391cd050672a6508:Scope 明确要求 A→B→A 和 late response 只更新原 source/session/Turn;本 head not_met。Result 要保留结果的原会话语义;旧记录覆盖新选择的情形 not_met。Authority 的伪造/撤销/源改变拒绝路径在后端已验证。Packaging 只取得当前源代码打包 frontstage 的有界证据,仍为 deferred;Acceptance 未引入新 ledger,既有 canonical dedupe 保留,本轮未完成丢失网络响应的完整资格。RFC 的本 PR 编辑没有覆盖掉先前的 Scope 要求。
对主干的风险
我用当前 head 重新构建并验证 Chat bundle,包括 frontend tsc,通过 ego-browser 操作打包 /chat/ 的真实 Settings 入口,连接隔离的 ChatHTTPServer/ChatRuntimeController/原生 Session 文件。外部 Codex app-server 采用仓库协议 fixture,没有真实模型或账户调用。先完成 A 正常消息,再只延迟真实 A GET 的返回(未伪造数据),切到 B,释放 A 响应并发送。浏览器 selected 是 B,POST 却指向 A Session;独立读取持久 project_context 和 Turn 确认其仍归 A。正常 B control 使用 B 自己的 Session,桌面与390px窄屏均检查了包含对话内容的整个 viewport;窄屏正常流程可用,不能抵消异步身份错误。
三项普通项目原生测试通过:两轮与重启连续性、不生成隐藏 Goal/portfolio、伪造字段/ref、撤销、外部受众和 symlink retarget 拒绝;两项 TS 测试通过。完整语义漂移与相邻 store/input、HTTP CORS、delegation wake scope 组 179 passed;control-plane typecheck、changed Python Ruff、CSS token 检查和 diff hygiene 通过。三个 commit 均有 DCO sign-off,公开截图为合成内容,未发现私有本机路径或 Goal 资料。一次查询不存在的 dashboard typecheck npm script 是 reviewer 准备错误;成功 package build 已执行真实 frontend tsc。按配置没有查询 CI。
最强反例恰好说明为什么上述 green backend tests 不足:它们直接调用 HTTP/context,不经过 React 的待返回 refresh。当前不存在修复后的 passing race receipt。完整相同 baseline 的公共 API/旧 Goal 分支执行矩阵、所有新增 root/recovery 变体、paid model/installed host、Lark DM 和跨 host 行为未测;没有宣称 observable equivalence 或全部 GQ 已完成。这里也没有 PostgreSQL store refactor,不涉及真实数据库迁移验收。
我的整体评价
REQUEST_CHANGES。共享 owner、权限分层和有用的普通对话入口是合理设计;backend 增量能连续执行并保留正确 read grant。但所选项目和 Session 不是同一个身份时,会混入旧项目上下文,执行用户没选择的工作区请求,这是当前 slice 的用户旅程回归。
最小修复保持现有 typed owner 和 store:用 current project/Session generation 或取消标识阻止迟到 hydration,在 send 前验证 project_ref;用户切换、新会话、关闭/停止均使过时回调失效。用真实打包入口重跑开头负例,要求 A 的迟到结果不改变 B,下一次 send 只能创建或使用 B Session。相邻未来重构检查支持这个窄生命周期 fence,不需要第二个聊天 store 或抽象框架。修复后再按新 exact head 完整复核。
English verdict: REQUEST_CHANGES - 1e0bedc. P1: a late Session-A refresh repopulates state after selecting Workspace-B, so the next B-intended message is posted and persisted to A's native Session. Reproduced on the rebuilt packaged frontend with real local HTTP/store and a fake external model protocol; independent B control passes. Native3, TS2 and adjacent179 cases pass, but they do not cover this race. Fence hydration and sends by selected project/session identity; CI was not consulted.
| async function refresh(sessionId: string) { | ||
| const current = await fetchChatSession(sessionId); | ||
| if (!mounted.current) return; | ||
| setSession(current.session); |
There was a problem hiding this comment.
[P1] Fence this hydration by the selected project/Session generation. Refreshing A does not mark the panel busy, so the user can select B while the real GET is pending; its late response restores A here, and send() then posts the B-intended input to A. Reproduced on the packaged frontend with real native HTTP/Session files. Ignore or abort obsolete results, verify session.project_ref before sending, and cover delayed A response after A→B (including A→B→A and close/new-Session variants).
Chat currently requires a Goal or the global manager, so an ordinary workspace question inherits a role it did not select. This change adds a host-authorized, read-only project context and a secondary Settings → Project conversation entry point. Two messages and a process restart retain the same Core Session and native thread without creating a Goal or collecting portfolio context.
The existing typed conversation owner resolves the workspace reference and grant; Python observes configured roots and performs IO. Sessions persist the exact context. Unknown, missing, or retargeted roots, Goal-bearing project requests, LoopX execution, and external audiences fail closed. The UI supports open/resume, new/close, canonical history, durable admission readback, and exact stop; an admission failure retains the draft and request identity.
This is an independently usable local workspace slice from current main. Lark project audience binding, timely later-input admission, authorized Agent selection, live Lark/model qualification and mobile Bot journeys remain open in the existing RFC. This PR does not include or depend on the unmerged transport scheduling changes in #5538 and does not qualify a deployment.
Validation:
a worktree venv wins over an unusable system python3fails because the fixture resolves Homebrew Python; reproduced on unchanged local checkout1e9e82b03(not the exact PR baseline99839aeb8). The resolver and test are unchanged in this PR.Public-safe synthetic visual evidence:
Current exact-head qualification (
1e0bedc160a5db421741cf26ff6cc8830e8fd51c):99839aeb8; this attribution does not cover every CI failure.