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.
- package/README.md +10 -8
- package/dist/extensions/dashboard-server.js +17 -6
- package/dist/extensions/mega-commands.js +12 -1
- package/dist/extensions/mega-compact.test.js +286 -51
- package/dist/extensions/mega-config.js +67 -5
- package/dist/extensions/mega-events.js +151 -27
- package/dist/extensions/mega-runtime.js +163 -32
- package/dist/extensions/openclaw-mega-compact.js +291 -0
- package/dist/src/dedup-engine.test.js +63 -38
- package/dist/src/minilm.js +92 -0
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-server.ts +17 -6
- package/extensions/mega-commands.ts +12 -1
- package/extensions/mega-compact.test.ts +947 -516
- package/extensions/mega-config.ts +84 -6
- package/extensions/mega-dashboard.ts +11 -0
- package/extensions/mega-events.ts +558 -360
- package/extensions/mega-runtime.ts +168 -32
- package/package.json +1 -1
- package/src/dedup-engine.test.ts +103 -42
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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) => {
|
|
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: () => ({
|
|
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) => {
|
|
135
|
-
|
|
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,
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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",
|
|
288
|
-
|
|
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: {
|
|
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" &&
|
|
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
|
|
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
|
|
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: {
|
|
398
|
+
preparation: {
|
|
399
|
+
firstKeptEntryId: "e0",
|
|
400
|
+
messagesToSummarize: [],
|
|
401
|
+
tokensBefore: 0,
|
|
402
|
+
},
|
|
317
403
|
signal: undefined,
|
|
318
404
|
}, h.ctx());
|
|
319
|
-
assert.
|
|
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({
|
|
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", {
|
|
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", {
|
|
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({
|
|
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({
|
|
344
|
-
|
|
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: {
|
|
353
|
-
|
|
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: {
|
|
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({
|
|
372
|
-
|
|
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:") &&
|
|
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",
|
|
381
|
-
["medium",
|
|
382
|
-
["high",
|
|
383
|
-
["ultra",
|
|
384
|
-
["mega",
|
|
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 (
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
419
|
-
|
|
420
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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) => {
|
|
719
|
+
notify: (s) => {
|
|
720
|
+
h.notifies.push(s);
|
|
721
|
+
},
|
|
502
722
|
select: () => { },
|
|
503
|
-
confirm: async () => {
|
|
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({
|
|
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({
|
|
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
|
-
/**
|
|
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
|
-
|
|
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:
|
|
127
|
+
fastGatePct: resolveFastGatePct(tierPct),
|
|
66
128
|
thresholdTokens,
|
|
67
129
|
anchorUserMessages: envFlag("MEGACOMPACT_ANCHOR_USER_MESSAGES", 3),
|
|
68
130
|
preserveRecent: envFlag("MEGACOMPACT_PRESERVE_RECENT", 4),
|