问题概述
当 agent 通过 run_shell_bg 启动一个任何长时间运行的命令时,模型经常会输出一句安抚性的回复(例如“任务正在运行中,完成后我会告诉你结果”),随后 本轮 agent run 立即结束。
任务真正完成后,没有任何机制把结束事件递回到模型 / 会话,用户也再也收不到任何反馈。模型嘴上的“收到通知后再告诉你”是一句系统层面永远兑现不了的承诺。
复现操作(最小化、与具体语言/框架无关):
- 在任意会话里让模型执行一个 1–10 分钟级的 bg 命令,例如:
run_shell_bg(command: "ping -n 60 127.0.0.1")
run_shell_bg(command: "<任何执行时间超过 ~3s 的脚本>")
- 引导模型在调用工具后说一句类似“正在跑/稍后告诉你”的话。
- 观察:模型给出该回复后,会话回到 idle,永不返回最终结果,即使后台命令已经成功结束并产生了 exit code。
预期:本 session 在本轮启动的 bg 任务结束前,agent loop 必须保持值守;任务结束时模型必须被唤回,把 exit code + tail output 反馈给用户。
根因分析(三层契约不一致)
层 1 — 工具描述层:run_shell_bg 给模型的提示文案是错的
src/tools/bg-shell.js 在 status: 'running' 分支返回的 hint:
"the agent will be suspended and automatically woken when this job ends…
Simply end your turn; the system delivers the job result automatically."
模型读到后非常听话地说一句“稍后再告诉你”然后结束本轮——但后半句承诺由 agent loop 兑现,而 agent loop 的实际行为见层 2。
层 2 — Agent loop 层:BG_WAIT_SKIPPED_MODEL_DONE 主动放弃等待
src/chat/agent-loop.js 在“模型本轮无 tool 调用”的收尾路径里:
if (assistantText.trim()) {
const _hasPendingEvents = !!(run._pendingBgJobEvents && run._pendingBgJobEvents.length);
if (!_hasPendingEvents) {
Logger.info('BG_WAIT_SKIPPED_MODEL_DONE', { jobs: [...myBgJobs()] });
break; // ← 直接跳出 BG 等待循环,本轮 run 结束
}
}
只要模型说了任何一句收尾话,且“正巧刚结束”的事件队列为空,循环立即 break。
这个分支的最初设计动机是“不要被 dev server / watcher 这类永不结束的任务挂死”,但它对“本轮模型刚启动的新长任务”做了同样的提前退出处理,正是本 issue 的直接触发点。
层 3 — 事件投递层:监听者只活在“当前 run”里
_bgJobEndHandler 是 run-scoped 订阅(onBgJobEnded(_bgJobEndHandler)),在 run 结束时 offBgJobEnded(_bgJobEndHandler)。当层 2 把 run 结束后:
terminal-monitor 之后发出的 bg-job-end 没有任何订阅者;
run._pendingBgJobEvents 随 run 对象 GC,没有任何 session 级队列保留;
- 因此不存在任何机制能把最终结果重新注入会话。
附加加剧条件(任何长任务都可能命中其一)
- Shell integration 缺失:
src/tools/bg-shell.js 仅在 usedSI === true 时才 addActiveBgJob。SI 在 3s 窗口内没就绪(例如 cmd.exe、自定义 shell、SI 还未握手完成)时,连登记都不做,整个事件链从一开始就缺位。
- 父进程偏差:某些工具/脚本会派生子进程异步执行真实工作,父进程提前退出时
onDidEndTerminalShellExecution 触发的“结束”是假阳性。
- 4h stale 上限:
src/tools/terminal-monitor.js 的 cleanupStaleJobs 把超过 4h 仍 running 的 job 当 stale 删除并广播 exitCode: null。任何合法的多小时长任务(训练、批处理、大型构建等)都可能被误杀。
用户视角看到的完整时间线
- 模型调用
run_shell_bg(command: "<long running command>")
- 2.5s 早退窗口内没结束 → bg-shell 返回
status: 'running' + 文案承诺“会自动唤醒”
- 模型输出一句“任务正在运行中…完成后我会告诉你”
- agent-loop 命中
BG_WAIT_SKIPPED_MODEL_DONE → run 终止
- (任意时间后)命令真的跑完 →
bg-job-end 广播 → 零订阅者 → 事件蒸发
- 用户视角:那句“完成后通知你”永远不会被兑现
期望行为
- 只要本轮
run_shell_bg 启动的 job 仍在运行,agent loop 不允许结束本轮,无视模型是否输出了文本回复。
- bg 任务真实结束后,必须把
exit code + tail output 作为 <system-reminder> 注入模型,由模型对用户做出最终反馈。
- 工具描述中所有“automatically woken / end your turn”之类承诺必须与系统实现一致,避免诱导模型主动结束。
- SI 不可靠时,必须有兜底通道感知任务结束,而不是默默丢事件。
修复计划(分阶段,零 UI 改动)
阶段 1(必做,止血)
-
在 agent-loop 给每个 run 加一个集合 run._sessionStartedBgJobs: Set<jobId>,只收纳本 run 内通过 run_shell_bg 启动的 job(用 ctx 注入回调登记,不让 bg-shell 反向依赖 run 对象)。
-
改写 BG_WAIT_SKIPPED_MODEL_DONE 的判据为:
assistantText 非空 且 无 pending 事件 且 run._sessionStartedBgJobs 与 myBgJobs() 没有交集 → 才允许 break。
含义:dev server / 上一轮长任务保持原有体验;本轮新启动的长任务强制值守。
-
把 run_shell_bg 工具 hint 改成对系统行为如实描述,明确告知模型:
“你不能用‘说完再走’的方式提前结束 turn;loop 会保持你值守,结束时会以 <system-reminder> 把结果再次喂给你。”
-
BG_WAIT_SKIPPED_MODEL_DONE 日志附带 sessionStartedJobs,便于回归验证。
阶段 2(建议):堵住 SI 不可靠导致“值守也等不到结果”
run_shell_bg 即便 usedSI === false 也调用 addActiveBgJob,并打标 polled: true。
terminal-monitor 增加一个仅在 _activeBgJobs 非空时存在的轮询线程(~10s 间隔),对 polled: true 的任务用 findTerminalByName + getRecentExecutions(t, 1) 判定结束,合成 payload 触发回调,跑空即清除定时器。
- 把 4h stale 硬限抬高(或做成可配置 / 二次确认),避免合法长跑任务被误杀。
阶段 3(可选):跨重启鲁棒性
SessionStore 加一个 pendingBgEvents: Array<{...}> 字段,向后兼容(默认 [])。
terminal-monitor 在广播 bg-job-end 的同时按 sessionId 落盘到该字段。
- agent-loop 在 turn 开头注入
_pendingBgJobEvents 之前,把 session.pendingBgEvents splice 到前面再清空。复用现有 wasSyncReturned 去重。
验收用例
- U1(阶段 1):在 SI 可用的终端跑
run_shell_bg(command: "ping -n 30 127.0.0.1") 并让模型回一句话。
预期:日志不再出现 BG_WAIT_SKIPPED_MODEL_DONE(或仅在与 sessionStartedJobs 无交集时出现);~30s 后看到 BG_JOB_END_INJECTED 并由模型对用户做出最终回复。
- U2(阶段 2):在显式关闭 SI 的 cmd.exe 下跑
timeout /t 30,预期通过轮询通道在 ~30–40s 内识别结束并注入 <system-reminder>。
- U3(阶段 3):跑长任务 → 立即 reload window → 任务结束后下次给该 session 发任意消息时,第一个 LLM 调用前就带上完成事件。
不在本 issue 范围
- UI 改动(webview / banner)—— 按需求保留现状。
- 引入任何针对特定语言/框架(如 notebook、训练框架)的专用工具 —— 单独评估。
- dev server / watcher 类长任务的语义 —— 阶段 1.2 通过“本轮自启”窗口与之严格区分,其体验保持不变。
相关代码位置
src/tools/bg-shell.js
src/tools/terminal-monitor.js
src/chat/agent-loop.js
src/chat/session-store.js(仅阶段 3)
日志关键字(便于检索/回归)
BG_WAIT_SKIPPED_MODEL_DONE(修复后在长任务场景应消失)
BG_JOB_END_INJECTED(修复后必须出现)
BG_JOB_STALE_CLEANED(合法长任务不应触发)
BG_SHELL_START / BG_SHELL_EARLY_EXIT
问题概述
当 agent 通过
run_shell_bg启动一个任何长时间运行的命令时,模型经常会输出一句安抚性的回复(例如“任务正在运行中,完成后我会告诉你结果”),随后 本轮 agent run 立即结束。任务真正完成后,没有任何机制把结束事件递回到模型 / 会话,用户也再也收不到任何反馈。模型嘴上的“收到通知后再告诉你”是一句系统层面永远兑现不了的承诺。
复现操作(最小化、与具体语言/框架无关):
run_shell_bg(command: "ping -n 60 127.0.0.1")run_shell_bg(command: "<任何执行时间超过 ~3s 的脚本>")预期:本 session 在本轮启动的 bg 任务结束前,agent loop 必须保持值守;任务结束时模型必须被唤回,把
exit code + tail output反馈给用户。根因分析(三层契约不一致)
层 1 — 工具描述层:
run_shell_bg给模型的提示文案是错的src/tools/bg-shell.js在status: 'running'分支返回的 hint:模型读到后非常听话地说一句“稍后再告诉你”然后结束本轮——但后半句承诺由 agent loop 兑现,而 agent loop 的实际行为见层 2。
层 2 — Agent loop 层:
BG_WAIT_SKIPPED_MODEL_DONE主动放弃等待src/chat/agent-loop.js在“模型本轮无 tool 调用”的收尾路径里:只要模型说了任何一句收尾话,且“正巧刚结束”的事件队列为空,循环立即 break。
这个分支的最初设计动机是“不要被 dev server / watcher 这类永不结束的任务挂死”,但它对“本轮模型刚启动的新长任务”做了同样的提前退出处理,正是本 issue 的直接触发点。
层 3 — 事件投递层:监听者只活在“当前 run”里
_bgJobEndHandler是 run-scoped 订阅(onBgJobEnded(_bgJobEndHandler)),在 run 结束时offBgJobEnded(_bgJobEndHandler)。当层 2 把 run 结束后:terminal-monitor之后发出的bg-job-end没有任何订阅者;run._pendingBgJobEvents随 run 对象 GC,没有任何 session 级队列保留;附加加剧条件(任何长任务都可能命中其一)
src/tools/bg-shell.js仅在usedSI === true时才addActiveBgJob。SI 在 3s 窗口内没就绪(例如 cmd.exe、自定义 shell、SI 还未握手完成)时,连登记都不做,整个事件链从一开始就缺位。onDidEndTerminalShellExecution触发的“结束”是假阳性。src/tools/terminal-monitor.js的cleanupStaleJobs把超过 4h 仍 running 的 job 当 stale 删除并广播exitCode: null。任何合法的多小时长任务(训练、批处理、大型构建等)都可能被误杀。用户视角看到的完整时间线
run_shell_bg(command: "<long running command>")status: 'running'+ 文案承诺“会自动唤醒”BG_WAIT_SKIPPED_MODEL_DONE→ run 终止bg-job-end广播 → 零订阅者 → 事件蒸发期望行为
run_shell_bg启动的 job 仍在运行,agent loop 不允许结束本轮,无视模型是否输出了文本回复。exit code + tail output作为<system-reminder>注入模型,由模型对用户做出最终反馈。修复计划(分阶段,零 UI 改动)
阶段 1(必做,止血)
在 agent-loop 给每个 run 加一个集合
run._sessionStartedBgJobs: Set<jobId>,只收纳本 run 内通过run_shell_bg启动的 job(用 ctx 注入回调登记,不让 bg-shell 反向依赖 run 对象)。改写
BG_WAIT_SKIPPED_MODEL_DONE的判据为:含义:dev server / 上一轮长任务保持原有体验;本轮新启动的长任务强制值守。
把
run_shell_bg工具 hint 改成对系统行为如实描述,明确告知模型:BG_WAIT_SKIPPED_MODEL_DONE日志附带sessionStartedJobs,便于回归验证。阶段 2(建议):堵住 SI 不可靠导致“值守也等不到结果”
run_shell_bg即便usedSI === false也调用addActiveBgJob,并打标polled: true。terminal-monitor增加一个仅在_activeBgJobs非空时存在的轮询线程(~10s 间隔),对polled: true的任务用findTerminalByName+getRecentExecutions(t, 1)判定结束,合成 payload 触发回调,跑空即清除定时器。阶段 3(可选):跨重启鲁棒性
SessionStore加一个pendingBgEvents: Array<{...}>字段,向后兼容(默认[])。terminal-monitor在广播bg-job-end的同时按 sessionId 落盘到该字段。_pendingBgJobEvents之前,把session.pendingBgEventssplice 到前面再清空。复用现有wasSyncReturned去重。验收用例
run_shell_bg(command: "ping -n 30 127.0.0.1")并让模型回一句话。预期:日志不再出现
BG_WAIT_SKIPPED_MODEL_DONE(或仅在与sessionStartedJobs无交集时出现);~30s 后看到BG_JOB_END_INJECTED并由模型对用户做出最终回复。timeout /t 30,预期通过轮询通道在 ~30–40s 内识别结束并注入<system-reminder>。不在本 issue 范围
相关代码位置
src/tools/bg-shell.jssrc/tools/terminal-monitor.jssrc/chat/agent-loop.jssrc/chat/session-store.js(仅阶段 3)日志关键字(便于检索/回归)
BG_WAIT_SKIPPED_MODEL_DONE(修复后在长任务场景应消失)BG_JOB_END_INJECTED(修复后必须出现)BG_JOB_STALE_CLEANED(合法长任务不应触发)BG_SHELL_START/BG_SHELL_EARLY_EXIT