pi-mega-compact 0.4.25 → 0.4.26

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.
@@ -15,6 +15,7 @@ import { C, MARKER_TYPE, } from "./mega-runtime.js";
15
15
  import { resolveRepoRoot, preserveRecentForPressure } from "./mega-config.js";
16
16
  import { runRaptor } from "../src/dedup/raptor/index.js";
17
17
  import { loadDedupConfig } from "../src/config/dedup.js";
18
+ import { upsertEmbedding as indexUpsertEmbedding } from "../src/store/vectorIndex.js";
18
19
  /** Run the full compaction pipeline and persist a checkpoint. Returns the result. */
19
20
  export function runCompact(pi, runtime, config, ctx, messages, opts = {}) {
20
21
  runtime.bindRepo(ctx.cwd);
@@ -131,6 +132,24 @@ export function runCompact(pi, runtime, config, ctx, messages, opts = {}) {
131
132
  /* non-fatal: tree refresh never blocks a compaction */
132
133
  }
133
134
  }
135
+ // Slice 2: best-effort mirror of the new checkpoint into the async global
136
+ // PGlite/HNSW vector index. Fires once per compaction (not per-add), so the
137
+ // shared global dir is never hammered by concurrent test workers.
138
+ // Non-fatal: a WASM init failure degrades to the sync scan silently.
139
+ if (!result.deduped) {
140
+ try {
141
+ const all = runtime.store.list(sid);
142
+ const latest = all.find((cp) => cp.checkpointId === result.checkpointId);
143
+ if (latest?.embedding) {
144
+ void indexUpsertEmbedding(runtime.currentStateDir, sid, latest.checkpointId, latest.embedding).catch(() => {
145
+ /* non-fatal: index refresh never blocks a compaction */
146
+ });
147
+ }
148
+ }
149
+ catch {
150
+ /* non-fatal: index refresh never blocks a compaction */
151
+ }
152
+ }
134
153
  runtime.setStatus(ctx, runtime.rt.persistedThisSession
135
154
  ? `mega-compact: ${result.checkpointId} · ${saved} tok saved`
136
155
  : `mega-compact: ready`);
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { homedir } from "node:os";
19
19
  import { join } from "node:path";
20
- import { mkdirSync } from "node:fs";
20
+ import { mkdirSync, rmSync, existsSync } from "node:fs";
21
21
  // PGlite + pgvector are script-free WASM (no native build) → survive pi's
22
22
  // install-script block. Imported lazily so a missing/broken package degrades
23
23
  // gracefully instead of crashing module load.
@@ -70,37 +70,61 @@ export function initVectorIndex() {
70
70
  return Promise.resolve(db);
71
71
  if (initPromise)
72
72
  return initPromise;
73
- initPromise = (async () => {
74
- try {
75
- const dir = indexDir();
76
- mkdirSync(dir, { recursive: true });
77
- const pg = await new PGlite({
78
- dataDir: dir,
79
- extensions: { vector },
80
- });
81
- await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
82
- await pg.exec(`
83
- CREATE TABLE IF NOT EXISTS vector_index (
84
- repo_id TEXT NOT NULL,
85
- session_id TEXT NOT NULL,
86
- checkpoint_id TEXT NOT NULL,
87
- embedding vector(${EMBEDDING_DIM}) NOT NULL,
88
- PRIMARY KEY (repo_id, session_id, checkpoint_id)
89
- );
90
- `);
91
- // HNSW index over cosine distance for fast NN. Created idempotently.
92
- await pg.exec("CREATE INDEX IF NOT EXISTS vector_index_hnsw ON vector_index USING hnsw (embedding vector_cosine_ops);");
93
- db = pg;
94
- return pg;
95
- }
96
- catch (err) {
97
- disabled = true;
98
- logWarn(`init failed: ${err instanceof Error ? err.message : String(err)}`);
99
- return undefined;
100
- }
101
- })();
73
+ initPromise = openPgLite(/* retryOnCorrupt */ true);
102
74
  return initPromise;
103
75
  }
76
+ /**
77
+ * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level
78
+ * abort (typically from a corrupted/torn data dir) triggers a delete + one
79
+ * retry — the dir is rebuilt from scratch by PGlite's initdb.
80
+ */
81
+ async function openPgLite(retryOnCorrupt) {
82
+ try {
83
+ const dir = indexDir();
84
+ mkdirSync(dir, { recursive: true });
85
+ const pg = await new PGlite({
86
+ dataDir: dir,
87
+ extensions: { vector },
88
+ });
89
+ await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
90
+ await pg.exec(`
91
+ CREATE TABLE IF NOT EXISTS vector_index (
92
+ repo_id TEXT NOT NULL,
93
+ session_id TEXT NOT NULL,
94
+ checkpoint_id TEXT NOT NULL,
95
+ embedding vector(${EMBEDDING_DIM}) NOT NULL,
96
+ PRIMARY KEY (repo_id, session_id, checkpoint_id)
97
+ );
98
+ `);
99
+ // HNSW index over cosine distance for fast NN. Created idempotently.
100
+ await pg.exec("CREATE INDEX IF NOT EXISTS vector_index_hnsw ON vector_index USING hnsw (embedding vector_cosine_ops);");
101
+ db = pg;
102
+ return pg;
103
+ }
104
+ catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ // Self-heal: a WASM Aborted() typically means the data dir is corrupted
107
+ // (torn WAL from concurrent access). Delete it and retry once.
108
+ if (retryOnCorrupt &&
109
+ (msg.includes("Aborted") || msg.includes("RuntimeError"))) {
110
+ try {
111
+ const dir = indexDir();
112
+ if (existsSync(dir)) {
113
+ rmSync(dir, { recursive: true, force: true });
114
+ }
115
+ // Clear singleton state so the retry starts fresh.
116
+ initPromise = undefined;
117
+ return openPgLite(/* retryOnCorrupt */ false);
118
+ }
119
+ catch {
120
+ // Self-heal failed — fall through to disable.
121
+ }
122
+ }
123
+ disabled = true;
124
+ logWarn(`init failed: ${msg}`);
125
+ return undefined;
126
+ }
127
+ }
104
128
  function toVectorLiteral(v) {
105
129
  // pgvector text form: [a,b,c]. Guard against NaN/Inf for a clean literal.
106
130
  const parts = v.map((x) => (Number.isFinite(x) ? x : 0));
@@ -182,6 +206,8 @@ export async function closeVectorIndex() {
182
206
  }
183
207
  db = undefined;
184
208
  initPromise = undefined;
209
+ disabled = false;
210
+ warned = false;
185
211
  }
186
212
  /**
187
213
  * Rebuild the entire index from the authoritative node:sqlite store. Used for
@@ -20,7 +20,7 @@ import { mmrRerank } from "./dedup/mmr.js";
20
20
  import { topK } from "./dedup/topk.js";
21
21
  import { openBloom, saveBloom } from "./store/bloom.js";
22
22
  import { listCheckpoints, nextCheckpointId, upsertCheckpoint, getCheckpoint, loadSessionState, saveSessionState, upsertMinhashSignature, insertLshBuckets, lshCandidateChunks, setDedupStatus, addTokensSaved, getDedupStats, bumpDedupStats, repoStats as repoStatsFromStore, dataInvariantStats, } from "./store/sqlite.js";
23
- import { upsertEmbedding as indexUpsertEmbedding, initVectorIndex, searchAsync as vectorIndexSearch, } from "./store/vectorIndex.js";
23
+ import { initVectorIndex, searchAsync as vectorIndexSearch, } from "./store/vectorIndex.js";
24
24
  import { rehydrateRaptorTree } from "./dedup/raptor/index.js";
25
25
  import { stagedExpansion } from "./dedup/raptor/retrieval.js";
26
26
  import { migrateJsonToSqlite } from "./store/migrate.js";
@@ -292,12 +292,6 @@ export class VectorStore {
292
292
  // Cumulative store-wide dedup accounting (attempt, not collapsed).
293
293
  bumpDedupStats(false, this.stateDir);
294
294
  onTier?.({ tier: "new", status: "stored" });
295
- // Slice 2: best-effort, fire-and-forget mirror of this new checkpoint into
296
- // the async global PGlite/HNSW index. NEVER awaited — must not block or
297
- // throw into the synchronous add() path. On failure the index degrades to
298
- // the sync scan (handled inside vectorIndex). The node:sqlite store remains
299
- // authoritative; the index is rebuildable from it at any time.
300
- void indexUpsertEmbedding(this.repoId, sessionId, checkpointId, checkpoint.embedding);
301
295
  return { checkpoint, deduped: false };
302
296
  }
303
297
  /**
@@ -22,6 +22,7 @@ import {
22
22
  import { resolveRepoRoot, preserveRecentForPressure, type MegaConfig } from "./mega-config.js";
23
23
  import { runRaptor } from "../src/dedup/raptor/index.js";
24
24
  import { loadDedupConfig } from "../src/config/dedup.js";
25
+ import { upsertEmbedding as indexUpsertEmbedding } from "../src/store/vectorIndex.js";
25
26
 
26
27
  export type RunCompactResult =
27
28
  | { skipped: true }
@@ -164,6 +165,29 @@ export function runCompact(
164
165
  }
165
166
  }
166
167
 
168
+ // Slice 2: best-effort mirror of the new checkpoint into the async global
169
+ // PGlite/HNSW vector index. Fires once per compaction (not per-add), so the
170
+ // shared global dir is never hammered by concurrent test workers.
171
+ // Non-fatal: a WASM init failure degrades to the sync scan silently.
172
+ if (!result.deduped) {
173
+ try {
174
+ const all = runtime.store.list(sid);
175
+ const latest = all.find((cp) => cp.checkpointId === result.checkpointId);
176
+ if (latest?.embedding) {
177
+ void indexUpsertEmbedding(
178
+ runtime.currentStateDir,
179
+ sid,
180
+ latest.checkpointId,
181
+ latest.embedding,
182
+ ).catch(() => {
183
+ /* non-fatal: index refresh never blocks a compaction */
184
+ });
185
+ }
186
+ } catch {
187
+ /* non-fatal: index refresh never blocks a compaction */
188
+ }
189
+ }
190
+
167
191
  runtime.setStatus(
168
192
  ctx,
169
193
  runtime.rt.persistedThisSession
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
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-2-Clause",
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { homedir } from "node:os";
20
20
  import { join } from "node:path";
21
- import { mkdirSync } from "node:fs";
21
+ import { mkdirSync, rmSync, existsSync } from "node:fs";
22
22
 
23
23
  // PGlite + pgvector are script-free WASM (no native build) → survive pi's
24
24
  // install-script block. Imported lazily so a missing/broken package degrades
@@ -82,37 +82,65 @@ export function initVectorIndex(): Promise<PGliteInstance | undefined> {
82
82
  if (isVectorIndexDisabled()) return Promise.resolve(undefined);
83
83
  if (db) return Promise.resolve(db);
84
84
  if (initPromise) return initPromise;
85
- initPromise = (async (): Promise<PGliteInstance | undefined> => {
86
- try {
87
- const dir = indexDir();
88
- mkdirSync(dir, { recursive: true });
89
- const pg = await new PGlite({
90
- dataDir: dir,
91
- extensions: { vector },
92
- });
93
- await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
94
- await pg.exec(`
95
- CREATE TABLE IF NOT EXISTS vector_index (
96
- repo_id TEXT NOT NULL,
97
- session_id TEXT NOT NULL,
98
- checkpoint_id TEXT NOT NULL,
99
- embedding vector(${EMBEDDING_DIM}) NOT NULL,
100
- PRIMARY KEY (repo_id, session_id, checkpoint_id)
101
- );
102
- `);
103
- // HNSW index over cosine distance for fast NN. Created idempotently.
104
- await pg.exec(
105
- "CREATE INDEX IF NOT EXISTS vector_index_hnsw ON vector_index USING hnsw (embedding vector_cosine_ops);",
85
+ initPromise = openPgLite(/* retryOnCorrupt */ true);
86
+ return initPromise;
87
+ }
88
+
89
+ /**
90
+ * Open + schema-init PGlite. When `retryOnCorrupt` is true, a WASM-level
91
+ * abort (typically from a corrupted/torn data dir) triggers a delete + one
92
+ * retry — the dir is rebuilt from scratch by PGlite's initdb.
93
+ */
94
+ async function openPgLite(
95
+ retryOnCorrupt: boolean,
96
+ ): Promise<PGliteInstance | undefined> {
97
+ try {
98
+ const dir = indexDir();
99
+ mkdirSync(dir, { recursive: true });
100
+ const pg = await new PGlite({
101
+ dataDir: dir,
102
+ extensions: { vector },
103
+ });
104
+ await pg.exec("CREATE EXTENSION IF NOT EXISTS vector;");
105
+ await pg.exec(`
106
+ CREATE TABLE IF NOT EXISTS vector_index (
107
+ repo_id TEXT NOT NULL,
108
+ session_id TEXT NOT NULL,
109
+ checkpoint_id TEXT NOT NULL,
110
+ embedding vector(${EMBEDDING_DIM}) NOT NULL,
111
+ PRIMARY KEY (repo_id, session_id, checkpoint_id)
106
112
  );
107
- db = pg;
108
- return pg;
109
- } catch (err) {
110
- disabled = true;
111
- logWarn(`init failed: ${err instanceof Error ? err.message : String(err)}`);
112
- return undefined;
113
+ `);
114
+ // HNSW index over cosine distance for fast NN. Created idempotently.
115
+ await pg.exec(
116
+ "CREATE INDEX IF NOT EXISTS vector_index_hnsw ON vector_index USING hnsw (embedding vector_cosine_ops);",
117
+ );
118
+ db = pg;
119
+ return pg;
120
+ } catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err);
122
+ // Self-heal: a WASM Aborted() typically means the data dir is corrupted
123
+ // (torn WAL from concurrent access). Delete it and retry once.
124
+ if (
125
+ retryOnCorrupt &&
126
+ (msg.includes("Aborted") || msg.includes("RuntimeError"))
127
+ ) {
128
+ try {
129
+ const dir = indexDir();
130
+ if (existsSync(dir)) {
131
+ rmSync(dir, { recursive: true, force: true });
132
+ }
133
+ // Clear singleton state so the retry starts fresh.
134
+ initPromise = undefined;
135
+ return openPgLite(/* retryOnCorrupt */ false);
136
+ } catch {
137
+ // Self-heal failed — fall through to disable.
138
+ }
113
139
  }
114
- })();
115
- return initPromise;
140
+ disabled = true;
141
+ logWarn(`init failed: ${msg}`);
142
+ return undefined;
143
+ }
116
144
  }
117
145
 
118
146
  function toVectorLiteral(v: number[]): string {
@@ -211,6 +239,8 @@ export async function closeVectorIndex(): Promise<void> {
211
239
  }
212
240
  db = undefined;
213
241
  initPromise = undefined;
242
+ disabled = false;
243
+ warned = false;
214
244
  }
215
245
 
216
246
  /**
@@ -40,7 +40,6 @@ import {
40
40
  dataInvariantStats,
41
41
  } from "./store/sqlite.js";
42
42
  import {
43
- upsertEmbedding as indexUpsertEmbedding,
44
43
  initVectorIndex,
45
44
  searchAsync as vectorIndexSearch,
46
45
  type VectorIndexHit,
@@ -388,12 +387,6 @@ export class VectorStore {
388
387
  // Cumulative store-wide dedup accounting (attempt, not collapsed).
389
388
  bumpDedupStats(false, this.stateDir);
390
389
  onTier?.({ tier: "new", status: "stored" });
391
- // Slice 2: best-effort, fire-and-forget mirror of this new checkpoint into
392
- // the async global PGlite/HNSW index. NEVER awaited — must not block or
393
- // throw into the synchronous add() path. On failure the index degrades to
394
- // the sync scan (handled inside vectorIndex). The node:sqlite store remains
395
- // authoritative; the index is rebuildable from it at any time.
396
- void indexUpsertEmbedding(this.repoId, sessionId, checkpointId, checkpoint.embedding);
397
390
  return { checkpoint, deduped: false };
398
391
  }
399
392