Skip to content

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

Open
dreampuf wants to merge 1 commit into
anomalyco:devfrom
dreampuf:fix/runner-queue-background-notification
Open

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

Conversation

@dreampuf

@dreampuf dreampuf 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] as const

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-actions github-actions Bot 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-actions github-actions Bot 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. 👍

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.

fix(session): notify parent when subagent sessions finish

1 participant