Skip to content

fix(core): simplify compaction semantics#36267

Merged
kitlangton merged 2 commits into
v2from
compaction-semantics
Jul 10, 2026
Merged

fix(core): simplify compaction semantics#36267
kitlangton merged 2 commits into
v2from
compaction-semantics

Conversation

@kitlangton

@kitlangton kitlangton commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

V2 automatic compaction estimated the next request by JSON-stringifying the full request and dividing its character count by four. When compaction ran, it also forced generation.maxTokens; OpenAI Responses lowered that setting to max_output_tokens. An endpoint that rejected this otherwise optional parameter caused compaction to fail, after which the runner sent the original oversized request and received context_length_exceeded.

This PR restores the simpler V1 semantic boundary: provider-reported usage decides when automatic compaction is required, projected messages decide what enters the checkpoint, and the reason for compaction decides whether model execution continues afterward.

The old failure path sent two requests that could not succeed

sequenceDiagram
  participant Runner
  participant Compactor
  participant Provider
  Runner->>Compactor: Automatic compaction
  Compactor->>Provider: Summary request with max_output_tokens
  Provider-->>Compactor: HTTP 400 unsupported parameter
  Compactor-->>Runner: false
  Runner->>Provider: Original oversized request
  Provider-->>Runner: context_length_exceeded
Loading

The boolean result erased the distinction between “compaction was unnecessary” and “compaction was required but failed.” The runner therefore treated a failed safety operation like a no-op.

Provider usage now drives automatic compaction

The latest completed assistant message already carries normalized provider usage. The TUI uses the same values to display context pressure. This PR uses that observation instead of estimating the entire next request.

used = input + output + reasoning + cache.read + cache.write
usable = model.context - (model.output || compaction.buffer)
required = used > 0 && used >= usable

The check reads only messages after the latest completed checkpoint because SessionHistory already projects the current compaction epoch. A completed checkpoint therefore resets automatic pressure instead of immediately retriggering from hidden historical usage.

flowchart TD
  U[Latest provider usage] --> Q{At usable context limit?}
  Q -- No --> M[Run the model request]
  Q -- Yes --> C[Compact older messages]
  C --> R{Compaction outcome}
  R -- Completed --> M
  R -- Failed --> S[Stop and preserve context]
Loading

The runner performs this check immediately after loading current history and resolving the model. It no longer materializes tools or builds the full model request before discovering that compaction must run.

Checkpoints preserve a coherent serialized suffix

Compaction still stores a generated summary and serialized recent context inside one durable checkpoint. Selection now operates message-by-message rather than splitting an arbitrary serialized string.

flowchart LR
  H[Projected messages] --> P[Walk backward to a user boundary]
  P --> O[Older prefix]
  P --> R[Recent serialized suffix]
  O --> S[Generate summary]
  S --> C[Checkpoint]
  R --> C
Loading

The selection rules are:

  • Serialize each projected message with the existing compaction serializer.
  • Walk backward until the configured recent-context budget is reached.
  • Move the cutoff to a user-message boundary.
  • Keep whole messages; never split an assistant message or tool sequence.
  • If the whole history fits the recent budget, retain the latest user-anchored span and summarize the earlier messages so compaction still reduces context.
  • Fold a previous checkpoint's recent string into the next summary while retaining the newest suffix.
  • Truncate old tool output in the summary input, but preserve attachment descriptions and exact paths/errors where available.

The rough character-based token estimate remains only as a quality control for the retained suffix. It no longer decides whether a provider request is safe to send.

Continuation depends on why compaction ran

Cause On completion On failure
Automatic context pressure Rebuild history and continue the blocked model call Stop execution and preserve the promoted input/history
Provider context overflow Rebuild history and retry the failed call once Publish the compaction failure and surface the original overflow
Manual request Commit the checkpoint and stop Publish the failure and stop

Manual compaction never creates a synthetic continuation. Independently pending work can still run through the normal scheduler, but the manual operation itself does not prompt the model again.

Compaction requests no longer inject an output parameter

The summary request keeps the selected model, structured checkpoint prompt, and empty tool set. It no longer sets generation.maxTokens, so OpenAI Responses does not add the unsupported max_output_tokens field solely for compaction.

The operation now returns an explicit terminal outcome derived from the canonical compaction message statuses:

type Outcome =
  | { status: "completed" }
  | { status: "failed"; error: SessionError.Error }

Failed outcomes carry the same error that is durably published. The runner can stop without guessing what false meant.

Implementation

  • packages/core/src/session/compaction.ts owns usage pressure, message-boundary selection, checkpoint planning, summary execution, and terminal outcome publication.
  • packages/core/src/session/runner/llm.ts owns continuation policy for automatic, overflow-recovery, and manual provenance.
  • packages/core/test/session-compaction.test.ts verifies manual checkpoint execution and omission of the generation override.
  • packages/core/test/session-runner.test.ts verifies usage-triggered compaction, retained recent context, repeated checkpoints, failed automatic compaction, overflow recovery, and manual barriers.

Verification

  • bun typecheck from packages/core: passed.
  • bun run test test/session-compaction.test.ts test/session-runner.test.ts from packages/core: 121 passed.
  • Pre-push bun turbo typecheck --concurrency=3: all 31 package tasks passed.
  • Full Core run: 1266 passed and 6 skipped; the unrelated existing provider-xai-responses.test.ts prompt-cache assertion still fails.
  • The two watcher timeouts seen under the full-suite load passed when test/filesystem/watcher.test.ts was rerun independently: 8 passed.

Fixes #36253

@kitlangton
kitlangton merged commit cf5e0ad into v2 Jul 10, 2026
9 checks passed
@kitlangton
kitlangton deleted the compaction-semantics branch July 10, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant