Skip to content

feat(delegation): 支持基于原任务的同会话续作 - #693

Open
asteroida123 wants to merge 17 commits into
xintaofei:mainfrom
asteroida123:codex/delegation-continuation-local
Open

feat(delegation): 支持基于原任务的同会话续作#693
asteroida123 wants to merge 17 commits into
xintaofei:mainfrom
asteroida123:codex/delegation-continuation-local

Conversation

@asteroida123

@asteroida123 asteroida123 commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

已完成的委托需要复验或返工时,父 Agent 可以把新任务交回原子会话,并获得独立的新 task ID。原任务结果保持不变,后续仍使用现有查询、取消和查看入口。

关联 #603;这是关闭的 #689 的缩小范围实现,不自动关闭 Issue。

delegate_to_agent(agent_type, task) → T0
get_delegation_status(task_ids=[T0]) → completed

delegate_to_agent(agent_type, new_task, continue_from_task_id=T0) → T1
get_delegation_status(task_ids=[T1]) → 本轮结果

实现与边界

  • 只扩展原 delegate_to_agent,使用一张 delegation_task 账本保存关联、恢复绑定、释放确认和冻结终态。不引入独立 Session/Turn 协调器或新的轮询面板。
  • 自动续作严格恢复原 Agent session,并把 agent / session ID / 工作目录作为身份硬约束;mode/config 只做 best-effort 恢复并记录漂移告警。恢复失败不会退回新会话。旧 resume_delegation 仅兼容无账本的历史中断任务,也改走严格恢复。
  • 每个来源任务只允许一个后继。相同来源与相同请求重试返回既有后继;不同请求返回冲突。沿最新 task ID 继续,不能从同一旧来源任意分叉。
  • 当前执行结束并完成释放后,人可以打开子会话继续聊天;已有活连接或仍在回收时,自动续作返回 busy。不长期占用子会话写权限。
  • 全局状态仲裁边界:若同一会话已经先被取消,随后到达的正常完成事件不会覆盖取消状态,也不会再次发出 ConversationStatusChanged。这是本 PR 对所有会话的显式行为变化。
  • 使用既有进程退出通知确认释放;保留 vendor ChildGuard 小修,避免监控 future 尚未首次 poll 就被丢弃时漏回收。不包含旧方案的全局 SIGKILL 与 10ms 轮询改造。
  • Codex 并行工具历史中的语义 MCP 事件按真实 ID、参数、结果恢复独立卡片;无法证明归属时保留原回退。各历史任务状态从账本恢复。
  • 复用现有委托总开关,由显式 continue_from_task_id 触发。不提供自动工作流、独立关闭工具、上下文压缩治理或 Token 节省承诺。

#603 范围映射

Issue 期望 本 PR 契约
主 Agent 审核后交回原子会话、用户打开子会话续聊 已实现;自动续作需来源结束且释放已确认,人工续聊沿用现有入口
每轮独立结果、旧结果不被覆盖 已实现;每轮新 task ID,与同一 child conversation 关联;不新增稳定 task ID 下的 turn_id / turn_version
恢复原 session、父连接变化后恢复 持久化绑定配合严格 resume/load;通过当前父会话身份鉴权,不依赖旧 parent connection ID
live/resumed/loaded 类型结果与热连接复用 采用不同契约:活连接返回 busy,恢复失败提供错误码;未公开提供上述 typed outcome
幂等与单写保护 每个来源单后继、同请求重试返回既有结果、异请求冲突;有完成/释放顺序及并发回归
异常退出后的 Running 无活任务的 running 账本投影为 unknown / interrupted;缺少终态或释放证据时拒绝自动续作,不盲目重发。历史卡片显示“已中断,结果未知”,不会把 unknown 查询结果写成已确认失败
父会话 @Session 搜索 delegation children 未实现;当前搜索默认排除 children。卡片查看入口不等价于 @Session 支持
close_session、完整多轮状态面板、统计评测 本 PR 不覆盖
HTTP/LAN UUID、CJK 编辑器行为 沿用现有入口;不新增 UUID 生成或编辑器发送路径,本轮未做专项实机验收

验证

当前 head 为 27e0ea9c,已合入 upstream/main a6918223。拆出的 #701(ChildGuard)和 #702(Codex semantic MCP cards)已在上游合并;本分支的对应冲突采用上游最终版本,三处拆分文件与 upstream/main 一致。F1–F4 返工以及本轮 F7、F9、可选参数兼容和 schema 文案修正均已推送;最新 CI 以 PR 页面为准。

  • 合并后前端全量:432 文件、6248 项通过,全量 ESLint 与静态导出通过;最后的中断提示改动另跑相关 67 项测试、目标 ESLint 和静态导出,全部通过。
  • 旧库升级:按新账本迁移之前的实际迁移链创建磁盘数据库,写入已完成及运行中旧子任务,关闭并重开后升级;旧会话记录保持一致,不补造旧账本,新任务入账和查询通过。该测试覆盖数据库记录,不声称覆盖外部 transcript 文件。
  • 混合历史:同一 Codex rollout 中交错原生子 Agent 与 MCP 首轮/续作事件,独立 task ID、原 child conversation 和各自结果关联通过。
  • vendor:仓内独立 cargo test --manifest-path vendor/sacp-tokio/Cargo.toml --lib 的离线版本 20 项通过,包含 unpolled monitor 回收测试。现有上游 CI 已有该独立步骤,本轮无需重复添加;该测试不会随主 crate 的测试命令自动执行。
  • 重启续作:磁盘数据库关闭重开、新 broker 和新父连接下,真实 run_connection 先恢复原 session,再仅发送一次新任务 prompt;无 session/new,新后继入账,旧结果不变。该协议夹具使用自定义 spawner,终态由测试调用 broker 完成钩子驱动,不覆盖生产 ConnectionManager 与生命周期事件接线的完整端到端路径。
  • 当前代码树所对应的既有验证:服务器全量 3601 项通过、1 忽略;桌面全量 3694 项通过、2 忽略。桌面 all-targets、服务器及 MCP 严格 clippy 均通过,服务器/MCP 编译通过。后续的小改动另经三项协议回归和最终 CI 验证。

此前 macOS 真实浏览器验收:Codex 主 Agent 与两名 Codex 子 Agent 完成三轮辩论,6 个任务、2 个子会话、4 次续作,跨轮记忆标记一致。重启后六个旧任务查询、历史卡片及两方查看入口通过;另一次任务实际点击停止、确认释放后,人工继续原子会话成功,六份冻结结果不变。这些是此前版本的实机记录,本轮新增的自动化验证不冒充重新执行该实机流程。

额外 macOS 真实供应商验收:真实 Codex 父 Agent 经 MCP 委托 Claude Code(实际模型 mimo-v2.5-pro)、Antigravity 和 CodeBuddy,验证多轮及重启后续作。Claude/MiMo 和 Antigravity 通过;CodeBuddy 的间歇认证限制见下。

DeepSeek Harness 实测发现:deepseek-acp 0.8.0 返回完成后,尾轮原生历史尚未保存,立即回收进程会丢失回复;独立于 Codeg 的最小 ACP 客户端亦可复现。临时 Codeg 保存等待补丁曾通过三轮、重启及停止复验,但已撤回,修复已提交至 deepseek-acp #8,使用公开 flush 接口确认保存;适配器补丁已通过独立客户端零等待退出及冷启动恢复复验。当前 PR 不声称 DSH 跨进程续作已可靠。

CodeBuddy 原会话续作首次再现 401;同一本机 CLI 直接调用成功后,有限重试通过并保留原暗号。未修改登录或凭据,401 的原因尚未确认,且 Codeg 将该错误归为 child_refusal。这里不声称修复了 CodeBuddy 的间歇认证问题。

Linux/Windows/Docker 的真实供应商实机仍未验证;旧 schema 升级测试不等于真实生产数据升级验收。

协议夹具将进程启动、恢复完成及退出阶段独立限为 30 秒,为 Windows 进程启动留出等待时间;逐轮响应仍限为 5 秒,50 轮、顺序和禁止冷建会话的断言保持不变。

@xintaofei

Copy link
Copy Markdown
Owner

评审意见(#693 同会话续作)

先说感受:这个 PR 的方向是对的,账本那一层写得也确实干净——尤其是 PR 描述里对验证边界的自我限定("协议夹具使用自定义 spawner…不覆盖生产 ConnectionManager 与生命周期事件接线的完整端到端路径")非常诚实,省了评审很多来回。下面的意见基本都是围绕执行边界回归面,不是对功能本身的质疑。

我这边跑过:cargo check --features test-utils --tests 通过(exit 0);cargo test --lib -- delegation_task_service continuation_protocol_tests 13 passed / 0 failed。另外让 Codex 做了一轮独立复核,它推翻了我最初的 3 条判断(见文末),这几条我已经删掉了。

总体判断:方向批准,但建议先返工 4 处再合。


一、先说做得好的(返工时请务必保留)

  • delegation_task 账本本身。 把委托状态从 conversation.delegation_call_id 这个可变指针升级成"每次执行一行",是这个功能唯一正确的地基。旧模型下一个子会话只能记住一轮,多轮必然互相覆盖。
  • 那条真·磁盘库升级测试m20260908_000001 里的 upgrades_pre_ledger_database_without_rewriting_legacy_history):先 Migrator::up(Some(ledger_idx)) 造出上游 schema、写入旧数据、close()重开连接再升级、逐字段比对旧投影——这是这个仓里升级测试该有的样子。
  • 来源槽位的并发语义:靠 source_task_id 唯一索引占位、insert race 由 reconcile_insert_race 归一到同一个赢家、幂等重试返回 Existing、不同请求返回 Conflict,而且回归测试是真并发(tokio::join!)。
  • admit_continuation 把"占位"和"推进子行指针"放进同一个事务,杜绝幻影指针。
  • lifecycle.rs 改用连接上不可变的 delegation_task_id 做 TurnComplete 路由,而不是子行可变的 delegation_call_id——顺手修掉了"人类在子会话里发一轮、结果解决了 broker 的 pending 委托"这个老问题。
  • vendor 的 ChildGuard 修复monitor_childasync fn 改成提前构造 guard 的 fn)是真 bug 真修:future 在首次 poll 前被 drop 时 Child 会被直接丢掉,on_exit 永不触发。这条独立于本 PR 就该合。
  • codex code-mode 里语义 McpToolCall 还原成独立卡片那块,tool_names.len() + 名字逐一相等的双重守卫足够保守,边界测试也写到位了。

二、建议合并前返工(4 条)

F1 · 释放屏障和断连保留被套到了所有 resume 出来的会话

connection.rs:2277 的判据是 delegation_task_id.is_some() || session_id.is_some()manager.rs:2501 / 2929retained 同理。于是每一条"打开一个已存在会话"产生的连接(侧边栏点开旧会话、work_task 引擎 resume、chat_channel、web 端 acp_connect)都进了 broker 的两阶段释放路径:

  1. 普通关标签页变成了"驱动被强杀"。 disconnect()driver_cancel.cancel()、之后才 cmd_tx.send(Disconnect)。等 driver 下次被 poll 时 cancelled() 已 ready,而 connection 分支还没拿到 Disconnect、不可能 ready,所以走 cancel 分支返回 Err(protocol("connection driver canceled"))connection.rs:2443 发出 AcpEvent::Error { terminal: true }。原来优雅路径返回 Ok(()),一个 Error 都不发。那个 emit 点的注释自己写着"唯一真正 terminal 的 emit 点,lifecycle 用这个 flag 决定要不要把行翻成 Cancelled"。
  2. 优雅 teardown 被跳过run_conversation_loop 的 Disconnect 分支(connection.rs:10011)会发 ACP session/cancelterminal_runtime.release_all_for_session,cancel 路径直接跳过。
  3. 关掉再打开同一个会话会报错。 槽位要等 driver_done && reaped 才释放。这期间再 acp_connect(session_id=S)find_connection_for_session(现在同时匹配 requested_session_idexternal_id)命中残留项 → 状态还是 Connected 就复用一个 driver 已被取消的死连接,否则返回 AcpError::SessionBusymanager.rs:847)直接抛给前端。而 use-connection-lifecycle.ts:343 明确:空闲标签页 unmount 时是会 disconnect 的(只有正在 prompting 才保活),所以"关掉旧会话→立刻再点开"是最日常的路径;idle sweep 定期回收也会撞同一条。
  4. 没有兜底:这条路径上 ChildGuard::drop 只发 SIGTERM,没有超时、没有 SIGKILL 升级(kill_tree_and_wait 只在 disconnect_by_agent_type 用)。一个忽略 SIGTERM 的 agent 会把这个 session 卡死到本次 app 退出。
  5. work_task 撞到 SessionBusy 会走 engine.rs:1240 的 resume 失败分支,记一条 resume_fallback新建 session + 新建 conversation 行(有日志,不算静默,但对用户就是"任务会话莫名断代")。
  6. park_draining 被跳过 → live_or_draining_agent_names()(外部状态恢复的互斥依据)看不到这些子进程;list_connections() 会返回幽灵连接。

这块目前零测试覆盖:所有 disconnect 测试用的 fake_connection / insert_test_connectionrequested_session_id 都是 None

建议retained 和 barrier 的判据都收紧成 delegation_task_id.is_some(),人类会话的 disconnect / dedup / reuse 语义一行不改。严格续作要判"session S 是否还被占用",去查现成的 draining 列表(那里本来就有 pid cell),配有界等待 + SIGKILL 升级——最坏等几百毫秒,而不是卡死一个 app 生命周期。

F2 · released / running 没有任何修复路径,续作会被永久锁死

mark_released 只有 connection_releasedmark_admission_ready 两个内存态入口,没有启动对账。admit_ondelegation_task_service.rs:200)要求来源 terminal && released,否则 broker 返回 ContinuationBusy。以下情况永久不可续作:

  • app 崩溃/被强杀时任务在跑 → 行停在 runningreleased=false
  • 正常退出时子连接还活着:disconnect_all / disconnect_by_agent_type 自己把 map 项摘掉了,barrier 的 owns_slotconnection.rs:1319)判定失败 → connection_released 永不触发;
  • admit_continuation 已推进子行指针、prompt 还没发出去就崩 → 来源多了一个永远跑不完的幽灵后继,来源侧冲突、后继侧 busy,这个子会话彻底死路。

而且 LLM 拿到的 continuation_busy("has not finished process cleanup")读起来像瞬时错误,schema 也没说这是终局,模型只会无限重试。

建议:加启动对账——所有 status = running 的行写入终态 unknown/interrupted 并置 released = true(重启后不可能还有上一轮进程被本 broker 持有),用现成的磁盘库测试风格覆盖。顺便在 continuation_busy 的 message 和 schema 里写清这是终局。

F3 · 新增的「已中断」标签在实时查询路径上根本到不了

src/lib/delegation-status.ts:582

case "unknown":
  return { status: "err", errorCode: "unknown" }   // ← 硬编码,丢弃 report.errorCode

后端中断报告是 {status:"unknown", error_code:"interrupted"}broker.rs:1081)。我实测跑过一条临时用例(已删除):

expected { status: 'err', errorCode: 'unknown' } to deeply equal { errorCode: 'interrupted' }

也就是说 get_delegation_status 对中断任务显示的是 "unknown task"——恰好是 schema 里定义为"这个 id 不认识"的那句话。只有历史路径能 work,因为 build_ledger_delegation_meta 把 status 改写成了 failed

新加的那条测试直接渲染 <StatusBadge status="err" errorCode="interrupted" />,绕过了 deriveBadge结构上不可能发现这个 bug

建议case "unknown": return { status: "err", errorCode: report.errorCode ?? "unknown" };断言点从展示组件挪到 parseStatusReportderiveBadge

F4 · 严格续作的门槛远超它要保护的不变量,且失败是永久的、错标的

manager.rs:4301config_fingerprint相等门。但 fingerprint_configcommands/acp.rs:9966)hash 的是 agent 的整个原生配置文件~/.codex/config.toml~/.grok/config.toml~/.cursor/cli-config.json…)加上全部非易失 runtime env。它在上游的用途是"需要重启才生效"这种建议性徽标。

拿它做硬门意味着:此后任何一次 agent 设置改动——加一个 MCP server、换默认模型、轮换 key——都会永久禁用改动之前所有任务的续作。而续作本来就是起新进程,天然会吃到新配置。

wait_for_strict_resume_readymanager.rs:2745)同样绝对:要求每一个记录在案的 config option 恢复到原值,而 preferred_config_values 是从子连接全量有效选项快照的(manager.rs:4425),不只是调用方指定的那几个。模型列表里删掉一个模型,这条任务的续作就死了。

两种失败都以 spawn_failed + 散文消息出现,没有 continuation 专属错误码。

建议:只对真正构成身份的东西设门(agent / session id / cwd);mode 与 config 降级为 best-effort + 可区分错误码;去掉整份配置 fingerprint 的相等判断。


三、建议修但不阻塞(11 条,展开)
# 严重度 位置 问题 修法
F5 Medium broker.rs:3836 / 4104 / 4175 / 4289 .unwrap_or(ScopedLookup::Hidden)Ok(Hidden) | Err(_) => unknown_report,把瞬时 DB 错误(SQLITE_BUSY 在多写者下很现实)与"不是你的任务"合并成同一个 unknown。父 agent 会被告知一个活着的子任务"不存在",进而重新委托 增加 Unavailable 分支,返回可重试错误
F6 Medium manager.rs:1634conversation_service.rs:168 send_prompt_linkeddelegation.is_none() 时无条件 clear_delegation_call_id——即每次普通用户 prompt 都在热路径上打一条 UPDATE。对账本之前的旧委托子会话,用户在里面发第一条消息就永久切断 get_by_delegation_call_id,父会话查旧 task 从真实状态退化成 unknown。而"人工打开子会话续聊"正是本 PR 主推的流程 把不可变的历史归属与可变的当前执行指针拆成两列
F7 Medium manager.rs:4470/4481broker.rs:2784 admit_continuationConflictDbError::Conflict 都被压成 SpawnerError::SendSpawnFailed,丢掉了 types.rs:300 已定义的 continuation_conflict。前置检查能挡大部分,剩下的正是竞态——恰恰是模型最需要正确错误码的时候 DelegationDispatch 加 typed conflict 分支
F8 Medium conversations.rs:1389 / 1619 / 1704 每次打开会话以及每次向上翻页都调 list_for_parent,而它对每行再发最多 4 条 SELECT 做鉴权。批量状态查询(broker.rs:3830)同样是串行 5N join 一次查完;翻页只取当前页需要的 task id
F9 Medium conversations.rs:1017 build_ledger_delegation_meta完整 entry.task 塞进 task_preview,而实时路径截到 TASK_PREVIEW_CAP(2 KiB),旧历史路径用的是短标题 同一套 UTF-8 安全的 2 KiB 截断
F10 Medium broker.rs:3374 / 1548manager.rs:4502 三条 durability 缺口:① finish() 失败只 log 不重试,随后照样断连——重启后行还是 running,明明已知的结果被丢掉;② mark_released() 失败无重试且没有后续事件会再进这两个 writer;③ 复合:prompt 发送失败 + 补偿 finish() 也失败 → 该行既无终态也无释放 进可重试队列,或下次读账本时惰性对账
F11 Medium manager.rs:4404 初次委托在 binding 校验和 admission 之前create_with_delegation 建好了子会话行。之后任一步失败都留下孤儿行且无清理路径。改动前建行紧挨着入队,失败面小得多 失败回滚,或推迟到 admission 成功后再建
F12 Medium connection.rs:9326 biased;read_update 优先于 prompt 完成、terminal 轮询和命令通道connection.rs:9541 / 9758)。一个持续有 update 的 agent 可以无限期推迟权限响应、Cancel、Disconnect——对所有 agent 生效 给 update 排空设预算,或周期性让出控制分支
F13 Medium manager.rs:4275 → 4356folder_service.rs:214 spawn_for_delegation 对子进程 cwd 做 canonicalize,而规范化后的字符串又被拿去 ensure_folder_for_path(精确字符串比较,没匹配就插新行)。macOS 上 /tmp/x/private/tmp/x会给同一个逻辑目录多建一条 folder 行 规范化只用于进程/会话绑定,folder 身份沿用既有解析
F14 Low continuation_protocol_tests.rs:45 测试新引入了对 python3 的硬依赖。本仓其余假进程测试都是 /bin/sh + #[cfg(unix)]。这些测试在 lib 里,会在 Windows server CI cell 和贡献者机器上跑 换成小的 Rust helper bin(CARGO_BIN_EXE_…),顺带消除 Python 里手抄一份 ACP schema 的重复
F15 Low lifecycle.rs:287-306 CAS 失败时不再发 ConversationStatusChanged。后果是所有会话:先被取消、随后又正常完成的会话会永远停在"已取消"且不发更新 至少在 PR 描述里点出这是全局行为变更

Codex 复核推翻的 3 条(我原本列了、经查不成立,写在这里免得返工白做):admit_continuation 的事务回滚是对的(SeaORM 1.1.19 DatabaseTransaction::Drop 会排队 rollback);ReleaseState 在健康路径上的两事件定序是对的(同一把锁下记录、后到者写库);SpawnDedupKey 去掉 cwd 本身是合理收紧——只是 manager.rs:800 的注释已经过期,顺手更一下就好。


四、方案层面的一点想法:账本记事实,别记许可

我去查了三家原生子智能体的现状,结论其实是支持这个 PR 的:

原生多轮 机制
Codex CLI ✅ 最完整 spawn_agent / send_message / wait_agent / list_agents / close_agent,v2 起 /root/<name> 路径寻址
Claude Code ✅ 补回来的 SendMessage(to=<agent id>) 自动 resume,transcript 存 subagents/agent-{id}.jsonl
OpenCode task 一次性;持久会话 PR 挂了 60 天被 bot 自动关掉

而且 Anthropic 为了把这个功能做对,连着发了七个补丁版本——其中三个恰好就是这个 PR 正在解的问题

Claude Code 版本 修的是什么 对应本 PR
v2.1.199 SendMessage 校验"名字还是不是原来那个 agent",被顶替就拒发 「每个来源单后继 / 冲突」
v2.1.205 resume 后任务列表显示 running(之前还挂着上一轮的 completed) 状态投影 project_ledger_report
v2.1.211 per-invocation 的 model 跨 resume 保留(之前 resume 会丢) ResumeBinding.preferred_config_values

这说明需求是真的,也说明这块地雷密度很高——所以上面要对账、要收窄门槛,不是挑刺。

不过 Claude Code 的做法有一点值得借鉴:它没有引入第二个真相源。子智能体 transcript 就是磁盘上的文件,"文件在 = 能 resume";"能不能续"是个现场可计算的问题(有没有被 stop、名字有没有被顶替),不是一个存在数据库里、必须靠内存握手恰好写对一次的 released 位。

本 PR 选了"新 task_id + 冻结旧结果"(更可审计,我认为方向比 Claude Code 更对,也正是 #603 要的),但代价是把"能不能续"变成了存储状态,于是任何一次崩溃都会永久锁死——也就是 F2。

所以核心建议就一句:账本继续记不可变事实(task_id → 子会话 + 任务文本 + 冻结报告),但"现在能不能续"改成现场计算(这个 external session 当下有没有进程挂着)。 这一步同时消掉 F1(不必再把释放屏障套到所有人类会话)和 F2(不必再有一个会丢的 released 位),PR 体量大概能掉一半。


五、委派提示词与组合场景:建议补一轮成功率验证 ⚠️

这部分我想单独提,因为它不在 diff 的正确性范围内,但回归风险最高tool_schema.json 是提示词,它的改动效果是统计性的,一次手工跑通不能说明问题。

5.1 一条具体的回归风险:continue_from_task_id: null 会硬失败

listener.rs:808-819

let continue_from_task_id = match req.input.get("continue_from_task_id") {
    None => None,
    Some(Value::String(v)) if !v.trim().is_empty() => Some(v.trim().to_string()),
    Some(_) => return report_failed("continuation_invalid", "…must be a non-empty string"),
};

Value::Null 落到 Some(_)整条委托硬失败。而紧挨着它的既有可选参数 working_dirlistener.rs:800-804)用的是 .and_then(|v| v.as_str())null 被静默当作缺省

这两个可选参数在同一个函数里,行为却相反。问题在于:把一个可选属性加进 schema,本身就制造了模型填 null 的可能——不少模型和 MCP 客户端 shim 会给列出的可选参数补 null。一旦发生,原本正常工作的一次性委托会直接失败,而不是退化成"没传"。

PR 自己的测试 invalid_continuation_id_is_rejected_before_spawn 里就断言了 Value::Null 必须失败,说明这是有意的——但我建议重新权衡:拒绝 null 换来的收益(防手滑)远小于它的回归面。

建议null 和空/纯空白字符串都按"缺省"处理,只对非字符串非 null 类型(数字、对象、数组)报 continuation_invalid。两行改动,消掉一整类跨模型回归。

5.2 描述里被删掉的那句话,是唯一一条"何时该委托"的指导

我做了句子级 diff:

- Hand off a self-contained sub-task to a separate local AI agent that runs in its own session
+ Hand off a task to a separate local AI agent
- The sub-agent CANNOT see this conversation, your open files, or earlier turns — it starts cold, so `task` must carry everything it needs
- Best for independent, parallelizable work you can describe up front; not for steps that need your ongoing back-and-forth
+ Without continue_from_task_id the sub-agent starts cold and cannot see this conversation…
+ With continue_from_task_id it strictly restores that task's child agent session…

Best for independent, parallelizable work…; not for steps that need your ongoing back-and-forth 被整句删掉且没有替代。 它是整个描述里唯一一条关于"什么时候该不该委托"的指导。删掉之后,模型判断"要不要开子智能体"的依据少了一条,委托率和适当性都可能漂移——而这条影响的是已经在正常工作的一次性流程

顺带:开头 sub-tasktaskthat runs in its own session 也被删了,但 agent_type 参数描述里仍写着 "Which local agent runs the sub-task",措辞已经不自洽了。

这段文字是有先例证明它承重的:b87b0f99("feat(delegation): recognize @agent mentions as explicit delegate_to_agent requests")只改了 tool_schema.json,2 insertions / 2 deletions——@AgentName 触发委托这个功能就是靠调措辞实现的。

建议:把"何时适合委托"那句以适配多轮的形式加回去,例如 "Best for work you can hand off and collect asynchronously — either self-contained in one round, or as successive rounds on the same child via continue_from_task_id.";同时把 agent_typesub-task 一并对齐。

5.3 建议的验证矩阵

因为提示词效果是统计性的,建议每格跑 N≥5 次记成功率,而不是单次手工确认。同时提醒一句:tool_schema.jsoninclude_str! 进去的,改完必须重新编译并重启 app,否则容易以为测过了其实没有。

必测场景(前 4 条是回归基线,验的是"没弄坏原来能用的"):

# 场景 关注点
1 普通一次性委托 委托率、task 是否仍然自包含、卡片是否仍然绑定到 tool_call
2 @AgentName / codeg://agent/<type> 提及触发 是否仍然稳定触发(b87b0f99 的既有能力)
3 并行 fan-out 2–3 个委托 DelegationMatchKey 新增了 continue_from_task_id 字段,关联是否仍然正确
4 模型给可选参数填 null 见 5.1,当前会硬失败
5 续作 happy path 是否真的 resume 而非冷启动
6 过期的来源 id 续作 → continuation_conflict 模型能否按提示改用最新 id 恢复,还是卡住
7 continuation_busy 模型会不会无限重试(见 F2)
8 对账本任务调用 resume_delegationnot_resumable + 散文提示 模型能否正确改走 continue_from_task_id
9 Codex code-mode 里同一脚本混合"新建 + 续作" 本 PR 新增的解析路径

必测宿主:PR 描述里已说明真实供应商验收仅限 Codex ACP 1.10.0 / GPT-5.6 Sol。但这份 schema 会注入 15 个内置 agent 里的 14 个(只有 OpenClaw 因不吃 MCP 排除),而各家的工具调用形状差别很大——Codex 走 code-mode 脚本(tools.mcp__codeg_mcp__delegate_to_agent({...}))、Claude Code 走标准 MCP、Cursor 的通告没有身份、CodeBuddy 要延迟解析工具名。同一段提示词在这些宿主上的落地效果不一样。

建议至少补 Claude Code + Gemini + Cursor/Cline 之一 这三档,覆盖三种不同的工具调用形状。如果人力有限,我认为优先级是:场景 1/2/4 × 宿主 Claude Code(回归面最大),其余可以留到后续 PR。


六、小结

  • 功能方向:赞成。 15 个内置 agent 里 14 个能当委托父,其中只有 2 个有原生多轮;跨厂商链路(Claude 主 → Codex 子审查 → 返工 → 复验)codeg 是唯一承载点,而这恰恰最需要多轮。
  • 优先级:不是 P0。 存在可用 workaround(子智能体写文件 / 人工进子会话续聊),代价是 token 和时延,不是功能缺失——所以不值得为它接受"关掉会话再打开会报错"这种回归
  • 可以先单独合的:vendor ChildGuard 修复、codex code-mode 语义化 MCP 卡片、F3 那个前端一行修复。这三块都独立成立,不该被压在这个 PR 底下等。

辛苦了,这个 PR 的工作量和自我审视的诚实度都很高 👍 上面任何一条如果是我读错了,欢迎直接指出来。

@suoweikeji-liqiang

Copy link
Copy Markdown

已按本轮 4 个 blocker 完成返工,并把修复推到 106a52af。这次只动 F1–F4,没有顺手扩到 F5–F15。

F1 — 释放屏障只属于 broker 委托连接

  • DelegationReleaseBarrierdisconnect()disconnect_by_owner_window() 都已收紧为仅看不可变的 delegation_task_id;普通 resume 出来的用户会话恢复原有优雅 teardown,不再先 cancel driver,也会重新进入 draining
  • 严格续作在启动同一 external session 前会检查 matching drainer,给旧进程一个有界退出窗口,随后复用现有的进程树 kill + reap 确认;确认不了就返回 continuation_busy,不会启动第二个写入者。
  • 特别补了 pid == 0 的竞态:0 既可能是已 reap,也可能是 on_spawn 尚未发布。drainer 现在记录进入时是否发布过 pid;从未发布且等待后仍为 0 时拒绝本次续作,等重试时再确认,不把“未知”当“已结束”。

F2 — 启动时修复上个进程遗留的账本

  • desktop 和 server 都会在 delegation listener 接受请求前执行一次启动对账。
  • 所有 released = false 的旧行会置为 released;其中仍为 running 的行冻结成终态 unknown + error_code: interrupted,不虚构成功或失败。
  • 对账是事务性的、幂等的;磁盘库测试真实 close/reopen 后验证:running 行得到 interrupted 快照,已完成但未 release 的行保留原结果,两者都解除续作锁,第二次对账为 0。

F3 — 实时状态保留 interrupted

  • deriveBadge 的 unknown 分支改为 report.errorCode ?? "unknown"
  • 测试改走 parseStatusReport -> deriveBadge,不再绕过真实路径直接测展示组件。

F4 — 只把身份当硬门

  • 仍严格校验 agent、external session id、cwd,并且 strict recovery 仍绝不 fallback 到 session/new
  • 删除整份原生配置 fingerprint 相等门。mode/config 仍会在 prompt 前尝试恢复,但失败或漂移只记录告警,不永久禁用续作。
  • 回归测试明确覆盖“身份一致,但旧 mode/config 已不存在或已变化”仍可续作。

本地验证:前端 76 项;账本 11 项;严格恢复协议 3 项;ConnectionManager 125 项;broker 137 项;desktop tests 编译和 server-only 编译均通过。修正后又做了一轮独立复核,当前 F1–F4 无剩余 blocker。

另外,两个与本功能可独立合并的问题已经拆出:#701(ChildGuard)和 #702(Codex semantic MCP cards),两边 CI 都是 7/7 通过;目前尚待 maintainer 合并。合并后我会把 main 合回本分支,届时 #693 的 diff 会自然缩小。

关于多轮委托的价值

我同意:这个价值不能成为接受 F1–F4 回归的理由,所以本轮先把执行边界补齐。但我不同意把文件交接或手动进入子会话视为等价替代:

  • 文件交接只保留显式写下来的结果,丢失子 agent 的活上下文、失败过的假设、工具状态和局部决策;下一轮必须重新序列化、重新理解。
  • 手动进入子会话会切断父 agent 的编排、机器可读 task lineage 和异步收集;它把 agent workflow 退化成人工搬运。
  • review -> fix -> retest 这类循环依赖同一个 child 对代码与判断持续负责。每轮换冷会话,不只是多一次点击,而是重复 rehydration,成本、延迟和误差会随任务规模与轮数累积。
  • 只支持 one-shot 还会反向鼓励一次塞进超长 prompt,降低可验证性;允许拿最新 task_id 继续,才能把任务拆成短、可检查的业务轮次。

所以我认为这里解决的是跨 provider 编排中的结构性缺口,不只是便利性优化;尤其 Codeg 能把不同 agent 串成同一条 review/fix/retest 链,这是文件或手工跳转保不住的能力。与此同时,合并门槛仍应由正确性和回归风险决定,这也是这轮按四个 blocker 逐项补测试、做独立复核的原因。

…ntinuation-local

# Conflicts:
#	src-tauri/src/parsers/codex.rs
#	src-tauri/vendor/sacp-tokio/src/acp_agent.rs
@asteroida123

asteroida123 commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

补充处理了 reviewer 列出的“小而确定”项,已推到 27e0ea9c

  • F7DelegationDispatch 新增 typed conflict,admission 竞态不再退化成 spawn_failed;保留 continuation_conflict 和胜出的 successor task ID,并覆盖 losing child 的断连与 broker bookkeeping 清理。
  • F9:实时、resume 与历史账本统一使用 UTF-8 安全的 2 KiB task_preview 上限。
  • 5.1continue_from_task_id: null、空串和纯空白按未提供处理;只有非字符串、非 null 类型返回 continuation_invalid
  • 5.2:补回适配多轮后的“何时适合委托”指导,并统一 task / sub-task 措辞。
  • F15:按建议把全局行为边界明确写进 PR 正文:会话先进入 canceled 后,迟到的正常完成不会覆盖该状态,也不会再次发 ConversationStatusChanged。本轮不再扩大这条全局 CAS 语义。

其余 F5、F6、F8、F10–F14 我建议拆成后续独立 PR,不是因为不重要,而是它们已经不是“几行修正”的责任范围:

  • F5 需要先定义并贯通 DB unavailable / hidden / unknown 的 wire 契约,否则只改一个 unwrap_or 会造成调用点语义不一致。
  • F6 是不可变历史归属与可变执行指针的 schema / migration 设计,必须单独验证旧数据升级和人工续聊兼容。
  • F8 是查询形态调整(join、批量鉴权、分页只取所需 task),应带查询数与分页回归,而不是在本功能 PR 里局部消 N+1。
  • F10–F11 涉及 durable retry / 补偿事务 / admission 与建行次序;它们会改变故障恢复模型,适合单独做可注入失败测试。
  • F12 是所有 agent 共用的事件循环公平性,必须验证 wire 顺序、Cancel/Disconnect 延迟和 update flood,回归面远超 continuation。
  • F13 要先统一“进程 cwd”和“folder 身份”的规范化契约,避免修重复行时反向破坏 symlink / 路径身份。
  • F14 会引入跨平台测试 helper 与测试装配变化,应在 Windows CI 上独立证明,避免把测试基础设施重构绑进功能 PR。

这样拆分能让每类改动有清晰的验收条件、独立 CI 与可回滚边界,也避免 #693 在 blocker 已关闭后继续膨胀。拆分不代表搁置;建议按 F10/F11 durability → F6 ownership migration → F8 query scaling → F12/F13/F14 infrastructure 分批推进,F5 可在先确定错误契约后单独落地。

本地验证:受影响的 5 个定向回归通过;continuation protocol 3/3;desktop test target 编译通过;server-only 编译通过。大范围 delegation 测试在受限沙箱中 263 项通过,8 项仅因 Unix socket / 进程通信被系统拒绝而失败,与本次改动无关;最终以最新 CI 为准。

Claude Code / Antigravity 作为父 Agent 的实机证据

另外补充此前已完成的真实父宿主验收。为避免把旧安装包误算成 PR 证据,测试时先核验实际进程来自本 PR worktree 的 target/debug/codeg,再建立全新的父/子链路:

  • Claude Code 父 Agent → Codex 子 Agent:首轮任务 71d9eaba、续作 13a11a1e、应用真实重启后的续作 f73ffb14,三轮都落在同一个 child conversation 16,并正确保留和回忆 MEMORY_VECTOR_519
  • Google Antigravity 父 Agent → Codex 子 Agent:首轮任务 0958e442、续作 0c103c08、同一次真实重启后的续作 2e212c03,三轮都落在同一个 child conversation 15,并正确保留和回忆 MEMORY_HELIX_604

这两条链证明的不是“把 Claude / Antigravity 当子 Agent 调用”,而是它们各自作为父 Agent,通过注入的 MCP 工具完成异步委托、按新 task ID 续作同一子会话,并跨应用重启恢复。此前误连已安装 codeg.app 的那批样本已作废,没有计入上述结果。

边界也明确说明:这组实机验收运行在返工 head 106a52af;随后合入 upstream/main 并追加本轮 F7/F9/5.1/5.2 小修得到当前 27e0ea9c。本轮已做定向回归和 CI,但没有把此前单次实机链路包装成当前 head 的 N≥5 提示词统计矩阵。

最新 CI

最新 CI run 345506851226/7 通过:Frontend、Linux/macOS/Windows desktop、Linux/macOS server 均通过。唯一失败是 Windows server 的上游既有测试 parsers::pi::tests::settings_json_session_dir_is_honored:夹具把 /srv/pi-sessions 当作绝对路径,但 Windows 将其判为非绝对路径并回退到临时 agent/sessions。upstream/main 自身的 run 也在同一测试失败;#693 未修改 Pi parser,因此不把这项无关修复继续塞入本 PR。PR 当前仍是 MERGEABLE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants