Skip to content

fix(runner): queue work when already running instead of discarding - #36375

Closed
dreampuf wants to merge 3 commits into
anomalyco:devfrom
dreampuf:fix/runner-queue-background-notification
Closed

fix(runner): queue work when already running instead of discarding#36375
dreampuf wants to merge 3 commits into
anomalyco:devfrom
dreampuf:fix/runner-queue-background-notification

Conversation

@dreampuf

@dreampufdreampuf commented Jul 11, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#35066

Type of change

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

What does this PR do?

When a background subagent completes and calls ops.prompt() to notify the parent session, the parent's Runner is often still Running (processing the previous turn). The old ensureRunning discarded the new work item and just awaited the current run's completion — so the notification was lost and the parent session would hang until the user typed "continue".

Root cause:Runner.ensureRunning in packages/opencode/src/effect/runner.ts had this behavior when state was Running:

case"Running":
case"ShellThenRun":
return[awaitDone(st.run.done),st]asconst

It returned the existing run's deferred and threw away the new work argument. So if the parent was busy when the subagent finished, the ops.prompt() call that should have queued a new turn was silently dropped.

Commit dabf2dc ("remove the need for polling from experimental background agents" #29179) removed the old polling-based idle check (resumeWhenIdle), replacing it with a direct ops.prompt() call that races with ensureRunning — which is exactly where the race lives.

Fix: Add a RunningThenRun state to the Runner state machine (mirroring the existing ShellThenRun pattern):

  1. ensureRunning when Running: store the new work as pending, transition to RunningThenRun, return a deferred that resolves when the pending work completes.
  2. finishRun when RunningThenRun: when the current run finishes, start the pending work and transition back to Running.
  3. cancel when RunningThenRun: interrupt both the current run's fiber and fail both deferreds (current + pending), then go idle.

Why this approach over PR #35041: PR #35041 (draft) moves the notification to the session completion path, but still has the same ensureRunning race — it doesn't fix the root cause. If the parent is busy when the notification fires, ensureRunning still discards the work. This PR fixes the root cause in the Runner itself, so all callers of ensureRunning (not just the subagent notification path) benefit.

How did you verify your code works?

  • Updated "concurrent callers share the same run" — now expects calls=2 because the queued run also increments.
  • Renamed "second ensureRunning ignores new work""second ensureRunning queues behind running and executes after" — now asserts the second work actually runs and returns its own result.
  • Added 3 new tests:
    • ensureRunning queues behind running and transitions through RunningThenRun
    • third ensureRunning shares pending in RunningThenRun
    • cancel in RunningThenRun cancels both current and pending
runner.test.ts: 29 pass, 0 fail
prompt.test.ts + task.test.ts: 74 pass, 0 fail, 1 skip
typecheck (all 30 packages): pass

Screenshots / recordings

N/A — no UI changes.

Checklist

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

When a background subagent completes and calls ops.prompt() to notify
the parent session, the parent's Runner is often still Running (processing
the previous turn). The old ensureRunning discarded the new work item and
just awaited the current run's completion, so the notification was lost
and the parent session would hang until the user typed 'continue'.
Add a RunningThenRun state to the Runner state machine. When ensureRunning
is called while Running, the new work is stored as pending and the runner
transitions to RunningThenRun. When the current run finishes, finishRun
starts the pending work and transitions back to Running. Cancel in the
RunningThenRun state interrupts both the current and pending work.
This fixes the root cause of the parent-not-notified bug. The alternative
approach in PR anomalyco#35041 moves the notification to the session completion path
but still races through ensureRunning and can lose the notification when
the parent is busy.
Refs: anomalyco#35066
@github-actionsgithub-actionsBot added needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Jul 11, 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

The following comment was made by an LLM, it may be inaccurate:

Potential related PR found:

This is related but takes a different approach. PR #36375 is the more comprehensive fix.

No other duplicate PRs found.

@github-actionsgithub-actionsBot removed needs:issue needs:compliance This means the issue will auto-close after 2 hours. labels Jul 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

CreatorGhost added a commit to CreatorGhost/TheCode that referenced this pull request Jul 12, 2026
CreatorGhost added a commit to CreatorGhost/TheCode that referenced this pull request Jul 12, 2026
* fix(runner): queue work when already running instead of discarding
Ported from upstream anomalyco#36375.
* test(runner): assert shared pending work runs exactly once
Addresses CodeRabbit nitpick on PR #10.
Cover the queued-work path repaired in anomalyco#36375 (refs anomalyco#35066). When a
background subagent notifies the parent by calling ensureRunning while the
parent turn is still running, the runner must queue that work and cycle
Running -> RunningThenRun -> Running for every arrival instead of dropping
it. Exercise two back-to-back notifications so the runner re-enters
RunningThenRun a second time after the first queued run is promoted.
Without the fix the runner never enters RunningThenRun, so the queued
wakeup is lost: the test fails (waitForState times out) on the pre-fix
runner.ts and passes with the fix in place.
…ion-test
test(opencode): add regression test for lost terminal-iteration wakeup
@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

@dreampuf

Copy link
Copy Markdown
Author

It's difficult to rely on the team seeing their PRs now that there are 1.2K, but I hope it catches your attention eventually.

Sign up for freeto 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.

fix(session): notify parent when subagent sessions finish

2 participants

@dreampuf@scotttesler