Skip to content

engine: compaction summarizer's inherited effort can bypass compactionMaxTokens, risking silent truncation #126

Description

@andybons

Found during PR #125's review (issue #124 fix: the compaction summarizer inherits the session's current effort level via `Session.Effort()` — see `runCompactionSummary`, `engine/compact.go`).

`compactionMaxTokens` (1024) documents itself as a deliberate cap on the summarization call's response. But at a non-off effort level:

  • `provider/anthropic` raises `max_tokens` above the thinking budget (up to ~20480 at `EffortHigh`), so the effective cap is far above 1024.
  • `provider/openai` (Responses) raises `MaxOutputTokens` to `reasoningOutputFloor` (up to 25000 at `EffortHigh`).
  • `provider/openaicompat` applies reasoning_effort with no `max_tokens` floor at all — the request still caps output at 1024, so a reasoning-heavy model can spend most or all of that budget on reasoning tokens before emitting any summary text.

`runCompactionSummary` has no `StopReason` guard (only an empty-response check), so a response truncated at the token cap mid-summary is indistinguishable from a complete one and gets folded into history as-is via `spliceCompact` — silent data loss at the layer that exists specifically to prevent context overflow.

Suggested fix: either clamp the inherited effort level for this call so it can never push the response cap past `compactionMaxTokens`'s intent, or add a `StopReason` == max-tokens guard to `runCompactionSummary` that fails the compaction (same path as `TestCompactFailureNoJournalNoMutation`) instead of folding a truncated summary.

See the round-1 review on PR #125 for the full analysis (including the openai/anthropic transcode.go line references).

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