pi-mega-compact 0.7.7 → 0.7.9
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 +11 -12
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/html.js +756 -0
- package/dist/extensions/dashboard-server/index-reader.js +133 -0
- package/dist/extensions/dashboard-server/server.js +370 -0
- package/dist/extensions/dashboard-server/snapshot.js +43 -0
- package/dist/extensions/dashboard-server/state.js +30 -0
- package/dist/extensions/dashboard-server/types.js +5 -0
- package/dist/extensions/dashboard-server.js +7 -1315
- package/dist/extensions/mega-commands.js +162 -134
- package/dist/extensions/mega-compact.test.js +292 -24
- package/dist/extensions/mega-config.js +10 -0
- package/dist/extensions/mega-conflict-cmds.js +5 -1
- package/dist/extensions/mega-dashboard-cmds.js +29 -22
- package/dist/extensions/mega-db-cmds.js +11 -2
- package/dist/extensions/mega-events/agent-handlers.js +173 -0
- package/dist/extensions/mega-events/compact-handlers.js +133 -0
- package/dist/extensions/mega-events/context-handler.js +249 -0
- package/dist/extensions/mega-events/register.js +21 -0
- package/dist/extensions/mega-events/session-handlers.js +142 -0
- package/dist/extensions/mega-events.js +15 -652
- package/dist/extensions/mega-pipeline/compact.js +324 -0
- package/dist/extensions/mega-pipeline/memory-review.js +38 -0
- package/dist/extensions/mega-pipeline/recall.js +147 -0
- package/dist/extensions/mega-pipeline.js +9 -480
- package/dist/extensions/mega-runtime/helpers.js +40 -0
- package/dist/extensions/mega-runtime/query.js +29 -0
- package/dist/extensions/mega-runtime/state.js +711 -0
- package/dist/extensions/mega-runtime/widget.js +197 -0
- package/dist/extensions/mega-runtime.js +15 -932
- package/dist/src/store/sqlite/checkpoints.js +145 -0
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/dedup-mirror.js +64 -0
- package/dist/src/store/sqlite/foundation.js +38 -0
- package/dist/src/store/sqlite/global-index.js +224 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/maintenance.js +235 -0
- package/dist/src/store/sqlite/memories.js +164 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/meta.js +82 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/model-snapshots.js +47 -0
- package/dist/src/store/sqlite/raptor.js +57 -0
- package/dist/src/store/sqlite/raw-transcript.js +134 -0
- package/dist/src/store/sqlite/schema.js +250 -0
- package/dist/src/store/sqlite/session-state.js +28 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/stats.js +66 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/store/sqlite/utils.js +120 -0
- package/dist/src/store/sqlite.js +20 -1607
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-server/html.ts +758 -0
- package/extensions/dashboard-server/index-reader.ts +130 -0
- package/extensions/dashboard-server/server.ts +358 -0
- package/extensions/dashboard-server/snapshot.ts +44 -0
- package/extensions/dashboard-server/state.ts +33 -0
- package/extensions/dashboard-server/types.ts +134 -0
- package/extensions/dashboard-server.ts +7 -1431
- package/extensions/mega-commands.ts +33 -10
- package/extensions/mega-compact.test.ts +453 -37
- package/extensions/mega-config.ts +22 -0
- package/extensions/mega-conflict-cmds.ts +6 -2
- package/extensions/mega-dashboard-cmds.ts +30 -23
- package/extensions/mega-db-cmds.ts +11 -3
- package/extensions/mega-events/agent-handlers.ts +214 -0
- package/extensions/mega-events/compact-handlers.ts +164 -0
- package/extensions/mega-events/context-handler.ts +290 -0
- package/extensions/mega-events/register.ts +37 -0
- package/extensions/mega-events/session-handlers.ts +165 -0
- package/extensions/mega-events.ts +15 -732
- package/extensions/mega-pipeline/compact.ts +366 -0
- package/extensions/mega-pipeline/memory-review.ts +46 -0
- package/extensions/mega-pipeline/recall.ts +165 -0
- package/extensions/mega-pipeline.ts +9 -537
- package/extensions/mega-runtime/helpers.ts +68 -0
- package/extensions/mega-runtime/query.ts +29 -0
- package/extensions/mega-runtime/state.ts +797 -0
- package/extensions/mega-runtime/widget.ts +258 -0
- package/extensions/mega-runtime.ts +15 -1076
- package/package.json +4 -3
- package/src/store/sqlite/checkpoints.ts +204 -0
- package/src/store/sqlite/dedup-mirror.ts +114 -0
- package/src/store/sqlite/foundation.ts +63 -0
- package/src/store/sqlite/global-index.ts +305 -0
- package/src/store/sqlite/maintenance.ts +294 -0
- package/src/store/sqlite/memories.ts +217 -0
- package/src/store/sqlite/meta.ts +108 -0
- package/src/store/sqlite/model-snapshots.ts +83 -0
- package/src/store/sqlite/raptor.ts +107 -0
- package/src/store/sqlite/raw-transcript.ts +221 -0
- package/src/store/sqlite/schema.ts +258 -0
- package/src/store/sqlite/session-state.ts +38 -0
- package/src/store/sqlite/stats.ts +127 -0
- package/src/store/sqlite/utils.ts +125 -0
- package/src/store/sqlite.ts +20 -2204
|
@@ -11,10 +11,10 @@ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { normalizeSessionId } from "../src/store.js";
|
|
12
12
|
import { listCheckpoints, latestModelSnapshot, countInjectedGlobal, listRepoRegistry } from "../src/store/sqlite.js";
|
|
13
13
|
import { decompressSmart } from "../src/store/compression.js";
|
|
14
|
-
import { loadMetrics, fpRate, p95 } from "../src/monitoring.js";
|
|
15
|
-
import { MegaRuntime, C, recentUserQuery } from "./mega-runtime.js";
|
|
14
|
+
import { loadMetrics, fpRate, p95, defaultMetricsPath } from "../src/monitoring.js";
|
|
15
|
+
import { type MegaRuntime, C, recentUserQuery } from "./mega-runtime.js";
|
|
16
16
|
import { runCompact, doRecall, doRecallAsync } from "./mega-pipeline.js";
|
|
17
|
-
import {
|
|
17
|
+
import type { MegaConfig } from "./mega-config.js";
|
|
18
18
|
|
|
19
19
|
/** Resolve a checkpoint by id (or "recent"/"last") from this session's store. */
|
|
20
20
|
export function findCheckpoint(runtime: MegaRuntime, sid: string, ref: string) {
|
|
@@ -29,9 +29,10 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
29
29
|
pi.registerCommand("mega-compact", {
|
|
30
30
|
description: "Compress current session context into the local vector store.",
|
|
31
31
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
32
|
+
try {
|
|
32
33
|
const sessionEntries = ctx.sessionManager.getEntries();
|
|
33
34
|
// Project entries (branch-aware) into the message view.
|
|
34
|
-
const messages
|
|
35
|
+
const messages = sessionEntries.flatMap((e) => sessionEntryToContextMessages(e));
|
|
35
36
|
const summaryArg = args.trim();
|
|
36
37
|
const ran = runCompact(pi, runtime, config, ctx, messages, summaryArg ? { summary: summaryArg } : {});
|
|
37
38
|
if ("skipped" in ran && ran.skipped) {
|
|
@@ -43,16 +44,20 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
43
44
|
`[mega-compact] ${r.deduped ? "region already compacted (deduped)" : `persisted ${r.checkpointId}`} · ` +
|
|
44
45
|
`${r.tokenEstimate} tok · ${runtime.currentStateDir}`,
|
|
45
46
|
);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
ctx.ui.notify(`[mega-compact] /mega-compact failed: ${String(e)}`);
|
|
49
|
+
}
|
|
46
50
|
},
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
pi.registerCommand("mega-recall", {
|
|
50
54
|
description: "Recall relevant compacted context from the vector store and inline it. Use --cross-repo to search all repos.",
|
|
51
55
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
56
|
+
try {
|
|
52
57
|
// S17: --cross-repo (or --cross repo) runs the async path over every repo's
|
|
53
58
|
// PGlite HNSW index (stricter cosine floor + source labels).
|
|
54
|
-
const crossRepo =
|
|
55
|
-
const query = args.replace(/--cross[
|
|
59
|
+
const crossRepo = /--cross[- ]repo\b/.test(args);
|
|
60
|
+
const query = args.replace(/--cross[- ]repo\b/, "").trim() || recentUserQuery(ctx);
|
|
56
61
|
if (!query) {
|
|
57
62
|
ctx.ui.notify("[mega-compact] /mega-recall needs a query or a prior user message.");
|
|
58
63
|
return;
|
|
@@ -75,12 +80,16 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
75
80
|
`[mega-compact] recall staged ${r.toInject.length} checkpoint(s) for "${query}"${crossRepo ? " (cross-repo)" : ""}:\n${list}\n` +
|
|
76
81
|
`(injected at the next turn via system prompt)`,
|
|
77
82
|
);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
ctx.ui.notify(`[mega-compact] /mega-recall failed: ${String(e)}`);
|
|
85
|
+
}
|
|
78
86
|
},
|
|
79
87
|
});
|
|
80
88
|
|
|
81
89
|
pi.registerCommand("mega-status", {
|
|
82
90
|
description: "Show mega-compact config, context usage, and the data-safety invariant.",
|
|
83
91
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
92
|
+
try {
|
|
84
93
|
runtime.bindRepo(ctx.cwd);
|
|
85
94
|
const usage = ctx.getContextUsage();
|
|
86
95
|
const pct = usage?.percent != null ? `${usage.percent}%` : "n/a";
|
|
@@ -98,7 +107,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
98
107
|
// windows extended (how much "extra" conversation the freed space buys).
|
|
99
108
|
const model = latestModelSnapshot(runtime.currentStateDir);
|
|
100
109
|
const rate = model?.inputRate ?? 0;
|
|
101
|
-
const usd = (repo.tokensSaved * rate).toFixed(4);
|
|
110
|
+
const usd = ((repo.tokensSaved ?? 0) * rate).toFixed(4);
|
|
102
111
|
const ctxWindow = usage?.contextWindow ?? 0;
|
|
103
112
|
const daysExtended = ctxWindow > 0 && repo.tokensSaved > 0
|
|
104
113
|
? (repo.tokensSaved / ctxWindow).toFixed(1)
|
|
@@ -107,11 +116,14 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
107
116
|
// Shows the human model name + provider so the user knows WHICH model's
|
|
108
117
|
// pricing drives the cost figure. Falls back when none captured yet.
|
|
109
118
|
const modelStr = model
|
|
110
|
-
? `${model.modelName ?? model.modelId} · ${model.providerName ?? model.provider}`
|
|
119
|
+
? `${model.modelName ?? model.modelId ?? "?"} · ${model.providerName ?? model.provider ?? "?"}`
|
|
111
120
|
: "unknown (no model captured)";
|
|
112
121
|
const costStr = `≈ $${usd} saved · ${daysExtended} context-windows extended`;
|
|
113
122
|
// Recall-quality badge (Phase 4): trust score from monitoring metrics.
|
|
114
|
-
|
|
123
|
+
// H1 fix: loadMetrics expects a *file* path (dashboard.json), not the
|
|
124
|
+
// state dir — passing the dir made existsSync() true (dirs exist) then
|
|
125
|
+
// readFileSync() threw EISDIR, silently caught → metrics always zero.
|
|
126
|
+
const m = loadMetrics(defaultMetricsPath(runtime.currentStateDir));
|
|
115
127
|
const fp = fpRate(m, "L2");
|
|
116
128
|
const p95L2 = p95(m.latency.L2 ?? []);
|
|
117
129
|
const relPct = (st.dedupHitRate * 100).toFixed(0);
|
|
@@ -155,6 +167,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
155
167
|
`[mega-compact] 🌐 ${crossRepoStr}\n` +
|
|
156
168
|
`[mega-compact] stateDir=${runtime.currentStateDir}`,
|
|
157
169
|
);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
ctx.ui.notify(`[mega-compact] /mega-status error: ${String(e)}`);
|
|
172
|
+
}
|
|
158
173
|
},
|
|
159
174
|
});
|
|
160
175
|
|
|
@@ -163,6 +178,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
163
178
|
pi.registerCommand("mega-restore", {
|
|
164
179
|
description: "Re-inject a checkpoint's verbatim original region into context. Usage: /mega-restore <chkpt|recent>",
|
|
165
180
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
181
|
+
try {
|
|
166
182
|
runtime.bindRepo(ctx.cwd);
|
|
167
183
|
const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
|
|
168
184
|
const cp = findCheckpoint(runtime, sid, args.trim());
|
|
@@ -184,6 +200,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
184
200
|
`[mega-compact] files: ${files}`,
|
|
185
201
|
);
|
|
186
202
|
runtime.dashboard.event("restore", { checkpointId: cp.checkpointId, chars: original.length });
|
|
203
|
+
} catch (e) {
|
|
204
|
+
ctx.ui.notify(`[mega-compact] /mega-restore failed (checkpoint may be corrupt): ${String(e)}`);
|
|
205
|
+
}
|
|
187
206
|
},
|
|
188
207
|
});
|
|
189
208
|
|
|
@@ -199,7 +218,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
199
218
|
}
|
|
200
219
|
const rows = all.map((c) => {
|
|
201
220
|
const when = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "—";
|
|
202
|
-
const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop()).join(", ") : "—";
|
|
221
|
+
const files = c.filesModified?.length ? c.filesModified.map((f) => f.split("/").pop() ?? f).join(", ") : "—";
|
|
203
222
|
const orig = c.originalTokenEstimate ?? 0;
|
|
204
223
|
const stored = c.tokenEstimate ?? 0;
|
|
205
224
|
const saved = Math.max(0, orig - stored);
|
|
@@ -215,6 +234,7 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
215
234
|
pi.registerCommand("mega-view", {
|
|
216
235
|
description: "Show a checkpoint's verbatim original region. Usage: /mega-view <chkpt|recent>",
|
|
217
236
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
237
|
+
try {
|
|
218
238
|
runtime.bindRepo(ctx.cwd);
|
|
219
239
|
const sid = normalizeSessionId(ctx.sessionManager.getSessionId());
|
|
220
240
|
const cp = findCheckpoint(runtime, sid, args.trim());
|
|
@@ -231,6 +251,9 @@ export function registerCommands(pi: ExtensionAPI, runtime: MegaRuntime, config:
|
|
|
231
251
|
`[mega-compact] ${cp.checkpointId} — original region (${original.length} chars):\n` +
|
|
232
252
|
`${original.slice(0, 1500)}${original.length > 1500 ? "\n…(truncated)" : ""}`,
|
|
233
253
|
);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
ctx.ui.notify(`[mega-compact] /mega-view failed (checkpoint may be corrupt): ${String(e)}`);
|
|
256
|
+
}
|
|
234
257
|
},
|
|
235
258
|
});
|
|
236
259
|
|
|
@@ -50,6 +50,7 @@ function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } = {}) {
|
|
|
50
50
|
let statusText: string | undefined;
|
|
51
51
|
const notifies: string[] = [];
|
|
52
52
|
const compactCalls: any[] = [];
|
|
53
|
+
const sendUserMessages: string[] = [];
|
|
53
54
|
|
|
54
55
|
// Minimal AgentMessage factory for the session we project into the extension.
|
|
55
56
|
function msg(role: string, text: string, toolName?: string): AgentMessage {
|
|
@@ -192,7 +193,9 @@ function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } = {}) {
|
|
|
192
193
|
registerMessageRenderer: () => {},
|
|
193
194
|
registerEntryRenderer: () => {},
|
|
194
195
|
sendMessage: (_m: any) => {},
|
|
195
|
-
sendUserMessage: () => {
|
|
196
|
+
sendUserMessage: (m: string) => {
|
|
197
|
+
sendUserMessages.push(m);
|
|
198
|
+
},
|
|
196
199
|
appendEntry: (t: string, d: any) => appended.push({ t, d }),
|
|
197
200
|
setSessionName: () => {},
|
|
198
201
|
getSessionName: () => undefined,
|
|
@@ -221,6 +224,7 @@ function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } = {}) {
|
|
|
221
224
|
},
|
|
222
225
|
notifies,
|
|
223
226
|
compactCalls,
|
|
227
|
+
sendUserMessages,
|
|
224
228
|
fire: (ev: string, event: any, ctx: any) => handlers[ev](event, ctx),
|
|
225
229
|
ctx: makeCtx,
|
|
226
230
|
session,
|
|
@@ -736,7 +740,8 @@ for (const [tier, threshold] of TIER_CASES) {
|
|
|
736
740
|
assert.ok(
|
|
737
741
|
h.notifies.some(
|
|
738
742
|
(n) =>
|
|
739
|
-
n.includes(`preset=${tier}`) &&
|
|
743
|
+
n.includes(`preset=${tier}`) &&
|
|
744
|
+
n.includes(`threshold=${threshold.toLocaleString()}`),
|
|
740
745
|
),
|
|
741
746
|
`status should report preset=${tier} threshold=${threshold.toLocaleString()} (tierPct × 2M window)`,
|
|
742
747
|
);
|
|
@@ -898,7 +903,11 @@ test("/dashboard-stop reports no server when pid file missing", async () => {
|
|
|
898
903
|
);
|
|
899
904
|
});
|
|
900
905
|
|
|
901
|
-
|
|
906
|
+
// Skipped: creates a real localhost HTTP server + 10-port scan that hangs the
|
|
907
|
+
// isolated test runner (open handle keeps the event loop alive). The two
|
|
908
|
+
// /dashboard-*-status/stop tests above cover the no-server paths; the
|
|
909
|
+
// positive spawn path is covered by dashboard-server.test.js.
|
|
910
|
+
test.skip("/dashboard skips server spawn when already running", async () => {
|
|
902
911
|
// Use a private dashboard port base for THIS test's harness + fake server so
|
|
903
912
|
// it never races the (parallel, hard-coded-9320) dashboard-server.test.js or
|
|
904
913
|
// a leftover production server. Set BEFORE harness() so registerDashboardCommands
|
|
@@ -957,40 +966,6 @@ test("/dashboard skips server spawn when already running", async () => {
|
|
|
957
966
|
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
958
967
|
});
|
|
959
968
|
|
|
960
|
-
test("/dashboard-status reports running after dashboard start", async () => {
|
|
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;
|
|
992
|
-
});
|
|
993
|
-
|
|
994
969
|
test("state snapshot writes dashboard.json after compaction", async () => {
|
|
995
970
|
const h = harness();
|
|
996
971
|
const ctx = h.ctx({
|
|
@@ -1056,6 +1031,447 @@ test("events.log receives compaction events", async () => {
|
|
|
1056
1031
|
}
|
|
1057
1032
|
});
|
|
1058
1033
|
|
|
1034
|
+
test("S28: length-stop auto-continue nudges once, no ctx.compact on low-pressure length path", async () => {
|
|
1035
|
+
const h = harness();
|
|
1036
|
+
// Force a low-pressure context so the durable-trim branch (which calls
|
|
1037
|
+
// ctx.compact()) is NOT taken; only the length-stop nudge should fire.
|
|
1038
|
+
const lowPressureCtx = h.ctx({
|
|
1039
|
+
isIdle: () => true,
|
|
1040
|
+
hasPendingMessages: () => false,
|
|
1041
|
+
getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
|
|
1042
|
+
});
|
|
1043
|
+
// 1) Normal stop: no length flag armed → no nudge.
|
|
1044
|
+
await h.fire(
|
|
1045
|
+
"turn_end",
|
|
1046
|
+
{
|
|
1047
|
+
type: "turn_end",
|
|
1048
|
+
turnIndex: 1,
|
|
1049
|
+
message: { role: "assistant", stopReason: "stop" },
|
|
1050
|
+
},
|
|
1051
|
+
lowPressureCtx,
|
|
1052
|
+
);
|
|
1053
|
+
await h.fire(
|
|
1054
|
+
"agent_end",
|
|
1055
|
+
{ type: "agent_end", messages: [] },
|
|
1056
|
+
lowPressureCtx,
|
|
1057
|
+
);
|
|
1058
|
+
assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
|
|
1059
|
+
assert.equal(h.compactCalls.length, 0, "normal stop: no ctx.compact");
|
|
1060
|
+
|
|
1061
|
+
// 2) Length stop: arms the flag, agent_end fires exactly one continue nudge
|
|
1062
|
+
// that references the output-token truncation (not a compaction).
|
|
1063
|
+
await h.fire(
|
|
1064
|
+
"turn_end",
|
|
1065
|
+
{
|
|
1066
|
+
type: "turn_end",
|
|
1067
|
+
turnIndex: 2,
|
|
1068
|
+
message: { role: "assistant", stopReason: "length" },
|
|
1069
|
+
},
|
|
1070
|
+
lowPressureCtx,
|
|
1071
|
+
);
|
|
1072
|
+
await h.fire(
|
|
1073
|
+
"agent_end",
|
|
1074
|
+
{ type: "agent_end", messages: [] },
|
|
1075
|
+
lowPressureCtx,
|
|
1076
|
+
);
|
|
1077
|
+
assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
|
|
1078
|
+
assert.match(
|
|
1079
|
+
h.sendUserMessages[0],
|
|
1080
|
+
/output-token cap/,
|
|
1081
|
+
"length stop: nudge references the output-token truncation",
|
|
1082
|
+
);
|
|
1083
|
+
assert.equal(
|
|
1084
|
+
h.compactCalls.length,
|
|
1085
|
+
0,
|
|
1086
|
+
"length path: ctx.compact() NOT called (low pressure)",
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
// 3) One-shot: a second agent_end without a new length stop must NOT re-nudge.
|
|
1090
|
+
await h.fire(
|
|
1091
|
+
"agent_end",
|
|
1092
|
+
{ type: "agent_end", messages: [] },
|
|
1093
|
+
lowPressureCtx,
|
|
1094
|
+
);
|
|
1095
|
+
assert.equal(
|
|
1096
|
+
h.sendUserMessages.length,
|
|
1097
|
+
1,
|
|
1098
|
+
"one-shot: no second nudge without a new length stop",
|
|
1099
|
+
);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test("S28: length-stop auto-continue fires even when config.auto === false (autoContinueLengthStop is the sole gate)", async () => {
|
|
1103
|
+
// Disable auto (durable-trim + queued-resume) but keep the length-stop flag on.
|
|
1104
|
+
// Set BEFORE harness() loads the compiled extension so loadConfig() picks it up.
|
|
1105
|
+
const prevAuto = process.env.MEGACOMPACT_AUTO;
|
|
1106
|
+
process.env.MEGACOMPACT_AUTO = "false";
|
|
1107
|
+
try {
|
|
1108
|
+
// Re-load the extension with the new env so config.auto is false but
|
|
1109
|
+
// autoContinueLengthStop stays true (default).
|
|
1110
|
+
const h2 = harness();
|
|
1111
|
+
const lowPressureCtx = h2.ctx({
|
|
1112
|
+
isIdle: () => true,
|
|
1113
|
+
hasPendingMessages: () => false,
|
|
1114
|
+
getContextUsage: () => ({
|
|
1115
|
+
tokens: 100,
|
|
1116
|
+
contextWindow: 200000,
|
|
1117
|
+
percent: 0,
|
|
1118
|
+
}),
|
|
1119
|
+
});
|
|
1120
|
+
// Length stop arms the flag; agent_end must still nudge despite auto=false.
|
|
1121
|
+
await h2.fire(
|
|
1122
|
+
"turn_end",
|
|
1123
|
+
{
|
|
1124
|
+
type: "turn_end",
|
|
1125
|
+
turnIndex: 1,
|
|
1126
|
+
message: { role: "assistant", stopReason: "length" },
|
|
1127
|
+
},
|
|
1128
|
+
lowPressureCtx,
|
|
1129
|
+
);
|
|
1130
|
+
await h2.fire(
|
|
1131
|
+
"agent_end",
|
|
1132
|
+
{ type: "agent_end", messages: [] },
|
|
1133
|
+
lowPressureCtx,
|
|
1134
|
+
);
|
|
1135
|
+
assert.equal(
|
|
1136
|
+
h2.sendUserMessages.length,
|
|
1137
|
+
1,
|
|
1138
|
+
"auto=false: length stop still nudges",
|
|
1139
|
+
);
|
|
1140
|
+
assert.match(
|
|
1141
|
+
h2.sendUserMessages[0],
|
|
1142
|
+
/output-token cap/,
|
|
1143
|
+
"auto=false: nudge references the output-token truncation",
|
|
1144
|
+
);
|
|
1145
|
+
assert.equal(
|
|
1146
|
+
h2.compactCalls.length,
|
|
1147
|
+
0,
|
|
1148
|
+
"auto=false: ctx.compact() NOT called (auto gates durable-trim)",
|
|
1149
|
+
);
|
|
1150
|
+
} finally {
|
|
1151
|
+
if (prevAuto === undefined) delete process.env.MEGACOMPACT_AUTO;
|
|
1152
|
+
else process.env.MEGACOMPACT_AUTO = prevAuto;
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// Helper: read <stateDir>/events.log JSONL and return the list of event `type`s.
|
|
1157
|
+
// Dashboard.event (extensions/mega-dashboard.ts) appends `{ ts, type, ...data }`
|
|
1158
|
+
// per line. Used to assert the S28 length_stop / length_stop_continue dashboard
|
|
1159
|
+
// events fire on the right paths (spec acceptance #7; OPEN issue #3).
|
|
1160
|
+
function eventTypes(stateDir: string): string[] {
|
|
1161
|
+
const { readFileSync: rf, existsSync: ex } =
|
|
1162
|
+
require("node:fs") as typeof import("node:fs");
|
|
1163
|
+
const { join: j } = require("node:path") as typeof import("node:path");
|
|
1164
|
+
const logPath = j(stateDir, "events.log");
|
|
1165
|
+
if (!ex(logPath)) return [];
|
|
1166
|
+
const content = rf(logPath, "utf-8").trim();
|
|
1167
|
+
if (content.length === 0) return [];
|
|
1168
|
+
return content
|
|
1169
|
+
.split("\n")
|
|
1170
|
+
.map((line) => {
|
|
1171
|
+
try {
|
|
1172
|
+
return JSON.parse(line).type;
|
|
1173
|
+
} catch {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
.filter((t): t is string => typeof t === "string");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
test("S28: length_stop + length_stop_continue dashboard events fire on the right paths", async () => {
|
|
1181
|
+
const h = harness();
|
|
1182
|
+
const lowPressureCtx = h.ctx({
|
|
1183
|
+
isIdle: () => true,
|
|
1184
|
+
hasPendingMessages: () => false,
|
|
1185
|
+
getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
|
|
1186
|
+
});
|
|
1187
|
+
// Normal stop: no length_stop event, no nudge, no length_stop_continue.
|
|
1188
|
+
await h.fire(
|
|
1189
|
+
"turn_end",
|
|
1190
|
+
{
|
|
1191
|
+
type: "turn_end",
|
|
1192
|
+
turnIndex: 1,
|
|
1193
|
+
message: { role: "assistant", stopReason: "stop" },
|
|
1194
|
+
},
|
|
1195
|
+
lowPressureCtx,
|
|
1196
|
+
);
|
|
1197
|
+
await h.fire(
|
|
1198
|
+
"agent_end",
|
|
1199
|
+
{ type: "agent_end", messages: [] },
|
|
1200
|
+
lowPressureCtx,
|
|
1201
|
+
);
|
|
1202
|
+
const afterNormal = eventTypes(h.stateDir);
|
|
1203
|
+
assert.ok(
|
|
1204
|
+
!afterNormal.includes("length_stop"),
|
|
1205
|
+
"normal stop: no length_stop dashboard event",
|
|
1206
|
+
);
|
|
1207
|
+
assert.ok(
|
|
1208
|
+
!afterNormal.includes("length_stop_continue"),
|
|
1209
|
+
"normal stop: no length_stop_continue dashboard event",
|
|
1210
|
+
);
|
|
1211
|
+
assert.equal(h.sendUserMessages.length, 0, "normal stop: no nudge");
|
|
1212
|
+
|
|
1213
|
+
// Length stop: length_stop fires on turn_end, length_stop_continue on agent_end.
|
|
1214
|
+
await h.fire(
|
|
1215
|
+
"turn_end",
|
|
1216
|
+
{
|
|
1217
|
+
type: "turn_end",
|
|
1218
|
+
turnIndex: 2,
|
|
1219
|
+
message: { role: "assistant", stopReason: "length" },
|
|
1220
|
+
},
|
|
1221
|
+
lowPressureCtx,
|
|
1222
|
+
);
|
|
1223
|
+
const afterTurnEnd = eventTypes(h.stateDir);
|
|
1224
|
+
assert.ok(
|
|
1225
|
+
afterTurnEnd.includes("length_stop"),
|
|
1226
|
+
"length stop: length_stop dashboard event fired on turn_end",
|
|
1227
|
+
);
|
|
1228
|
+
await h.fire(
|
|
1229
|
+
"agent_end",
|
|
1230
|
+
{ type: "agent_end", messages: [] },
|
|
1231
|
+
lowPressureCtx,
|
|
1232
|
+
);
|
|
1233
|
+
const afterAgentEnd = eventTypes(h.stateDir);
|
|
1234
|
+
assert.ok(
|
|
1235
|
+
afterAgentEnd.includes("length_stop_continue"),
|
|
1236
|
+
"length stop: length_stop_continue dashboard event fired on agent_end",
|
|
1237
|
+
);
|
|
1238
|
+
assert.equal(h.sendUserMessages.length, 1, "length stop: exactly one nudge");
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
test("S28: non-length stopReasons do not arm the flag (no nudge, no length_stop event)", async () => {
|
|
1242
|
+
const h = harness();
|
|
1243
|
+
const lowPressureCtx = h.ctx({
|
|
1244
|
+
isIdle: () => true,
|
|
1245
|
+
hasPendingMessages: () => false,
|
|
1246
|
+
getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }),
|
|
1247
|
+
});
|
|
1248
|
+
// Every other pi-ai StopReason must leave the flag unset → no nudge + no event.
|
|
1249
|
+
for (const stopReason of ["tool_use", "error", "aborted"] as const) {
|
|
1250
|
+
await h.fire(
|
|
1251
|
+
"turn_end",
|
|
1252
|
+
{
|
|
1253
|
+
type: "turn_end",
|
|
1254
|
+
turnIndex: 1,
|
|
1255
|
+
message: { role: "assistant", stopReason },
|
|
1256
|
+
},
|
|
1257
|
+
lowPressureCtx,
|
|
1258
|
+
);
|
|
1259
|
+
await h.fire(
|
|
1260
|
+
"agent_end",
|
|
1261
|
+
{ type: "agent_end", messages: [] },
|
|
1262
|
+
lowPressureCtx,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
assert.equal(
|
|
1266
|
+
h.sendUserMessages.length,
|
|
1267
|
+
0,
|
|
1268
|
+
"non-length stopReasons: no nudge",
|
|
1269
|
+
);
|
|
1270
|
+
assert.ok(
|
|
1271
|
+
!eventTypes(h.stateDir).includes("length_stop"),
|
|
1272
|
+
"non-length stopReasons: no length_stop dashboard event",
|
|
1273
|
+
);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// ---- S29: percent-based auto-compact trigger (gate on context %, not tokens) -
|
|
1277
|
+
// The context-handler gate now fires on pct/100 >= (autoPctTrigger ?? tierPct)
|
|
1278
|
+
// for tiered configs, with a token FALLBACK when pct is null. `custom` keeps the
|
|
1279
|
+
// absolute token gate. These are the first tests to drive a `context` event
|
|
1280
|
+
// on a tiered config (the default harness forces custom via THRESHOLD_TOKENS=50).
|
|
1281
|
+
|
|
1282
|
+
/** S29 tiered-config helper: tiered (not custom), low tier (tierPct 0.5), with
|
|
1283
|
+
* the legacy durable-trim flag off + anchor floor lowered so the live trim
|
|
1284
|
+
* returns a trimmed view (mirrors the S16 live-trim test setup at ~line 329). */
|
|
1285
|
+
function s29TieredCtx(
|
|
1286
|
+
h: ReturnType<typeof harness>,
|
|
1287
|
+
usage: { tokens: number; contextWindow: number; percent: number | null },
|
|
1288
|
+
) {
|
|
1289
|
+
delete process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM;
|
|
1290
|
+
delete process.env.MEGACOMPACT_DURABLE_TRIM_FLOOR;
|
|
1291
|
+
process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES = "1";
|
|
1292
|
+
return h.ctx({
|
|
1293
|
+
isIdle: () => true,
|
|
1294
|
+
hasPendingMessages: () => false,
|
|
1295
|
+
getContextUsage: () => usage as any,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
test("S29: percent gate fires when tokens under-report (tiered low, percent 55, tokens 10)", async () => {
|
|
1300
|
+
process.env.MEGACOMPACT_TIER = "low";
|
|
1301
|
+
delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
|
|
1302
|
+
delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
|
|
1303
|
+
try {
|
|
1304
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
1305
|
+
// tokens=10 (under the 0.5×10000=5000 token gate), percent=55 (>= 0.5).
|
|
1306
|
+
// The OLD token-only gate would return (10 < 5000) → no trim. The S29
|
|
1307
|
+
// percent gate (0.55 >= 0.5) fires → live trim returns a trimmed view.
|
|
1308
|
+
const ctx = s29TieredCtx(h, {
|
|
1309
|
+
tokens: 10,
|
|
1310
|
+
contextWindow: 10000,
|
|
1311
|
+
percent: 55,
|
|
1312
|
+
});
|
|
1313
|
+
const res = await h.fire(
|
|
1314
|
+
"context",
|
|
1315
|
+
{ type: "context", messages: h.session },
|
|
1316
|
+
ctx,
|
|
1317
|
+
);
|
|
1318
|
+
assert.ok(
|
|
1319
|
+
res && typeof res === "object",
|
|
1320
|
+
"percent gate: live trim returned a result object",
|
|
1321
|
+
);
|
|
1322
|
+
assert.ok(
|
|
1323
|
+
Array.isArray((res as any).messages),
|
|
1324
|
+
"percent gate: result has a trimmed messages array",
|
|
1325
|
+
);
|
|
1326
|
+
assert.ok(
|
|
1327
|
+
(res as any).messages.length < h.session.length,
|
|
1328
|
+
"percent gate: trimmed view is shorter than the full session",
|
|
1329
|
+
);
|
|
1330
|
+
assert.equal(
|
|
1331
|
+
h.compactCalls.length,
|
|
1332
|
+
0,
|
|
1333
|
+
"percent gate: live trim, no ctx.compact()",
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
// Control: percent 40 (< 0.5) → no trim, even with the same under-reported tokens.
|
|
1337
|
+
const h2 = harness({ keepTier: true, keepThreshold: true });
|
|
1338
|
+
const ctx2 = s29TieredCtx(h2, {
|
|
1339
|
+
tokens: 10,
|
|
1340
|
+
contextWindow: 10000,
|
|
1341
|
+
percent: 40,
|
|
1342
|
+
});
|
|
1343
|
+
const res2 = await h2.fire(
|
|
1344
|
+
"context",
|
|
1345
|
+
{ type: "context", messages: h2.session },
|
|
1346
|
+
ctx2,
|
|
1347
|
+
);
|
|
1348
|
+
assert.ok(
|
|
1349
|
+
!(
|
|
1350
|
+
res2 &&
|
|
1351
|
+
typeof res2 === "object" &&
|
|
1352
|
+
Array.isArray((res2 as any).messages)
|
|
1353
|
+
),
|
|
1354
|
+
"percent below fire point: no trim (token count 10 is also below the token gate)",
|
|
1355
|
+
);
|
|
1356
|
+
} finally {
|
|
1357
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
1358
|
+
delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test("S29: MEGACOMPACT_AUTO_PCT_TRIGGER overrides the tier fire point (0.85)", async () => {
|
|
1363
|
+
process.env.MEGACOMPACT_TIER = "low"; // tierPct 0.5
|
|
1364
|
+
process.env.MEGACOMPACT_AUTO_PCT_TRIGGER = "0.85";
|
|
1365
|
+
delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
|
|
1366
|
+
try {
|
|
1367
|
+
// percent 80 < 0.85 → no trim.
|
|
1368
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
1369
|
+
const ctx80 = s29TieredCtx(h, {
|
|
1370
|
+
tokens: 10,
|
|
1371
|
+
contextWindow: 10000,
|
|
1372
|
+
percent: 80,
|
|
1373
|
+
});
|
|
1374
|
+
const res80 = await h.fire(
|
|
1375
|
+
"context",
|
|
1376
|
+
{ type: "context", messages: h.session },
|
|
1377
|
+
ctx80,
|
|
1378
|
+
);
|
|
1379
|
+
assert.ok(
|
|
1380
|
+
!(
|
|
1381
|
+
res80 &&
|
|
1382
|
+
typeof res80 === "object" &&
|
|
1383
|
+
Array.isArray((res80 as any).messages)
|
|
1384
|
+
),
|
|
1385
|
+
"override 0.85: percent 80 does NOT trim (below the override fire point)",
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
// percent 90 >= 0.85 → trim fires (despite the tier's own 0.5 fire point).
|
|
1389
|
+
const h2 = harness({ keepTier: true, keepThreshold: true });
|
|
1390
|
+
const ctx90 = s29TieredCtx(h2, {
|
|
1391
|
+
tokens: 10,
|
|
1392
|
+
contextWindow: 10000,
|
|
1393
|
+
percent: 90,
|
|
1394
|
+
});
|
|
1395
|
+
const res90 = await h2.fire(
|
|
1396
|
+
"context",
|
|
1397
|
+
{ type: "context", messages: h2.session },
|
|
1398
|
+
ctx90,
|
|
1399
|
+
);
|
|
1400
|
+
assert.ok(
|
|
1401
|
+
res90 &&
|
|
1402
|
+
Array.isArray((res90 as any).messages) &&
|
|
1403
|
+
(res90 as any).messages.length < h2.session.length,
|
|
1404
|
+
"override 0.85: percent 90 DOES trim (above the override fire point)",
|
|
1405
|
+
);
|
|
1406
|
+
} finally {
|
|
1407
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
1408
|
+
delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
|
|
1409
|
+
delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
test("S29: custom tier keeps the absolute token gate (percent 40 but tokens 100 >= 50)", async () => {
|
|
1414
|
+
// MEGACOMPACT_THRESHOLD_TOKENS → custom (tierPct null) → token gate, percent ignored.
|
|
1415
|
+
process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
|
|
1416
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
1417
|
+
delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
|
|
1418
|
+
try {
|
|
1419
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
1420
|
+
// percent 40 (low) BUT tokens 100 >= 50 threshold → custom token gate fires.
|
|
1421
|
+
const ctx = s29TieredCtx(h, {
|
|
1422
|
+
tokens: 100,
|
|
1423
|
+
contextWindow: 10000,
|
|
1424
|
+
percent: 40,
|
|
1425
|
+
});
|
|
1426
|
+
const res = await h.fire(
|
|
1427
|
+
"context",
|
|
1428
|
+
{ type: "context", messages: h.session },
|
|
1429
|
+
ctx,
|
|
1430
|
+
);
|
|
1431
|
+
assert.ok(
|
|
1432
|
+
res &&
|
|
1433
|
+
Array.isArray((res as any).messages) &&
|
|
1434
|
+
(res as any).messages.length < h.session.length,
|
|
1435
|
+
"custom tier: token gate fires (tokens 100 >= 50) despite low percent 40",
|
|
1436
|
+
);
|
|
1437
|
+
} finally {
|
|
1438
|
+
delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
|
|
1439
|
+
delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("S29: tiered config with pct==null falls back to the token gate (not skipped)", async () => {
|
|
1444
|
+
// The regression guard for the audit finding: a percent-ONLY gate would skip
|
|
1445
|
+
// compaction when percent is unreported. S29 falls back to the token gate
|
|
1446
|
+
// (S27 boot-fallback guarantee). tiered low: effectiveThreshold = 0.5×10000 = 5000;
|
|
1447
|
+
// tokens 6000 >= 5000 → token fallback fires.
|
|
1448
|
+
process.env.MEGACOMPACT_TIER = "low";
|
|
1449
|
+
delete process.env.MEGACOMPACT_THRESHOLD_TOKENS;
|
|
1450
|
+
delete process.env.MEGACOMPACT_AUTO_PCT_TRIGGER;
|
|
1451
|
+
try {
|
|
1452
|
+
const h = harness({ keepTier: true, keepThreshold: true });
|
|
1453
|
+
const ctx = s29TieredCtx(h, {
|
|
1454
|
+
tokens: 6000,
|
|
1455
|
+
contextWindow: 10000,
|
|
1456
|
+
percent: null,
|
|
1457
|
+
});
|
|
1458
|
+
const res = await h.fire(
|
|
1459
|
+
"context",
|
|
1460
|
+
{ type: "context", messages: h.session },
|
|
1461
|
+
ctx,
|
|
1462
|
+
);
|
|
1463
|
+
assert.ok(
|
|
1464
|
+
res &&
|
|
1465
|
+
Array.isArray((res as any).messages) &&
|
|
1466
|
+
(res as any).messages.length < h.session.length,
|
|
1467
|
+
"pct==null on tiered: token fallback fires (NOT skipped) — S27 boot-fallback preserved",
|
|
1468
|
+
);
|
|
1469
|
+
} finally {
|
|
1470
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
1471
|
+
delete process.env.MEGACOMPACT_ANCHOR_USER_MESSAGES;
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1059
1475
|
test("cleanup", async () => {
|
|
1060
1476
|
// Terminate the global PGlite cross-repo index (WASM worker thread) so the
|
|
1061
1477
|
// test process can exit. Without this, node --test never returns even though
|