Skip to content

fix(session): interrupt a running prompt when a new one is submitted - #41748

Open
ShinjukuZhu wants to merge 3 commits into
anomalyco:devfrom
ShinjukuZhu:fix/prompt-interrupts-running-tool
Open

fix(session): interrupt a running prompt when a new one is submitted#41748
ShinjukuZhu wants to merge 3 commits into
anomalyco:devfrom
ShinjukuZhu:fix/prompt-interrupts-running-tool

Conversation

@ShinjukuZhu

@ShinjukuZhuShinjukuZhu commented Aug 11, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#41753

Type of change

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

What does this PR do?

Problem. When the assistant is mid-task and calls the bash tool with a long-running command (e.g. sleep 30), submitting a new prompt is not handled until the command finishes. The new prompt is saved but no response arrives until the running tool exits — the user has to wait for the whole command before their prompt is answered.

Root cause.Runner.ensureRunning in packages/opencode/src/effect/runner.ts treats an active run as a single-flight/join point:

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

A second ensureRunning call (the new prompt's loop) simply awaits the current run's completion and drops the new work. The new prompt is only eventually answered because the current run loop happens to pick up the newly-saved user message after the tool finishes.

Fix. When a new run is requested while one is active, interrupt the current run and start the new work immediately so the new prompt is answered promptly and the current tool (e.g. a shell command) is cancelled. The interrupted caller resolves through onInterrupt (for prompt loops that is lastAssistant). The fiber interrupt runs on a separate fiber because the interrupted run's finishRun acquires the same SynchronizedRef — interrupting while holding the lock would deadlock.

Note on approach vs. #36375: that PR queues the new work behind the running run, which fixes dropped work but still makes a new prompt wait for a long-running tool. This PR interrupts instead, which is what the linked issue asks for. The two are mutually exclusive implementations of the Running case; if the maintainers prefer queueing semantics over interrupting, this PR can be reworked accordingly.

How did you verify your code works?

  • Added a regression test (a new prompt takes over a running bash tool instead of waiting for it) that starts a first prompt loop executing sleep 30 via the bash tool, then submits a second prompt. Before the fix the second loop times out waiting for the sleep; after the fix it completes immediately and the interrupted bash tool is cancelled.
  • Updated the Runner tests that documented the old join/drop semantics to the new interrupt-and-replace behavior.
  • packages/opencode: bun test test/effect/runner.test.ts test/session/ pass (one pre-existing glob tool keeps instance context timeout fails on a clean tree in this environment as well, unrelated to this change).
  • bun typecheck and oxlint pass for packages/opencode.

Screenshots / recordings

N/A — no UI changes.

Checklist

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

When the user submits a new prompt while a run is active, the Runner's
ensureRunning simply awaited the current run's completion and dropped the
new work. If the assistant was blocked on a long-running tool like
`sleep`, the new prompt was not answered until the tool finished.
Now ensureRunning interrupts the active run and starts the new work, so a
new prompt is handled promptly and the current tool (e.g. a shell command)
is cancelled. The interrupted caller still resolves through onInterrupt.
Adds a regression test that submits a second prompt while a bash `sleep`
is running and asserts the second prompt completes immediately.
@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-actionsgithub-actionsBot added the needs:compliance This means the issue will auto-close after 2 hours. label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

Based on my search, I found one potentially related PR that's worth noting:

Related PR

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

This PR appears to address a similar issue with the Runner handling multiple work submissions. However, it focuses on queuing work rather than interrupting the current run like PR #41748 does. The approaches are different:

These represent different strategies for handling concurrent prompts in a session runner.

Other related PRs (different scope):

Conclusion: PR #41748 appears to be addressing a distinct improvement to the session runner behavior that's not covered by existing open PRs.

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

Copy link
Copy Markdown
Contributor

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

const old = st.run
yield* Deferred.fail(old.done, new Cancelled()).pipe(Effect.asVoid)
const done = yield* Deferred.make<A, E | Cancelled>()
const run = yield* startRun(work, done)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we prevent the old run from publishing status after the replacement starts? Since startRun happens before the old fiber is interrupted, its interruption handler can publish idle after the replacement publishes busy, and CLI/TUI consumers may treat the active replacement as complete. Is this a concern?

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.

Good point. I addressed this by making the replacement work wait until the old run has finished interruption cleanup. The interruption still runs on an independent fiber so finishRun can acquire the SynchronizedRef without deadlocking. In addition, finishRun keeps its run-ID guard, so a replaced run cannot publish idle for the active replacement. I added regression coverage that holds the old cleanup open and verifies that no stale idle is published while the replacement is active.

// `finishRun` acquires this same ref, so interrupting it while holding
// the lock would deadlock.
const old = st.run
yield* Deferred.fail(old.done, new Cancelled()).pipe(Effect.asVoid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we defer failing old.done until the old fiber has completed interruption cleanup? Failing it here wakes onInterrupt (lastAssistant) before interruption begins, so a synchronous prompt caller can receive an assistant message whose tool is still running and whose completion/error metadata is not finalized. Is this a concern?

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.

Yes, this was a real concern. I removed the eager failure of old.done; it is now completed by the old fiber through the existing finishRun path after interruption cleanup has finished. This prevents onInterrupt / lastAssistant from returning while the tool and its metadata are still being finalized. I also added regression coverage for this ordering, successive replacements, and cancellation while a replacement is waiting.

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

New prompt waits for a running bash tool to finish before it is answered

2 participants

@ShinjukuZhu@bvolpato