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
+2 -2
View File
@@ -38,7 +38,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
// Run 1: a fresh agent on a KNOWN session id learns a secret, then we
// dispose the whole context (simulating process exit) so only the JSONL
// log on disk survives.
ctx = await codingHarness(process.cwd(), root)
ctx = await codingHarness(process.cwd(), { persistenceRoot: root })
const first = ctx.agents.create({
agentId: AgentId('resume-1'),
sessionId: SESSION_ID,
@@ -52,7 +52,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
// Run 2: a brand-new context over the SAME root resumes the persisted
// session. The loaded event log seeds the live session, so the model sees
// run 1's exchange as conversation history.
ctx = await codingHarness(process.cwd(), root)
ctx = await codingHarness(process.cwd(), { persistenceRoot: root })
const resumed = (await ctx.agents.resume({
agentId: AgentId('resume-2'),
resumeSessionId: SESSION_ID,