9.5 KiB
TaskThread SessionKey 隔离修正(2026-03-29)
本文补充并修正 XWorkmate 当前“任务线 / 线程 / 工作目录”设计中的一个关键约束:
- 左侧任务线不能只是派生 UI 项
- 每个可执行任务线都必须先落成真实
TaskThread - 每个可执行任务线都必须绑定独立、稳定的
sessionKey - single-agent 的
workingDirectory只能从该sessionKey对应的TaskThread.workspaceBinding读取
本文是对现有 TaskThread 主模型的补充,不替代:
task-control-plane-unification.md
1. 问题定义
当前实现里,single-agent 可执行工作目录实际由以下链路决定:
currentSessionKey
-> normalizedAssistantSessionKeyInternal(sessionKey)
-> assistantWorkspacePathForSession(sessionKey)
-> resolveSingleAgentWorkingDirectoryForSessionInternal(sessionKey)
-> GoTaskServiceRequest.workingDirectory
这条链路说明:
- 真正决定执行目录的是
sessionKey - prompt 文本不会创建 first binding,也不会覆盖当前线程绑定
- 空
sessionKey会被归一为main - 一旦多个任务线没有真正切换到独立
sessionKey,它们就会共享main
因此,现象上即使 UI 展示出多个“任务线”,底层也可能仍然只有一条真正可执行线程:
mainsettings.workspacePath/.xworkmate/threads/main
这会直接导致:
- 每个任务线没有自己的本地工作目录
- single-agent 请求继续复用
threads/main - 右栏显示、消息上下文、技能选择和工作区记录都可能混叠到同一线程
2. 修正结论
本轮设计修正后的主结论如下:
- “任务线”不是纯展示概念,而是
TaskThread的 UI 呈现。 - 每个可执行任务线必须有自己的
TaskThread.threadId。 TaskThread.threadId就是该任务线的sessionKey。- 任何可触发执行的入口都不得在空
sessionKey下运行。 - 对非主线程任务线,禁止 silent fallback 到
main。 - prompt
workspace_root不是线程身份,也不再参与主链更新;workspaceBinding只能由显式 create/load 绑定或结构化执行结果回写更新。
换句话说:
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 可执行状态。
推荐形式:
draft:<timestamp-or-ulid>
agent:<agentId>:<baseKey>
禁止:
- 仅创建 UI 派生卡片,不创建
TaskThread - 创建后仍停留在空 key
- 通过“后面再补 sessionKey”的方式进入可执行状态
4.2 任务线切换约束
用户切换任务线时,必须先完成:
UI selection
-> switchSession(sessionKey)
-> ensureDesktopTaskThreadBindingInternal(sessionKey)
-> read TaskThread(sessionKey)
-> allow execute
禁止:
- 任务线视觉上切换了,但
currentSessionKey没变 - 任务线详情来自 A,执行请求却仍发送到
main
4.3 single-agent 执行约束
single-agent 请求必须满足:
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 的确认更新
因此正确顺序应为:
Structured execution result
-> update current TaskThread.workspaceBinding
-> persist on that TaskThread
-> subsequent execute reads TaskThread.workspaceBinding.workspacePath
而不是:
Prompt text side-channel
-> bypass thread binding
-> directly becomes runtime workingDirectory
6. 修正后的信息流
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 入口必须在执行前验证:
- 当前任务线是不是主线程以外的真实线程
currentSessionKey是否已切换完成workingDirectory是否来自当前线程自己的TaskThread
若不满足,应报线程绑定错误,而不是继续执行。
7.4 派生任务视图
DerivedTasksController 只能消费 session summary,不得反向定义线程身份。
也就是说:
- 它可以显示任务线
- 它可以反映状态
- 它不能成为“当前线程是谁”的真相源
线程身份仍然只能由 TaskThread.threadId / currentSessionKey 决定。
8. 迁移与兼容
8.1 现有 main 线程
现有主线程 main 继续保留,作为默认主会话。
8.2 历史上误共享 main 的任务线
对历史数据不做自动拆分迁移,原因是:
- 无法可靠推断哪些 UI 任务线原本应拆成独立线程
- 自动拆分会破坏既有消息上下文和 provider 会话连续性
处理原则:
- 历史共享
main的记录继续作为main - 从修正版本开始,新建任务线必须创建独立
sessionKey - 对已暴露出共享问题的入口,优先阻止继续 silent fallback,并移除 prompt / runtime side-channel first-binding
8.3 未绑定任务线
任何未完成 sessionKey 分配或 TaskThread 创建的任务线,必须显式显示为:
- 未绑定
- 不可运行
- 需要创建线程
而不是隐式执行到 main。
9. 验收标准
设计修正完成后,必须满足以下验收标准:
- 新建两个任务线时,它们生成两个不同
sessionKey。 - 两个任务线的本地目录分别落在不同路径:
.xworkmate/threads/<task-a>.xworkmate/threads/<task-b>
- 切换任务线后,
currentSessionKey与右栏路径同步变化。 - single-agent 请求里的
sessionId / threadId / workingDirectory始终对应当前线程。 - 任意非主线程缺少
sessionKey时,执行被阻止,而不是回落到main。 - workspace 更新只接受结构化回写或显式绑定;prompt-only 文本不会进入线程 binding 主链。
10. 与现有架构文档的关系
本文补充的是“线程身份隔离约束”,重点回答:
- 为什么任务线必须先成为真实
TaskThread - 为什么
sessionKey才是 single-agent 工作目录的身份锚点 - 为什么 prompt side-channel 不能替代线程身份或 workspaceBinding
推荐阅读顺序:
task-control-plane-unification.mdtask-thread-session-key-isolation-20260329.md(本文)