Files
PaperClipAI/packages
DottaandClaude Opus 5.5 d1ee25d0f7 fix(runner): fail a pending Codex turn start at once with the runner's own terminal failure (#19722)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane starts each Codex turn through runnerd and waits
for the turn's start event (`#startTurnWork` in
`runnerd-codex-transport.ts`)
> - In restart storm 5, runnerd failed an accepted continuation turn
before it started (`turn_binding_mismatch`, fixed at its source in
#19718)
> - The transport held that terminal for the pending start and waited
the full 30 s bound
> - It then reported "runnerd did not report the provider turn
identity", which hid the real cause
> - This pull request makes a pending start fail at once with the
runner's own terminal failure
> - The benefit is a faster failure that names its real cause, so
recovery and operators act on the correct fact

## Linked Issues or Issue Description

Refs #19718, #19699, #19608

**What happened?**

Restart storm 5 (seed 7106), task t01 (Codex). Runnerd accepted the
restart continuation turn. About 0.5 s later it committed a
`harness.diagnostic`, a non-recoverable `turn.failed`
(`turn_binding_mismatch`) and a failed `run.terminal`. The transport's
event pump holds a turn terminal while a turn start is pending, so the
start loop never saw it. The start waited 30 s, the #19608 adoption
check found no active turn, and the run failed with "runnerd did not
report the provider turn identity".

**Expected behavior**

When runnerd reports a final failure of the pending turn (a
non-recoverable `turn.failed` or a failed `run.terminal`) before the
turn starts, the start fails at once. The error names the runner's code
and message.

**Steps to reproduce**

1. Use fake Codex with `--foreign-usage-before-turn-started
--turn-started-delay-ms 20000`.
2. Start a turn through the runnerd Codex transport.
3. Before this change, `turn/start` rejects after the full bound with
"runnerd did not report the provider turn identity".

## What Changed

- `runnerd-codex-transport.ts`: new `runnerdPendingTurnStartFailure()`.
It finds a non-recoverable `turn.failed` or a failed `run.terminal`
among the terminals held for the pending start and the committed events
after the cursor.
- `#startTurnWork` checks it while it waits for `turn/started`, before
the adoption snapshot, and in the post-adoption wait. It throws `runnerd
failed the provider turn before it started: <code>: <message>`.
- The check runs only when the turn has not started. A recoverable
terminal does not trigger it. The #19608 late-start adoption path is
unchanged.
- Fake Codex: new `--foreign-usage-before-turn-started` flag. It sends
usage for a turn the runner does not own right after it accepts a turn.
This makes runnerd fail the turn with `turn_binding_mismatch` before it
starts.
- New test in `runnerd-codex-transport-deadlines.test.ts` with real
runnerd and fake Codex.

## Verification

- Red before the fix: `npx vitest run
src/live/runnerd-codex-transport-deadlines.test.ts` → the new test
failed after 16.4 s with "runnerd did not report the provider turn
identity".
- Green after the fix: the start rejects in under 5 s with
`turn_binding_mismatch`, and fake Codex saw exactly one `turn/start`.
- The #19608 tests ("adopts a durably accepted turn whose start
notification outlives the turn-start bound" and "does not adopt an
accepted turn the runner no longer reports active") still pass.
- `npx vitest run src/live/runnerd-codex-transport`: 9 files, 399 tests
pass.
- `pnpm run typecheck` in `packages/paperclip-runner` passes. `cargo
test --bin fake-codex-app-server` passes. `cargo fmt` is clean.
- `pnpm verify` exits 2 (broad input: the fake Codex binary). Required
CI supplies the broad coverage.

## Risks

- Low risk. The new failure fires only when runnerd itself already
reported a final failure and no turn start was observed. In that state
the start could not succeed, and before this change it failed after the
bound anyway.
- If a provider sends a non-recoverable terminal before its own turn
start for a turn that later starts, the start now fails earlier. Codex
sends `turn/started` before its terminal, and runnerd's terminal for a
protocol failure is final, so I do not expect this.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file editing). Default context window and
reasoning mode.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:48:29 -06:00
..