Skip to content

run_shell_bg: agent 在长任务启动后过早结束本轮,永远不返回最终结果 #167

@ZhouChaunge

Description

@ZhouChaunge

问题概述

当 agent 通过 run_shell_bg 启动一个任何长时间运行的命令时,模型经常会输出一句安抚性的回复(例如“任务正在运行中,完成后我会告诉你结果”),随后 本轮 agent run 立即结束

任务真正完成后,没有任何机制把结束事件递回到模型 / 会话,用户也再也收不到任何反馈。模型嘴上的“收到通知后再告诉你”是一句系统层面永远兑现不了的承诺。

复现操作(最小化、与具体语言/框架无关):

  1. 在任意会话里让模型执行一个 1–10 分钟级的 bg 命令,例如:
    • run_shell_bg(command: "ping -n 60 127.0.0.1")
    • run_shell_bg(command: "<任何执行时间超过 ~3s 的脚本>")
  2. 引导模型在调用工具后说一句类似“正在跑/稍后告诉你”的话。
  3. 观察:模型给出该回复后,会话回到 idle,永不返回最终结果,即使后台命令已经成功结束并产生了 exit code。

预期:本 session 在本轮启动的 bg 任务结束前,agent loop 必须保持值守;任务结束时模型必须被唤回,把 exit code + tail output 反馈给用户。


根因分析(三层契约不一致)

层 1 — 工具描述层:run_shell_bg 给模型的提示文案是错的

src/tools/bg-shell.jsstatus: '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.jscleanupStaleJobs 把超过 4h 仍 running 的 job 当 stale 删除并广播 exitCode: null。任何合法的多小时长任务(训练、批处理、大型构建等)都可能被误杀。

用户视角看到的完整时间线

  1. 模型调用 run_shell_bg(command: "<long running command>")
  2. 2.5s 早退窗口内没结束 → bg-shell 返回 status: 'running' + 文案承诺“会自动唤醒”
  3. 模型输出一句“任务正在运行中…完成后我会告诉你”
  4. agent-loop 命中 BG_WAIT_SKIPPED_MODEL_DONE → run 终止
  5. (任意时间后)命令真的跑完 → bg-job-end 广播 → 零订阅者 → 事件蒸发
  6. 用户视角:那句“完成后通知你”永远不会被兑现

期望行为

  • 只要本轮 run_shell_bg 启动的 job 仍在运行,agent loop 不允许结束本轮,无视模型是否输出了文本回复。
  • bg 任务真实结束后,必须把 exit code + tail output 作为 <system-reminder> 注入模型,由模型对用户做出最终反馈。
  • 工具描述中所有“automatically woken / end your turn”之类承诺必须与系统实现一致,避免诱导模型主动结束。
  • SI 不可靠时,必须有兜底通道感知任务结束,而不是默默丢事件。

修复计划(分阶段,零 UI 改动)

阶段 1(必做,止血)

  1. 在 agent-loop 给每个 run 加一个集合 run._sessionStartedBgJobs: Set<jobId>,只收纳本 run 内通过 run_shell_bg 启动的 job(用 ctx 注入回调登记,不让 bg-shell 反向依赖 run 对象)。

  2. 改写 BG_WAIT_SKIPPED_MODEL_DONE 的判据为:

    assistantText 非空 无 pending 事件 run._sessionStartedBgJobsmyBgJobs() 没有交集 → 才允许 break。

    含义:dev server / 上一轮长任务保持原有体验;本轮新启动的长任务强制值守。

  3. run_shell_bg 工具 hint 改成对系统行为如实描述,明确告知模型:

    “你不能用‘说完再走’的方式提前结束 turn;loop 会保持你值守,结束时会以 <system-reminder> 把结果再次喂给你。”

  4. BG_WAIT_SKIPPED_MODEL_DONE 日志附带 sessionStartedJobs,便于回归验证。

阶段 2(建议):堵住 SI 不可靠导致“值守也等不到结果”

  1. run_shell_bg 即便 usedSI === false 也调用 addActiveBgJob,并打标 polled: true
  2. terminal-monitor 增加一个仅在 _activeBgJobs 非空时存在的轮询线程(~10s 间隔),对 polled: true 的任务用 findTerminalByName + getRecentExecutions(t, 1) 判定结束,合成 payload 触发回调,跑空即清除定时器。
  3. 把 4h stale 硬限抬高(或做成可配置 / 二次确认),避免合法长跑任务被误杀。

阶段 3(可选):跨重启鲁棒性

  1. SessionStore 加一个 pendingBgEvents: Array<{...}> 字段,向后兼容(默认 [])。
  2. terminal-monitor 在广播 bg-job-end 的同时按 sessionId 落盘到该字段。
  3. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions