@monotykamary/pi-vcc 0.3.16 → 0.4.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.
@@ -0,0 +1,373 @@
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
+ });
@@ -5,9 +5,10 @@
5
5
  * - When proactive trigger calls ctx.compact(), session_before_compact
6
6
  * must NOT cancel the compaction even if tokensBefore differs from
7
7
  * getContextUsage().
8
- * - When the global threshold triggers compaction but the model can
9
- * handle more (per-model threshold not crossed), session_before_compact
10
- * must cancel.
8
+ * - The per-model threshold guard was removed from session_before_compact
9
+ * because the event carries no "reason" field — manual /compact and
10
+ * auto-compaction are indistinguishable. The proactive trigger handles
11
+ * the "compact earlier than pi-core" direction.
11
12
  */
12
13
  import { describe, test, expect, afterEach, beforeAll, afterAll } from "bun:test";
13
14
  import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync } from "fs";
@@ -100,11 +101,6 @@ describe("integration: proactive trigger + before-compact", () => {
100
101
  });
101
102
 
102
103
  test("proactive trigger then before-compact: does NOT cancel when proactiveTriggerActive", () => {
103
- // Scenario: agent_end fires, getContextUsage says 110k tokens,
104
- // per-model threshold is 95232 (128k - 32768). Proactive triggers compact.
105
- // Then session_before_compact fires with tokensBefore = 94000
106
- // (slightly different due to estimation).
107
- // Without the proactiveTriggerActive guard, this would be cancelled.
108
104
  setConfig({
109
105
  debug: false,
110
106
  overrideDefaultCompaction: true,
@@ -119,13 +115,9 @@ describe("integration: proactive trigger + before-compact", () => {
119
115
  registerProactiveThresholdHook(pi);
120
116
  registerBeforeCompactHook(pi);
121
117
 
122
- // Step 1: agent_end fires → proactive trigger calls ctx.compact()
123
118
  emit("agent_end", { type: "agent_end", messages: [] });
124
119
  expect(compactCalls).toHaveLength(1);
125
120
 
126
- // Step 2: session_before_compact fires with tokensBefore = 94000
127
- // (below the 95232 threshold — but we should NOT cancel because
128
- // we ourselves triggered this compaction)
129
121
  const entries = [
130
122
  msg("m1", "user", "hello"),
131
123
  msg("m2", "assistant", "hi"),
@@ -136,14 +128,14 @@ describe("integration: proactive trigger + before-compact", () => {
136
128
 
137
129
  // Should NOT cancel — proactiveTriggerActive is true
138
130
  expect(result?.cancel).toBeUndefined();
139
- // Should proceed with compaction (pi-vcc's own summary)
140
131
  expect(result?.compaction).toBeDefined();
141
132
  });
142
133
 
143
- test("global trigger + before-compact: CANCELS when per-model threshold not crossed and no proactive trigger", () => {
144
- // Scenario: pi-core's global threshold triggers compaction,
145
- // but the model's per-model threshold hasn't been crossed.
146
- // No proactive trigger was set — this is pi-core's own initiative.
134
+ test("global trigger + before-compact: compaction proceeds (threshold guard removed)", () => {
135
+ // Previously: pi-core's global threshold triggers compaction below the
136
+ // per-model threshold → cancelled. Now: the threshold guard is removed
137
+ // because manual /compact can't be distinguished from auto-compaction,
138
+ // so compaction proceeds.
147
139
  setConfig({
148
140
  debug: false,
149
141
  overrideDefaultCompaction: true,
@@ -151,24 +143,23 @@ describe("integration: proactive trigger + before-compact", () => {
151
143
  "neuralwatt/GLM-5.1": { reserveTokens: 32768 },
152
144
  },
153
145
  });
154
- const { pi, ctx, emit, compactCalls } = createMockPi(
146
+ const { pi, ctx, emit } = createMockPi(
155
147
  { id: "GLM-5.1", provider: "neuralwatt", contextWindow: 128000 },
156
- // getContextUsage says 80k — well below per-model threshold
157
148
  { tokens: 80000, contextWindow: 128000, percent: 63 },
158
149
  );
159
150
  registerProactiveThresholdHook(pi);
160
151
  registerBeforeCompactHook(pi);
161
152
 
162
- // No agent_end fired (global threshold triggered this)
163
153
  const entries = [
164
154
  msg("m1", "user", "hello"),
165
155
  msg("m2", "assistant", "hi"),
166
156
  msg("m3", "user", "do work"),
167
157
  msg("m4", "assistant", "done"),
168
158
  ];
169
- // tokensBefore = 90k < 95232 — should cancel
170
159
  const result = emit("session_before_compact", makeBeforeCompactEvent(entries, undefined, 90000));
171
- expect(result).toEqual({ cancel: true });
160
+ // No longer cancelled — threshold guard removed
161
+ expect(result?.cancel).toBeUndefined();
162
+ expect(result?.compaction).toBeDefined();
172
163
  });
173
164
 
174
165
  test("proactiveTriggerActive is cleared after session_compact", () => {
@@ -186,16 +177,13 @@ describe("integration: proactive trigger + before-compact", () => {
186
177
  registerProactiveThresholdHook(pi);
187
178
  registerBeforeCompactHook(pi);
188
179
 
189
- // Step 1: proactive trigger
190
180
  emit("agent_end", { type: "agent_end", messages: [] });
191
181
  expect(compactCalls).toHaveLength(1);
192
182
 
193
- // Step 2: compaction completes
194
183
  emit("session_compact", { type: "session_compact", compactionEntry: {} });
195
184
 
196
- // Step 3: now a subsequent session_before_compact (e.g., from pi-core
197
- // also triggering) with tokensBelow below threshold — should CANCEL
198
- // because proactiveTriggerActive was cleared by session_compact
185
+ // After session_compact, proactiveTriggerActive is cleared.
186
+ // But the threshold guard is also removed, so compaction proceeds.
199
187
  const entries = [
200
188
  msg("m1", "user", "hello"),
201
189
  msg("m2", "assistant", "hi"),
@@ -203,10 +191,11 @@ describe("integration: proactive trigger + before-compact", () => {
203
191
  msg("m4", "assistant", "done"),
204
192
  ];
205
193
  const result = emit("session_before_compact", makeBeforeCompactEvent(entries, undefined, 90000));
206
- expect(result).toEqual({ cancel: true });
194
+ expect(result?.cancel).toBeUndefined();
195
+ expect(result?.compaction).toBeDefined();
207
196
  });
208
197
 
209
- test("explicit /pi-vcc bypasses both the proactive flag AND threshold guard", () => {
198
+ test("explicit /pi-vcc proceeds regardless of context level", () => {
210
199
  setConfig({
211
200
  debug: false,
212
201
  overrideDefaultCompaction: true,
@@ -216,19 +205,18 @@ describe("integration: proactive trigger + before-compact", () => {
216
205
  });
217
206
  const { pi, ctx, emit } = createMockPi(
218
207
  { id: "GLM-5.1", provider: "neuralwatt", contextWindow: 128000 },
219
- { tokens: 50000, contextWindow: 128000, percent: 39 },
208
+ { tokens: 5000, contextWindow: 128000, percent: 4 },
220
209
  );
221
210
  registerProactiveThresholdHook(pi);
222
211
  registerBeforeCompactHook(pi);
223
212
 
224
- // Direct /pi-vcc with tokens well below threshold — should still proceed
225
213
  const entries = [
226
214
  msg("m1", "user", "hello"),
227
215
  msg("m2", "assistant", "hi"),
228
216
  msg("m3", "user", "do work"),
229
217
  msg("m4", "assistant", "done"),
230
218
  ];
231
- const result = emit("session_before_compact", makeBeforeCompactEvent(entries, "__pi_vcc__", 50000));
219
+ const result = emit("session_before_compact", makeBeforeCompactEvent(entries, "__pi_vcc__", 5000));
232
220
  expect(result?.cancel).toBeUndefined();
233
221
  expect(result?.compaction).toBeDefined();
234
222
  });
@@ -241,10 +229,6 @@ describe("integration: proactive trigger + before-compact with overrideDefaultCo
241
229
  });
242
230
 
243
231
  test("proactive trigger fires, before-compact doesn't cancel, then pi-core handles summary", () => {
244
- // When overrideDefaultCompaction: false, pi-vcc doesn't handle the summary
245
- // for non-/pi-vcc compactions. But the threshold guard and proactive trigger
246
- // should still work — the threshold guard controls WHEN compaction triggers,
247
- // and pi-core handles the actual summary.
248
232
  setConfig({
249
233
  debug: false,
250
234
  overrideDefaultCompaction: false,
@@ -259,14 +243,9 @@ describe("integration: proactive trigger + before-compact with overrideDefaultCo
259
243
  registerProactiveThresholdHook(pi);
260
244
  registerBeforeCompactHook(pi);
261
245
 
262
- // agent_end fires → proactive trigger
263
246
  emit("agent_end", { type: "agent_end", messages: [] });
264
247
  expect(compactCalls).toHaveLength(1);
265
248
 
266
- // session_before_compact fires: threshold is crossed (tokensBefore = 110k
267
- // > 95232), and our guard allows it because proactiveTriggerActive.
268
- // Then overrideDefaultCompaction: false means pi-vcc returns undefined,
269
- // letting pi-core handle the summary.
270
249
  const entries = [
271
250
  msg("m1", "user", "hello"),
272
251
  msg("m2", "assistant", "hi"),
@@ -275,11 +254,10 @@ describe("integration: proactive trigger + before-compact with overrideDefaultCo
275
254
  ];
276
255
  const result = emit("session_before_compact", makeBeforeCompactEvent(entries, undefined, 110000));
277
256
  // pi-vcc doesn't handle it (overrideDefaultCompaction: false) → returns undefined
278
- // pi-core will handle the summary
279
257
  expect(result).toBeUndefined();
280
258
  });
281
259
 
282
- test("threshold guard cancels even with overrideDefaultCompaction: false", () => {
260
+ test("compaction proceeds even with overrideDefaultCompaction: false (threshold guard removed)", () => {
283
261
  setConfig({
284
262
  debug: false,
285
263
  overrideDefaultCompaction: false,
@@ -294,15 +272,16 @@ describe("integration: proactive trigger + before-compact with overrideDefaultCo
294
272
  registerProactiveThresholdHook(pi);
295
273
  registerBeforeCompactHook(pi);
296
274
 
297
- // No proactive trigger (context below threshold)
298
- // Pi-core's global threshold triggers compaction, but per-model threshold
299
- // says the model can handle more → cancel
275
+ // Previously this would cancel (threshold guard). Now: compaction
276
+ // proceeds (threshold guard removed).
300
277
  const entries = [
301
278
  msg("m1", "user", "hello"),
302
279
  msg("m2", "assistant", "hi"),
303
280
  ];
304
281
  const result = emit("session_before_compact", makeBeforeCompactEvent(entries, undefined, 80000));
305
- expect(result).toEqual({ cancel: true });
282
+ // No longer cancelled — returns undefined (pi-vcc doesn't handle it,
283
+ // but doesn't block it either)
284
+ expect(result).toBeUndefined();
306
285
  });
307
286
  });
308
287
 
@@ -326,11 +305,9 @@ describe("integration: cooldown prevents double compaction", () => {
326
305
  );
327
306
  registerProactiveThresholdHook(pi);
328
307
 
329
- // agent_end triggers compact and sets cooldown
330
308
  emit("agent_end", { type: "agent_end", messages: [] });
331
309
  expect(compactCalls).toHaveLength(1);
332
310
 
333
- // model_select fires on same turn — blocked by cooldown
334
311
  emit("model_select", { type: "model_select" });
335
312
  expect(compactCalls).toHaveLength(1);
336
313
  });
@@ -352,7 +329,6 @@ describe("integration: cooldown prevents double compaction", () => {
352
329
  emit("agent_end", { type: "agent_end", messages: [] });
353
330
  expect(compactCalls).toHaveLength(1);
354
331
 
355
- // No session_compact between — cooldown still active
356
332
  emit("agent_end", { type: "agent_end", messages: [] });
357
333
  expect(compactCalls).toHaveLength(1);
358
334
  });