pi-condense 2.4.3 → 2.6.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 +14 -0
- package/PRUNING.md +96 -54
- package/README.md +6 -1
- package/index.ts +28 -42
- package/package.json +1 -1
- package/src/batch-capture.test.ts +75 -1
- package/src/batch-capture.ts +22 -13
- package/src/chain-compressor.test.ts +114 -0
- package/src/chain-compressor.ts +29 -4
- package/src/chain-detector.test.ts +49 -0
- package/src/chain-detector.ts +7 -0
- package/src/chain-range-prune.test.ts +342 -7
- package/src/chain-range-prune.ts +161 -48
- package/src/commands.test.ts +31 -2
- package/src/commands.ts +25 -35
- package/src/config.test.ts +27 -1
- package/src/diagnostics.test.ts +114 -0
- package/src/diagnostics.ts +46 -0
- package/src/frontier.test.ts +138 -16
- package/src/frontier.ts +0 -1
- package/src/id-collision.integration.test.ts +251 -0
- package/src/indexer.test.ts +336 -0
- package/src/indexer.ts +168 -55
- package/src/occurrence-key.test.ts +57 -0
- package/src/occurrence-key.ts +36 -0
- package/src/orphan-sweep.test.ts +67 -0
- package/src/orphan-sweep.ts +40 -0
- package/src/oversized-spill.integration.test.ts +7 -2
- package/src/pruner.test.ts +471 -64
- package/src/pruner.ts +84 -54
- package/src/query-tool.test.ts +117 -0
- package/src/query-tool.ts +47 -31
- package/src/range-compression.integration.test.ts +7 -44
- package/src/recovery-grace.test.ts +13 -0
- package/src/recovery-grace.ts +12 -3
- package/src/spill.test.ts +108 -1
- package/src/spill.ts +5 -3
- package/src/summary-refs.test.ts +51 -1
- package/src/summary-refs.ts +15 -4
- package/src/test-support.ts +54 -0
- package/src/tree-browser.ts +2 -1
- package/src/types.ts +56 -49
- package/src/thinking-strip.test.ts +0 -257
- package/src/thinking-strip.ts +0 -83
package/src/pruner.test.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { pruneMessages, sizeMessages } from "./pruner.js";
|
|
3
|
-
import type { ChainCompressionConfig, ChainCompressionEntry, ThinkingStripConfig } from "./types.js";
|
|
4
3
|
import { ToolCallIndexer } from "./indexer.js";
|
|
4
|
+
import { CUSTOM_TYPE_INDEX } from "./types.js";
|
|
5
|
+
import type { ChainCompressionConfig, ChainCompressionEntry } from "./types.js";
|
|
6
|
+
import { DiagnosticSink } from "./diagnostics.js";
|
|
7
|
+
import { pruneWithZeroSweepAssertion } from "./test-support.js";
|
|
5
8
|
|
|
6
9
|
// Minimal mock exposing only the ToolCallIndexer surface that pruneMessages calls.
|
|
10
|
+
// `hasLegacyBareRecord` defaults to the bare `summarized` set: most of the fixture
|
|
11
|
+
// messages in this file carry no occurrence-keyed records, so the fail-closed lookup
|
|
12
|
+
// in pruneMessages falls through to this legacy-bare-id path for them, matching how
|
|
13
|
+
// pre-upgrade sessions behave (that path must stay covered - it is a supported
|
|
14
|
+
// shape). The "occurrence-keyed coverage" describe block below seeds `summarized` /
|
|
15
|
+
// `records` / `shortRefs` with `id@timestamp`-shaped keys instead, so those tests
|
|
16
|
+
// hit `isSummarized(key)` directly - the occurrence branch of the ladder - rather
|
|
17
|
+
// than falling through to `hasLegacyBareRecord`.
|
|
7
18
|
function makeMockIndexer({
|
|
8
19
|
summarized = new Set<string>(),
|
|
9
20
|
shortRefs = new Map<string, string>(),
|
|
@@ -19,6 +30,7 @@ function makeMockIndexer({
|
|
|
19
30
|
} = {}) {
|
|
20
31
|
return {
|
|
21
32
|
isSummarized: (id: string) => summarized.has(id),
|
|
33
|
+
hasLegacyBareRecord: (id: string) => summarized.has(id),
|
|
22
34
|
getShortRefForToolCallId: (id: string) => shortRefs.get(id),
|
|
23
35
|
getRecord: (id: string) => records.get(id),
|
|
24
36
|
getChainEntries: () => chainEntries,
|
|
@@ -46,6 +58,7 @@ describe("pruneMessages", () => {
|
|
|
46
58
|
shortRefs: new Map([["tc1", "t1"]]),
|
|
47
59
|
});
|
|
48
60
|
const messages = [
|
|
61
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
49
62
|
{
|
|
50
63
|
role: "toolResult",
|
|
51
64
|
toolCallId: "tc1",
|
|
@@ -57,8 +70,8 @@ describe("pruneMessages", () => {
|
|
|
57
70
|
];
|
|
58
71
|
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
59
72
|
expect(pruned).toBe(true);
|
|
60
|
-
expect(out[
|
|
61
|
-
expect(out[
|
|
73
|
+
expect(out[1].content[0].text).toContain("`t1`");
|
|
74
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
62
75
|
});
|
|
63
76
|
|
|
64
77
|
it("returns original array reference when nothing is summarized or compressed", () => {
|
|
@@ -318,9 +331,12 @@ describe("pruneMessages", () => {
|
|
|
318
331
|
spillPath: "/blobs/tc1.txt", isError: false, turnIndex: 0, timestamp: 1,
|
|
319
332
|
}]]),
|
|
320
333
|
});
|
|
321
|
-
const messages = [
|
|
334
|
+
const messages = [
|
|
335
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
336
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "bash", content: [{ type: "text", text: "x" }], isError: false, timestamp: 1 },
|
|
337
|
+
];
|
|
322
338
|
const { messages: out } = pruneMessages(messages, indexer);
|
|
323
|
-
const text = out[
|
|
339
|
+
const text = out[1].content[0].text as string;
|
|
324
340
|
expect(text).toContain("/blobs/tc1.txt");
|
|
325
341
|
expect(text).toContain("?");
|
|
326
342
|
expect(text).not.toContain("Summarized in pruner summary");
|
|
@@ -335,13 +351,16 @@ describe("pruneMessages", () => {
|
|
|
335
351
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
336
352
|
}]]),
|
|
337
353
|
});
|
|
338
|
-
const messages = [
|
|
339
|
-
role: "
|
|
340
|
-
|
|
341
|
-
|
|
354
|
+
const messages = [
|
|
355
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "fetch", input: {} }], timestamp: 0 },
|
|
356
|
+
{
|
|
357
|
+
role: "toolResult", toolCallId: "tc1", toolName: "fetch",
|
|
358
|
+
content: [{ type: "text", text: "huge" }], isError: false, timestamp: 1,
|
|
359
|
+
},
|
|
360
|
+
];
|
|
342
361
|
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
343
362
|
expect(pruned).toBe(true);
|
|
344
|
-
const text = out[
|
|
363
|
+
const text = out[1].content[0].text as string;
|
|
345
364
|
expect(text).toContain("/blobs/tc1.txt");
|
|
346
365
|
expect(text).toContain("PREVIEW-HEAD");
|
|
347
366
|
expect(text).toContain("1048576");
|
|
@@ -360,6 +379,7 @@ describe("pruneMessages", () => {
|
|
|
360
379
|
const indexer = makeMockIndexer({ chainEntries: [chainEntry] });
|
|
361
380
|
const messages = [
|
|
362
381
|
{ role: "user", content: "hi", timestamp: 100 },
|
|
382
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc-x", name: "bash", input: {} }], timestamp: 140 },
|
|
363
383
|
{
|
|
364
384
|
role: "toolResult",
|
|
365
385
|
toolCallId: "tc-x",
|
|
@@ -375,7 +395,7 @@ describe("pruneMessages", () => {
|
|
|
375
395
|
expect(pruned).toBe(false);
|
|
376
396
|
});
|
|
377
397
|
|
|
378
|
-
it("
|
|
398
|
+
it("leaves thinking blocks on every assistant turn (no thinking-strip phase)", () => {
|
|
379
399
|
const indexer = makeMockIndexer({ summarized: new Set(["c10"]), shortRefs: new Map([["c10", "t1"]]) });
|
|
380
400
|
const mkAsst = (ts: number) => ({
|
|
381
401
|
role: "assistant",
|
|
@@ -394,44 +414,22 @@ describe("pruneMessages", () => {
|
|
|
394
414
|
messages.push(mkAsst(10 + i));
|
|
395
415
|
messages.push({ role: "toolResult", toolCallId: id, toolName: "bash", content: [{ type: "text", text: "o" }], isError: false, timestamp: 100 + i });
|
|
396
416
|
}
|
|
397
|
-
const { messages: out, pruned } = pruneMessages(messages, indexer,
|
|
398
|
-
enabled: true,
|
|
399
|
-
keepLastTurns: 2,
|
|
417
|
+
const { messages: out, pruned } = pruneMessages(messages, indexer, {
|
|
418
|
+
enabled: true, rollingWindow: 0, stripFinalAssistantThinking: false, fuseRangeSummary: false,
|
|
400
419
|
});
|
|
420
|
+
// Phase 1 still fires: c10's toolResult is stub-replaced.
|
|
401
421
|
expect(pruned).toBe(true);
|
|
402
|
-
|
|
403
|
-
// Phase 1: c10 toolResult stub-replaced
|
|
404
422
|
const tr = out.find((m: any) => m.role === "toolResult" && m.toolCallId === "c10") as any;
|
|
405
423
|
expect(tr.content[0].text).toContain("`t1`");
|
|
406
|
-
|
|
407
|
-
// Phase 4: oldest 3 assistant turns stripped, last 2 keep thinking
|
|
424
|
+
// No phase strips thinking any more — all five assistants keep theirs.
|
|
408
425
|
const assistants = out.filter((m: any) => m.role === "assistant");
|
|
409
|
-
|
|
410
|
-
expect(assistants.
|
|
411
|
-
expect(assistants.slice(-2).every((a: any) => hasThinking(a))).toBe(true);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("threads thinkingBoundaryTimestamp into phase 4", () => {
|
|
415
|
-
const indexer = new ToolCallIndexer();
|
|
416
|
-
const strip: ThinkingStripConfig = { enabled: true, keepLastTurns: 16 };
|
|
417
|
-
const messages: any[] = [
|
|
418
|
-
{ role: "user", content: [{ type: "text", text: "go" }], timestamp: 1 },
|
|
419
|
-
{ role: "assistant", content: [{ type: "thinking", thinking: "old", thinkingSignature: "s" }, { type: "text", text: "a" }], timestamp: 10, usage: {}, stopReason: "stop" },
|
|
420
|
-
{ role: "assistant", content: [{ type: "thinking", thinking: "new", thinkingSignature: "s" }, { type: "text", text: "b" }], timestamp: 30, usage: {}, stopReason: "stop" },
|
|
421
|
-
];
|
|
422
|
-
// Boundary 20: ts=10 assistant older -> stripped; ts=30 kept.
|
|
423
|
-
// Live-count would strip nothing (2 assistants < keepLastTurns=16), so a pass
|
|
424
|
-
// proves the boundary arg reached phase 4.
|
|
425
|
-
const { messages: out, pruned } = pruneMessages(messages, indexer, undefined, undefined, strip, undefined, 0, 20);
|
|
426
|
-
expect(pruned).toBe(true);
|
|
427
|
-
const older = out.find((m: any) => m.timestamp === 10) as any;
|
|
428
|
-
const newer = out.find((m: any) => m.timestamp === 30) as any;
|
|
429
|
-
expect(older.content.some((c: any) => c.type === "thinking")).toBe(false);
|
|
430
|
-
expect(newer.content.some((c: any) => c.type === "thinking")).toBe(true);
|
|
426
|
+
expect(assistants.length).toBe(5);
|
|
427
|
+
expect(assistants.every((a: any) => a.content.some((c: any) => c.type === "thinking"))).toBe(true);
|
|
431
428
|
});
|
|
432
429
|
});
|
|
433
430
|
|
|
434
431
|
describe("render-time protection re-check", () => {
|
|
432
|
+
const skillAsst = { role: "assistant", content: [{ type: "toolCall", id: "tc-skill", name: "read", input: {} }], timestamp: 5 };
|
|
435
433
|
const skillMsg = {
|
|
436
434
|
role: "toolResult",
|
|
437
435
|
toolCallId: "tc-skill",
|
|
@@ -457,17 +455,17 @@ describe("render-time protection re-check", () => {
|
|
|
457
455
|
|
|
458
456
|
it("leaves a summarized record verbatim once its path matches protectedPaths", () => {
|
|
459
457
|
const { messages, pruned } = pruneMessages(
|
|
460
|
-
[skillMsg], indexer as any, undefined, undefined,
|
|
458
|
+
[skillAsst, skillMsg], indexer as any, undefined, undefined,
|
|
461
459
|
{ protectedTools: [], protectedPaths: ["**/skills/**/*.md"] },
|
|
462
460
|
);
|
|
463
461
|
expect(pruned).toBe(false);
|
|
464
|
-
expect(messages[
|
|
462
|
+
expect(messages[1].content[0].text).toBe("FULL SKILL BODY");
|
|
465
463
|
});
|
|
466
464
|
|
|
467
465
|
it("still stubs when no protection config is passed", () => {
|
|
468
|
-
const { messages, pruned } = pruneMessages([skillMsg], indexer as any);
|
|
466
|
+
const { messages, pruned } = pruneMessages([skillAsst, skillMsg], indexer as any);
|
|
469
467
|
expect(pruned).toBe(true);
|
|
470
|
-
expect(messages[
|
|
468
|
+
expect(messages[1].content[0].text).toContain("context_tree_query");
|
|
471
469
|
});
|
|
472
470
|
});
|
|
473
471
|
|
|
@@ -481,15 +479,20 @@ describe("pruneMessages recovery grace", () => {
|
|
|
481
479
|
timestamp,
|
|
482
480
|
});
|
|
483
481
|
const mkUser = (timestamp: number) => ({ role: "user", content: [{ type: "text", text: "go" }], timestamp });
|
|
482
|
+
const mkAsst = (toolCallId: string, toolName: string, timestamp: number) => ({
|
|
483
|
+
role: "assistant",
|
|
484
|
+
content: [{ type: "toolCall", id: toolCallId, name: toolName, input: {} }],
|
|
485
|
+
timestamp,
|
|
486
|
+
});
|
|
484
487
|
|
|
485
488
|
it("renders a context_tree_query recovery output verbatim at age 0 within grace", () => {
|
|
486
489
|
const indexer = makeMockIndexer({
|
|
487
490
|
summarized: new Set(["tc-recover"]),
|
|
488
491
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
489
492
|
});
|
|
490
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
491
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
492
|
-
expect(out[
|
|
493
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
494
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
495
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
493
496
|
});
|
|
494
497
|
|
|
495
498
|
it("stubs a context_tree_query recovery output aged past the grace window", () => {
|
|
@@ -497,8 +500,8 @@ describe("pruneMessages recovery grace", () => {
|
|
|
497
500
|
summarized: new Set(["tc-recover"]),
|
|
498
501
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
499
502
|
});
|
|
500
|
-
const messages: any[] = [mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
501
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
503
|
+
const messages: any[] = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
504
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
502
505
|
const tr = out.find((m: any) => m.toolCallId === "tc-recover") as any;
|
|
503
506
|
expect(tr.content[0].text).toContain("context_tree_query");
|
|
504
507
|
expect(tr.content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
@@ -509,10 +512,10 @@ describe("pruneMessages recovery grace", () => {
|
|
|
509
512
|
summarized: new Set(["tc-recover"]),
|
|
510
513
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
511
514
|
});
|
|
512
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
513
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
514
|
-
expect(out[
|
|
515
|
-
expect(out[
|
|
515
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
516
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 0);
|
|
517
|
+
expect(out[1].content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
518
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
516
519
|
});
|
|
517
520
|
|
|
518
521
|
it("does not apply the grace window to non-context_tree_query outputs", () => {
|
|
@@ -521,6 +524,7 @@ describe("pruneMessages recovery grace", () => {
|
|
|
521
524
|
shortRefs: new Map([["tc-bash", "t1"]]),
|
|
522
525
|
});
|
|
523
526
|
const messages = [
|
|
527
|
+
mkAsst("tc-bash", "bash", 0),
|
|
524
528
|
{
|
|
525
529
|
role: "toolResult",
|
|
526
530
|
toolCallId: "tc-bash",
|
|
@@ -530,9 +534,9 @@ describe("pruneMessages recovery grace", () => {
|
|
|
530
534
|
timestamp: 1,
|
|
531
535
|
},
|
|
532
536
|
];
|
|
533
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
534
|
-
expect(out[
|
|
535
|
-
expect(out[
|
|
537
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
538
|
+
expect(out[1].content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
539
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
536
540
|
});
|
|
537
541
|
|
|
538
542
|
it("isProtected precedence: a protected context_tree_query output stays verbatim even with grace off", () => {
|
|
@@ -544,9 +548,9 @@ describe("pruneMessages recovery grace", () => {
|
|
|
544
548
|
resultText: "", isError: false, turnIndex: 0, timestamp: 1,
|
|
545
549
|
}]]),
|
|
546
550
|
});
|
|
547
|
-
const messages: any[] = [mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
551
|
+
const messages: any[] = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
548
552
|
const { messages: out } = pruneMessages(
|
|
549
|
-
messages, indexer, undefined, undefined,
|
|
553
|
+
messages, indexer, undefined, undefined,
|
|
550
554
|
{ protectedTools: [], protectedPaths: ["**/skills/**/*.md"] },
|
|
551
555
|
0,
|
|
552
556
|
);
|
|
@@ -564,9 +568,9 @@ describe("pruneMessages recovery grace", () => {
|
|
|
564
568
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
565
569
|
}]]),
|
|
566
570
|
});
|
|
567
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
568
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
569
|
-
expect(out[
|
|
571
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
572
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
573
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
570
574
|
});
|
|
571
575
|
|
|
572
576
|
it("stubs a spilled context_tree_query recovery output aged past the grace window to the spill-pointer stub", () => {
|
|
@@ -579,8 +583,8 @@ describe("pruneMessages recovery grace", () => {
|
|
|
579
583
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
580
584
|
}]]),
|
|
581
585
|
});
|
|
582
|
-
const messages: any[] = [mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
583
|
-
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined,
|
|
586
|
+
const messages: any[] = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
587
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
584
588
|
const tr = out.find((m: any) => m.toolCallId === "tc-recover") as any;
|
|
585
589
|
expect(tr.content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
586
590
|
expect(tr.content[0].text).toContain("/blobs/tc-recover.txt");
|
|
@@ -588,11 +592,294 @@ describe("pruneMessages recovery grace", () => {
|
|
|
588
592
|
});
|
|
589
593
|
});
|
|
590
594
|
|
|
595
|
+
describe("occurrence-keyed stub replacement", () => {
|
|
596
|
+
it("stubs the summarized occurrence and leaves the live one verbatim", () => {
|
|
597
|
+
const idx = new ToolCallIndexer();
|
|
598
|
+
idx.addBatch(
|
|
599
|
+
{
|
|
600
|
+
turnIndex: 0,
|
|
601
|
+
timestamp: 1000,
|
|
602
|
+
assistantText: "",
|
|
603
|
+
toolCalls: [{ toolCallId: "bash_23", toolName: "bash", args: {}, resultText: "OLD", isError: false, resultTimestamp: 1150 }],
|
|
604
|
+
} as any,
|
|
605
|
+
() => {},
|
|
606
|
+
);
|
|
607
|
+
const messages: any[] = [
|
|
608
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 1100 },
|
|
609
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 1150 },
|
|
610
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 3100 },
|
|
611
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 3150 },
|
|
612
|
+
];
|
|
613
|
+
const out = pruneMessages(messages, idx);
|
|
614
|
+
expect(out.messages[1].content[0].text).toContain("Summarized in pruner summary");
|
|
615
|
+
expect(out.messages[3].content[0].text).toBe("LIVE");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("fail-closed: a timestamped result with no occurrence record is never stubbed", () => {
|
|
619
|
+
const idx = new ToolCallIndexer();
|
|
620
|
+
idx.addBatch(
|
|
621
|
+
{
|
|
622
|
+
turnIndex: 0,
|
|
623
|
+
timestamp: 1000,
|
|
624
|
+
assistantText: "",
|
|
625
|
+
toolCalls: [{ toolCallId: "bash_23", toolName: "bash", args: {}, resultText: "OLD", isError: false, resultTimestamp: 1150 }],
|
|
626
|
+
} as any,
|
|
627
|
+
() => {},
|
|
628
|
+
);
|
|
629
|
+
const messages: any[] = [
|
|
630
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 9100 },
|
|
631
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 9150 },
|
|
632
|
+
];
|
|
633
|
+
const out = pruneMessages(messages, idx);
|
|
634
|
+
expect(out.pruned).toBe(false);
|
|
635
|
+
expect(out.messages).toBe(messages);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("fail-closed: a mixed legacy+occurrence bare id does not stub a live later occurrence (F1 regression)", () => {
|
|
639
|
+
const idx = new ToolCallIndexer();
|
|
640
|
+
idx.reconstructFromSession({
|
|
641
|
+
sessionManager: {
|
|
642
|
+
getBranch: () => [
|
|
643
|
+
{
|
|
644
|
+
type: "custom",
|
|
645
|
+
customType: CUSTOM_TYPE_INDEX,
|
|
646
|
+
data: { toolCalls: [{ toolCallId: "bash_23", toolName: "bash", args: {}, resultText: "OLD-LEGACY", isError: false, turnIndex: 0, timestamp: 500 }] },
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
} as any);
|
|
651
|
+
idx.addBatch(
|
|
652
|
+
{
|
|
653
|
+
turnIndex: 1,
|
|
654
|
+
timestamp: 2000,
|
|
655
|
+
assistantText: "",
|
|
656
|
+
toolCalls: [{ toolCallId: "bash_23", toolName: "bash", args: {}, resultText: "MID", isError: false, resultTimestamp: 2150 }],
|
|
657
|
+
} as any,
|
|
658
|
+
() => {},
|
|
659
|
+
);
|
|
660
|
+
const messages: any[] = [
|
|
661
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 9100 },
|
|
662
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 9150 },
|
|
663
|
+
];
|
|
664
|
+
const out = pruneMessages(messages, idx);
|
|
665
|
+
expect(out.messages[1].content[0].text).toBe("LIVE");
|
|
666
|
+
expect(out.pruned).toBe(false);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("legacy bare-id records still stub (pre-upgrade sessions keep working)", () => {
|
|
670
|
+
const idx = new ToolCallIndexer();
|
|
671
|
+
idx.reconstructFromSession({
|
|
672
|
+
sessionManager: {
|
|
673
|
+
getBranch: () => [
|
|
674
|
+
{
|
|
675
|
+
type: "custom",
|
|
676
|
+
customType: CUSTOM_TYPE_INDEX,
|
|
677
|
+
data: { toolCalls: [{ toolCallId: "bash_7", toolName: "bash", args: {}, resultText: "OLD", isError: false, turnIndex: 0, timestamp: 500 }] },
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
},
|
|
681
|
+
} as any);
|
|
682
|
+
const messages: any[] = [
|
|
683
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_7", name: "bash", input: {} }], timestamp: 500 },
|
|
684
|
+
{ role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 550 },
|
|
685
|
+
];
|
|
686
|
+
const out = pruneMessages(messages, idx);
|
|
687
|
+
expect(out.pruned).toBe(true);
|
|
688
|
+
expect(out.messages[1].content[0].text).toContain("Summarized in pruner summary");
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("accepted limitation: a pure-legacy summarized bash_7 stubs a LIVE colliding bash_7 result (pre-upgrade sessions only)", () => {
|
|
692
|
+
const idx = new ToolCallIndexer();
|
|
693
|
+
idx.reconstructFromSession({
|
|
694
|
+
sessionManager: {
|
|
695
|
+
getBranch: () => [
|
|
696
|
+
{
|
|
697
|
+
type: "custom",
|
|
698
|
+
customType: CUSTOM_TYPE_INDEX,
|
|
699
|
+
data: { toolCalls: [{ toolCallId: "bash_7", toolName: "bash", args: {}, resultText: "OLD", isError: false, turnIndex: 0, timestamp: 500 }] },
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
} as any);
|
|
704
|
+
|
|
705
|
+
// No migration: a bare-keyed legacy record has no occurrence-keyed
|
|
706
|
+
// siblings, so hasLegacyBareRecord stays true even though a later, live,
|
|
707
|
+
// unrelated occurrence of the same reused provider id now exists.
|
|
708
|
+
expect(idx.hasLegacyBareRecord("bash_7")).toBe(true);
|
|
709
|
+
|
|
710
|
+
const messages: any[] = [
|
|
711
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_7", name: "bash", input: {} }], timestamp: 9100 },
|
|
712
|
+
{ role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 9150 },
|
|
713
|
+
];
|
|
714
|
+
const out = pruneMessages(messages, idx);
|
|
715
|
+
// Accepted, documented exposure (PRUNING.md): a session spanning the
|
|
716
|
+
// upgrade keeps this pre-upgrade behavior for its legacy half - the live
|
|
717
|
+
// result is stub-replaced with the stale legacy record's content.
|
|
718
|
+
expect(out.pruned).toBe(true);
|
|
719
|
+
expect(out.messages[1].content[0].text).toContain("Summarized in pruner summary");
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
describe("orphan sweep in pruneMessages", () => {
|
|
724
|
+
it("a clean render returns the identical array reference with pruned false", () => {
|
|
725
|
+
const idx = new ToolCallIndexer();
|
|
726
|
+
const messages: any[] = [
|
|
727
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "a", name: "bash", input: {} }], timestamp: 1 },
|
|
728
|
+
{ role: "toolResult", toolCallId: "a", toolName: "bash", content: [{ type: "text", text: "x" }], isError: false, timestamp: 2 },
|
|
729
|
+
];
|
|
730
|
+
const out = pruneMessages(messages, idx);
|
|
731
|
+
expect(out.messages).toBe(messages);
|
|
732
|
+
expect(out.pruned).toBe(false);
|
|
733
|
+
expect(out.beforeChars).toBe(0);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("sweeps an orphan and reports the diagnostic once across repeated renders of the same input", () => {
|
|
737
|
+
const idx = new ToolCallIndexer();
|
|
738
|
+
const appended: any[] = [];
|
|
739
|
+
const sink = new DiagnosticSink((_customType, data) => appended.push(data));
|
|
740
|
+
const messages: any[] = [
|
|
741
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "a", name: "bash", input: {} }], timestamp: 1 },
|
|
742
|
+
{ role: "toolResult", toolCallId: "a", toolName: "bash", content: [{ type: "text", text: "x" }], isError: false, timestamp: 2 },
|
|
743
|
+
{ role: "toolResult", toolCallId: "ghost", toolName: "bash", content: [{ type: "text", text: "y" }], isError: false, timestamp: 3 },
|
|
744
|
+
];
|
|
745
|
+
const first = pruneMessages(messages, idx, undefined, undefined, undefined, 0, sink as any);
|
|
746
|
+
expect(first.pruned).toBe(true);
|
|
747
|
+
expect(first.messages).toHaveLength(2);
|
|
748
|
+
expect(first.messages.some((m: any) => m.toolCallId === "ghost")).toBe(false);
|
|
749
|
+
expect(first.messages.some((m: any) => m.toolCallId === "a")).toBe(true);
|
|
750
|
+
|
|
751
|
+
// Same orphan on a second render of the same (still-unsupplemented) input
|
|
752
|
+
// must not write a second diagnostic entry: DiagnosticSink dedups per
|
|
753
|
+
// (kind, dedupKey), and pruneMessages must compute the same dedupKey both
|
|
754
|
+
// times for the same swept id set.
|
|
755
|
+
const second = pruneMessages(messages, idx, undefined, undefined, undefined, 0, sink as any);
|
|
756
|
+
expect(second.pruned).toBe(true);
|
|
757
|
+
|
|
758
|
+
expect(appended).toHaveLength(1);
|
|
759
|
+
expect(appended[0].kind).toBe("orphan-sweep");
|
|
760
|
+
expect(appended[0].detail).toContain("ghost");
|
|
761
|
+
expect(appended[0].detail).toContain("swept 1 orphan");
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("bounds the sweep dedup key and truncation marker for a large orphan set", () => {
|
|
765
|
+
const idx = new ToolCallIndexer();
|
|
766
|
+
const reports: any[] = [];
|
|
767
|
+
const sink = { report: (kind: string, key: string, detail: string) => reports.push({ kind, key, detail }), counts: () => ({}) as any };
|
|
768
|
+
const messages: any[] = [
|
|
769
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "keep", name: "bash", input: {} }], timestamp: 1 },
|
|
770
|
+
{ role: "toolResult", toolCallId: "keep", toolName: "bash", content: [{ type: "text", text: "x" }], isError: false, timestamp: 2 },
|
|
771
|
+
];
|
|
772
|
+
for (let i = 0; i < 12; i++) {
|
|
773
|
+
messages.push({ role: "toolResult", toolCallId: `ghost-${i}`, toolName: "bash", content: [{ type: "text", text: "y" }], isError: false, timestamp: 3 + i });
|
|
774
|
+
}
|
|
775
|
+
const out = pruneMessages(messages, idx, undefined, undefined, undefined, 0, sink as any);
|
|
776
|
+
expect(out.pruned).toBe(true);
|
|
777
|
+
expect(reports).toHaveLength(1);
|
|
778
|
+
// A short, bounded hash key regardless of how many ids were swept.
|
|
779
|
+
expect(reports[0].key.length).toBe(16);
|
|
780
|
+
expect(reports[0].detail).toContain("swept 12 orphan");
|
|
781
|
+
expect(reports[0].detail).toContain("... +7 more");
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe("occurrence-keyed coverage via mock indexer (spill / protection / grace)", () => {
|
|
786
|
+
it("stub-replaces via a direct occurrence-key hit (not the legacy branch)", () => {
|
|
787
|
+
const indexer = makeMockIndexer({
|
|
788
|
+
summarized: new Set(["tc1@1500"]),
|
|
789
|
+
shortRefs: new Map([["tc1@1500", "t1"]]),
|
|
790
|
+
});
|
|
791
|
+
const messages = [
|
|
792
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 1400 },
|
|
793
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "bash", content: [{ type: "text", text: "big output" }], isError: false, timestamp: 1500 },
|
|
794
|
+
];
|
|
795
|
+
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
796
|
+
expect(pruned).toBe(true);
|
|
797
|
+
expect(out[1].content[0].text).toContain("`t1`");
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it("emits the mechanical spill stub via a direct occurrence-key hit", () => {
|
|
801
|
+
const indexer = makeMockIndexer({
|
|
802
|
+
summarized: new Set(["tc1@1500"]),
|
|
803
|
+
records: new Map([["tc1@1500", {
|
|
804
|
+
toolCallId: "tc1", toolName: "fetch", args: { url: "https://x" },
|
|
805
|
+
resultText: "", resultPreview: "PREVIEW-HEAD", spillPath: "/blobs/tc1.txt",
|
|
806
|
+
spillBytes: 1048576, isError: false, turnIndex: 0, resultTimestamp: 1500, timestamp: 1400,
|
|
807
|
+
}]]),
|
|
808
|
+
});
|
|
809
|
+
const messages = [
|
|
810
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "fetch", input: {} }], timestamp: 1400 },
|
|
811
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "fetch", content: [{ type: "text", text: "huge" }], isError: false, timestamp: 1500 },
|
|
812
|
+
];
|
|
813
|
+
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
814
|
+
expect(pruned).toBe(true);
|
|
815
|
+
const text = out[1].content[0].text as string;
|
|
816
|
+
expect(text).toContain("/blobs/tc1.txt");
|
|
817
|
+
expect(text).toContain("PREVIEW-HEAD");
|
|
818
|
+
expect(text).not.toContain("Summarized in pruner summary");
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("render-time protection re-check applies to a direct occurrence-key hit", () => {
|
|
822
|
+
const indexer = makeMockIndexer({
|
|
823
|
+
summarized: new Set(["tc-skill@1500"]),
|
|
824
|
+
shortRefs: new Map([["tc-skill@1500", "t1"]]),
|
|
825
|
+
records: new Map([["tc-skill@1500", {
|
|
826
|
+
toolCallId: "tc-skill", toolName: "read", args: { path: "/h/skills/x/SKILL.md" },
|
|
827
|
+
resultText: "", isError: false, turnIndex: 0, resultTimestamp: 1500, timestamp: 1400,
|
|
828
|
+
}]]),
|
|
829
|
+
});
|
|
830
|
+
const messages = [
|
|
831
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc-skill", name: "read", input: {} }], timestamp: 1400 },
|
|
832
|
+
{ role: "toolResult", toolCallId: "tc-skill", toolName: "read", content: [{ type: "text", text: "FULL SKILL BODY" }], isError: false, timestamp: 1500 },
|
|
833
|
+
];
|
|
834
|
+
const { messages: out, pruned } = pruneMessages(
|
|
835
|
+
messages, indexer, undefined, undefined,
|
|
836
|
+
{ protectedTools: [], protectedPaths: ["**/skills/**/*.md"] },
|
|
837
|
+
);
|
|
838
|
+
expect(pruned).toBe(false);
|
|
839
|
+
expect(out[1].content[0].text).toBe("FULL SKILL BODY");
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("recovery grace protects a direct occurrence-key hit at age 0", () => {
|
|
843
|
+
const indexer = makeMockIndexer({
|
|
844
|
+
summarized: new Set(["tc-recover@1500"]),
|
|
845
|
+
shortRefs: new Map([["tc-recover@1500", "t1"]]),
|
|
846
|
+
});
|
|
847
|
+
const messages = [
|
|
848
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc-recover", name: "context_tree_query", input: {} }], timestamp: 1400 },
|
|
849
|
+
{ role: "toolResult", toolCallId: "tc-recover", toolName: "context_tree_query", content: [{ type: "text", text: "VERBATIM RECOVERY OUTPUT" }], isError: false, timestamp: 1500 },
|
|
850
|
+
];
|
|
851
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
852
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("a graced occurrence does not protect a different occurrence of the same reused bare id", () => {
|
|
856
|
+
const indexer = makeMockIndexer({
|
|
857
|
+
summarized: new Set(["reused@1500", "reused@9500"]),
|
|
858
|
+
shortRefs: new Map([["reused@1500", "t1"], ["reused@9500", "t2"]]),
|
|
859
|
+
});
|
|
860
|
+
const messages = [
|
|
861
|
+
// Graced context_tree_query recovery at occurrence reused@1500 (age 0).
|
|
862
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "reused", name: "context_tree_query", input: {} }], timestamp: 1400 },
|
|
863
|
+
{ role: "toolResult", toolCallId: "reused", toolName: "context_tree_query", content: [{ type: "text", text: "VERBATIM RECOVERY OUTPUT" }], isError: false, timestamp: 1500 },
|
|
864
|
+
// A LATER, unrelated summarized occurrence of the same reused provider id.
|
|
865
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "reused", name: "bash", input: {} }], timestamp: 9400 },
|
|
866
|
+
{ role: "toolResult", toolCallId: "reused", toolName: "bash", content: [{ type: "text", text: "different output" }], isError: false, timestamp: 9500 },
|
|
867
|
+
];
|
|
868
|
+
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
869
|
+
// The grace-protected recovery output stays verbatim...
|
|
870
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
871
|
+
// ...but the later, different occurrence of the same bare id is NOT
|
|
872
|
+
// shielded by that grace entry - it gets stubbed on its own merits.
|
|
873
|
+
expect(out[3].content[0].text).toContain("`t2`");
|
|
874
|
+
expect(out[3].content[0].text).not.toBe("different output");
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
|
|
591
878
|
describe("sizeMessages", () => {
|
|
592
879
|
it("counts hidden fields (thinking blocks), not just visible text", () => {
|
|
593
880
|
// Two messages with identical visible .text but different hidden content.
|
|
594
881
|
// sizeMessages must count the full serialized weight so all reclaim
|
|
595
|
-
// mechanisms (
|
|
882
|
+
// mechanisms (stub-replace, error-purge, chain-range-prune) register correctly.
|
|
596
883
|
const withThinking = [{
|
|
597
884
|
role: "assistant",
|
|
598
885
|
content: [
|
|
@@ -630,6 +917,7 @@ describe("pruneMessages beforeChars/afterChars", () => {
|
|
|
630
917
|
shortRefs: new Map([["tc1", "t1"]]),
|
|
631
918
|
});
|
|
632
919
|
const messages = [
|
|
920
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
633
921
|
{
|
|
634
922
|
role: "toolResult",
|
|
635
923
|
toolCallId: "tc1",
|
|
@@ -644,5 +932,124 @@ describe("pruneMessages beforeChars/afterChars", () => {
|
|
|
644
932
|
expect(result.beforeChars).toBe(sizeMessages(messages));
|
|
645
933
|
expect(result.afterChars).toBe(sizeMessages(result.messages));
|
|
646
934
|
expect(result.afterChars).toBeLessThan(result.beforeChars);
|
|
935
|
+
expect(result.messages).toHaveLength(2);
|
|
647
936
|
});
|
|
648
937
|
});
|
|
938
|
+
|
|
939
|
+
describe("G4/C3: orphan-sweep zero-fire proof across pruner fixtures", () => {
|
|
940
|
+
// Wraps a representative set of existing pruneMessages fixtures with a
|
|
941
|
+
// counting DiagnosticSink (pruneWithZeroSweepAssertion, src/test-support.ts)
|
|
942
|
+
// and fails if the orphan-sweep diagnostic ever fires. Deliberately excludes
|
|
943
|
+
// the two tests in "orphan sweep in pruneMessages" above that construct an
|
|
944
|
+
// orphan on purpose - those pin the OPPOSITE contract (the sweep firing when
|
|
945
|
+
// it should).
|
|
946
|
+
const fixtures: Array<[string, () => void]> = [
|
|
947
|
+
["stub-replaces a summarized tool result", () => {
|
|
948
|
+
const indexer = makeMockIndexer({ summarized: new Set(["tc1"]), shortRefs: new Map([["tc1", "t1"]]) });
|
|
949
|
+
const messages = [
|
|
950
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
951
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "bash", content: [{ type: "text", text: "big output" }], isError: false, timestamp: 1 },
|
|
952
|
+
];
|
|
953
|
+
pruneWithZeroSweepAssertion(messages, indexer);
|
|
954
|
+
}],
|
|
955
|
+
["applies chain compression after stub-replace", () => {
|
|
956
|
+
const toolCallId = "tc-mid";
|
|
957
|
+
const chainEntry: ChainCompressionEntry = {
|
|
958
|
+
blockId: "b1", startUserTimestamp: 100, droppedToolCallIds: [toolCallId],
|
|
959
|
+
finalAssistantTimestamp: 300, toolRefs: ["t1"], compressedAt: 999,
|
|
960
|
+
};
|
|
961
|
+
const indexer = makeMockIndexer({
|
|
962
|
+
summarized: new Set([toolCallId]), shortRefs: new Map([[toolCallId, "t1"]]),
|
|
963
|
+
chainEntries: [chainEntry], summaryBodyMap: new Map([[toolCallId, "ran bash, got results"]]),
|
|
964
|
+
});
|
|
965
|
+
const messages: any[] = [
|
|
966
|
+
{ role: "user", content: [{ type: "text", text: "do it" }], timestamp: 100 },
|
|
967
|
+
{ role: "assistant", content: [{ type: "toolCall", id: toolCallId, name: "bash", arguments: {} }], timestamp: 200, usage: {}, stopReason: "tool_use" },
|
|
968
|
+
{ role: "toolResult", toolCallId, toolName: "bash", content: [{ type: "text", text: "output" }], isError: false, timestamp: 210 },
|
|
969
|
+
{ role: "assistant", content: [{ type: "text", text: "done" }], timestamp: 300, usage: {}, stopReason: "end_turn" },
|
|
970
|
+
];
|
|
971
|
+
pruneWithZeroSweepAssertion(messages, indexer, enabledCC);
|
|
972
|
+
}],
|
|
973
|
+
["purges errored toolCall args through errorPurge wiring", () => {
|
|
974
|
+
const indexer = makeMockIndexer();
|
|
975
|
+
const largeArgs = { content: "x".repeat(200) };
|
|
976
|
+
const messages: any[] = [
|
|
977
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc-err", name: "write", arguments: largeArgs }], timestamp: 100, usage: {}, stopReason: "tool_use" },
|
|
978
|
+
{ role: "toolResult", toolCallId: "tc-err", toolName: "write", content: [{ type: "text", text: "Error: permission denied" }], isError: true, timestamp: 110 },
|
|
979
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc2", name: "bash", arguments: { cmd: "ls" } }], timestamp: 200, usage: {}, stopReason: "tool_use" },
|
|
980
|
+
{ role: "toolResult", toolCallId: "tc2", toolName: "bash", content: [{ type: "text", text: "ok" }], isError: false, timestamp: 210 },
|
|
981
|
+
];
|
|
982
|
+
pruneWithZeroSweepAssertion(
|
|
983
|
+
messages, indexer,
|
|
984
|
+
{ enabled: false, rollingWindow: 3, stripFinalAssistantThinking: true, fuseRangeSummary: false },
|
|
985
|
+
{ enabled: true, cooldownTurns: 2, minArgChars: 100 },
|
|
986
|
+
);
|
|
987
|
+
}],
|
|
988
|
+
["legacy bare-id records still stub (pre-upgrade sessions)", () => {
|
|
989
|
+
const idx = new ToolCallIndexer();
|
|
990
|
+
idx.reconstructFromSession({
|
|
991
|
+
sessionManager: {
|
|
992
|
+
getBranch: () => [
|
|
993
|
+
{ type: "custom", customType: CUSTOM_TYPE_INDEX, data: { toolCalls: [{ toolCallId: "bash_7", toolName: "bash", args: {}, resultText: "OLD", isError: false, turnIndex: 0, timestamp: 500 }] } },
|
|
994
|
+
{ type: "message", message: { role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 550 } },
|
|
995
|
+
],
|
|
996
|
+
},
|
|
997
|
+
} as any);
|
|
998
|
+
const messages: any[] = [
|
|
999
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_7", name: "bash", input: {} }], timestamp: 500 },
|
|
1000
|
+
{ role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 550 },
|
|
1001
|
+
];
|
|
1002
|
+
pruneWithZeroSweepAssertion(messages, idx);
|
|
1003
|
+
}],
|
|
1004
|
+
["occurrence-keyed: stubs the summarized occurrence and leaves the live one verbatim", () => {
|
|
1005
|
+
const idx = new ToolCallIndexer();
|
|
1006
|
+
idx.addBatch(
|
|
1007
|
+
{ turnIndex: 0, timestamp: 1000, assistantText: "", toolCalls: [{ toolCallId: "bash_23", toolName: "bash", args: {}, resultText: "OLD", isError: false, resultTimestamp: 1150 }] } as any,
|
|
1008
|
+
() => {},
|
|
1009
|
+
);
|
|
1010
|
+
const messages: any[] = [
|
|
1011
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 1100 },
|
|
1012
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 1150 },
|
|
1013
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_23", name: "bash", input: {} }], timestamp: 3100 },
|
|
1014
|
+
{ role: "toolResult", toolCallId: "bash_23", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 3150 },
|
|
1015
|
+
];
|
|
1016
|
+
pruneWithZeroSweepAssertion(messages, idx);
|
|
1017
|
+
}],
|
|
1018
|
+
["G1 conformance fixture: the accepted pre-upgrade legacy collision case still triggers no orphan sweep", () => {
|
|
1019
|
+
const idx = new ToolCallIndexer();
|
|
1020
|
+
idx.reconstructFromSession({
|
|
1021
|
+
sessionManager: {
|
|
1022
|
+
getBranch: () => [
|
|
1023
|
+
{ type: "custom", customType: CUSTOM_TYPE_INDEX, data: { toolCalls: [{ toolCallId: "bash_7", toolName: "bash", args: {}, resultText: "OLD", isError: false, turnIndex: 0, timestamp: 500 }] } },
|
|
1024
|
+
{ type: "message", message: { role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "OLD" }], isError: false, timestamp: 550 } },
|
|
1025
|
+
{ type: "message", message: { role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 9150 } },
|
|
1026
|
+
],
|
|
1027
|
+
},
|
|
1028
|
+
} as any);
|
|
1029
|
+
const messages: any[] = [
|
|
1030
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "bash_7", name: "bash", input: {} }], timestamp: 9100 },
|
|
1031
|
+
{ role: "toolResult", toolCallId: "bash_7", toolName: "bash", content: [{ type: "text", text: "LIVE" }], isError: false, timestamp: 9150 },
|
|
1032
|
+
];
|
|
1033
|
+
pruneWithZeroSweepAssertion(messages, idx);
|
|
1034
|
+
}],
|
|
1035
|
+
["spill mechanical stub for a spilled record", () => {
|
|
1036
|
+
const indexer = makeMockIndexer({
|
|
1037
|
+
summarized: new Set(["tc1"]),
|
|
1038
|
+
records: new Map([["tc1", {
|
|
1039
|
+
toolCallId: "tc1", toolName: "fetch", args: { url: "https://x" },
|
|
1040
|
+
resultText: "", resultPreview: "PREVIEW-HEAD", spillPath: "/blobs/tc1.txt",
|
|
1041
|
+
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
1042
|
+
}]]),
|
|
1043
|
+
});
|
|
1044
|
+
const messages = [
|
|
1045
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "fetch", input: {} }], timestamp: 0 },
|
|
1046
|
+
{ role: "toolResult", toolCallId: "tc1", toolName: "fetch", content: [{ type: "text", text: "huge" }], isError: false, timestamp: 1 },
|
|
1047
|
+
];
|
|
1048
|
+
pruneWithZeroSweepAssertion(messages, indexer);
|
|
1049
|
+
}],
|
|
1050
|
+
];
|
|
1051
|
+
|
|
1052
|
+
for (const [name, run] of fixtures) {
|
|
1053
|
+
it(`zero orphan sweeps: ${name}`, run);
|
|
1054
|
+
}
|
|
1055
|
+
});
|