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.
@@ -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 { ModelSnapshot, GameState } from "../../src/store/sqlite.js";
24
- import type { MegaConfig, PressureBand } from "../mega-config.js";
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 { SessionRuntime } from "./helpers.js";
27
- import type { TickerEntry, WidgetData } from "./widget.js";
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) — inlined by the
322
- * raptor-promotion merge (R1–R3 retry-redesign fields); reset-runtime.ts was
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
- const sid = normalizeSessionId(sessionId);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",