Feat/delegation continue sessions - #382
Conversation
Replace the v1 one-shot delegation lifecycle (child torn down at first terminal state, broker.rs "v1 one-shot" comment) with continuable sessions shared between the parent LLM and the user. Backend: - Session/Turn/Operation three-layer split inside the broker; the completed cache now only caches result text (evicting it can no longer change domain behavior) - continue_with_session / close_session MCP tools (wire-compatible with upstream PR xintaofei#375; close is release semantics, session_released with session_closed accepted as a historical alias) - Keep-alive with kept_alive_cap (global + per-parent FIFO) on top of idle-sweep reclamation; resume via external_id with triple binding checks (agent_type/folder/uniqueness) and pre-side-effect resume_unavailable instead of silent session/new downgrade - Startup rebuild protocol (rebuilding retryable state, per-row failure isolation, ledger not reused across restarts) - Continuation registered cancellable before the follow-up prompt is sent (closes the register-window cancel race), close-vs-continue serialization hardened at the settle_session choke point - update_external_id resume-safe/skip-delegate guards at all four call sites so a degraded session/new can never overwrite the resume credential of a delegate row - session-scoped delegation_session_update event (task_id, turn_id, turn_version, origin); no duplicate completion against an already-terminal parent_tool_use_id User entry point (not present in upstream PR xintaofei#375): - continue/close/availability HTTP + Tauri commands keyed by conversation id, continuation_id idempotency ledger across all three entries (MCP arm included, listener never mints ids) - Sub-agent dialog composer gated by five-state continuation availability; errors surfaced with stable codes - Sidebar discoverability: Sub badge, sub-session filter toggle (default off), delegate rows excluded from the root list by DB markers only (immune to worktree indentation) - Multi-turn timeline: prefix cut anchored to the in-flight user turn so earlier completed turns stay visible while a new turn streams Verification: rust --lib 1805 passed / clippy -D warnings clean / frontend vitest 2835 passed / tsc clean. Independent read-only review (heterogeneous model) returned 0 critical; both important findings (close-vs-continue compound-failure leak, unguarded external_id call sites) fixed with red-green tests in this change. Spec: docs/specs/delegation-continue-session/{requirements,design,tasks}.md (3-round codex cross-review converged; introspection archived)
…igin settles
① 根因: 续聊轮(turn_version>1)按 2.8a 抑制第二次 delegation_completed,
替代事件 delegation_session_update 存在两个消费缺口——
a) delegation-context(状态卡)只消费 started/completed,卡片在续聊轮
的 started 重播后翻回"运行中"却永远收不到落停信号;
b) User-origin 续聊的 parent_connection_id 是合成 id,emitter 解析不到
即静默丢弃 → 用户从 Dialog 发起的续聊完成时前端零推送,只能重开
Dialog 才能看到结果。
② 修复: emitter 增加 child_connection_id 回落(父连接解析不到时改走子连接
事件流,桌面 firehose / web attach 流均可达);delegation-context 消费
session_update 将卡片落停(ok + 调度 detach,与 completed 路径对称);
Dialog 收到本子会话的 session_update 时 refetchDetail(preserveLive)。
③ 影响面: event_emitter trait 及 Noop/Mock/prod 三实现、broker 调用点与
注释、delegation-context、sub-agent-session-dialog;新增 Rust 回落
fan-out 测试;Rust delegation 349 passed、clippy 零告警、tsc 零错、
vitest 57 passed。
…lt-in Task tool ① 根因: 用户说"派个 sub"时,主 AI 默认选自家内置 Task/Agent 工具—— 其进程内 subagent 对用户不可见不可交互(源码核实 v2.1.88:寻址表与 实体均为进程内存,无外部驱动面),用户诉求的"可切入交互/待命"落空。 ② 修复: delegate_to_agent 描述开头新增与内置 Task 工具的抉择规则—— 用户要求可交互/可待命/可回查的子代理派发时用本工具;@ 提及仍是 确定性硬路由(已有)。 ③ 影响面: 仅 tool_schema.json 文案;companion 59 测试通过,JSON 校验 OK。
评审结论:架构扎实,但恢复路径有两个洞,建议先修再合本地在 PR 分支( 验证结果
阻断项1. 用户侧续聊的 resume 路径完全不可用(100% 复现)
// src-tauri/src/acp/manager.rs:2576
let parent = conns.get(parent_connection_id).ok_or_else(|| {
SpawnerError::Spawn(format!("parent connection {parent_connection_id} not found"))
})?;
也就是说,弹窗判定为 修法:resume spawn 需要一条不依赖父连接的路径(比如把 emitter / owner_window 从子会话自身或 AppState 取,或让用户侧传一个真实可解析的宿主连接)。 2. MCP 侧 resume 会静默降级成冷会话,丢掉全部上下文PR 的核心承诺是「恢复不了在发送前返回稳定错误码,而不是静默开新会话丢上下文」(Requirement 3.3)。这条目前没有端到端闭合。
// src-tauri/src/acp/connection.rs:3221
tracing::warn!("[ACP] session/load failed ({err_str}), falling back to session/new");
// ... "Method not found" is expected for agents that don't support session resume (e.g. Cline)
// → 该情况不发可见错误,直接 session/new,取 fallback_sid,照常 emit SessionStarted完整链路(父连接真实存在的 MCP 续聊路径):
注意第 2 步的 optimistic 分支在真实场景里是常态而非例外——进程一死 state 就从 map 里移除了。
修法上有个坑要注意: 3. 非正常退出后,子会话会永久卡在 Running
于是崩溃 / 强退 / 升级重启时如果有子代理正在跑:
rebuild 的两个用例都没覆盖到这条—— 建议 rebuild 时把 (严格说不是"绝对永久"——直接改 DB 或其他状态写入方能改回来;但在正常产品流程里没有出路。) 4.
|
让
delegate_to_agent派出的子代理不再是一次性的:continue_with_session(task_id, message, continuation_id)/close_session两个 MCP 工具。子任务完成后子代理保留(kept-alive 池,全局 + 每父会话双层上限,FIFO 驱逐),主 AI 可在同一task_id下多轮追问,不再重派、不再重复探索。
get_delegation_status同样可见(同一 task 的新 turn)。session/loadresume;恢复不了会在发送前返回稳定错误码(resume_unavailable等 6 个),而不是静默开新会话丢上下文。Sub徽标。与 #375 的关系
实现了与 #375 相同的 wire 契约(工具名/参数/错误码可对齐),动手前对 #375 做过逐行审阅,发现三个问题,本 PR 均已在结构上规避:
released是资源状态,resume 凭证external_id永不被覆盖),回收后续聊走 resume 路径。
其它设计点:
continuation_id强制幂等(重试同一次提交返回首次结果,不双发);事件按turn_version单调排序,乱序/丢失时前端以get_delegation_status为权威源重查;续聊可用性由后端五档判定(running / continuable_live /continuable_resume / released / not_continuable),前端不自行推导。
兼容性
delegation_completed仍只对首轮 tool_call 发一次;续聊轮终态由新增的delegation_session_update(session 级寻址)宣告,旧前端忽略该事件不受影响。测试
cargo clippy -D warnings干净