pi-mega-compact 0.6.9 → 0.7.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.
@@ -47,12 +47,39 @@ function harness(opts = {}) {
47
47
  // Minimal AgentMessage factory for the session we project into the extension.
48
48
  function msg(role, text, toolName) {
49
49
  if (role === "assistant" && toolName) {
50
- return { role: "assistant", content: [{ type: "toolCall", name: toolName, id: "c1", arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: 0 };
50
+ return {
51
+ role: "assistant",
52
+ content: [
53
+ { type: "toolCall", name: toolName, id: "c1", arguments: {} },
54
+ ],
55
+ api: "anthropic-messages",
56
+ provider: "anthropic",
57
+ model: "m",
58
+ usage: {
59
+ inputTokens: 1,
60
+ outputTokens: 1,
61
+ cacheReadTokens: 0,
62
+ cacheWriteTokens: 0,
63
+ },
64
+ stopReason: "tool_use",
65
+ timestamp: 0,
66
+ };
51
67
  }
52
68
  if (role === "toolResult" && toolName) {
53
- return { role: "toolResult", toolCallId: "c1", toolName, content: [{ type: "text", text }], isError: false, timestamp: 0 };
69
+ return {
70
+ role: "toolResult",
71
+ toolCallId: "c1",
72
+ toolName,
73
+ content: [{ type: "text", text }],
74
+ isError: false,
75
+ timestamp: 0,
76
+ };
54
77
  }
55
- return { role: "user", content: text, timestamp: 0 };
78
+ return {
79
+ role: "user",
80
+ content: text,
81
+ timestamp: 0,
82
+ };
56
83
  }
57
84
  const session = [
58
85
  msg("user", "read src/vec.ts and understand the index"),
@@ -85,7 +112,10 @@ function harness(opts = {}) {
85
112
  function makeCtx(over = {}) {
86
113
  return {
87
114
  ui: {
88
- setStatus: (k, t) => { statusKey = k; statusText = t; },
115
+ setStatus: (k, t) => {
116
+ statusKey = k;
117
+ statusText = t;
118
+ },
89
119
  notify: (s) => notifies.push(s),
90
120
  select: () => { },
91
121
  confirm: async () => true,
@@ -104,7 +134,11 @@ function harness(opts = {}) {
104
134
  abort: () => { },
105
135
  hasPendingMessages: () => false,
106
136
  shutdown: () => { },
107
- getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }),
137
+ getContextUsage: () => ({
138
+ tokens: 200000,
139
+ contextWindow: 200000,
140
+ percent: 100,
141
+ }),
108
142
  // Faithful mock: ctx.compact() starts pi's flow, which fires the
109
143
  // session_before_compact handler (where WE supply the durable trim).
110
144
  compact: (opts) => {
@@ -131,8 +165,12 @@ function harness(opts = {}) {
131
165
  };
132
166
  }
133
167
  const pi = {
134
- on: (ev, h) => { handlers[ev] = h; },
135
- registerCommand: (name, opts) => { commands[name] = opts; },
168
+ on: (ev, h) => {
169
+ handlers[ev] = h;
170
+ },
171
+ registerCommand: (name, opts) => {
172
+ commands[name] = opts;
173
+ },
136
174
  registerTool: () => { },
137
175
  registerShortcut: () => { },
138
176
  registerFlag: () => { },
@@ -158,7 +196,15 @@ function harness(opts = {}) {
158
196
  const mod = require("./mega-compact.js");
159
197
  mod.default(pi);
160
198
  return {
161
- stateDir, handlers, commands, appended, get status() { return { statusKey, statusText }; }, notifies, compactCalls,
199
+ stateDir,
200
+ handlers,
201
+ commands,
202
+ appended,
203
+ get status() {
204
+ return { statusKey, statusText };
205
+ },
206
+ notifies,
207
+ compactCalls,
162
208
  fire: (ev, event, ctx) => handlers[ev](event, ctx),
163
209
  ctx: makeCtx,
164
210
  session,
@@ -177,7 +223,13 @@ test("auto-trigger (legacy): past threshold persists a chkpt and starts a durabl
177
223
  process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = "0";
178
224
  process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
179
225
  try {
180
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
226
+ const ctx = h.ctx({
227
+ getContextUsage: () => ({
228
+ tokens: 200000,
229
+ contextWindow: 200000,
230
+ percent: 100,
231
+ }),
232
+ });
181
233
  const res = await h.fire("context", { type: "context", messages }, ctx);
182
234
  // L1->L4 ran: a checkpoint was persisted to the SQLite store + a marker entry written.
183
235
  const { listCheckpoints } = await import("../src/store/sqlite.js");
@@ -205,7 +257,13 @@ test("auto-trigger: skips ctx.compact() when pi would no-op (session too small,
205
257
  delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
206
258
  process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
207
259
  try {
208
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
260
+ const ctx = h.ctx({
261
+ getContextUsage: () => ({
262
+ tokens: 200000,
263
+ contextWindow: 200000,
264
+ percent: 100,
265
+ }),
266
+ });
209
267
  const res = await h.fire("context", { type: "context", messages }, ctx);
210
268
  assert.equal(res, undefined, "legacy context handler returns nothing (no local drop)");
211
269
  assert.equal(h.compactCalls.length, 0, "ctx.compact() NOT called — pi would no-op");
@@ -230,7 +288,13 @@ test("auto-trigger (S16): trims the live view and does NOT call ctx.compact()",
230
288
  delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
231
289
  process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
232
290
  try {
233
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
291
+ const ctx = h.ctx({
292
+ getContextUsage: () => ({
293
+ tokens: 200000,
294
+ contextWindow: 200000,
295
+ percent: 100,
296
+ }),
297
+ });
234
298
  const res = await h.fire("context", { type: "context", messages }, ctx);
235
299
  // S16: context handler returns a TRIMMED messages array (live trim), not undefined.
236
300
  assert.ok(res && typeof res === "object", "context handler returns a result object (live trim)");
@@ -254,7 +318,13 @@ test("auto-trigger (S16): does not trim when below the anchor floor (returns und
254
318
  delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
255
319
  delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
256
320
  const shortSession = [h.session[0], h.session[1]]; // one user + one assistant
257
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
321
+ const ctx = h.ctx({
322
+ getContextUsage: () => ({
323
+ tokens: 200000,
324
+ contextWindow: 200000,
325
+ percent: 100,
326
+ }),
327
+ });
258
328
  const res = await h.fire("context", { type: "context", messages: shortSession }, ctx);
259
329
  // Either it skipped (undefined) or trimmed safely — but it must never call ctx.compact.
260
330
  assert.equal(h.compactCalls.length, 0, "ctx.compact() NOT called under live trim (short session)");
@@ -284,8 +354,11 @@ test("auto-trigger (S16): durable trim still happens via pi native auto-compacti
284
354
  tokensBefore: 500,
285
355
  };
286
356
  const res = await h.fire("session_before_compact", {
287
- type: "session_before_compact", reason: "threshold", willRetry: false,
288
- signal: undefined, preparation: prep,
357
+ type: "session_before_compact",
358
+ reason: "threshold",
359
+ willRetry: false,
360
+ signal: undefined,
361
+ preparation: prep,
289
362
  }, h.ctx());
290
363
  assert.ok(res?.compaction, "we supply a durable compaction result to pi's native path");
291
364
  assert.ok(res.compaction.firstKeptEntryId === "e2", "reuses pi's boundary (PREVENT-PI-002)");
@@ -298,41 +371,78 @@ test("session_before_compact supplies our durable trim (not pi's summary)", asyn
298
371
  type: "session_before_compact",
299
372
  reason: "overflow",
300
373
  willRetry: true,
301
- preparation: { firstKeptEntryId: "e2", messagesToSummarize: h.session.slice(0, 2), tokensBefore: 500 },
374
+ preparation: {
375
+ firstKeptEntryId: "e2",
376
+ messagesToSummarize: h.session.slice(0, 2),
377
+ tokensBefore: 500,
378
+ },
302
379
  signal: undefined,
303
380
  }, h.ctx());
304
381
  assert.ok(res && res.compaction, "returns a compaction result");
305
382
  assert.equal(res.compaction.firstKeptEntryId, "e2", "reuses pi's cut boundary (PREVENT-PI-002 safe)");
306
- assert.ok(typeof res.compaction.summary === "string" && res.compaction.summary.length > 0, "our summary supplied");
383
+ assert.ok(typeof res.compaction.summary === "string" &&
384
+ res.compaction.summary.length > 0, "our summary supplied");
307
385
  assert.ok(res.compaction.tokensBefore >= 0, "tokensBefore reported");
308
386
  });
309
- test("session_before_compact falls back to pi when nothing to summarize", async () => {
387
+ test("session_before_compact supplies a fallback summary when nothing to summarize", async () => {
310
388
  const h = harness();
311
- // Empty preparation → no messages to summarize return {} so pi compacts natively.
389
+ // Empty preparation → no messages to summarize (anchor floor protects
390
+ // everything). We MUST still supply a compaction (never {}), otherwise pi
391
+ // runs its own compact() which throws "Nothing to compact (session too
392
+ // small)" and leaves the session stuck with no resume context. The fallback
393
+ // records a minimal resume summary so the session always resumes.
312
394
  const res = await h.fire("session_before_compact", {
313
395
  type: "session_before_compact",
314
396
  reason: "threshold",
315
397
  willRetry: false,
316
- preparation: { firstKeptEntryId: "e0", messagesToSummarize: [], tokensBefore: 0 },
398
+ preparation: {
399
+ firstKeptEntryId: "e0",
400
+ messagesToSummarize: [],
401
+ tokensBefore: 0,
402
+ },
317
403
  signal: undefined,
318
404
  }, h.ctx());
319
- assert.deepEqual(res, {}, "no compaction supplied pi runs its own");
405
+ assert.ok(res && res.compaction, "fallback compaction supplied (never {})");
406
+ assert.ok(res.compaction.summary.includes("context compacted"), "fallback summary injected so the session resumes");
407
+ assert.equal(res.compaction.firstKeptEntryId, "e0", "keeps pi's cut point");
320
408
  });
321
409
  test("resume auto-inline stages recall into the system prompt", async () => {
322
410
  const h = harness();
323
411
  // Seed a checkpoint first (simulate a prior session that compacted).
324
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) }));
412
+ await h.fire("context", { type: "context", messages: h.session }, h.ctx({
413
+ getContextUsage: () => ({
414
+ tokens: 200000,
415
+ contextWindow: 200000,
416
+ percent: 100,
417
+ }),
418
+ }));
325
419
  // Fresh resume: session_start with reason "resume".
326
420
  const ctx = h.ctx();
327
- await h.fire("session_start", { type: "session_start", reason: "resume", previousSessionFile: undefined }, ctx);
421
+ await h.fire("session_start", {
422
+ type: "session_start",
423
+ reason: "resume",
424
+ previousSessionFile: undefined,
425
+ }, ctx);
328
426
  // The next before_agent_start must prepend the recalled block.
329
- const res = await h.fire("before_agent_start", { type: "before_agent_start", prompt: "base system", images: undefined, systemPrompt: "base system", systemPromptOptions: {} }, ctx);
427
+ const res = await h.fire("before_agent_start", {
428
+ type: "before_agent_start",
429
+ prompt: "base system",
430
+ images: undefined,
431
+ systemPrompt: "base system",
432
+ systemPromptOptions: {},
433
+ }, ctx);
330
434
  assert.ok(res && typeof res.systemPrompt === "string", "before_agent_start returns a systemPrompt");
331
435
  assert.ok(res.systemPrompt.includes("Recalled context"), "recalled block injected into system prompt");
332
436
  });
333
437
  test("/recall-context reports and stages the top checkpoint", async () => {
334
438
  const h = harness();
335
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) }));
439
+ await h.fire("context", { type: "context", messages: h.session }, h.ctx({
440
+ getContextUsage: () => ({
441
+ tokens: 200000,
442
+ contextWindow: 200000,
443
+ percent: 100,
444
+ }),
445
+ }));
336
446
  const ctx = h.ctx();
337
447
  await h.commands["mega-recall"].handler("dedupe bug store.ts", ctx);
338
448
  assert.ok(h.notifies.some((n) => n.includes("recall staged")), "command reports staged checkpoints");
@@ -340,8 +450,20 @@ test("/recall-context reports and stages the top checkpoint", async () => {
340
450
  });
341
451
  test("/megacompact-status reports live store stats", async () => {
342
452
  const h = harness();
343
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) }));
344
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 50000, contextWindow: 200000, percent: 25 }) });
453
+ await h.fire("context", { type: "context", messages: h.session }, h.ctx({
454
+ getContextUsage: () => ({
455
+ tokens: 200000,
456
+ contextWindow: 200000,
457
+ percent: 100,
458
+ }),
459
+ }));
460
+ const ctx = h.ctx({
461
+ getContextUsage: () => ({
462
+ tokens: 50000,
463
+ contextWindow: 200000,
464
+ percent: 25,
465
+ }),
466
+ });
345
467
  await h.commands["mega-status"].handler("", ctx);
346
468
  assert.ok(h.notifies.some((n) => n.includes("store:") && n.includes("chkpt")), "status shows checkpoint count");
347
469
  });
@@ -349,8 +471,18 @@ test("/megacompact-status reports live store stats", async () => {
349
471
  test("model_select captures model + provider into SQL", async () => {
350
472
  const h = harness();
351
473
  const modelCtx = h.ctx({
352
- model: { id: "claude-opus-4-8", name: "Claude Opus 4.8", provider: "anthropic", contextWindow: 200000, maxTokens: 32000, reasoning: false, cost: { input: 0.000015, output: 0.000075 } },
353
- modelRegistry: { getProviderDisplayName: (p) => (p === "anthropic" ? "Anthropic" : p) },
474
+ model: {
475
+ id: "claude-opus-4-8",
476
+ name: "Claude Opus 4.8",
477
+ provider: "anthropic",
478
+ contextWindow: 200000,
479
+ maxTokens: 32000,
480
+ reasoning: false,
481
+ cost: { input: 0.000015, output: 0.000075 },
482
+ },
483
+ modelRegistry: {
484
+ getProviderDisplayName: (p) => p === "anthropic" ? "Anthropic" : p,
485
+ },
354
486
  });
355
487
  await h.fire("model_select", {}, modelCtx);
356
488
  const { latestModelSnapshot } = await import("../src/store/sqlite.js");
@@ -364,37 +496,71 @@ test("model_select captures model + provider into SQL", async () => {
364
496
  test("/mega-status surfaces the captured model + provider", async () => {
365
497
  const h = harness();
366
498
  const modelCtx = h.ctx({
367
- model: { id: "claude-opus-4-8", name: "Claude Opus 4.8", provider: "anthropic", contextWindow: 200000, maxTokens: 32000, reasoning: false, cost: { input: 0.000015, output: 0.000075 } },
499
+ model: {
500
+ id: "claude-opus-4-8",
501
+ name: "Claude Opus 4.8",
502
+ provider: "anthropic",
503
+ contextWindow: 200000,
504
+ maxTokens: 32000,
505
+ reasoning: false,
506
+ cost: { input: 0.000015, output: 0.000075 },
507
+ },
368
508
  modelRegistry: { getProviderDisplayName: () => "Anthropic" },
369
509
  });
370
510
  await h.fire("model_select", {}, modelCtx);
371
- await h.fire("context", { type: "context", messages: h.session }, h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) }));
372
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 50000, contextWindow: 200000, percent: 25 }) });
511
+ await h.fire("context", { type: "context", messages: h.session }, h.ctx({
512
+ getContextUsage: () => ({
513
+ tokens: 200000,
514
+ contextWindow: 200000,
515
+ percent: 100,
516
+ }),
517
+ }));
518
+ const ctx = h.ctx({
519
+ getContextUsage: () => ({
520
+ tokens: 50000,
521
+ contextWindow: 200000,
522
+ percent: 25,
523
+ }),
524
+ });
373
525
  await h.commands["mega-status"].handler("", ctx);
374
- assert.ok(h.notifies.some((n) => n.includes("🤖 model:") && n.includes("Claude Opus 4.8") && n.includes("Anthropic")), "status surfaces captured model + provider");
526
+ assert.ok(h.notifies.some((n) => n.includes("🤖 model:") &&
527
+ n.includes("Claude Opus 4.8") &&
528
+ n.includes("Anthropic")), "status surfaces captured model + provider");
375
529
  });
376
530
  // ---- Named compaction tiers -------------------------------------------------
377
531
  // low=50k, medium=100k, high=200k, ultra=1M, mega=10M. Driven through the REAL
378
532
  // loadConfig()/status path by setting MEGACOMPACT_TIER before loading the ext.
533
+ // Percentage-based thresholds: tierPct × the model context window. The harness
534
+ // getContextUsage below reports contextWindow=2_000_000, so each tier resolves to
535
+ // tierPct × 2_000_000 — which fires BELOW pi's native ~80% auto-compaction for
536
+ // ANY model size (200k or 1M). Driven through the REAL loadConfig()/status path
537
+ // by setting MEGACOMPACT_TIER before loading the ext.
379
538
  const TIER_CASES = [
380
- ["low", 50_000],
381
- ["medium", 100_000],
382
- ["high", 200_000],
383
- ["ultra", 1_000_000],
384
- ["mega", 10_000_000],
539
+ ["low", 1_000_000], // 0.50 × 2_000_000
540
+ ["medium", 1_200_000], // 0.60 × 2_000_000
541
+ ["high", 1_400_000], // 0.70 × 2_000_000
542
+ ["ultra", 1_400_000], // 0.70 × 2_000_000
543
+ ["mega", 1_500_000], // 0.75 × 2_000_000
385
544
  ];
386
545
  for (const [tier, threshold] of TIER_CASES) {
387
- test(`tier "${tier}" resolves to a ${threshold}-token threshold (preset; live band shown separately)`, async () => {
546
+ test(`tier "${tier}" resolves to a ${threshold.toLocaleString()}-token threshold (tierPct × 2M window; live band shown separately)`, async () => {
388
547
  // Keep tier + keep threshold UNSET so the tier (not an explicit number)
389
548
  // drives the threshold. harness() would otherwise reset the threshold.
390
549
  delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
391
550
  process.env.MEGACOMPACT_TIER = tier;
392
551
  const h = harness({ keepTier: true, keepThreshold: true });
393
552
  // tokens=1 against a 2M window → near-zero pressure → live band "low".
394
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 1, contextWindow: 2_000_000, percent: 0.01 }) });
553
+ const ctx = h.ctx({
554
+ getContextUsage: () => ({
555
+ tokens: 1,
556
+ contextWindow: 2_000_000,
557
+ percent: 0.01,
558
+ }),
559
+ });
395
560
  await h.commands["mega-status"].handler("", ctx);
396
561
  delete process.env.MEGACOMPACT_TIER;
397
- assert.ok(h.notifies.some((n) => n.includes(`preset=${tier}`) && n.includes(`threshold=${threshold}`)), `status should report preset=${tier} threshold=${threshold}`);
562
+ // /mega-status renders threshold with toLocaleString() (thousands commas).
563
+ assert.ok(h.notifies.some((n) => n.includes(`preset=${tier}`) && n.includes(`threshold=${threshold.toLocaleString()}`)), `status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`);
398
564
  // S24: the headline tier is the LIVE pressure band, shown as "tier=low (live)".
399
565
  assert.ok(h.notifies.some((n) => n.includes("tier=low (live)")), "live band reported (low at near-zero pressure)");
400
566
  });
@@ -403,7 +569,13 @@ test("explicit MEGACOMPACT_THRESHOLD_TOKENS overrides the tier", async () => {
403
569
  process.env.MEGACOMPACT_TIER = "mega";
404
570
  process.env.MEGACOMPACT_THRESHOLD_TOKENS = "777";
405
571
  const h = harness({ keepTier: true, keepThreshold: true });
406
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 1, contextWindow: 2_000_000, percent: 0.01 }) });
572
+ const ctx = h.ctx({
573
+ getContextUsage: () => ({
574
+ tokens: 1,
575
+ contextWindow: 2_000_000,
576
+ percent: 0.01,
577
+ }),
578
+ });
407
579
  await h.commands["mega-status"].handler("", ctx);
408
580
  delete process.env.MEGACOMPACT_TIER;
409
581
  assert.ok(h.notifies.some((n) => n.includes("preset=custom") && n.includes("threshold=777")), "explicit threshold wins over tier (preset=custom)");
@@ -415,9 +587,34 @@ test("explicit MEGACOMPACT_THRESHOLD_TOKENS overrides the tier", async () => {
415
587
  function decisionSession() {
416
588
  const out = [];
417
589
  for (let i = 0; i < 14; i++) {
418
- out.push({ role: "user", content: `actually we decided to use approach ${i} for module ${i}`, timestamp: i });
419
- out.push({ role: "assistant", content: [{ type: "toolCall", name: "Edit", id: `c${i}`, arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: i });
420
- out.push({ role: "toolResult", content: [{ type: "text", text: `edited module ${i}` }], toolCallId: `c${i}`, toolName: "Edit", isError: false, timestamp: i });
590
+ out.push({
591
+ role: "user",
592
+ content: `actually we decided to use approach ${i} for module ${i}`,
593
+ timestamp: i,
594
+ });
595
+ out.push({
596
+ role: "assistant",
597
+ content: [{ type: "toolCall", name: "Edit", id: `c${i}`, arguments: {} }],
598
+ api: "anthropic-messages",
599
+ provider: "anthropic",
600
+ model: "m",
601
+ usage: {
602
+ inputTokens: 1,
603
+ outputTokens: 1,
604
+ cacheReadTokens: 0,
605
+ cacheWriteTokens: 0,
606
+ },
607
+ stopReason: "tool_use",
608
+ timestamp: i,
609
+ });
610
+ out.push({
611
+ role: "toolResult",
612
+ content: [{ type: "text", text: `edited module ${i}` }],
613
+ toolCallId: `c${i}`,
614
+ toolName: "Edit",
615
+ isError: false,
616
+ timestamp: i,
617
+ });
421
618
  }
422
619
  return out;
423
620
  }
@@ -428,7 +625,13 @@ test("S24: high pressure triggers a memory review on compaction", async () => {
428
625
  process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "false";
429
626
  try {
430
627
  const messages = decisionSession();
431
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
628
+ const ctx = h.ctx({
629
+ getContextUsage: () => ({
630
+ tokens: 200000,
631
+ contextWindow: 200000,
632
+ percent: 100,
633
+ }),
634
+ });
432
635
  await h.fire("context", { type: "context", messages }, ctx);
433
636
  // review-on-compact runs as a fire-and-forget async (doCompact is sync), so
434
637
  // let the microtask/macrotask queue drain before asserting the side effect.
@@ -450,7 +653,13 @@ test("S24: /mega-status reports the live pressure band + %", async () => {
450
653
  // Populate the runtime's live context first (a context event sets
451
654
  // lastCtxTokens/lastCtxPercent), then read /mega-status. At 100% usage the live
452
655
  // band must read "mega" and pressure must report 100%.
453
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
656
+ const ctx = h.ctx({
657
+ getContextUsage: () => ({
658
+ tokens: 200000,
659
+ contextWindow: 200000,
660
+ percent: 100,
661
+ }),
662
+ });
454
663
  await h.fire("context", { type: "context", messages: h.session }, ctx);
455
664
  await h.commands["mega-status"].handler("", ctx);
456
665
  assert.ok(h.notifies.some((n) => n.includes("tier=mega (live)")), "live band reported as mega at 100% pressure");
@@ -489,7 +698,16 @@ test("/dashboard skips server spawn when already running", async () => {
489
698
  const { createServer } = await import("node:http");
490
699
  const server = createServer((_req, res) => {
491
700
  res.writeHead(200, { "Content-Type": "application/json" });
492
- res.end(JSON.stringify({ updatedAt: new Date().toISOString(), tier: "test", version: 1, config: {}, session: {}, context: {}, trigger: {}, store: {} }));
701
+ res.end(JSON.stringify({
702
+ updatedAt: new Date().toISOString(),
703
+ tier: "test",
704
+ version: 1,
705
+ config: {},
706
+ session: {},
707
+ context: {},
708
+ trigger: {},
709
+ store: {},
710
+ }));
493
711
  });
494
712
  await new Promise((r) => server.listen(livPort, "127.0.0.1", r));
495
713
  const { join: j } = await import("node:path");
@@ -498,9 +716,14 @@ test("/dashboard skips server spawn when already running", async () => {
498
716
  const ctx = h.ctx({
499
717
  ui: {
500
718
  setStatus: () => { },
501
- notify: (s) => { h.notifies.push(s); },
719
+ notify: (s) => {
720
+ h.notifies.push(s);
721
+ },
502
722
  select: () => { },
503
- confirm: async () => { confirms.push(true); return true; },
723
+ confirm: async () => {
724
+ confirms.push(true);
725
+ return true;
726
+ },
504
727
  input: async () => "",
505
728
  },
506
729
  });
@@ -533,7 +756,13 @@ test("/dashboard-status reports running after dashboard start", async () => {
533
756
  });
534
757
  test("state snapshot writes dashboard.json after compaction", async () => {
535
758
  const h = harness();
536
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
759
+ const ctx = h.ctx({
760
+ getContextUsage: () => ({
761
+ tokens: 200000,
762
+ contextWindow: 200000,
763
+ percent: 100,
764
+ }),
765
+ });
537
766
  // Fire auto-trigger compaction (context event above 80% threshold)
538
767
  await h.fire("context", { type: "context", messages: h.session }, ctx);
539
768
  const { existsSync: ex, readFileSync: rf } = await import("node:fs");
@@ -554,7 +783,13 @@ test("state snapshot writes dashboard.json after compaction", async () => {
554
783
  });
555
784
  test("events.log receives compaction events", async () => {
556
785
  const h = harness();
557
- const ctx = h.ctx({ getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }) });
786
+ const ctx = h.ctx({
787
+ getContextUsage: () => ({
788
+ tokens: 200000,
789
+ contextWindow: 200000,
790
+ percent: 100,
791
+ }),
792
+ });
558
793
  // Fire auto-trigger compaction twice (first fires compaction, second also fires)
559
794
  await h.fire("context", { type: "context", messages: h.session }, ctx);
560
795
  const { readFileSync: rf, existsSync: ex } = await import("node:fs");
@@ -21,6 +21,20 @@ export const COMPACT_TIERS = {
21
21
  ultra: 1_000_000,
22
22
  mega: 10_000_000,
23
23
  };
24
+ /**
25
+ * Compaction thresholds as a FRACTION of the model's context window (NOT a
26
+ * static token amount). The live + durable trim fire at tier% of the window,
27
+ * so they always fire BELOW pi's native auto-compaction (~80% of window) for
28
+ * any model size (200k or 1M). `custom` (explicit MEGACOMPACT_THRESHOLD_TOKENS)
29
+ * is NOT scaled by this map — it stays an absolute token count.
30
+ */
31
+ export const TIER_PCT = {
32
+ low: 0.5,
33
+ medium: 0.6,
34
+ high: 0.7,
35
+ ultra: 0.7,
36
+ mega: 0.75,
37
+ };
24
38
  function envFlag(name, fallback) {
25
39
  const v = process.env[name];
26
40
  if (v == null || v === "")
@@ -34,17 +48,64 @@ function envBool(name, fallback) {
34
48
  return fallback;
35
49
  return v === "true" || v === "1";
36
50
  }
37
- /** Resolve the effective token threshold from TIER (or explicit) env vars. */
51
+ /**
52
+ * Resolve the effective token threshold from TIER (or explicit) env vars.
53
+ *
54
+ * For a named tier the returned `thresholdTokens` is a BOOT FALLBACK
55
+ * (`round(tierPct * 200_000)`) — sane before any context event reaches the
56
+ * runtime. The true fire point is computed per-window at runtime via
57
+ * `effectiveThresholdTokens(...)`. `custom` (explicit MEGACOMPACT_THRESHOLD_TOKENS)
58
+ * keeps `tierPct: null` and an ABSOLUTE `thresholdTokens` (never percent-scaled).
59
+ */
38
60
  function resolveThreshold() {
39
61
  const explicit = process.env.MEGACOMPACT_THRESHOLD_TOKENS;
40
62
  if (explicit != null && explicit !== "") {
41
63
  const n = Number(explicit);
42
64
  if (Number.isFinite(n))
43
- return { tier: "custom", thresholdTokens: n };
65
+ return { tier: "custom", tierPct: null, thresholdTokens: n };
44
66
  }
45
67
  const raw = (process.env.MEGACOMPACT_TIER ?? "low").toLowerCase();
46
68
  const tier = (raw in COMPACT_TIERS ? raw : "low");
47
- return { tier, thresholdTokens: COMPACT_TIERS[tier] };
69
+ const tierPct = TIER_PCT[tier];
70
+ // Boot fallback: sane gate before the first context event provides a window.
71
+ const thresholdTokens = Math.round(tierPct * 200_000);
72
+ return { tier, tierPct, thresholdTokens };
73
+ }
74
+ /**
75
+ * Pure helper: the real compaction fire point, given the model context window.
76
+ *
77
+ * custom -> explicitThreshold (ABSOLUTE, never percent-scaled)
78
+ * tiered+window>0 -> round(tierPct * window)
79
+ * tiered+window<=0 -> fallbackThreshold (boot fallback; no window known yet)
80
+ *
81
+ * This is the single source of truth consumed by the runtime gates
82
+ * (FAST GATE / autoCompactCheck / agent_end durable trigger) and the
83
+ * pressure/armed/ready computations. Keeping it pure makes it trivially
84
+ * unit-testable without the pi runtime.
85
+ */
86
+ export function effectiveThresholdTokens(opts) {
87
+ if (opts.tierPct == null) {
88
+ // custom: absolute threshold, never percent-scaled
89
+ return opts.explicitThreshold ?? opts.fallbackThreshold;
90
+ }
91
+ if (opts.window > 0)
92
+ return Math.round(opts.tierPct * opts.window);
93
+ return opts.fallbackThreshold;
94
+ }
95
+ /**
96
+ * Resolve the optional manual arming-floor override (MEGACOMPACT_FAST_GATE_PCT).
97
+ * Kept for backward-compat: when unset, the default arming floor equals the
98
+ * tier's percent threshold (tierPct*100) so the dashboard stays consistent;
99
+ * `custom` (tierPct null) falls back to the legacy 70% default.
100
+ */
101
+ function resolveFastGatePct(tierPct) {
102
+ const raw = process.env.MEGACOMPACT_FAST_GATE_PCT;
103
+ if (raw != null && raw !== "") {
104
+ const n = Number(raw);
105
+ if (Number.isFinite(n))
106
+ return n;
107
+ }
108
+ return tierPct != null ? Math.round(tierPct * 100) : 70;
48
109
  }
49
110
  /**
50
111
  * Pressure helpers for adaptive compression live in src/config.ts (pi-agnostic)
@@ -56,13 +117,14 @@ function resolveThreshold() {
56
117
  export { pressureFromPct, preserveRecentForPressure, pressureRatio, pressureBand, memoryReviewCadence, } from "../src/config.js";
57
118
  /** Build the resolved config from env + defaults. */
58
119
  export function loadConfig() {
59
- const { tier, thresholdTokens } = resolveThreshold();
120
+ const { tier, tierPct, thresholdTokens } = resolveThreshold();
60
121
  return {
61
122
  tier,
123
+ tierPct,
62
124
  // Global default; the live store/dashboard are rebound per-repo at runtime
63
125
  // via MegaRuntime.bindRepo() so each git repo gets its own isolated state dir.
64
126
  stateDir: process.env.MEGACOMPACT_STATE_DIR ?? STATE_DIR_DEFAULT,
65
- fastGatePct: envFlag("MEGACOMPACT_FAST_GATE_PCT", 70),
127
+ fastGatePct: resolveFastGatePct(tierPct),
66
128
  thresholdTokens,
67
129
  anchorUserMessages: envFlag("MEGACOMPACT_ANCHOR_USER_MESSAGES", 3),
68
130
  preserveRecent: envFlag("MEGACOMPACT_PRESERVE_RECENT", 4),