ultimate-pi 0.4.0 → 0.4.1
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/.pi/PACKAGING.md +3 -2
- package/.pi/extensions/lib/harness-vcc-settings.ts +50 -0
- package/.pi/extensions/ultimate-pi-vcc.ts +17 -0
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +40 -0
- package/.pi/harness/docs/adrs/README.md +1 -0
- package/.pi/prompts/harness-setup.md +2 -2
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +8 -0
- package/.pi/scripts/vendor-sync-pi-vcc.sh +40 -0
- package/.pi/settings.example.json +1 -6
- package/CHANGELOG.md +9 -7
- package/THIRD_PARTY_NOTICES.md +8 -22
- package/package.json +7 -6
- package/vendor/pi-vcc/README.md +215 -0
- package/vendor/pi-vcc/UPSTREAM_PIN.md +12 -0
- package/vendor/pi-vcc/demo.gif +0 -0
- package/vendor/pi-vcc/index.ts +12 -0
- package/vendor/pi-vcc/package.json +26 -0
- package/vendor/pi-vcc/scripts/audit-sessions.ts +88 -0
- package/vendor/pi-vcc/scripts/benchmark-real-sessions.ts +25 -0
- package/vendor/pi-vcc/scripts/compare-before-after.ts +36 -0
- package/vendor/pi-vcc/scripts/dump-branch-output.ts +20 -0
- package/vendor/pi-vcc/src/commands/pi-vcc.ts +36 -0
- package/vendor/pi-vcc/src/commands/vcc-recall.ts +65 -0
- package/vendor/pi-vcc/src/core/brief.ts +381 -0
- package/vendor/pi-vcc/src/core/build-sections.ts +79 -0
- package/vendor/pi-vcc/src/core/content.ts +60 -0
- package/vendor/pi-vcc/src/core/filter-noise.ts +42 -0
- package/vendor/pi-vcc/src/core/format-recall.ts +27 -0
- package/vendor/pi-vcc/src/core/format.ts +49 -0
- package/vendor/pi-vcc/src/core/lineage.ts +26 -0
- package/vendor/pi-vcc/src/core/load-messages.ts +41 -0
- package/vendor/pi-vcc/src/core/normalize.ts +66 -0
- package/vendor/pi-vcc/src/core/recall-scope.ts +14 -0
- package/vendor/pi-vcc/src/core/render-entries.ts +55 -0
- package/vendor/pi-vcc/src/core/report.ts +237 -0
- package/vendor/pi-vcc/src/core/sanitize.ts +5 -0
- package/vendor/pi-vcc/src/core/search-entries.ts +221 -0
- package/vendor/pi-vcc/src/core/settings.ts +8 -0
- package/vendor/pi-vcc/src/core/skill-collapse.ts +35 -0
- package/vendor/pi-vcc/src/core/summarize.ts +157 -0
- package/vendor/pi-vcc/src/core/tool-args.ts +14 -0
- package/vendor/pi-vcc/src/details.ts +7 -0
- package/vendor/pi-vcc/src/extract/commits.ts +69 -0
- package/vendor/pi-vcc/src/extract/files.ts +80 -0
- package/vendor/pi-vcc/src/extract/goals.ts +79 -0
- package/vendor/pi-vcc/src/extract/preferences.ts +55 -0
- package/vendor/pi-vcc/src/hooks/before-compact.ts +314 -0
- package/vendor/pi-vcc/src/sections.ts +12 -0
- package/vendor/pi-vcc/src/tools/recall.ts +109 -0
- package/vendor/pi-vcc/src/types.ts +14 -0
- package/vendor/pi-vcc/tests/before-compact-hook.test.ts +204 -0
- package/vendor/pi-vcc/tests/before-compact.test.ts +145 -0
- package/vendor/pi-vcc/tests/brief.test.ts +206 -0
- package/vendor/pi-vcc/tests/build-sections.test.ts +59 -0
- package/vendor/pi-vcc/tests/compile.test.ts +80 -0
- package/vendor/pi-vcc/tests/content.test.ts +31 -0
- package/vendor/pi-vcc/tests/extract-goals.test.ts +86 -0
- package/vendor/pi-vcc/tests/extract-preferences.test.ts +30 -0
- package/vendor/pi-vcc/tests/filter-noise.test.ts +61 -0
- package/vendor/pi-vcc/tests/fixtures.ts +61 -0
- package/vendor/pi-vcc/tests/format-recall.test.ts +30 -0
- package/vendor/pi-vcc/tests/format.test.ts +62 -0
- package/vendor/pi-vcc/tests/lineage.test.ts +33 -0
- package/vendor/pi-vcc/tests/load-messages.test.ts +51 -0
- package/vendor/pi-vcc/tests/normalize.test.ts +97 -0
- package/vendor/pi-vcc/tests/real-sessions.test.ts +38 -0
- package/vendor/pi-vcc/tests/recall-expand.test.ts +15 -0
- package/vendor/pi-vcc/tests/recall-scope.test.ts +32 -0
- package/vendor/pi-vcc/tests/recall-tool-scope.test.ts +67 -0
- package/vendor/pi-vcc/tests/render-entries.test.ts +62 -0
- package/vendor/pi-vcc/tests/report.test.ts +44 -0
- package/vendor/pi-vcc/tests/sanitize.test.ts +24 -0
- package/vendor/pi-vcc/tests/search-entries.test.ts +144 -0
- package/vendor/pi-vcc/tests/support/load-session.ts +23 -0
- package/vendor/pi-vcc/tests/support/real-sessions.ts +51 -0
- package/.pi/pi-vcc-config.json +0 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { normalize } from "../src/core/normalize";
|
|
3
|
+
import {
|
|
4
|
+
userMsg,
|
|
5
|
+
assistantText,
|
|
6
|
+
assistantWithThinking,
|
|
7
|
+
assistantWithToolCall,
|
|
8
|
+
toolResult,
|
|
9
|
+
} from "./fixtures";
|
|
10
|
+
|
|
11
|
+
describe("normalize", () => {
|
|
12
|
+
it("returns empty for empty input", () => {
|
|
13
|
+
expect(normalize([])).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("normalizes user message (string content)", () => {
|
|
17
|
+
const blocks = normalize([userMsg("fix the bug")]);
|
|
18
|
+
expect(blocks).toEqual([{ kind: "user", text: "fix the bug", sourceIndex: 0 }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("normalizes assistant text message", () => {
|
|
22
|
+
const blocks = normalize([assistantText("done")]);
|
|
23
|
+
expect(blocks).toEqual([{ kind: "assistant", text: "done", sourceIndex: 0 }]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("normalizes assistant string content", () => {
|
|
27
|
+
const msg = { ...assistantText("done"), content: "plain text" } as any;
|
|
28
|
+
expect(normalize([msg])).toEqual([{ kind: "assistant", text: "plain text", sourceIndex: 0 }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("splits assistant thinking + text", () => {
|
|
32
|
+
const blocks = normalize([assistantWithThinking("result", "hmm")]);
|
|
33
|
+
expect(blocks).toHaveLength(2);
|
|
34
|
+
expect(blocks[0]).toEqual({
|
|
35
|
+
kind: "thinking", text: "hmm", redacted: false, sourceIndex: 0,
|
|
36
|
+
});
|
|
37
|
+
expect(blocks[1]).toEqual({ kind: "assistant", text: "result", sourceIndex: 0 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("normalizes tool call", () => {
|
|
41
|
+
const blocks = normalize([assistantWithToolCall("Read", { path: "a.ts" })]);
|
|
42
|
+
expect(blocks).toEqual([{
|
|
43
|
+
kind: "tool_call", name: "Read", args: { path: "a.ts" }, sourceIndex: 0,
|
|
44
|
+
}]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("normalizes tool result", () => {
|
|
48
|
+
const blocks = normalize([toolResult("Read", "file contents")]);
|
|
49
|
+
expect(blocks).toEqual([{
|
|
50
|
+
kind: "tool_result", name: "Read",
|
|
51
|
+
text: "file contents", isError: false, sourceIndex: 0,
|
|
52
|
+
}]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("normalizes error tool result", () => {
|
|
56
|
+
const blocks = normalize([toolResult("Edit", "not found", true)]);
|
|
57
|
+
expect(blocks[0]).toMatchObject({
|
|
58
|
+
kind: "tool_result", isError: true,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("handles mixed message sequence", () => {
|
|
63
|
+
const blocks = normalize([
|
|
64
|
+
userMsg("fix it"),
|
|
65
|
+
assistantWithToolCall("Read", { path: "x.ts" }),
|
|
66
|
+
toolResult("Read", "code"),
|
|
67
|
+
assistantText("done"),
|
|
68
|
+
]);
|
|
69
|
+
expect(blocks).toHaveLength(4);
|
|
70
|
+
expect(blocks.map((b) => b.kind)).toEqual([
|
|
71
|
+
"user", "tool_call", "tool_result", "assistant",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("produces image placeholder for user image content", () => {
|
|
76
|
+
const msg = {
|
|
77
|
+
role: "user" as const,
|
|
78
|
+
content: [
|
|
79
|
+
{ type: "text" as const, text: "look at this" },
|
|
80
|
+
{ type: "image" as const, data: "abc", mimeType: "image/png" },
|
|
81
|
+
],
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
};
|
|
84
|
+
const blocks = normalize([msg]);
|
|
85
|
+
expect(blocks).toHaveLength(2);
|
|
86
|
+
expect(blocks[0]).toEqual({ kind: "user", text: "look at this", sourceIndex: 0 });
|
|
87
|
+
expect(blocks[1]).toEqual({ kind: "user", text: "[image: image/png]", sourceIndex: 0 });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("skips unknown message roles gracefully", () => {
|
|
91
|
+
const weird = { role: "bashExecution", command: "ls", output: "files", exitCode: 0 } as any;
|
|
92
|
+
const blocks = normalize([weird]);
|
|
93
|
+
expect(blocks).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildCompactReport } from "../src/core/report";
|
|
3
|
+
import { prepareSessionSamples, readSourceStat, type SessionSample } from "./support/real-sessions";
|
|
4
|
+
import { loadSessionMessages } from "./support/load-session";
|
|
5
|
+
|
|
6
|
+
let samples: SessionSample[] = [];
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
samples = await prepareSessionSamples(2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("real session integration", () => {
|
|
13
|
+
it("compiles copied large sessions without mutating originals", async () => {
|
|
14
|
+
for (const sample of samples) {
|
|
15
|
+
const before = await readSourceStat(sample);
|
|
16
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
17
|
+
const report = buildCompactReport({ messages: loaded.messages });
|
|
18
|
+
const after = await readSourceStat(sample);
|
|
19
|
+
|
|
20
|
+
expect(loaded.messageCount).toBeGreaterThan(0);
|
|
21
|
+
expect(loaded.skippedCount).toBeGreaterThanOrEqual(0);
|
|
22
|
+
expect(report.summary.length).toBeGreaterThan(0);
|
|
23
|
+
expect(report.summary).toContain("[");
|
|
24
|
+
expect(report.before.preview.length).toBeGreaterThan(0);
|
|
25
|
+
expect(report.after.summaryPreview.length).toBeGreaterThan(0);
|
|
26
|
+
expect(report.compression.charsBefore).toBeGreaterThan(0);
|
|
27
|
+
expect(report.recall.probes.length).toBeGreaterThan(0);
|
|
28
|
+
expect(after).toEqual(before);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("uses read-only copied fixtures", () => {
|
|
33
|
+
for (const sample of samples) {
|
|
34
|
+
expect(sample.copy).not.toBe(sample.source);
|
|
35
|
+
expect(sample.copy.includes("pi-vcc-sessions-")).toBe(true);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { invalidExpandIndices } from "../src/tools/recall";
|
|
3
|
+
|
|
4
|
+
describe("invalidExpandIndices", () => {
|
|
5
|
+
it("returns indices that are not in available lineage index set", () => {
|
|
6
|
+
const available = new Set([0, 2, 5]);
|
|
7
|
+
expect(invalidExpandIndices([0, 2], available)).toEqual([]);
|
|
8
|
+
expect(invalidExpandIndices([1, 2, 7], available)).toEqual([1, 7]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rejects non-integer indices", () => {
|
|
12
|
+
const available = new Set([0, 1, 2]);
|
|
13
|
+
expect(invalidExpandIndices([1.5, 2], available)).toEqual([1.5]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { normalizeRecallScope, parseRecallScope } from "../src/core/recall-scope";
|
|
3
|
+
|
|
4
|
+
describe("normalizeRecallScope", () => {
|
|
5
|
+
it("defaults to active lineage", () => {
|
|
6
|
+
expect(normalizeRecallScope()).toBe("lineage");
|
|
7
|
+
expect(normalizeRecallScope("lineage")).toBe("lineage");
|
|
8
|
+
expect(normalizeRecallScope("unknown")).toBe("lineage");
|
|
9
|
+
expect(normalizeRecallScope(123)).toBe("lineage");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("accepts all scope", () => {
|
|
13
|
+
expect(normalizeRecallScope("all")).toBe("all");
|
|
14
|
+
expect(normalizeRecallScope("ALL")).toBe("all");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("parseRecallScope", () => {
|
|
19
|
+
it("removes scope token from command text", () => {
|
|
20
|
+
expect(parseRecallScope("license scope:all page:2")).toEqual({
|
|
21
|
+
scope: "all",
|
|
22
|
+
text: "license page:2",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("defaults to lineage when no scope token is present", () => {
|
|
27
|
+
expect(parseRecallScope("license page:2")).toEqual({
|
|
28
|
+
scope: "lineage",
|
|
29
|
+
text: "license page:2",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { registerRecallTool } from "../src/tools/recall";
|
|
6
|
+
|
|
7
|
+
const makeSession = () => {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-vcc-recall-scope-"));
|
|
9
|
+
const file = join(dir, "session.jsonl");
|
|
10
|
+
const lines = [
|
|
11
|
+
JSON.stringify({ type: "message", id: "m1", message: { role: "user", content: "active lineage token" } }),
|
|
12
|
+
JSON.stringify({ type: "message", id: "m2", message: { role: "user", content: "off lineage secret" } }),
|
|
13
|
+
];
|
|
14
|
+
writeFileSync(file, lines.join("\n") + "\n", "utf8");
|
|
15
|
+
return { dir, file };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const register = () => {
|
|
19
|
+
let tool: any;
|
|
20
|
+
registerRecallTool({ registerTool: (t: any) => { tool = t; } } as any);
|
|
21
|
+
return tool;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const invoke = async (tool: any, file: string, params: Record<string, unknown>) => {
|
|
25
|
+
const result = await tool.execute("tool-call", params, undefined, undefined, {
|
|
26
|
+
sessionManager: {
|
|
27
|
+
getSessionFile: () => file,
|
|
28
|
+
getBranch: () => [{ id: "m1" }],
|
|
29
|
+
getEntries: () => [{ id: "m1" }, { id: "m2" }],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return result.content[0].text as string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe("vcc_recall scope", () => {
|
|
36
|
+
it("defaults to active lineage and opts into all-session search explicitly", async () => {
|
|
37
|
+
const { dir, file } = makeSession();
|
|
38
|
+
try {
|
|
39
|
+
const tool = register();
|
|
40
|
+
|
|
41
|
+
const lineage = await invoke(tool, file, { query: "secret" });
|
|
42
|
+
expect(lineage).toContain("No matches");
|
|
43
|
+
|
|
44
|
+
const all = await invoke(tool, file, { query: "secret", scope: "all" });
|
|
45
|
+
expect(all).toContain("scope: all");
|
|
46
|
+
expect(all).toContain("off lineage secret");
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps expand strict by default but allows off-lineage expand with scope all", async () => {
|
|
53
|
+
const { dir, file } = makeSession();
|
|
54
|
+
try {
|
|
55
|
+
const tool = register();
|
|
56
|
+
|
|
57
|
+
const lineage = await invoke(tool, file, { expand: [1] });
|
|
58
|
+
expect(lineage).toContain("Cannot expand indices outside active lineage: 1");
|
|
59
|
+
|
|
60
|
+
const all = await invoke(tool, file, { expand: [1], scope: "all" });
|
|
61
|
+
expect(all).toContain("Scope: all");
|
|
62
|
+
expect(all).toContain("#1 [user] off lineage secret");
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { renderMessage } from "../src/core/render-entries";
|
|
3
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
4
|
+
import { userMsg, assistantText, assistantWithToolCall, toolResult } from "./fixtures";
|
|
5
|
+
|
|
6
|
+
describe("renderMessage", () => {
|
|
7
|
+
it("renders user message", () => {
|
|
8
|
+
const r = renderMessage(userMsg("hello"), 0);
|
|
9
|
+
expect(r).toEqual({ index: 0, role: "user", summary: "hello" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders assistant text", () => {
|
|
13
|
+
const r = renderMessage(assistantText("done"), 1);
|
|
14
|
+
expect(r.role).toBe("assistant");
|
|
15
|
+
expect(r.summary).toBe("done");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders tool result", () => {
|
|
19
|
+
const r = renderMessage(toolResult("Read", "file contents"), 2);
|
|
20
|
+
expect(r.role).toBe("tool_result");
|
|
21
|
+
expect(r.summary).toContain("[Read]");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders tool call arguments with values", () => {
|
|
25
|
+
const r = renderMessage(assistantWithToolCall("Read", { path: "a.ts" }), 2);
|
|
26
|
+
expect(r.summary).toContain("Read(path=a.ts)");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders error tool result with prefix", () => {
|
|
30
|
+
const r = renderMessage(toolResult("bash", "not found", true), 3);
|
|
31
|
+
expect(r.summary).toStartWith("ERROR");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("truncates long user text", () => {
|
|
35
|
+
const long = "x".repeat(500);
|
|
36
|
+
const r = renderMessage(userMsg(long), 0);
|
|
37
|
+
expect(r.summary.length).toBeLessThanOrEqual(300);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renders bashExecution message", () => {
|
|
41
|
+
const msg = { role: "bashExecution", command: "ls -la", output: "total 0\n" } as any;
|
|
42
|
+
const r = renderMessage(msg, 5);
|
|
43
|
+
expect(r.role).toBe("bash");
|
|
44
|
+
expect(r.summary).toContain("$ ls -la");
|
|
45
|
+
expect(r.summary).toContain("total 0");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders bashExecution with missing output", () => {
|
|
49
|
+
const msg = { role: "bashExecution", command: "exit 1" } as any;
|
|
50
|
+
const r = renderMessage(msg, 6);
|
|
51
|
+
expect(r.role).toBe("bash");
|
|
52
|
+
expect(r.summary).toContain("$ exit 1");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("handles message with undefined content", () => {
|
|
56
|
+
const msg = { role: "assistant", content: undefined } as any;
|
|
57
|
+
const r = renderMessage(msg, 3);
|
|
58
|
+
expect(r.role).toBe("assistant");
|
|
59
|
+
expect(r.summary).toBe("");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { buildCompactReport } from "../src/core/report";
|
|
3
|
+
import {
|
|
4
|
+
userMsg,
|
|
5
|
+
assistantText,
|
|
6
|
+
assistantWithToolCall,
|
|
7
|
+
toolResult,
|
|
8
|
+
} from "./fixtures";
|
|
9
|
+
|
|
10
|
+
describe("buildCompactReport", () => {
|
|
11
|
+
it("includes before and after compact metrics", () => {
|
|
12
|
+
const report = buildCompactReport({
|
|
13
|
+
messages: [
|
|
14
|
+
userMsg("Fix login bug in auth.ts"),
|
|
15
|
+
assistantWithToolCall("Read", { path: "auth.ts" }),
|
|
16
|
+
toolResult("Read", "const x = 1;"),
|
|
17
|
+
assistantText("Found the root cause in auth.ts.\n1. Fix validation\n2. Run tests"),
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
expect(report.before.messageCount).toBe(4);
|
|
21
|
+
expect(report.before.roleCounts.user).toBe(1);
|
|
22
|
+
expect(report.before.topFiles).toContain("auth.ts");
|
|
23
|
+
expect(report.before.preview).toContain("Fix login bug in auth.ts");
|
|
24
|
+
expect(report.after.sectionCount).toBeGreaterThan(0);
|
|
25
|
+
expect(report.after.summaryPreview).toContain("[Session Goal]");
|
|
26
|
+
expect(report.after.summaryPreview).toContain('* Read "auth.ts"');
|
|
27
|
+
expect(report.after.briefTranscriptLines).toBeGreaterThan(0);
|
|
28
|
+
expect(report.compression.ratio).toBeGreaterThan(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("marks recall probe coverage for goal and file queries", () => {
|
|
32
|
+
const report = buildCompactReport({
|
|
33
|
+
messages: [
|
|
34
|
+
userMsg("Fix login bug in auth.ts"),
|
|
35
|
+
assistantWithToolCall("Read", { path: "auth.ts" }),
|
|
36
|
+
toolResult("Read", "code content"),
|
|
37
|
+
assistantText("Found the root cause"),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
expect(report.recall.probes.length).toBeGreaterThanOrEqual(1);
|
|
41
|
+
const goalProbe = report.recall.probes.find((p) => p.label === "goal");
|
|
42
|
+
expect(goalProbe?.summaryMentioned).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { sanitize } from "../src/core/sanitize";
|
|
3
|
+
|
|
4
|
+
describe("sanitize", () => {
|
|
5
|
+
it("strips ANSI escape codes", () => {
|
|
6
|
+
expect(sanitize("\x1b[31mred\x1b[0m")).toBe("red");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("normalizes CRLF to LF", () => {
|
|
10
|
+
expect(sanitize("a\r\nb\r\n")).toBe("a\nb\n");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("strips bare CR", () => {
|
|
14
|
+
expect(sanitize("a\rb")).toBe("a\nb");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("strips control characters but preserves newlines and tabs", () => {
|
|
18
|
+
expect(sanitize("a\x00b\tc\nd")).toBe("ab\tc\nd");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("passes clean text unchanged", () => {
|
|
22
|
+
expect(sanitize("hello world")).toBe("hello world");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { searchEntries } from "../src/core/search-entries";
|
|
3
|
+
import type { RenderedEntry } from "../src/core/render-entries";
|
|
4
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
5
|
+
|
|
6
|
+
const entries: RenderedEntry[] = [
|
|
7
|
+
{ index: 0, role: "user", summary: "Fix login bug" },
|
|
8
|
+
{ index: 1, role: "assistant", summary: "Reading auth.ts" },
|
|
9
|
+
{ index: 2, role: "tool_result", summary: "[Read] code here" },
|
|
10
|
+
{ index: 3, role: "assistant", summary: "Found the root cause in auth module" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const messages: Message[] = [
|
|
14
|
+
{ role: "user", content: "Fix login bug" } as any,
|
|
15
|
+
{ role: "assistant", content: [{ type: "text", text: "Reading auth.ts" }] } as any,
|
|
16
|
+
{ role: "toolResult", content: [{ type: "text", text: "[Read] code here" }] } as any,
|
|
17
|
+
{ role: "assistant", content: [{ type: "text", text: "Found the root cause in auth module" }] } as any,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("searchEntries", () => {
|
|
21
|
+
it("returns all for empty query", () => {
|
|
22
|
+
expect(searchEntries(entries, messages)).toEqual(entries);
|
|
23
|
+
expect(searchEntries(entries, messages, "")).toEqual(entries);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("filters by single term", () => {
|
|
27
|
+
const r = searchEntries(entries, messages, "login");
|
|
28
|
+
expect(r).toHaveLength(1);
|
|
29
|
+
expect(r[0].index).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns empty for no match", () => {
|
|
33
|
+
expect(searchEntries(entries, messages, "xyz123")).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("finds keyword beyond clip boundary in full content", () => {
|
|
37
|
+
const longText = "A".repeat(400) + " hidden_keyword here";
|
|
38
|
+
const longEntries: RenderedEntry[] = [
|
|
39
|
+
{ index: 0, role: "user", summary: "A".repeat(300) },
|
|
40
|
+
];
|
|
41
|
+
const longMsgs: Message[] = [
|
|
42
|
+
{ role: "user", content: longText } as any,
|
|
43
|
+
];
|
|
44
|
+
const r = searchEntries(longEntries, longMsgs, "hidden_keyword");
|
|
45
|
+
expect(r).toHaveLength(1);
|
|
46
|
+
expect(r[0].snippet).toContain("hidden_keyword");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns snippet around matched term", () => {
|
|
50
|
+
const r = searchEntries(entries, messages, "root");
|
|
51
|
+
expect(r).toHaveLength(1);
|
|
52
|
+
expect(r[0].snippet).toBeDefined();
|
|
53
|
+
expect(r[0].snippet).toContain("root");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── regex support ──
|
|
57
|
+
|
|
58
|
+
it("supports regex pattern: alternation", () => {
|
|
59
|
+
const r = searchEntries(entries, messages, "login|auth");
|
|
60
|
+
expect(r).toHaveLength(3); // "login bug", "auth.ts", "auth module"
|
|
61
|
+
expect(r.map((h) => h.index).sort()).toEqual([0, 1, 3]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("supports regex pattern: wildcard", () => {
|
|
65
|
+
const r = searchEntries(entries, messages, "Read.*auth");
|
|
66
|
+
expect(r).toHaveLength(1);
|
|
67
|
+
expect(r[0].index).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to escaped literal for invalid regex", () => {
|
|
71
|
+
const extraEntries: RenderedEntry[] = [
|
|
72
|
+
{ index: 0, role: "user", summary: "test (foo" },
|
|
73
|
+
{ index: 1, role: "assistant", summary: "no match here" },
|
|
74
|
+
];
|
|
75
|
+
const extraMsgs: Message[] = [
|
|
76
|
+
{ role: "user", content: "error with (foo pattern" } as any,
|
|
77
|
+
{ role: "assistant", content: [{ type: "text", text: "no match here" }] } as any,
|
|
78
|
+
];
|
|
79
|
+
const r = searchEntries(extraEntries, extraMsgs, "(foo");
|
|
80
|
+
expect(r).toHaveLength(1);
|
|
81
|
+
expect(r[0].index).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("regex is case-insensitive", () => {
|
|
85
|
+
const r = searchEntries(entries, messages, "FIX|ROOT");
|
|
86
|
+
expect(r).toHaveLength(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── natural language queries (OR logic + ranking) ──
|
|
90
|
+
|
|
91
|
+
it("natural language query uses OR logic", () => {
|
|
92
|
+
// "root cause auth" -- matches entries containing ANY of these terms
|
|
93
|
+
const r = searchEntries(entries, messages, "root cause auth");
|
|
94
|
+
expect(r.length).toBeGreaterThanOrEqual(2); // #3 has all 3, #1 has auth
|
|
95
|
+
// Best match (highest BM25) should come first
|
|
96
|
+
expect(r[0].index).toBe(3); // "Found the root cause in auth module" matches all 3
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("natural language ranks by BM25 score", () => {
|
|
100
|
+
const r = searchEntries(entries, messages, "root cause auth");
|
|
101
|
+
// Top result has more terms matched = higher BM25 score
|
|
102
|
+
expect(r[0].matchCount!).toBeGreaterThanOrEqual(r[r.length - 1].matchCount!);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("filters stopwords from queries", () => {
|
|
106
|
+
// "the root cause of it" → stopwords: the, of, it → meaningful: root, cause
|
|
107
|
+
const r = searchEntries(entries, messages, "the root cause of it");
|
|
108
|
+
expect(r).toHaveLength(1);
|
|
109
|
+
expect(r[0].index).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("keeps all terms if all are stopwords", () => {
|
|
113
|
+
// When all terms are stopwords, keep them (don't drop everything)
|
|
114
|
+
// "the" appears in "Found the root cause" so it matches
|
|
115
|
+
const r = searchEntries(entries, messages, "the");
|
|
116
|
+
expect(r.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── line-based snippet ──
|
|
120
|
+
|
|
121
|
+
it("snippet shows context lines around match", () => {
|
|
122
|
+
const multiline = "line 0\nline 1\nline 2 TARGET\nline 3\nline 4\nline 5";
|
|
123
|
+
const e: RenderedEntry[] = [{ index: 0, role: "user", summary: "test" }];
|
|
124
|
+
const m: Message[] = [{ role: "user", content: multiline } as any];
|
|
125
|
+
const r = searchEntries(e, m, "TARGET");
|
|
126
|
+
expect(r).toHaveLength(1);
|
|
127
|
+
const snip = r[0].snippet!;
|
|
128
|
+
expect(snip).toContain("line 2 TARGET");
|
|
129
|
+
expect(snip).toContain("line 0");
|
|
130
|
+
expect(snip).toContain("line 4");
|
|
131
|
+
expect(snip).not.toContain("line 5");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("snippet handles match at beginning", () => {
|
|
135
|
+
const multiline = "TARGET here\nline 1\nline 2\nline 3";
|
|
136
|
+
const e: RenderedEntry[] = [{ index: 0, role: "user", summary: "test" }];
|
|
137
|
+
const m: Message[] = [{ role: "user", content: multiline } as any];
|
|
138
|
+
const r = searchEntries(e, m, "TARGET");
|
|
139
|
+
const snip = r[0].snippet!;
|
|
140
|
+
expect(snip).toContain("TARGET here");
|
|
141
|
+
expect(snip).toContain("line 2");
|
|
142
|
+
expect(snip).not.toContain("line 3");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { buildSessionContext, loadEntriesFromFile } from "../../node_modules/@mariozechner/pi-coding-agent/dist/core/session-manager.js";
|
|
2
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
3
|
+
|
|
4
|
+
export interface LoadedSession {
|
|
5
|
+
messageCount: number;
|
|
6
|
+
skippedCount: number;
|
|
7
|
+
messages: Message[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const loadSessionMessages = (file: string): LoadedSession => {
|
|
11
|
+
const entries = loadEntriesFromFile(file);
|
|
12
|
+
const sessionEntries = entries.filter((entry) => entry.type !== "header");
|
|
13
|
+
const context = buildSessionContext(sessionEntries as any);
|
|
14
|
+
const messages = (context.messages as any[]).filter(
|
|
15
|
+
(msg): msg is Message =>
|
|
16
|
+
msg && typeof msg.role === "string" && "content" in msg,
|
|
17
|
+
);
|
|
18
|
+
return {
|
|
19
|
+
messageCount: messages.length,
|
|
20
|
+
skippedCount: context.messages.length - messages.length,
|
|
21
|
+
messages,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, copyFile, chmod, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SESSION_ROOT = join(process.env.HOME ?? "", ".pi/agent/sessions");
|
|
6
|
+
|
|
7
|
+
export interface SessionSample {
|
|
8
|
+
source: string;
|
|
9
|
+
copy: string;
|
|
10
|
+
size: number;
|
|
11
|
+
mtimeMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const walk = async (dir: string): Promise<string[]> => {
|
|
15
|
+
const names = await readdir(dir, { withFileTypes: true });
|
|
16
|
+
const out: string[] = [];
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const path = join(dir, name.name);
|
|
19
|
+
if (name.isDirectory()) out.push(...await walk(path));
|
|
20
|
+
else if (name.isFile() && path.endsWith(".jsonl")) out.push(path);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const pickLargest = async (limit: number): Promise<string[]> => {
|
|
26
|
+
const files = await walk(SESSION_ROOT);
|
|
27
|
+
const sized = await Promise.all(
|
|
28
|
+
files.map(async (file) => ({ file, size: (await stat(file)).size })),
|
|
29
|
+
);
|
|
30
|
+
return sized.sort((a, b) => b.size - a.size).slice(0, limit).map((x) => x.file);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const prepareSessionSamples = async (limit = 2): Promise<SessionSample[]> => {
|
|
34
|
+
const selected = await pickLargest(limit);
|
|
35
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-vcc-sessions-"));
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
const samples: SessionSample[] = [];
|
|
38
|
+
for (const source of selected) {
|
|
39
|
+
const srcStat = await stat(source);
|
|
40
|
+
const copy = join(dir, basename(source));
|
|
41
|
+
await copyFile(source, copy);
|
|
42
|
+
await chmod(copy, 0o444);
|
|
43
|
+
samples.push({ source, copy, size: srcStat.size, mtimeMs: srcStat.mtimeMs });
|
|
44
|
+
}
|
|
45
|
+
return samples;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const readSourceStat = async (sample: SessionSample) => {
|
|
49
|
+
const s = await stat(sample.source);
|
|
50
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
51
|
+
};
|
package/.pi/pi-vcc-config.json
DELETED