# TaskThread SessionKey 隔离修正(2026-03-29) 术语说明: - 本文写于 `single-agent` 仍是主术语的阶段;凡正文出现 `single-agent`,当前都应读作任务对话模式下的 `agent` 一级目标 - 当前任务对话框 provider 选择与 target/catalog 真源口径,以 [Task Dialog Provider Selection Mainline](/Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app/docs/architecture/task-dialog-provider-selection-mainline.md) 为准 本文补充并修正 XWorkmate 当前“任务线 / 线程 / 工作目录”设计中的一个关键约束: - 左侧任务线不能只是派生 UI 项 - 每个可执行任务线都必须先落成真实 `TaskThread` - 每个可执行任务线都必须绑定独立、稳定的 `sessionKey` - single-agent 的 `workingDirectory` 只能从该 `sessionKey` 对应的 `TaskThread.workspaceBinding` 读取 本文是对现有 `TaskThread` 主模型的补充,不替代: - `task-control-plane-unification.md` ## 1. 问题定义 当前实现里,single-agent 可执行工作目录实际由以下链路决定: ```text currentSessionKey -> normalizedAssistantSessionKeyInternal(sessionKey) -> assistantWorkspacePathForSession(sessionKey) -> resolveSingleAgentWorkingDirectoryForSessionInternal(sessionKey) -> GoTaskServiceRequest.workingDirectory ``` 这条链路说明: 1. 真正决定执行目录的是 `sessionKey` 2. prompt 文本不会创建 first binding,也不会覆盖当前线程绑定 3. 空 `sessionKey` 会被归一为 `main` 4. 一旦多个任务线没有真正切换到独立 `sessionKey`,它们就会共享 `main` 因此,现象上即使 UI 展示出多个“任务线”,底层也可能仍然只有一条真正可执行线程: - `main` - `settings.workspacePath/.xworkmate/threads/main` 这会直接导致: - 每个任务线没有自己的本地工作目录 - single-agent 请求继续复用 `threads/main` - 右栏显示、消息上下文、技能选择和工作区记录都可能混叠到同一线程 ## 2. 修正结论 本轮设计修正后的主结论如下: 1. “任务线”不是纯展示概念,而是 `TaskThread` 的 UI 呈现。 2. 每个可执行任务线必须有自己的 `TaskThread.threadId`。 3. `TaskThread.threadId` 就是该任务线的 `sessionKey`。 4. 任何可触发执行的入口都不得在空 `sessionKey` 下运行。 5. 对非主线程任务线,禁止 silent fallback 到 `main`。 6. prompt `workspace_root` 不是线程身份,也不再参与主链更新;`workspaceBinding` 只能由显式 create/load 绑定或结构化执行结果回写更新。 换句话说: ```text Task Line == TaskThread == sessionKey == threadId ``` 只要这四者没有对齐,任务线隔离就是伪隔离。 ## 3. 设计目标 本修正要保证以下目标: ### 3.1 身份目标 - 每个任务线都有稳定身份 - 该身份可持久化、可恢复、可切换 - UI 当前选中的永远是 `TaskThread.threadId` ### 3.2 执行目标 - single-agent 执行只读取当前线程自己的 `workspaceBinding.workspacePath` - 不允许从消息文本、派生视图或全局临时状态猜测目标线程 - 不允许把未绑定线程默认送到 `main` ### 3.3 信息一致性目标 - 左栏任务线 - 主体消息区 - 右栏工作路径 - 技能选择 - provider 返回回写 以上信息必须围绕同一个 `sessionKey` 聚合。 ## 4. 核心约束 ### 4.1 任务线创建约束 创建任务线时,必须先分配唯一 `sessionKey`,再允许进入 UI 可执行状态。 推荐形式: ```text draft: agent:: ``` 禁止: - 仅创建 UI 派生卡片,不创建 `TaskThread` - 创建后仍停留在空 key - 通过“后面再补 sessionKey”的方式进入可执行状态 ### 4.2 任务线切换约束 用户切换任务线时,必须先完成: ```text UI selection -> switchSession(sessionKey) -> ensureDesktopTaskThreadBindingInternal(sessionKey) -> read TaskThread(sessionKey) -> allow execute ``` 禁止: - 任务线视觉上切换了,但 `currentSessionKey` 没变 - 任务线详情来自 A,执行请求却仍发送到 `main` ### 4.3 single-agent 执行约束 single-agent 请求必须满足: ```text request.sessionId == request.threadId == current TaskThread.threadId request.workingDirectory == current TaskThread.workspaceBinding.workspacePath ``` 禁止: - 从 prompt 文本中提取 `workspace_root` 或其他 side-channel 直接替代线程身份 - 因 `sessionKey` 缺失而自动转发到 `main` - 因右栏展示路径存在而绕过线程绑定 ### 4.4 fallback 约束 `main` 只能作为“主线程身份”,不能作为“任何未绑定线程的兜底线程”。 因此: - 空 `sessionKey -> main` 只允许用于真正的主线程初始化阶段 - 非主线程任务线若缺少 `sessionKey`,状态应为 `needs_binding` 或 `not_runnable` - 本地可执行线程在 create/load 阶段必须已经拥有唯一工作目录 - 不允许继续执行并偷偷落到 `threads/main` ## 5. Workspace 更新的正确角色 运行期 workspace 更新必须通过结构化数据进入主链。 它不是: - prompt 文本中的 `workspace_root` - 线程身份 - session 选择器 - 运行时对当前线程的隐式覆盖命令 它只能是: - create/load 阶段对当前线程 `workspaceBinding` 的显式绑定 - 外部 provider / transport 返回的结构化字段(例如 `resolvedWorkingDirectory` 与 `resolvedWorkspaceRefKind`)对当前线程 binding 的确认更新 因此正确顺序应为: ```text Structured execution result -> update current TaskThread.workspaceBinding -> persist on that TaskThread -> subsequent execute reads TaskThread.workspaceBinding.workspacePath ``` 而不是: ```text Prompt text side-channel -> bypass thread binding -> directly becomes runtime workingDirectory ``` ## 6. 修正后的信息流 ```mermaid flowchart LR A["创建任务线"] --> B["分配唯一 sessionKey"] B --> C["创建 / 更新 TaskThread"] C --> D["写入 workspaceBinding / executionBinding / contextState"] D --> E["任务线进入可见列表"] E --> F["用户切换任务线"] F --> G["switchSession(sessionKey)"] G --> H["currentSessionKey = TaskThread.threadId"] H --> I["ensureDesktopTaskThreadBindingInternal(sessionKey)"] I --> J["读取当前 TaskThread"] J --> K["workspaceBinding.workspacePath"] J --> L["executionBinding"] J --> M["contextState"] K --> N["GoTaskServiceRequest.workingDirectory"] L --> O["provider / execution mode"] M --> P["messages / model / skills"] N --> Q["single-agent execute"] O --> Q P --> Q Q --> R["provider 返回结果"] R --> S["仅回写同一 sessionKey 的 TaskThread"] ``` ## 7. 模块修正要求 ### 7.1 UI / 任务列表 任务列表中的每个可点击项都必须满足: - 拥有真实 `sessionKey` - 该 key 可被 `switchSession(sessionKey)` 命中 - 该 key 能在 `assistantThreadRecordsInternal` 中找到或即时创建对应 `TaskThread` 如果某个列表项没有真实 `sessionKey`,它只能是只读提示项,不能作为可执行任务线。 ### 7.2 Thread binding 层 `localThreadWorkspacePathInternal(sessionKey)` 与 `buildDesktopWorkspaceBindingInternal(sessionKey, ...)` 必须建立在“当前 sessionKey 已经真实存在”的前提下。 它们负责: - 把线程身份映射成稳定目录 - 在同一线程下复用既有 binding 它们不负责: - 替 UI 猜当前任务线是谁 - 把无 key 任务偷偷归并到 `main` ### 7.3 single-agent 入口 single-agent 入口必须在执行前验证: 1. 当前任务线是不是主线程以外的真实线程 2. `currentSessionKey` 是否已切换完成 3. `workingDirectory` 是否来自当前线程自己的 `TaskThread` 若不满足,应报线程绑定错误,而不是继续执行。 ### 7.4 派生任务视图 `DerivedTasksController` 只能消费 session summary,不得反向定义线程身份。 也就是说: - 它可以显示任务线 - 它可以反映状态 - 它不能成为“当前线程是谁”的真相源 线程身份仍然只能由 `TaskThread.threadId / currentSessionKey` 决定。 ## 8. 迁移与兼容 ### 8.1 现有 `main` 线程 现有主线程 `main` 继续保留,作为默认主会话。 ### 8.2 历史上误共享 `main` 的任务线 对历史数据不做自动拆分迁移,原因是: - 无法可靠推断哪些 UI 任务线原本应拆成独立线程 - 自动拆分会破坏既有消息上下文和 provider 会话连续性 处理原则: 1. 历史共享 `main` 的记录继续作为 `main` 2. 从修正版本开始,新建任务线必须创建独立 `sessionKey` 3. 对已暴露出共享问题的入口,优先阻止继续 silent fallback,并移除 prompt / runtime side-channel first-binding ### 8.3 未绑定任务线 任何未完成 `sessionKey` 分配或 `TaskThread` 创建的任务线,必须显式显示为: - 未绑定 - 不可运行 - 需要创建线程 而不是隐式执行到 `main`。 ## 9. 验收标准 设计修正完成后,必须满足以下验收标准: 1. 新建两个任务线时,它们生成两个不同 `sessionKey`。 2. 两个任务线的本地目录分别落在不同路径: - `.xworkmate/threads/` - `.xworkmate/threads/` 3. 切换任务线后,`currentSessionKey` 与右栏路径同步变化。 4. single-agent 请求里的 `sessionId / threadId / workingDirectory` 始终对应当前线程。 5. 任意非主线程缺少 `sessionKey` 时,执行被阻止,而不是回落到 `main`。 6. workspace 更新只接受结构化回写或显式绑定;prompt-only 文本不会进入线程 binding 主链。 ## 10. 与现有架构文档的关系 本文补充的是“线程身份隔离约束”,重点回答: - 为什么任务线必须先成为真实 `TaskThread` - 为什么 `sessionKey` 才是 single-agent 工作目录的身份锚点 - 为什么 prompt side-channel 不能替代线程身份或 workspaceBinding 推荐阅读顺序: 1. `task-control-plane-unification.md` 3. `task-thread-session-key-isolation-20260329.md`(本文)