You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The bulk provider switch (#360) exists for one moment: a provider's subscription window is exhausted and every agent on it is stuck. Today the switch transaction (docs/design/provider-switching.md) still asks the source provider for a live turn whenever the history does not fit the target, or whenever a Codex session has ever compacted:
frequent with a 1M Claude model (budget ≈ 232k tokens on gpt-6-astra)
OpenCode → any, oversized
one handoff turn
sometimes
When the source is at its limit that turn cannot happen: Claude Code does not retry a subscriber 429 and persists a rate_limit error record; Codex returns usage_limit_reached immediately. The switch then waits its full 300 s timeout per agent, shows one native confirmation dialog per agent, and the agents that most need to move are the ones that cannot.
Source facts that make a fix possible (verified against the vendored Claude Code and Codex sources and real local transcripts):
Codex keeps every pre-compaction record on disk in plaintext. The compacted payload holds the user's prompts in plaintext plus one encrypted summary item. Codex never decrypts that item locally; it is sent back to OpenAI verbatim. There is no client-side logic to reproduce.
Claude's compaction summary is plaintext and already portable.
Both providers absorb overflow natively at the target with their own quota: Codex runs pre-turn auto-compaction from the seeded token count (fatal only if the input exceeds the window), Claude auto-compacts before the first turn and offers "Resume from summary" for large stale sessions.
Deterministic, model-free shrinking is the blessed approach on both sides: Anthropic's clear_tool_uses context editing and OpenAI's own externalAgentConfig/import (which narrates every tool call into a note and truncates results to 4,000 characters; it is Claude → Codex only and does no size handling, so it is an oracle for our projector, not a replacement).
Intended behavior
No switch requires the source provider. Default policy allowSourceTurns: false. Codex → Claude carries the raw history with encrypted markers dropped. Claude → Codex uses Claude's plaintext summaries plus raw tail, then a deterministic shrink ladder (clear old tool results, then drop oldest complete turns with an index of dropped prompts) so the projection stays under the target's auto-compact threshold. The existing source-side /compact path remains as an explicit opt-in that is disabled when the source is exhausted.
Compact on arrival for Codex → Claude: after the target pane is live, run Claude's native /compact (or answer Claude's own resume dialog with "Resume from summary") using Claude's quota. Per-batch checkbox, default on above a size threshold; off keeps the full raw history.
Exhaustion is read structurally from the usage snapshot (Claude session/weekly rows, Codex primary/secondary windows) and from live signals (Claude rate_limit records, Codex usage_limit_reached via codex-headless#46). The bulk modal shows the state and reset time, defaults the direction, and offers "switch model instead" when the hit limit is family-scoped (Opus/Sonnet/Fable) rather than all-models.
Honest labeling. Each switched agent reports its strategy (native, raw, shrunk) in the batch summary and pane toast.
One confirmation per batch instead of one native dialog per agent; agents idle because of a limit error are switchable even if their pane still shows the provider's wait banner.
A Codex session with one and with several compacted records switches to Claude with no Codex process activity beyond wake, and the resulting Claude transcript resumes and answers a read-only marker prompt in the live probe.
A Claude session larger than the Codex budget switches to Codex with no Claude turn; the projected rollout is under Codex's auto-compact limit; Codex resumes and answers the marker prompt.
With the source provider's usage row at 100 percent, the bulk modal defaults to that source, disables source-side compaction, and a 10-agent batch completes without any 300 s wait.
Parser tests run against redacted real fixtures (agent-transcript-parser#24); no invented transcript literals.
docs/design/provider-switching.md updated: capacity outcomes gain raw-history and shrunk, source mutation becomes opt-in, arrival compaction documented.
Typecheck, unit, system, renderer suites green; opt-in live probe run once and its report attached to the PR.
The implementation PR opened without the Stage 7 live probe, by the user's decision. Two acceptance criteria above depend on that run and are not met by this PR — they now live in #833 (test(provider-switch): run the live probe for quota-independent switching), together with the four Unknowns the probe settles (2, 3, 6, 7) and the shrink ladder's two placeholder thresholds:
"With the source provider's usage row at 100 percent, … a 10-agent batch completes without any 300 s wait."
"… opt-in live probe run once and its report attached to the PR."
Nothing above is deleted; read those two bullets as deferred to #833.
Motivation
The bulk provider switch (#360) exists for one moment: a provider's subscription window is exhausted and every agent on it is stuck. Today the switch transaction (
docs/design/provider-switching.md) still asks the source provider for a live turn whenever the history does not fit the target, or whenever a Codex session has ever compacted:compacted/compact+ handoff turn/compacton ClaudeWhen the source is at its limit that turn cannot happen: Claude Code does not retry a subscriber 429 and persists a
rate_limiterror record; Codex returnsusage_limit_reachedimmediately. The switch then waits its full 300 s timeout per agent, shows one native confirmation dialog per agent, and the agents that most need to move are the ones that cannot.Source facts that make a fix possible (verified against the vendored Claude Code and Codex sources and real local transcripts):
compactedpayload holds the user's prompts in plaintext plus one encrypted summary item. Codex never decrypts that item locally; it is sent back to OpenAI verbatim. There is no client-side logic to reproduce.clear_tool_usescontext editing and OpenAI's ownexternalAgentConfig/import(which narrates every tool call into a note and truncates results to 4,000 characters; it is Claude → Codex only and does no size handling, so it is an oracle for our projector, not a replacement).Intended behavior
allowSourceTurns: false. Codex → Claude carries the raw history with encrypted markers dropped. Claude → Codex uses Claude's plaintext summaries plus raw tail, then a deterministic shrink ladder (clear old tool results, then drop oldest complete turns with an index of dropped prompts) so the projection stays under the target's auto-compact threshold. The existing source-side/compactpath remains as an explicit opt-in that is disabled when the source is exhausted./compact(or answer Claude's own resume dialog with "Resume from summary") using Claude's quota. Per-batch checkbox, default on above a size threshold; off keeps the full raw history.rate_limitrecords, Codexusage_limit_reachedvia codex-headless#46). The bulk modal shows the state and reset time, defaults the direction, and offers "switch model instead" when the hit limit is family-scoped (Opus/Sonnet/Fable) rather than all-models.native,raw,shrunk) in the batch summary and pane toast.Acceptance criteria
compactedrecords switches to Claude with no Codex process activity beyond wake, and the resulting Claude transcript resumes and answers a read-only marker prompt in the live probe.docs/design/provider-switching.mdupdated: capacity outcomes gainraw-historyandshrunk, source mutation becomes opt-in, arrival compaction documented.Refs #360, #720, #756, #820, Juliusolsson05/agent-transcript-parser#24, Juliusolsson05/codex-headless#46.
Amended 2026-09-07
The implementation PR opened without the Stage 7 live probe, by the user's decision. Two acceptance criteria above depend on that run and are not met by this PR — they now live in #833 (
test(provider-switch): run the live probe for quota-independent switching), together with the four Unknowns the probe settles (2, 3, 6, 7) and the shrink ladder's two placeholder thresholds:Nothing above is deleted; read those two bullets as deferred to #833.