Skip to content

feat(cli): prefix-cache-preserving compaction (Reasonix-style) - #13077

Open
miroyong wants to merge 2 commits into
continuedev:mainfrom
miroyong:feat/prefix-cache-preserving-compaction
Open

feat(cli): prefix-cache-preserving compaction (Reasonix-style)#13077
miroyong wants to merge 2 commits into
continuedev:mainfrom
miroyong:feat/prefix-cache-preserving-compaction

Conversation

@miroyong

@miroyong miroyong commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Ports Reasonix's prefix-cache strategy to the Continue CLI's auto-compaction so providers with automatic prompt caching (DeepSeek, OpenAI) can serve cache hits across turns.

Problem: today compactChatHistory collapses the history into [system, summary], and getHistoryForLLM trims everything before the compaction index. Any turn after that starts with a different prompt prefix than the previous turn, cold-starting the provider's prompt cache. Additionally, the system message is reconstructed on every streaming iteration — re-reading AGENTS.md/CLAUDE.md and re-running git status — so any change invalidates the whole prefix.

Changes

  • compaction.ts
    • compactChatHistory pins the cache-stable prefix verbatim (system message, first user turn when ≤1.5K tokens, and any prior digests) and keeps a token-budgeted recent tail (12K tokens, ≥2 messages), splicing the new digest in the middle instead of replacing everything.
    • No-op path: when the conversation already fits within pinned prefix + tail, the summarizer is not invoked and history is returned unchanged.
    • getHistoryForLLM returns the full history — after compaction the stored history is the compacted layout (pinned prefix + digest + tail), so trimming before the compaction index would drop the cache-stable prefix.
    • The digest prompt is now a structured briefing (## Standing facts & constraints, ## Goal, ## Decisions & rationale, ## Files & code, ## Commands & outcomes, ## Errors & fixes, ## Pending & next step), mirroring Reasonix's summarySystemPrompt.
  • services/SystemMessageService.ts: memoizes the constructed system message per (mode, additionalRules, format, headless), keeping the prefix byte-identical across turns.
  • services/ChatHistoryService.ts: getHistoryForLLM returns the full history (the compacted layout is already prefix-preserving).
  • Tests updated/added: compaction.test.ts, compaction.infiniteLoop.test.ts (new tokenizer mock surface), ChatHistoryService.test.ts, SystemMessageService.test.ts.

Test plan

  • npx vitest run src/compaction.test.ts src/compaction.infiniteLoop.test.ts src/compaction.pruneLastMessage.test.ts — 38/38 pass
  • npx vitest run src/services/ChatHistoryService.test.ts src/services/SystemMessageService.test.ts src/systemMessage.test.ts src/session.test.ts src/messageConversion.test.ts src/services/ServiceContainer.test.ts — 99/99 pass
  • npx tsc --noEmit — clean
  • npx eslint on changed files — clean

Note: the full CLI suite hangs in the TUIChat UI tests on this Android/Termux environment (unrelated, pre-existing).

Ports Reasonix's prefix-cache strategy to the CLI's auto-compaction:

- compactChatHistory now pins the cache-stable prefix (system message,
  first user turn when small, prior digests) verbatim and keeps a
  token-budgeted recent tail, splicing the new digest in the middle
  instead of collapsing history to [system, summary]. The prompt prefix
  stays byte-identical across turns so providers with automatic prompt
  caching (DeepSeek, OpenAI) serve cache hits on subsequent requests.
- No-op when the conversation already fits within pinned prefix + tail,
  so the summarizer isn't invoked needlessly.
- getHistoryForLLM returns the full history: after compaction the stored
  history already IS the compacted layout, so trimming before the
  compaction index would drop the cache-stable prefix.
- SystemMessageService memoizes the constructed system message per
  (mode, additionalRules, format, headless), since it is re-fetched on
  every streaming iteration and its construction re-reads AGENTS.md /
  runs git status — any change cold-starts the provider's prompt cache.
- Compaction digest prompt rewritten as a structured briefing
  (Standing facts, Goal, Decisions, Files & code, Commands, Errors,
  Pending & next step) mirroring Reasonix's summarySystemPrompt.

Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>
@miroyong
miroyong requested a review from a team as a code owner August 3, 2026 02:26
@miroyong
miroyong requested review from sestinj and removed request for a team August 3, 2026 02:26
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Generated with [Continue](https://continue.dev)

Co-Authored-By: Continue <noreply@continue.dev>
@miroyong

miroyong commented Aug 3, 2026

Copy link
Copy Markdown
Author

Summary

This PR ports Reasonix-style prefix-cache-preserving compaction to the CLI. The core idea: after a compaction, the prompt prefix stays byte-identical across turns (system message + first user turn verbatim + prior digests), so providers with automatic prompt caching (DeepSeek, OpenAI) serve cache hits on subsequent requests.

Changes

compaction.ts

  • Structured briefing prompt (inspired by Reasonix's summarySystemPrompt): output under fixed headings — ## Standing facts & constraints, ## Goal, ## Decisions & rationale, ## Files & code, ## Commands & outcomes, ## Errors & fixes, ## Pending & next step — instead of free-form prose. Rules require terse fragments, exact identifiers/paths/numbers, and no invented content.
  • getPinnedPrefixLength: computes the leading messages that a compaction must preserve verbatim — system message, the first user turn (when ≤ 1.5k tokens, so it stays a brief and a fold never summarizes the user's stated facts away), and any prior compaction digests (so a later fold never re-summarizes an earlier digest into nothing).
  • Bounded recent tail: RECENT_TAIL_TOKEN_BUDGET = 12_000 / MIN_RECENT_TAIL_MESSAGES = 2 — the verbatim tail after a fold is budgeted so large tool outputs cannot keep the history above the auto-compaction trigger and re-fire compaction every turn.
  • No-op fix: when the whole history fits within the budget, compaction returns compactionIndex = -1 without invoking the summarizer (the previous loop condition never included the last message, so small conversations were compacted unnecessarily).

ChatHistoryService.ts

  • getHistoryForLLM now returns the full history (previously it dropped everything before compactionIndex). This is what actually preserves the prefix: the LLM receives system + pinned first turn + digests + tail, and compactChatHistory filters internally for what gets summarized.

SystemMessageService.ts

  • Caches the rendered system message by (mode, rules, format, headless) — relevant for cacheability since the system message is part of the prefix.

Tests

  • compaction.test.ts rewritten for the new contract (pinned prefix, tail budget, no-op path, digest chaining).
  • compaction.infiniteLoop.test.ts updated for the new tokenizer mock and exercises the pruning loop with a large history.
  • ChatHistoryService.test.ts / SystemMessageService.test.ts updated/added for the new behavior and the system-message cache.

All CLI tests pass on the CI matrix (Node 18/20/22/24 × ubuntu/macos/windows).

@miroyong

miroyong commented Aug 3, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant