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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pgOpenGuard.ts — bound the PGlite open so a stalled WASM init can never wedge
|
|
3
|
+
* a pi turn.
|
|
4
|
+
*
|
|
5
|
+
* Both index modules (vectorIndex / memoryIndex) cache their in-flight open in a
|
|
6
|
+
* module-level `initPromise`. That cache is what turns a single stalled open
|
|
7
|
+
* into a permanent hang: PGlite is a single-writer WASM Postgres over a shared
|
|
8
|
+
* dataDir (~/.pi/mega-compact-vector), so a second pi process opening the same
|
|
9
|
+
* dir can block indefinitely. `await new PGlite(...)` then never settles, the
|
|
10
|
+
* never-settling promise is cached, and every later caller awaits that same dead
|
|
11
|
+
* promise — with no timers and no sockets left, node reports
|
|
12
|
+
* "Promise resolution is still pending but the event loop has already resolved"
|
|
13
|
+
* and the pi turn that awaited it never ends.
|
|
14
|
+
*
|
|
15
|
+
* withOpenTimeout() puts a ceiling on that wait. On timeout the caller gets
|
|
16
|
+
* undefined (both modules already degrade to a synchronous scan), and the
|
|
17
|
+
* abandoned open is disowned: if it does eventually settle, the instance is
|
|
18
|
+
* closed so a stray PGlite can't keep the loop alive or hold the dataDir lock.
|
|
19
|
+
*
|
|
20
|
+
* A rejected open is NOT swallowed — it propagates so the callers' existing
|
|
21
|
+
* corrupt-dir detection (Aborted / RuntimeError → wipe + one retry) still runs.
|
|
22
|
+
* Only the timeout resolves to undefined.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Sentinel so a legitimately-undefined open is distinguishable from a timeout. */
|
|
26
|
+
const TIMED_OUT = Symbol("pglite-open-timeout");
|
|
27
|
+
|
|
28
|
+
/** Default ceiling for a PGlite open. Generous — a cold WASM + HNSW init is slow. */
|
|
29
|
+
export const DEFAULT_PG_OPEN_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
/** Resolve the open timeout. 0 (or negative) disables the guard entirely. */
|
|
32
|
+
export function pgOpenTimeoutMs(): number {
|
|
33
|
+
const raw = process.env.MEGACOMPACT_PGLITE_OPEN_TIMEOUT_MS;
|
|
34
|
+
if (raw === undefined || raw.trim() === "") return DEFAULT_PG_OPEN_TIMEOUT_MS;
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_PG_OPEN_TIMEOUT_MS;
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A PGlite-ish handle we may need to dispose of after abandoning it. */
|
|
41
|
+
interface Closable {
|
|
42
|
+
close?: () => Promise<unknown> | unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Race `open` against the configured timeout.
|
|
47
|
+
*
|
|
48
|
+
* Resolves to the opened value, or to undefined when the open outruns the
|
|
49
|
+
* timeout (`onTimeout` fires first so the caller can log and flip its own
|
|
50
|
+
* disabled state). Rejections propagate to the caller unchanged.
|
|
51
|
+
*/
|
|
52
|
+
export async function withOpenTimeout<T>(
|
|
53
|
+
open: Promise<T>,
|
|
54
|
+
onTimeout: (reason: string) => void,
|
|
55
|
+
timeoutMs: number = pgOpenTimeoutMs(),
|
|
56
|
+
): Promise<T | undefined> {
|
|
57
|
+
// Guard disabled — preserve the original unbounded behavior verbatim.
|
|
58
|
+
if (timeoutMs <= 0) return open;
|
|
59
|
+
|
|
60
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
61
|
+
const expiry = new Promise<typeof TIMED_OUT>((resolve) => {
|
|
62
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
63
|
+
// Never hold the process open on account of the guard itself.
|
|
64
|
+
timer.unref?.();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// `open` is raced as-is so a rejection rejects the race — and therefore
|
|
69
|
+
// this function — leaving the caller's corrupt-retry path intact.
|
|
70
|
+
const winner = await Promise.race([open, expiry]);
|
|
71
|
+
|
|
72
|
+
if (winner === TIMED_OUT) {
|
|
73
|
+
// Disown the open. If it ever settles, close the instance so an orphaned
|
|
74
|
+
// PGlite cannot keep the event loop alive or hold the dataDir lock.
|
|
75
|
+
void open
|
|
76
|
+
.then((late) => {
|
|
77
|
+
try {
|
|
78
|
+
void (late as Closable | undefined)?.close?.();
|
|
79
|
+
} catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.catch(() => {
|
|
84
|
+
/* the abandoned open failed on its own — nothing left to release */
|
|
85
|
+
});
|
|
86
|
+
onTimeout(`timed out after ${timeoutMs}ms`);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return winner as T;
|
|
90
|
+
} finally {
|
|
91
|
+
if (timer) clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/store/vectorIndex.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface VectorIndexHit {
|
|
|
40
40
|
score: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
import { withOpenTimeout } from "./pgOpenGuard.js";
|
|
44
|
+
|
|
43
45
|
let db: PGliteInstance | undefined;
|
|
44
46
|
let initPromise: Promise<PGliteInstance | undefined> | undefined;
|
|
45
47
|
let disabled = false;
|
|
@@ -131,12 +133,19 @@ async function openPgLite(
|
|
|
131
133
|
if (!mod) return undefined;
|
|
132
134
|
const dir = indexDir();
|
|
133
135
|
mkdirSync(dir, { recursive: true });
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
await
|
|
136
|
+
// Bounded open: PGlite is single-writer over a shared dataDir, so a second
|
|
137
|
+
// pi process on the same dir can block here forever. Without the ceiling the
|
|
138
|
+
// never-settling promise gets cached in initPromise and every later caller
|
|
139
|
+
// awaits it — which is how a stalled index wedged a whole pi turn.
|
|
140
|
+
let openTimedOut = false;
|
|
141
|
+
const pg = await withOpenTimeout(
|
|
142
|
+
(async () => {
|
|
143
|
+
const inst = await new mod.PGlite({
|
|
144
|
+
dataDir: dir,
|
|
145
|
+
extensions: { vector: mod.vector },
|
|
146
|
+
});
|
|
147
|
+
await inst.exec("CREATE EXTENSION IF NOT EXISTS vector;");
|
|
148
|
+
await inst.exec(`
|
|
140
149
|
CREATE TABLE IF NOT EXISTS vector_index (
|
|
141
150
|
repo_id TEXT NOT NULL,
|
|
142
151
|
session_id TEXT NOT NULL,
|
|
@@ -145,10 +154,27 @@ async function openPgLite(
|
|
|
145
154
|
PRIMARY KEY (repo_id, session_id, checkpoint_id)
|
|
146
155
|
);
|
|
147
156
|
`);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
// HNSW index over cosine distance for fast NN. Created idempotently.
|
|
158
|
+
await inst.exec(
|
|
159
|
+
"CREATE INDEX IF NOT EXISTS vector_index_hnsw ON vector_index USING hnsw (embedding vector_cosine_ops);",
|
|
160
|
+
);
|
|
161
|
+
return inst;
|
|
162
|
+
})(),
|
|
163
|
+
(reason) => {
|
|
164
|
+
openTimedOut = true;
|
|
165
|
+
logWarn(`init ${reason}`);
|
|
166
|
+
},
|
|
151
167
|
);
|
|
168
|
+
if (!pg) {
|
|
169
|
+
if (openTimedOut) {
|
|
170
|
+
// Don't leave the dead open cached, and don't retry on the next call —
|
|
171
|
+
// a contended dataDir would just burn another full timeout per caller.
|
|
172
|
+
// Same terminal state as any other init failure: fall back to the scan.
|
|
173
|
+
initPromise = undefined;
|
|
174
|
+
disabled = true;
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
152
178
|
db = pg;
|
|
153
179
|
return pg;
|
|
154
180
|
} catch (err) {
|