xworkmate-app/docs/architecture/task-thread-session-key-isolation-20260329.md

324 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<timestamp-or-ulid>
agent:<agentId>:<baseKey>
```
禁止:
- 仅创建 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/<task-a>`
- `.xworkmate/threads/<task-b>`
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`(本文)