@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/src/sections.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TranscriptEntry } from "./core/brief";
|
|
2
|
+
import type { SymbolRef } from "./extract/shared-symbols";
|
|
3
|
+
|
|
4
|
+
export interface SectionData {
|
|
5
|
+
sessionGoal: string[];
|
|
6
|
+
outstandingContext: string[];
|
|
7
|
+
filesAndChanges: string[];
|
|
8
|
+
commits: string[];
|
|
9
|
+
userPreferences: string[];
|
|
10
|
+
/** Exported signatures from modified/read files */
|
|
11
|
+
typeCatalog: string[];
|
|
12
|
+
/** Symbol-level changes (function/type/class names per file) */
|
|
13
|
+
symbolChanges: SymbolRef[];
|
|
14
|
+
/** Current working context: what's being worked on, last action, next steps */
|
|
15
|
+
currentStatus: string[];
|
|
16
|
+
/** Per-turn one-liner summaries for the HCA zone (heaviest compression, oldest turns) */
|
|
17
|
+
turnSummaries: string[];
|
|
18
|
+
/** Structured reference anchors for zero-tool-call recall */
|
|
19
|
+
anchors: string[];
|
|
20
|
+
briefTranscript: string;
|
|
21
|
+
/** Structured transcript entries (verbose object format) */
|
|
22
|
+
transcriptEntries: TranscriptEntry[];
|
|
23
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { loadAllMessages } from "../core/load-messages";
|
|
5
|
+
import { searchEntries } from "../core/search-entries";
|
|
6
|
+
import { formatRecallOutput } from "../core/format-recall";
|
|
7
|
+
import { getActiveLineageEntryIds } from "../core/lineage";
|
|
8
|
+
import { normalizeRecallScope } from "../core/recall-scope";
|
|
9
|
+
import type { PiVccCompactionDetails } from "../details";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_RECENT = 25;
|
|
12
|
+
const PAGE_SIZE = 5;
|
|
13
|
+
|
|
14
|
+
export const invalidExpandIndices = (requested: number[], available: Set<number>): number[] =>
|
|
15
|
+
requested.filter((i) => !Number.isInteger(i) || !available.has(i));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read the session file, find compaction entries, and resolve the
|
|
19
|
+
* message range for a given compaction index (0-based).
|
|
20
|
+
*
|
|
21
|
+
* The stored messageRange uses entry IDs ([firstId, lastId]).
|
|
22
|
+
* This function resolves those IDs to global message indices
|
|
23
|
+
* by scanning the session file.
|
|
24
|
+
*
|
|
25
|
+
* Returns [startIndex, endIndex] or undefined if not found.
|
|
26
|
+
*/
|
|
27
|
+
const resolveCompactionMessageRange = (
|
|
28
|
+
sessionFile: string,
|
|
29
|
+
scopeStr: string,
|
|
30
|
+
): [number, number] | undefined => {
|
|
31
|
+
const isLatest = scopeStr === "compaction:latest";
|
|
32
|
+
const targetIndex = isLatest
|
|
33
|
+
? -1
|
|
34
|
+
: parseInt(scopeStr.replace("compaction:", ""), 10);
|
|
35
|
+
if (isNaN(targetIndex) && !isLatest) return undefined;
|
|
36
|
+
|
|
37
|
+
const content = readFileSync(sessionFile, "utf-8");
|
|
38
|
+
const entries: any[] = [];
|
|
39
|
+
for (const line of content.split("\n")) {
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
try {
|
|
42
|
+
entries.push(JSON.parse(line));
|
|
43
|
+
} catch {
|
|
44
|
+
/* skip malformed lines */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Collect pi-vcc compaction entries in order
|
|
49
|
+
const compactions = entries.filter(
|
|
50
|
+
(e: any) => e.type === "compaction" && e.details?.compactor === "pi-vcc",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (compactions.length === 0) return undefined;
|
|
54
|
+
|
|
55
|
+
const target = isLatest
|
|
56
|
+
? compactions[compactions.length - 1]
|
|
57
|
+
: compactions[targetIndex];
|
|
58
|
+
|
|
59
|
+
const msgRange = target?.details?.messageRange as [string, string] | undefined;
|
|
60
|
+
if (!msgRange) return undefined;
|
|
61
|
+
|
|
62
|
+
// Build entry-id → global-index map from the full session file
|
|
63
|
+
const idToGlobal = new Map<string, number>();
|
|
64
|
+
let globalIdx = 0;
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (e.type === "message" && e.message) {
|
|
67
|
+
if (e.id) idToGlobal.set(e.id, globalIdx);
|
|
68
|
+
globalIdx++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const firstIdx = idToGlobal.get(msgRange[0]);
|
|
73
|
+
const lastIdx = idToGlobal.get(msgRange[1]);
|
|
74
|
+
if (firstIdx === undefined || lastIdx === undefined) return undefined;
|
|
75
|
+
|
|
76
|
+
return [firstIdx, lastIdx];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const registerRecallTool = (pi: ExtensionAPI) => {
|
|
80
|
+
pi.registerTool({
|
|
81
|
+
name: "vcc_recall",
|
|
82
|
+
label: "VCC Recall",
|
|
83
|
+
description:
|
|
84
|
+
"Search session history. Defaults to active lineage; use scope:'all' to include off-lineage branches." +
|
|
85
|
+
" Supports regex queries, paging, and expand indices. " +
|
|
86
|
+
"Use scope:'compaction:N' to search within a specific compaction's message range.",
|
|
87
|
+
promptSnippet:
|
|
88
|
+
"vcc_recall: Search history; default scope is active lineage. " +
|
|
89
|
+
"Use scope:'all' for off-lineage branches. " +
|
|
90
|
+
"Use scope:'compaction:N' or scope:'compaction:latest' for targeted search within a compaction segment.",
|
|
91
|
+
parameters: Type.Object({
|
|
92
|
+
query: Type.Optional(
|
|
93
|
+
Type.String({ description: "Search terms or regex pattern (e.g. 'hook|inject', 'fail.*build'). Multi-word = OR ranked by relevance." }),
|
|
94
|
+
),
|
|
95
|
+
expand: Type.Optional(
|
|
96
|
+
Type.Array(Type.Number(), { description: "Entry indices to return full untruncated content for" }),
|
|
97
|
+
),
|
|
98
|
+
page: Type.Optional(
|
|
99
|
+
Type.Number({ description: "Page number (1-based) for paginated search results. Default: 1." }),
|
|
100
|
+
),
|
|
101
|
+
scope: Type.Optional(
|
|
102
|
+
Type.String({ description: "Search scope. Options: 'lineage' (default), 'all' (entire session), 'compaction:N' (within compaction #N), 'compaction:latest' (most recent compaction segment)." }),
|
|
103
|
+
),
|
|
104
|
+
}),
|
|
105
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
106
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
107
|
+
if (!sessionFile) {
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: "No session file available." }],
|
|
110
|
+
details: undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rawScope = normalizeRecallScope(params.scope);
|
|
115
|
+
const scopeStr = String(params.scope ?? "").toLowerCase();
|
|
116
|
+
const isCompactionScope = scopeStr.startsWith("compaction:");
|
|
117
|
+
|
|
118
|
+
// Resolve compaction-scoped message range
|
|
119
|
+
let entryFilter: ((idx: number) => boolean) | undefined;
|
|
120
|
+
let scopeLabel = "";
|
|
121
|
+
|
|
122
|
+
if (isCompactionScope) {
|
|
123
|
+
const range = resolveCompactionMessageRange(sessionFile, scopeStr);
|
|
124
|
+
if (!range) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: `No compaction found for scope: ${scopeStr}. Use scope:'lineage' (default) or scope:'all'.` }],
|
|
127
|
+
details: undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const [start, end] = range;
|
|
131
|
+
entryFilter = (idx: number) => idx >= start && idx <= end;
|
|
132
|
+
scopeLabel = ` (scope: ${scopeStr}, messages [#${start}, #${end}])`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lineageEntryIds = rawScope === "lineage"
|
|
136
|
+
? getActiveLineageEntryIds(ctx.sessionManager)
|
|
137
|
+
: undefined;
|
|
138
|
+
const expandSet = new Set(params.expand ?? []);
|
|
139
|
+
const hasExpand = expandSet.size > 0;
|
|
140
|
+
|
|
141
|
+
if (hasExpand && !params.query) {
|
|
142
|
+
const { rendered: fullMsgs } = loadAllMessages(sessionFile, true, lineageEntryIds, entryFilter);
|
|
143
|
+
const requested = [...expandSet];
|
|
144
|
+
const byIndex = new Map(fullMsgs.map((m) => [m.index, m]));
|
|
145
|
+
const invalid = invalidExpandIndices(requested, new Set(byIndex.keys()));
|
|
146
|
+
if (invalid.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: `Cannot expand indices outside ${rawScope === "all" ? "session history" : "active lineage"}${scopeLabel}: ${invalid.join(", ")}` }],
|
|
149
|
+
details: undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const expanded = requested.map((i) => byIndex.get(i)).filter((m): m is NonNullable<typeof m> => Boolean(m));
|
|
154
|
+
const output = (scopeLabel || (rawScope === "all" ? "Scope: all" : "")) + "\n" + formatRecallOutput(expanded);
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text", text: output }],
|
|
157
|
+
details: undefined,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { rendered: msgs, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds, entryFilter);
|
|
162
|
+
const allResults = params.query?.trim()
|
|
163
|
+
? searchEntries(msgs, rawMessages, params.query)
|
|
164
|
+
: msgs.slice(-DEFAULT_RECENT);
|
|
165
|
+
|
|
166
|
+
if (params.query?.trim()) {
|
|
167
|
+
const searchScopeLabel = scopeLabel || (rawScope === "all" ? " (scope: all)" : "");
|
|
168
|
+
const page = Math.max(1, params.page ?? 1);
|
|
169
|
+
const totalPages = Math.ceil(allResults.length / PAGE_SIZE);
|
|
170
|
+
// Cap pages: don't let the agent page through hundreds of results.
|
|
171
|
+
// If totalPages is too large, suggest narrowing the query instead.
|
|
172
|
+
const MAX_PAGES = 5;
|
|
173
|
+
if (allResults.length > 0 && page > Math.min(totalPages, MAX_PAGES)) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: `Too many results to page through (${allResults.length} matches across ${totalPages} pages). Try a more specific query or use scope:'compaction:N' to narrow the range${searchScopeLabel}.` }],
|
|
176
|
+
details: undefined,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
180
|
+
const pageResults = allResults.slice(start, start + PAGE_SIZE);
|
|
181
|
+
const header = totalPages > 1
|
|
182
|
+
? `Page ${page}/${totalPages} (${allResults.length} total matches${searchScopeLabel})`
|
|
183
|
+
: `${allResults.length} matches${searchScopeLabel}`;
|
|
184
|
+
const footer = page < totalPages && page < MAX_PAGES
|
|
185
|
+
? `\n--- Use page:${page + 1} for more results ---`
|
|
186
|
+
: totalPages > MAX_PAGES
|
|
187
|
+
? `\n--- Results truncated at ${MAX_PAGES} pages. Use a more specific query or scope to narrow results. ---`
|
|
188
|
+
: "";
|
|
189
|
+
const output = formatRecallOutput(pageResults, params.query, header) + footer;
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: output }],
|
|
192
|
+
details: undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const output = (scopeLabel || (rawScope === "all" ? "Scope: all" : "")) + "\n" + formatRecallOutput(allResults, params.query);
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: output }],
|
|
199
|
+
details: undefined,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export interface FileOps {
|
|
4
|
+
readFiles?: string[];
|
|
5
|
+
modifiedFiles?: string[];
|
|
6
|
+
createdFiles?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Pre-computed look-ahead index: maps tool_call index → nearest tool_result block. */
|
|
10
|
+
export interface ToolResultIndex {
|
|
11
|
+
get(callIndex: number): Extract<NormalizedBlock, { kind: "tool_result" }> | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type NormalizedBlock =
|
|
15
|
+
| { kind: "user"; text: string; sourceIndex?: number }
|
|
16
|
+
| { kind: "assistant"; text: string; sourceIndex?: number }
|
|
17
|
+
| { kind: "tool_call"; name: string; args: Record<string, unknown>; sourceIndex?: number }
|
|
18
|
+
| { kind: "tool_result"; name: string; text: string; isError: boolean; sourceIndex?: number }
|
|
19
|
+
| { kind: "bash"; command: string; output: string; exitCode: number | undefined; sourceIndex?: number }
|
|
20
|
+
| { kind: "thinking"; text: string; redacted: boolean; sourceIndex?: number };
|