fix(compact-basic): replay conversation prefix so summarization reuses KV cache

Automatic compaction fires mid-conversation, right after the loop warmed
the provider's KV cache with the last routed request. The default
summarizer then issued a separate request whose prefix shared nothing
with that warm request — a bespoke summarizer system prompt followed by
the older history flattened to one rendered transcript string — so a
differing first token invalidated the entire cached prefix and every
compaction re-processed the whole replayed history twice.

Move the compaction directive from the FRONT (a fresh system prompt) to
the END (a trailing user message), and replay the last routed request's
own system prompt, tools, message prefix, and shadowed-region messages
verbatim via session.requestHeader() + deriveEventMessage. The auxiliary
call is now a genuine prefix-extension of the warm request, so the
provider reuses the cached tokens up to the trailing instruction.

SummarizationInput carries the replayed prefix instead of a flat string;
the now-unused renderTranscript/renderContentBlocks path is removed with
its spec. Cache reuse is best-effort (head compaction guarantees a hit;
a mid-range compaction or a differently-routed summarizer forgoes it),
correctness is not.
This commit is contained in:
Turtle
2026-07-21 13:55:22 +08:00
parent 9a5c81f9e5
commit a0268d2509
15 changed files with 271 additions and 318 deletions

View File

@@ -6,7 +6,7 @@ This package is the interface tier of the compaction capability, split so each c
| Package | Role |
|---|---|
| `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + tool-pairing boundary helpers + the shared transcript renderer (`renderTranscript`/`renderContentBlocks`) |
| `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + tool-pairing boundary helpers |
| `@deepseek-ai/dsh-compact-basic` | a backend: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization |
| `@deepseek-ai/dsh-tool-compact` (deferred) | the model-facing `/compact` tool over `ctx.compact` |
@@ -63,7 +63,7 @@ Subclass `CompactService`, implement `compactIfNeeded` and `compactRegion`, and
#### What the model sees
A successful implementation replaces an older surface range with one user-role summary checkpoint; the raw events stay logged but stop appearing in derived model messages. The seam itself performs no rewrite.
A successful implementation replaces an older surface range with one user-role summary checkpoint — a `user/message` carrying `surfaceOp: { op: 'replace', start, end }`; the raw events stay logged but stop appearing in derived model messages. The seam itself performs no rewrite.
#### Token effect
@@ -73,20 +73,6 @@ Zero direct tokens from this interface. A backend trades many retained history t
A successful backend replacement invalidates reuse from the first shadowed history token; the seam itself does not alter a request.
### Transcript supplied to a compaction consumer
#### What the model sees
`renderTranscript()` joins entries with one blank line and renders them exactly as `User: <content>`, `Assistant: <content>`, `Tool result (call <callId>): <content>`, `Tool error (call <callId>): <content>`, `[Context: <content>]`, or `[Steering: <content>]`. Non-text blocks render exactly as `[reasoning: <text>]`, `[tool-call: <name>(<arguments>)]`, `[tool-result: <content>]`, `[tool-result]`, or `[<block-type>]`.
#### Token effect
Data-dependent input tokens are paid only by the auxiliary model or consumer that requests this transcript; the conversation model does not receive a duplicate transcript.
#### KV Cache effect
No conversation-cache invalidation. A consumer's auxiliary request can reuse only the exact prefix produced by this rendering; changed or compacted entries invalidate reuse from their first difference.
## Known Limitations and Deferred Work
- **No model-facing consumer tier yet** — `@deepseek-ai/dsh-tool-compact` (the `/compact` tool) is deferred; compaction is reachable only via direct `ctx.compact` calls or a backend's auto listener.