/** * DeterministicCompactor — the pure pipeline surface behind the `Compactor` contract * (ARCHITECTURE §11, saga gy5n0k.6). * * Assembles gy5n0k.1–.5 behind the three-method `Compactor` interface: * - `normalize` → gy5n0k.1 `normalize` (SessionEntry[] → NormalizedBlock[]) * - `buildSections` → gy5n0k.4 `buildSections` (runs the .2 extractors, plumbs ctx.codeIndex) * - `formatSummary` → gy5n0k.5 `compile` (formatSummary + mergePrevious + HANDOFF_PREAMBLE) * * Pure pipeline ONLY — no `shouldCompact`. The *need* for compaction is a separate seam * (`CompactionTrigger`, gy5n0k.3, decision Q2): the trigger decides when/where to cut, * this class renders the summary for whatever front slice it's handed. Swapping a * `Compactor` impl never touches the trigger, and vice versa. * * The compaction orchestration (findCutPoint → normalize → buildSections → formatSummary * → `CompactionEntry`) lives in `AgentSession` (gy5n0k.7), NOT here — this class carries no * I/O, no session coupling, and no knowledge of the live window. It is a thin, deterministic * adapter over the free-function pipeline so a consumer can ask for "the compactor" in one * call and so `CompactionEntry.compactor` records `"deterministic"`. * * Unit-testable in isolation; see `tests/core/compactor/compactor.test.ts`. */ import type { SessionEntry } from '@/types/session-entry'; import type { NormalizedBlock } from '@/types/normalized-block'; import type { Compactor, SectionData } from '@/contracts/compactor'; import type { ExtractorContext } from '@/contracts/extractor'; import { normalize } from './normalize'; import { buildSections } from './build-sections'; import { compile } from './compile'; /** * Name recorded on `CompactionEntry.compactor` so a session log identifies which impl * produced a given summary. Stable across compactions within a session; distinct from any * future LLM-backed compactor's name. */ export const DETERMINISTIC_COMPACTOR_NAME = 'deterministic'; /** * Default `Compactor` impl: a thin adapter over the gy5n0k.1–.5 free-function pipeline. * * `formatSummary` delegates to `compile` (not the bare `formatSummary` free fn) so the * returned string is the full handoff document — preamble + merged sections + line-wrap — * exactly what `.7` stores on `CompactionEntry.summary` and prepends to the next turn's * system prompt. When `previousSummary` is omitted, `compile` emits the first-pass summary * (no merge); when present, it merges stable sections + re-caps volatile ones + re-strips * any legacy preamble baked into `prev` (gy5n0k.5). */ export class DeterministicCompactor implements Compactor { /** Recorded on `CompactionEntry.compactor`. */ readonly name = DETERMINISTIC_COMPACTOR_NAME; /** Flatten the JSONL row stream; pair tool_call ↔ tool_result via a shared look-ahead index. */ normalize(entries: SessionEntry[]): NormalizedBlock[] { return normalize(entries); } /** Run the .2 extractors + brief/outstanding-context (.4) over the normalized stream. */ buildSections(blocks: NormalizedBlock[], ctx: ExtractorContext): SectionData { return buildSections(blocks, ctx); } /** * Render the section doc — `formatSummary` + `mergePrevious` + `HANDOFF_PREAMBLE` + line * wrap (gy5n0k.5 `compile`). The returned string is the handoff summary `.7` stores and * prepends; it already carries the `This summary captures work done before…` preamble. */ formatSummary(sections: SectionData, previousSummary?: string): string { return compile(sections, previousSummary); } }