@monotykamary/pi-vcc 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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/__tests__/helpers.ts +0 -135
- package/__tests__/root-causes.test.ts +0 -858
- package/demo.gif +0 -0
- package/knip.json +0 -4
- package/scripts/audit-sessions.ts +0 -88
- package/scripts/benchmark-real-sessions.ts +0 -25
- package/scripts/compare-before-after.ts +0 -36
- package/scripts/dump-branch-output.ts +0 -20
- package/stress-debug.ts +0 -202
- package/stress-degradation-curve.ts +0 -251
- package/tests/australian-government.test.ts +0 -479
- package/tests/before-compact-hook.test.ts +0 -185
- package/tests/before-compact.test.ts +0 -227
- package/tests/breadcrumbs.test.ts +0 -312
- package/tests/brief.test.ts +0 -215
- package/tests/build-sections.test.ts +0 -59
- package/tests/compile.test.ts +0 -89
- package/tests/content.test.ts +0 -66
- package/tests/deep-extraction.test.ts +0 -322
- package/tests/extract-goals.test.ts +0 -86
- package/tests/extract-preferences.test.ts +0 -30
- package/tests/filter-noise.test.ts +0 -61
- package/tests/fixtures.ts +0 -61
- package/tests/format-recall.test.ts +0 -87
- package/tests/format.test.ts +0 -73
- package/tests/government-domain.test.ts +0 -542
- package/tests/lineage.test.ts +0 -33
- package/tests/load-messages.test.ts +0 -49
- package/tests/model-threshold-hook.test.ts +0 -311
- package/tests/model-threshold.test.ts +0 -223
- package/tests/normalize.test.ts +0 -104
- package/tests/proactive-threshold.test.ts +0 -490
- package/tests/real-sessions.test.ts +0 -38
- package/tests/recall-expand.test.ts +0 -15
- package/tests/recall-scope.test.ts +0 -32
- package/tests/recall-tool-scope.test.ts +0 -67
- package/tests/render-entries.test.ts +0 -92
- package/tests/report.test.ts +0 -58
- package/tests/sanitize.test.ts +0 -24
- package/tests/search-entries.test.ts +0 -233
- package/tests/stress-100-compactions.test.ts +0 -373
- package/tests/support/load-session.ts +0 -23
- package/tests/support/real-sessions.ts +0 -51
- package/tests/threshold-integration.test.ts +0 -335
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stress test: 100 successive compactions measuring information degradation.
|
|
3
|
-
*
|
|
4
|
-
* Each compaction cycle:
|
|
5
|
-
* 1. Adds a fresh "round" of messages (user request → edits → reads → response)
|
|
6
|
-
* 2. Compiles with the previous summary as previousSummary
|
|
7
|
-
* 3. Measures what survived and what was lost
|
|
8
|
-
*
|
|
9
|
-
* Degradation metrics per round:
|
|
10
|
-
* - Output size (chars, lines)
|
|
11
|
-
* - Section counts (how many of the 7 header sections present)
|
|
12
|
-
* - Files known (paths in Files And Changes)
|
|
13
|
-
* - Breadcrumbs present (count of - ...recall: lines and +recall: markers)
|
|
14
|
-
* - Goals known (Session Goal lines)
|
|
15
|
-
* - Type catalog entries
|
|
16
|
-
* - Brief transcript size (lines after ---)
|
|
17
|
-
* - Recall recoverability: vcc_recall search for a given file/goal term
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { describe, test, expect } from "bun:test";
|
|
21
|
-
import { compile } from "../src/core/summarize";
|
|
22
|
-
import type { Message } from "@earendil-works/pi-ai";
|
|
23
|
-
|
|
24
|
-
// Deterministic message factories
|
|
25
|
-
|
|
26
|
-
const makeUserMsg = (text: string): Message => ({
|
|
27
|
-
role: "user",
|
|
28
|
-
content: text,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const makeAssistantMsg = (text: string): Message => ({
|
|
32
|
-
role: "assistant",
|
|
33
|
-
content: text,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const makeToolCall = (
|
|
37
|
-
name: string,
|
|
38
|
-
id: string,
|
|
39
|
-
args: Record<string, unknown>,
|
|
40
|
-
): Message => ({
|
|
41
|
-
role: "assistant",
|
|
42
|
-
content: [
|
|
43
|
-
{ type: "text", text: "" },
|
|
44
|
-
{ type: "toolCall", name, id, arguments: args },
|
|
45
|
-
],
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const makeToolResult = (
|
|
49
|
-
toolCallId: string,
|
|
50
|
-
toolName: string,
|
|
51
|
-
content: string,
|
|
52
|
-
isError = false,
|
|
53
|
-
): Message => ({
|
|
54
|
-
role: "toolResult",
|
|
55
|
-
toolCallId,
|
|
56
|
-
toolName,
|
|
57
|
-
content,
|
|
58
|
-
isError,
|
|
59
|
-
} as any);
|
|
60
|
-
|
|
61
|
-
// Each "round" simulates a realistic compaction-worthy conversation chunk.
|
|
62
|
-
// We vary the files and goals to create accumulation pressure.
|
|
63
|
-
|
|
64
|
-
const FILES = [
|
|
65
|
-
"src/auth.ts", "src/users.ts", "src/api.ts", "src/db.ts",
|
|
66
|
-
"src/utils.ts", "src/config.ts", "src/routes.ts", "src/middleware.ts",
|
|
67
|
-
"src/models.ts", "src/services.ts", "src/handlers.ts", "src/types.ts",
|
|
68
|
-
"src/errors.ts", "src/validators.ts", "src/cache.ts", "src/logger.ts",
|
|
69
|
-
"src/session.ts", "src/token.ts", "src/oauth.ts", "src/crypto.ts",
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const GOALS = [
|
|
73
|
-
"Build an authentication system",
|
|
74
|
-
"Add user management endpoints",
|
|
75
|
-
"Create the REST API layer",
|
|
76
|
-
"Set up the database connection pool",
|
|
77
|
-
"Implement utility functions",
|
|
78
|
-
"Load configuration from environment",
|
|
79
|
-
"Define route handlers",
|
|
80
|
-
"Add authentication middleware",
|
|
81
|
-
"Create data models",
|
|
82
|
-
"Build the service layer",
|
|
83
|
-
"Implement request handlers",
|
|
84
|
-
"Add TypeScript type definitions",
|
|
85
|
-
"Create error handling utilities",
|
|
86
|
-
"Build input validators",
|
|
87
|
-
"Add caching layer",
|
|
88
|
-
"Set up structured logging",
|
|
89
|
-
"Implement session management",
|
|
90
|
-
"Add JWT token handling",
|
|
91
|
-
"Build OAuth2 integration",
|
|
92
|
-
"Add cryptographic utilities",
|
|
93
|
-
"Refactor auth to support SSO",
|
|
94
|
-
"Add rate limiting to API",
|
|
95
|
-
"Migrate database to PostgreSQL",
|
|
96
|
-
"Add integration tests",
|
|
97
|
-
"Set up CI pipeline",
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
/** Generate messages for one "round" of work */
|
|
101
|
-
const makeRound = (roundIdx: number): Message[] => {
|
|
102
|
-
const file1 = FILES[roundIdx % FILES.length];
|
|
103
|
-
const file2 = FILES[(roundIdx + 7) % FILES.length];
|
|
104
|
-
const goal = GOALS[roundIdx % GOALS.length];
|
|
105
|
-
const id1 = `r${roundIdx}-1`;
|
|
106
|
-
const id2 = `r${roundIdx}-2`;
|
|
107
|
-
const id3 = `r${roundIdx}-3`;
|
|
108
|
-
|
|
109
|
-
return [
|
|
110
|
-
makeUserMsg(goal),
|
|
111
|
-
makeToolCall("Edit", id1, {
|
|
112
|
-
file_path: file1,
|
|
113
|
-
oldText: "// placeholder",
|
|
114
|
-
newText: `export function fn${roundIdx}() { return ${roundIdx}; }`,
|
|
115
|
-
}),
|
|
116
|
-
makeToolResult(id1, "Edit", "OK"),
|
|
117
|
-
makeAssistantMsg(`Edited ${file1} with function fn${roundIdx}.`),
|
|
118
|
-
makeToolCall("read", id2, { file_path: file2 }),
|
|
119
|
-
makeToolResult(id2, "read", `// ${file2}\nexport const DATA = ${roundIdx};`),
|
|
120
|
-
makeAssistantMsg(`Read ${file2} to understand the interface.`),
|
|
121
|
-
// Sometimes add a commit
|
|
122
|
-
...(roundIdx % 3 === 0
|
|
123
|
-
? [
|
|
124
|
-
makeToolCall("bash", id3, { command: `git commit -m "feat: ${goal.toLowerCase()}"` }),
|
|
125
|
-
makeToolResult(id3, "bash", `[main abc${roundIdx.toString().padStart(4, "0")}] feat: ${goal.toLowerCase()}`),
|
|
126
|
-
makeAssistantMsg(`Committed: feat: ${goal.toLowerCase()}.`),
|
|
127
|
-
]
|
|
128
|
-
: []),
|
|
129
|
-
];
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Metrics extraction
|
|
133
|
-
|
|
134
|
-
const countBreadcrumbs = (text: string): number => {
|
|
135
|
-
let count = 0;
|
|
136
|
-
// - ...recall: lines in header sections
|
|
137
|
-
for (const line of text.split("\n")) {
|
|
138
|
-
if (line.startsWith("- ...recall:")) count++;
|
|
139
|
-
}
|
|
140
|
-
// +recall: markers in Files And Changes
|
|
141
|
-
const plusRecall = text.match(/\+recall:/g);
|
|
142
|
-
count += plusRecall ? plusRecall.length : 0;
|
|
143
|
-
return count;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const extractFilePaths = (text: string): Set<string> => {
|
|
147
|
-
const paths = new Set<string>();
|
|
148
|
-
for (const line of text.split("\n")) {
|
|
149
|
-
const match = line.match(/^-\s*(?:Modified|Created|Read):\s*(.*)/);
|
|
150
|
-
if (!match) continue;
|
|
151
|
-
const rest = match[1]
|
|
152
|
-
.replace(/\+recall:\s*/g, "")
|
|
153
|
-
.replace(/\s*\([^)]*\)/g, "");
|
|
154
|
-
for (const p of rest.split(",")) {
|
|
155
|
-
const trimmed = p.trim();
|
|
156
|
-
if (trimmed && !trimmed.startsWith("+")) paths.add(trimmed);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return paths;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const extractGoals = (text: string): string[] => {
|
|
163
|
-
const section = text.indexOf("[Session Goal]");
|
|
164
|
-
if (section < 0) return [];
|
|
165
|
-
const after = text.slice(section);
|
|
166
|
-
const nextSection = after.indexOf("\n[", 1);
|
|
167
|
-
const block = (nextSection > 0 ? after.slice(0, nextSection) : after);
|
|
168
|
-
return block.split("\n")
|
|
169
|
-
.filter(l => l.startsWith("- ") && !l.startsWith("- ...recall:"))
|
|
170
|
-
.map(l => l.slice(2));
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const countTypeCatalogEntries = (text: string): number => {
|
|
174
|
-
const section = text.indexOf("[Type Catalog]");
|
|
175
|
-
if (section < 0) return 0;
|
|
176
|
-
const after = text.slice(section);
|
|
177
|
-
const nextSection = after.indexOf("\n\n[", 1);
|
|
178
|
-
const nextSep = after.indexOf("\n\n---\n\n", 1);
|
|
179
|
-
const end = Math.min(
|
|
180
|
-
nextSection > 0 ? nextSection : Infinity,
|
|
181
|
-
nextSep > 0 ? nextSep : Infinity,
|
|
182
|
-
);
|
|
183
|
-
const block = after.slice(0, end);
|
|
184
|
-
return block.split("\n").filter(l => l.match(/^\s*src\//) || l.match(/^\s*\S+\.\w{1,12}:$/)).length;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const countSections = (text: string): number => {
|
|
188
|
-
const matches = text.match(/^\[.+\]/gm);
|
|
189
|
-
return matches ? matches.length : 0;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const briefTranscriptSize = (text: string): { lines: number; chars: number } => {
|
|
193
|
-
const idx = text.indexOf("\n\n---\n\n");
|
|
194
|
-
if (idx < 0) return { lines: 0, chars: 0 };
|
|
195
|
-
const brief = text.slice(idx + 6);
|
|
196
|
-
return { lines: brief.split("\n").filter(Boolean).length, chars: brief.length };
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const previousSummaryChars = (text: string): number => text.length;
|
|
200
|
-
|
|
201
|
-
/** Check if a file path is recoverable via breadcrumb or section content */
|
|
202
|
-
const isFileRecoverable = (text: string, filePath: string): boolean => {
|
|
203
|
-
if (text.includes(filePath)) return true;
|
|
204
|
-
// Check breadcrumbs for partial path match
|
|
205
|
-
const shortName = filePath.split("/").pop() ?? filePath;
|
|
206
|
-
for (const line of text.split("\n")) {
|
|
207
|
-
if (line.includes("+recall:") || line.startsWith("- ...recall:")) {
|
|
208
|
-
if (line.includes(shortName)) return true;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return false;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
/** Check if a goal is recoverable via breadcrumb or section content */
|
|
215
|
-
const isGoalRecoverable = (text: string, goal: string): boolean => {
|
|
216
|
-
if (text.includes(goal)) return true;
|
|
217
|
-
// Check breadcrumbs for keywords from the goal
|
|
218
|
-
const keywords = goal.split(/\s+/).filter(w => w.length > 3).slice(0, 3);
|
|
219
|
-
for (const line of text.split("\n")) {
|
|
220
|
-
if (line.startsWith("- ...recall:")) {
|
|
221
|
-
const allPresent = keywords.every(kw => line.toLowerCase().includes(kw.toLowerCase()));
|
|
222
|
-
if (allPresent) return true;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
describe("stress: 100 compactions", () => {
|
|
229
|
-
test("100 successive compactions with degradation metrics", () => {
|
|
230
|
-
const TOTAL_ROUNDS = 100;
|
|
231
|
-
const previousSummary: string[] = [""];
|
|
232
|
-
const metrics: Array<{
|
|
233
|
-
round: number;
|
|
234
|
-
outputChars: number;
|
|
235
|
-
outputLines: number;
|
|
236
|
-
sections: number;
|
|
237
|
-
breadcrumbs: number;
|
|
238
|
-
filesKnown: number;
|
|
239
|
-
goalsKnown: number;
|
|
240
|
-
typeCatalogEntries: number;
|
|
241
|
-
briefLines: number;
|
|
242
|
-
briefChars: number;
|
|
243
|
-
// Cumulative tracking
|
|
244
|
-
totalFilesTouched: number;
|
|
245
|
-
totalGoalsSet: number;
|
|
246
|
-
fileRecallRate: number;
|
|
247
|
-
goalRecallRate: number;
|
|
248
|
-
}> = [];
|
|
249
|
-
|
|
250
|
-
const allFilesEver = new Set<string>();
|
|
251
|
-
const allGoalsEver: string[] = [];
|
|
252
|
-
|
|
253
|
-
for (let round = 0; round < TOTAL_ROUNDS; round++) {
|
|
254
|
-
const messages = makeRound(round);
|
|
255
|
-
|
|
256
|
-
// Track what was introduced this round
|
|
257
|
-
const goal = GOALS[round % GOALS.length];
|
|
258
|
-
allGoalsEver.push(goal);
|
|
259
|
-
allFilesEver.add(FILES[round % FILES.length]);
|
|
260
|
-
allFilesEver.add(FILES[(round + 7) % FILES.length]);
|
|
261
|
-
|
|
262
|
-
const result = compile({
|
|
263
|
-
messages,
|
|
264
|
-
previousSummary: previousSummary[0] || undefined,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
previousSummary[0] = result;
|
|
268
|
-
|
|
269
|
-
// Extract metrics
|
|
270
|
-
const filePaths = extractFilePaths(result);
|
|
271
|
-
const goals = extractGoals(result);
|
|
272
|
-
const bc = countBreadcrumbs(result);
|
|
273
|
-
const tcEntries = countTypeCatalogEntries(result);
|
|
274
|
-
const sections = countSections(result);
|
|
275
|
-
const brief = briefTranscriptSize(result);
|
|
276
|
-
|
|
277
|
-
// Recall rates: how many of all-ever files/goals can be found?
|
|
278
|
-
let filesRecoverable = 0;
|
|
279
|
-
for (const f of allFilesEver) {
|
|
280
|
-
if (isFileRecoverable(result, f)) filesRecoverable++;
|
|
281
|
-
}
|
|
282
|
-
let goalsRecoverable = 0;
|
|
283
|
-
for (const g of allGoalsEver) {
|
|
284
|
-
if (isGoalRecoverable(result, g)) goalsRecoverable++;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
metrics.push({
|
|
288
|
-
round: round + 1,
|
|
289
|
-
outputChars: result.length,
|
|
290
|
-
outputLines: result.split("\n").length,
|
|
291
|
-
sections,
|
|
292
|
-
breadcrumbs: bc,
|
|
293
|
-
filesKnown: filePaths.size,
|
|
294
|
-
goalsKnown: goals.length,
|
|
295
|
-
typeCatalogEntries: tcEntries,
|
|
296
|
-
briefLines: brief.lines,
|
|
297
|
-
briefChars: brief.chars,
|
|
298
|
-
totalFilesTouched: allFilesEver.size,
|
|
299
|
-
totalGoalsSet: allGoalsEver.length,
|
|
300
|
-
fileRecallRate: allFilesEver.size > 0 ? filesRecoverable / allFilesEver.size : 1,
|
|
301
|
-
goalRecallRate: allGoalsEver.length > 0 ? goalsRecoverable / allGoalsEver.length : 1,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Assertions on degradation
|
|
306
|
-
|
|
307
|
-
// 1. Output should never be empty
|
|
308
|
-
for (const m of metrics) {
|
|
309
|
-
expect(m.outputChars).toBeGreaterThan(0);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// 2. Sections should stabilize (at least Session Goal + Files + brief)
|
|
313
|
-
const lateMetrics = metrics.slice(-20);
|
|
314
|
-
for (const m of lateMetrics) {
|
|
315
|
-
expect(m.sections).toBeGreaterThanOrEqual(2);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// 3. File recall rate should not collapse to 0
|
|
319
|
-
// With +recall: breadcrumbs, even capped files should be partially recoverable
|
|
320
|
-
const finalMetric = metrics[metrics.length - 1];
|
|
321
|
-
expect(finalMetric.fileRecallRate).toBeGreaterThan(0);
|
|
322
|
-
|
|
323
|
-
// 4. Goal recall should not collapse to 0
|
|
324
|
-
expect(finalMetric.goalRecallRate).toBeGreaterThan(0);
|
|
325
|
-
|
|
326
|
-
// 5. Output size should be bounded — not growing unboundedly
|
|
327
|
-
// The output should plateau, not grow linearly
|
|
328
|
-
const charGrowthRate = (finalMetric.outputChars - metrics[49].outputChars) / 50;
|
|
329
|
-
const firstHalfGrowthRate = (metrics[49].outputChars - metrics[0].outputChars) / 50;
|
|
330
|
-
// Second half growth rate should be <= first half (plateauing)
|
|
331
|
-
expect(charGrowthRate).toBeLessThanOrEqual(firstHalfGrowthRate * 1.5);
|
|
332
|
-
|
|
333
|
-
// 6. Breadcrumbs should appear by round 20+ (sections start capping)
|
|
334
|
-
const breadcrumbsBy20 = metrics[19].breadcrumbs;
|
|
335
|
-
expect(breadcrumbsBy20).toBeGreaterThanOrEqual(0); // might be 0 if no section capped yet
|
|
336
|
-
|
|
337
|
-
// 7. By round 50, breadcrumbs should definitely exist
|
|
338
|
-
const breadcrumbsBy50 = metrics[49].breadcrumbs;
|
|
339
|
-
expect(breadcrumbsBy50).toBeGreaterThan(0);
|
|
340
|
-
|
|
341
|
-
// Print summary table for manual inspection
|
|
342
|
-
const sampleRounds = [1, 10, 25, 50, 75, 100];
|
|
343
|
-
console.log("\n╔══════════════════════════════════════════════════════════════════════════════════════════╗");
|
|
344
|
-
console.log("║ 100-COMPACTION STRESS TEST — DEGRADATION METRICS ║");
|
|
345
|
-
console.log("╠═══════╦═════════╦═════════╦═══════════╦═══════════╦══════════╦══════════╦══════════════╣");
|
|
346
|
-
console.log("║ Round ║ Chars ║ Lines ║ Sections ║ Breadcrumbs║ Files ║ Goals ║ Recall Rate ║");
|
|
347
|
-
console.log("║ ║ ║ ║ ║ ║ Known ║ Known ║ F / G ║");
|
|
348
|
-
console.log("╠═══════╬═════════╬═════════╬═══════════╬═══════════╬══════════╬══════════╬══════════════╣");
|
|
349
|
-
|
|
350
|
-
for (const r of sampleRounds) {
|
|
351
|
-
const m = metrics[r - 1];
|
|
352
|
-
const fileRate = `${(m.fileRecallRate * 100).toFixed(0)}%`;
|
|
353
|
-
const goalRate = `${(m.goalRecallRate * 100).toFixed(0)}%`;
|
|
354
|
-
console.log(
|
|
355
|
-
`║ ${String(r).padStart(5)} ║ ${String(m.outputChars).padStart(7)} ║ ${String(m.outputLines).padStart(7)} ║ ${String(m.sections).padStart(9)} ║ ${String(m.breadcrumbs).padStart(9)} ║ ${String(m.filesKnown).padStart(7)}/${String(m.totalFilesTouched).padStart(2)} ║ ${String(m.goalsKnown).padStart(7)}/${String(m.totalGoalsSet).padStart(3)} ║ ${fileRate.padStart(4)} / ${goalRate.padStart(4)} ║`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
console.log("╚═══════╩═════════╩═════════╩═══════════╩═══════════╩══════════╩══════════╩══════════════╝");
|
|
360
|
-
|
|
361
|
-
// Brief transcript degradation
|
|
362
|
-
console.log("\nBrief transcript size over time:");
|
|
363
|
-
for (const r of sampleRounds) {
|
|
364
|
-
const m = metrics[r - 1];
|
|
365
|
-
console.log(` Round ${String(r).padStart(3)}: ${m.briefLines} lines, ${m.briefChars} chars`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Final state check
|
|
369
|
-
console.log(`\nFinal output (${finalMetric.outputChars} chars, ${finalMetric.outputLines} lines):`);
|
|
370
|
-
console.log(previousSummary[0].slice(0, 800));
|
|
371
|
-
console.log("...");
|
|
372
|
-
});
|
|
373
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { buildSessionContext, loadEntriesFromFile } from "../../node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js";
|
|
2
|
-
import type { Message } from "@earendil-works/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
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
};
|