pi-condense 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/PRUNING.md +26 -6
- package/README.md +9 -3
- package/index.ts +18 -1
- package/package.json +1 -1
- package/src/budget.test.ts +49 -2
- package/src/budget.ts +22 -8
- package/src/chain-compressor.test.ts +419 -7
- package/src/chain-compressor.ts +144 -3
- package/src/chain-range-prune.test.ts +9 -0
- package/src/chain-range-prune.ts +1 -1
- package/src/commands.test.ts +10 -0
- package/src/commands.ts +6 -2
- package/src/diagnostics.test.ts +11 -2
- package/src/diagnostics.ts +1 -0
- package/src/id-collision.integration.test.ts +20 -2
- package/src/indexer.test.ts +211 -2
- package/src/indexer.ts +45 -4
- package/src/query-tool.test.ts +29 -0
- package/src/range-compression.integration.test.ts +395 -2
- package/src/spill.ts +19 -6
- package/src/types.ts +26 -7
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it, test } from "bun:test";
|
|
2
|
-
import { selectEligible, compressEligible } from "./chain-compressor.js";
|
|
2
|
+
import { selectEligible, compressEligible, extractChainRecords, buildDeterministicBody } from "./chain-compressor.js";
|
|
3
3
|
import type { ChainCompressorIndexerDeps } from "./chain-compressor.js";
|
|
4
|
-
import type { ChainRange, ChainCompressionEntry } from "./types.js";
|
|
4
|
+
import type { ChainRange, ChainCompressionEntry, ToolCallRecord } from "./types.js";
|
|
5
5
|
import { CUSTOM_TYPE_CHAIN } from "./types.js";
|
|
6
6
|
import { detectChains } from "./chain-detector.js";
|
|
7
7
|
import { inGraceRecoveryToolCallIds } from "./recovery-grace.js";
|
|
8
|
+
import { occKey } from "./occurrence-key.js";
|
|
8
9
|
|
|
9
10
|
function closed(startUserTimestamp: number, toolCallIds: string[] = [`tc-${startUserTimestamp}`]): ChainRange {
|
|
10
11
|
return { startUserTimestamp, middleToolCallIds: toolCallIds, finalAssistantTimestamp: startUserTimestamp + 100 };
|
|
@@ -102,6 +103,8 @@ describe("compressEligible", () => {
|
|
|
102
103
|
getPerBatchSummariesForToolCallIds: (_ids: string[]) => opts.perBatchSummaries ?? [],
|
|
103
104
|
getToolRefsForToolCallIds: (_ids: string[]) => opts.toolRefs ?? [],
|
|
104
105
|
registerChain: (_entry: ChainCompressionEntry) => {},
|
|
106
|
+
getIndex: () => new Map(),
|
|
107
|
+
backfillChainRecords: async () => [{ shortId: "t1", toolCallId: "c1", resultTimestamp: 1050 }],
|
|
105
108
|
} satisfies ChainCompressorIndexerDeps;
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -110,6 +113,15 @@ describe("compressEligible", () => {
|
|
|
110
113
|
return { issue: () => ids[i++] ?? `b${i}` } satisfies Pick<import("./block-refs.js").BlockRefIssuer, "issue">;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
// Covered-path fixtures never take the deterministic branch (hasSummary
|
|
117
|
+
// defaults true), so these three fields are unused at runtime - but
|
|
118
|
+
// CompressEligibleDeps requires them, so the fixture supplies inert defaults.
|
|
119
|
+
const NOOP_BACKFILL_DEPS = {
|
|
120
|
+
messages: [] as any[],
|
|
121
|
+
diagnostics: { report: () => {} },
|
|
122
|
+
backfill: { spillThreshold: 1_000_000, spillPreviewBytes: 2048, sessionDir: "/tmp", sessionId: "s1" },
|
|
123
|
+
};
|
|
124
|
+
|
|
113
125
|
test("compresses eligible chains and returns entries", async () => {
|
|
114
126
|
const chains = [closed(100, ["tc1"]), closed(300), closed(500), closed(700)];
|
|
115
127
|
const appended: unknown[] = [];
|
|
@@ -118,6 +130,7 @@ describe("compressEligible", () => {
|
|
|
118
130
|
blockRefs: makeBlockRefs(["b1"]),
|
|
119
131
|
appendEntry: (_type, data) => appended.push(data),
|
|
120
132
|
now: () => 9999,
|
|
133
|
+
...NOOP_BACKFILL_DEPS,
|
|
121
134
|
});
|
|
122
135
|
expect(result.compressedEntries).toHaveLength(1);
|
|
123
136
|
expect(result.compressedEntries[0].blockId).toBe("b1");
|
|
@@ -126,17 +139,59 @@ describe("compressEligible", () => {
|
|
|
126
139
|
expect(appended).toHaveLength(1);
|
|
127
140
|
});
|
|
128
141
|
|
|
129
|
-
test("
|
|
142
|
+
test("covered-path chain entry pinned shape (identity pin, pre-deterministic-branch)", async () => {
|
|
143
|
+
// Pins the FULL entry shape produced by the existing covered path before
|
|
144
|
+
// the deterministic zero-LLM branch is introduced. Must stay byte-identical.
|
|
145
|
+
const chains = [closed(100, ["tc1"]), closed(300), closed(500), closed(700)];
|
|
146
|
+
const appended: unknown[] = [];
|
|
147
|
+
const result = await compressEligible(chains, 3, {
|
|
148
|
+
indexer: makeIndexer({ hasSummary: true, toolRefs: ["t1"] }),
|
|
149
|
+
blockRefs: makeBlockRefs(["b1"]),
|
|
150
|
+
appendEntry: (_type, data) => appended.push(data),
|
|
151
|
+
now: () => 42,
|
|
152
|
+
});
|
|
153
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
154
|
+
const entry = result.compressedEntries[0];
|
|
155
|
+
expect(entry).toEqual({
|
|
156
|
+
blockId: "b1",
|
|
157
|
+
startUserTimestamp: 100,
|
|
158
|
+
droppedToolCallIds: ["tc1"],
|
|
159
|
+
finalAssistantTimestamp: 200,
|
|
160
|
+
toolRefs: ["t1"],
|
|
161
|
+
compressedAt: 42,
|
|
162
|
+
});
|
|
163
|
+
expect(entry.bodySource).toBeUndefined();
|
|
164
|
+
expect(appended[0]).toEqual(entry);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("no coverage -> deterministic compression, not a permanent skip", async () => {
|
|
168
|
+
// Was: "skips chain with no summary and records reason". Per spec
|
|
169
|
+
// 2026-08-14, zero coverage now routes to the deterministic zero-LLM
|
|
170
|
+
// branch instead of a terminal skip.
|
|
171
|
+
const chainMessages = [
|
|
172
|
+
{ role: "user", timestamp: 100, content: [{ type: "text", text: "u" }] },
|
|
173
|
+
{ role: "assistant", timestamp: 101, content: [{ type: "toolCall", id: "tc1", name: "bash", input: { cmd: "a" } }] },
|
|
174
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "bash", timestamp: 150, isError: false, content: [{ type: "text", text: "out" }] },
|
|
175
|
+
{ role: "assistant", timestamp: 200, content: [{ type: "text", text: "done" }] },
|
|
176
|
+
];
|
|
130
177
|
const chains = [closed(100, ["tc1"]), closed(300, ["tc2"]), closed(500), closed(700)];
|
|
131
178
|
const result = await compressEligible(chains, 3, {
|
|
132
|
-
indexer:
|
|
179
|
+
indexer: {
|
|
180
|
+
...makeIndexer({ hasSummary: false }),
|
|
181
|
+
getIndex: () => new Map(),
|
|
182
|
+
backfillChainRecords: async () => [{ shortId: "t1", toolCallId: "tc1", resultTimestamp: 1050 }],
|
|
183
|
+
},
|
|
133
184
|
blockRefs: makeBlockRefs(),
|
|
134
185
|
appendEntry: () => {},
|
|
135
186
|
now: () => 1,
|
|
187
|
+
messages: chainMessages,
|
|
188
|
+
diagnostics: { report: () => {} },
|
|
189
|
+
backfill: { spillThreshold: 1_000_000, spillPreviewBytes: 2048, sessionDir: "/tmp", sessionId: "s1" },
|
|
136
190
|
});
|
|
137
|
-
expect(result.
|
|
138
|
-
expect(result.
|
|
139
|
-
expect(result.
|
|
191
|
+
expect(result.skipped).toHaveLength(0);
|
|
192
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
193
|
+
expect(result.compressedEntries[0].bodySource).toBe("deterministic");
|
|
194
|
+
expect(result.compressedEntries[0].startUserTimestamp).toBe(100);
|
|
140
195
|
});
|
|
141
196
|
|
|
142
197
|
test("reports already-compressed chains in skipped list", async () => {
|
|
@@ -155,6 +210,7 @@ describe("compressEligible", () => {
|
|
|
155
210
|
blockRefs: makeBlockRefs(),
|
|
156
211
|
appendEntry: () => {},
|
|
157
212
|
now: () => 1,
|
|
213
|
+
...NOOP_BACKFILL_DEPS,
|
|
158
214
|
});
|
|
159
215
|
// Primary contract: already-compressed chains must never be double-compressed.
|
|
160
216
|
expect(result.compressedEntries).toHaveLength(0);
|
|
@@ -170,6 +226,7 @@ describe("compressEligible", () => {
|
|
|
170
226
|
blockRefs: makeBlockRefs(["b1"]),
|
|
171
227
|
appendEntry: (type, data) => calls.push({ type, data }),
|
|
172
228
|
now: () => 0,
|
|
229
|
+
...NOOP_BACKFILL_DEPS,
|
|
173
230
|
});
|
|
174
231
|
expect(calls).toHaveLength(1);
|
|
175
232
|
expect(calls[0].type).toBe(CUSTOM_TYPE_CHAIN);
|
|
@@ -183,6 +240,7 @@ describe("compressEligible", () => {
|
|
|
183
240
|
blockRefs: makeBlockRefs(["b1"]),
|
|
184
241
|
appendEntry: () => {},
|
|
185
242
|
now: () => 1,
|
|
243
|
+
...NOOP_BACKFILL_DEPS,
|
|
186
244
|
fuseRange: async (text) => {
|
|
187
245
|
fuseCalls.push(text);
|
|
188
246
|
return "FUSED";
|
|
@@ -200,6 +258,7 @@ describe("compressEligible", () => {
|
|
|
200
258
|
blockRefs: makeBlockRefs(["b1"]),
|
|
201
259
|
appendEntry: () => {},
|
|
202
260
|
now: () => 1,
|
|
261
|
+
...NOOP_BACKFILL_DEPS,
|
|
203
262
|
fuseRange: async () => {
|
|
204
263
|
fuseCalled = true;
|
|
205
264
|
return "FUSED";
|
|
@@ -216,6 +275,7 @@ describe("compressEligible", () => {
|
|
|
216
275
|
blockRefs: makeBlockRefs(["b1"]),
|
|
217
276
|
appendEntry: () => {},
|
|
218
277
|
now: () => 1,
|
|
278
|
+
...NOOP_BACKFILL_DEPS,
|
|
219
279
|
fuseRange: async () => null,
|
|
220
280
|
});
|
|
221
281
|
expect(result.compressedEntries).toHaveLength(1);
|
|
@@ -229,6 +289,7 @@ describe("compressEligible", () => {
|
|
|
229
289
|
blockRefs: makeBlockRefs(["b1"]),
|
|
230
290
|
appendEntry: () => {},
|
|
231
291
|
now: () => 1,
|
|
292
|
+
...NOOP_BACKFILL_DEPS,
|
|
232
293
|
fuseRange: async () => {
|
|
233
294
|
throw new Error("boom");
|
|
234
295
|
},
|
|
@@ -253,6 +314,7 @@ describe("compressEligible", () => {
|
|
|
253
314
|
blockRefs: makeBlockRefs(["b1"]),
|
|
254
315
|
appendEntry: (_type, data) => appended.push(data),
|
|
255
316
|
now: () => 1,
|
|
317
|
+
...NOOP_BACKFILL_DEPS,
|
|
256
318
|
},
|
|
257
319
|
);
|
|
258
320
|
expect(result.compressedEntries).toHaveLength(1);
|
|
@@ -267,6 +329,7 @@ describe("compressEligible", () => {
|
|
|
267
329
|
blockRefs: makeBlockRefs(["b1"]),
|
|
268
330
|
appendEntry: () => {},
|
|
269
331
|
now: () => 1,
|
|
332
|
+
...NOOP_BACKFILL_DEPS,
|
|
270
333
|
});
|
|
271
334
|
expect(result.compressedEntries).toHaveLength(1);
|
|
272
335
|
expect("protectedToolCallIds" in result.compressedEntries[0]).toBe(false);
|
|
@@ -279,6 +342,7 @@ describe("compressEligible", () => {
|
|
|
279
342
|
blockRefs: makeBlockRefs(["b1"]),
|
|
280
343
|
appendEntry: () => {},
|
|
281
344
|
now: () => 1,
|
|
345
|
+
...NOOP_BACKFILL_DEPS,
|
|
282
346
|
});
|
|
283
347
|
expect(result.compressedEntries[0].rangeSummaryText).toBeUndefined();
|
|
284
348
|
});
|
|
@@ -427,3 +491,351 @@ describe("selectEligible - recovery grace wiring (production boundary)", () => {
|
|
|
427
491
|
expect(eligible.map((c) => c.startUserTimestamp)).toEqual([3]);
|
|
428
492
|
});
|
|
429
493
|
});
|
|
494
|
+
|
|
495
|
+
describe("extractChainRecords", () => {
|
|
496
|
+
function messages() {
|
|
497
|
+
return [
|
|
498
|
+
{ role: "user", timestamp: 1000, content: [{ type: "text", text: "u" }] },
|
|
499
|
+
{ role: "assistant", timestamp: 1001, content: [{ type: "toolCall", id: "c1", name: "bash", input: { cmd: "a" } }] },
|
|
500
|
+
{ role: "toolResult", toolCallId: "c1", toolName: "bash", timestamp: 1050, isError: false, content: [{ type: "text", text: "out1" }] },
|
|
501
|
+
{ role: "assistant", timestamp: 1002, content: [{ type: "toolCall", id: "c2", name: "read", input: { path: "x" } }] },
|
|
502
|
+
{ role: "toolResult", toolCallId: "c2", toolName: "read", timestamp: 1150, isError: false, content: [{ type: "text", text: "out2" }] },
|
|
503
|
+
{ role: "assistant", timestamp: 1200, content: [{ type: "text", text: "done" }] },
|
|
504
|
+
];
|
|
505
|
+
}
|
|
506
|
+
const chain = { startUserTimestamp: 1000, finalAssistantTimestamp: 1200, protectedToolCallIds: [] as string[] };
|
|
507
|
+
|
|
508
|
+
test("happy path: 2 middle calls -> 2 records with resultTimestamp/turnIndex/resultText", () => {
|
|
509
|
+
const records = extractChainRecords(messages(), chain, () => false);
|
|
510
|
+
expect(records).toHaveLength(2);
|
|
511
|
+
expect(records[0]).toEqual({
|
|
512
|
+
toolCallId: "c1",
|
|
513
|
+
toolName: "bash",
|
|
514
|
+
args: { cmd: "a" },
|
|
515
|
+
resultText: "out1",
|
|
516
|
+
isError: false,
|
|
517
|
+
turnIndex: -1,
|
|
518
|
+
timestamp: 1050,
|
|
519
|
+
resultTimestamp: 1050,
|
|
520
|
+
});
|
|
521
|
+
expect(records[1]).toEqual({
|
|
522
|
+
toolCallId: "c2",
|
|
523
|
+
toolName: "read",
|
|
524
|
+
args: { path: "x" },
|
|
525
|
+
resultText: "out2",
|
|
526
|
+
isError: false,
|
|
527
|
+
turnIndex: -1,
|
|
528
|
+
timestamp: 1150,
|
|
529
|
+
resultTimestamp: 1150,
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("excludes protected and already-indexed occurrence keys", () => {
|
|
534
|
+
const protectedChain = { ...chain, protectedToolCallIds: ["c1"] };
|
|
535
|
+
const indexedKey = occKey("c2", 1150);
|
|
536
|
+
const records = extractChainRecords(messages(), protectedChain, (k) => k === indexedKey);
|
|
537
|
+
expect(records).toHaveLength(0);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("falls back to block.args when block.input is absent (batch-capture parity)", () => {
|
|
541
|
+
const msgs = [
|
|
542
|
+
{ role: "user", timestamp: 1000, content: [{ type: "text", text: "u" }] },
|
|
543
|
+
{ role: "assistant", timestamp: 1001, content: [{ type: "toolCall", id: "c1", name: "bash", args: { cmd: "a" } }] },
|
|
544
|
+
{ role: "toolResult", toolCallId: "c1", toolName: "bash", timestamp: 1050, isError: false, content: [{ type: "text", text: "out1" }] },
|
|
545
|
+
{ role: "assistant", timestamp: 1200, content: [{ type: "text", text: "done" }] },
|
|
546
|
+
];
|
|
547
|
+
const records = extractChainRecords(msgs, { ...chain, finalAssistantTimestamp: 1200 }, () => false);
|
|
548
|
+
expect(records).toHaveLength(1);
|
|
549
|
+
expect(records[0].args).toEqual({ cmd: "a" });
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
describe("buildDeterministicBody", () => {
|
|
554
|
+
function record(overrides: Partial<ToolCallRecord>): ToolCallRecord {
|
|
555
|
+
return {
|
|
556
|
+
toolCallId: "x",
|
|
557
|
+
toolName: "bash",
|
|
558
|
+
args: {},
|
|
559
|
+
resultText: "",
|
|
560
|
+
isError: false,
|
|
561
|
+
turnIndex: -1,
|
|
562
|
+
timestamp: 0,
|
|
563
|
+
...overrides,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
test("full grammar pin", () => {
|
|
568
|
+
const records = [
|
|
569
|
+
record({ toolCallId: "a1", toolName: "bash", args: { cmd: "ls" }, resultText: "r1", timestamp: 1000, resultTimestamp: 1000 }),
|
|
570
|
+
record({ toolCallId: "a2", toolName: "bash", args: { cmd: "pwd" }, resultText: "r2", timestamp: 2000, resultTimestamp: 2000 }),
|
|
571
|
+
record({ toolCallId: "a3", toolName: "read", args: { path: "f" }, resultText: "r3", timestamp: 3000, resultTimestamp: 3000 }),
|
|
572
|
+
];
|
|
573
|
+
const body = buildDeterministicBody(records, ["t1", "t2", "t3"]);
|
|
574
|
+
expect(body).toBe(
|
|
575
|
+
[
|
|
576
|
+
"Deterministic chain compression (no per-batch summary existed for this span; raw outputs recoverable via context_tree_query).",
|
|
577
|
+
"Calls: 3",
|
|
578
|
+
"Tools: bash x2, read x1",
|
|
579
|
+
`Span: ${new Date(1000).toISOString()} -> ${new Date(3000).toISOString()} (2s)`,
|
|
580
|
+
'First: bash {"cmd":"ls"}',
|
|
581
|
+
'Last: read {"path":"f"}',
|
|
582
|
+
"Refs: t1, t2, t3",
|
|
583
|
+
].join("\n"),
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("sorts/spans by resultTimestamp when it differs from timestamp", () => {
|
|
588
|
+
// timestamp order is reversed vs resultTimestamp order; resultTimestamp must win.
|
|
589
|
+
const records = [
|
|
590
|
+
record({ toolCallId: "a1", toolName: "bash", args: { cmd: "ls" }, timestamp: 9000, resultTimestamp: 1000 }),
|
|
591
|
+
record({ toolCallId: "a2", toolName: "read", args: { path: "f" }, timestamp: 1000, resultTimestamp: 9000 }),
|
|
592
|
+
];
|
|
593
|
+
const body = buildDeterministicBody(records, ["t1", "t2"]);
|
|
594
|
+
expect(body).toContain("First: bash ");
|
|
595
|
+
expect(body).toContain("Last: read ");
|
|
596
|
+
expect(body).toContain(`Span: ${new Date(1000).toISOString()} -> ${new Date(9000).toISOString()} (8s)`);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("empty toolRefs falls back to bare toolCallIds for the Refs line", () => {
|
|
600
|
+
const records = [
|
|
601
|
+
record({ toolCallId: "a1", toolName: "bash", timestamp: 1000 }),
|
|
602
|
+
record({ toolCallId: "a2", toolName: "read", timestamp: 2000 }),
|
|
603
|
+
];
|
|
604
|
+
const body = buildDeterministicBody(records, []);
|
|
605
|
+
expect(body).toContain("Refs: a1, a2");
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("300-char args excerpt is capped at 200 chars + '...'", () => {
|
|
609
|
+
const longArgs = { s: "a".repeat(300) };
|
|
610
|
+
const records = [record({ toolName: "bash", args: longArgs, timestamp: 0 })];
|
|
611
|
+
const body = buildDeterministicBody(records, ["t1"]);
|
|
612
|
+
const firstLine = body.split("\n").find((l) => l.startsWith("First: "))!;
|
|
613
|
+
const excerptText = firstLine.slice("First: bash ".length);
|
|
614
|
+
expect(excerptText).toHaveLength(203);
|
|
615
|
+
expect(excerptText.endsWith("...")).toBe(true);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("empty-args chain still produces a non-empty body with Calls/Tools/Span lines", () => {
|
|
619
|
+
const records = [record({ toolName: "bash", args: {}, resultText: "", timestamp: 500 })];
|
|
620
|
+
const body = buildDeterministicBody(records, ["t1"]);
|
|
621
|
+
expect(body.length).toBeGreaterThan(0);
|
|
622
|
+
expect(body).toContain("Calls: 1");
|
|
623
|
+
expect(body).toContain("Tools: bash x1");
|
|
624
|
+
expect(body).toContain("Span:");
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe("compressEligible - deterministic zero-LLM branch", () => {
|
|
629
|
+
function chainMessages() {
|
|
630
|
+
return [
|
|
631
|
+
{ role: "user", timestamp: 1000, content: [{ type: "text", text: "u" }] },
|
|
632
|
+
{ role: "assistant", timestamp: 1001, content: [{ type: "toolCall", id: "c1", name: "bash", input: { cmd: "a" } }] },
|
|
633
|
+
{ role: "toolResult", toolCallId: "c1", toolName: "bash", timestamp: 1050, isError: false, content: [{ type: "text", text: "out1" }] },
|
|
634
|
+
{ role: "assistant", timestamp: 1002, content: [{ type: "toolCall", id: "c2", name: "read", input: { path: "x" } }] },
|
|
635
|
+
{ role: "toolResult", toolCallId: "c2", toolName: "read", timestamp: 1150, isError: false, content: [{ type: "text", text: "out2" }] },
|
|
636
|
+
{ role: "assistant", timestamp: 1200, content: [{ type: "text", text: "done" }] },
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function uncoveredChain(): ChainRange {
|
|
641
|
+
return {
|
|
642
|
+
startUserTimestamp: 1000,
|
|
643
|
+
middleToolCallIds: ["c1", "c2"],
|
|
644
|
+
finalAssistantTimestamp: 1200,
|
|
645
|
+
protectedToolCallIds: [],
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function makeDeterministicDeps(opts: {
|
|
650
|
+
messages?: any[];
|
|
651
|
+
indexRecords?: Map<string, ToolCallRecord>;
|
|
652
|
+
backfillImpl?: (records: ToolCallRecord[], opts: unknown) => Promise<import("./types.js").SummaryToolCallRef[]>;
|
|
653
|
+
toolRefs?: string[];
|
|
654
|
+
diagnosticsReport?: (kind: string, dedupKey: string, detail: string) => void;
|
|
655
|
+
fuseRange?: (text: string) => Promise<string | null>;
|
|
656
|
+
} = {}) {
|
|
657
|
+
const backfillCalls: Array<{ records: ToolCallRecord[]; opts: unknown }> = [];
|
|
658
|
+
const registerChainCalls: ChainCompressionEntry[] = [];
|
|
659
|
+
const appended: unknown[] = [];
|
|
660
|
+
const deps = {
|
|
661
|
+
indexer: {
|
|
662
|
+
getChainEntries: () => [],
|
|
663
|
+
hasPerBatchSummaryCoveringAny: () => false,
|
|
664
|
+
getPerBatchSummariesForToolCallIds: () => [],
|
|
665
|
+
getToolRefsForToolCallIds: () => opts.toolRefs ?? ["t1", "t2"],
|
|
666
|
+
registerChain: (entry: ChainCompressionEntry) => registerChainCalls.push(entry),
|
|
667
|
+
getIndex: () => opts.indexRecords ?? new Map<string, ToolCallRecord>(),
|
|
668
|
+
backfillChainRecords: async (records: ToolCallRecord[], backfillOpts: unknown) => {
|
|
669
|
+
backfillCalls.push({ records, opts: backfillOpts });
|
|
670
|
+
if (opts.backfillImpl) return opts.backfillImpl(records, backfillOpts);
|
|
671
|
+
return [
|
|
672
|
+
{ shortId: "t1", toolCallId: "c1", resultTimestamp: 1050 },
|
|
673
|
+
{ shortId: "t2", toolCallId: "c2", resultTimestamp: 1150 },
|
|
674
|
+
];
|
|
675
|
+
},
|
|
676
|
+
} satisfies ChainCompressorIndexerDeps,
|
|
677
|
+
blockRefs: { issue: () => "b1" } satisfies Pick<import("./block-refs.js").BlockRefIssuer, "issue">,
|
|
678
|
+
appendEntry: (_type: string, data: unknown) => appended.push(data),
|
|
679
|
+
now: () => 1,
|
|
680
|
+
fuseRange: opts.fuseRange,
|
|
681
|
+
messages: opts.messages ?? chainMessages(),
|
|
682
|
+
diagnostics: { report: opts.diagnosticsReport ?? (() => {}) },
|
|
683
|
+
backfill: { spillThreshold: 1_000_000, spillPreviewBytes: 2048, sessionDir: "/tmp", sessionId: "s1" },
|
|
684
|
+
};
|
|
685
|
+
return { deps, backfillCalls, registerChainCalls, appended };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
test("compresses an uncovered chain with a deterministic body", async () => {
|
|
689
|
+
let fuseCalled = false;
|
|
690
|
+
const { deps, backfillCalls, registerChainCalls } = makeDeterministicDeps({
|
|
691
|
+
fuseRange: async () => {
|
|
692
|
+
fuseCalled = true;
|
|
693
|
+
return "FUSED";
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
const result = await compressEligible(
|
|
697
|
+
[{ ...uncoveredChain(), protectedToolCallIds: ["c2"] }],
|
|
698
|
+
0,
|
|
699
|
+
deps as any,
|
|
700
|
+
);
|
|
701
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
702
|
+
const entry = result.compressedEntries[0];
|
|
703
|
+
expect(entry.bodySource).toBe("deterministic");
|
|
704
|
+
expect(entry.rangeSummaryText).toBeTruthy();
|
|
705
|
+
expect(entry.toolRefs).toEqual(["t1", "t2"]);
|
|
706
|
+
expect(fuseCalled).toBe(false);
|
|
707
|
+
expect(backfillCalls).toHaveLength(1);
|
|
708
|
+
expect(backfillCalls[0].records).toHaveLength(1);
|
|
709
|
+
expect(backfillCalls[0].records.map((r) => r.toolCallId)).not.toContain("c2");
|
|
710
|
+
expect(registerChainCalls).toHaveLength(1);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("covered path is untouched: backfill never invoked, entry matches identity pin", async () => {
|
|
714
|
+
const { deps, backfillCalls } = makeDeterministicDeps();
|
|
715
|
+
// Override to simulate coverage so the covered branch (not the deterministic one) runs.
|
|
716
|
+
(deps.indexer as any).hasPerBatchSummaryCoveringAny = () => true;
|
|
717
|
+
(deps.indexer as any).getToolRefsForToolCallIds = () => ["t1"];
|
|
718
|
+
const chain: ChainRange = {
|
|
719
|
+
startUserTimestamp: 100,
|
|
720
|
+
middleToolCallIds: ["tc1"],
|
|
721
|
+
finalAssistantTimestamp: 200,
|
|
722
|
+
protectedToolCallIds: [],
|
|
723
|
+
};
|
|
724
|
+
const result = await compressEligible([chain], 0, deps as any);
|
|
725
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
726
|
+
const entry = result.compressedEntries[0];
|
|
727
|
+
expect(entry).toEqual({
|
|
728
|
+
blockId: "b1",
|
|
729
|
+
startUserTimestamp: 100,
|
|
730
|
+
droppedToolCallIds: ["tc1"],
|
|
731
|
+
finalAssistantTimestamp: 200,
|
|
732
|
+
toolRefs: ["t1"],
|
|
733
|
+
compressedAt: 1,
|
|
734
|
+
});
|
|
735
|
+
expect(backfillCalls).toHaveLength(0);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("fail-closed: backfillChainRecords rejecting keeps the no-summary skip", async () => {
|
|
739
|
+
const { deps, registerChainCalls, appended } = makeDeterministicDeps({
|
|
740
|
+
backfillImpl: async () => {
|
|
741
|
+
throw new Error("spill failed");
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
const result = await compressEligible([uncoveredChain()], 0, deps as any);
|
|
745
|
+
expect(result.compressedEntries).toHaveLength(0);
|
|
746
|
+
expect(result.skipped).toEqual([{ startUserTimestamp: 1000, reason: "no-summary" }]);
|
|
747
|
+
expect(registerChainCalls).toHaveLength(0);
|
|
748
|
+
expect(appended).toHaveLength(0);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
test("retry discriminator: members already indexed, nothing new extracted -> composes from index", async () => {
|
|
752
|
+
const indexed: ToolCallRecord = {
|
|
753
|
+
toolCallId: "c1",
|
|
754
|
+
toolName: "bash",
|
|
755
|
+
args: { cmd: "a" },
|
|
756
|
+
resultText: "out1",
|
|
757
|
+
isError: false,
|
|
758
|
+
turnIndex: -1,
|
|
759
|
+
timestamp: 1050,
|
|
760
|
+
resultTimestamp: 1050,
|
|
761
|
+
};
|
|
762
|
+
const indexed2: ToolCallRecord = {
|
|
763
|
+
toolCallId: "c2",
|
|
764
|
+
toolName: "read",
|
|
765
|
+
args: { path: "x" },
|
|
766
|
+
resultText: "out2",
|
|
767
|
+
isError: false,
|
|
768
|
+
turnIndex: -1,
|
|
769
|
+
timestamp: 1150,
|
|
770
|
+
resultTimestamp: 1150,
|
|
771
|
+
};
|
|
772
|
+
const index = new Map<string, ToolCallRecord>([
|
|
773
|
+
[occKey("c1", 1050), indexed],
|
|
774
|
+
[occKey("c2", 1150), indexed2],
|
|
775
|
+
]);
|
|
776
|
+
const chainWithOccKeys: ChainRange = {
|
|
777
|
+
...uncoveredChain(),
|
|
778
|
+
middleOccurrenceKeys: [occKey("c1", 1050), occKey("c2", 1150)],
|
|
779
|
+
};
|
|
780
|
+
const { deps, backfillCalls } = makeDeterministicDeps({ indexRecords: index });
|
|
781
|
+
const result = await compressEligible([chainWithOccKeys], 0, deps as any);
|
|
782
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
783
|
+
expect(result.compressedEntries[0].bodySource).toBe("deterministic");
|
|
784
|
+
expect(backfillCalls).toHaveLength(0);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test("retry discriminator: genuine span mismatch -> skip + backfill-empty diagnostic", async () => {
|
|
788
|
+
const reports: Array<[string, string, string]> = [];
|
|
789
|
+
const { deps } = makeDeterministicDeps({
|
|
790
|
+
messages: [], // span cannot resolve -> extraction empty, index empty
|
|
791
|
+
diagnosticsReport: (kind, dedupKey, detail) => reports.push([kind, dedupKey, detail]),
|
|
792
|
+
});
|
|
793
|
+
const result = await compressEligible([uncoveredChain()], 0, deps as any);
|
|
794
|
+
expect(result.compressedEntries).toHaveLength(0);
|
|
795
|
+
expect(result.skipped).toEqual([{ startUserTimestamp: 1000, reason: "no-summary" }]);
|
|
796
|
+
expect(reports).toHaveLength(1);
|
|
797
|
+
expect(reports[0][0]).toBe("backfill-empty");
|
|
798
|
+
expect(reports[0][1]).toBe("1000");
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("fully-protected uncovered chain: plain no-summary skip, no backfill-empty diagnostic", async () => {
|
|
802
|
+
// Every middle id is protected -> extraction excludes all of them by design,
|
|
803
|
+
// not by span mismatch. Reporting backfill-empty here would be a false alarm.
|
|
804
|
+
const reports: Array<[string, string, string]> = [];
|
|
805
|
+
const { deps } = makeDeterministicDeps({
|
|
806
|
+
diagnosticsReport: (kind, dedupKey, detail) => reports.push([kind, dedupKey, detail]),
|
|
807
|
+
});
|
|
808
|
+
const chain: ChainRange = {
|
|
809
|
+
startUserTimestamp: 1000,
|
|
810
|
+
middleToolCallIds: ["c1", "c2"],
|
|
811
|
+
finalAssistantTimestamp: 1200,
|
|
812
|
+
protectedToolCallIds: ["c1", "c2"],
|
|
813
|
+
};
|
|
814
|
+
const result = await compressEligible([chain], 0, deps as any);
|
|
815
|
+
expect(result.compressedEntries).toHaveLength(0);
|
|
816
|
+
expect(result.skipped).toEqual([{ startUserTimestamp: 1000, reason: "no-summary" }]);
|
|
817
|
+
expect(reports).toHaveLength(0);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("empty-args chain compresses with a non-empty deterministic body", async () => {
|
|
821
|
+
const messages = [
|
|
822
|
+
{ role: "user", timestamp: 1000, content: [{ type: "text", text: "u" }] },
|
|
823
|
+
{ role: "assistant", timestamp: 1001, content: [{ type: "toolCall", id: "c1", name: "bash", input: {} }] },
|
|
824
|
+
{ role: "toolResult", toolCallId: "c1", toolName: "bash", timestamp: 1050, isError: false, content: [{ type: "text", text: "" }] },
|
|
825
|
+
{ role: "assistant", timestamp: 1200, content: [{ type: "text", text: "done" }] },
|
|
826
|
+
];
|
|
827
|
+
const chain: ChainRange = {
|
|
828
|
+
startUserTimestamp: 1000,
|
|
829
|
+
middleToolCallIds: ["c1"],
|
|
830
|
+
finalAssistantTimestamp: 1200,
|
|
831
|
+
protectedToolCallIds: [],
|
|
832
|
+
};
|
|
833
|
+
const { deps } = makeDeterministicDeps({ messages });
|
|
834
|
+
const result = await compressEligible([chain], 0, deps as any);
|
|
835
|
+
expect(result.compressedEntries).toHaveLength(1);
|
|
836
|
+
const body = result.compressedEntries[0].rangeSummaryText!;
|
|
837
|
+
expect(body).toContain("Calls: 1");
|
|
838
|
+
expect(body).toContain("Tools: bash x1");
|
|
839
|
+
expect(body.length).toBeGreaterThan(0);
|
|
840
|
+
});
|
|
841
|
+
});
|