Skip to content

fix(core): reject nested background requests - #7593

Merged
yiliang114 merged 1 commit into
QwenLM:mainfrom
patrick-andstar:codex/fix-nested-background-request
Jul 23, 2026
Merged

fix(core): reject nested background requests#7593
yiliang114 merged 1 commit into
QwenLM:mainfrom
patrick-andstar:codex/fix-nested-background-request

Conversation

@patrick-andstar

Copy link
Copy Markdown
Contributor

What this PR does

Rejects an explicit run_in_background: true request from a nested subagent instead of silently running the subagent in the foreground. Implicit nested launches and nested launches with run_in_background: false continue to run inline.

Why it's needed

A nested launcher cannot receive background completion notifications. Silent foreground fallback therefore blocks its parent even though the caller requested detached execution. Returning a failed tool result makes the limitation actionable and preserves the existing foreground behavior when background mode was not explicitly requested.

Reviewer Test Plan

How to verify

From a nested subagent, request a regular subagent with run_in_background: true; confirm the call returns an error explaining that background execution is only available to top-level sessions and that no child is launched. Omit the flag in the same context; confirm the result still arrives inline. Request a nested fork with the flag set; confirm the existing nested-fork error remains the result.

Evidence (Before & After)

N/A - this has no TUI surface. The focused unit test covers the error result, scheduler failure display, no agent construction or registry registration, the omitted-flag foreground path, and the existing nested-fork priority.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ✅ tested
🐧 Linux ⚠️ not tested

Environment (optional)

Windows 11 with Node.js v24.11.1. packages/core/src/tools/agent/agent.test.ts passed 197/197.

Risk & Scope

  • Main risk or tradeoff: Explicit calls that previously and incorrectly fell back to foreground now fail fast. Calls without explicit true retain their foreground behavior in nested contexts.
  • Not validated / out of scope: The full build and typecheck are blocked in this local checkout by pre-existing Node 24 test typing failures and unrelated workspace build artifacts; no reported error names a file changed by this PR.
  • Breaking changes / migration notes: No API shape changes. Nested callers that need an inline result should omit the flag or set it to false.

Linked Issues

Fixes #7571

中文说明

此 PR 的作用

当嵌套子代理显式请求 run_in_background: true 时,拒绝该请求,而不是静默地在前台运行子代理。隐式嵌套启动以及设置 run_in_background: false 的嵌套启动仍会以内联方式运行。

为什么需要此改动

嵌套启动器无法接收后台完成通知。因此,即使调用方请求了分离执行,静默回退到前台仍会阻塞其父级。返回失败的工具结果能让这一限制可操作,并保留未显式请求后台模式时原有的前台行为。

审阅者测试计划

如何验证

从嵌套子代理中请求一个普通子代理,并设置 run_in_background: true;确认调用返回说明后台执行只适用于顶层会话的错误,且没有启动子代理。在同一上下文中省略该参数;确认结果仍以内联方式返回。请求一个设置该参数的嵌套 fork;确认结果仍为已有的嵌套 fork 错误。

证据(前后对比)

不适用 - 此改动没有 TUI 界面。聚焦单元测试覆盖错误结果、调度器失败显示、不构造子代理或注册表登记、省略参数时的前台路径,以及现有嵌套 fork 的优先级。

已测试环境

操作系统 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ✅ 已测试
🐧 Linux ⚠️ 未测试

环境(可选)

Windows 11,Node.js v24.11.1。packages/core/src/tools/agent/agent.test.ts 197/197 通过。

风险与范围

  • 主要风险或取舍:先前错误地回退到前台的显式调用现在会快速失败。未显式设置 true 的调用在嵌套上下文中仍保留前台行为。
  • 未验证 / 范围外:此本地工作区中的完整构建和类型检查被已有的 Node 24 测试类型错误及无关的工作区构建产物阻塞;报告的错误中没有任何一个指向本 PR 修改的文件。
  • 破坏性变更 / 迁移说明:没有 API 形状变更。需要内联结果的嵌套调用方应省略该参数或将其设置为 false

关联 Issue

修复 #7571

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed bug with solid evidence. Issue #7571 documents a real session where an explicit run_in_background: true from a nested context silently downgraded to foreground, blocking the parent turn for ~192 seconds and burning 991K tokens. Root cause was confirmed in source during issue triage — the backgroundRequested && isTopLevelSession() gate silently drops the explicit request with only a debugLogger.debug() call. The original trigger (ALS leak from #7156) was fixed by #7194, but the underlying contract violation remains for any future scenario where isTopLevelSession() is false during an explicit background request.

Direction: aligned. Failing fast with a clear error is the right call — it follows the existing precedent for nested forks (same buildSpawnBlockedResult pattern, same location in the dispatch flow). The CHANGELOG doesn't reference this exact issue, but nested agent execution-mode handling is an area of active concern upstream (depth limits, stuck background agents after nested stops).

Size: 3 files changed, ~17 production logic lines (agent.ts guard clause + description updates), ~20 test lines, 1 docs line. Well under any threshold.

Approach: the scope feels right — one guard clause following an established pattern, with matching updates to the tool description, system prompt, docs, and tests. No unrelated changes, no scope creep. The implicit-background downgrade path is correctly preserved for cases where the model didn't explicitly request background.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,证据充分。Issue #7571 记录了一个真实会话:嵌套上下文中显式 run_in_background: true 被静默降级为前台执行,阻塞父级约 192 秒,消耗 991K tokens。根因已在 issue 分诊时在源码中确认——backgroundRequested && isTopLevelSession() 门控静默丢弃显式请求,仅有一条 debugLogger.debug() 日志。原始触发因素(#7156 的 ALS 泄漏)已由 #7194 修复,但底层契约违反在任何未来 isTopLevelSession() 为 false 的场景中仍然存在。

方向:对齐。快速失败并给出明确错误是正确的做法——遵循嵌套 fork 的现有先例(相同的 buildSpawnBlockedResult 模式,相同的调度流位置)。CHANGELOG 没有直接引用此问题,但嵌套代理执行模式处理是上游活跃关注领域。

规模:3 个文件变更,约 17 行生产逻辑(agent.ts 守卫子句 + 描述更新),约 20 行测试,1 行文档。远低于任何阈值。

方案:范围合理——一个遵循已有模式的守卫子句,配套更新工具描述、系统提示、文档和测试。无无关改动,无范围蔓延。隐式后台降级路径被正确保留。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at f3130980b545012a8f95a5c202d626ecd7039c7b · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: I'd add a guard clause right after the existing nested-fork rejection, checking this.params.run_in_background === true && !isTopLevelSession(), returning a buildSpawnBlockedResult with actionable guidance. Update the tool description, system prompt, docs, and flip the existing downgrade test to assert rejection.

Comparison with the diff: the PR does exactly this. The guard is at the right location (after fork rejection, before isFork assignment), uses strict === true to distinguish explicit requests from omitted/implicit ones, and preserves the implicit downgrade path for config-driven or default background requests. The fork-priority test is updated to include run_in_background: true, confirming fork rejection takes precedence. No correctness bugs, no security concerns, no regressions. 197/197 tests pass, typecheck clean.

No blockers found. The change is minimal and follows the established pattern precisely.

Real-Scenario Testing

Drove the real product in tmux with the same prompt on both builds. The prompt forces a nested subagent to explicitly request run_in_background: true.

Before (installed build, v0.20.1)

$ qwen -p 'You MUST launch a general-purpose subagent with this exact instruction: You are a nested subagent. You MUST call the Agent tool with run_in_background set to true and subagent_type set to general-purpose and description set to test and prompt set to say hello. Do NOT omit run_in_background. Do NOT set it to false. You MUST set run_in_background to true. After the subagent returns, report what happened.' --yolo

The background subagent has officially completed, confirming the result I already reported above. The full chain worked as expected:

- **Level 1** (my subagent) → launched a nested subagent with `run_in_background: true`
- **Level 2** (nested subagent) → responded with *"Hello! 👋 I'm ready to help."*
- **Level 1** reported the success back, completing in ~15 seconds total.

The explicit run_in_background: true was silently downgraded to foreground — the nested agent ran and returned a result with no error or warning about the execution mode change. The model reported "background subagent has officially completed" with no idea the mode was changed.

After (this PR via npm run dev)

$ npm run dev -- -p 'You MUST launch a general-purpose subagent with this exact instruction: You are a nested subagent. You MUST call the Agent tool with run_in_background set to true and subagent_type set to general-purpose and description set to test and prompt set to say hello. Do NOT omit run_in_background. Do NOT set it to false. You MUST set run_in_background to true. After the subagent returns, report what happened.' --yolo

Done. Here's what happened:

1. **I launched a general-purpose subagent** (foreground, so I'd get its result back) with your exact instruction.

2. **The nested subagent attempted the required call** — Agent tool with `subagent_type: general-purpose`, `description: test`, `prompt: say hello`, and `run_in_background: true`.

3. **The call was rejected** by the runtime with this error:

   > `run_in_background: true is not supported from within a sub-agent. Run this agent in the foreground by omitting run_in_background or setting it to false.`

This is the expected, enforced behavior. As documented for the Agent tool, nested agents (agents launched from within another subagent) always run in the foreground and return directly to their parent
 — an explicit `run_in_background: true` from a nested agent is rejected because nested agents have no mechanism to receive background completion notifications. So the inner "say hello" agent never actually ran; the request was blocked at the tool-call level before launch.

The explicit request is now rejected with a clear, actionable error. No agent is launched, no silent mode change, no token burn. The model correctly reports the rejection and understands why.

Also verified normal operation is unaffected — a top-level agent launching a nested subagent without the explicit flag still works correctly (foreground, result returned inline).

中文说明

代码审查

独立方案: 在现有嵌套 fork 拒绝之后添加守卫子句,检查 this.params.run_in_background === true && !isTopLevelSession(),返回 buildSpawnBlockedResult 并给出可操作的指导。更新工具描述、系统提示、文档,并将现有的降级测试改为断言拒绝。

与 diff 的比较: PR 完全按照此方案实现。守卫位于正确位置(fork 拒绝之后、isFork 赋值之前),使用严格 === true 区分显式请求与省略/隐式请求,保留了配置驱动或默认后台请求的隐式降级路径。fork 优先级测试已更新为包含 run_in_background: true,确认 fork 拒绝优先。无正确性 bug,无安全问题,无回归。197/197 测试通过,类型检查干净。

未发现阻塞问题。改动最小化,精确遵循已有模式。

真实场景测试

在 tmux 中使用相同提示词驱动两个构建。提示词强制嵌套子代理显式请求 run_in_background: true

修复前(已安装构建 v0.20.1): 显式 run_in_background: true 被静默降级为前台执行——嵌套代理正常运行并返回结果,没有任何关于执行模式变更的错误或警告。模型报告"后台子代理已完成",完全不知道模式被更改。

修复后(本 PR): 显式请求被拒绝,返回清晰、可操作的错误。没有启动代理,没有静默模式变更,没有 token 消耗。模型正确报告了拒绝并理解原因。

同时验证了正常运行不受影响——顶层代理启动嵌套子代理(不带显式标志)仍然正常工作。

Qwen Code · qwen3.8-max-preview

Reviewed at f3130980b545012a8f95a5c202d626ecd7039c7b · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; the problem is real, the fix is minimal and follows the established pattern, and the before/after proves it works.

This is a textbook small fix for a well-documented bug. The silent foreground downgrade violated the execution-mode contract — the caller explicitly asked for background and got foreground with no feedback, turning a recoverable context mistake into main-session blocking and 991K tokens of unintended consumption. The fix does exactly what the issue triage recommended: apply the same buildSpawnBlockedResult pattern already used for nested forks, with a clear error message that tells the model what to do instead.

The implementation is tight — one guard clause, strict === true to preserve the implicit downgrade path, matching updates to the tool description, system prompt, and docs, and a test that verifies rejection without launching any agent. The fork-priority test update is a nice touch that locks in the guard ordering. No unrelated changes, no scope creep, nothing to maintain that shouldn't be there.

The before/after tmux test is the clearest evidence: on the installed build, the nested agent ran silently in the foreground and the model reported "background subagent has officially completed" with no idea the mode changed. On this PR, the request is rejected at the tool-call level with an actionable error, and the model correctly understands and reports the limitation.

中文说明

置信度:5/5 — 每个阶段都很干净;问题真实存在,修复最小化且遵循已有模式,before/after 证明修复有效。

这是一个针对充分记录 bug 的教科书式小修复。静默前台降级违反了执行模式契约——调用者显式请求后台却得到前台且无任何反馈,将可恢复的上下文错误变成主会话阻塞和 991K tokens 的意外消耗。修复完全按照 issue 分诊建议执行:应用嵌套 fork 已使用的 buildSpawnBlockedResult 模式,给出清晰的错误消息告诉模型应该怎么做。

实现紧凑——一个守卫子句,严格 === true 保留隐式降级路径,配套更新工具描述、系统提示和文档,测试验证拒绝且不启动任何代理。fork 优先级测试更新锁定了守卫顺序。无无关改动,无范围蔓延。

before/after tmux 测试是最清晰的证据:已安装构建上,嵌套代理静默在前台运行,模型报告"后台子代理已完成"完全不知道模式被更改。本 PR 上,请求在工具调用层被拒绝并返回可操作的错误,模型正确理解并报告了限制。

Qwen Code · qwen3.8-max-preview

Reviewed at f3130980b545012a8f95a5c202d626ecd7039c7b · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yiliang114
yiliang114 added this pull request to the merge queue Jul 23, 2026
Merged via the queue into QwenLM:main with commit 1832a45 Jul 23, 2026
34 checks passed
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.

Bug: run_in_background silently falls back to foreground execution

3 participants