fix: stop background subagents from blocking the main agent - #1140
fix: stop background subagents from blocking the main agent#1140josemonteiro wants to merge 4 commits into
Conversation
Background agents already fire a completion notification, but the get_subagent_result tool invited blocking with its text and waited indefinitely with no timeout or abort handling. This froze the main agent's turn and queued user input. - Rework LLM-facing text so backgrounding is paired with "you will be notified" instead of "use get_subagent_result / wait: true". - Cap get_subagent_result(wait: true) at 60s and wire the AbortSignal so Esc aborts the wait. - On timeout/abort, leave the result unconsumed and the nudge armed so the completion notification still delivers the result later. - Preserve the legitimate bounded-join use case used by ferment worker handoff (steps.ts:185). Closes internal report: background subagent polling blocks main agent. Label: bug Co-Authored-By: Kimchi <noreply@kimchi.dev>
Kimchi Code Review
Summary📊 Review Score: 90/100 (overall code quality — 0 lowest, 100 highest) 🧪 Tests: yes — Comprehensive regression coverage was added: guideline content assertions guard against endorsing polling/blocking, three contract tests cover the 60s cap, abort-signal interruption, and normal completion paths for 📝 Found 1 issue(s). See inline comments for details. What to expectKimchi will analyze the changes in this pull request and post:
The review typically completes within a few minutes. This comment will be updated once the review is ready. Interact with Kimchi
ConfigurationReviews are configured by your organization admin. Powered by Kimchi — AI-powered code review by CAST AI |
There was a problem hiding this comment.
📊 Review Score: 90/100 (overall code quality — 0 lowest, 100 highest)
⏱️ Estimated effort to review: 3/5 (1 = trivial, 5 = very complex)
🧪 Tests: yes — Comprehensive regression coverage was added: guideline content assertions guard against endorsing polling/blocking, three contract tests cover the 60s cap, abort-signal interruption, and normal completion paths for get_subagent_result(wait: true), plus a background-spawn contract test verifying the caller is told not to block. Tests use fake timers to verify the cap and verify that a capped/aborted wait leaves the result unconsumed so the completion notification can still fire.
📝 Found 1 issue(s). See inline comments for details.
| signal?.removeEventListener("abort", onAbort) | ||
| resolve(outcome) | ||
| } | ||
| if (signal?.aborted) { |
There was a problem hiding this comment.
ℹ️
waitForAgentCompletion treats a rejected agentPromise identically to a fulfilled one by resolving "completed" in both branches, swallowing the rejection reason. The original await record.promise would have propagated the rejection as a tool-execution failure. While the caller later checks record.status, the lost reason reduces observability and changes error-handling semantics if the manager ever rejects the promise before setting record.status to "error".
💡 Suggestion: Log the rejection reason inside the rejection handler before calling finish("completed") (e.g. console.error("Agent run rejected during wait:", reason)), and document/verify that the manager always updates record.status to "error" before rejecting record.promise.
Backgrounding is now the default for Agent/subagent calls. This aligns the harness with the expectation that subagents run independently and notify the caller on completion, instead of blocking the main agent by default. - resolveAgentInvocationConfig defaults runInBackground to true. - Agent tool guidelines/schema now say: omit run_in_background for background (default), set run_in_background: false only for hard dependencies. - Custom agent template default updated to true. - Orchestration and ferment instruction text updated to reflect the new default. - Added regression tests for the default and for explicit opt-out. Co-Authored-By: Kimchi <noreply@kimchi.dev>
Three follow-ups from the two-axis spec review of PR #1140: - wait: true now distinguishes outcomes: a capped join reports "Waited 60s (cap)" with re-join guidance for hard dependencies (e.g. ferment worker join), and an aborted wait reports "Wait cancelled". Previously both collapsed into the generic still-running text, which also countermanded the ferment "wait, then retry complete_ferment_step" instruction. - Queued workers (background-by-default behind the concurrency cap) now report "queued behind the cap, not started" instead of falling through to "No output.", for both plain and wait: true calls. - Result-consumption semantics unchanged: timeout/abort/queued never consume the result and the completion nudge stays armed. Tests: timeout/abort assertion updates plus queued-state cases in get_subagent_result wait contract. Co-Authored-By: Kimchi <noreply@kimchi.dev>
Background-by-default broke the ferment-oneshot-exit smoke test in CI: in --print/headless mode there is no interactive loop to consume completion notifications, so the orchestrator ended its turn after spawning a backgrounded worker and the process exited mid-step (ferment abandoned). Master CI is green on the same base, confirming the regression is ours. resolveAgentInvocationConfig now takes a caller-supplied runInBackground fallback driven by ctx.hasUI: interactive sessions default to background, headless runs keep the old foreground default. Explicit run_in_background params and persona policy still override both defaults, so scripts that opt into backgrounded headless work are unaffected and the interactive UX is unchanged. Verified locally: ferment-oneshot-exit smoke test passes; agent and invocation-config suites green; lint and typecheck clean. (The ferment-v2-print-exit/workflows smoke failures reproduced on the pre-fix commit locally as well while passing in CI — local environment flakes, unrelated.) Co-Authored-By: Kimchi <noreply@kimchi.dev>
Background agents already fire a completion notification, but the
get_subagent_resulttool invited blocking with its text and waited indefinitely with no timeout or abort handling. This froze the main agent's turn and queued user input. This PR makes background the default, fixes the blocking, and makes wait outcomes explicit.get_subagent_result(wait: true)at 60s and wire the AbortSignal so Esc aborts the wait.run_in_backgroundnow defaults totrue; setrun_in_background: falseonly when the next step in the workflow depends on the agent's result. Persona policy and explicit caller values still override the default.src/extensions/ferment/tools/steps.ts:185).Closes internal report: background subagent polling blocks main agent.
Commits
cbdaa47d— bounded, interruptibleget_subagent_result(wait: true)+ LLM contract fixes55187f91— default subagents to background unless the workflow depends on them329c0d57— explicit wait outcomes (timeout/abort) and queued state, from two-axis spec reviewChecklist
pnpm run lintandpnpm run typecheck.