Files
PaperClipAI/packages
DottaandClaude Opus 5.5 42f6b3a870 fix(runner): keep a restart-lost Codex turn's late usage from failing its continuation (#19718)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex runs continue a turn lost to a control-plane restart
once, in the same conversation (#19699)
> - In restart storm 5, a Codex run crossed one graceful restart. The
continuation started, but the task stopped and needed a Retry
> - The runner log shows the cause. The resumed Codex thread sent the
usage of the lost turn after it accepted the continuation turn. The
runner rejected that usage as `turn_binding_mismatch` and failed the run
> - Recovery marks the lost turn as settled in the durable ledger. It
does not tell the live Codex provider, so the provider treats the old
turn's usage as a foreign event
> - This pull request records the lost turn as settled in the live
provider too
> - The benefit is that a restart-lost Codex turn continues without a
Retry

## Linked Issues or Issue Description

Refs #19699, #19608, #19501, #15297

**What happened?**

Restart storm 5 (seed 7106). Task t01 (Codex, B5) crossed one K2 SIGTERM
restart. Recovery resumed the dead runner. Runnerd restored the
conversation and recorded `turn.failed` with
`provider_turn_lost_on_restore` (recoverable). The control plane started
the continuation (#19699). Runnerd accepted it as a new provider turn.
About 0.5 s later, Codex sent `thread/tokenUsage/updated` for the lost
turn id. The runner's durable state records these events:

- `turn.accepted` for the new turn `01a1b3e2-…`
- `harness.diagnostic` `turn_binding_mismatch`, method
`thread/tokenUsage/updated`, `expectedTurnId` = new turn,
`receivedTurnId` = lost turn `01a1b3e0-…`
- `turn.failed` (`recoverable: false`) and `run.terminal` failed

The control plane did not see `turn/started` and failed after 30 s with
"runnerd did not report the provider turn identity". The task needed a
manual Retry.

**Expected behavior**

Usage for the lost turn is history. The runner keeps it as a resume
usage snapshot. The continuation runs, and the task continues without a
Retry.

**Steps to reproduce**

1. Start a Codex turn through the durable runner.
2. Kill the runner while the turn is active. Restore so that the
provider reports no active turn (`provider_turn_lost_on_restore`).
3. Start the continuation turn. Let the provider send
`thread/tokenUsage/updated` for the lost turn after it accepts the new
turn.
4. Before this change, the runner fails the continuation with
`turn_binding_mismatch`.

## What Changed

- `provider_backend.rs`: when recovery finds that the previous active
turn ended while the runner was offline, it now also restores that turn
id into the live provider's settled-turn set. The durable ledger already
settled it. The existing settled-turn path then turns its late usage
into `paperclip/resumeUsageSnapshot` and does not bill its `last` usage
to the run.
- The code skips this step when the settled-turn epoch needs a rollover,
the same as the durable ledger.
- Fake Codex: new `--stale-usage-after-turn-start` flag. With durable
turn ids, it sends usage for the previous turn right after it accepts
the next turn, before `turn/started`. This matches the live Codex order.
- New test
`restart_continuation_ignores_late_usage_for_the_turn_lost_on_restore`
in `tests/codex_provider.rs`. It uses the real durable Codex executor
with fake Codex.

## Verification

- Red before the fix: `cargo test -p paperclip-runner-core --test
codex_provider restart_continuation_ignores_late_usage` failed with the
same diagnostic as the live run (`turn_binding_mismatch`, method
`thread/tokenUsage/updated`, `receivedTurnId: provider-turn-1`,
`expectedTurnId: provider-turn-2`).
- Green after the fix: the same test passes. The continuation emits
`turn.started` for `provider-turn-2`, there is no `turn.failed` or
`run.terminal`, and fake Codex saw exactly two `turn/start` calls (the
original and one continuation).
- `cargo test -p paperclip-runner-core`: all suites pass.
`acpx_park_readiness::hosted_park_acpx_preserves_late_goals_and_rejects_foreign_or_busy_observations`
failed once and passed on rerun. This change does not touch ACPX.
- `cargo fmt --check` is clean.
- Not run: the live restart-chaos replay of
`w-20261107T023921-s7106/t01`. The local harness needs provider
credentials that were not available to this session.

## Risks

- Low risk. The change adds one already-ended turn id to the live
provider's settled set. That set is only used to ignore stale events and
to reject reuse of a settled id. So a provider that later reports the
lost turn id as a new active turn is now rejected, as the durable ledger
already does.
- The change does not address a second, separate problem: after the
runner failed the continuation, the control-plane transport waited the
full 30 s turn-start bound and reported "runnerd did not report the
provider turn identity" instead of the runner's `turn_binding_mismatch`.
That message only applies when such a failure occurs. This PR removes
this specific trigger.

## 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:17:38 -06:00
..