Run stays "starting" when its thread's recovery state can't be read#16441

opened25d agobyYyaacovcorcos

What happens

When provider-turn.start runs, ProviderTurnStartService reads the thread's runtime recovery state (getRuntimeRecoveryProjection) before it opens a provider session. If that read fails on every attempt, the effect worker gives up and marks the effect failed, but the run stays starting. No error item is written, so the thread shows a turn that never starts and never fails.

Session-open and thread-load failures do not have this problem: on the last attempt they fail the run with an error item (settleStartFailure).

Reproduction

In ProviderTurnStartService.test.ts, make getRuntimeRecoveryProjection fail with a ProjectionStoreReadError and call start({ threadId, runId }) without willRetry (the last attempt). start returns the error and writes nothing; the run is still starting. A real trigger is a stored turn item whose payload no longer decodes: every attempt fails the same way.

Expected

On the last attempt, the run, attempt and root node fail with an error item that names the read failure, as they do for a session-open failure. Earlier attempts keep returning the error so the worker retries.

Note on current design

The existing test "does not commit running state when inherited background routing cannot be read" asserts that nothing is written on this path, and the code comment above loadFromProvider keeps store failures as typed errors. The proposed direction is to treat this one read, which runs before any provider work, like a session-open failure on the last attempt.

Environment

macOS, current main 9bd1d8009a.

A fix with focused tests is ready on a branch: it settles the run on the last attempt and updates the existing test accordingly. I'll open it as a PR if this direction is agreed.

Found while porting fixes from a downstream fork (Scient). Investigated with Claude Opus 5.5 in Claude Code.

Reproduced on Linux with current main a12d6ed942, using vp run dev --no-seed and the web client. A normal Codex turn completed. With a malformed running turn-item payload injected when the next message committed, provider-turn.start exhausted all five attempts; the effect became failed, the run stayed starting, and no error item was written.

Runtime transcript. If the malformed item exists before server startup, recovery fails earlier and the server exits; the reproduction above injects it after startup.