Skip to content

bug(operations): shrink ladder cannot remove message attachments and never revisits protected turns, so one pasted image makes a conversation unfittable #28

Closed

Description

@Juliusolsson05

Expected behaviour

shrinkConversationToBudget throws ConversationUnfittableError only when no complete user turn can be made to fit — that is, when the newest turn's authored text alone exceeds the budget. Attachment payload (an image, document or opaque content item inside a message) is consumed input, like a tool output, and the ladder should be able to replace it with a placeholder before giving up. Recent-turn protection is a preference for keeping "what I was just doing" intact, not a reason to refuse a switch.

Actual behaviour

A real Claude transcript (1,100 entries, 16 safe boundaries, 2,458,176 estimated characters) against the 288,000-character OpenCode budget:

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, estimatedCharactersAfter: 549785 }

The newest user turn is [text(15), image(549,526)] — a 15-character prompt and a pasted PNG. Two turns earlier another user message carries an opaque 713,997-character block. Rungs 2 and 3 skip both (protected turns); rung 4 drops every earlier turn and the suffix is still 549,620; the fallback keeps that turn whole and throws.

Reproduction is deterministic: decode the transcript with decodeClaudeConversation, call planConversationContext(conversation, 'opencode', 288_000, { allowSourceTurns: false }).

Root cause

  1. No rung touches message content. clearToolResults and trimToolInputs only look at tool-result and tool-call entries. A message whose estimate is dominated by an image / document / opaque content item can only be dropped whole, and the newest one cannot be dropped at all.
  2. dropOldestTurns' final fallback ("keep the last complete turn whole and say so") is reached with the recent-turn protection still in force, so the payload inside those turns is never reclaimed. The design already relaxes the protection for single-turn conversations on exactly this argument ("declaring such a session unfittable while nine tenths of it is stale tool output would be a worse answer than the evidence supports"); the same argument applies whenever the protected suffix alone exceeds the budget.

Proposed fix

  • New rung between clearing tool results and trimming inputs: replace non-text content items in messages with a text placeholder, oldest first, same protection and net-savings guard as rung 2. Report clearedAttachments / clearedAttachmentChars.
  • When the drop rung cannot fit any boundary, re-run the clearing rungs with the protection lifted on the pre-drop conversation and drop again; throw only if that still fails. Report liftedRecentTurnProtection.

Host side: Juliusolsson05/agent-code#998 (the user-visible failure).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions