fix(core): simplify compaction semantics#36267
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tomax_output_tokens. An endpoint that rejected this otherwise optional parameter caused compaction to fail, after which the runner sent the original oversized request and receivedcontext_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
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.
The check reads only messages after the latest completed checkpoint because
SessionHistoryalready 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]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
summaryand serializedrecentcontext inside one durable checkpoint. Selection now operates message-by-message rather than splitting an arbitrary serialized string.The selection rules are:
recentstring into the next summary while retaining the newest suffix.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
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 unsupportedmax_output_tokensfield solely for compaction.The operation now returns an explicit terminal outcome derived from the canonical compaction message statuses:
Failed outcomes carry the same error that is durably published. The runner can stop without guessing what
falsemeant.Implementation
packages/core/src/session/compaction.tsowns usage pressure, message-boundary selection, checkpoint planning, summary execution, and terminal outcome publication.packages/core/src/session/runner/llm.tsowns continuation policy for automatic, overflow-recovery, and manual provenance.packages/core/test/session-compaction.test.tsverifies manual checkpoint execution and omission of the generation override.packages/core/test/session-runner.test.tsverifies usage-triggered compaction, retained recent context, repeated checkpoints, failed automatic compaction, overflow recovery, and manual barriers.Verification
bun typecheckfrompackages/core: passed.bun run test test/session-compaction.test.ts test/session-runner.test.tsfrompackages/core: 121 passed.bun turbo typecheck --concurrency=3: all 31 package tasks passed.provider-xai-responses.test.tsprompt-cache assertion still fails.test/filesystem/watcher.test.tswas rerun independently: 8 passed.Fixes #36253