Skip to content

fix(claude): hold the CLI open for backgrounded agents and workflows - #1291

Open
HandyS11 wants to merge 2 commits into
siteboon:mainfrom
HandyS11:fix/background-agents-killed-at-turn-end
Open

HandyS11 wants to merge 2 commits into
siteboon:mainfrom
HandyS11:fix/background-agents-killed-at-turn-end

Conversation

@HandyS11

@HandyS11 HandyS11 commented Sep 8, 2026

Copy link
Copy Markdown

Fixes #1268.

The bug

startsBackgroundWork() decides whether a turn's CLI process is held open past its result. It matches Bash and the DEFERRED_WORK_TOOLS set, but nothing matches Agent — so a turn whose only outstanding work is a background subagent is scored as having nothing pending. releasePromptStream() runs immediately, the SDK closes stdin, the CLI reads that EOF as print wind-down, and the agent is killed mid-run.

The Node server never restarts and the browser stays on the same transcript, so the only visible trace is on the next message:

No completion record was found for background agent "<name>" from the previous session.

As #1268 notes, this is a gap in the fix that shipped for #1113 rather than a regression: that work predates background agents, and its own notes list subagents as finishing inside a turn, which was true at the time.

Reproduce

  1. Ask Claude to spawn a background agent that runs for a minute or two.
  2. Wait for the turn to end — the agent is still running.
  3. Send any follow-up message.
  4. The follow-up opens with the missing-completion-record warning, and the agent produced no result.

The change

Agent gets its own arm rather than a place in DEFERRED_WORK_TOOLS, for the reason given in the issue: that set would also match foreground agents, which never push a follow-up turn and would therefore pin a CLI process for the full 30-minute BG_WAIT_CEILING_MS.

The test is !== false rather than === true because run_in_background is optional on AgentInput and agents background by default — an omitted field means background, so === true would still drop the common case.

Workflow is added to DEFERRED_WORK_TOOLS, and that is deliberate: WorkflowInput has no foreground option at all, so every call returns a task id immediately and reports back in a later turn. It has the identical failure mode in the identical function. Happy to split it out if you would rather keep this PR to the letter of the issue.

Tests

New server/modules/providers/tests/claude-background-work.test.ts, following the existing claude-subagent-echo.test.ts pattern; startsBackgroundWork is exported for it the same way isSubagentPromptEcho is.

Ten cases, including guards against the two ways this could be got wrong — a foreground Agent must not hold (or it pins the process for the ceiling), and the existing Bash/Monitor behaviour must not shift.

Verified the tests fail without the fix rather than assuming it: reverting just the two logic changes fails exactly the 4 fix-dependent cases and leaves the other 6 green.

Checks

  • npm test — 421 pass / 4 fail. The 4 failures are claude-cli-path.test.ts and are pre-existing on unmodified main: this machine has a native /usr/bin/claude, which those tests assume absent. Confirmed by stashing and re-running.
  • npm run build — passes.
  • oxlint — clean on both changed files.
  • No lockfile or unrelated changes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Agent tasks now run in the background by default unless explicitly configured for foreground execution.
    • Workflow actions are recognized as deferred background work.
    • Messages containing multiple tool calls are now supported for background processing.
  • Bug Fixes

    • Improved classification of background and foreground work across supported tools, including Bash, Agent, and Workflow actions.

startsBackgroundWork() decides whether a turn's CLI process is held open
past `result`. It matched Bash and the deferred-work set, but not Agent,
so a turn whose only outstanding work was a background subagent scored as
having nothing pending. stdin was released immediately, the CLI read that
EOF as print wind-down, and the agent was killed mid-run — surfacing on
the next message as "No completion record was found for background agent".

This is a gap in the fix that shipped for siteboon#1113, not a regression: that
work predates background agents, and its own notes list subagents as
finishing inside a turn, which was true then.

Agent gets its own arm rather than a place in DEFERRED_WORK_TOOLS, because
that set would also match foreground agents, which never push a follow-up
turn and would pin a process for the full BG_WAIT_CEILING_MS. The test is
`!== false` rather than `=== true` since `run_in_background` is optional on
AgentInput and agents background by default.

Workflow does belong in the set: WorkflowInput has no foreground option at
all, so every call returns a task id immediately and reports back later.

Fixes siteboon#1268

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 8, 2026 12:29
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d7a3169e-2162-4898-b57b-fdfaf1e9615e

📥 Commits

Reviewing files that changed from the base of the PR and between 79c7686 and a8eb8aa.

📒 Files selected for processing (1)
  • server/modules/providers/list/claude/claude-runtime.provider.js

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The Claude runtime now detects background Agent and Workflow tool calls. The detector is exported and tested for foreground, background, deferred, mixed, and empty tool-call messages.

Changes

Claude background work

Layer / File(s) Summary
Background work detection and validation
server/modules/providers/list/claude/claude-runtime.provider.js, server/modules/providers/tests/claude-background-work.test.ts
Workflow is treated as deferred work. Agent runs in the background unless run_in_background is explicitly false. startsBackgroundWork is exported and tested across supported tool-call cases.

Suggested reviewers: blackmammoth

Priority: ➖ Normal — Schedule this Claude runtime fix because background agents and workflows can be terminated before deferred work completes, a medium-severity product defect.

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to a8eb8

Background Agent and Workflow calls now keep the CLI alive as intended, while explicitly foreground Agents remain excluded. No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: keeping the Claude CLI open for backgrounded agents and workflows.
Linked Issues check ✅ Passed The changes satisfy issue [#1268]. Backgrounded Agent calls now keep the CLI open, explicit foreground agents do not, and tests cover the required behavior. Workflow support is also implemented as def…
Out of Scope Changes check ✅ Passed The implementation, export change, and tests are related to background-work detection and the linked issue objectives. No unrelated code changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each tool,
Background agents stay alive,
Workflows wait in line,
Foreground paths remain clear,
Tests guard every branch.

Comment @coderabbitai help to get the list of available commands.

Copilot AI 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.

🟡 Changes recommended

It introduces/extends backend-module export and file-type conventions (export-at-declaration and JS-in-server/modules/) that should be addressed to comply with the repository’s backend module standards.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes an issue in the Claude provider where turns that launch background work via the Agent tool were not recognized as “background work pending,” causing the CLI stdin to be released immediately after result and killing the background agent mid-run (issue #1268). It also extends the same “hold CLI open” behavior to Workflow, which always reports back in a later turn.

Changes:

  • Update startsBackgroundWork() to treat Agent tool calls as background work unless run_in_background === false.
  • Add Workflow to the deferred-work tool set so workflow task IDs keep the CLI open until follow-up.
  • Add a focused unit test suite covering background/foreground Bash, Agent, Workflow, and regressions for existing deferred tools.
File summaries
File Description
server/modules/providers/list/claude/claude-runtime.provider.js Extends background-work detection for Agent and Workflow, and exports the helper for unit testing.
server/modules/providers/tests/claude-background-work.test.ts Adds unit coverage for the background-work scoring logic across key tool scenarios.
Review details

Suppressed comments (1)

server/modules/providers/list/claude/claude-runtime.provider.js:1230

  • If startsBackgroundWork is exported at its declaration, it should be removed from the end-of-file export list to avoid duplicate exports and to keep the file’s public API surface explicit at the definition site.
  extractTokenBudget,
  extractCumulativeTokenBudget,
  startsBackgroundWork
};
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +542 to +547
// Tool calls that leave work running past the end of a turn. Bash and Agent only
// count when they are backgrounded; the rest defer or watch work by nature.
// Workflow belongs here rather than in a branch of its own: its input schema has
// no foreground option at all, so every call returns a task id immediately and
// reports back in a later turn.
const DEFERRED_WORK_TOOLS = new Set(['Monitor', 'ScheduleWakeup', 'CronCreate', 'TaskCreate', 'Workflow']);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'd rather not do this one here, and I think the premise is slightly off.

It's correct that this is the only .js file in list/claude/. But it isn't an isolated straggler — all three runtime providers are .js:

File Lines
list/claude/claude-runtime.provider.js 1233
list/opencode/opencode-runtime.provider.js 434
list/cursor/cursor-runtime.provider.js 387

That's 5 of the 5 non-TS files under server/modules/ accounted for by the runtime layer and the notification orchestrator. Migrating this one alone would make it the odd provider out rather than bringing it in line.

On the standard itself (.agents/skills/backend-module-standards/SKILL.md), the two relevant rules are:

Use TypeScript for every file inside server/modules/. Do not add JavaScript backend module files.

This PR adds no .js file — the new test is .ts.

When touched JavaScript utilities belong to the work, migrate them to TypeScript. Place a one-use utility in its sole component; place a utility used in at least two locations in server/shared/utils.ts.

The placement guidance scopes this to utilities. A 1,233-line runtime provider isn't one, and there's nowhere for it to go under either branch of that rule.

Against that, CONTRIBUTING.md asks to keep PRs focused. A 1,233-line JS→TS migration would dwarf a 17-line bug fix and make the actual behaviour change hard to review — on a file whose failure mode is silently killed background work.

Taking the follow-up option you offered: happy to open an issue to migrate all three runtime providers together, or to send it as a separate PR if the maintainers would prefer. Just say which.

Comment on lines +571 to +575
// A backgrounded subagent outlives the turn exactly like a backgrounded
// Bash does, so the process has to be held open for it to report back.
// Agents background by default — `run_in_background` is optional and only
// an explicit `false` opts out — hence `!== false` rather than `=== true`.
// A foreground agent must stay out of DEFERRED_WORK_TOOLS: it never pushes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in a8eb8aaexport function startsBackgroundWork() at the declaration, removed from the end-of-file block, with a Used by ... note naming the consumer per the standard's rule on documenting exported components.

Worth noting the precedent holds in the other direction too: isSubagentPromptEcho is exported the same way and its only cross-file consumer is also a test, so this isn't a new kind of export for the module.

I left the nine remaining symbols in the end-of-file block alone — they're pre-existing and unrelated to this fix.

Backend module standards require exports at the declaration rather than
collected at the end of an implementation file, and a note naming the
consumer. Matches isSubagentPromptEcho, the module's other test-facing
predicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@andriilh-amzn

Copy link
Copy Markdown

Confirmed this fix against 1.37.3 (cherry-picked onto it, 10/10 of the new tests pass, 171/172 of the providers suite with 1 pre-existing skip). Two observations from diagnosing the same failure, in case they are useful — the second one suggests a follow-up is still needed.

1. The CLI child does not exit at wind-down — it detaches and keeps working.

sdkOptions.env sets CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS to BG_WAIT_CEILING_MS (30 min) on every spawn, so when stdin closes the CLI waits out its background agents rather than killing them. #1268 describes the child as exiting and the agent as dying mid-run; what I actually see is the agent still running in a process nothing is attached to. Two of them, both resuming the same session id, alive 8 and 2 minutes after their turns had reported complete:

PID     ELAPSED  COMMAND
265319  08:03    claude --output-format stream-json … --resume <session-id>
273444  02:05    claude --output-format stream-json … --resume <session-id>

The user-visible result is the same and the fix here is still correct — but the agent's work usually is not lost, it lands in the transcript with no client attached. It also means two CLI processes can hold one session id and append to one JSONL concurrently.

2. A second teardown path survives this fix.

queryClaudeSDK releases the previous run's held stdin at the start of every new turn:

// A new turn supersedes any earlier one still holding this session's process
// open, so held runs cannot stack up across a conversation.
if (sessionKey()) {
  getSession(sessionKey())?.releaseInput?.();
}

Since every turn builds a fresh query() with resume rather than pushing the prompt into the existing input stream, the hold this PR arms is torn down by the next user message. So the sequence that produced my report — background agent still running, user sends a follow-up — still ends with a rival --resume process whose orphan scan reports "No completion record was found…", because startsBackgroundWork scoring the turn correctly does not stop the release at line ~735.

That reading is from the code path plus the process evidence above rather than an A/B test with this patch applied, so I may be missing something — but if it holds, the complete fix needs the new turn delivered into the held run's input stream (createHeldPromptStream already parks an open generator for exactly that shape) instead of releasing it and spawning a second process on the same session id. Happy to open a separate issue for that half if you'd rather keep this PR scoped to the tool matching.

@liran-funaro

Copy link
Copy Markdown
Contributor

Heads-up on an interaction with #1296, which touches the same twenty lines of this file — and on a measurement that supports your change.

Where they meet. #1296 adds a second condition to the same hold, so the process is kept open while the CLI's own list of outstanding work is non-empty, not only when this turn's tool calls scored true:

function shouldHoldForBackgroundWork(pendingFromThisTurn, outstandingTasks) {
  return Boolean(pendingFromThisTurn) || outstandingTasks.length > 0;
}

That list arrives as system / background_tasks_changed, and its task_type vocabulary in CLI 2.1.263 includes local_agent and remote_agent beside local_bash — so a backgrounded agent does appear in it. In other words the two changes close the same gap from opposite ends: yours by scoring the tool call that starts the work, mine by holding while the CLI still says work is outstanding.

Neither replaces the other, and yours is the one that belongs at the root:

  • the tool-call test is what holds the process across the first result, before any background_tasks_changed has necessarily arrived;
  • the list is what holds it across a turn boundary, which is the case a resumed turn hits — the tool call that armed the work belongs to a turn that has already ended, so nothing in the new turn scores true.

Caveat on my evidence, since it is partial: I confirmed the task_type vocabulary in the CLI binary and watched background_tasks_changed carry task_type: "local_bash" from a live run. I have not driven a live background agent through it, so treat "agents appear in that list" as strongly indicated rather than measured.

The practical bit. #1296 inserts two helpers immediately above startsBackgroundWork, where you edit DEFERRED_WORK_TOOLS and its comment, so whichever merges second will conflict textually there. Yours is smaller, targeted at a reported defect, and the root-cause half — so it should go first, and I will rebase #1296 onto it. Nothing for you to do about mine.

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.

Background agents are killed at end of turn: startsBackgroundWork() does not match the Agent tool

4 participants