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