refactor(compact): turn-agnostic retention + dedicated agent/pre-request seam

Reform the compaction blueprint so a runaway turn survives and the design
stops drifting across review rounds:

- Drop in-flight-turn protection ("layer 2"). Retention is a uniform tail→head
  whole-unit walk; the only structural guard is step-alignment. A single turn
  that alone exceeds the window now compacts its own early closed steps instead
  of being retained verbatim (the failure mode that motivated this).
- Move auto-compaction off the agent/request waterfall onto a new awaited
  agent/pre-request loop seam, fired before history derivation. Compaction
  mutates the surface; the loop derives once from the result — no double-derive,
  and a listener structurally cannot act on not-yet-derived messages.
- Tighten compactIfNeeded to required (session, system, model, signal).
- Enforce a single-pass convergence invariant in resolveConfig: reject configs
  where summarizationMaxTokens + retainTokens exceeds the threshold, so a
  compaction can never immediately re-trigger.
- Document the crash vs recoverable failure taxonomy; core session repair stays
  compaction-agnostic (a log-only orphaned compact/start is inert).
- Wire dsh-compact-basic into examples/coding-agent and add a with-key
  compaction e2e (compaction's first real-world exercise + runaway net).
- Rewrite the RFC to encode the blueprint and move it to implemented/.

The runaway-turn snapshot is a named deferred follow-up: dsh-llm-replay cannot
yet serve the interleaved summarization model call.
This commit is contained in:
Hypatia May
2026-06-26 08:59:33 +08:00
parent aa9afcefc7
commit cec32faa4e
22 changed files with 724 additions and 424 deletions

View File

@@ -180,10 +180,32 @@ declare module 'cordis' {
'agent/step-end'(agent: Agent, turn: number, step: number): void
// ---- interception seams (waterfall) ----
/**
* Awaited surface-mutation checkpoint, fired BEFORE the step's message
* history is derived (and thus before {@link agent/request}). The loop
* awaits `ctx.parallel('agent/pre-request', …)` after assembling the system
* prompt but before `session.deriveMessages()`, then derives ONCE from
* whatever the surface now holds. This is where compaction belongs: it
* mutates the session surface in place (shadowing an older range with a
* summary node), and the single subsequent derive reflects the mutation —
* so there is no double-derive and no listener can see (or be expected to
* act on) an assembled `messages` array that does not exist yet.
*
* Awaited (parallel), not a waterfall: a listener mutates the surface as a
* side effect; there is nothing to transform or veto, but the loop must wait
* for the mutation to complete before deriving. `system`/`model` are the
* assembled values a listener needs to measure pressure (system counts
* toward the budget) and to summarize (the model). `signal` cancels any
* in-flight work a listener starts (e.g. a summarization model call).
* @mode parallel
*/
'agent/pre-request'(agent: Agent, turn: number, step: number, system: string, model: string, signal: AbortSignal): Promise<void> | void
/**
* Waterfall: mutate the fully-assembled {@link GenerateOptions} before the
* model call (hooks, compaction, model switching, tool filtering, …). Call
* `next()` to delegate, or return without it to short-circuit.
* model call (hooks, model switching, tool filtering, …). Call `next()` to
* delegate, or return without it to short-circuit. For surface mutation that
* must precede history derivation (compaction), use {@link agent/pre-request}
* instead — by the time this fires, `options.messages` is already derived.
* @mode waterfall
*/
'agent/request'(agent: Agent, turn: number, step: number, options: GenerateOptions, next: () => Promise<GenerateOptions>): Promise<GenerateOptions>