pi-condense 2.5.0 → 2.7.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 +20 -0
- package/PRUNING.md +138 -23
- package/README.md +17 -1
- package/index.ts +305 -116
- 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 +168 -5
- package/src/commands.ts +44 -11
- package/src/context-metrics.test.ts +335 -0
- package/src/context-metrics.ts +152 -0
- package/src/diagnostics.test.ts +114 -0
- package/src/diagnostics.ts +46 -0
- package/src/frontier.test.ts +1 -0
- 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 +456 -25
- package/src/pruner.ts +84 -36
- package/src/query-tool.test.ts +117 -0
- package/src/query-tool.ts +47 -31
- package/src/range-compression.integration.test.ts +6 -1
- package/src/recovery-grace.test.ts +13 -0
- package/src/recovery-grace.ts +12 -3
- package/src/reload-rearm.integration.test.ts +647 -0
- package/src/spill.test.ts +108 -1
- package/src/spill.ts +5 -3
- package/src/summarizer-wiring.test.ts +2 -0
- 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 +89 -10
package/src/pruner.test.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { pruneMessages, sizeMessages } from "./pruner.js";
|
|
3
|
+
import { ToolCallIndexer } from "./indexer.js";
|
|
4
|
+
import { CUSTOM_TYPE_INDEX } from "./types.js";
|
|
3
5
|
import type { ChainCompressionConfig, ChainCompressionEntry } from "./types.js";
|
|
6
|
+
import { DiagnosticSink } from "./diagnostics.js";
|
|
7
|
+
import { pruneWithZeroSweepAssertion } from "./test-support.js";
|
|
4
8
|
|
|
5
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`.
|
|
6
18
|
function makeMockIndexer({
|
|
7
19
|
summarized = new Set<string>(),
|
|
8
20
|
shortRefs = new Map<string, string>(),
|
|
@@ -18,6 +30,7 @@ function makeMockIndexer({
|
|
|
18
30
|
} = {}) {
|
|
19
31
|
return {
|
|
20
32
|
isSummarized: (id: string) => summarized.has(id),
|
|
33
|
+
hasLegacyBareRecord: (id: string) => summarized.has(id),
|
|
21
34
|
getShortRefForToolCallId: (id: string) => shortRefs.get(id),
|
|
22
35
|
getRecord: (id: string) => records.get(id),
|
|
23
36
|
getChainEntries: () => chainEntries,
|
|
@@ -45,6 +58,7 @@ describe("pruneMessages", () => {
|
|
|
45
58
|
shortRefs: new Map([["tc1", "t1"]]),
|
|
46
59
|
});
|
|
47
60
|
const messages = [
|
|
61
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
48
62
|
{
|
|
49
63
|
role: "toolResult",
|
|
50
64
|
toolCallId: "tc1",
|
|
@@ -56,8 +70,8 @@ describe("pruneMessages", () => {
|
|
|
56
70
|
];
|
|
57
71
|
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
58
72
|
expect(pruned).toBe(true);
|
|
59
|
-
expect(out[
|
|
60
|
-
expect(out[
|
|
73
|
+
expect(out[1].content[0].text).toContain("`t1`");
|
|
74
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
it("returns original array reference when nothing is summarized or compressed", () => {
|
|
@@ -317,9 +331,12 @@ describe("pruneMessages", () => {
|
|
|
317
331
|
spillPath: "/blobs/tc1.txt", isError: false, turnIndex: 0, timestamp: 1,
|
|
318
332
|
}]]),
|
|
319
333
|
});
|
|
320
|
-
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
|
+
];
|
|
321
338
|
const { messages: out } = pruneMessages(messages, indexer);
|
|
322
|
-
const text = out[
|
|
339
|
+
const text = out[1].content[0].text as string;
|
|
323
340
|
expect(text).toContain("/blobs/tc1.txt");
|
|
324
341
|
expect(text).toContain("?");
|
|
325
342
|
expect(text).not.toContain("Summarized in pruner summary");
|
|
@@ -334,13 +351,16 @@ describe("pruneMessages", () => {
|
|
|
334
351
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
335
352
|
}]]),
|
|
336
353
|
});
|
|
337
|
-
const messages = [
|
|
338
|
-
role: "
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
];
|
|
341
361
|
const { messages: out, pruned } = pruneMessages(messages, indexer);
|
|
342
362
|
expect(pruned).toBe(true);
|
|
343
|
-
const text = out[
|
|
363
|
+
const text = out[1].content[0].text as string;
|
|
344
364
|
expect(text).toContain("/blobs/tc1.txt");
|
|
345
365
|
expect(text).toContain("PREVIEW-HEAD");
|
|
346
366
|
expect(text).toContain("1048576");
|
|
@@ -359,6 +379,7 @@ describe("pruneMessages", () => {
|
|
|
359
379
|
const indexer = makeMockIndexer({ chainEntries: [chainEntry] });
|
|
360
380
|
const messages = [
|
|
361
381
|
{ role: "user", content: "hi", timestamp: 100 },
|
|
382
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc-x", name: "bash", input: {} }], timestamp: 140 },
|
|
362
383
|
{
|
|
363
384
|
role: "toolResult",
|
|
364
385
|
toolCallId: "tc-x",
|
|
@@ -408,6 +429,7 @@ describe("pruneMessages", () => {
|
|
|
408
429
|
});
|
|
409
430
|
|
|
410
431
|
describe("render-time protection re-check", () => {
|
|
432
|
+
const skillAsst = { role: "assistant", content: [{ type: "toolCall", id: "tc-skill", name: "read", input: {} }], timestamp: 5 };
|
|
411
433
|
const skillMsg = {
|
|
412
434
|
role: "toolResult",
|
|
413
435
|
toolCallId: "tc-skill",
|
|
@@ -433,17 +455,17 @@ describe("render-time protection re-check", () => {
|
|
|
433
455
|
|
|
434
456
|
it("leaves a summarized record verbatim once its path matches protectedPaths", () => {
|
|
435
457
|
const { messages, pruned } = pruneMessages(
|
|
436
|
-
[skillMsg], indexer as any, undefined, undefined,
|
|
458
|
+
[skillAsst, skillMsg], indexer as any, undefined, undefined,
|
|
437
459
|
{ protectedTools: [], protectedPaths: ["**/skills/**/*.md"] },
|
|
438
460
|
);
|
|
439
461
|
expect(pruned).toBe(false);
|
|
440
|
-
expect(messages[
|
|
462
|
+
expect(messages[1].content[0].text).toBe("FULL SKILL BODY");
|
|
441
463
|
});
|
|
442
464
|
|
|
443
465
|
it("still stubs when no protection config is passed", () => {
|
|
444
|
-
const { messages, pruned } = pruneMessages([skillMsg], indexer as any);
|
|
466
|
+
const { messages, pruned } = pruneMessages([skillAsst, skillMsg], indexer as any);
|
|
445
467
|
expect(pruned).toBe(true);
|
|
446
|
-
expect(messages[
|
|
468
|
+
expect(messages[1].content[0].text).toContain("context_tree_query");
|
|
447
469
|
});
|
|
448
470
|
});
|
|
449
471
|
|
|
@@ -457,15 +479,20 @@ describe("pruneMessages recovery grace", () => {
|
|
|
457
479
|
timestamp,
|
|
458
480
|
});
|
|
459
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
|
+
});
|
|
460
487
|
|
|
461
488
|
it("renders a context_tree_query recovery output verbatim at age 0 within grace", () => {
|
|
462
489
|
const indexer = makeMockIndexer({
|
|
463
490
|
summarized: new Set(["tc-recover"]),
|
|
464
491
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
465
492
|
});
|
|
466
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
493
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
467
494
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
468
|
-
expect(out[
|
|
495
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
469
496
|
});
|
|
470
497
|
|
|
471
498
|
it("stubs a context_tree_query recovery output aged past the grace window", () => {
|
|
@@ -473,7 +500,7 @@ describe("pruneMessages recovery grace", () => {
|
|
|
473
500
|
summarized: new Set(["tc-recover"]),
|
|
474
501
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
475
502
|
});
|
|
476
|
-
const messages: any[] = [mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
503
|
+
const messages: any[] = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
477
504
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
478
505
|
const tr = out.find((m: any) => m.toolCallId === "tc-recover") as any;
|
|
479
506
|
expect(tr.content[0].text).toContain("context_tree_query");
|
|
@@ -485,10 +512,10 @@ describe("pruneMessages recovery grace", () => {
|
|
|
485
512
|
summarized: new Set(["tc-recover"]),
|
|
486
513
|
shortRefs: new Map([["tc-recover", "t1"]]),
|
|
487
514
|
});
|
|
488
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
515
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
489
516
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 0);
|
|
490
|
-
expect(out[
|
|
491
|
-
expect(out[
|
|
517
|
+
expect(out[1].content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
518
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
492
519
|
});
|
|
493
520
|
|
|
494
521
|
it("does not apply the grace window to non-context_tree_query outputs", () => {
|
|
@@ -497,6 +524,7 @@ describe("pruneMessages recovery grace", () => {
|
|
|
497
524
|
shortRefs: new Map([["tc-bash", "t1"]]),
|
|
498
525
|
});
|
|
499
526
|
const messages = [
|
|
527
|
+
mkAsst("tc-bash", "bash", 0),
|
|
500
528
|
{
|
|
501
529
|
role: "toolResult",
|
|
502
530
|
toolCallId: "tc-bash",
|
|
@@ -507,8 +535,8 @@ describe("pruneMessages recovery grace", () => {
|
|
|
507
535
|
},
|
|
508
536
|
];
|
|
509
537
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
510
|
-
expect(out[
|
|
511
|
-
expect(out[
|
|
538
|
+
expect(out[1].content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
539
|
+
expect(out[1].content[0].text).toContain("context_tree_query");
|
|
512
540
|
});
|
|
513
541
|
|
|
514
542
|
it("isProtected precedence: a protected context_tree_query output stays verbatim even with grace off", () => {
|
|
@@ -520,7 +548,7 @@ describe("pruneMessages recovery grace", () => {
|
|
|
520
548
|
resultText: "", isError: false, turnIndex: 0, timestamp: 1,
|
|
521
549
|
}]]),
|
|
522
550
|
});
|
|
523
|
-
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)];
|
|
524
552
|
const { messages: out } = pruneMessages(
|
|
525
553
|
messages, indexer, undefined, undefined,
|
|
526
554
|
{ protectedTools: [], protectedPaths: ["**/skills/**/*.md"] },
|
|
@@ -540,9 +568,9 @@ describe("pruneMessages recovery grace", () => {
|
|
|
540
568
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
541
569
|
}]]),
|
|
542
570
|
});
|
|
543
|
-
const messages = [mkQueryResult("tc-recover", 1)];
|
|
571
|
+
const messages = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1)];
|
|
544
572
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
545
|
-
expect(out[
|
|
573
|
+
expect(out[1].content[0].text).toBe("VERBATIM RECOVERY OUTPUT");
|
|
546
574
|
});
|
|
547
575
|
|
|
548
576
|
it("stubs a spilled context_tree_query recovery output aged past the grace window to the spill-pointer stub", () => {
|
|
@@ -555,7 +583,7 @@ describe("pruneMessages recovery grace", () => {
|
|
|
555
583
|
spillBytes: 1048576, isError: false, turnIndex: 0, timestamp: 1,
|
|
556
584
|
}]]),
|
|
557
585
|
});
|
|
558
|
-
const messages: any[] = [mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
586
|
+
const messages: any[] = [mkAsst("tc-recover", "context_tree_query", 0), mkQueryResult("tc-recover", 1), mkUser(2), mkUser(3), mkUser(4), mkUser(5)];
|
|
559
587
|
const { messages: out } = pruneMessages(messages, indexer, undefined, undefined, undefined, 3);
|
|
560
588
|
const tr = out.find((m: any) => m.toolCallId === "tc-recover") as any;
|
|
561
589
|
expect(tr.content[0].text).not.toBe("VERBATIM RECOVERY OUTPUT");
|
|
@@ -564,6 +592,289 @@ describe("pruneMessages recovery grace", () => {
|
|
|
564
592
|
});
|
|
565
593
|
});
|
|
566
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
|
+
|
|
567
878
|
describe("sizeMessages", () => {
|
|
568
879
|
it("counts hidden fields (thinking blocks), not just visible text", () => {
|
|
569
880
|
// Two messages with identical visible .text but different hidden content.
|
|
@@ -606,6 +917,7 @@ describe("pruneMessages beforeChars/afterChars", () => {
|
|
|
606
917
|
shortRefs: new Map([["tc1", "t1"]]),
|
|
607
918
|
});
|
|
608
919
|
const messages = [
|
|
920
|
+
{ role: "assistant", content: [{ type: "toolCall", id: "tc1", name: "bash", input: {} }], timestamp: 0 },
|
|
609
921
|
{
|
|
610
922
|
role: "toolResult",
|
|
611
923
|
toolCallId: "tc1",
|
|
@@ -620,5 +932,124 @@ describe("pruneMessages beforeChars/afterChars", () => {
|
|
|
620
932
|
expect(result.beforeChars).toBe(sizeMessages(messages));
|
|
621
933
|
expect(result.afterChars).toBe(sizeMessages(result.messages));
|
|
622
934
|
expect(result.afterChars).toBeLessThan(result.beforeChars);
|
|
935
|
+
expect(result.messages).toHaveLength(2);
|
|
623
936
|
});
|
|
624
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
|
+
});
|