Files
PaperClipAI/packages
DottaandClaude Opus 5.5 ece09c156a fix(runner): keep an accepted result when ACPX reports its turn lost on restore (#19691)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Native Codex, OpenCode, and ACPX runs survive control-plane
restarts: runnerd restores the conversation, and the control plane
continues the task from its checkpoint
> - #19658 made recovery keep a durable result when a Codex restore
loses the turn that proposed it. It covered the recovery read path only
> - ACPX reports the lost turn differently. After the restored
conversation reopens, a live `turn.failed` arrives with
`provider_turn_lost_on_restore`
> - That live terminal still turned an accepted `done` result into a
failed run. The run ended `failed` (`adapter_failed`), and the finished
work needed a Retry
> - This pull request moves the rule into the shared terminal mapping. A
restore-lost turn with a held result for that exact turn settles as
completed, whichever path reports it
> - The benefit is that ACPX and OpenCode tasks that already finished
keep their accepted result after a restart, as Codex tasks now do

## Linked Issues or Issue Description

Refs #19658, #19525, #19501, #19539

**What happened?**
In a restart chaos wave, an ACPX Claude task (workload A1) wrote its
brief and proposed a `done` result (`run.result.proposed`, then a
completed `paperclip_finish` tool call). Then a K1 and a K6 crash loop
hit the control plane, and `resume_dead_runner` ran at controller
generations 2 and 3. The restored ACPX conversation emitted
`turn.failed` with `provider_turn_lost_on_restore` (`recoverable: true`)
for the checkpointed turn. The control plane then emitted
`run.result.accepted` with the `done` result, but `run.terminal` came
back `runTerminalState: failed` / `turnTerminalState: failed` /
`reportedWorkDisposition: done`. The run ended `failed` /
`adapter_failed`, "Native session failed".

**Steps to reproduce**
1. Start a native ACPX (or OpenCode) task through runnerd.
2. Let the agent call `paperclip_finish`, and let the control plane
persist the proposal.
3. Kill the control plane and runner before the turn ends.
4. Let restart recovery resume the run. The restored runner reports the
turn lost on restore as a live terminal.

**Expected behavior**
Recovery finalizes the accepted result, and the run succeeds.

**Actual behavior**
ACPX and OpenCode use the same Codex harness session as Codex
(`codex-native-backend.ts`). Recovery restored the held result for the
turn from the durable proposal. But the live `turn/completed`
notification with `status: failed` and `provider_turn_lost_on_restore`
went through `mapTerminalTurn`, which emitted `turn.failed`. The backend
maps `turn.failed` to `runTerminalState: failed`. #19658 handled only
the `thread/read` reconciliation path.

## What Changed

- `codex-session-terminal.ts`: a new
`settleRestoreLostTurnWithHeldResult` helper. When a terminal is
`failed` with `provider_turn_lost_on_restore` and the session holds the
semantic result that this exact turn proposed, the turn settles as
`completed`. `mapTerminalTurn` applies it first, so live notifications
and recovery reads follow one rule.
- `codex-session-terminal.ts`: the helper also covers
`acpx_active_turn_recovery_closed`. Runnerd's ACPX backend
(`acpx_provider_backend.rs`, `restore_session_if_needed`) emits that
code instead of restoring the conversation when its own state already
recorded the accepted result. It is the same restart loss, with the
result already accepted. #19525 already keeps accepted results when
close cannot prove suspension, and session close still handles
`provider_exit_unconfirmed`.
- `codex-harness-session.ts`: `reconcile()` keeps only the missing-turn
case from #19658. The restore-lost read case and the prior-terminal
check now use the shared helper.
- `doc/execution-semantics.md`: the restart paragraph now states that
the rule covers Codex, OpenCode, and ACPX, and both report paths.

## Verification

- Red before the fix, with the real Codex native backend
(`createCodexNativeSessionBackend`) and a fake transport through
`executeNativeSession`: `pnpm --filter @paperclipai/paperclip-runner
exec vitest run src/native-session-runtime.test.ts -t "journaled
completion proposal"`. The new `restore-lost live` scenario failed.
Green after the fix: the run completes with the journaled result on
`turn-1` and a succeeded terminal, and no `turn/start` is sent. The
`restore-lost read` scenario passes on both (#19658 already covered it).
- Driver red/green:
`src/drivers/codex/codex-app-server-driver.recovery.test.ts -t "live
restore-lost"`. With a held result, the live lost terminal was
`turn.failed` before the fix and is `turn.completed` after it. With no
held result, it stays `turn.failed` with the lost cause, so the #19501
continuation is unchanged.
- The driver case was extended to `acpx_active_turn_recovery_closed`
(red with a held result before the extension, then green). It was found
in a live replay where the run ended `failed`/`adapter_failed` with an
accepted `done` result.
- Suites: `src/drivers/codex/` and `src/native-session-runtime.test.ts`
(580 tests), plus `src/backends/` and
`src/live/runnerd-restart-continuation.test.ts`, pass.
`typecheck:typescript` passes.
- Live chaos replays (ACPX Claude): results and a separate new signature
are in a comment below.

## Risks

- The rule needs a held result whose turn id equals the lost turn.
Recovery restores that result only from the durable
`run.result.proposed` for the checkpointed turn, so a result for another
turn does not settle it.
- A restore-lost turn without a held result keeps its failure and the
bounded same-conversation continuation.
- Real provider failures and other terminal states are unchanged.
- No schema, profile, pack contract, or database changes.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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:07:20 -06:00
..