pi-mega-compact 0.13.0 → 0.13.2
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/dist/extensions/dashboard-server/api-contracts/embedder-health.js +11 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +225 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/types.js +1 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +5 -224
- package/dist/extensions/dashboard-server/api-contracts/raptor.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.test/_helpers.js +32 -0
- package/dist/extensions/dashboard-server/routes-cache.test/_helpers.js +48 -0
- package/dist/extensions/dashboard-server/routes-embedder-health.js +67 -0
- package/dist/extensions/dashboard-server/routes-raptor.js +88 -0
- package/dist/extensions/dashboard-server/routes-setup.js +1 -1
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +5 -1
- package/dist/extensions/mega-cache-replay.test/_helpers.js +175 -0
- package/dist/extensions/mega-commands/dataCommands.js +151 -0
- package/dist/extensions/mega-commands/helpers.js +61 -0
- package/dist/extensions/mega-commands/historyCommands.js +97 -0
- package/dist/extensions/mega-commands/setupCommand.js +200 -0
- package/dist/extensions/mega-commands.js +14 -499
- package/dist/extensions/mega-compact-s38.test/_helpers.js +194 -0
- package/dist/extensions/mega-compact.js +12 -2
- package/dist/extensions/mega-compact.test/_helpers.js +225 -0
- package/dist/extensions/mega-events/agent-handlers/agentEndHandler.js +211 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.js +49 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.js +33 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.js +387 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/event.js +7 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.js +55 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.js +12 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.js +22 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.js +38 -0
- package/dist/extensions/mega-events/agent-handlers/turnEndHandler.js +36 -0
- package/dist/extensions/mega-events/agent-handlers.js +4 -799
- package/dist/extensions/mega-events/context-handler/afterCompact.js +163 -0
- package/dist/extensions/mega-events/context-handler/messageText.js +12 -0
- package/dist/extensions/mega-events/context-handler/tailResult.js +46 -0
- package/dist/extensions/mega-events/context-handler.js +11 -198
- package/dist/extensions/mega-events/separated-prompt.test/_helpers.js +25 -0
- package/dist/src/config.js +2 -0
- package/dist/src/dedup-engine.test/_helpers.js +55 -0
- package/dist/src/e2e.test/_helpers.js +52 -0
- package/dist/src/embedder.js +1 -0
- package/dist/src/httpEmbedder.js +6 -0
- package/dist/src/hyde.js +146 -0
- package/dist/src/importance.test/_helpers.js +5 -0
- package/dist/src/memoryGraph/sources/checkpoints.js +78 -0
- package/dist/src/memoryGraph/sources/edges.js +17 -0
- package/dist/src/memoryGraph/sources/flags.js +22 -0
- package/dist/src/memoryGraph/sources/helpers.js +14 -0
- package/dist/src/memoryGraph/sources/memories.js +88 -0
- package/dist/src/memoryGraph/sources/raptor.js +26 -0
- package/dist/src/memoryGraph/sources/turns.js +176 -0
- package/dist/src/memoryGraph/sources.js +15 -441
- package/dist/src/memoryGraph-gates.test/_helpers.js +120 -0
- package/dist/src/ratio.bench.test/_helpers.js +198 -0
- package/dist/src/recall/async.js +112 -0
- package/dist/src/recall/format.js +149 -0
- package/dist/src/recall/memory.js +62 -0
- package/dist/src/recall/reformulate.js +82 -0
- package/dist/src/recall/sync.js +169 -0
- package/dist/src/recall/types.js +1 -0
- package/dist/src/recall.js +13 -530
- package/dist/src/store/sqlite/schema/core.js +232 -0
- package/dist/src/store/sqlite/schema/game.js +65 -0
- package/dist/src/store/sqlite/schema/plan-v2.js +107 -0
- package/dist/src/store/sqlite/schema/turns.js +59 -0
- package/dist/src/store/sqlite/schema.js +8 -441
- package/dist/src/store/turns/contract-compliance.test/_helpers.js +21 -0
- package/dist/src/store/turns/sqlite-store/admin.js +171 -0
- package/dist/src/store/turns/sqlite-store/ctx.js +1 -0
- package/dist/src/store/turns/sqlite-store/read.js +111 -0
- package/dist/src/store/turns/sqlite-store/rows.js +42 -0
- package/dist/src/store/turns/sqlite-store/write.js +102 -0
- package/dist/src/store/turns/sqlite-store.js +31 -367
- package/dist/src/vectorStore/class.js +356 -0
- package/dist/src/vectorStore/hash.js +10 -0
- package/dist/src/vectorStore/types.js +2 -0
- package/dist/src/vectorStore.js +8 -359
- package/dist/src/vectorStore.test/_helpers.js +17 -0
- package/extensions/dashboard-client/dist/assets/{CacheTab-BZNnKhh5.js → CacheTab-DPRHjYZ0.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BZNnKhh5.js.map → CacheTab-DPRHjYZ0.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CzF5L42U.js → EventsTab-CgwYq-Qf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CzF5L42U.js.map → EventsTab-CgwYq-Qf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-BLpidMrX.js → HealthTab-BgOCjVzR.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-BLpidMrX.js.map → HealthTab-BgOCjVzR.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Yw1yFpsy.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-yHUX7Yzi.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-B0PCysqs.js → MetricsTab-CcH9fP8P.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-B0PCysqs.js.map → MetricsTab-CcH9fP8P.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DsOhqFu4.js → OverviewTab-BOs8ZNev.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DsOhqFu4.js.map → OverviewTab-BOs8ZNev.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-B3YKYuOf.js → ReposTab-B-MfSPvg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-B3YKYuOf.js.map → ReposTab-B-MfSPvg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-B96FWcrC.js → SessionsTab-BmeN649Q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-B96FWcrC.js.map → SessionsTab-BmeN649Q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-nOknoph5.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BfkUVTvA.js → TimeSavedCard-DnPg0L0z.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BfkUVTvA.js.map → TimeSavedCard-DnPg0L0z.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DLRsZmXJ.js → TopicsTab-BODHOg_b.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TopicsTab-DLRsZmXJ.js.map → TopicsTab-BODHOg_b.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DKGU2Bf6.js → TurnsTab-BK0I7hKr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DKGU2Bf6.js.map → TurnsTab-BK0I7hKr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-D-lPLRWB.js → index-mOlljTFI.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-mOlljTFI.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-ClbbcgIp.js → useSSE-BYOtv1-3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-ClbbcgIp.js.map → useSSE-BYOtv1-3.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/App.tsx +0 -4
- package/extensions/dashboard-client/src/api/client.ts +16 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/ActionsCard.tsx +178 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DbStatsCard.tsx +140 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DebugBundleCard.tsx +109 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/HealthMitigationCard.tsx +98 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/SchemaHealthCard.tsx +108 -0
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +12 -598
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/MemoryMapView.tsx +476 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +163 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +56 -466
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderHealthCard.tsx +109 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +468 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +23 -484
- package/extensions/dashboard-server/api-contracts/embedder-health.ts +32 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +360 -0
- package/extensions/dashboard-server/api-contracts/endpoints/types.ts +350 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +27 -648
- package/extensions/dashboard-server/api-contracts/index.ts +6 -0
- package/extensions/dashboard-server/api-contracts/raptor.ts +49 -0
- package/extensions/dashboard-server/api-contracts.test/_helpers.ts +47 -0
- package/extensions/dashboard-server/routes-cache.test/_helpers.ts +94 -0
- package/extensions/dashboard-server/routes-embedder-health.ts +77 -0
- package/extensions/dashboard-server/routes-raptor.ts +128 -0
- package/extensions/dashboard-server/routes-setup.ts +1 -1
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +4 -0
- package/extensions/mega-cache-replay.test/_helpers.ts +188 -0
- package/extensions/mega-commands/dataCommands.ts +169 -0
- package/extensions/mega-commands/helpers.ts +62 -0
- package/extensions/mega-commands/historyCommands.ts +117 -0
- package/extensions/mega-commands/setupCommand.ts +235 -0
- package/extensions/mega-commands.ts +25 -538
- package/extensions/mega-compact-s38.test/_helpers.ts +201 -0
- package/extensions/mega-compact.test/_helpers.ts +237 -0
- package/extensions/mega-compact.ts +12 -2
- package/extensions/mega-events/agent-handlers/agentEndHandler.ts +238 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/cacheStripe.ts +74 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/contextHealth.ts +50 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/errorRetry.ts +421 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/event.ts +16 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/gameScoring.ts +73 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/lengthStop.ts +29 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/memoryReview.ts +42 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler/recordTurnRow.ts +61 -0
- package/extensions/mega-events/agent-handlers/turnEndHandler.ts +64 -0
- package/extensions/mega-events/agent-handlers.ts +15 -861
- package/extensions/mega-events/context-handler/afterCompact.ts +213 -0
- package/extensions/mega-events/context-handler/messageText.ts +20 -0
- package/extensions/mega-events/context-handler/tailResult.ts +63 -0
- package/extensions/mega-events/context-handler.ts +17 -235
- package/extensions/mega-events/separated-prompt.test/_helpers.ts +35 -0
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dedup-engine.test/_helpers.ts +79 -0
- package/src/e2e.test/_helpers.ts +59 -0
- package/src/embedder.ts +4 -0
- package/src/httpEmbedder.ts +7 -0
- package/src/hyde.ts +162 -0
- package/src/importance.test/_helpers.ts +5 -0
- package/src/memoryGraph/sources/checkpoints.ts +99 -0
- package/src/memoryGraph/sources/edges.ts +23 -0
- package/src/memoryGraph/sources/flags.ts +26 -0
- package/src/memoryGraph/sources/helpers.ts +13 -0
- package/src/memoryGraph/sources/memories.ts +108 -0
- package/src/memoryGraph/sources/raptor.ts +37 -0
- package/src/memoryGraph/sources/turns.ts +226 -0
- package/src/memoryGraph/sources.ts +19 -544
- package/src/memoryGraph-gates.test/_helpers.ts +164 -0
- package/src/ratio.bench.test/_helpers.ts +268 -0
- package/src/recall/async.ts +142 -0
- package/src/recall/format.ts +173 -0
- package/src/recall/memory.ts +90 -0
- package/src/recall/reformulate.ts +111 -0
- package/src/recall/sync.ts +196 -0
- package/src/recall/types.ts +63 -0
- package/src/recall.ts +25 -712
- package/src/store/sqlite/schema/core.ts +233 -0
- package/src/store/sqlite/schema/game.ts +66 -0
- package/src/store/sqlite/schema/plan-v2.ts +108 -0
- package/src/store/sqlite/schema/turns.ts +60 -0
- package/src/store/sqlite/schema.ts +13 -441
- package/src/store/turns/contract-compliance.test/_helpers.ts +38 -0
- package/src/store/turns/sqlite-store/admin.ts +249 -0
- package/src/store/turns/sqlite-store/ctx.ts +14 -0
- package/src/store/turns/sqlite-store/read.ts +174 -0
- package/src/store/turns/sqlite-store/rows.ts +55 -0
- package/src/store/turns/sqlite-store/write.ts +193 -0
- package/src/store/turns/sqlite-store.ts +33 -514
- package/src/vectorStore/class.ts +433 -0
- package/src/vectorStore/hash.ts +11 -0
- package/src/vectorStore/types.ts +58 -0
- package/src/vectorStore.test/_helpers.ts +20 -0
- package/src/vectorStore.ts +13 -489
- package/extensions/dashboard-client/dist/assets/ConfigTab-DG0WkuuG.js +0 -2
- package/extensions/dashboard-client/dist/assets/ConfigTab-DG0WkuuG.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-BQF7W7xX.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-BQF7W7xX.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CPtHLv0A.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CPtHLv0A.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/SetupTab-hbzQ_NZv.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-hbzQ_NZv.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-D-lPLRWB.js.map +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared harness for the mega-compact-s38 split test files.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the top of mega-compact-s38.test.ts: the mock-pi harness that
|
|
5
|
+
* loads the compiled extension, the eventTypes/eventPayloads log readers, the
|
|
6
|
+
* s38TurnEnd helpers, and the classifyError fixtures.
|
|
7
|
+
*
|
|
8
|
+
* Every split file imports this module, which also registers an `after()` hook
|
|
9
|
+
* that force-exits the test subprocess. Each harness() creates a MegaRuntime
|
|
10
|
+
* with an fs.watch game-state watcher that is never disposed (no
|
|
11
|
+
* session_shutdown in tests); those handles keep the event loop alive
|
|
12
|
+
* indefinitely, so without a force-exit `node --test` (without
|
|
13
|
+
* --test-force-exit) would hang. The hook drains stdout/stderr then defers the
|
|
14
|
+
* exit — a bare process.exit() discards unflushed pipe buffers (observed
|
|
15
|
+
* 2026-07-30: piped test results silently vanished).
|
|
16
|
+
*
|
|
17
|
+
* Relative paths are one directory deeper than the original file, hence
|
|
18
|
+
* `../mega-compact.js` / `../mega-events.js`.
|
|
19
|
+
*/
|
|
20
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import { after } from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { closeVectorIndex } from "../../src/store/vectorIndex.js";
|
|
27
|
+
export const require = createRequire(import.meta.url);
|
|
28
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-s38-"));
|
|
29
|
+
export const baseTmpDir = baseTmp;
|
|
30
|
+
export { closeVectorIndex };
|
|
31
|
+
// Isolate the machine-wide repo index so test runs never pollute the real one.
|
|
32
|
+
process.env.MEGACOMPACT_INDEX_DIR = join(baseTmp, "index");
|
|
33
|
+
let counter = 0;
|
|
34
|
+
after(async () => {
|
|
35
|
+
// PGlite WASM close can hang; race with a timeout to prevent 40-min hangs.
|
|
36
|
+
try {
|
|
37
|
+
await Promise.race([closeVectorIndex(), new Promise((r) => setTimeout(r, 3000))]);
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore */ }
|
|
40
|
+
if (existsSync(baseTmp))
|
|
41
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
42
|
+
// Drain stdout/stderr, then defer the exit so the final test report flushes.
|
|
43
|
+
await new Promise((r) => process.stdout.write("", r));
|
|
44
|
+
await new Promise((r) => process.stderr.write("", r));
|
|
45
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
/** Read events.log and return an array of event type strings. */
|
|
49
|
+
export function eventTypes(stateDir) {
|
|
50
|
+
const { readFileSync: rf, existsSync: ex } = require("node:fs");
|
|
51
|
+
const { join: j } = require("node:path");
|
|
52
|
+
const logPath = j(stateDir, "events.log");
|
|
53
|
+
if (!ex(logPath))
|
|
54
|
+
return [];
|
|
55
|
+
const content = rf(logPath, "utf-8").trim();
|
|
56
|
+
if (content.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
return content
|
|
59
|
+
.split("\n")
|
|
60
|
+
.map((line) => { try {
|
|
61
|
+
return JSON.parse(line).type;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
} })
|
|
66
|
+
.filter((t) => typeof t === "string");
|
|
67
|
+
}
|
|
68
|
+
/** R11: read events.log and return full JSON payloads for a given type. */
|
|
69
|
+
export function eventPayloads(stateDir, type) {
|
|
70
|
+
const { readFileSync: rf, existsSync: ex } = require("node:fs");
|
|
71
|
+
const { join: j } = require("node:path");
|
|
72
|
+
const logPath = j(stateDir, "events.log");
|
|
73
|
+
if (!ex(logPath))
|
|
74
|
+
return [];
|
|
75
|
+
const content = rf(logPath, "utf-8").trim();
|
|
76
|
+
if (content.length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
return content
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map((line) => { try {
|
|
81
|
+
return JSON.parse(line);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
} })
|
|
86
|
+
.filter((p) => typeof p === "object" && p !== null && p.type === type);
|
|
87
|
+
}
|
|
88
|
+
/** Build a mock pi + ctx and load the extension into them. */
|
|
89
|
+
export function harness(opts = {}) {
|
|
90
|
+
const stateDir = join(baseTmp, `run-${counter++}`);
|
|
91
|
+
process.env.MEGACOMPACT_STATE_DIR = stateDir;
|
|
92
|
+
process.env.MEGACOMPACT_DEBUG = "true";
|
|
93
|
+
if (!opts.keepThreshold)
|
|
94
|
+
process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
|
|
95
|
+
if (!opts.keepTier)
|
|
96
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
97
|
+
process.env.MEGACOMPACT_FAST_GATE_PCT = "1";
|
|
98
|
+
const handlers = {};
|
|
99
|
+
const appended = [];
|
|
100
|
+
const sendUserMessages = [];
|
|
101
|
+
const compactCalls = [];
|
|
102
|
+
const session = [
|
|
103
|
+
{ role: "user", content: "read src/vec.ts and understand the index", timestamp: 0 },
|
|
104
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "Read", id: "c1", arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: 0 },
|
|
105
|
+
{ role: "user", content: "edit src/vec.ts to add a cosine helper", timestamp: 0 },
|
|
106
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "Edit", id: "c1", arguments: {} }], api: "anthropic-messages", provider: "anthropic", model: "m", usage: { inputTokens: 1, outputTokens: 1, cacheReadTokens: 0, cacheWriteTokens: 0 }, stopReason: "tool_use", timestamp: 0 },
|
|
107
|
+
];
|
|
108
|
+
const toEntry = (m, i) => ({ type: "message", id: `e${i}`, parentId: null, timestamp: String(i), message: m });
|
|
109
|
+
const sessionManager = { getSessionId: () => "sess_ext_001", getEntries: () => session.map(toEntry), getBranch: () => session.map(toEntry) };
|
|
110
|
+
function makeCtx(over = {}) {
|
|
111
|
+
return {
|
|
112
|
+
ui: { setStatus: () => { }, notify: () => { }, select: () => { }, confirm: async () => true, input: async () => "", setWidget: () => { } },
|
|
113
|
+
mode: "tui", hasUI: true, cwd: stateDir, sessionManager,
|
|
114
|
+
modelRegistry: {}, model: undefined,
|
|
115
|
+
isIdle: () => true, isProjectTrusted: () => true,
|
|
116
|
+
signal: undefined, abort: () => { }, hasPendingMessages: () => false, shutdown: () => { },
|
|
117
|
+
getContextUsage: () => ({ tokens: 200000, contextWindow: 200000, percent: 100 }),
|
|
118
|
+
compact: (opts) => {
|
|
119
|
+
compactCalls.push(opts);
|
|
120
|
+
const _sbc = handlers["session_before_compact"];
|
|
121
|
+
if (_sbc && _sbc.length)
|
|
122
|
+
return _sbc[0]({ type: "session_before_compact", reason: "threshold", willRetry: false, signal: undefined, preparation: { firstKeptEntryId: "e2", messagesToSummarize: session.slice(0, 2), tokensBefore: 500 } }, makeCtx());
|
|
123
|
+
return undefined;
|
|
124
|
+
},
|
|
125
|
+
getSystemPrompt: () => "system base", ...over,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const pi = {
|
|
129
|
+
on: (ev, h) => { if (!handlers[ev])
|
|
130
|
+
handlers[ev] = []; handlers[ev].push(h); },
|
|
131
|
+
registerCommand: () => { }, registerTool: () => { }, registerShortcut: () => { },
|
|
132
|
+
registerFlag: () => { }, getFlag: () => undefined,
|
|
133
|
+
registerMessageRenderer: () => { }, registerEntryRenderer: () => { },
|
|
134
|
+
sendMessage: () => { }, sendUserMessage: (m) => { sendUserMessages.push(m); },
|
|
135
|
+
appendEntry: (t, d) => appended.push({ t, d }),
|
|
136
|
+
setSessionName: () => { }, getSessionName: () => undefined, setLabel: () => { },
|
|
137
|
+
exec: async () => ({ stdout: "", stderr: "", code: 0 }),
|
|
138
|
+
getActiveTools: () => [], getAllTools: () => { }, setActiveTools: () => { },
|
|
139
|
+
getCommands: () => [], setModel: async () => false,
|
|
140
|
+
getThinkingLevel: () => "off", setThinkingLevel: () => { },
|
|
141
|
+
};
|
|
142
|
+
const mod = require("../mega-compact.js");
|
|
143
|
+
mod.default(pi);
|
|
144
|
+
return {
|
|
145
|
+
stateDir, handlers, appended, compactCalls, sendUserMessages,
|
|
146
|
+
fire: async (ev, event, ctx) => { let r; for (const h of handlers[ev] || [])
|
|
147
|
+
r = await h(event, ctx); return r; },
|
|
148
|
+
ctx: makeCtx, session,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export const { classifyError: classifyErrorFn, classifyErrorDetailed: classifyErrorDetailedFn, extractErrorSignature: extractErrorSignatureFn } = require("../mega-events.js");
|
|
152
|
+
/** S38 helper: fire a turn_end with a given stopReason + optional text, using a
|
|
153
|
+
* low-pressure ctx so the durable-trim branch (ctx.compact) does NOT fire. */
|
|
154
|
+
export async function s38TurnEnd(h, stopReason, text) {
|
|
155
|
+
const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
|
|
156
|
+
const message = { role: "assistant" };
|
|
157
|
+
if (stopReason !== undefined)
|
|
158
|
+
message.stopReason = stopReason;
|
|
159
|
+
if (text)
|
|
160
|
+
message.content = text;
|
|
161
|
+
await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message }, lowCtx);
|
|
162
|
+
}
|
|
163
|
+
/** R3 helper: like s38TurnEnd but attaches a `usage` object so the classifier's
|
|
164
|
+
* 0-token poisoned-context signal is exercised (usage PRESENT with 0 tokens).
|
|
165
|
+
* Pass `tokens` > 0 to simulate a turn that reached the model. */
|
|
166
|
+
export async function s38TurnEndUsage(h, stopReason, text, tokens) {
|
|
167
|
+
const lowCtx = h.ctx({ isIdle: () => true, hasPendingMessages: () => false, getContextUsage: () => ({ tokens: 100, contextWindow: 200000, percent: 0 }) });
|
|
168
|
+
const message = { role: "assistant", usage: { inputTokens: tokens, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } };
|
|
169
|
+
if (stopReason !== undefined)
|
|
170
|
+
message.stopReason = stopReason;
|
|
171
|
+
if (text)
|
|
172
|
+
message.content = text;
|
|
173
|
+
await h.fire("turn_end", { type: "turn_end", turnIndex: 1, message }, lowCtx);
|
|
174
|
+
}
|
|
175
|
+
/** R7 helper: fire the same transient error text `count` times with turn_start
|
|
176
|
+
* + elapsed backoff between turns so each turn's nudge can fire. */
|
|
177
|
+
export async function r7RepeatTurns(h, text, count) {
|
|
178
|
+
for (let i = 0; i < count; i++) {
|
|
179
|
+
await s38TurnEnd(h, "error", text);
|
|
180
|
+
await h.fire("turn_start", { type: "turn_start", turnIndex: i + 2 }, h.ctx());
|
|
181
|
+
await new Promise((r) => setTimeout(r, 3));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** R7 helper: shared assertions for "stays transient" — no poisoned upgrade,
|
|
185
|
+
* no /clear advise, and the transient retry path actually ran. */
|
|
186
|
+
export function assertStaysTransient(h, label) {
|
|
187
|
+
assert.ok(!eventTypes(h.stateDir).includes("poisoned_context"), `${label}: no poisoned_context event (network errors never upgrade)`);
|
|
188
|
+
assert.ok(!h.sendUserMessages.some((m) => m.includes("/clear")), `${label}: no /clear advise message`);
|
|
189
|
+
assert.ok(eventTypes(h.stateDir).includes("error_retry"), `${label}: transient retry path ran (error_retry event)`);
|
|
190
|
+
}
|
|
191
|
+
/** The exact error bodies from the 2026-07-30 incident (GLM router flapping). */
|
|
192
|
+
export const R8_NO_HEALTHY_TARGET = '{"message":"No healthy target selected for alias \'hf:zai-org/GLM-4.7\'","type":"api_error"}';
|
|
193
|
+
export const R8_SOCKET_CLOSED = "All targets failed: modal/zai-org/GLM-5.1-FP8. Last error: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()"; // guardrails-allow PREVENT-PI-004: verbatim 2026-07-30 incident error text (string fixture, not a network call)
|
|
194
|
+
export const R8_TOO_MANY_CONCURRENT = 'All targets failed: modal/zai-org/GLM-5.1-FP8. Last error: {"error": "Too many concurrent requests for this model"}';
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { closeVectorIndex } from "../src/store/vectorIndex.js";
|
|
29
29
|
import { closeMemoryIndex } from "../src/store/memoryIndex.js";
|
|
30
|
-
import { loadConfig } from "./mega-config.js";
|
|
30
|
+
import { loadConfig, repoStateDir } from "./mega-config.js";
|
|
31
31
|
import { loadMegaEnv } from "./mega-runtime/env-loader.js";
|
|
32
32
|
import { MegaRuntime } from "./mega-runtime.js";
|
|
33
33
|
import { registerEventHandlers } from "./mega-events.js";
|
|
@@ -42,7 +42,17 @@ export default function (pi) {
|
|
|
42
42
|
// Load .mega-compact.env (written by /mega-setup + dashboard Setup tab) BEFORE
|
|
43
43
|
// config resolution so the wizard's choices take effect. Shell env wins over
|
|
44
44
|
// the file (see env-loader.ts). Non-fatal: missing/malformed file is a no-op.
|
|
45
|
-
|
|
45
|
+
//
|
|
46
|
+
// The dashboard Setup tab + /mega-setup write the file to the PER-REPO state
|
|
47
|
+
// dir (<repo>/.pi/mega-compact, resolved via repoStateDir), NOT the global
|
|
48
|
+
// default (~/.pi/agent/extensions/pi-mega-compact). Loading from the global
|
|
49
|
+
// dir was the root cause of the embedder-never-activates bug: the file sat in
|
|
50
|
+
// the per-repo dir, the loader read the empty global dir, and the configured
|
|
51
|
+
// embedder silently never loaded after restart. Load per-repo first (where
|
|
52
|
+
// the wizard writes), then the global dir as a legacy fallback.
|
|
53
|
+
const globalStateDir = loadConfig().stateDir;
|
|
54
|
+
loadMegaEnv(repoStateDir(process.cwd(), globalStateDir));
|
|
55
|
+
loadMegaEnv(globalStateDir);
|
|
46
56
|
const config = loadConfig();
|
|
47
57
|
// S38.9: preflight env validation — check for obviously invalid values at startup.
|
|
48
58
|
// Non-fatal: log warnings and fall back to defaults.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared harness for the mega-compact split test files.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the top of mega-compact.test.ts: the mock-pi harness that loads
|
|
5
|
+
* the compiled extension and lets tests fire event/command handlers against it.
|
|
6
|
+
* Each split test file imports `harness()` (and `require`/`baseTmp` where
|
|
7
|
+
* needed) so the per-test isolated state-dir environment is preserved.
|
|
8
|
+
*
|
|
9
|
+
* Note: relative paths are one directory deeper than the original file, hence
|
|
10
|
+
* `../mega-compact.js` for the extension entry and `../../src/...` for src.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { closeVectorIndex } from "../../src/store/vectorIndex.js";
|
|
17
|
+
export const require = createRequire(import.meta.url);
|
|
18
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-ext-"));
|
|
19
|
+
// Isolate the machine-wide repo index so test runs (which call bindRepo ->
|
|
20
|
+
// upsertRepoRegistry) never pollute the developer's real ~/.mega-compact-index.
|
|
21
|
+
process.env.MEGACOMPACT_INDEX_DIR = join(baseTmp, "index");
|
|
22
|
+
// This end-to-end extension test drives the compact/recall flow through the
|
|
23
|
+
// synchronous node:sqlite store (the authoritative path). The redundant PGlite
|
|
24
|
+
// HNSW cross-repo index is additive and exercised in its own dedicated test
|
|
25
|
+
// (store/vectorIndex.test.ts); starting the WASM worker here only risks the
|
|
26
|
+
// teardown stall that hangs node --test on some machines. Disable it for this
|
|
27
|
+
// suite — the extension behavior under test is identical with it off.
|
|
28
|
+
process.env.MEGACOMPACT_PGLITE_DISABLED = "1";
|
|
29
|
+
export const baseTmpDir = baseTmp;
|
|
30
|
+
export { closeVectorIndex };
|
|
31
|
+
let counter = 0;
|
|
32
|
+
/** Build a mock pi + ctx and load the extension into them. */
|
|
33
|
+
export function harness(opts = {}) {
|
|
34
|
+
const stateDir = join(baseTmp, `run-${counter++}`);
|
|
35
|
+
process.env.MEGACOMPACT_STATE_DIR = stateDir;
|
|
36
|
+
process.env.MEGACOMPACT_DEBUG = "true";
|
|
37
|
+
// Low threshold so the auto-trigger gate trips on our small mock context.
|
|
38
|
+
// Tier tests opt out (keepTier/keepThreshold) so they can drive the real
|
|
39
|
+
// tier resolution instead of the forced 50-token threshold.
|
|
40
|
+
if (!opts.keepThreshold)
|
|
41
|
+
process.env.MEGACOMPACT_THRESHOLD_TOKENS = "50";
|
|
42
|
+
if (!opts.keepTier)
|
|
43
|
+
delete process.env.MEGACOMPACT_TIER;
|
|
44
|
+
process.env.MEGACOMPACT_FAST_GATE_PCT = "1";
|
|
45
|
+
const handlers = {};
|
|
46
|
+
const commands = {};
|
|
47
|
+
const appended = [];
|
|
48
|
+
let statusKey;
|
|
49
|
+
let statusText;
|
|
50
|
+
const notifies = [];
|
|
51
|
+
const compactCalls = [];
|
|
52
|
+
const sendUserMessages = [];
|
|
53
|
+
// Minimal AgentMessage factory for the session we project into the extension.
|
|
54
|
+
function msg(role, text, toolName) {
|
|
55
|
+
if (role === "assistant" && toolName) {
|
|
56
|
+
return {
|
|
57
|
+
role: "assistant",
|
|
58
|
+
content: [
|
|
59
|
+
{ type: "toolCall", name: toolName, id: "c1", arguments: {} },
|
|
60
|
+
],
|
|
61
|
+
api: "anthropic-messages",
|
|
62
|
+
provider: "anthropic",
|
|
63
|
+
model: "m",
|
|
64
|
+
usage: {
|
|
65
|
+
inputTokens: 1,
|
|
66
|
+
outputTokens: 1,
|
|
67
|
+
cacheReadTokens: 0,
|
|
68
|
+
cacheWriteTokens: 0,
|
|
69
|
+
},
|
|
70
|
+
stopReason: "tool_use",
|
|
71
|
+
timestamp: 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (role === "toolResult" && toolName) {
|
|
75
|
+
return {
|
|
76
|
+
role: "toolResult",
|
|
77
|
+
toolCallId: "c1",
|
|
78
|
+
toolName,
|
|
79
|
+
content: [{ type: "text", text }],
|
|
80
|
+
isError: false,
|
|
81
|
+
timestamp: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
role: "user",
|
|
86
|
+
content: text,
|
|
87
|
+
timestamp: 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const session = [
|
|
91
|
+
msg("user", "read src/vec.ts and understand the index"),
|
|
92
|
+
msg("assistant", "ok", "Read"),
|
|
93
|
+
msg("user", "edit src/vec.ts to add a cosine helper"),
|
|
94
|
+
msg("assistant", "ok", "Edit"),
|
|
95
|
+
msg("user", "now fix the dedupe bug in store.ts"),
|
|
96
|
+
msg("assistant", "ok", "Edit"),
|
|
97
|
+
msg("user", "actually we should add recall sorting too"),
|
|
98
|
+
msg("assistant", "ok", "Edit"),
|
|
99
|
+
];
|
|
100
|
+
// Mirror the REAL SessionManager: getEntries() returns SessionEntry objects,
|
|
101
|
+
// which the extension projects to messages via the SDK's
|
|
102
|
+
// sessionEntryToContextMessages(entry). The harness must use the same shape
|
|
103
|
+
// (type:"message" with a .message) or recentUserQuery() silently queries "".
|
|
104
|
+
const toEntry = (m, i) => ({
|
|
105
|
+
type: "message",
|
|
106
|
+
id: `e${i}`,
|
|
107
|
+
parentId: null,
|
|
108
|
+
timestamp: String(i),
|
|
109
|
+
message: m,
|
|
110
|
+
});
|
|
111
|
+
const sessionManager = {
|
|
112
|
+
getSessionId: () => "sess_ext_001",
|
|
113
|
+
getEntries: () => session.map(toEntry),
|
|
114
|
+
// Faithful mock: getBranch() returns the current branch's entries, which
|
|
115
|
+
// piCompactWouldNoop() reads to predict whether ctx.compact() would no-op.
|
|
116
|
+
getBranch: () => session.map(toEntry),
|
|
117
|
+
};
|
|
118
|
+
function makeCtx(over = {}) {
|
|
119
|
+
return {
|
|
120
|
+
ui: {
|
|
121
|
+
setStatus: (k, t) => {
|
|
122
|
+
statusKey = k;
|
|
123
|
+
statusText = t;
|
|
124
|
+
},
|
|
125
|
+
notify: (s) => notifies.push(s),
|
|
126
|
+
select: () => { },
|
|
127
|
+
confirm: async () => true,
|
|
128
|
+
input: async () => "",
|
|
129
|
+
setWidget: () => { },
|
|
130
|
+
},
|
|
131
|
+
mode: "tui",
|
|
132
|
+
hasUI: true,
|
|
133
|
+
cwd: stateDir,
|
|
134
|
+
sessionManager,
|
|
135
|
+
modelRegistry: {},
|
|
136
|
+
model: undefined,
|
|
137
|
+
isIdle: () => true,
|
|
138
|
+
isProjectTrusted: () => true,
|
|
139
|
+
signal: undefined,
|
|
140
|
+
abort: () => { },
|
|
141
|
+
hasPendingMessages: () => false,
|
|
142
|
+
shutdown: () => { },
|
|
143
|
+
getContextUsage: () => ({
|
|
144
|
+
tokens: 200000,
|
|
145
|
+
contextWindow: 200000,
|
|
146
|
+
percent: 100,
|
|
147
|
+
}),
|
|
148
|
+
// Faithful mock: ctx.compact() starts pi's flow, which fires the
|
|
149
|
+
// session_before_compact handler (where WE supply the durable trim).
|
|
150
|
+
compact: (opts) => {
|
|
151
|
+
compactCalls.push(opts);
|
|
152
|
+
const _sbc = handlers["session_before_compact"];
|
|
153
|
+
if (_sbc && _sbc.length) {
|
|
154
|
+
return _sbc[0]({
|
|
155
|
+
type: "session_before_compact",
|
|
156
|
+
reason: "threshold",
|
|
157
|
+
willRetry: false,
|
|
158
|
+
signal: undefined,
|
|
159
|
+
// pi computed the cut honoring anchor floor + tool-pair (PREVENT-PI-002);
|
|
160
|
+
// our handler reuses it as firstKeptEntryId.
|
|
161
|
+
preparation: {
|
|
162
|
+
firstKeptEntryId: "e2",
|
|
163
|
+
messagesToSummarize: session.slice(0, 2),
|
|
164
|
+
tokensBefore: 500,
|
|
165
|
+
},
|
|
166
|
+
}, makeCtx());
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
},
|
|
170
|
+
getSystemPrompt: () => "system base",
|
|
171
|
+
...over,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const pi = {
|
|
175
|
+
on: (ev, h) => {
|
|
176
|
+
if (!handlers[ev])
|
|
177
|
+
handlers[ev] = [];
|
|
178
|
+
handlers[ev].push(h);
|
|
179
|
+
},
|
|
180
|
+
registerCommand: (name, opts) => {
|
|
181
|
+
commands[name] = opts;
|
|
182
|
+
},
|
|
183
|
+
registerTool: () => { },
|
|
184
|
+
registerShortcut: () => { },
|
|
185
|
+
registerFlag: () => { },
|
|
186
|
+
getFlag: () => undefined,
|
|
187
|
+
registerMessageRenderer: () => { },
|
|
188
|
+
registerEntryRenderer: () => { },
|
|
189
|
+
sendMessage: (_m) => { },
|
|
190
|
+
sendUserMessage: (m) => {
|
|
191
|
+
sendUserMessages.push(m);
|
|
192
|
+
},
|
|
193
|
+
appendEntry: (t, d) => appended.push({ t, d }),
|
|
194
|
+
setSessionName: () => { },
|
|
195
|
+
getSessionName: () => undefined,
|
|
196
|
+
setLabel: () => { },
|
|
197
|
+
exec: async () => ({ stdout: "", stderr: "", code: 0 }),
|
|
198
|
+
getActiveTools: () => [],
|
|
199
|
+
getAllTools: () => [],
|
|
200
|
+
setActiveTools: () => { },
|
|
201
|
+
getCommands: () => [],
|
|
202
|
+
setModel: async () => false,
|
|
203
|
+
getThinkingLevel: () => "off",
|
|
204
|
+
setThinkingLevel: () => { },
|
|
205
|
+
};
|
|
206
|
+
// Import the compiled extension (same dist dir as this test's parent).
|
|
207
|
+
const mod = require("../mega-compact.js");
|
|
208
|
+
mod.default(pi);
|
|
209
|
+
return {
|
|
210
|
+
stateDir,
|
|
211
|
+
handlers,
|
|
212
|
+
commands,
|
|
213
|
+
appended,
|
|
214
|
+
get status() {
|
|
215
|
+
return { statusKey, statusText };
|
|
216
|
+
},
|
|
217
|
+
notifies,
|
|
218
|
+
compactCalls,
|
|
219
|
+
sendUserMessages,
|
|
220
|
+
fire: async (ev, event, ctx) => { let r; for (const h of handlers[ev] || [])
|
|
221
|
+
r = await h(event, ctx); return r; },
|
|
222
|
+
ctx: makeCtx,
|
|
223
|
+
session,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { piCompactWouldNoop } from "../../mega-pipeline.js";
|
|
2
|
+
import { safeSendUserMessage } from "../send-safe.js";
|
|
3
|
+
/** Handle the `agent_end` pi event. Non-fatal end-to-end. */
|
|
4
|
+
export async function handleAgentEnd(ctx, pi, runtime, config) {
|
|
5
|
+
runtime.activeAgents = Math.max(0, runtime.activeAgents - 1);
|
|
6
|
+
runtime.dashboard.event("agent_end", {
|
|
7
|
+
activeAgents: runtime.activeAgents,
|
|
8
|
+
});
|
|
9
|
+
if (runtime.activeAgents > 0) {
|
|
10
|
+
runtime.setStatus(ctx, `mega-compact: ▶ ${runtime.activeAgents} agent${runtime.activeAgents === 1 ? "" : "s"}`);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
runtime.setStatus(ctx, config.auto ? "mega-compact: ready" : "mega-compact: manual only");
|
|
14
|
+
}
|
|
15
|
+
// S16 continuation fallback: if the turn settled idle right after a live-trim
|
|
16
|
+
// compaction AND there is queued work AND we haven't nudged recently, nudge
|
|
17
|
+
// once so the agent continues (the live trim should make this rare). Guarded
|
|
18
|
+
// to never busy-loop: one nudge per 30s, only when truly idle + queued.
|
|
19
|
+
if ((config.auto || config.autoContinueLengthStop) &&
|
|
20
|
+
runtime.activeAgents === 0) {
|
|
21
|
+
try {
|
|
22
|
+
const idle = ctx.isIdle?.() ?? true;
|
|
23
|
+
const queued = ctx.hasPendingMessages?.() ?? false;
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
// S38.5: read LIVE pressure from ctx.getContextUsage() instead of the
|
|
26
|
+
// stale runtime.lastCtxTokens (only updated by the `context` event).
|
|
27
|
+
// agent_end may fire without a preceding context event this turn (e.g.
|
|
28
|
+
// a sub-agent settling), so the cached value can be null/stale and the
|
|
29
|
+
// durable-trim branch would be unreachable. ctx.getContextUsage() is the
|
|
30
|
+
// authoritative live reading (mirrors context-handler.ts:86). Fall back
|
|
31
|
+
// to the cached value only if the ctx omits it.
|
|
32
|
+
const liveUsage = ctx.getContextUsage?.();
|
|
33
|
+
const liveTokens = typeof liveUsage?.tokens === "number"
|
|
34
|
+
? liveUsage.tokens
|
|
35
|
+
: (runtime.lastCtxTokens ?? 0);
|
|
36
|
+
// Keep the cache fresh for snapshot()/diag regardless of which source we use.
|
|
37
|
+
if (typeof liveUsage?.tokens === "number") {
|
|
38
|
+
runtime.lastCtxTokens = liveUsage.tokens;
|
|
39
|
+
}
|
|
40
|
+
if (typeof liveUsage?.percent === "number") {
|
|
41
|
+
runtime.lastCtxPercent = liveUsage.percent;
|
|
42
|
+
}
|
|
43
|
+
if (typeof liveUsage?.contextWindow === "number") {
|
|
44
|
+
runtime.lastCtxWindow = liveUsage.contextWindow;
|
|
45
|
+
}
|
|
46
|
+
// DIAG (team-run relief): surface whether the agent is idle + over
|
|
47
|
+
// threshold at agent_end so we can see if a mid-run durable-trim trigger
|
|
48
|
+
// *should* have fired but didn't.
|
|
49
|
+
const overThreshold = liveTokens >= runtime.effectiveThreshold;
|
|
50
|
+
runtime.diagAgentEndIdle++;
|
|
51
|
+
runtime.logger.info("agent-end-idle", {
|
|
52
|
+
sessionId: runtime.rt.sessionId,
|
|
53
|
+
idle,
|
|
54
|
+
queued,
|
|
55
|
+
overThreshold,
|
|
56
|
+
ctxPct: runtime.lastCtxPercent,
|
|
57
|
+
ctxTokens: runtime.lastCtxTokens,
|
|
58
|
+
thresholdTokens: config.thresholdTokens,
|
|
59
|
+
wouldNudge: idle &&
|
|
60
|
+
(queued || overThreshold) &&
|
|
61
|
+
now >= runtime.resumeNudgeUntil,
|
|
62
|
+
});
|
|
63
|
+
// S16+S24: MID-RUN DURABLE TRIM. During a long team run (sub-agents),
|
|
64
|
+
// pi's native durable compaction only fires from _checkCompaction at
|
|
65
|
+
// PARENT settle (agent-session.js:760/844), so the on-disk transcript +
|
|
66
|
+
// context meter balloon to ~150k and never relieve until the very end
|
|
67
|
+
// ("compacts but doesn't resume"). agent_end with activeAgents===0 is a
|
|
68
|
+
// SAFE, settled point: calling ctx.compact() here does NOT abort an
|
|
69
|
+
// in-flight turn (the S16 danger is only mid-turn). ctx.compact() runs
|
|
70
|
+
// pi's flow, which fires our session_before_compact handler to supply
|
|
71
|
+
// the durable trim (truncates the transcript from firstKeptEntryId).
|
|
72
|
+
// Guarded three ways: only when truly idle + over threshold, only when
|
|
73
|
+
// pi would actually compact (piCompactWouldNoop skips the user-facing
|
|
74
|
+
// no-op throw), and debounced (one durable trim per 2s) to avoid
|
|
75
|
+
// thrashing the transcript while sub-agents keep settling.
|
|
76
|
+
//
|
|
77
|
+
// FIX "compacts but doesn't resume": the manual ctx.compact() path
|
|
78
|
+
// STOPS the agent loop (agent-session.js:1345). The old resume-nudge
|
|
79
|
+
// was gated on `queued`, so when a sub-agent settled with no
|
|
80
|
+
// *immediately* queued message, the trim fired but the nudge did not,
|
|
81
|
+
// and the (stopped) session hung. The trim still fires on
|
|
82
|
+
// `idle && overThreshold` — we intentionally do NOT add a `!queued`
|
|
83
|
+
// guard, because that would suppress mid-run relief exactly during
|
|
84
|
+
// team-run waves where queued is usually true and relief is needed
|
|
85
|
+
// most. Instead we DECOUPLE the nudge from `queued`: after a durable
|
|
86
|
+
// trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
|
|
87
|
+
let didDurableTrim = false;
|
|
88
|
+
if (config.auto &&
|
|
89
|
+
idle &&
|
|
90
|
+
overThreshold &&
|
|
91
|
+
now >= runtime.debounceUntil) {
|
|
92
|
+
// COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
|
|
93
|
+
// NATIVE auto-compaction just fired (or is in-flight). pi emits
|
|
94
|
+
// agent_end BEFORE its own _checkCompaction (per its docstring:
|
|
95
|
+
// "Called after agent_end and before prompt submission"), so a
|
|
96
|
+
// synchronous `piCompactWouldNoop` branch check misses a native
|
|
97
|
+
// compaction that hasn't appended its entry yet — calling
|
|
98
|
+
// ctx.compact() then races with pi and throws "Already compacted"
|
|
99
|
+
// to the user. The `lastNativeCompactAt` cooldown (updated by the
|
|
100
|
+
// session_compact listener for EVERY compaction, native or
|
|
101
|
+
// extension-supplied) closes that race window.
|
|
102
|
+
//
|
|
103
|
+
// S38.5: strict race guard widens the cooldown 10s -> 30s AND defers
|
|
104
|
+
// ctx.compact() via setTimeout(500) with a re-check, so pi's
|
|
105
|
+
// about-to-run native _checkCompaction can append its `compaction`
|
|
106
|
+
// branch entry first (closes the first-race-in-burst window). Gated
|
|
107
|
+
// by MEGACOMPACT_RACE_GUARD_STRICT (default true); false reverts to
|
|
108
|
+
// the v0.7.4 synchronous 10s guard. Mirrors the legacy path in
|
|
109
|
+
// context-handler.ts:258-287 so both call sites stay in sync.
|
|
110
|
+
const cooldownMs = config.raceGuardStrict ? 30_000 : 10_000;
|
|
111
|
+
const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
112
|
+
// CRITICAL-OVER ESCAPE HATCH: when context is critically over the
|
|
113
|
+
// window (>= 90%), force the durable ctx.compact() even if
|
|
114
|
+
// piCompactWouldNoop says it would no-op ("Already compacted").
|
|
115
|
+
// Without this, the on-disk transcript never truncates and the
|
|
116
|
+
// session stays overflowed on every resume — the "Already
|
|
117
|
+
// compacted" + overflow death-spiral (2026-08-01 incident). The
|
|
118
|
+
// cooldown still applies (we don't spam compact), but the no-op
|
|
119
|
+
// gate is bypassed when critical. pi's ctx.compact() may throw
|
|
120
|
+
// "Already compacted" in the truly-no-op case — wrapped in
|
|
121
|
+
// try/catch so the throw is non-fatal.
|
|
122
|
+
const criticalOver = (runtime.lastCtxPercent ?? 0) >= 90;
|
|
123
|
+
if (sinceCompact < cooldownMs) {
|
|
124
|
+
runtime.diagAgentEndDurableSkipRecent++;
|
|
125
|
+
}
|
|
126
|
+
else if (criticalOver || !piCompactWouldNoop(ctx)) {
|
|
127
|
+
runtime.debounceUntil = now + 2000;
|
|
128
|
+
runtime.diagAgentEndDurable++;
|
|
129
|
+
runtime.logger.info("agent-end-durable-trigger", {
|
|
130
|
+
sessionId: runtime.rt.sessionId,
|
|
131
|
+
ctxTokens: runtime.lastCtxTokens,
|
|
132
|
+
thresholdTokens: config.thresholdTokens,
|
|
133
|
+
queued,
|
|
134
|
+
});
|
|
135
|
+
if (config.raceGuardStrict) {
|
|
136
|
+
// Strict: defer ctx.compact() with a re-check so pi's
|
|
137
|
+
// about-to-run native _checkCompaction can append its
|
|
138
|
+
// `compaction` branch entry first. setTimeout(500) — pi's
|
|
139
|
+
// compaction-summary append is async I/O, so queueMicrotask
|
|
140
|
+
// would re-check before it lands.
|
|
141
|
+
const stamp = runtime.rt.lastNativeCompactAt;
|
|
142
|
+
const liveSid = runtime.rt.sessionId;
|
|
143
|
+
const liveCritical = criticalOver;
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
try {
|
|
146
|
+
if (runtime.rt.sessionId !== liveSid)
|
|
147
|
+
return; // session reset
|
|
148
|
+
// IMPORTANT: recompute Date.now() inside the timeout.
|
|
149
|
+
// `now` above is the durable-trim handler's timestamp and can be
|
|
150
|
+
// ~500ms stale by the time this callback runs.
|
|
151
|
+
const since2 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
152
|
+
if (runtime.rt.lastNativeCompactAt !== stamp &&
|
|
153
|
+
since2 < cooldownMs)
|
|
154
|
+
return;
|
|
155
|
+
// CRITICAL-OVER ESCAPE HATCH: bypass the no-op gate when
|
|
156
|
+
// critical (captured at handler time) so the durable trim fires.
|
|
157
|
+
if (!liveCritical && piCompactWouldNoop(ctx))
|
|
158
|
+
return;
|
|
159
|
+
ctx.compact({
|
|
160
|
+
customInstructions: undefined,
|
|
161
|
+
}); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; deferred + re-validated. May throw "Already compacted" when the escape hatch forces it — caught below.
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* non-fatal: "Already compacted" throws are expected when the critical-over hatch forces a no-op compact */
|
|
165
|
+
}
|
|
166
|
+
}, 500);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
try {
|
|
170
|
+
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. May throw "Already compacted" when the critical-over escape hatch forces it — caught.
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* non-fatal */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
didDurableTrim = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Restart the agent after a mid-run durable trim (which stopped it), or
|
|
180
|
+
// when it settled idle with queued work. Decoupled from `queued` for the
|
|
181
|
+
// durable-trim case — see FIX note above. Debounced 30s; never blocks.
|
|
182
|
+
const lengthStop = config.autoContinueLengthStop && runtime.rt.lengthStopPending;
|
|
183
|
+
if (idle &&
|
|
184
|
+
now >= runtime.resumeNudgeUntil &&
|
|
185
|
+
((config.auto && (didDurableTrim || queued)) || lengthStop)) {
|
|
186
|
+
runtime.resumeNudgeUntil = now + 30_000;
|
|
187
|
+
if (runtime.rt.lengthStopPending) {
|
|
188
|
+
runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
|
|
189
|
+
runtime.dashboard.event("length_stop_continue", {
|
|
190
|
+
turnIndex: runtime.currentTurn,
|
|
191
|
+
});
|
|
192
|
+
runtime.logger.info("length_stop_continue", {
|
|
193
|
+
sessionId: runtime.rt.sessionId,
|
|
194
|
+
didDurableTrim,
|
|
195
|
+
queued,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// S28: when a length-stop (max-output-token truncation) fired WITHOUT a durable trim, do NOT claim a compaction happened
|
|
199
|
+
// (nothing was compacted on the low-pressure length path). Branch the message so the nudge matches reality.
|
|
200
|
+
const nudgeMsg = lengthStop && !didDurableTrim
|
|
201
|
+
? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
|
|
202
|
+
: "[mega-compact] continue from the compacted context above.";
|
|
203
|
+
await safeSendUserMessage(pi, nudgeMsg);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* non-fatal: a failed nudge never blocks */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
runtime.snapshot(ctx);
|
|
211
|
+
}
|