@monotykamary/pi-vcc 0.3.16
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/README.md +688 -0
- package/__tests__/helpers.ts +135 -0
- package/__tests__/root-causes.test.ts +858 -0
- package/demo.gif +0 -0
- package/index.ts +21 -0
- package/knip.json +4 -0
- package/npm-shrinkwrap.json +3001 -0
- package/package.json +36 -0
- package/scripts/audit-sessions.ts +88 -0
- package/scripts/benchmark-real-sessions.ts +25 -0
- package/scripts/compare-before-after.ts +36 -0
- package/scripts/dump-branch-output.ts +20 -0
- package/src/commands/pi-vcc.ts +36 -0
- package/src/commands/vcc-recall.ts +65 -0
- package/src/core/brief.ts +497 -0
- package/src/core/build-sections.ts +431 -0
- package/src/core/content.ts +71 -0
- package/src/core/filter-noise.ts +42 -0
- package/src/core/format-recall.ts +177 -0
- package/src/core/format.ts +98 -0
- package/src/core/invisible-continue.ts +172 -0
- package/src/core/lineage.ts +26 -0
- package/src/core/load-messages.ts +40 -0
- package/src/core/normalize.ts +73 -0
- package/src/core/recall-scope.ts +49 -0
- package/src/core/render-entries.ts +58 -0
- package/src/core/report.ts +237 -0
- package/src/core/sanitize.ts +5 -0
- package/src/core/search-entries.ts +375 -0
- package/src/core/settings.ts +190 -0
- package/src/core/skill-collapse.ts +35 -0
- package/src/core/summarize.ts +179 -0
- package/src/core/tool-args.ts +14 -0
- package/src/details.ts +19 -0
- package/src/extract/commits.ts +69 -0
- package/src/extract/goals.ts +79 -0
- package/src/extract/preferences.ts +55 -0
- package/src/extract/shared-symbols.ts +357 -0
- package/src/hooks/before-compact.ts +564 -0
- package/src/hooks/proactive-threshold.ts +131 -0
- package/src/sections.ts +23 -0
- package/src/tools/recall.ts +204 -0
- package/src/types.ts +20 -0
- package/tests/australian-government.test.ts +479 -0
- package/tests/before-compact-hook.test.ts +185 -0
- package/tests/before-compact.test.ts +227 -0
- package/tests/brief.test.ts +215 -0
- package/tests/build-sections.test.ts +59 -0
- package/tests/compile.test.ts +89 -0
- package/tests/content.test.ts +66 -0
- package/tests/deep-extraction.test.ts +313 -0
- package/tests/extract-goals.test.ts +86 -0
- package/tests/extract-preferences.test.ts +30 -0
- package/tests/filter-noise.test.ts +61 -0
- package/tests/fixtures.ts +61 -0
- package/tests/format-recall.test.ts +87 -0
- package/tests/format.test.ts +73 -0
- package/tests/government-domain.test.ts +542 -0
- package/tests/lineage.test.ts +33 -0
- package/tests/load-messages.test.ts +49 -0
- package/tests/model-threshold-hook.test.ts +377 -0
- package/tests/model-threshold.test.ts +223 -0
- package/tests/normalize.test.ts +104 -0
- package/tests/proactive-threshold.test.ts +490 -0
- package/tests/real-sessions.test.ts +38 -0
- package/tests/recall-expand.test.ts +15 -0
- package/tests/recall-scope.test.ts +32 -0
- package/tests/recall-tool-scope.test.ts +67 -0
- package/tests/render-entries.test.ts +92 -0
- package/tests/report.test.ts +58 -0
- package/tests/sanitize.test.ts +24 -0
- package/tests/search-entries.test.ts +233 -0
- package/tests/support/load-session.ts +23 -0
- package/tests/support/real-sessions.ts +51 -0
- package/tests/threshold-integration.test.ts +359 -0
- package/tsconfig.json +20 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@monotykamary/pi-vcc",
|
|
3
|
+
"version": "0.3.16",
|
|
4
|
+
"description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"vcc",
|
|
10
|
+
"compact",
|
|
11
|
+
"compaction"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/monotykamary/pi-vcc.git"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@earendil-works/pi-ai": ">=0.74.0 <1.0.0",
|
|
19
|
+
"@earendil-works/pi-agent-core": ">=0.74.0 <1.0.0",
|
|
20
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0 <1.0.0",
|
|
21
|
+
"typebox": ">=1.1.24 <2.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "6.0.3",
|
|
25
|
+
"knip": "6.14.1"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"lint:dead": "knip"
|
|
30
|
+
},
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./index.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { basename, dirname } from "node:path";
|
|
2
|
+
import { compile } from "../src/core/summarize";
|
|
3
|
+
import { normalize } from "../src/core/normalize";
|
|
4
|
+
import { filterNoise } from "../src/core/filter-noise";
|
|
5
|
+
import { renderMessage } from "../src/core/render-entries";
|
|
6
|
+
import { prepareSessionSamples } from "../tests/support/real-sessions";
|
|
7
|
+
import { loadSessionMessages } from "../tests/support/load-session";
|
|
8
|
+
|
|
9
|
+
const SEP = "=".repeat(80);
|
|
10
|
+
const samples = await prepareSessionSamples(10);
|
|
11
|
+
|
|
12
|
+
for (const sample of samples) {
|
|
13
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
14
|
+
const { messages } = loaded;
|
|
15
|
+
|
|
16
|
+
const rawBlocks = normalize(messages);
|
|
17
|
+
const filteredBlocks = filterNoise(rawBlocks);
|
|
18
|
+
const afterText = compile({ messages });
|
|
19
|
+
|
|
20
|
+
const rendered = messages.map((m, i) => renderMessage(m, i));
|
|
21
|
+
const beforeChars = rendered.reduce((s, e) => s + e.summary.length, 0);
|
|
22
|
+
|
|
23
|
+
const project = dirname(sample.source).split("--").filter(Boolean).pop() ?? "unknown";
|
|
24
|
+
|
|
25
|
+
const goalSection = afterText.match(/\[Session Goal\]\n([\s\S]*?)(?=\n\n\[|$)/)?.[1] ?? "(empty)";
|
|
26
|
+
const stateSection = afterText.match(/\[Current State\]\n([\s\S]*?)(?=\n\n\[|$)/)?.[1] ?? "(empty)";
|
|
27
|
+
const doneSection = afterText.match(/\[What Was Done\]\n([\s\S]*?)(?=\n\n\[|$)/)?.[1] ?? "(empty)";
|
|
28
|
+
const problemsSection = afterText.match(/\[Open Problems\]\n([\s\S]*?)(?=\n\n\[|$)/)?.[1] ?? "(empty)";
|
|
29
|
+
const nextSection = afterText.match(/\[Next Best Steps\]\n([\s\S]*?)(?=\n\n\[|$)/)?.[1] ?? "(empty)";
|
|
30
|
+
|
|
31
|
+
const doneLines = doneSection.split("\n").filter(l => l.trim());
|
|
32
|
+
const problemLines = problemsSection.split("\n").filter(l => l.trim());
|
|
33
|
+
|
|
34
|
+
// Detect issues
|
|
35
|
+
const issues: string[] = [];
|
|
36
|
+
|
|
37
|
+
// 1. Goal quality
|
|
38
|
+
const goalLines = goalSection.split("\n").map(l => l.replace(/^- /, "").trim()).filter(Boolean);
|
|
39
|
+
if (goalLines[0] && goalLines[0].length < 5) issues.push(`GOAL_TOO_SHORT: "${goalLines[0]}"`);
|
|
40
|
+
if (goalLines.length === 0) issues.push("GOAL_EMPTY");
|
|
41
|
+
|
|
42
|
+
// 2. Sensitive data in What Was Done
|
|
43
|
+
if (/sshpass|password|secret|token=|api[_-]?key/i.test(doneSection)) {
|
|
44
|
+
issues.push("SENSITIVE_DATA_IN_DONE");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Raw code/minified JS in summary
|
|
48
|
+
if (/\{[a-zA-Z$_]+:[a-zA-Z$_]+,[a-zA-Z$_]+:/.test(afterText) || /var [a-zA-Z]+=/.test(afterText)) {
|
|
49
|
+
issues.push("RAW_CODE_LEAK");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Open problems count
|
|
53
|
+
if (problemLines.length > 10) issues.push(`PROBLEMS_OVERCOUNT: ${problemLines.length}`);
|
|
54
|
+
|
|
55
|
+
// 5. Next steps empty
|
|
56
|
+
if (nextSection === "(empty)") issues.push("NEXT_STEPS_EMPTY");
|
|
57
|
+
|
|
58
|
+
// 6. What Was Done too verbose
|
|
59
|
+
if (doneLines.length > 15) issues.push(`DONE_TOO_VERBOSE: ${doneLines.length} lines`);
|
|
60
|
+
|
|
61
|
+
// 7. Summary too large (>10K chars)
|
|
62
|
+
if (afterText.length > 10000) issues.push(`SUMMARY_TOO_LARGE: ${afterText.length} chars`);
|
|
63
|
+
|
|
64
|
+
console.log(SEP);
|
|
65
|
+
console.log(`PROJECT: ${project}`);
|
|
66
|
+
console.log(`FILE: ${basename(sample.source)}`);
|
|
67
|
+
console.log(`Size: ${(sample.size / 1024).toFixed(0)}KB | Msgs: ${messages.length} | Blocks raw: ${rawBlocks.length} -> filtered: ${filteredBlocks.length}`);
|
|
68
|
+
console.log(`Before: ${beforeChars} chars | After: ${afterText.length} chars | Ratio: ${(beforeChars / afterText.length).toFixed(1)}x`);
|
|
69
|
+
console.log(`Issues: ${issues.length === 0 ? "NONE" : issues.join(", ")}`);
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log("--- GOAL ---");
|
|
72
|
+
console.log(goalSection.slice(0, 300));
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log("--- CURRENT STATE (first 300c) ---");
|
|
75
|
+
console.log(stateSection.slice(0, 300));
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log("--- WHAT WAS DONE (first 5 lines) ---");
|
|
78
|
+
console.log(doneLines.slice(0, 5).join("\n"));
|
|
79
|
+
console.log(`... (${doneLines.length} total lines)`);
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log("--- OPEN PROBLEMS (first 5 lines) ---");
|
|
82
|
+
console.log(problemLines.slice(0, 5).join("\n"));
|
|
83
|
+
console.log(`... (${problemLines.length} total lines)`);
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log("--- NEXT STEPS ---");
|
|
86
|
+
console.log(nextSection.slice(0, 300));
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { buildCompactReport } from "../src/core/report";
|
|
4
|
+
import { prepareSessionSamples } from "../tests/support/real-sessions";
|
|
5
|
+
import { loadSessionMessages } from "../tests/support/load-session";
|
|
6
|
+
|
|
7
|
+
const samples = await prepareSessionSamples(2);
|
|
8
|
+
for (const sample of samples) {
|
|
9
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
10
|
+
const start = performance.now();
|
|
11
|
+
const report = buildCompactReport({ messages: loaded.messages });
|
|
12
|
+
const elapsedMs = performance.now() - start;
|
|
13
|
+
console.log(JSON.stringify({
|
|
14
|
+
sourceFile: basename(sample.source),
|
|
15
|
+
sourceSizeBytes: sample.size,
|
|
16
|
+
copiedToTemp: true,
|
|
17
|
+
loadedMessages: loaded.messageCount,
|
|
18
|
+
skippedMessages: loaded.skippedCount,
|
|
19
|
+
compileMs: Number(elapsedMs.toFixed(2)),
|
|
20
|
+
before: report.before,
|
|
21
|
+
after: report.after,
|
|
22
|
+
compression: report.compression,
|
|
23
|
+
recall: report.recall,
|
|
24
|
+
}, null, 2));
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { compile } from "../src/core/summarize";
|
|
3
|
+
import { renderMessage } from "../src/core/render-entries";
|
|
4
|
+
import { clip } from "../src/core/content";
|
|
5
|
+
import { prepareSessionSamples } from "../tests/support/real-sessions";
|
|
6
|
+
import { loadSessionMessages } from "../tests/support/load-session";
|
|
7
|
+
|
|
8
|
+
const SEP = "=".repeat(80);
|
|
9
|
+
const samples = await prepareSessionSamples(2);
|
|
10
|
+
|
|
11
|
+
for (const sample of samples) {
|
|
12
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
13
|
+
const { messages } = loaded;
|
|
14
|
+
|
|
15
|
+
const rendered = messages.map((m, i) => renderMessage(m, i));
|
|
16
|
+
const beforeLines = rendered.map(
|
|
17
|
+
(e) => `#${e.index} [${e.role}] ${clip(e.summary, 300)}`,
|
|
18
|
+
);
|
|
19
|
+
const beforeText = beforeLines.join("\n");
|
|
20
|
+
const afterText = compile({ messages });
|
|
21
|
+
|
|
22
|
+
console.log(SEP);
|
|
23
|
+
console.log(`FILE: ${basename(sample.source)}`);
|
|
24
|
+
console.log(`Messages: ${messages.length} | Before chars: ${beforeText.length} | After chars: ${afterText.length}`);
|
|
25
|
+
console.log(`Compression: ${(beforeText.length / afterText.length).toFixed(1)}x`);
|
|
26
|
+
console.log(SEP);
|
|
27
|
+
|
|
28
|
+
console.log("\n--- BEFORE (raw context, first 40 + last 20 entries) ---\n");
|
|
29
|
+
for (const line of beforeLines.slice(0, 40)) console.log(line);
|
|
30
|
+
if (beforeLines.length > 60) console.log(`\n... (${beforeLines.length - 60} entries omitted) ...\n`);
|
|
31
|
+
for (const line of beforeLines.slice(-20)) console.log(line);
|
|
32
|
+
|
|
33
|
+
console.log("\n--- AFTER (pi-vcc compiled summary) ---\n");
|
|
34
|
+
console.log(afterText);
|
|
35
|
+
console.log("\n");
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { compile } from "../src/core/summarize";
|
|
3
|
+
import { prepareSessionSamples } from "../tests/support/real-sessions";
|
|
4
|
+
import { loadSessionMessages } from "../tests/support/load-session";
|
|
5
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const outDir = "/tmp/pi-vcc-compare";
|
|
8
|
+
const branch = process.argv[2] || "unknown";
|
|
9
|
+
mkdirSync(`${outDir}/${branch}`, { recursive: true });
|
|
10
|
+
|
|
11
|
+
const samples = await prepareSessionSamples(5);
|
|
12
|
+
for (const sample of samples) {
|
|
13
|
+
const name = basename(sample.source).slice(0, 30);
|
|
14
|
+
const loaded = loadSessionMessages(sample.copy);
|
|
15
|
+
const summary = compile({ messages: loaded.messages });
|
|
16
|
+
const outFile = `${outDir}/${branch}/${name}.txt`;
|
|
17
|
+
writeFileSync(outFile, summary);
|
|
18
|
+
console.log(`${name} (${loaded.messageCount} msgs) => ${summary.length} chars`);
|
|
19
|
+
}
|
|
20
|
+
console.log(`\nSaved to ${outDir}/${branch}/`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getLastCompactionStats, PI_VCC_COMPACT_INSTRUCTION } from "../hooks/before-compact";
|
|
3
|
+
|
|
4
|
+
const formatTokens = (n: number): string => {
|
|
5
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
6
|
+
return String(n);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const registerPiVccCommand = (pi: ExtensionAPI) => {
|
|
10
|
+
pi.registerCommand("pi-vcc", {
|
|
11
|
+
description: "Compact conversation with pi-vcc structured summary",
|
|
12
|
+
handler: async (_args, ctx) => {
|
|
13
|
+
ctx.compact({
|
|
14
|
+
customInstructions: PI_VCC_COMPACT_INSTRUCTION,
|
|
15
|
+
onComplete: () => {
|
|
16
|
+
const stats = getLastCompactionStats();
|
|
17
|
+
if (stats) {
|
|
18
|
+
ctx.ui.notify(
|
|
19
|
+
`pi-vcc: ${stats.summarized} source entries processed; tail kept ${stats.kept} (~${formatTokens(stats.keptTokensEst)} tok).`,
|
|
20
|
+
"info",
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
ctx.ui.notify("Compacted with pi-vcc", "info");
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
onError: (err) => {
|
|
27
|
+
if (err.message === "Compaction cancelled" || err.message === "Already compacted") {
|
|
28
|
+
ctx.ui.notify("Nothing to compact", "warning");
|
|
29
|
+
} else {
|
|
30
|
+
ctx.ui.notify(`Compaction failed: ${err.message}`, "error");
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadAllMessages } from "../core/load-messages";
|
|
3
|
+
import { searchEntries } from "../core/search-entries";
|
|
4
|
+
import { formatRecallOutput } from "../core/format-recall";
|
|
5
|
+
import { getActiveLineageEntryIds } from "../core/lineage";
|
|
6
|
+
import { parseRecallScope } from "../core/recall-scope";
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 5;
|
|
9
|
+
const DEFAULT_RECENT = 25;
|
|
10
|
+
|
|
11
|
+
export const registerVccRecallCommand = (pi: ExtensionAPI) => {
|
|
12
|
+
pi.registerCommand("pi-vcc-recall", {
|
|
13
|
+
description: "Search session history. Defaults to active lineage; add scope:all for off-lineage branches.",
|
|
14
|
+
handler: async (args: string, ctx) => {
|
|
15
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
16
|
+
if (!sessionFile) {
|
|
17
|
+
ctx.ui.notify("No session file available.", "error");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = args.trim();
|
|
22
|
+
const parsed = parseRecallScope(raw);
|
|
23
|
+
const lineageEntryIds = parsed.scope === "lineage"
|
|
24
|
+
? getActiveLineageEntryIds(ctx.sessionManager)
|
|
25
|
+
: undefined;
|
|
26
|
+
if (!parsed.text) {
|
|
27
|
+
// No query: show recent
|
|
28
|
+
const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
|
|
29
|
+
const recent = rendered.slice(-DEFAULT_RECENT);
|
|
30
|
+
const output = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
|
|
31
|
+
pi.sendMessage({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse page:N from args
|
|
36
|
+
const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
|
|
37
|
+
const page = pageMatch ? Math.max(1, parseInt(pageMatch[1], 10)) : 1;
|
|
38
|
+
const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
|
|
39
|
+
|
|
40
|
+
if (!query) {
|
|
41
|
+
const { rendered } = loadAllMessages(sessionFile, false, lineageEntryIds);
|
|
42
|
+
const recent = rendered.slice(-DEFAULT_RECENT);
|
|
43
|
+
const output = (parsed.scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
|
|
44
|
+
pi.sendMessage({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: true });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
|
|
49
|
+
const allResults = searchEntries(rendered, rawMessages, query);
|
|
50
|
+
|
|
51
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
52
|
+
const pageResults = allResults.slice(start, start + PAGE_SIZE);
|
|
53
|
+
const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
|
|
54
|
+
const scopeSuffix = parsed.scope === "all" ? " (scope: all)" : "";
|
|
55
|
+
const header = totalPages > 1
|
|
56
|
+
? `Page ${page}/${totalPages} (${allResults.length} total matches${scopeSuffix})`
|
|
57
|
+
: `${allResults.length} matches${scopeSuffix}`;
|
|
58
|
+
const footer = page < totalPages
|
|
59
|
+
? `\n--- /pi-vcc-recall ${query}${parsed.scope === "all" ? " scope:all" : ""} page:${page + 1} ---`
|
|
60
|
+
: "";
|
|
61
|
+
const output = formatRecallOutput(pageResults, query, header) + footer;
|
|
62
|
+
pi.sendMessage({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: true });
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
};
|