Skip to content

fix(core): await child background work before subagent completion - #49305

Open
Ploppy3 wants to merge 13 commits into
anomalyco:v2from
Ploppy3:subagent-quiescence
Open

Ploppy3 wants to merge 13 commits into
anomalyco:v2from
Ploppy3:subagent-quiescence

Conversation

@Ploppy3

@Ploppy3 Ploppy3 commented Sep 16, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #48826

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A subagent that ends a turn while it still has pending background work (shell or nested subagent with background: true) is reported to its parent as completed at that first idle. The subagent job settled as soon as sessions.resume(child) returned, so the parent received the child's early reply; when the pending work later woke the child and it produced its real result, nothing delivered it.

  • SubagentCompletion.finalText runs the child to quiescence before reading its response: while the child still owns pending background notifications that will wake it, it waits for their delivery and resumes again.
  • New Job.awaitBackground(notificationID) resolves when the notification is admitted (its durable marker clears). Waiting for job settlement alone races that asynchronous delivery.
  • Live subagent jobs and restart recovery share the same body, so recovered jobs behave identically.

How did you verify your code works?

cd packages/core && bun test test/job.test.ts test/tool-subagent.test.ts — the new regression test (waits for pending child background work before notifying the parent) fails when run against the un-fixed source (Expected "running", Received "completed") and passes with the fix.

Reproduced the original failure on v2: the parent was notified with SHELL_STARTED while the child's background shell was still running, and the child's later WOKEN_RESULT_COLLECTED stayed stranded in the child session.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Sep 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@Ploppy3

Ploppy3 commented Sep 16, 2026

Copy link
Copy Markdown
Author

The description now follows the PR template section for section and the compliance check passes:

  • ### Issue for this PR — Closes #48826
  • ### Type of change — bug fix checked
  • ### What does this PR do?
  • ### How did you verify your code works?
  • ### Screenshots / recordings — not a UI change
  • ### Checklist — both items checked

One note for maintainers on the remaining needs:issue label: it can't clear itself for PRs targeting v2. The pull_request_target run uses the workflow copy without the v2 body fallback, so it only reads closingIssuesReferences, and GitHub returns 0 closing references for PRs that don't target the default branch. Other v2 PRs with a Closes reference (e.g. #48928, #49295) carry the same label. The issue link is present in the description.

@jadonwb

jadonwb commented Sep 18, 2026

Copy link
Copy Markdown

I also am wanting this behavior fixed, so thanks for the PR. Just to confirm: the behavior is such that the subagent will now 'block' when it has pending work, and not return to parent until fully done?

@Ploppy3

Ploppy3 commented Sep 18, 2026

Copy link
Copy Markdown
Author

Yes, pretty much, the sub agent returns early, it doesn't wait for subagent background task to complete which cause all sort of issues.

Edo771977 pushed a commit to Edo771977/opencode that referenced this pull request Sep 23, 2026
…running

Upstream is fixing, for its own runner, a subagent reported to its parent as
completed while background work it started is still pending
(anomalyco#49305). That fix rests on machinery this fork does not have,
so the question was whether our fan-out has the same gap. It does.

The test drives three levels through the real loop: the session fans out to a
subagent, which starts a nested `task({ background: true })` and ends its turn
while that task hangs. The fan-out reports `nested: COMPLETED` and hands the
caller the child's last words, the parent's turn finishes, and the nested job is
still running — with its result, when it arrives, injected into the child's
session, which nobody reads once the fan-out has answered.

It takes an experimental flag, a subagent permitted to spawn one, and
`subagent_depth: 2` to reach, which is why it is a characterisation test rather
than a fix: it records today's behaviour so a change to it is deliberate and
visible. Whoever closes the gap should expect these assertions to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Gr2KVmmJbKQPntkpbsnP9
Edo771977 pushed a commit to Edo771977/opencode that referenced this pull request Sep 23, 2026
The test added in the previous commit found the gap it went looking for: a
subagent that starts a task with `background: true` and then ends its turn is
reported to its caller as simply completed, while that task keeps running and
delivers its answer into the subagent's own session — which nobody reads once
the caller has its summary. The caller acts on a result that is missing a piece,
with nothing to tell it so.

Both spawn paths now name what was left behind, and where its answer will go, so
a caller can wait for it, read that session, or decide it does not need it. What
they do not do is wait: `background: true` is a request to detach, and honouring
it by blocking the caller would make the option meaningless. Upstream takes the
other road for its own runner in anomalyco#49305, running a subagent to
quiescence before reading its answer; that rests on machinery this fork does not
have, and it is a change of meaning rather than of wording, so it wants its own
decision.

The characterisation test becomes a specification: the fan-out still reports the
subtask completed, and now has to say what it left running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Gr2KVmmJbKQPntkpbsnP9

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants