Skip to content

Settle pending turns when providers exit before turn start - #1234

Merged
ymichael merged 3 commits into
get-bb:mainfrom
DevVig:fix/provider-exit-before-turn-start
Aug 13, 2026
Merged

Settle pending turns when providers exit before turn start#1234
ymichael merged 3 commits into
get-bb:mainfrom
DevVig:fix/provider-exit-before-turn-start

Conversation

@DevVig

@DevVig DevVig commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

When a user submits a turn, the server persists client/turn/requested and
marks the thread active before the provider emits turn/started.

A provider can acknowledge the turn/start RPC and then exit before emitting
that first lifecycle event. At that point the command has succeeded, so there
is no command failure to reconcile, but activeTurnId is still null. Existing
provider-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 pendingTurnStart state in the final
per-thread snapshot captured before process-exit cleanup. On an unexpected
provider exit, the host daemon applies these rules:

  1. An active turn follows the existing turn-scoped completion and error path.
  2. A pending turn start with no active turn emits the existing thread-scoped
    provider_process_exited error.
  3. An idle session emits nothing.
  4. An expected process exit emits nothing.

The server already treats provider_process_exited as run.failed, so the
thread moves from active to error, 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

  • The behavior is provider-independent, although the reported failure was
    observed with Claude Code.
  • It relies on an actual unexpected process exit; it does not add an inactivity
    timeout, watchdog, or general stuck-turn reconciler.
  • pendingTurnStart is set before dispatch and cleared by turn/started,
    turn/completed, a terminal provider error, command failure cleanup, or
    thread cleanup. This distinguishes the vulnerable window from an idle
    resident session.
  • No persisted event schema or database model changes are required.

Wire compatibility

HOST_DAEMON_PROTOCOL_VERSION is bumped to 116 because an updated daemon
can now emit a failure event for a pre-turn/started process exit.

Verification

  • Real child-process regression: turn/start succeeds, the provider exits
    before turn/started, and the exit snapshot retains
    pendingTurnStart: true.
  • Host-daemon event construction and event transport are covered.
  • Server ingestion confirms provider_process_exited transitions an active
    thread to error and 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.
  • Turbo typechecks passed for all four packages above.
  • Prettier and git diff --check passed.

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.

Copilot AI lite review requested due to automatic review settings August 9, 2026 18:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 pendingTurnStart and plumb it through captureThreadExitState.
  • In host-daemon reconciliation for unexpected provider exits, emit a thread-scoped system/error (provider_process_exited) when pendingTurnStart is true and there is no active turn.
  • Add runtime and host-daemon regression tests covering the pre-turn/started provider-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 ymichael self-assigned this Aug 13, 2026
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>
DevVig and others added 3 commits August 12, 2026 22:17
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
ymichael force-pushed the fix/provider-exit-before-turn-start branch from 0f574f7 to 8b98bf6 Compare August 13, 2026 05:23
@ymichael ymichael changed the title fix: settle turns when provider exits before start Settle pending turns when providers exit before turn start Aug 13, 2026
@ymichael
ymichael merged commit 8fb1aa0 into get-bb:main Aug 13, 2026
9 checks passed
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>
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.

3 participants