Skip to content

fix: stop background subagents from blocking the main agent - #1140

Draft
josemonteiro wants to merge 4 commits into
masterfrom
fix/background-subagent-blocking
Draft

fix: stop background subagents from blocking the main agent#1140
josemonteiro wants to merge 4 commits into
masterfrom
fix/background-subagent-blocking

Conversation

@josemonteiro

@josemonteiro josemonteiro commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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. This PR makes background the default, fixes the blocking, and makes wait outcomes explicit.

  • 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.
  • run_in_background now defaults to true; set run_in_background: false only when the next step in the workflow depends on the agent's result. Persona policy and explicit caller values still override the default.
  • A capped join reports "Waited 60s (cap)" with re-join guidance for hard dependencies (e.g. ferment worker join); an aborted wait reports "Wait cancelled"; queued workers (behind the concurrency cap) report "queued, not started" instead of "No output.".
  • Preserve the legitimate bounded-join use case used by ferment worker handoff (src/extensions/ferment/tools/steps.ts:185).

Closes internal report: background subagent polling blocks main agent.

Commits

  • cbdaa47d — bounded, interruptible get_subagent_result(wait: true) + LLM contract fixes
  • 55187f91 — default subagents to background unless the workflow depends on them
  • 329c0d57 — explicit wait outcomes (timeout/abort) and queued state, from two-axis spec review

Checklist

  • I have run pnpm run lint and pnpm run typecheck.
  • I have added/updated tests for this change (346 agent-extension tests, 1454 ferment tests green).

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>
@josemonteiro josemonteiro added the bug Something isn't working label Sep 4, 2026
@kimchi-review

kimchi-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

Kimchi Code Review

Property Value
Commit cbdaa47
Author @josemonteiro
Files changed 2
Review status Completed
Comments 1 (1 info)
Duration 113s

Summary

📊 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.

What to expect

Kimchi will analyze the changes in this pull request and post:

  • A summary of the overall changes
  • Inline comments on specific lines with findings categorized by issue type

The review typically completes within a few minutes. This comment will be updated once the review is ready.

Interact with Kimchi
  • @getkimchi review — re-trigger a full review on the latest commit
  • @getkimchi summary — regenerate the PR summary
  • @getkimchi ignore — skip this PR (no review will be posted)
  • Reply to any inline comment to ask follow-up questions or request clarification
Configuration

Reviews are configured by your organization admin.
Review instructions, excluded directories, and severity thresholds can be adjusted per repository in the Kimchi dashboard.


Powered by Kimchi — AI-powered code review by CAST AI

@kimchi-review kimchi-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📊 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️⚠️ Error Handling

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.

josemonteiro and others added 3 commits September 4, 2026 15:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant