1483 字
7 分钟
打断后台 Subagent 后,如何找回它的输出 —— 开发 recovering-subagent-output

一句话核心#

在使用 OpenCode 派发后台 subagent 时有个经典痛点:如果我打断了后台任务,前台的 primary agent 就看不到 subagent 的输出。我在排查这个问题的过程中发现了一个反直觉的事实——subagent 的输出从来没有丢过,它一直在 OpenCode 的 SQLite 数据库里,只是前台的 agent 没去查

于是我把这个发现做成了一键恢复的技能:

7emotions
/
recovering-subagent-output
Waiting for api.github.com...
00K
0K
0K
Waiting...

背景:后台任务协议#

OpenCode(以及基于它的 OhMyOpenCode)的后台任务有一个标准协议:

launch bg_xxx → 结束回合 → 等 <system-reminder> → background_output(bg_xxx) 取回结果

这个协议在不被干扰时工作得很好。但一旦打断,链路就断了:

  1. 回合被 abort——新回合的上下文里没有上一回合派发的 bg_ 任务 ID;
  2. 通知协议断裂——primary agent 不知道”有结果待取”;
  3. 即使拿到 ID 调 background_output,被取消的任务也只返回元数据(状态表 + 原始 prompt),没有内容。

结果就是:后台 subagent 明明跑出了东西(甚至在你打断前已经产出了一大段),前台却表现为”看不见”。

调查:输出到底存哪了#

直觉告诉我:UI 上能看到 subagent 的输出,说明它一定被持久化了。顺着这个思路,我在 ~/.local/share/opencode/ 里找到了主存储 opencode.db(一个 11GB 的 SQLite)。

数据结构非常清晰:

作用
session所有会话;parent_id 列把 subagent 挂到主会话下
message每个会话的消息
part每条消息的每个部分:text / reasoning / tool / step-start / step-finish

关键在 part 表:工具调用(type=tool)的完整输出存在 state.output 里,即使命令被用户中止,中止元数据也在。我实测了一个被打断的 bash 调用:

{
  "type": "tool",
  "tool": "bash",
  "state": {
    "status": "completed",
    "input": { "command": "sleep 30" },
    "output": "(no output)\n\n<shell_metadata>\nUser aborted the command\n</shell_metadata>"
  }
}

所以真相是:打断不丢数据,一切都在库里。真正的问题是前台 agent 不知道去哪查、用什么 ID 查。

顺带挖出的 opencode task 机制#

在开发这个技能的过程中,我读了 OpenCode 1.18.25 的源码(packages/opencode/src/tool/task.ts),发现两种派发模式有完全不同的打断语义——这也解释了之前观察到的所有”矛盾”。

前台 vs 后台:两种模式#

前台 run_in_background=false后台 run_in_background=true
行为阻塞等待 subagent 完成,返回最终结果立即返回,完成后 system-reminder 通知
打断前台时 subagent被取消继续运行
取消机制ctx.abort → ops.cancel(subagent)无 abort 连线

前台模式的核心源码——这就是”打断前台也会打断 subagent”的原因:

// 前台等待期间,前台 abort 信号直接取消 subagent
function onAbort() {
  runCancel.fork(cancel)
}
// 释放阶段:若前台被中断,显式取消任务
if (Exit.hasInterrupts(exit))
  yield* Effect.all([cancel, background.cancel(nextSession.id)])

而后台模式没有这套连线——subagent 是解耦的独立任务(BackgroundJob),前台中断与它无关,它会一直跑到完成。要真正停掉后台任务,只能走 UI 的取消按钮或 background.cancel

注:bg_ 任务 ID 和 background_output / background_cancel 工具是 oh-my-openagent 插件层在 OpenCode 原生 BackgroundJob(job ID 即 session ID)之上的包装。后台模式需要 OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true

实测对照#

我做了四组对照实验验证这套语义:

派发模式前台中断后最终结果
后台 · tick 循环(180 tick)无人打断跑完 180 tick(3m3s)
后台 · 长作文(8000 字)打断前台继续跑完 10,860 字
后台 · tick 循环(300 tick)打断前台继续跑完 300 tick(5m4s)
前台 · 长作文(8000 字)打断前台立即停住,断在句子中间,部分输出完整可读

最有力的一次是前台模式的长作文测试:作文断在”人类对秩序的需要植根于存在”——一个未写完的句子,证明打断确实在写作中途掐断了它,而前面的约 1600 字全部可以通过技能完整读回。

解决方案:恢复技能#

理解了存储结构,解决方案就很简单了:不需要预先记住任务 ID——subagent 会话通过 parent_id 天然挂在主会话下,打断后按 parent_id 一查就能枚举出自己派发的全部 subagent。

-- 1. 枚举我的所有 subagent
SELECT id, title FROM session WHERE parent_id = '<我的会话ID>';

-- 2. 读某个 subagent 的完整输出(含工具调用)
SELECT data FROM part WHERE session_id = '<subagent ID>' ORDER BY time_created;

技能封装了四个命令:

S=~/.agents/skills/recovering-subagent-output/scripts/recover-subagent.py

python3 $S list                          # 当前会话的全部 subagent + 状态标记
python3 $S read <session_id>             # 完整 transcript,含每次工具调用的 stdout/stderr
python3 $S search <关键>               # 按标题找会话
python3 $S --session <parent_id> list    # 指定父会话

状态检测逻辑:工具输出含 User abortedaborted(被中止);会话无 step-finish 收尾 → cut-off(中途截止);正常结束 → completed

使用场景#

  1. 打断后台任务后捞回部分输出——前台 agent 在全新回合(没有 bg_ ID)里,用 list 枚举 → read 拉回打断前的全部内容;
  2. 审计 subagent 干了什么——任何 subagent 会话的完整工具调用记录(含命令和输出)都在库里,可离线排查;
  3. 多任务协调——派发多个后台任务后按 parent_id 统一枚举、逐个取回,不依赖完成通知;
  4. 调试 OpenCode 自身行为——part 表里存了 reasoning、step 生命周期,可用于理解 Agent 的决策过程。

小结#

这个技能的开发过程本身也是一次”Agent 调试 Agent”的实践:问题出在 agent 的上下文协议上,答案藏在 agent 自己的数据存储里,最终用一把 SQLite 查询把它变成了一键恢复的能力。如果你也遇到过”后台任务输出不见了”的困惑,希望这个技能和这份 task 机制分析能帮到你。

仓库地址:7emotions/recovering-subagent-output,MIT 协议,欢迎使用和提 issue。

打断后台 Subagent 后,如何找回它的输出 —— 开发 recovering-subagent-output
https://lorenzofeng.top/posts/recovering-subagent-output/
作者
Lorenzo Feng
发布于
2026-08-28
许可协议
CC BY-NC-SA 4.0