Uh oh!
There was an error while loading. Please reload this page.
fix(runner): queue work when already running instead of discarding - #36375
fix(runner): queue work when already running instead of discarding#36375dreampuf wants to merge 3 commits into
Conversation
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
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
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. |
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
Ported from upstream anomalyco#36375.
* 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
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:
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
commented
Aug 12, 2026
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. |
Issue for this PR
Closes#35066
Type of change
What does this PR do?
When a background subagent completes and calls
ops.prompt()to notify the parent session, the parent'sRunneris often stillRunning(processing the previous turn). The oldensureRunningdiscarded 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.ensureRunninginpackages/opencode/src/effect/runner.tshad this behavior when state wasRunning:It returned the existing run's deferred and threw away the new
workargument. So if the parent was busy when the subagent finished, theops.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 directops.prompt()call that races withensureRunning— which is exactly where the race lives.Fix: Add a
RunningThenRunstate to the Runner state machine (mirroring the existingShellThenRunpattern):ensureRunningwhenRunning: store the new work aspending, transition toRunningThenRun, return a deferred that resolves when the pending work completes.finishRunwhenRunningThenRun: when the current run finishes, start the pending work and transition back toRunning.cancelwhenRunningThenRun: 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
ensureRunningrace — it doesn't fix the root cause. If the parent is busy when the notification fires,ensureRunningstill discards the work. This PR fixes the root cause in the Runner itself, so all callers ofensureRunning(not just the subagent notification path) benefit.How did you verify your code works?
"concurrent callers share the same run"— now expectscalls=2because the queued run also increments."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.ensureRunning queues behind running and transitions through RunningThenRunthird ensureRunning shares pending in RunningThenRuncancel in RunningThenRun cancels both current and pendingScreenshots / recordings
N/A — no UI changes.
Checklist