Skip to content

fix(session): answer prompts that land while the previous turn is finishing - #207

Merged
filipeforattini merged 1 commit into
mainfrom
drain-boundary-prompt
Sep 11, 2026
Merged

fix(session): answer prompts that land while the previous turn is finishing#207
filipeforattini merged 1 commit into
mainfrom
drain-boundary-prompt

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Sep 11, 2026

Copy link
Copy Markdown

Problem

Runner.ensureRunning in the Running state discarded the incoming work and only awaited the current run. SessionPrompt.prompt writes the user message straight into history and then calls loop(), so a prompt that landed after the loop's final history read but before finishRun flipped the state to Idle (the window includes lastAssistant(), compaction.discard, and the Turn.Ended hooks/events) was persisted but never answered. The session went idle with an unanswered trailing user message until the next prompt.

Fix

Runner-level, per CONTEXT.md ("the runner promotes one queued prompt when the Session would otherwise become idle, then reevaluates continuation before promoting another"):

  • Running gains an optional pending handle. ensureRunning while running records the arrival (latest work thunk, one shared done deferred) and returns an await on that deferred.
  • finishRun starts the pending run instead of going idle when the finishing run was not interrupted. onIdle is not fired between the two runs, so status does not flicker.
  • A cancel already leaves Running before the interrupted fiber exits, so it never restarts; it now also fails the pending deferred so callers that arrived during the run resolve through onInterrupt. A run interrupted any other way (scope teardown) drops the pending work rather than starting work in a dying scope.

Test

  • test/session/prompt.test.ts: "answers a prompt that lands while the previous turn is finishing" holds the first turn open in a Turn.Ended hook, sends a second prompt in that window, and asserts it gets its own reply, the history ends with an assistant message answering the last user message, the session is idle, and the fake provider has no unconsumed reply. Fails before the fix (second prompt resolved with the first turn's reply), passes after.
  • test/session/prompt.test.ts: "cancel while a turn is finishing does not start another run" cancels inside the same window and asserts exactly one Turn.Ended, the second reply is never requested, and the session is idle.
  • test/effect/runner.test.ts: three new cases (follow-up run before idle, callers sharing one follow-up run, cancel dropping pending work) fail before and pass after; two existing cases that encoded the old "silently join" semantics are updated to the new contract.
  • test/session directory: 514 pass, 0 fail. bun run typecheck clean.

https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…ishing

A prompt that arrived after the running turn's last look at history but
before the runner flipped the session to idle was persisted and never
answered: `ensureRunning` in the `Running` state discarded the incoming
work and only awaited the current run, so the session went idle with an
unanswered trailing user message until the next prompt.

The runner now records work that arrives during a run as a pending
handle and, when the current run finishes, starts one more run instead
of transitioning to idle. Callers that arrived during the run resolve
when that follow-up run completes. A cancel leaves `Running` before the
interrupted fiber exits, so it never restarts anything, and it fails the
pending handle so those callers resolve through `onInterrupt` too.

Claude-Session: https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS
@filipeforattini
filipeforattini merged commit 15a6d1f into main Sep 11, 2026
12 of 15 checks passed
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.

1 participant