Skip to content

summarize: the role=system summary is placed where the provider rejects it (measured 400) #112

Description

@amiddavid

summarize emits its summary as a role: "system" message between msgs[0] and the kept tail
(components/offload/summarize.go:247 and :300, no provider branch). That placement is
deliberate and the reasoning is sound — an operator directive added mid-conversation leaves the
cached prefix intact, where editing the top-level system block would invalidate everything
after it. The problem is only the placement, and it is a hard provider rejection.

Measured

Probed directly against the gateway (benchmark endpoint, not Context Guru), identical ~3k-token
cached system prefix, varying only messages:

shape claude-sonnet-5 claude-opus-5
[user] (warm the prefix) write=3064 read=0 write=3063 read=0
[user] again write=0 read=3064 write=0 read=3063
[user, system, user] — what summarize emits HTTP 400 HTTP 400
[user, system, assistant, user] write=0 read=3064 write=0 read=3063
[user] after write=0 read=3064 write=0 read=3063
messages.1: role 'system' must precede an 'assistant' message or end the array;
the directive-only form (content: [] with output_config) is accepted at any position

Two conclusions. It is not a model-support restriction — both models accept a
mid-conversation system message. And the cache rationale is confirmed: in the legal shape the
prefix bills read in full, so nothing is being folded into system by the gateway.

Scope

Any turn where summarize acts and the kept tail begins with a non-assistant message emits an
invalid request. #110 narrowed this incidentally — snapping the tail off a split tool exchange
lands on the declaring assistant message — but did not close it: keep_last: 1 on a transcript
ending in a user turn still produces [user, summary, user].

Why #110 does not fix it

The obvious fix — require the kept tail to begin with an assistant message — was implemented and
reverted, because it over-reaches. On a transcript with no assistant message near the
boundary the snap walks back to the span start and summarize declines entirely; that regressed
four existing tests (TestSummarizeRestructures, TestSummarizeCountChangeLossless,
TestSummarizeModelErrorFailsOpen, TestSummarizeReusesCheckpoint, all reporting
skipped=true / calls=0). Silently not compacting is a worse failure than the one being
fixed.

So this needs a decision rather than a patch:

  1. Fall back to role: "user" for the summary when the tail cannot start with an assistant
    message.
    Always legal, no placement constraint. Changes how the model weights the summary
    and gives up the operator-channel/injection-resistance property in the fallback case.
  2. Always use role: "user". Simplest and uniform; gives up the property everywhere. Note
    the prefix benefit is a property of where the message sits, not of its role, so this likely
    keeps the cache win — worth confirming with the same probe.
  3. Emit the summary as the directive-only form the error message mentions (content: [] with
    output_config), accepted at any position — needs checking whether a summary can be carried
    that way at all.
  4. Snap the tail to an assistant message when one exists, else fall back to (1). Keeps the
    property in the common agent-loop case, correct in all cases, most moving parts.

(2) or (4) look best; (4) is the one that preserves the intent. Whichever is chosen, the
placement assertion written for #110 should come back — it caught the residual keep_last: 1
case immediately and is already drafted.

Acceptance

  • No emitted wire contains a role: "system" message followed by a non-assistant message.
  • Verified against the probe shapes above, not only in unit tests.
  • The four tests listed above still pass, i.e. summarize still acts on transcripts with no
    assistant message near the boundary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions