feat: 任务完成送回闭环(issue #56) #60

Merged
Yushu merged 2 commits from feature/56-task-completion-loop into main 2026-07-24 08:16:02 +00:00
Member

Closes #56

这个 PR 做了什么

实现 issue #56「任务完成送回闭环:Callback 落库 + 取消竞态 + 崩溃语义 + 端到端验收」——打通深挖任务委托的完整用户可见路径。这是 Phase 5(issue #52 PRD)最后一片:子智能体(#54)产出结果 → 写入 Callback(#53 的存储切片)→ 既有到期扫描管线无门控送回,语气经模型平时的人设正常组织。完成后 #52 描述的端到端用户故事(问深挖问题→应声→干等不阻塞对话→之后用平时语气收到分析结论)第一次可以被真实观察到。

Callback 写入(runtime_loop.py::_run_delegated_task

  • 子智能体正常完成后写入一条 trigger_source="task_completion" 的 Callback(fire_at 留空即"立即到期",交由 issue #53 既有的 _callback_scan 送回,不新建任何送回机制)
  • cancel_delegate_task 真中断时不写——CancelledError 会在 finally 跑完登记表清理后继续向外传播,不会执行到写 Callback 的代码(ADR-0020"取消是真中断,不是等跑完再丢结果")
  • 任务真失败时(DelegatedTaskResult.failed=True,issue #54 review 加的 fail-closed 分支)仍写一条固定失败提示——与用户核实的设计决定:已经应了一声"我看看啊",履约语义要求给一个结果而不是永远沉默

取消竞态窗口(_handle_cancel_delegate_task

  • 补上"任务已完成、Callback 已写入、但还没被下一次到期扫描送回"这个窄窗口——登记表里找不到任务时,改为撤销该 chat 上 task_completion 来源的待送回记录,不连带撤掉同 chat 其它来源(如未来 register_callback)的 Callback
  • 已知残余竞态(文档里如实记录):list_callbacks/delete_callback 之间没有加锁,理论上可能与 _callback_scan 的送回 tick 交织;后果是措辞尴尬(模型报告"已取消"但消息已送达)而非数据错误(不重复送达、不崩溃),暂不引入跨请求锁

测试

  • Callback 写入:正常完成 / 失败兜底 / 取消不写(取消测试让任务真的跑进一次工具调用再取消,不是取消一个从未执行过的任务)
  • 取消竞态窗口:含"不误删其它来源记录"
  • 崩溃恢复语义:验证"任务登记表不持久化、但 Callback 记录持久化"这一不对称设计——模拟重启后 Callback 记录原样还在
  • 私聊/群聊完整端到端:子智能体产出 → Callback 落库 → 真实 _callback_scan 无门控送回 → 验证正确路由到发起请求的那个 chat

全部 887 个测试通过,ruff check/ruff format 干净。

Code review

跑过 /code-review(Standards + Spec 两轴并行子智能体):

  • Spec 轴:核心路径无越界(register_callback 等 Out of Scope 条目均未触碰),发现两处测试薄弱点
  • Standards 轴:验证了 CancelledError 会跳过 try/finally 之后代码这一关键论断(手动 repro 确认),发现窄竞态窗口 + 两处测试没有真正验证其声称的路径 + 字面量重复
  • 已修复:抽出 TASK_COMPLETION_TRIGGER_SOURCE 共享常量;竞态窗口文档如实说明残余风险(不过度声称"完全关闭");重写取消测试让任务真正进入工具调用后再取消;重写崩溃恢复测试验证"登记表不持久化/Callback 持久化"这一实际不变量(原测试只断言 recover() 不报错,接近重言式)

至此 Phase 5「深挖任务委托」(issue #52 PRD,#53-#56)全部实现完毕。

Closes #56 ## 这个 PR 做了什么 实现 issue #56「任务完成送回闭环:Callback 落库 + 取消竞态 + 崩溃语义 + 端到端验收」——打通深挖任务委托的完整用户可见路径。这是 Phase 5(issue #52 PRD)最后一片:子智能体(#54)产出结果 → 写入 Callback(#53 的存储切片)→ 既有到期扫描管线无门控送回,语气经模型平时的人设正常组织。完成后 #52 描述的端到端用户故事(问深挖问题→应声→干等不阻塞对话→之后用平时语气收到分析结论)第一次可以被真实观察到。 ### Callback 写入(`runtime_loop.py::_run_delegated_task`) - 子智能体正常完成后写入一条 `trigger_source="task_completion"` 的 Callback(`fire_at` 留空即"立即到期",交由 issue #53 既有的 `_callback_scan` 送回,不新建任何送回机制) - 被 `cancel_delegate_task` 真中断时不写——`CancelledError` 会在 `finally` 跑完登记表清理后继续向外传播,不会执行到写 Callback 的代码(ADR-0020"取消是真中断,不是等跑完再丢结果") - 任务真失败时(`DelegatedTaskResult.failed=True`,issue #54 review 加的 fail-closed 分支)仍写一条固定失败提示——与用户核实的设计决定:已经应了一声"我看看啊",履约语义要求给一个结果而不是永远沉默 ### 取消竞态窗口(`_handle_cancel_delegate_task`) - 补上"任务已完成、Callback 已写入、但还没被下一次到期扫描送回"这个窄窗口——登记表里找不到任务时,改为撤销该 chat 上 `task_completion` 来源的待送回记录,不连带撤掉同 chat 其它来源(如未来 `register_callback`)的 Callback - 已知残余竞态(文档里如实记录):`list_callbacks`/`delete_callback` 之间没有加锁,理论上可能与 `_callback_scan` 的送回 tick 交织;后果是措辞尴尬(模型报告"已取消"但消息已送达)而非数据错误(不重复送达、不崩溃),暂不引入跨请求锁 ### 测试 - Callback 写入:正常完成 / 失败兜底 / 取消不写(取消测试让任务真的跑进一次工具调用再取消,不是取消一个从未执行过的任务) - 取消竞态窗口:含"不误删其它来源记录" - 崩溃恢复语义:验证"任务登记表不持久化、但 Callback 记录持久化"这一不对称设计——模拟重启后 Callback 记录原样还在 - 私聊/群聊完整端到端:子智能体产出 → Callback 落库 → 真实 `_callback_scan` 无门控送回 → 验证正确路由到发起请求的那个 chat 全部 887 个测试通过,`ruff check`/`ruff format` 干净。 ### Code review 跑过 `/code-review`(Standards + Spec 两轴并行子智能体): - Spec 轴:核心路径无越界(`register_callback` 等 Out of Scope 条目均未触碰),发现两处测试薄弱点 - Standards 轴:验证了 `CancelledError` 会跳过 `try/finally` 之后代码这一关键论断(手动 repro 确认),发现窄竞态窗口 + 两处测试没有真正验证其声称的路径 + 字面量重复 - 已修复:抽出 `TASK_COMPLETION_TRIGGER_SOURCE` 共享常量;竞态窗口文档如实说明残余风险(不过度声称"完全关闭");重写取消测试让任务真正进入工具调用后再取消;重写崩溃恢复测试验证"登记表不持久化/Callback 持久化"这一实际不变量(原测试只断言 `recover()` 不报错,接近重言式) 至此 Phase 5「深挖任务委托」(issue #52 PRD,#53-#56)全部实现完毕。
实现 issue #56「任务完成送回闭环:Callback 落库 + 取消竞态 + 崩溃语义 +
端到端验收」——打通深挖任务委托的完整用户可见路径。这是 Phase 5(issue
#52 PRD)最后一片:子智能体产出结果 → 写入 Callback → 既有到期扫描管线
(issue #53)无门控送回,语气经模型平时的人设正常组织。

- runtime_loop.py::_run_delegated_task:子智能体正常完成后写入一条
  trigger_source="task_completion" 的 Callback(fire_at 留空即"立即到期");
  被 cancel_delegate_task 真中断时不写(CancelledError 在 finally 跑完
  登记表清理后继续向外传播,不会执行到写 Callback 的代码);任务真失败
  (result.failed=True)时仍写一条固定失败提示(与用户核实的设计决定:
  已经应了一声,不该让用户永远干等)
- _handle_cancel_delegate_task:补上"任务已完成、Callback 已写入、但还
  没被下一次到期扫描送回"这个竞态窗口——登记表里找不到任务时,改为撤销
  该 chat 上 trigger_source="task_completion" 的待送回记录,不连带撤掉
  同 chat 其它来源(如未来 register_callback)的 Callback

测试:Callback 写入(正常完成/失败兜底/取消不写)、取消竞态窗口(含
"不误删其它来源记录")、崩溃恢复语义(登记表本就不落盘,`recover()`
与它无关)、私聊/群聊完整端到端(子智能体产出 → Callback 落库 → 真实
_callback_scan 无门控送回 → 验证正确路由到发起请求的那个 chat)。

全部 887 个测试通过,`ruff check`/`ruff format` 干净。至此 Phase 5「深挖
任务委托」(issue #52 PRD,#53-#56)全部实现完毕。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- delegate_task.py/runtime_loop.py:新增 TASK_COMPLETION_TRIGGER_SOURCE
  常量,消除写入侧与竞态窗口撤销侧两处散落的 "task_completion" 字面量
- runtime_loop.py:_cancel_pending_task_completion_callback 补充文档,
  如实说明已知的窄竞态窗口(list_callbacks 与逐条 delete_callback 之间
  没有加锁,理论上可能与 _callback_scan 的送回 tick 交织,导致模型报告
  "已取消"但消息其实已经送达)——后果是措辞尴尬而非数据错误,暂不引入
  跨请求锁/事务;同时明确该兜底路径不校验 task_id 的原因(CallbackRecord
  本身不携带 task_id)
- 测试:test_a_cancelled_task_does_not_write_a_callback 原先在任务从未
  开始执行时就取消,没有真正验证"取消跳过收尾代码"这件事——改为让任务
  真的跑进一次工具调用(用 asyncio.Event 确认已进入)再取消;
  test_recover_is_unrelated_to_the_delegate_task_registry 原先只断言
  recover() 不报错(近乎重言式)——改为验证"任务登记表不持久化、但
  Callback 记录持久化"这一不对称设计:模拟重启后 Callback 记录原样还在

全部 887 个测试通过,`ruff check`/`ruff format` 干净。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit de56dde36d into main 2026-07-24 08:16:02 +00:00
Yushu deleted branch feature/56-task-completion-loop 2026-07-24 08:16:03 +00:00
Sign in to join this conversation.
No description provided.