22//
33// Deterministic ConversationTurn[] -> ConversationTurn[] compactor: prunes
44// completed-task context while preserving the active task, recent turns, plan
5- // state, file/tool references, and unresolved errors. The context store keeps
6- // the full run history; only the inference-facing context is curated here .
5+ // state, file/tool references, and unresolved errors. Curates only the
6+ // inference-facing context; the store keeps the full run history .
77
88import type {
99 ConversationTurn ,
@@ -47,28 +47,27 @@ export interface CompactorConfig {
4747 /** Read at compaction time; passed to `summarize` with live workflow state. */
4848 summaryContext ?: ( ) => SummaryContext | undefined ;
4949 /** Previous fat handoff body: the next fold unions its files/commands and
50- * full goal text instead of spine-truncated cuts. */
50+ * full goal text rather than spine cuts. */
5151 readPriorHandoff ?: ( ) => Promise < string | undefined > ;
52- // Recorded for compatibility; the live fold keeps spine + token-capped tail.
52+ // Kept for compatibility; the live fold keeps spine + token-capped tail.
5353 maxAnchorTurns : number ;
54- /** Budgeted-tail shape (token budget, not turn-count floor). Partial:
55- * missing fields resolve against DEFAULT_TAIL_COMPACTION_SHAPE. */
54+ /** Budgeted-tail shape (token budget, not turn-count floor). Missing
55+ * fields resolve against DEFAULT_TAIL_COMPACTION_SHAPE. */
5656 compactionShape ?: Partial < CompactionShape > ;
5757}
5858
5959/**
60- * Shape of the live tail the fold keeps: extract + summary + actions (thin
61- * spine / fat handoff file) plus a token-capped raw tail (~2–3k tokens by
62- * default).
60+ * Live-tail shape the fold keeps: extract + summary + actions (thin spine /
61+ * fat handoff file) plus a token-capped raw tail (~2–3k tokens by default).
6362 */
6463export interface CompactionShape {
6564 /** Live-tail budget in tokens (chars/4 estimate). Default ~2500. */
6665 tailBudgetTokens : number ;
67- /** Tool outputs in the tail longer than this are head+tail excerpted. */
66+ /** Tail tool outputs longer than this are head+tail excerpted. */
6867 maxTailToolOutputChars : number ;
69- /** Keep the head of a shortened tail tool output. */
68+ /** Keep the head of a shortened tail output. */
7069 excerptHead : boolean ;
71- /** Keep the tail of a shortened tail tool output. */
70+ /** Keep the tail of a shortened tail output. */
7271 excerptTail : boolean ;
7372 /** Newest user messages (plus attachments) stay whole up to the budget. */
7473 preserveWholeUserMessages : boolean ;
@@ -91,14 +90,13 @@ export function resolveCompactionShape(
9190 return { ...DEFAULT_TAIL_COMPACTION_SHAPE , ...partial } ;
9291}
9392
94- // Fold marker; the handoff format is owned by ./compaction-handoff.js;
95- // re-exported here so existing importers keep working.
93+ // Fold marker ( handoff format owned by ./compaction-handoff.js); re-exported
94+ // so existing importers keep working.
9695export { COMPACTED_PREFIX } ;
9796
98- // Visible sentinel inserted between adjacent user turns so the history stays
99- // role-alternating and adapters keep a non-empty assistant turn. Identity is
100- // the reserved producer id on `compactSpacerTurn`, not this text or a missing
101- // `model` field.
97+ // Sentinel between adjacent user turns so the history stays role-alternating
98+ // and adapters keep a non-empty assistant turn. Identity is the reserved
99+ // producer id on `compactSpacerTurn`, not this text or a missing `model`.
102100export const COMPACT_SPACER_TEXT = "[compact]" ;
103101export const LEGACY_COMPACT_SPACER_TEXT = "[compaction]" ;
104102export const HARNESS_COMPACT_SPACER_MODEL = "harness" ;
@@ -116,16 +114,15 @@ function extraInstructionParameter(
116114 return { extraInstructions : extra } ;
117115}
118116
119- // `apply` no-ops at or below this turn count: a single turn cannot shrink.
117+ // `apply` no-ops at or below this turn count: one turn cannot shrink.
120118export function compactorNoOpFloor ( ) : number {
121119 return 1 ;
122120}
123121
124- // Query tools deduped by full-argument identity: a later identical
125- // grep/search_files/list_dir call reflects newer workspace state, so older
126- // results are stale. run_shell is excluded — the same command is not
127- // idempotent, so an older result can be the only record of a distinct
128- // outcome.
122+ // Query tools dedup by full-argument identity: a later identical call
123+ // reflects newer workspace state, so older results are stale. run_shell is
124+ // excluded — the same command is not idempotent, so an older result can be
125+ // the only record of a distinct outcome.
129126const QUERY_TOOLS = new Set ( [ ...SEARCH_QUERY_TOOLS , "list_dir" ] ) ;
130127
131128function isReplayableResultTool ( name : string ) : boolean {
@@ -135,7 +132,7 @@ function isReplayableResultTool(name: string): boolean {
135132// Call-id index for stub rendering (name + path). Dedup keys live on `readKey`.
136133interface ToolCallInfo {
137134 name : string ;
138- /** Display path for stubs (always the raw path arg when present). */
135+ /** Display path for stubs (the raw path arg when present). */
139136 pathArg ?: string ;
140137 /** Dedup identity for re-read stubbing. Full-file reads share the path;
141138 * ranged reads (offset/limit) get a distinct key so chunked reads of one
@@ -145,7 +142,7 @@ interface ToolCallInfo {
145142
146143interface PathRead {
147144 callId : string ;
148- /** Monotonic order across the turn list ; higher = later in the session. */
145+ /** Monotonic order across turns ; higher = later in the session. */
149146 order : number ;
150147 isError : boolean ;
151148}
@@ -156,9 +153,9 @@ function scalarArg(value: unknown): string {
156153 return "" ;
157154}
158155
159- /** Path + re-read identity from a tool_call's arguments: path alone for
160- * full-file reads; path+offset+limit when either range arg is present so
161- * partial reads don't supersede each other. */
156+ /** Path + re-read identity from tool_call arguments: path alone for
157+ * full-file reads; path+offset+limit when a range arg is present so partial
158+ * reads don't supersede each other. */
162159function readIdentityFromArguments (
163160 raw : unknown ,
164161) : { path : string ; readKey : string } | undefined {
@@ -184,7 +181,7 @@ function readIdentityFromArguments(
184181 return { path, readKey } ;
185182}
186183
187- // Deterministic key for structurally equal arguments regardless of key order.
184+ // Stable key for structurally equal arguments regardless of key order.
188185function stableStringify ( value : unknown ) : string {
189186 if ( Array . isArray ( value ) ) return `[${ value . map ( stableStringify ) . join ( "," ) } ]` ;
190187 if ( value !== null && typeof value === "object" ) {
@@ -224,8 +221,8 @@ function buildCallIndex(
224221 for ( const turn of turns ) {
225222 for ( const block of turn . content ) {
226223 if ( block . type !== "tool_call" ) continue ;
227- // Persisted calls keep the name the model emitted on the wire
228- // (read/glob/…); the read/query classification sets are engine-keyed.
224+ // Persisted calls keep the wire name (read/glob/…); the read/query
225+ // classification sets are engine-keyed.
229226 const info : ToolCallInfo = { name : canonicalToolName ( block . name ) } ;
230227 const identity = readIdentityFromArguments ( block . arguments ) ;
231228 if ( identity !== undefined ) {
@@ -247,9 +244,8 @@ function buildCallIndex(
247244
248245/**
249246 * Read-identity → matching replayable read/query results in session order.
250- *
251- * Callers pass only surviving turns: supersession over the full transcript
252- * would hollow a kept older read when its newer re-read was summarized away.
247+ * Callers pass only surviving turns: full-transcript supersession would
248+ * hollow a kept older read whose newer re-read was summarized away.
253249 */
254250function buildPathToReads (
255251 turns : readonly ConversationTurn [ ] ,
@@ -282,7 +278,7 @@ function buildPathToReads(
282278
283279/**
284280 * Call ids of successful results superseded by a later successful call of the
285- * same identity. Error results never appear — they stay verbatim so the model
281+ * same identity. Errors stay verbatim — never superseded — so the model
286282 * still sees the failure.
287283 */
288284function supersededReadCallIds (
@@ -292,17 +288,17 @@ function supersededReadCallIds(
292288 for ( const reads of pathToReads . values ( ) ) {
293289 const successes = reads . filter ( ( r ) => ! r . isError ) ;
294290 if ( successes . length < 2 ) continue ;
295- // Newest success (highest order) stays whole; every earlier success stubs.
291+ // Newest success stays whole; every earlier success stubs.
296292 for ( const read of successes . slice ( 0 , - 1 ) ) {
297293 superseded . add ( read . callId ) ;
298294 }
299295 }
300296 return superseded ;
301297}
302298
303- // Turn index of each tool_call and its matching tool_result. A call lives on
304- // one turn and its result on the next, so a pair can straddle a
305- // keep/summarize boundary.
299+ // Turn index of each tool_call and its matching tool_result; a call lives on
300+ // one turn, its result on the next, so a pair can straddle a keep/summarize
301+ // boundary.
306302interface PairLocation {
307303 callIdx ?: number ;
308304 resultIdx ?: number ;
@@ -325,10 +321,9 @@ function buildPairIndex(turns: ConversationTurn[]): Map<string, PairLocation> {
325321 return pairs ;
326322}
327323
328- // Whitespace-collapsed error-text prefix length for matching repeated
329- // failures. Long enough to separate distinct errors, short enough that
330- // trailing variable detail (line numbers, retry counters) does not defeat
331- // the collapse.
324+ // Whitespace-collapsed error-text prefix for repeated-failure matching:
325+ // long enough to separate distinct errors, short enough that trailing
326+ // variable detail (line numbers, retry counters) does not defeat the collapse.
332327const ERROR_SIGNATURE_PREFIX_CHARS = 120 ;
333328
334329type ToolResultBlock = Extract <
@@ -352,8 +347,8 @@ function erroredResultSignature(
352347
353348/**
354349 * Call ids of errored results whose (name, error-text prefix) signature
355- * recurs on a later turn . Every occurrence but the last is returned,
356- * collapsing a retry loop to its most recent failure.
350+ * recurs later. Every occurrence but the last is returned, collapsing a
351+ * retry loop to its most recent failure.
357352 */
358353function repeatedErroredResultCallIds (
359354 turns : readonly ConversationTurn [ ] ,
@@ -427,7 +422,7 @@ function buildResultStub(
427422 return `[${ name } — ${ size } chars, omitted]` ;
428423}
429424
430- // Hollow out superseded successful read_file results; errors and the newest
425+ // Hollow superseded successful read_file results; errors and the newest
431426// success per path stay whole.
432427function stubSupersededReads (
433428 turn : ConversationTurn ,
@@ -460,9 +455,9 @@ function isPlainTextTurn(turn: ConversationTurn): boolean {
460455}
461456
462457/**
463- * Age base64 images in every turn outside the budgeted tail into rehydratable
464- * attachment:// markers + StrategyBlob spills. Tail turns keep live bytes so a
465- * just-pasted screenshot still reaches the model.
458+ * Age base64 images outside the budgeted tail into rehydratable
459+ * attachment:// markers + StrategyBlob spills. Tail turns keep live bytes so
460+ * a just-pasted screenshot still reaches the model.
466461 */
467462async function ageImagesOutsidePicked (
468463 turns : ConversationTurn [ ] ,
@@ -511,11 +506,11 @@ async function ageImagesOutsidePicked(
511506
512507// Merge adjacent same-role turns whose later half is plain text: compaction
513508// can leave same-role neighbors, which the Anthropic Messages API rejects.
514- // A turn carrying a tool_result never fuses — result bodies are bulk the tail
515- // budgets per turn, and a hybrid turn could not be budgeted independently.
516- // Call headers stay fusible; a surviving result still lands right after its
517- // tool_call, so no call/result sequence is disturbed. Result /text neighbors
518- // that no longer fuse get a [compact] spacer from separateAdjacentUserTurns.
509+ // A tool_result turn never fuses — result bodies are bulk the tail budgets
510+ // per turn, so a hybrid turn could not be budgeted independently. Call
511+ // headers stay fusible; a surviving result still lands right after its
512+ // tool_call, so no call/result sequence is disturbed. Unfused result /text
513+ // neighbors get a [compact] spacer from separateAdjacentUserTurns.
519514function carriesToolResult ( turn : ConversationTurn ) : boolean {
520515 return turn . content . some ( ( block ) => block . type === "tool_result" ) ;
521516}
@@ -599,8 +594,8 @@ function isCompactedSummaryTurn(turn: ConversationTurn): boolean {
599594
600595// Harness spacers stamp `model: "harness"`. Missing `model` is unattributed
601596// (replay sanitizer), except persisted `[compaction]` spacers from before
602- // producer-id stamping. Model-produced copies carry a real model id and must
603- // not enter the frozen prefix.
597+ // producer-id stamping. Model copies carry a real model id and must not
598+ // enter the frozen prefix.
604599export function isHarnessCompactSpacer ( turn : ConversationTurn ) : boolean {
605600 if ( turn . role !== "assistant" ) return false ;
606601 const text = firstTextBlock ( turn ) ;
@@ -662,11 +657,11 @@ function excerptTailText(
662657}
663658
664659// Excerpted live copy of a tail turn: thinking/redacted_thinking drop except
665- // on the last assistant that still has a tool_call (Anthropic continuation
666- // needs the unmodified thinking+signature on that tool_use). Whole blocks
667- // drop so signatures never ship with truncated text . Large tool_result text
668- // parts shrink to head+tail excerpts; user text, attachments, tool calls,
669- // and error results pass whole (errors are resume state, not bulk).
660+ // on the last assistant with a tool_call (Anthropic continuation needs the
661+ // unmodified thinking+signature on that tool_use). Whole blocks drop so
662+ // signatures never ship truncated. Large tool_result text parts shrink to
663+ // head+tail excerpts; user text, attachments, tool calls, and errors pass
664+ // whole (they are resume state, not bulk).
670665function excerptTailTurn (
671666 turn : ConversationTurn ,
672667 shape : CompactionShape ,
@@ -708,9 +703,9 @@ function excerptTailTurn(
708703}
709704
710705interface TailSelection {
711- /** Indices actually selected for the live tail — not a contiguous slice. */
706+ /** Indices selected for the live tail — not a contiguous slice. */
712707 picked : Set < number > ;
713- /** Excerpted live copies for tail turns that needed shortening. */
708+ /** Excerpted live copies of tail turns that needed shortening. */
714709 excerpted : Map < number , ConversationTurn > ;
715710 shortenedToolOutputs : number ;
716711 /** Token estimate over the emitted (excerpted) tail. */
@@ -893,7 +888,7 @@ export function createPruningCompactor(
893888 turns : ConversationTurn [ ] ,
894889 _ctx : StrategyContext ,
895890 ) : Promise < StrategyResult < ConversationTurn [ ] > > {
896- // Prior compacted summaries are folded into the next handoff, not frozen.
891+ // Prior compacted summaries fold into the next handoff, not frozen.
897892 const shape = resolveCompactionShape ( cfg . compactionShape ) ;
898893
899894 if ( turns . length <= compactorNoOpFloor ( ) ) {
@@ -1101,10 +1096,10 @@ export function createPruningCompactor(
11011096 // The fold writes a fat structured handoff file (goal, constraints,
11021097 // decisions, evidence markers, files/commands, verification, dead ends,
11031098 // next actions, verbatim exact-facts appendix) under one stable latest
1104- // key, and keeps only a thin spine plus a pointer to that file in the
1099+ // key, keeping only a thin spine plus a pointer to that file in the
11051100 // live prompt. The spine unions carried facts with new ones; dropped
1106- // prior spines are adopted into the evidence archive so the completeness
1107- // gate still certifies the fold.
1101+ // prior spines join the evidence archive so the completeness gate
1102+ // still certifies the fold.
11081103 const priorFileText = await cfg . readPriorHandoff ?.( ) ;
11091104 const handoff = buildHandoffFold ( summarizedTurns , summary , {
11101105 ...( priorFileText !== undefined ? { priorFileText } : { } ) ,
@@ -1121,8 +1116,8 @@ export function createPruningCompactor(
11211116
11221117 // Tail turns stay contentful except for path-dedup: older successful
11231118 // re-reads of the same file become one-line stubs, the newest stays
1124- // whole, and error results are never stubbed. Tail turns keep live
1125- // base64 so a just-pasted screenshot still reaches the model.
1119+ // whole, errors are never stubbed. Tail turns keep live base64 so a
1120+ // just-pasted screenshot still reaches the model.
11261121 const process = ( t : ConversationTurn ) : ConversationTurn =>
11271122 stubSupersededReads ( t , supersededReads , callIndex ) ;
11281123 const liveOutput = separateAdjacentUserTurns (
0 commit comments