Repository navigation
feat: forks, switches and rewinds continue the conversation natively on Claude and Codex - #388
Merged
Merged
Conversation
A fork, backend switch or rewind used to hand Claude, Codex and pi a text transcript in the first prompt (tool I/O cut to 2,000 chars, reasoning dropped), even when nothing changed backend. Now the session asks the backend to continue natively first (#387): - same backend: the backend forks its own conversation at the turn (Claude Agent SDK forkSession; Codex thread/fork), from a native point every turn now records (NormalizedTurnResult.nativeRef, stamped on the canonical turn) — nothing converted or cut, reasoning kept; - another backend: native history built from the canonical turns — a Claude session file with real tool_use/tool_result blocks, verified with the SDK's own reader before use, or Codex items via thread/inject_items. The transcript is the fallback (pi, the Gemini CLI, any failure), the session says which way it went, and session.nativeContinuation / CODEOID_NATIVE_CONTINUATION turns it off. Also fixes Codex never saving its thread id, which silently emptied every Codex conversation on a daemon restart, and marks a switched-to backend fresh until its first prompt so a restart re-seeds it. Closes #387 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…restored turns quoted, pin and validate fork points Security review: a native fork copies the backend's own log, which still holds what a tool_result hook redacted from codeoid's record, so a fork is not used once any carried tool call was redacted (codeoid's record is rebuilt natively instead). Turns rebuilt from a displayed transcript (an import) stay a quoted transcript rather than native history in the assistant's voice. Native refs are shape-checked, and Claude's fork lookup is pinned to the source session's project dir. The session file is written with wx into an owner-only dir and its temp file removed on failure. The Codex fallback notice no longer forwards backend error text to clients. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…into an overflow, recovery reseeds Correctness review: - A fork that adopted a native backing counted as having queried, so its ZeroID agent identity was never registered and model calls fell back to the shared badge. Identity now registers on the first send after any native continuation. - No native fork from an errored, interrupted or budget-stopped turn (no nativeRef is stamped), nor when the source's last context was within 85% of the target's window: a fork stays the way out of an overflowed session (#381), with budgeted native history instead. - A Claude fork is read back before it is adopted, and Claude's 'No conversation found' recovery now reseeds the new session from the conversation instead of silently keeping only the latest prompt. - Stale turn anchors are cleared on a session reset or loop rebuild; a rewind to the start clears the native flag; long workdir paths are named exactly as the SDK names them; Codex anchors only completed turns, forks with excludeTurns, and can't adopt a thread after a teardown. - Settings UI exposes session.nativeContinuation; docs cover qwen and how this sits with CODEOID_CONTEXT_STRATEGY. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…urably; imported sessions may still fork - A tool_result redaction now marks the BACKING, not just the call: every later turn of it carries native.redacted, the set is written to the session meta the moment a redaction happens (so a crash before the turn ends can't forget it), restored on resume, and inherited by forks. A partial restart load, log compaction or a squeezed giant turn can no longer hide the redaction from the fork guard. - Turns rebuilt from a displayed transcript block only BUILDING native history (allowHistory: false); a fork of the backend's own record is still allowed. - With no usage on record, whether a fork fits is estimated from the history instead of assumed. - Claude recovery: a transcript seed it applies records the prompt it rides (so an overflow retries there), and adopting a backing doesn't re-arm recovery. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…tion flag can't leak into the next turn - Claude's 'No conversation found' recovery latch no longer clears when the backing is reset (recovery resets it itself, then may adopt a resumable native backing): it clears only after a turn on the new backing succeeds. If that backing is missing too, the turn now fails instead of recovering in a loop. (The round-2 guard in #adoptBacking was dead: the reset ran before it.) - The per-turn redaction flag is cleared when a new turn starts, so a redaction in a turn that never reached turn_done can't mark the next turn's backing. - A late tool result can't rewrite a destroyed session's meta. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…keep the zero-turn docs on their tests Round-4 audit: clean. Folds its two cheap lows. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Collaborator
Author
|
pr-shepherd: CI is green on d0e810c (daemon-check, web-check, Socket Security: Project Report, Socket Security: Pull Request Alerts all SUCCESS). No auto-fixes were needed, and the branch is up to date with main. Ready for human review. |
…the code providers-pi.md no longer says Claude can't take native history; FEATURES states the imported-turns rule as it is (never built into native history, a later real turn may still fork) and only claims a notice on fork/switch; COMPARISON and CHANGELOG name every transcript backend and the recovery fix; #seedProviderFromHistory and the Codex header describe the native path. On a switch the continuation notice now follows the switch line, and its transcript wording no longer says the backend can't (the feature may be off). Codex's thread config is one helper; small type and naming tidy-ups. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
rsharath
approved these changes
Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #387.
What changes
When you fork a session, switch its backend, or rewind it, the new backend has to be given the conversation so far. Until now Claude, Codex and pi got it as a text transcript in the first prompt:
That happened even when the fork stayed on the same backend.
Now the conversation goes over in the backend's own form wherever the backend allows it:
claudetool_use/tool_resultblocks, and checks it with the SDK's own reader before using itcodexopenai,gemini,llamacpppi,gemini-cli,qwenThe transcript stays as the fallback. It is used for any native path that fails. On a fork or switch the session says which way it went:
↪ … continues its own conversation,↪ … was given … as its own history, or↪ … carried as a text transcript.session.nativeContinuation(alsoCODEOID_NATIVE_CONTINUATIONand the Settings UI) turns this off.Two guards on what crosses natively:
tool_resulthook redacted output, a native fork would copy the backend's unredacted log. The redaction is tracked per backing session, durably, and inherited by forks.A native fork is skipped when the source was already near the target's window, or from a turn that ended in an error. So forking stays the way out of an overflowed session (#381).
Also fixed along the way:
How the spike shaped the design
The spike is in the issue; each point was verified against the real CLIs.
sessionStoreisn't usable. A resume through it is not durable: the CLI resumes from a temp copy, and a plain resume later fails. So codeoid writes a normal Claude session file and verifies it withgetSessionMessages. Once verified, Claude owns it like any other session.Code tour
providers/interface.ts):AgentProvider.continueNatively(req)takeshistory,forkFrom?,maxChars,workdirandallowHistory?, and returns{ via: "fork" | "history", backingId?, truncation? }.NormalizedTurnResult.nativeRefis{ backingId, ref, dir?, redacted? }.providers/canonical.ts): assistant turns carrynative(where they end in the backend's own conversation) andrestored. Tool calls carryredacted. Shared helpers:fitTurnsToBudgetandcanonicalTurnChars.providers/claude/index.ts,providers/claude/native-history.ts):translateSDKMessagetracks the last main-chain entry uuid per turn (not subagent, not replay). It is stamped on successful results only, together with the query's cwd.continueNatively:forkSession(pinned to the source's project dir), then read the fork back, then adopt it.toClaudeSessionEntries, thenwriteClaudeSession(0600 file,wx, 0700 dir), thengetSessionMessagesmust read it back exactly, then adopt it.providers/codex/index.ts,providers/codex/native-history.ts):thread/forkwithlastTurnIdandexcludeTurns, orthread/startplusthread/inject_items.#adoptThread.session.ts):#seedProviderFromHistorytries#continueNativelybefore the context strategy. It decidesforkFrom(same backend, valid ref, no redaction, fits the window) andallowHistory(no restored turns). Supporting changes:#identityOnNextSend, so a native continuation still registers the ZeroID agent on its first send;#nativeBackingReady, so the backend isn't treated as fresh when it already holds the conversation;#redactedBackings, persisted in session meta, restored on resume and inherited by forks;#announceContinuationnotice.onRecoveryNeededreseeds the new backing from the history before the prompt.session.nativeContinuation,CODEOID_NATIVE_CONTINUATION, and a Settings manifest entry.Verification
Tests:
native-continuation.test.ts:continueNatively(fork, fork read-back, verify-or-remove,allowHistory, pinned dir);/mcp/controltest; it passed 3/3 on its own and in the next full run.provider-claude.test.ts: backing recovery can't loop, and it re-arms after a successful turn.native.redactedcheck, survives because the backing-level redaction set catches the same cases, so it is defence in depth. The 21 cover:Run live against real Claude (Haiku) and real Codex (gpt-5.6-luna), through
SessionManager.handlewith the real provider registry, in a scratch store:catcommand and outputThe Claude fork, the restart, and Codex → Claude were re-run after the round-1 fixes and still pass. Session files the runs left in
~/.claudeand~/.codexwere cleaned up.What did not run:
SessionManager.handle, the same entry point the WebSocket server calls.enabled_repos.Audit rounds:
tool_resultredaction.Known limitation: if a recovery adopts a native backing that also can't be resumed, the session reports the error and stays failed until
/rotateor a restart. It no longer loops. The suggested alternative, silently starting a blank session, would lose the conversation, so I didn't take it.Follow-ups (not in this PR)
🤖 Generated with Claude Code