Skip to content

fix(server): "Edit from here" works on a message after a stopped run - #13883

Open
juliusmarminge wants to merge 2 commits into
t3code/codex-turn-mappingfrom
v2/rollback-stopped-runs
Open

juliusmarminge wants to merge 2 commits into
t3code/codex-turn-mappingfrom
v2/rollback-stopped-runs

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

"Edit from here" on a message that follows an interrupted or cancelled run failed client-side with "Rollback target run ordinal N is unavailable". Only completed runs captured a checkpoint (appRunOrdinal), so the client had nothing to target for the stopped run.

What changed

  • Stopped runs capture a checkpoint. RunExecutionService now enqueues the checkpoint.capture effect for interrupted and cancelled terminals as well as completed. CheckpointCaptureService accepts a stopped run. It writes the checkpoint and its checkpointId but keeps the run's terminal status (no completed flip, no node update). The capture is enqueued together with the terminal events, so it runs on the thread's effect lane before any later run's start. The workspace is therefore still in the stopped run's state when it is captured.
  • Rollback marks every discarded run. CheckpointRollbackService counted only completed runs after the target as rolled_back. Stopped and failed runs after the target are dropped from the provider conversation too, so they now leave the transcript as well. The existing Pi thread_rollback_after_stop fixture had silently left the stopped turn 2 visible after rolling back to turn 1. Its assertions now cover that.

Failed runs still don't capture. The maintainer decision was about stopped runs, and a failed run's workspace state is less trustworthy.

Providers

Rollback targets are now provider turns that may be partial. Here is what I checked for each adapter:

  • Codex: counts terminal turns after the target and calls thread/revert. Interrupted turns keep a strong native turn id. The live recording confirms Codex keeps the stopped turn and drops only the turn after it.
  • Claude: resumes at the target's last assistant-message uuid. That works when the stopped turn produced an assistant message. A turn stopped before its first assistant message only has a synthetic id, and rollback to it fails loudly with "no SDK assistant message cursor was recorded". That is a clear error, not silent corruption.
  • Pi: forks at the next turn's user entry and keeps the target. It needs strong refs, which Stop captures (fix(server): Pi rollback works past a turn you stopped #13797).
  • OpenCode: forks at the next turn's native user message. There is no status filter.
  • ACP: resets the session and ignores the target, which is unchanged behavior.
  • Cursor: rollback is unsupported and blocked by CommandPolicy.

Verification

  • New fixture thread_rollback_to_stopped_turn, recorded live against Codex 0.156.1 (gpt-6-luna) with a new recorder scenario. Steps: turn 1; turn 2 stopped mid-tool; turn 3; rollback to turn 2's checkpoint; turn 4. The recall in turn 4 includes turns 1 and 2 and excludes turn 3.
    • Without the fix: fails. Run 2 has no checkpoint, so the rollback is rejected, and turn 4 diverges from the recording (expected 3 to be at least 4 provider turns, run 4 failed).
    • With the fix: passes.
  • vp test run passed 215/215 across OrchestratorReplayFixtures.integration, CodexReplayFixtures.integration, OrchestratorReplayFixtures.contract, CheckpointCaptureService, CheckpointRollbackService, RunExecutionService, runtimeLayer, and ProviderRuntimeRecoveryService.
  • It also passed 24/24 across ThreadFork.integration, ThreadMergeBack.integration, OrchestratorReplayRecovery.integration, and EffectWorker.
  • tsc --noEmit -p apps/server is clean. vp lint on the touched files is clean. knip reports nothing new.
  • Not run: repo-wide checks, and a live Claude/Pi/OpenCode rollback to a stopped turn.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

Only completed runs captured a checkpoint, so a rollback targeting a stopped
run's ordinal failed client-side with "Rollback target run ordinal N is
unavailable". Interrupted and cancelled runs now capture one too, keeping
their terminal status. Rollback also marks stopped and failed runs after the
target as rolled back, since the provider drops them from its conversation.

Adds a live-recorded Codex 0.156.1 fixture: turn 1, turn 2 stopped mid-tool,
turn 3, rollback to turn 2, turn 4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 26, 2026
completedAt: capturedAt,
checkpointScopeId: scope.id,
},
payload: stopped

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High orchestration-v2/CheckpointCaptureService.ts:200

A concurrent rollback is overwritten here: this stale run payload changes the run from rolled_back back to interrupted and commits a checkpoint the rollback did not see, making the discarded turn available again while the workspace remains restored. Because rollback can run after the initial read and before commitCommand, capture must conditionally commit only if the run is still the same stopped version (or otherwise serialize/recheck against rollback).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/CheckpointCaptureService.ts around line 200:

A concurrent rollback is overwritten here: this stale `run` payload changes the run from `rolled_back` back to `interrupted` and commits a checkpoint the rollback did not see, making the discarded turn available again while the workspace remains restored. Because rollback can run after the initial read and before `commitCommand`, capture must conditionally commit only if the run is still the same stopped version (or otherwise serialize/recheck against rollback).

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.1 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 1 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.8 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: a31a450 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

macroscopeapp Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Would Approve

Macroscope's review found this PR approvable — This is a focused server bug fix that preserves stopped-run state while making existing rollback behavior work correctly, with targeted replay coverage and no schema, default, security, or deployment changes. A concurrency correctness risk and missing cancelled-path coverage remain noted in the review findings.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Stopped runs now capture a checkpoint, so a capture can find its run already
rolled back: rollback and capture share the thread's effect lane, and a
rollback can commit while a failed capture waits out its retry, or ahead of
a capture a restart requeued. Committing then would write the run back to
interrupted with a checkpoint the rollback never saw, bringing the discarded
turn back.

Capture now settles without committing when the run is already rolled back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
yield* projections.getCheckpointCaptureContext(input.threadId, input);
// A stopped run is already terminal. Its checkpoint is the rollback point
// for the message after it, so capture leaves its status alone.
const stopped = run?.status === "interrupted" || run?.status === "cancelled";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new cancelled capture path has no focused test: the stopped-turn replay covers interrupted, while the cancelled replay covers a queued run that never reaches capture. Could you add a test using the real capture service with test layers that starts from a running turn finalized as cancelled and verifies its checkpoint is recorded without completing the run or root node?

Posted via Macroscope — Effect Service Conventions

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant