Settle pending turns when providers exit before turn start - #1234
Merged
ymichael merged 3 commits intoAug 13, 2026
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a reconciliation gap where a provider process can exit after a client turn request activates a server thread but before the provider emits turn/started, leaving the live command pending until timeout. The change plumbs the runtime’s pending-turn-start state into the provider-exit snapshot and synthesizes a thread-scoped terminal error on unexpected provider exits during that window.
Changes:
- Extend the runtime provider-exit per-thread snapshot with
pendingTurnStartand plumb it throughcaptureThreadExitState. - In host-daemon reconciliation for unexpected provider exits, emit a thread-scoped
system/error(provider_process_exited) whenpendingTurnStartis true and there is no active turn. - Add runtime and host-daemon regression tests covering the pre-
turn/startedprovider-exit window and update existing fixtures to include the new snapshot field.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/agent-runtime/src/types.ts | Adds pendingTurnStart to the per-thread provider-exit snapshot type. |
| packages/agent-runtime/src/runtime.ts | Captures pendingTurnStart from the runtime’s pending-turn-start tracking set in exit snapshots. |
| packages/agent-runtime/src/runtime.process-lifecycle.test.ts | Adds coverage asserting exit snapshots include pendingTurnStart: true when the provider dies mid turn-start. |
| apps/host-daemon/src/runtime-manager.ts | Synthesizes a thread-scoped terminal error when the provider exits unexpectedly before turn/started. |
| apps/host-daemon/src/runtime-manager.test.ts | Adds regression test for the pre-start crash window and updates existing exit fixtures. |
| apps/host-daemon/test/command/thread-dispatch.test.ts | Updates process-exit fixture to include pendingTurnStart. |
| apps/host-daemon/src/app.test.ts | Updates onProcessExit test fixtures to include pendingTurnStart. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ymichael
added a commit
that referenced
this pull request
Aug 13, 2026
Fixes #1431. The original diagnosis and narrow Claude Code fix are Jerrison's work. This branch preserves that commit and credits Jerrison on the generalized change. ## Problem Claude Code handles `/clear` locally and returns a successful zero-work result: there is no assistant message, stream event, or tool call to start a bb turn. The result handler previously required `state.currentTurnId`, so it emitted no `turn/completed`. The thread remained `active`, `bb thread wait --status idle` hung, and accepted input queued behind the abandoned turn could not drain. The underlying lifecycle problem is broader than Claude's `/clear`: a provider can accept a prompt and then report a terminal signal before emitting any of the ordinary activity that starts a bb turn. ## Contract Provider terminal translators now resolve the turn through one shared rule: 1. Complete the currently open turn when one exists. 2. Otherwise, start and complete a synthetic turn only while accepted input is still pending. 3. Emit nothing when neither condition holds. The pending accepted-input queue is the ownership proof that keeps this narrow. `onTurnStart` drains it, so a late terminal signal cannot open a second empty turn after the real turn has already closed. ## Provider coverage - **Claude Code:** a terminal `result` can settle accepted input even when no earlier SDK message started the turn. This covers the `/clear` regression. - **ACP:** `turn/completed` follows the same terminal-turn rule. - **Pi:** `agent_end` follows the same rule, and the bridge reports when `session.prompt()` settles without producing an SDK event. Codex is unchanged because its native lifecycle already settles this shape. The synthetic lifecycle also drives the existing server completion path, so a queued message is sent after the zero-work turn completes instead of remaining stuck. ## Deliberate non-goals - Process-detach recovery remains separate and is covered by #1234. - This does not add a general stuck-turn watchdog or reconciler. - This does not introduce a provider-independent `/clear` command or a new first-class clear timeline event; it fixes settlement when a provider handles a prompt without ordinary turn activity. ## Wire compatibility `HOST_DAEMON_PROTOCOL_VERSION` is bumped to **115** because Pi now sends a prompt-settled bridge event that can change host-daemon event output. ## Verification - `@bb/agent-runtime`: 941 tests passed across 46 files. - `@bb/server`: 1,470 tests passed across 162 files. - `@bb/host-daemon-contract`: 49 tests passed across 3 files. - Typechecks passed for `@bb/agent-runtime`, `@bb/server`, `@bb/host-daemon`, and `@bb/host-daemon-contract` through Turbo. - Live reproduction against the source-built app: ```sh bb thread tell <id> "/clear" bb thread wait <id> --status idle --timeout 60 ``` The thread reached `idle` in 1.6 seconds, and its accepted-message queue was empty afterward. --------- Co-authored-by: Jerrison Li <1813092+jerrison@users.noreply.github.com> Co-authored-by: Michael Yong <wrong92@gmail.com>
Exercise the actual lifecycle gap: turn/start has succeeded, but no provider turn event has arrived before the process exits. Clarify that the process-exit snapshot represents pending starts as well as active turns. Co-authored-by: Jonathan Borgwing <jon@learnvig.com>
ymichael
force-pushed
the
fix/provider-exit-before-turn-start
branch
from
August 13, 2026 05:23
0f574f7 to
8b98bf6
Compare
ymichael
added a commit
that referenced
this pull request
Aug 17, 2026
…gences - 87 protocol bumps (26->121), not 92; turn-settlement fixes are #1196/#1234/#1321/#1432 - #75 is a pre-GitHub ticket id; cite commit 1a5620b - ProviderAdapter has 18 members, not 16; corrected per-provider line counts - The outbound vocabulary is a shared 7-method core with real divergences (acp lacks fork; codex maps stop/discard/compact/skills to different methods and has 4 methods no bb bridge speaks) - phase 1 must pick canonical mappings; codex bridge is a mapping layer, not a passthrough - Canonical PendingInteractionPayload union lives in @bb/domain, not shared/pending-interaction-normalization.ts (codex-only helper) - classify split is claude-vs-rest; normalizeExecutionOptions is claude-only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a user submits a turn, the server persists
client/turn/requestedandmarks the thread active before the provider emits
turn/started.A provider can acknowledge the
turn/startRPC and then exit before emittingthat first lifecycle event. At that point the command has succeeded, so there
is no command failure to reconcile, but
activeTurnIdis still null. Existingprovider-exit handling treated the thread like an idle resident session and
emitted nothing, leaving it visibly Working with no provider process alive.
Lifecycle contract
The runtime now includes its existing
pendingTurnStartstate in the finalper-thread snapshot captured before process-exit cleanup. On an unexpected
provider exit, the host daemon applies these rules:
provider_process_exitederror.The server already treats
provider_process_exitedasrun.failed, so thethread moves from
activetoerror, pending interactions are interrupted,and parent-thread notification behavior remains consistent with other failed
turns.
Relationship to #1432
#1432 settles prompts when a provider returns a terminal signal without first
starting a turn. This PR covers the complementary case where the provider
process disappears and no terminal signal can arrive.
Safety and non-goals
observed with Claude Code.
timeout, watchdog, or general stuck-turn reconciler.
pendingTurnStartis set before dispatch and cleared byturn/started,turn/completed, a terminal provider error, command failure cleanup, orthread cleanup. This distinguishes the vulnerable window from an idle
resident session.
Wire compatibility
HOST_DAEMON_PROTOCOL_VERSIONis bumped to 116 because an updated daemoncan now emit a failure event for a pre-
turn/startedprocess exit.Verification
turn/startsucceeds, the provider exitsbefore
turn/started, and the exit snapshot retainspendingTurnStart: true.provider_process_exitedtransitions an activethread to
errorand preserves parent-notification behavior.@bb/agent-runtime: 941 tests passed across 46 files.@bb/host-daemon: 548 tests passed across 46 files.@bb/host-daemon-contract: 49 tests passed across 3 files.@bb/server: 1,471 tests passed across 162 files.git diff --checkpassed.Jonathan Borgwing authored the original diagnosis and implementation. The
rebased branch preserves both original commits and credits Jonathan on the
post-ack regression hardening commit.