pi-mega-compact 0.9.0 → 0.9.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/README.md +1 -0
- package/dist/extensions/mega-compact.js +16 -1
- package/dist/extensions/mega-config.js +1 -0
- package/dist/extensions/mega-runtime/game-state.js +7 -0
- package/dist/extensions/mega-runtime/reset-runtime.js +8 -0
- package/dist/extensions/mega-runtime/runtime.js +12 -50
- package/dist/extensions/mega-shutdown-widget.test.js +121 -0
- package/dist/src/compact.js +4 -2
- package/dist/src/memoryOps.test.js +16 -0
- package/dist/src/store/memoryIndex.js +29 -7
- package/dist/src/store/pgOpenGuard.js +83 -0
- package/dist/src/store/pgOpenGuard.test.js +74 -0
- package/dist/src/store/vectorIndex.js +30 -8
- package/extensions/mega-compact.ts +16 -1
- package/extensions/mega-config.ts +8 -0
- package/extensions/mega-runtime/game-state.ts +9 -0
- package/extensions/mega-runtime/reset-runtime.ts +88 -0
- package/extensions/mega-runtime/runtime.ts +27 -59
- package/extensions/mega-shutdown-widget.test.ts +141 -0
- package/package.json +1 -1
- package/src/compact.ts +209 -174
- package/src/memoryOps.test.ts +16 -0
- package/src/store/memoryIndex.ts +34 -8
- package/src/store/pgOpenGuard.test.ts +89 -0
- package/src/store/pgOpenGuard.ts +93 -0
- package/src/store/vectorIndex.ts +35 -9
|
@@ -147,6 +147,13 @@ export interface MegaConfig {
|
|
|
147
147
|
* a hit whose summary is ≥ dedupSim similar to a live message — "dedupe on
|
|
148
148
|
* inline/read" so we never re-inject context already resident. */
|
|
149
149
|
windowDedupe: boolean;
|
|
150
|
+
/** Render the above-editor TUI widget (MEGACOMPACT_TUI_WIDGET). Default true.
|
|
151
|
+
* Set to 0 to suppress the panel entirely — the widget is a persistent,
|
|
152
|
+
* animated, full-width region that repaints on its own cadence, which fights
|
|
153
|
+
* terminals where the user drives scrollback themselves (notably pi running
|
|
154
|
+
* inside a Neovim `:terminal`, where every repaint yanks the view back to
|
|
155
|
+
* the bottom). Compaction is unaffected; only the panel is suppressed. */
|
|
156
|
+
tuiWidget: boolean;
|
|
150
157
|
debug: boolean;
|
|
151
158
|
}
|
|
152
159
|
|
|
@@ -292,6 +299,7 @@ export function loadConfig(): MegaConfig {
|
|
|
292
299
|
memoryReviewInterval: envFlag("MEGACOMPACT_MEMORY_REVIEW_INTERVAL", 10),
|
|
293
300
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
|
294
301
|
windowDedupe: envBool("MEGACOMPACT_WINDOW_DEDUPE", true),
|
|
302
|
+
tuiWidget: envBool("MEGACOMPACT_TUI_WIDGET", true),
|
|
295
303
|
debug: envBool("MEGACOMPACT_DEBUG", false),
|
|
296
304
|
};
|
|
297
305
|
}
|
|
@@ -14,6 +14,8 @@ import { disposePerf, type PerfContext } from "./perf.js";
|
|
|
14
14
|
|
|
15
15
|
export interface GameWatcherLike {
|
|
16
16
|
close(): void;
|
|
17
|
+
/** fs.FSWatcher has it; test doubles need not. */
|
|
18
|
+
unref?(): unknown;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface GameStateContext {
|
|
@@ -142,6 +144,13 @@ export function ensureGameStateWatcherImpl(self: GameStateContext, view: GameSta
|
|
|
142
144
|
}
|
|
143
145
|
},
|
|
144
146
|
);
|
|
147
|
+
// The watcher is a cache-eviction convenience, never a reason to stay
|
|
148
|
+
// alive: an active+referenced fs_event handle holds node's event loop
|
|
149
|
+
// open, so a runtime that was never dispose()d (every extension test, and
|
|
150
|
+
// any `pi -p` run that skips session_shutdown) hangs the process after all
|
|
151
|
+
// work is done. unref'd like the perf interval — while pi runs there are
|
|
152
|
+
// always other referenced handles, so the watcher still fires normally.
|
|
153
|
+
self.gameStateWatcher.unref?.();
|
|
145
154
|
self.gameStateWatchDir = self.currentStateDir;
|
|
146
155
|
} catch {
|
|
147
156
|
/* non-fatal: missing dir / platform issue — next snapshot re-queries */
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reset-runtime.ts — extracted `MegaRuntime.resetRuntime()`: the per-session
|
|
3
|
+
* state reset used by the session_start / session_tree handlers. The class
|
|
4
|
+
* keeps a thin `resetRuntimeImpl(this, sessionId)` delegate so every call
|
|
5
|
+
* site is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Follows the same context-interface + free-function + thin-delegate pattern as
|
|
8
|
+
* effects.ts / game-state.ts / capture-model.ts / bind-repo.ts / perf.ts /
|
|
9
|
+
* runtime-helpers.ts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { normalizeSessionId } from "../../src/store.js";
|
|
13
|
+
import type { TickerEntry } from "./widget.js";
|
|
14
|
+
import type { GameState } from "../../src/store/sqlite.js";
|
|
15
|
+
import type { SessionRuntime } from "./helpers.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------- types
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The slice of `MegaRuntime` resetRuntime mutates. `trimCache` is typed
|
|
21
|
+
* `unknown` — this function only ever *clears* it, so the precise
|
|
22
|
+
* snapshot-cache shape does not need to be imported.
|
|
23
|
+
*/
|
|
24
|
+
export interface ResetRuntimeContext {
|
|
25
|
+
rt: SessionRuntime;
|
|
26
|
+
trimCache: unknown;
|
|
27
|
+
ticker: TickerEntry[];
|
|
28
|
+
cachedGameState: GameState | undefined;
|
|
29
|
+
statusKey: string | undefined;
|
|
30
|
+
activeAgents: number;
|
|
31
|
+
currentTurn: number;
|
|
32
|
+
lastActivityAt: number;
|
|
33
|
+
tierTrace: string | undefined;
|
|
34
|
+
pulsing: boolean;
|
|
35
|
+
savedGoal: number;
|
|
36
|
+
lastWhy: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --------------------------------------------------------------- resetRuntime
|
|
40
|
+
|
|
41
|
+
export function resetRuntimeImpl(
|
|
42
|
+
self: ResetRuntimeContext,
|
|
43
|
+
sessionId: string | undefined,
|
|
44
|
+
): void {
|
|
45
|
+
const sid = normalizeSessionId(sessionId);
|
|
46
|
+
if (self.rt.sessionId === sid && self.rt.persistedThisSession) return; // same session, keep checkpoint memory
|
|
47
|
+
self.rt = {
|
|
48
|
+
sessionId: sid,
|
|
49
|
+
persistedThisSession: false,
|
|
50
|
+
lastCheckpointId: undefined,
|
|
51
|
+
lastCompactedFrom: 0,
|
|
52
|
+
lastCompactedTokens: 0,
|
|
53
|
+
dedupSkips: 0,
|
|
54
|
+
dedupAttempts: 0,
|
|
55
|
+
tokensSaved: 0,
|
|
56
|
+
lastCompactAt: null,
|
|
57
|
+
lastNativeCompactAt: null,
|
|
58
|
+
compactCount: 0,
|
|
59
|
+
recallInjections: 0,
|
|
60
|
+
cacheHitTokens: 0,
|
|
61
|
+
lengthStopPending: false,
|
|
62
|
+
errorRetryCount: 0,
|
|
63
|
+
errorRetryUntil: 0,
|
|
64
|
+
consecutiveErrors: 0,
|
|
65
|
+
lastErrorRetryAt: 0,
|
|
66
|
+
retryNudgePending: false,
|
|
67
|
+
errorRetrySessionCount: 0,
|
|
68
|
+
lastErrorText: undefined,
|
|
69
|
+
errorTextRepeatCount: 0,
|
|
70
|
+
poisonedAdviseSent: false,
|
|
71
|
+
poisonedCompactSignatures: new Set(),
|
|
72
|
+
poisonedCount: 0,
|
|
73
|
+
};
|
|
74
|
+
self.trimCache = null; // v0.8.6: never replay a stale trim into a new session
|
|
75
|
+
self.statusKey = undefined;
|
|
76
|
+
self.activeAgents = 0;
|
|
77
|
+
self.currentTurn = 0;
|
|
78
|
+
self.lastActivityAt = 0;
|
|
79
|
+
self.tierTrace = undefined;
|
|
80
|
+
self.ticker.length = 0;
|
|
81
|
+
self.pulsing = false;
|
|
82
|
+
self.savedGoal = 50_000;
|
|
83
|
+
self.lastWhy = undefined;
|
|
84
|
+
// S31 audit P2: symmetry with bindRepo — a reset can coincide with a context
|
|
85
|
+
// that re-binds the repo, so drop the memo too. Cheap; the next
|
|
86
|
+
// getCachedGameState() re-queries lazily.
|
|
87
|
+
self.cachedGameState = undefined;
|
|
88
|
+
}
|
|
@@ -20,11 +20,22 @@ import { VectorStore } from "../../src/vectorStore.js";
|
|
|
20
20
|
import type { toEngineMessages } from "../../src/adapt.js";
|
|
21
21
|
import { normalizeSessionId } from "../../src/store.js";
|
|
22
22
|
import { Logger } from "../../src/log.js";
|
|
23
|
-
import type {
|
|
24
|
-
|
|
23
|
+
import type {
|
|
24
|
+
ModelSnapshot,
|
|
25
|
+
GameState,
|
|
26
|
+
} from "../../src/store/sqlite.js";
|
|
27
|
+
import type {
|
|
28
|
+
MegaConfig,
|
|
29
|
+
PressureBand,
|
|
30
|
+
} from "../mega-config.js";
|
|
25
31
|
import { Dashboard } from "../mega-dashboard.js";
|
|
26
|
-
import type {
|
|
27
|
-
|
|
32
|
+
import type {
|
|
33
|
+
SessionRuntime,
|
|
34
|
+
} from "./helpers.js";
|
|
35
|
+
import type {
|
|
36
|
+
TickerEntry,
|
|
37
|
+
WidgetData,
|
|
38
|
+
} from "./widget.js";
|
|
28
39
|
import {
|
|
29
40
|
ensureGameStateWatcherImpl,
|
|
30
41
|
getCachedGameStateImpl,
|
|
@@ -48,6 +59,7 @@ import {
|
|
|
48
59
|
effectiveThresholdImpl,
|
|
49
60
|
pressureBandImpl,
|
|
50
61
|
} from "./pressure-getters.js";
|
|
62
|
+
import { resetRuntimeImpl } from "./reset-runtime.js";
|
|
51
63
|
import { appendEventImpl } from "./append-event.js";
|
|
52
64
|
import { getStateDirImpl } from "./get-state-dir.js";
|
|
53
65
|
import { renderWidgetImpl } from "./render-widget.js";
|
|
@@ -83,7 +95,6 @@ export class MegaRuntime {
|
|
|
83
95
|
errorRetryCount: 0,
|
|
84
96
|
errorRetryUntil: 0,
|
|
85
97
|
consecutiveErrors: 0,
|
|
86
|
-
// R1-R3 (retry redesign): in-flight dedup, session cap, poisoned-context state.
|
|
87
98
|
lastErrorRetryAt: 0,
|
|
88
99
|
retryNudgePending: false,
|
|
89
100
|
errorRetrySessionCount: 0,
|
|
@@ -122,12 +133,7 @@ export class MegaRuntime {
|
|
|
122
133
|
* compaction start). Threaded into widgetData as `activeEffect`; the widget
|
|
123
134
|
* computes the per-frame phase from startedAt vs Date.now() (non-expired).
|
|
124
135
|
* Null when idle/expired. */
|
|
125
|
-
activeEffect: {
|
|
126
|
-
type: "pulse" | "flash";
|
|
127
|
-
role: "accent" | "mega" | "red";
|
|
128
|
-
startedAt: number;
|
|
129
|
-
durationMs: number;
|
|
130
|
-
} | null = null;
|
|
136
|
+
activeEffect: { type: "pulse" | "flash"; role: "accent" | "mega" | "red"; startedAt: number; durationMs: number } | null = null;
|
|
131
137
|
megaCacheFlarePct = 0;
|
|
132
138
|
levelUpFlare = false;
|
|
133
139
|
lastLevel = 0;
|
|
@@ -307,8 +313,14 @@ export class MegaRuntime {
|
|
|
307
313
|
}
|
|
308
314
|
|
|
309
315
|
/** Width-aware above-editor widget factory registration — thin delegate to
|
|
310
|
-
* `renderWidgetImpl` (render-widget.ts).
|
|
316
|
+
* `renderWidgetImpl` (render-widget.ts).
|
|
317
|
+
*
|
|
318
|
+
* Gated on `config.tuiWidget` (MEGACOMPACT_TUI_WIDGET=0 to disable). This
|
|
319
|
+
* is the single chokepoint every caller funnels through, so returning here
|
|
320
|
+
* means setWidget is never called and the panel is never registered — as
|
|
321
|
+
* opposed to registering an empty one, which would still occupy a row. */
|
|
311
322
|
renderWidget(ctx: ExtensionContext): void {
|
|
323
|
+
if (!this.config.tuiWidget) return;
|
|
312
324
|
renderWidgetImpl(this, ctx);
|
|
313
325
|
}
|
|
314
326
|
|
|
@@ -318,54 +330,10 @@ export class MegaRuntime {
|
|
|
318
330
|
setStatusImpl(this, ctx, text);
|
|
319
331
|
}
|
|
320
332
|
|
|
321
|
-
/** Per-session state reset (session_start / session_tree) —
|
|
322
|
-
*
|
|
323
|
-
* retired by that branch. */
|
|
333
|
+
/** Per-session state reset (session_start / session_tree) — thin delegate to
|
|
334
|
+
* `resetRuntimeImpl` (reset-runtime.ts). */
|
|
324
335
|
resetRuntime(sessionId: string | undefined): void {
|
|
325
|
-
|
|
326
|
-
if (this.rt.sessionId === sid && this.rt.persistedThisSession) return; // same session, keep checkpoint memory
|
|
327
|
-
this.rt = {
|
|
328
|
-
sessionId: sid,
|
|
329
|
-
persistedThisSession: false,
|
|
330
|
-
lastCheckpointId: undefined,
|
|
331
|
-
lastCompactedFrom: 0,
|
|
332
|
-
lastCompactedTokens: 0,
|
|
333
|
-
dedupSkips: 0,
|
|
334
|
-
dedupAttempts: 0,
|
|
335
|
-
tokensSaved: 0,
|
|
336
|
-
lastCompactAt: null,
|
|
337
|
-
lastNativeCompactAt: null,
|
|
338
|
-
compactCount: 0,
|
|
339
|
-
recallInjections: 0,
|
|
340
|
-
cacheHitTokens: 0,
|
|
341
|
-
lengthStopPending: false,
|
|
342
|
-
errorRetryCount: 0,
|
|
343
|
-
errorRetryUntil: 0,
|
|
344
|
-
consecutiveErrors: 0,
|
|
345
|
-
// R1-R3 (retry redesign): in-flight dedup, session cap, poisoned-context state.
|
|
346
|
-
lastErrorRetryAt: 0,
|
|
347
|
-
retryNudgePending: false,
|
|
348
|
-
errorRetrySessionCount: 0,
|
|
349
|
-
lastErrorText: undefined,
|
|
350
|
-
errorTextRepeatCount: 0,
|
|
351
|
-
poisonedAdviseSent: false,
|
|
352
|
-
poisonedCompactSignatures: new Set(),
|
|
353
|
-
poisonedCount: 0,
|
|
354
|
-
};
|
|
355
|
-
this.trimCache = null; // v0.8.6: never replay a stale trim into a new session
|
|
356
|
-
this.statusKey = undefined;
|
|
357
|
-
this.activeAgents = 0;
|
|
358
|
-
this.currentTurn = 0;
|
|
359
|
-
this.lastActivityAt = 0;
|
|
360
|
-
this.tierTrace = undefined;
|
|
361
|
-
this.ticker.length = 0;
|
|
362
|
-
this.pulsing = false;
|
|
363
|
-
this.savedGoal = 50_000;
|
|
364
|
-
this.lastWhy = undefined;
|
|
365
|
-
// S31 audit P2: symmetry with bindRepo — a reset can coincide with a context
|
|
366
|
-
// that re-binds the repo, so drop the memo too. Cheap; the next
|
|
367
|
-
// getCachedGameState() re-queries lazily.
|
|
368
|
-
this.cachedGameState = undefined;
|
|
336
|
+
resetRuntimeImpl(this, sessionId);
|
|
369
337
|
}
|
|
370
338
|
|
|
371
339
|
captureModel(ctx: ExtensionContext): void {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-shutdown-widget.test.ts — regression cover for two host-integration
|
|
3
|
+
* fixes that the rest of the suite cannot see, because both are about what
|
|
4
|
+
* happens at the boundary with pi rather than inside the compaction engine:
|
|
5
|
+
*
|
|
6
|
+
* 1. session_shutdown must close the PGlite indexes. They are lazily-opened
|
|
7
|
+
* module singletons whose handles keep node's event loop alive, so leaving
|
|
8
|
+
* them open made `pi -p` produce its answer and then hang instead of
|
|
9
|
+
* exiting. closeVectorIndex()/closeMemoryIndex() existed but had no
|
|
10
|
+
* non-test callers.
|
|
11
|
+
*
|
|
12
|
+
* 2. The above-editor widget must be suppressible. It is a persistent,
|
|
13
|
+
* animated, full-width panel that repaints on its own cadence, which fights
|
|
14
|
+
* terminals where the user drives scrollback (pi inside a Neovim
|
|
15
|
+
* `:terminal`). MEGACOMPACT_TUI_WIDGET=0 turns it off without touching
|
|
16
|
+
* compaction.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { test } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import {
|
|
25
|
+
closeVectorIndex,
|
|
26
|
+
initVectorIndex,
|
|
27
|
+
isVectorIndexDisabled,
|
|
28
|
+
} from "../src/store/vectorIndex.js";
|
|
29
|
+
import { closeMemoryIndex } from "../src/store/memoryIndex.js";
|
|
30
|
+
import { loadConfig } from "./mega-config.js";
|
|
31
|
+
import { MegaRuntime } from "./mega-runtime.js";
|
|
32
|
+
|
|
33
|
+
const baseTmp = mkdtempSync(join(tmpdir(), "mc-shutdown-"));
|
|
34
|
+
process.env.MEGACOMPACT_INDEX_DIR = join(baseTmp, "index");
|
|
35
|
+
let counter = 0;
|
|
36
|
+
|
|
37
|
+
/** Fresh per-test state dir so concurrent runs never collide on disk. */
|
|
38
|
+
function isolate(): void {
|
|
39
|
+
process.env.MEGACOMPACT_STATE_DIR = join(baseTmp, `run-${counter++}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Minimal ExtensionContext slice: renderWidget only reaches ctx.ui.setWidget. */
|
|
43
|
+
function widgetCtx(calls: string[]): any {
|
|
44
|
+
return {
|
|
45
|
+
ui: {
|
|
46
|
+
setWidget: (key: string) => calls.push(key),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test.after(async () => {
|
|
52
|
+
// This file is itself a demonstration of the bug under test: without these
|
|
53
|
+
// closes the PGlite handles opened above keep the event loop alive and the
|
|
54
|
+
// test process never exits.
|
|
55
|
+
await Promise.all([closeVectorIndex(), closeMemoryIndex()]);
|
|
56
|
+
rmSync(baseTmp, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("MEGACOMPACT_TUI_WIDGET defaults on and is disabled by 0", () => {
|
|
60
|
+
isolate();
|
|
61
|
+
delete process.env.MEGACOMPACT_TUI_WIDGET;
|
|
62
|
+
assert.equal(loadConfig().tuiWidget, true, "widget should default to on");
|
|
63
|
+
|
|
64
|
+
process.env.MEGACOMPACT_TUI_WIDGET = "0";
|
|
65
|
+
assert.equal(loadConfig().tuiWidget, false);
|
|
66
|
+
|
|
67
|
+
process.env.MEGACOMPACT_TUI_WIDGET = "1";
|
|
68
|
+
assert.equal(loadConfig().tuiWidget, true);
|
|
69
|
+
|
|
70
|
+
delete process.env.MEGACOMPACT_TUI_WIDGET;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("renderWidget registers the panel when tuiWidget is on", () => {
|
|
74
|
+
isolate();
|
|
75
|
+
delete process.env.MEGACOMPACT_TUI_WIDGET;
|
|
76
|
+
const runtime = new MegaRuntime(loadConfig());
|
|
77
|
+
try {
|
|
78
|
+
const calls: string[] = [];
|
|
79
|
+
runtime.renderWidget(widgetCtx(calls));
|
|
80
|
+
assert.equal(calls.length, 1, "expected one setWidget registration");
|
|
81
|
+
} finally {
|
|
82
|
+
runtime.dispose();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("renderWidget registers nothing when tuiWidget is off", () => {
|
|
87
|
+
isolate();
|
|
88
|
+
process.env.MEGACOMPACT_TUI_WIDGET = "0";
|
|
89
|
+
const runtime = new MegaRuntime(loadConfig());
|
|
90
|
+
try {
|
|
91
|
+
const calls: string[] = [];
|
|
92
|
+
// Repeated calls, because the panel is re-registered on every snapshot
|
|
93
|
+
// and every game-state change — one guarded path is not enough.
|
|
94
|
+
runtime.renderWidget(widgetCtx(calls));
|
|
95
|
+
runtime.renderWidget(widgetCtx(calls));
|
|
96
|
+
assert.deepEqual(calls, [], "widget must never be registered when disabled");
|
|
97
|
+
} finally {
|
|
98
|
+
runtime.dispose();
|
|
99
|
+
delete process.env.MEGACOMPACT_TUI_WIDGET;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("the extension's session_shutdown handler awaits index teardown", async () => {
|
|
104
|
+
isolate();
|
|
105
|
+
const handlers: Record<string, Function[]> = {};
|
|
106
|
+
const pi: any = {
|
|
107
|
+
on(event: string, handler: Function) {
|
|
108
|
+
(handlers[event] ??= []).push(handler);
|
|
109
|
+
},
|
|
110
|
+
registerCommand() {},
|
|
111
|
+
registerProvider() {},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const { default: extension } = await import("./mega-compact.js");
|
|
115
|
+
extension(pi);
|
|
116
|
+
|
|
117
|
+
const shutdown = handlers["session_shutdown"] ?? [];
|
|
118
|
+
assert.ok(shutdown.length, "extension must register a session_shutdown handler");
|
|
119
|
+
|
|
120
|
+
const event = { type: "session_shutdown" } as any;
|
|
121
|
+
const ctx = {
|
|
122
|
+
ui: { setStatus: () => {}, notify: () => {}, setWidget: () => {} },
|
|
123
|
+
cwd: process.env.MEGACOMPACT_STATE_DIR,
|
|
124
|
+
} as any;
|
|
125
|
+
|
|
126
|
+
// Assert the close by identity rather than by "some handler returned a
|
|
127
|
+
// promise": several modules register on session_shutdown and at least one
|
|
128
|
+
// other is already async, so a promise proves nothing about teardown. The
|
|
129
|
+
// index is a module singleton, so if shutdown really closed it, re-init
|
|
130
|
+
// hands back a *different* instance.
|
|
131
|
+
const before = await initVectorIndex();
|
|
132
|
+
if (isVectorIndexDisabled() || !before) return; // PGlite unavailable — nothing to close
|
|
133
|
+
|
|
134
|
+
await Promise.all(shutdown.map((handler) => handler(event, ctx)));
|
|
135
|
+
|
|
136
|
+
const after = await initVectorIndex();
|
|
137
|
+
assert.notEqual(after, before, "session_shutdown must close the PGlite vector index");
|
|
138
|
+
|
|
139
|
+
// Idempotent: a second shutdown (reload, double-fire) must not throw.
|
|
140
|
+
await Promise.all(shutdown.map((handler) => handler(event, ctx)));
|
|
141
|
+
});
|
package/package.json
CHANGED