Repository navigation
Description
Expected behaviour
Switching a Claude agent to OpenCode with the default (quota-independent) policy always succeeds when the conversation has any complete user turn that fits the target budget. The shrink ladder exists precisely so an over-budget history is trimmed rather than refused; a pasted screenshot in the newest turn should be dropped with a "1 image omitted" line in the toast, not abort the switch.
Actual behaviour
Two consecutive attempts (2026-09-18 20:28:21Z and 20:30:12Z, app run 2026-09-18T20-21-26-461Z-main-76278-c6757d, build ad9b2697 main) both failed inside the session:switch-provider handler:
ConversationUnfittableError: Conversation cannot fit the target budget of 288000 characters:
the smallest complete suffix the ladder can produce is 549785 characters.
report: { clearedResults: 256, clearedChars: 664449, trimmedInputs: 3, trimmedChars: 24773,
droppedEntries: 1079, droppedTurns: 15, estimatedCharactersBefore: 2458176,
estimatedCharactersAfter: 549785, budgetCharacters: 288000 }
288,000 is the OpenCode target budget (128k tokens × 0.9 × 2.5 chars/token).
Evidence (source transcript decoded with the parser, 1,100 entries, 16 safe boundaries)
| entry | line | kind | estimate | content |
|---|---|---|---|---|
| 1070 | 992 | user message | 714,226 | 129-char prompt + opaque block of 713,997 chars (an OpenCode {type:'file', mime:'image/png', url:'data:…'} part copied into the Claude transcript by an earlier OpenCode → Claude switch, see #999) |
| 1079 | 1000 | user message | 549,620 | 15-char prompt This [Image #1] + image block of 549,526 chars (base64 PNG pasted via the composer) |
| 1080–1099 | 1001–1020 | opaque | 0 | attachments, api_error, turn_duration |
Suffix costs from the last boundaries: from entry 1055 = 1,294,156; from 1070 = 1,280,149; from 1079 = 549,620. Every rung ran: 256 tool outputs cleared, 3 inputs trimmed, 15 turns dropped. The one thing left, the newest user turn, is 99.98 % one image, and no rung can touch message content. The ladder then keeps "the last complete turn whole" and throws.
Impact
- The quota-independent switch (feat(provider-switch): switch agents off an exhausted provider without a source-side summarization turn #821) is unusable for any conversation whose newest turns carry a pasted screenshot, which is exactly how users report UI bugs to an agent. On a 288k OpenCode budget a single ~400 KB PNG (≈550k base64 chars) is unfittable on its own.
- The user had already lost the Claude session to the API 400 described in bug(provider-switch): switching OpenCode → Claude with a pasted image writes a foreign 'file' block and the Claude API rejects every later turn #999, so the failed switch left no working provider for that conversation.
Root cause (in agent-transcript-parser)
- The shrink ladder has no rung for
image/document/opaquecontent items inside messages, so a user turn dominated by an attachment can never shrink. - Rungs 2 and 3 honour recent-turn protection and the drop rung's final fallback keeps the last turn whole and throws, so payload inside the protected turns is never reclaimed even when that is the only thing standing between the conversation and the budget.
Fixed by the parser change tracked in Juliusolsson05/agent-transcript-parser#28 and landed here by the submodule bump PR.
Environment
macOS 26.6 (Darwin 25.6.0), Electron 43.1.1, Node 24.18, Claude Code 2.1.277, OpenCode target on a 128k profile.
Activity
fix(provider-switch): make the loss notice readable and take the merg…