Skip to content

fix(server): a Claude-initiated turn no longer ends a queued /compact early - #14547

Merged
juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/claude-foreign-result-compact
Oct 1, 2026
Merged

juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/claude-foreign-result-compact

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Problem

This ports main's #14497 (Claude /compact no longer ends early and leaves the thread busy) to V2's Claude adapter.

Claude runs turns of its own between prompts. A resumed process first reports background work the previous process left behind, and peer messages wake the agent. A queued prompt, /compact included, waits behind them. V2 already parked a task-notification wake on a process that echoed early. It missed two cases where the stray turn's result settled the /compact run before its compact_boundary, so the compaction landed on no run:

  1. A positive-turn wake before the process's first echo. This is the first prompt after a resume, while promptEchoMode is still "unknown".
  2. Any non-human origin other than task-notification, such as peer messages. These ended /compact early in both echo modes.

Change

isClaudeResultForOtherTurn decides whether a result answers a different turn than the pending prompt's:

  • If it echoes user_message_uuid(s), it's another turn's when those leave out the prompt's uuid. Same rule as main.
  • If it echoes nothing, it's another turn's when its origin is non-human and the process is known to echo.

The second condition is where this departs from main's plain rule. Applying main's rule as-is broke 4 existing tests from aa71184. That commit fixed an 8-hour production hang: on a CLI that never echoes, an unechoed positive task-notification result can be the turn's only result.

To know the process echoes before its first echo (gap 1), the existing promptEchoMode gains an "acknowledged" state. It is set when the CLI's command_lifecycle frame names the prompt's uuid. Evidence:

  • The CLI emits that frame only for uuid-stamped prompts, from 2.1.252 on. It first appears in the SDK types in 0.3.252.
  • In my provider logs, 201 of the 202 echoed results had a started frame for the same uuid before them.
  • Every acknowledged prompt either got an echoing result or was interrupted, cancelled, or failed.

Older CLIs send no command_lifecycle and no echo, so they take exactly today's path.

This extends the existing gate rather than adding a second mechanism. Another turn's result never settles the prompt's turn:

  • Zero-turn debris: dropped, as before.
  • Early-echo process: held frames and the result go to a continuation run. This was the existing task-notification path; it now covers every origin.
  • Otherwise: that turn's output has already streamed into the active run, so only its result stays out. A continuation would show the reply twice. The prompt's own echoing result ends the run.

The replay harness now remaps command_uuid the same way it already remaps echoed uuids.

Fixtures

All three are hand-ported from real frames: a live claude 2.1.285 headless /compact capture (compact_boundary, synthetic summary user frames, the local_command: "compact" echoing result) plus real recorded or logged turns.

  • claude_compact_after_resume_wake (gap 1): idle release forces a resumed process. Before /compact runs, it reports an orphaned task (frame from a provider log) and runs the recorded positive-turn wake turn from claude_background_wake_before_queued_prompt. Echo mode is still unknown here.
  • claude_compact_after_peer_turn (gap 2): an early-echo process runs a peer-origin turn ahead of the queued /compact. Frame order and origin shape come from a real peer turn in my provider logs.
  • claude_compact_after_peer_turn_no_echo: the worst case for the hang condition. A CLI from before 2.1.252 (no echo, no command_lifecycle) gets the same peer turn while /compact is active, and nothing else follows. The peer result still settles /compact (completed), exactly as before, so nothing hangs.

On current V2 the first two fail: /compact shows compactions: [] with the stray turn's reply on it. With the fix:

  • Resume: /compact carries the 27445 → 1192 compaction and ends completed.
  • Peer: the peer reply moves to its own continuation run.

Verification

  • vp exec tsc --noEmit -p . in apps/server: exit 0.
  • Inside unshare -U --map-current-user -p -f --mount-proc: vp test run on ClaudeAdapterV2.test.ts, ClaudeReplayFixtures.integration.test.ts, OrchestratorReplayFixtures.integration.test.ts and OrchestratorReplayFixtures.contract.test.ts with -t "claude|Claude|contract": 160 passed, 0 failed. That includes all existing Claude fixtures and the 3 new ones.
  • New fixtures against the unfixed adapter: resume and peer fail as described above; no-echo passes, since it pins unchanged behaviour.
  • vp lint on the touched files: no new findings. The two no-unused-vars warnings are already on the base branch.
  • vp fmt on the touched files.
  • Not run: repo-wide checks, or other providers' tests (no shared code changed).

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Oct 1, 2026
@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 1, 2026
@github-actions github-actions Bot added the size:L 100-499 changed lines (additions + deletions). label Oct 1, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes production Claude orchestration state and result routing across resume, peer, task, and compaction scenarios. Although targeted replay fixtures are included, the asynchronous state-machine expansion has broader runtime impact than an auto-approvable small fix.

No code changes detected at 62bbd58. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Oct 1, 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.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 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.7 KiB — 29.3 KiB ✅
Claude Live turn messages — 1 — 8 ✅

Baseline: unavailable · PR result: 62bbd58 · 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.

juliusmarminge and others added 2 commits October 1, 2026 10:07
… early

Ports main's #14497 to the V2 Claude adapter. Claude runs turns of its own
between prompts: a resumed process first reports background work the previous
process left, and peer messages wake the agent. A queued prompt, /compact
included, waits behind them, and V2 let two shapes of such a turn's result
settle the prompt's turn before its compaction ran:

- a positive-turn wake arriving before the process had echoed a prompt (the
  first prompt after a resume), and
- any non-task-notification Claude-initiated turn, such as a peer message.

A result is now another turn's when its echo leaves out the pending prompt's
uuid, or when it echoes nothing, has a non-human origin, and the process is
known to echo. The CLI's command_lifecycle ack of the prompt's uuid proves the
latter before the first echo, so a resume needs no earlier turn. On a CLI that
sends neither (before 2.1.252) the path is unchanged, so a result that may be
the turn's only one still settles it.

Another turn's result never settles the prompt's turn: zero-turn debris is
dropped, an early-echo process sends the held turn to a continuation run as it
already did for task-notification wakes, and otherwise only its result stays
out since its output already streamed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only its own input file uses it, so knip flags the export.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juliusmarminge
juliusmarminge force-pushed the v2/claude-foreign-result-compact branch from 2973c50 to 62bbd58 Compare October 1, 2026 17:07
@juliusmarminge
juliusmarminge merged commit 0fbd755 into t3code/codex-turn-mapping Oct 1, 2026
30 checks passed
@juliusmarminge
juliusmarminge deleted the v2/claude-foreign-result-compact branch October 1, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews 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