pi-mega-compact 0.6.7 → 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 +112 -14
- 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/compact.js +5 -1
- package/dist/src/dedup-engine.test.js +63 -38
- package/dist/src/minilm.js +92 -0
- package/dist/src/supersede.js +8 -5
- package/dist/src/wordpiece.js +129 -0
- package/extensions/dashboard-server.ts +125 -14
- 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/compact.ts +5 -1
- package/src/dedup-engine.test.ts +103 -42
- package/src/supersede.ts +8 -5
|
@@ -30,422 +30,743 @@ let counter = 0;
|
|
|
30
30
|
|
|
31
31
|
/** Build a mock pi + ctx and load the extension into them. */
|
|
32
32
|
function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } = {}) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
33
|
+
const stateDir = join(baseTmp, `run-${counter++}`);
|
|
34
|
+
process.env.MEGACOMPACT_STATE_DIR = stateDir;
|
|
35
|
+
process.env.MEGACOMPACT_DEBUG = "true";
|
|
36
|
+
// Low threshold so the auto-trigger gate trips on our small mock context.
|
|
37
|
+
// Tier tests opt out (keepTier/keepThreshold) so they can drive the real
|
|
38
|
+
// tier resolution instead of the forced 50-token threshold.
|
|
39
|
+
if (!opts.keepThreshold) process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
|
|
40
|
+
if (!opts.keepTier) delete process.env.MEGACOMPACT_TIER;
|
|
41
|
+
process.env.MEGACOMPACT_FAST_GATE_PCT = "1";
|
|
42
|
+
|
|
43
|
+
const handlers: Record<string, Function> = {};
|
|
44
|
+
const commands: Record<
|
|
45
|
+
string,
|
|
46
|
+
{ handler: (a: string, c: any) => Promise<void> }
|
|
47
|
+
> = {};
|
|
48
|
+
const appended: any[] = [];
|
|
49
|
+
let statusKey: string | undefined;
|
|
50
|
+
let statusText: string | undefined;
|
|
51
|
+
const notifies: string[] = [];
|
|
52
|
+
const compactCalls: any[] = [];
|
|
53
|
+
|
|
54
|
+
// Minimal AgentMessage factory for the session we project into the extension.
|
|
55
|
+
function msg(role: string, text: string, toolName?: string): AgentMessage {
|
|
56
|
+
if (role === "assistant" && toolName) {
|
|
57
|
+
return {
|
|
58
|
+
role: "assistant",
|
|
59
|
+
content: [
|
|
60
|
+
{ type: "toolCall", name: toolName, id: "c1", arguments: {} },
|
|
61
|
+
],
|
|
62
|
+
api: "anthropic-messages",
|
|
63
|
+
provider: "anthropic",
|
|
64
|
+
model: "m",
|
|
65
|
+
usage: {
|
|
66
|
+
inputTokens: 1,
|
|
67
|
+
outputTokens: 1,
|
|
68
|
+
cacheReadTokens: 0,
|
|
69
|
+
cacheWriteTokens: 0,
|
|
70
|
+
},
|
|
71
|
+
stopReason: "tool_use",
|
|
72
|
+
timestamp: 0,
|
|
73
|
+
} as unknown as AgentMessage;
|
|
74
|
+
}
|
|
75
|
+
if (role === "toolResult" && toolName) {
|
|
76
|
+
return {
|
|
77
|
+
role: "toolResult",
|
|
78
|
+
toolCallId: "c1",
|
|
79
|
+
toolName,
|
|
80
|
+
content: [{ type: "text", text }],
|
|
81
|
+
isError: false,
|
|
82
|
+
timestamp: 0,
|
|
83
|
+
} as unknown as AgentMessage;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
role: "user",
|
|
87
|
+
content: text,
|
|
88
|
+
timestamp: 0,
|
|
89
|
+
} as unknown as AgentMessage;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const session: AgentMessage[] = [
|
|
93
|
+
msg("user", "read src/vec.ts and understand the index"),
|
|
94
|
+
msg("assistant", "ok", "Read"),
|
|
95
|
+
msg("user", "edit src/vec.ts to add a cosine helper"),
|
|
96
|
+
msg("assistant", "ok", "Edit"),
|
|
97
|
+
msg("user", "now fix the dedupe bug in store.ts"),
|
|
98
|
+
msg("assistant", "ok", "Edit"),
|
|
99
|
+
msg("user", "actually we should add recall sorting too"),
|
|
100
|
+
msg("assistant", "ok", "Edit"),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Mirror the REAL SessionManager: getEntries() returns SessionEntry objects,
|
|
104
|
+
// which the extension projects to messages via the SDK's
|
|
105
|
+
// sessionEntryToContextMessages(entry). The harness must use the same shape
|
|
106
|
+
// (type:"message" with a .message) or recentUserQuery() silently queries "".
|
|
107
|
+
const toEntry = (m: AgentMessage, i: number): any => ({
|
|
108
|
+
type: "message",
|
|
109
|
+
id: `e${i}`,
|
|
110
|
+
parentId: null,
|
|
111
|
+
timestamp: String(i),
|
|
112
|
+
message: m,
|
|
113
|
+
});
|
|
114
|
+
const sessionManager = {
|
|
115
|
+
getSessionId: () => "sess_ext_001",
|
|
116
|
+
getEntries: () => session.map(toEntry),
|
|
117
|
+
// Faithful mock: getBranch() returns the current branch's entries, which
|
|
118
|
+
// piCompactWouldNoop() reads to predict whether ctx.compact() would no-op.
|
|
119
|
+
getBranch: () => session.map(toEntry),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function makeCtx(over: Partial<any> = {}) {
|
|
123
|
+
return {
|
|
124
|
+
ui: {
|
|
125
|
+
setStatus: (k: string, t: string | undefined) => {
|
|
126
|
+
statusKey = k;
|
|
127
|
+
statusText = t;
|
|
128
|
+
},
|
|
129
|
+
notify: (s: string) => notifies.push(s),
|
|
130
|
+
select: () => {},
|
|
131
|
+
confirm: async () => true,
|
|
132
|
+
input: async () => "",
|
|
133
|
+
setWidget: () => {},
|
|
134
|
+
},
|
|
135
|
+
mode: "tui" as any,
|
|
136
|
+
hasUI: true,
|
|
137
|
+
cwd: stateDir,
|
|
138
|
+
sessionManager,
|
|
139
|
+
modelRegistry: {} as any,
|
|
140
|
+
model: undefined,
|
|
141
|
+
isIdle: () => true,
|
|
142
|
+
isProjectTrusted: () => true,
|
|
143
|
+
signal: undefined,
|
|
144
|
+
abort: () => {},
|
|
145
|
+
hasPendingMessages: () => false,
|
|
146
|
+
shutdown: () => {},
|
|
147
|
+
getContextUsage: () => ({
|
|
148
|
+
tokens: 200000,
|
|
149
|
+
contextWindow: 200000,
|
|
150
|
+
percent: 100,
|
|
151
|
+
}),
|
|
152
|
+
// Faithful mock: ctx.compact() starts pi's flow, which fires the
|
|
153
|
+
// session_before_compact handler (where WE supply the durable trim).
|
|
154
|
+
compact: (opts?: any) => {
|
|
155
|
+
compactCalls.push(opts);
|
|
156
|
+
if (handlers["session_before_compact"]) {
|
|
157
|
+
return handlers["session_before_compact"](
|
|
158
|
+
{
|
|
159
|
+
type: "session_before_compact",
|
|
160
|
+
reason: "threshold",
|
|
161
|
+
willRetry: false,
|
|
162
|
+
signal: undefined,
|
|
163
|
+
// pi computed the cut honoring anchor floor + tool-pair (PREVENT-PI-002);
|
|
164
|
+
// our handler reuses it as firstKeptEntryId.
|
|
165
|
+
preparation: {
|
|
166
|
+
firstKeptEntryId: "e2",
|
|
167
|
+
messagesToSummarize: session.slice(0, 2),
|
|
168
|
+
tokensBefore: 500,
|
|
169
|
+
},
|
|
170
|
+
} as any,
|
|
171
|
+
makeCtx(),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
},
|
|
176
|
+
getSystemPrompt: () => "system base",
|
|
177
|
+
...over,
|
|
178
|
+
} as any;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pi = {
|
|
182
|
+
on: (ev: string, h: Function) => {
|
|
183
|
+
handlers[ev] = h;
|
|
184
|
+
},
|
|
185
|
+
registerCommand: (name: string, opts: any) => {
|
|
186
|
+
commands[name] = opts;
|
|
187
|
+
},
|
|
188
|
+
registerTool: () => {},
|
|
189
|
+
registerShortcut: () => {},
|
|
190
|
+
registerFlag: () => {},
|
|
191
|
+
getFlag: () => undefined,
|
|
192
|
+
registerMessageRenderer: () => {},
|
|
193
|
+
registerEntryRenderer: () => {},
|
|
194
|
+
sendMessage: (_m: any) => {},
|
|
195
|
+
sendUserMessage: () => {},
|
|
196
|
+
appendEntry: (t: string, d: any) => appended.push({ t, d }),
|
|
197
|
+
setSessionName: () => {},
|
|
198
|
+
getSessionName: () => undefined,
|
|
199
|
+
setLabel: () => {},
|
|
200
|
+
exec: async () => ({ stdout: "", stderr: "", code: 0 }),
|
|
201
|
+
getActiveTools: () => [],
|
|
202
|
+
getAllTools: () => [],
|
|
203
|
+
setActiveTools: () => {},
|
|
204
|
+
getCommands: () => [],
|
|
205
|
+
setModel: async () => false,
|
|
206
|
+
getThinkingLevel: () => "off" as any,
|
|
207
|
+
setThinkingLevel: () => {},
|
|
208
|
+
} as any;
|
|
209
|
+
|
|
210
|
+
// Import the compiled extension (same dist/extensions dir as this test).
|
|
211
|
+
const mod = require("./mega-compact.js") as { default: (p: any) => void };
|
|
212
|
+
mod.default(pi);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
stateDir,
|
|
216
|
+
handlers,
|
|
217
|
+
commands,
|
|
218
|
+
appended,
|
|
219
|
+
get status() {
|
|
220
|
+
return { statusKey, statusText };
|
|
221
|
+
},
|
|
222
|
+
notifies,
|
|
223
|
+
compactCalls,
|
|
224
|
+
fire: (ev: string, event: any, ctx: any) => handlers[ev](event, ctx),
|
|
225
|
+
ctx: makeCtx,
|
|
226
|
+
session,
|
|
227
|
+
};
|
|
179
228
|
}
|
|
180
229
|
|
|
181
230
|
test("auto-trigger (legacy): past threshold persists a chkpt and starts a durable trim via ctx.compact", async () => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
const h = harness();
|
|
232
|
+
const messages = h.session;
|
|
233
|
+
// The mock session is tiny (~100 tokens). piCompactWouldNoop() would skip
|
|
234
|
+
// ctx.compact() for a transcript under pi's keepRecentTokens budget — so
|
|
235
|
+
// lower the floor to 0 to simulate a transcript large enough that pi WOULD
|
|
236
|
+
// compact (the positive path this test exercises).
|
|
237
|
+
// S16: this is the LEGACY path — the default no longer calls ctx.compact()
|
|
238
|
+
// (it returns a live-trimmed view instead). Set the legacy flag to exercise
|
|
239
|
+
// the v0.4.28 ctx.compact durable-trim flow this test asserts.
|
|
240
|
+
process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR = "0";
|
|
241
|
+
process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
|
|
242
|
+
try {
|
|
243
|
+
const ctx = h.ctx({
|
|
244
|
+
getContextUsage: () => ({
|
|
245
|
+
tokens: 200000,
|
|
246
|
+
contextWindow: 200000,
|
|
247
|
+
percent: 100,
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
const res = await h.fire("context", { type: "context", messages }, ctx);
|
|
251
|
+
// L1->L4 ran: a checkpoint was persisted to the SQLite store + a marker entry written.
|
|
252
|
+
const { listCheckpoints } = await import("../src/store/sqlite.js");
|
|
253
|
+
assert.ok(
|
|
254
|
+
listCheckpoints("sess_ext_001", h.stateDir).length > 0,
|
|
255
|
+
"checkpoint persisted to local vector db",
|
|
256
|
+
);
|
|
257
|
+
assert.equal(
|
|
258
|
+
h.appended.some((a) => a.t === "mega-compact-marker"),
|
|
259
|
+
true,
|
|
260
|
+
"marker sentinel appended",
|
|
261
|
+
);
|
|
262
|
+
// The legacy context handler triggers pi's compaction flow (ctx.compact),
|
|
263
|
+
// which calls our session_before_compact handler to supply the DURABLE trim.
|
|
264
|
+
assert.equal(
|
|
265
|
+
res,
|
|
266
|
+
undefined,
|
|
267
|
+
"legacy context handler returns nothing (no local drop)",
|
|
268
|
+
);
|
|
269
|
+
assert.equal(
|
|
270
|
+
h.compactCalls.length,
|
|
271
|
+
1,
|
|
272
|
+
"ctx.compact() called to start durable trim (legacy path)",
|
|
273
|
+
);
|
|
274
|
+
// The durable trim was supplied (summary + firstKeptEntryId from pi's prep).
|
|
275
|
+
assert.ok(h.compactCalls[0] !== undefined, "compaction flow executed");
|
|
276
|
+
} finally {
|
|
277
|
+
delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
|
|
278
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
279
|
+
}
|
|
210
280
|
});
|
|
211
281
|
|
|
212
282
|
test("auto-trigger: skips ctx.compact() when pi would no-op (session too small, legacy path)", async () => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
283
|
+
const h = harness();
|
|
284
|
+
const messages = h.session;
|
|
285
|
+
// Default floor (20000): the tiny mock transcript is below pi's
|
|
286
|
+
// keepRecentTokens budget, so piCompactWouldNoop() must skip ctx.compact()
|
|
287
|
+
// rather than surface pi's "Nothing to compact (session too small)" throw.
|
|
288
|
+
// S16: exercised under the legacy flag (the default path never calls ctx.compact).
|
|
289
|
+
delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
|
|
290
|
+
process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "true";
|
|
291
|
+
try {
|
|
292
|
+
const ctx = h.ctx({
|
|
293
|
+
getContextUsage: () => ({
|
|
294
|
+
tokens: 200000,
|
|
295
|
+
contextWindow: 200000,
|
|
296
|
+
percent: 100,
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const res = await h.fire("context", { type: "context", messages }, ctx);
|
|
300
|
+
assert.equal(
|
|
301
|
+
res,
|
|
302
|
+
undefined,
|
|
303
|
+
"legacy context handler returns nothing (no local drop)",
|
|
304
|
+
);
|
|
305
|
+
assert.equal(
|
|
306
|
+
h.compactCalls.length,
|
|
307
|
+
0,
|
|
308
|
+
"ctx.compact() NOT called — pi would no-op",
|
|
309
|
+
);
|
|
310
|
+
// Our recall checkpoint still persisted (Path A) — the durable trim is the
|
|
311
|
+
// only thing skipped; recall is independent of it.
|
|
312
|
+
const { listCheckpoints } = await import("../src/store/sqlite.js");
|
|
313
|
+
assert.ok(
|
|
314
|
+
listCheckpoints("sess_ext_001", h.stateDir).length > 0,
|
|
315
|
+
"recall checkpoint still persisted",
|
|
316
|
+
);
|
|
317
|
+
assert.equal(
|
|
318
|
+
h.appended.some((a) => a.t === "mega-compact-marker"),
|
|
319
|
+
true,
|
|
320
|
+
"marker sentinel still appended",
|
|
321
|
+
);
|
|
322
|
+
} finally {
|
|
323
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
324
|
+
}
|
|
234
325
|
});
|
|
235
326
|
|
|
236
327
|
test("auto-trigger (S16): trims the live view and does NOT call ctx.compact()", async () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
328
|
+
const h = harness();
|
|
329
|
+
const messages = h.session;
|
|
330
|
+
// S16 default: live context-event trim. No legacy flag. Lower the anchor floor
|
|
331
|
+
// so the trimmed recent window (4 messages, 2 user) clears the anchor check
|
|
332
|
+
// and the live trim actually fires — mirrors how the legacy test lowers the
|
|
333
|
+
// durable floor to exercise its positive path.
|
|
334
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
335
|
+
delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
|
|
336
|
+
process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
|
|
337
|
+
try {
|
|
338
|
+
const ctx = h.ctx({
|
|
339
|
+
getContextUsage: () => ({
|
|
340
|
+
tokens: 200000,
|
|
341
|
+
contextWindow: 200000,
|
|
342
|
+
percent: 100,
|
|
343
|
+
}),
|
|
344
|
+
});
|
|
345
|
+
const res = await h.fire("context", { type: "context", messages }, ctx);
|
|
346
|
+
// S16: context handler returns a TRIMMED messages array (live trim), not undefined.
|
|
347
|
+
assert.ok(
|
|
348
|
+
res && typeof res === "object",
|
|
349
|
+
"context handler returns a result object (live trim)",
|
|
350
|
+
);
|
|
351
|
+
assert.ok(
|
|
352
|
+
Array.isArray((res as any).messages),
|
|
353
|
+
"result has a trimmed messages array",
|
|
354
|
+
);
|
|
355
|
+
// The trimmed view starts with the compacted summary (user-role) + is shorter.
|
|
356
|
+
assert.ok(
|
|
357
|
+
(res as any).messages.length < messages.length,
|
|
358
|
+
"trimmed view is shorter than the full session",
|
|
359
|
+
);
|
|
360
|
+
// S16: ctx.compact() is NEVER called (it would stop the agent).
|
|
361
|
+
assert.equal(
|
|
362
|
+
h.compactCalls.length,
|
|
363
|
+
0,
|
|
364
|
+
"ctx.compact() NOT called — compact-and-continue",
|
|
365
|
+
);
|
|
366
|
+
// The recall checkpoint is still persisted (the durable value).
|
|
367
|
+
const { listCheckpoints } = await import("../src/store/sqlite.js");
|
|
368
|
+
assert.ok(
|
|
369
|
+
listCheckpoints("sess_ext_001", h.stateDir).length > 0,
|
|
370
|
+
"recall checkpoint persisted under live trim",
|
|
371
|
+
);
|
|
372
|
+
} finally {
|
|
373
|
+
delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
374
|
+
}
|
|
262
375
|
});
|
|
263
376
|
|
|
264
377
|
test("auto-trigger (S16): does not trim when below the anchor floor (returns undefined, no ctx.compact)", async () => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
378
|
+
const h = harness();
|
|
379
|
+
// A session so short that buildLiveTrimmedView's anchor floor can't hold — the
|
|
380
|
+
// live trim skips this call (returns undefined, the next context event retries).
|
|
381
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
382
|
+
delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
|
|
383
|
+
const shortSession = [h.session[0], h.session[1]]; // one user + one assistant
|
|
384
|
+
const ctx = h.ctx({
|
|
385
|
+
getContextUsage: () => ({
|
|
386
|
+
tokens: 200000,
|
|
387
|
+
contextWindow: 200000,
|
|
388
|
+
percent: 100,
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
const res = await h.fire(
|
|
392
|
+
"context",
|
|
393
|
+
{ type: "context", messages: shortSession },
|
|
394
|
+
ctx,
|
|
395
|
+
);
|
|
396
|
+
// Either it skipped (undefined) or trimmed safely — but it must never call ctx.compact.
|
|
397
|
+
assert.equal(
|
|
398
|
+
h.compactCalls.length,
|
|
399
|
+
0,
|
|
400
|
+
"ctx.compact() NOT called under live trim (short session)",
|
|
401
|
+
);
|
|
402
|
+
if (res === undefined) {
|
|
403
|
+
// skipped path is fine
|
|
404
|
+
assert.ok(
|
|
405
|
+
true,
|
|
406
|
+
"below anchor floor → no trim this call (retries next event)",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
279
409
|
});
|
|
280
410
|
|
|
281
411
|
test("auto-trigger (S16): sendUserMessage resume nudge fires only when idle + queued + not already nudged", async () => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
412
|
+
const h = harness();
|
|
413
|
+
// No queued messages → the nudge must NOT fire (the guard prevents busy-loops).
|
|
414
|
+
// We assert the extension did not throw and did not push a spurious resume.
|
|
415
|
+
const ctx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false });
|
|
416
|
+
await h.fire("agent_end", { type: "agent_end", messages: [] }, ctx);
|
|
417
|
+
// No throw + no spurious nudge side-effect is the contract; appended stays
|
|
418
|
+
// free of any auto "continue" marker when there is no queued work.
|
|
419
|
+
assert.equal(
|
|
420
|
+
h.appended.some((a) => a.t && /continue/i.test(String(a.d ?? ""))),
|
|
421
|
+
false,
|
|
422
|
+
"no spurious continue when no queued work",
|
|
423
|
+
);
|
|
290
424
|
});
|
|
291
425
|
|
|
292
426
|
test("auto-trigger (S16): durable trim still happens via pi native auto-compaction (session_before_compact)", async () => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
427
|
+
const h = harness();
|
|
428
|
+
// pi's native auto-compaction fires at agent-end with reason "threshold" (the
|
|
429
|
+
// CONTINUING path). Our session_before_compact handler must still supply the
|
|
430
|
+
// durable trim summary — independent of the live context-event trim.
|
|
431
|
+
const prep = {
|
|
432
|
+
firstKeptEntryId: "e2",
|
|
433
|
+
messagesToSummarize: h.session.slice(0, 4),
|
|
434
|
+
tokensBefore: 500,
|
|
435
|
+
};
|
|
436
|
+
const res = await h.fire(
|
|
437
|
+
"session_before_compact",
|
|
438
|
+
{
|
|
439
|
+
type: "session_before_compact",
|
|
440
|
+
reason: "threshold",
|
|
441
|
+
willRetry: false,
|
|
442
|
+
signal: undefined,
|
|
443
|
+
preparation: prep,
|
|
444
|
+
} as any,
|
|
445
|
+
h.ctx(),
|
|
446
|
+
);
|
|
447
|
+
assert.ok(
|
|
448
|
+
res?.compaction,
|
|
449
|
+
"we supply a durable compaction result to pi's native path",
|
|
450
|
+
);
|
|
451
|
+
assert.ok(
|
|
452
|
+
res.compaction.firstKeptEntryId === "e2",
|
|
453
|
+
"reuses pi's boundary (PREVENT-PI-002)",
|
|
454
|
+
);
|
|
455
|
+
assert.ok(res.compaction.summary.length > 0, "summary is non-empty");
|
|
309
456
|
});
|
|
310
457
|
|
|
311
458
|
test("session_before_compact supplies our durable trim (not pi's summary)", async () => {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
459
|
+
const h = harness();
|
|
460
|
+
// pi fires session_before_compact with its own computed preparation.
|
|
461
|
+
const res = await h.fire(
|
|
462
|
+
"session_before_compact",
|
|
463
|
+
{
|
|
464
|
+
type: "session_before_compact",
|
|
465
|
+
reason: "overflow",
|
|
466
|
+
willRetry: true,
|
|
467
|
+
preparation: {
|
|
468
|
+
firstKeptEntryId: "e2",
|
|
469
|
+
messagesToSummarize: h.session.slice(0, 2),
|
|
470
|
+
tokensBefore: 500,
|
|
471
|
+
},
|
|
472
|
+
signal: undefined,
|
|
473
|
+
} as any,
|
|
474
|
+
h.ctx(),
|
|
475
|
+
);
|
|
476
|
+
assert.ok(res && res.compaction, "returns a compaction result");
|
|
477
|
+
assert.equal(
|
|
478
|
+
res.compaction.firstKeptEntryId,
|
|
479
|
+
"e2",
|
|
480
|
+
"reuses pi's cut boundary (PREVENT-PI-002 safe)",
|
|
481
|
+
);
|
|
482
|
+
assert.ok(
|
|
483
|
+
typeof res.compaction.summary === "string" &&
|
|
484
|
+
res.compaction.summary.length > 0,
|
|
485
|
+
"our summary supplied",
|
|
486
|
+
);
|
|
487
|
+
assert.ok(res.compaction.tokensBefore >= 0, "tokensBefore reported");
|
|
329
488
|
});
|
|
330
489
|
|
|
331
|
-
test("session_before_compact
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
490
|
+
test("session_before_compact supplies a fallback summary when nothing to summarize", async () => {
|
|
491
|
+
const h = harness();
|
|
492
|
+
// Empty preparation → no messages to summarize (anchor floor protects
|
|
493
|
+
// everything). We MUST still supply a compaction (never {}), otherwise pi
|
|
494
|
+
// runs its own compact() which throws "Nothing to compact (session too
|
|
495
|
+
// small)" and leaves the session stuck with no resume context. The fallback
|
|
496
|
+
// records a minimal resume summary so the session always resumes.
|
|
497
|
+
const res = await h.fire(
|
|
498
|
+
"session_before_compact",
|
|
499
|
+
{
|
|
500
|
+
type: "session_before_compact",
|
|
501
|
+
reason: "threshold",
|
|
502
|
+
willRetry: false,
|
|
503
|
+
preparation: {
|
|
504
|
+
firstKeptEntryId: "e0",
|
|
505
|
+
messagesToSummarize: [],
|
|
506
|
+
tokensBefore: 0,
|
|
507
|
+
},
|
|
508
|
+
signal: undefined,
|
|
509
|
+
} as any,
|
|
510
|
+
h.ctx(),
|
|
511
|
+
);
|
|
512
|
+
assert.ok(
|
|
513
|
+
res && (res as any).compaction,
|
|
514
|
+
"fallback compaction supplied (never {})",
|
|
515
|
+
);
|
|
516
|
+
assert.ok(
|
|
517
|
+
(res as any).compaction.summary.includes("context compacted"),
|
|
518
|
+
"fallback summary injected so the session resumes",
|
|
519
|
+
);
|
|
520
|
+
assert.equal(
|
|
521
|
+
(res as any).compaction.firstKeptEntryId,
|
|
522
|
+
"e0",
|
|
523
|
+
"keeps pi's cut point",
|
|
524
|
+
);
|
|
346
525
|
});
|
|
347
526
|
|
|
348
527
|
test("resume auto-inline stages recall into the system prompt", async () => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
528
|
+
const h = harness();
|
|
529
|
+
// Seed a checkpoint first (simulate a prior session that compacted).
|
|
530
|
+
await h.fire(
|
|
531
|
+
"context",
|
|
532
|
+
{ type: "context", messages: h.session },
|
|
533
|
+
h.ctx({
|
|
534
|
+
getContextUsage: () => ({
|
|
535
|
+
tokens: 200000,
|
|
536
|
+
contextWindow: 200000,
|
|
537
|
+
percent: 100,
|
|
538
|
+
}),
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
541
|
+
// Fresh resume: session_start with reason "resume".
|
|
542
|
+
const ctx = h.ctx();
|
|
543
|
+
await h.fire(
|
|
544
|
+
"session_start",
|
|
545
|
+
{
|
|
546
|
+
type: "session_start",
|
|
547
|
+
reason: "resume",
|
|
548
|
+
previousSessionFile: undefined,
|
|
549
|
+
} as any,
|
|
550
|
+
ctx,
|
|
551
|
+
);
|
|
552
|
+
// The next before_agent_start must prepend the recalled block.
|
|
553
|
+
const res = await h.fire(
|
|
554
|
+
"before_agent_start",
|
|
555
|
+
{
|
|
556
|
+
type: "before_agent_start",
|
|
557
|
+
prompt: "base system",
|
|
558
|
+
images: undefined,
|
|
559
|
+
systemPrompt: "base system",
|
|
560
|
+
systemPromptOptions: {},
|
|
561
|
+
} as any,
|
|
562
|
+
ctx,
|
|
563
|
+
);
|
|
564
|
+
assert.ok(
|
|
565
|
+
res && typeof res.systemPrompt === "string",
|
|
566
|
+
"before_agent_start returns a systemPrompt",
|
|
567
|
+
);
|
|
568
|
+
assert.ok(
|
|
569
|
+
res.systemPrompt.includes("Recalled context"),
|
|
570
|
+
"recalled block injected into system prompt",
|
|
571
|
+
);
|
|
359
572
|
});
|
|
360
573
|
|
|
361
574
|
test("/recall-context reports and stages the top checkpoint", async () => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
575
|
+
const h = harness();
|
|
576
|
+
await h.fire(
|
|
577
|
+
"context",
|
|
578
|
+
{ type: "context", messages: h.session },
|
|
579
|
+
h.ctx({
|
|
580
|
+
getContextUsage: () => ({
|
|
581
|
+
tokens: 200000,
|
|
582
|
+
contextWindow: 200000,
|
|
583
|
+
percent: 100,
|
|
584
|
+
}),
|
|
585
|
+
}),
|
|
586
|
+
);
|
|
587
|
+
const ctx = h.ctx();
|
|
588
|
+
await h.commands["mega-recall"].handler("dedupe bug store.ts", ctx);
|
|
589
|
+
assert.ok(
|
|
590
|
+
h.notifies.some((n) => n.includes("recall staged")),
|
|
591
|
+
"command reports staged checkpoints",
|
|
592
|
+
);
|
|
593
|
+
assert.ok(
|
|
594
|
+
h.notifies.some((n) => n.includes("chkpt_")),
|
|
595
|
+
"command names the checkpoint",
|
|
596
|
+
);
|
|
368
597
|
});
|
|
369
598
|
|
|
370
599
|
test("/megacompact-status reports live store stats", async () => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
600
|
+
const h = harness();
|
|
601
|
+
await h.fire(
|
|
602
|
+
"context",
|
|
603
|
+
{ type: "context", messages: h.session },
|
|
604
|
+
h.ctx({
|
|
605
|
+
getContextUsage: () => ({
|
|
606
|
+
tokens: 200000,
|
|
607
|
+
contextWindow: 200000,
|
|
608
|
+
percent: 100,
|
|
609
|
+
}),
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
const ctx = h.ctx({
|
|
613
|
+
getContextUsage: () => ({
|
|
614
|
+
tokens: 50000,
|
|
615
|
+
contextWindow: 200000,
|
|
616
|
+
percent: 25,
|
|
617
|
+
}),
|
|
618
|
+
});
|
|
619
|
+
await h.commands["mega-status"].handler("", ctx);
|
|
620
|
+
assert.ok(
|
|
621
|
+
h.notifies.some((n) => n.includes("store:") && n.includes("chkpt")),
|
|
622
|
+
"status shows checkpoint count",
|
|
623
|
+
);
|
|
376
624
|
});
|
|
377
625
|
|
|
378
626
|
// ---- Model/provider capture (Phase 5b model_snapshots) ----------------------
|
|
379
627
|
test("model_select captures model + provider into SQL", async () => {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
628
|
+
const h = harness();
|
|
629
|
+
const modelCtx = h.ctx({
|
|
630
|
+
model: {
|
|
631
|
+
id: "claude-opus-4-8",
|
|
632
|
+
name: "Claude Opus 4.8",
|
|
633
|
+
provider: "anthropic",
|
|
634
|
+
contextWindow: 200000,
|
|
635
|
+
maxTokens: 32000,
|
|
636
|
+
reasoning: false,
|
|
637
|
+
cost: { input: 0.000015, output: 0.000075 },
|
|
638
|
+
},
|
|
639
|
+
modelRegistry: {
|
|
640
|
+
getProviderDisplayName: (p: string) =>
|
|
641
|
+
p === "anthropic" ? "Anthropic" : p,
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
await h.fire("model_select", {}, modelCtx);
|
|
645
|
+
const { latestModelSnapshot } = await import("../src/store/sqlite.js");
|
|
646
|
+
const snap = latestModelSnapshot(h.stateDir);
|
|
647
|
+
assert.ok(snap, "model_snapshots row persisted");
|
|
648
|
+
assert.equal(snap!.modelId, "claude-opus-4-8", "correct model id captured");
|
|
649
|
+
assert.equal(snap!.provider, "anthropic", "correct provider captured");
|
|
650
|
+
assert.equal(
|
|
651
|
+
snap!.providerName,
|
|
652
|
+
"Anthropic",
|
|
653
|
+
"provider display name resolved",
|
|
654
|
+
);
|
|
655
|
+
assert.equal(snap!.inputRate, 0.000015, "input rate captured");
|
|
393
656
|
});
|
|
394
657
|
|
|
395
658
|
test("/mega-status surfaces the captured model + provider", async () => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
659
|
+
const h = harness();
|
|
660
|
+
const modelCtx = h.ctx({
|
|
661
|
+
model: {
|
|
662
|
+
id: "claude-opus-4-8",
|
|
663
|
+
name: "Claude Opus 4.8",
|
|
664
|
+
provider: "anthropic",
|
|
665
|
+
contextWindow: 200000,
|
|
666
|
+
maxTokens: 32000,
|
|
667
|
+
reasoning: false,
|
|
668
|
+
cost: { input: 0.000015, output: 0.000075 },
|
|
669
|
+
},
|
|
670
|
+
modelRegistry: { getProviderDisplayName: () => "Anthropic" },
|
|
671
|
+
});
|
|
672
|
+
await h.fire("model_select", {}, modelCtx);
|
|
673
|
+
await h.fire(
|
|
674
|
+
"context",
|
|
675
|
+
{ type: "context", messages: h.session },
|
|
676
|
+
h.ctx({
|
|
677
|
+
getContextUsage: () => ({
|
|
678
|
+
tokens: 200000,
|
|
679
|
+
contextWindow: 200000,
|
|
680
|
+
percent: 100,
|
|
681
|
+
}),
|
|
682
|
+
}),
|
|
683
|
+
);
|
|
684
|
+
const ctx = h.ctx({
|
|
685
|
+
getContextUsage: () => ({
|
|
686
|
+
tokens: 50000,
|
|
687
|
+
contextWindow: 200000,
|
|
688
|
+
percent: 25,
|
|
689
|
+
}),
|
|
690
|
+
});
|
|
691
|
+
await h.commands["mega-status"].handler("", ctx);
|
|
692
|
+
assert.ok(
|
|
693
|
+
h.notifies.some(
|
|
694
|
+
(n) =>
|
|
695
|
+
n.includes("🤖 model:") &&
|
|
696
|
+
n.includes("Claude Opus 4.8") &&
|
|
697
|
+
n.includes("Anthropic"),
|
|
698
|
+
),
|
|
699
|
+
"status surfaces captured model + provider",
|
|
700
|
+
);
|
|
406
701
|
});
|
|
407
702
|
|
|
408
703
|
// ---- Named compaction tiers -------------------------------------------------
|
|
409
704
|
// low=50k, medium=100k, high=200k, ultra=1M, mega=10M. Driven through the REAL
|
|
410
705
|
// loadConfig()/status path by setting MEGACOMPACT_TIER before loading the ext.
|
|
706
|
+
// Percentage-based thresholds: tierPct × the model context window. The harness
|
|
707
|
+
// getContextUsage below reports contextWindow=2_000_000, so each tier resolves to
|
|
708
|
+
// tierPct × 2_000_000 — which fires BELOW pi's native ~80% auto-compaction for
|
|
709
|
+
// ANY model size (200k or 1M). Driven through the REAL loadConfig()/status path
|
|
710
|
+
// by setting MEGACOMPACT_TIER before loading the ext.
|
|
411
711
|
const TIER_CASES: Array<[string, number]> = [
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
712
|
+
["low", 1_000_000], // 0.50 × 2_000_000
|
|
713
|
+
["medium", 1_200_000], // 0.60 × 2_000_000
|
|
714
|
+
["high", 1_400_000], // 0.70 × 2_000_000
|
|
715
|
+
["ultra", 1_400_000], // 0.70 × 2_000_000
|
|
716
|
+
["mega", 1_500_000], // 0.75 × 2_000_000
|
|
417
717
|
];
|
|
418
718
|
for (const [tier, threshold] of TIER_CASES) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
719
|
+
test(`tier "${tier}" resolves to a ${threshold.toLocaleString()}-token threshold (tierPct × 2M window; live band shown separately)`, async () => {
|
|
720
|
+
// Keep tier + keep threshold UNSET so the tier (not an explicit number)
|
|
721
|
+
// drives the threshold. harness() would otherwise reset the threshold.
|
|
722
|
+
delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
|
|
723
|
+
process.env.MEGACOMPACT_TIER = tier;
|
|
724
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
725
|
+
// tokens=1 against a 2M window → near-zero pressure → live band "low".
|
|
726
|
+
const ctx = h.ctx({
|
|
727
|
+
getContextUsage: () => ({
|
|
728
|
+
tokens: 1,
|
|
729
|
+
contextWindow: 2_000_000,
|
|
730
|
+
percent: 0.01,
|
|
731
|
+
}),
|
|
732
|
+
});
|
|
733
|
+
await h.commands["mega-status"].handler("", ctx);
|
|
734
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
735
|
+
// /mega-status renders threshold with toLocaleString() (thousands commas).
|
|
736
|
+
assert.ok(
|
|
737
|
+
h.notifies.some(
|
|
738
|
+
(n) =>
|
|
739
|
+
n.includes(`preset=${tier}`) && n.includes(`threshold=${threshold.toLocaleString()}`),
|
|
740
|
+
),
|
|
741
|
+
`status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`,
|
|
742
|
+
);
|
|
743
|
+
// S24: the headline tier is the LIVE pressure band, shown as "tier=low (live)".
|
|
744
|
+
assert.ok(
|
|
745
|
+
h.notifies.some((n) => n.includes("tier=low (live)")),
|
|
746
|
+
"live band reported (low at near-zero pressure)",
|
|
747
|
+
);
|
|
748
|
+
});
|
|
436
749
|
}
|
|
437
750
|
|
|
438
751
|
test("explicit MEGACOMPACT_THRESHOLD_TOKENS overrides the tier", async () => {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
752
|
+
process.env.MEGACOMPACT_TIER = "mega";
|
|
753
|
+
process.env.MEGACOMPACT_THRESHOLD_TOKENS = "777";
|
|
754
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
755
|
+
const ctx = h.ctx({
|
|
756
|
+
getContextUsage: () => ({
|
|
757
|
+
tokens: 1,
|
|
758
|
+
contextWindow: 2_000_000,
|
|
759
|
+
percent: 0.01,
|
|
760
|
+
}),
|
|
761
|
+
});
|
|
762
|
+
await h.commands["mega-status"].handler("", ctx);
|
|
763
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
764
|
+
assert.ok(
|
|
765
|
+
h.notifies.some(
|
|
766
|
+
(n) => n.includes("preset=custom") && n.includes("threshold=777"),
|
|
767
|
+
),
|
|
768
|
+
"explicit threshold wins over tier (preset=custom)",
|
|
769
|
+
);
|
|
449
770
|
});
|
|
450
771
|
|
|
451
772
|
// ---- S24: memory review tied to pressure / compaction -----------------------
|
|
@@ -453,182 +774,292 @@ test("explicit MEGACOMPACT_THRESHOLD_TOKENS overrides the tier", async () => {
|
|
|
453
774
|
// non-deduped) compaction. Each user turn contains a decision phrase
|
|
454
775
|
// (/\bactually\b/i, /\bwe (?:use|decided)\b/i) so reviewConversation yields ops.
|
|
455
776
|
function decisionSession(): AgentMessage[] {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
777
|
+
const out: AgentMessage[] = [];
|
|
778
|
+
for (let i = 0; i < 14; i++) {
|
|
779
|
+
out.push({
|
|
780
|
+
role: "user",
|
|
781
|
+
content: `actually we decided to use approach ${i} for module ${i}`,
|
|
782
|
+
timestamp: i,
|
|
783
|
+
} as unknown as AgentMessage);
|
|
784
|
+
out.push({
|
|
785
|
+
role: "assistant",
|
|
786
|
+
content: [{ type: "toolCall", name: "Edit", id: `c${i}`, arguments: {} }],
|
|
787
|
+
api: "anthropic-messages",
|
|
788
|
+
provider: "anthropic",
|
|
789
|
+
model: "m",
|
|
790
|
+
usage: {
|
|
791
|
+
inputTokens: 1,
|
|
792
|
+
outputTokens: 1,
|
|
793
|
+
cacheReadTokens: 0,
|
|
794
|
+
cacheWriteTokens: 0,
|
|
795
|
+
},
|
|
796
|
+
stopReason: "tool_use",
|
|
797
|
+
timestamp: i,
|
|
798
|
+
} as unknown as AgentMessage);
|
|
799
|
+
out.push({
|
|
800
|
+
role: "toolResult",
|
|
801
|
+
content: [{ type: "text", text: `edited module ${i}` }],
|
|
802
|
+
toolCallId: `c${i}`,
|
|
803
|
+
toolName: "Edit",
|
|
804
|
+
isError: false,
|
|
805
|
+
timestamp: i,
|
|
806
|
+
} as unknown as AgentMessage);
|
|
807
|
+
}
|
|
808
|
+
return out;
|
|
463
809
|
}
|
|
464
810
|
|
|
465
811
|
test("S24: high pressure triggers a memory review on compaction", async () => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
812
|
+
const h = harness();
|
|
813
|
+
// Force a real (non-legacy) compaction at full pressure → pressureBand "mega",
|
|
814
|
+
// which must fire the shared runMemoryReview on compact (review-on-compact).
|
|
815
|
+
process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM = "false";
|
|
816
|
+
try {
|
|
817
|
+
const messages = decisionSession();
|
|
818
|
+
const ctx = h.ctx({
|
|
819
|
+
getContextUsage: () => ({
|
|
820
|
+
tokens: 200000,
|
|
821
|
+
contextWindow: 200000,
|
|
822
|
+
percent: 100,
|
|
823
|
+
}),
|
|
824
|
+
});
|
|
825
|
+
await h.fire("context", { type: "context", messages }, ctx);
|
|
826
|
+
// review-on-compact runs as a fire-and-forget async (doCompact is sync), so
|
|
827
|
+
// let the microtask/macrotask queue drain before asserting the side effect.
|
|
828
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
829
|
+
const { listMemories, listCheckpoints } = await import(
|
|
830
|
+
"../src/store/sqlite.js"
|
|
831
|
+
);
|
|
832
|
+
// A checkpoint must have been persisted (proves compaction ran, not skipped).
|
|
833
|
+
assert.ok(
|
|
834
|
+
listCheckpoints("sess_ext_001", h.stateDir).length > 0,
|
|
835
|
+
"checkpoint persisted to local vector db",
|
|
836
|
+
);
|
|
837
|
+
// The just-compacted region is worth remembering, so durable memories must
|
|
838
|
+
// have been written to the SQLite store (review-on-compact path).
|
|
839
|
+
const mem = listMemories(null, 50, h.stateDir);
|
|
840
|
+
assert.ok(
|
|
841
|
+
mem.length > 0,
|
|
842
|
+
"memory review wrote durable memories on compact",
|
|
843
|
+
);
|
|
844
|
+
} finally {
|
|
845
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
846
|
+
}
|
|
487
847
|
});
|
|
488
848
|
|
|
489
849
|
test("S24: /mega-status reports the live pressure band + %", async () => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
850
|
+
const h = harness();
|
|
851
|
+
// Populate the runtime's live context first (a context event sets
|
|
852
|
+
// lastCtxTokens/lastCtxPercent), then read /mega-status. At 100% usage the live
|
|
853
|
+
// band must read "mega" and pressure must report 100%.
|
|
854
|
+
const ctx = h.ctx({
|
|
855
|
+
getContextUsage: () => ({
|
|
856
|
+
tokens: 200000,
|
|
857
|
+
contextWindow: 200000,
|
|
858
|
+
percent: 100,
|
|
859
|
+
}),
|
|
860
|
+
});
|
|
861
|
+
await h.fire("context", { type: "context", messages: h.session }, ctx);
|
|
862
|
+
await h.commands["mega-status"].handler("", ctx);
|
|
863
|
+
assert.ok(
|
|
864
|
+
h.notifies.some((n) => n.includes("tier=mega (live)")),
|
|
865
|
+
"live band reported as mega at 100% pressure",
|
|
866
|
+
);
|
|
867
|
+
assert.ok(
|
|
868
|
+
h.notifies.some((n) => n.includes("pressure=100%")),
|
|
869
|
+
"live pressure % reported",
|
|
870
|
+
);
|
|
499
871
|
});
|
|
500
872
|
|
|
501
873
|
// ---- /dashboard commands ----------------------------------------------------
|
|
502
874
|
test("/dashboard-status reports no server when pid file missing", async () => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
875
|
+
// Private base so this asserts "no server" on a range nothing else uses,
|
|
876
|
+
// not the machine-global 9320 family (which may hold a leftover/production server).
|
|
877
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = "49320";
|
|
878
|
+
try {
|
|
879
|
+
const h = harness();
|
|
880
|
+
const ctx = h.ctx();
|
|
881
|
+
await h.commands["mega-dashboard-status"].handler("", ctx);
|
|
882
|
+
assert.ok(
|
|
883
|
+
h.notifies.some((n) => n.includes("not running")),
|
|
884
|
+
"reports no server running",
|
|
885
|
+
);
|
|
886
|
+
} finally {
|
|
887
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
888
|
+
}
|
|
514
889
|
});
|
|
515
890
|
|
|
516
891
|
test("/dashboard-stop reports no server when pid file missing", async () => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
892
|
+
const h = harness();
|
|
893
|
+
const ctx = h.ctx();
|
|
894
|
+
await h.commands["mega-dashboard-stop"].handler("", ctx);
|
|
895
|
+
assert.ok(
|
|
896
|
+
h.notifies.some((n) => n.includes("no dashboard server running")),
|
|
897
|
+
"reports no server",
|
|
898
|
+
);
|
|
521
899
|
});
|
|
522
900
|
|
|
523
901
|
test("/dashboard skips server spawn when already running", async () => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
902
|
+
// Use a private dashboard port base for THIS test's harness + fake server so
|
|
903
|
+
// it never races the (parallel, hard-coded-9320) dashboard-server.test.js or
|
|
904
|
+
// a leftover production server. Set BEFORE harness() so registerDashboardCommands
|
|
905
|
+
// reads our base for findLivePort().
|
|
906
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = "29320";
|
|
907
|
+
const h = harness();
|
|
908
|
+
const confirms: boolean[] = [];
|
|
909
|
+
const livPort = 29320; // inside the harness's private scan range (29320–29329)
|
|
910
|
+
const { createServer } = await import("node:http");
|
|
911
|
+
const server = createServer((_req, res) => {
|
|
912
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
913
|
+
res.end(
|
|
914
|
+
JSON.stringify({
|
|
915
|
+
updatedAt: new Date().toISOString(),
|
|
916
|
+
tier: "test",
|
|
917
|
+
version: 1,
|
|
918
|
+
config: {},
|
|
919
|
+
session: {},
|
|
920
|
+
context: {},
|
|
921
|
+
trigger: {},
|
|
922
|
+
store: {},
|
|
923
|
+
}),
|
|
924
|
+
);
|
|
925
|
+
});
|
|
926
|
+
await new Promise<void>((r) => server.listen(livPort, "127.0.0.1", r));
|
|
927
|
+
const { join: j } = await import("node:path");
|
|
928
|
+
const { writeFileSync: wf } = await import("node:fs");
|
|
929
|
+
wf(
|
|
930
|
+
j(h.stateDir, "port.pid"),
|
|
931
|
+
JSON.stringify({ port: livPort, pid: process.pid }),
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
const ctx = h.ctx({
|
|
935
|
+
ui: {
|
|
936
|
+
setStatus: () => {},
|
|
937
|
+
notify: (s: string) => {
|
|
938
|
+
h.notifies.push(s);
|
|
939
|
+
},
|
|
940
|
+
select: () => {},
|
|
941
|
+
confirm: async () => {
|
|
942
|
+
confirms.push(true);
|
|
943
|
+
return true;
|
|
944
|
+
},
|
|
945
|
+
input: async () => "",
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
await h.commands["mega-dashboard"].handler("", ctx);
|
|
950
|
+
assert.ok(
|
|
951
|
+
h.notifies.some((n) => n.includes("already running")),
|
|
952
|
+
"reports already running",
|
|
953
|
+
);
|
|
954
|
+
assert.ok(confirms.length > 0, "confirm dialog was shown");
|
|
955
|
+
|
|
956
|
+
await new Promise<void>((r) => server.close(() => r()));
|
|
957
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
558
958
|
});
|
|
559
959
|
|
|
560
960
|
test("/dashboard-status reports running after dashboard start", async () => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
961
|
+
// Private dashboard port base for this harness — never collides with the
|
|
962
|
+
// parallel dashboard-server.test.js (9320 family) or a leftover server.
|
|
963
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = "39320";
|
|
964
|
+
const h = harness();
|
|
965
|
+
const livPort = 39320;
|
|
966
|
+
const { createServer } = await import("node:http");
|
|
967
|
+
const { join: j } = await import("node:path");
|
|
968
|
+
const { writeFileSync: wf } = await import("node:fs");
|
|
969
|
+
const server = createServer((_req, res) => {
|
|
970
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
971
|
+
res.end(
|
|
972
|
+
JSON.stringify({ updatedAt: new Date().toISOString(), tier: "test" }),
|
|
973
|
+
);
|
|
974
|
+
});
|
|
975
|
+
await new Promise<void>((r) => server.listen(livPort, "127.0.0.1", r));
|
|
976
|
+
wf(
|
|
977
|
+
j(h.stateDir, "port.pid"),
|
|
978
|
+
JSON.stringify({ port: livPort, pid: process.pid }),
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const ctx = h.ctx();
|
|
982
|
+
await h.commands["mega-dashboard-status"].handler("", ctx);
|
|
983
|
+
assert.ok(
|
|
984
|
+
h.notifies.some(
|
|
985
|
+
(n) => n.includes("running") && n.includes(String(livPort)),
|
|
986
|
+
),
|
|
987
|
+
"reports running with port",
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
await new Promise<void>((r) => server.close(() => r()));
|
|
991
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
582
992
|
});
|
|
583
993
|
|
|
584
994
|
test("state snapshot writes dashboard.json after compaction", async () => {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
995
|
+
const h = harness();
|
|
996
|
+
const ctx = h.ctx({
|
|
997
|
+
getContextUsage: () => ({
|
|
998
|
+
tokens: 200000,
|
|
999
|
+
contextWindow: 200000,
|
|
1000
|
+
percent: 100,
|
|
1001
|
+
}),
|
|
1002
|
+
});
|
|
1003
|
+
// Fire auto-trigger compaction (context event above 80% threshold)
|
|
1004
|
+
await h.fire("context", { type: "context", messages: h.session }, ctx);
|
|
1005
|
+
const { existsSync: ex, readFileSync: rf } = await import("node:fs");
|
|
1006
|
+
const { join: j } = await import("node:path");
|
|
1007
|
+
const snapPath = j(h.stateDir, "dashboard.json");
|
|
1008
|
+
assert.ok(ex(snapPath), "dashboard.json written after compaction");
|
|
1009
|
+
const snap = JSON.parse(rf(snapPath, "utf-8"));
|
|
1010
|
+
// Item B: the honest token model is wired — the original dropped region was
|
|
1011
|
+
// captured (originalTokens > 0), and the saved amount never exceeds the
|
|
1012
|
+
// original (saved = max(0, original − stored) ≤ original). For this tiny
|
|
1013
|
+
// harness session the summary can be ≥ the region, so saved may be 0; the
|
|
1014
|
+
// positive "saved > 0" case with a large region is covered by the
|
|
1015
|
+
// vectorStore unit tests.
|
|
1016
|
+
assert.ok(
|
|
1017
|
+
snap.store.originalTokens > 0,
|
|
1018
|
+
"snapshot.store.originalTokens captured after compaction",
|
|
1019
|
+
);
|
|
1020
|
+
assert.ok(
|
|
1021
|
+
snap.store.originalTokens >= snap.store.tokensSaved,
|
|
1022
|
+
"model invariant: original region >= tokens saved",
|
|
1023
|
+
);
|
|
1024
|
+
// Item A: crew (live agent) block is present in the dashboard snapshot.
|
|
1025
|
+
assert.ok(
|
|
1026
|
+
snap.crew && typeof snap.crew.activeAgents === "number",
|
|
1027
|
+
"snapshot.crew.activeAgents present",
|
|
1028
|
+
);
|
|
607
1029
|
});
|
|
608
1030
|
|
|
609
1031
|
test("events.log receives compaction events", async () => {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1032
|
+
const h = harness();
|
|
1033
|
+
const ctx = h.ctx({
|
|
1034
|
+
getContextUsage: () => ({
|
|
1035
|
+
tokens: 200000,
|
|
1036
|
+
contextWindow: 200000,
|
|
1037
|
+
percent: 100,
|
|
1038
|
+
}),
|
|
1039
|
+
});
|
|
1040
|
+
// Fire auto-trigger compaction twice (first fires compaction, second also fires)
|
|
1041
|
+
await h.fire("context", { type: "context", messages: h.session }, ctx);
|
|
1042
|
+
const { readFileSync: rf, existsSync: ex } = await import("node:fs");
|
|
1043
|
+
const { join: j } = await import("node:path");
|
|
1044
|
+
const logPath = j(h.stateDir, "events.log");
|
|
1045
|
+
if (ex(logPath)) {
|
|
1046
|
+
const content = rf(logPath, "utf-8").trim();
|
|
1047
|
+
// At minimum, we expect at least one event logged
|
|
1048
|
+
assert.ok(content.length > 0, "events.log is non-empty after compaction");
|
|
1049
|
+
} else {
|
|
1050
|
+
// events.log may not exist if the DashboardEmitter path differs from stateDir;
|
|
1051
|
+
// verify dashboard.json was written (proves the post-compact path executed)
|
|
1052
|
+
assert.ok(
|
|
1053
|
+
ex(j(h.stateDir, "dashboard.json")),
|
|
1054
|
+
"dashboard.json proves post-compact ran",
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
626
1057
|
});
|
|
627
1058
|
|
|
628
1059
|
test("cleanup", async () => {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1060
|
+
// Terminate the global PGlite cross-repo index (WASM worker thread) so the
|
|
1061
|
+
// test process can exit. Without this, node --test never returns even though
|
|
1062
|
+
// every test passed — the leaked worker keeps the event loop alive.
|
|
1063
|
+
await closeVectorIndex();
|
|
1064
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
634
1065
|
});
|