pi-mega-compact 0.9.0 → 0.9.1
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/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/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/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/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
|
@@ -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