memorix 1.9.6 → 1.9.8

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/dist/index.js CHANGED
@@ -286,6 +286,74 @@ var init_launch_root = __esm({
286
286
  }
287
287
  });
288
288
 
289
+ // src/store/sqlite-reliability.ts
290
+ function isTransientSqliteError(error) {
291
+ const code = error?.code;
292
+ if (typeof code === "string") {
293
+ return /^SQLITE_BUSY(?:_|$)/i.test(code);
294
+ }
295
+ return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
296
+ }
297
+ function isSqliteUnavailableError(error) {
298
+ return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
299
+ }
300
+ function degradedReadError(storeName) {
301
+ return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
302
+ }
303
+ function degradedWriteError(storeName) {
304
+ return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
305
+ }
306
+ function formatSqliteFailure(error) {
307
+ return error instanceof Error && error.message ? error.message : String(error);
308
+ }
309
+ function delay(ms) {
310
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
311
+ }
312
+ async function retrySqliteBusy(operation, label) {
313
+ for (let attempt = 0; ; attempt++) {
314
+ try {
315
+ return await operation();
316
+ } catch (error) {
317
+ if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
318
+ const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
319
+ console.error(
320
+ `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
321
+ );
322
+ await delay(waitMs);
323
+ }
324
+ }
325
+ }
326
+ async function initializeSqliteStore(factory, dataDir, label) {
327
+ try {
328
+ loadBetterSqlite3();
329
+ } catch (error) {
330
+ throw new SqliteUnavailableError(error);
331
+ }
332
+ return retrySqliteBusy(async () => {
333
+ const store = factory();
334
+ await store.init(dataDir);
335
+ return store;
336
+ }, label);
337
+ }
338
+ var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
339
+ var init_sqlite_reliability = __esm({
340
+ "src/store/sqlite-reliability.ts"() {
341
+ "use strict";
342
+ init_esm_shims();
343
+ init_sqlite_db();
344
+ SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
345
+ SqliteUnavailableError = class extends Error {
346
+ code = "MEMORIX_SQLITE_UNAVAILABLE";
347
+ constructor(cause) {
348
+ super(
349
+ cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
350
+ );
351
+ this.name = "SqliteUnavailableError";
352
+ }
353
+ };
354
+ }
355
+ });
356
+
289
357
  // src/store/sqlite-fts.ts
290
358
  function normalizeFtsToken(token) {
291
359
  return token.replace(/"/g, '""');
@@ -299,22 +367,27 @@ function buildFtsQuery(query) {
299
367
  function initializeObservationLexicalIndex(db2) {
300
368
  const known = ftsAvailability.get(db2);
301
369
  if (known !== void 0) return known;
302
- try {
303
- db2.exec(CREATE_OBSERVATION_FTS);
304
- const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
305
- const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
306
- if (observationCount !== indexedCount) {
307
- db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
370
+ let lastError;
371
+ for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
372
+ try {
373
+ db2.exec(CREATE_OBSERVATION_FTS);
374
+ const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
375
+ const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
376
+ if (observationCount !== indexedCount) {
377
+ db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
378
+ }
379
+ ftsAvailability.set(db2, true);
380
+ return true;
381
+ } catch (error) {
382
+ lastError = error;
383
+ if (!isTransientSqliteError(error) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
308
384
  }
309
- ftsAvailability.set(db2, true);
310
- return true;
311
- } catch (error) {
312
- ftsAvailability.set(db2, false);
313
- console.warn(
314
- `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error instanceof Error ? error.message : String(error)}`
315
- );
316
- return false;
317
385
  }
386
+ ftsAvailability.set(db2, false);
387
+ console.warn(
388
+ `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`
389
+ );
390
+ return false;
318
391
  }
319
392
  function isObservationLexicalIndexEnabled(db2) {
320
393
  return ftsAvailability.get(db2) === true;
@@ -368,11 +441,12 @@ function searchObservationLexically(db2, options) {
368
441
  score: Math.max(0, Number(row.lexical_score ?? 0))
369
442
  }));
370
443
  }
371
- var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS;
444
+ var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS, FTS_INIT_MAX_ATTEMPTS;
372
445
  var init_sqlite_fts = __esm({
373
446
  "src/store/sqlite-fts.ts"() {
374
447
  "use strict";
375
448
  init_esm_shims();
449
+ init_sqlite_reliability();
376
450
  ftsAvailability = /* @__PURE__ */ new WeakMap();
377
451
  FTS_TABLE = "observations_fts";
378
452
  CREATE_OBSERVATION_FTS = `
@@ -406,6 +480,7 @@ CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BE
406
480
  VALUES (new.id, new.entityName, new.title, new.narrative, new.facts, new.filesModified, new.concepts, new.attachments);
407
481
  END;
408
482
  `;
483
+ FTS_INIT_MAX_ATTEMPTS = 3;
409
484
  }
410
485
  });
411
486
 
@@ -2557,74 +2632,6 @@ var init_types = __esm({
2557
2632
  }
2558
2633
  });
2559
2634
 
2560
- // src/store/sqlite-reliability.ts
2561
- function isTransientSqliteError(error) {
2562
- const code = error?.code;
2563
- if (typeof code === "string") {
2564
- return /^SQLITE_BUSY(?:_|$)/i.test(code);
2565
- }
2566
- return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
2567
- }
2568
- function isSqliteUnavailableError(error) {
2569
- return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
2570
- }
2571
- function degradedReadError(storeName) {
2572
- return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
2573
- }
2574
- function degradedWriteError(storeName) {
2575
- return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
2576
- }
2577
- function formatSqliteFailure(error) {
2578
- return error instanceof Error && error.message ? error.message : String(error);
2579
- }
2580
- function delay(ms) {
2581
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2582
- }
2583
- async function retrySqliteBusy(operation, label) {
2584
- for (let attempt = 0; ; attempt++) {
2585
- try {
2586
- return await operation();
2587
- } catch (error) {
2588
- if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
2589
- const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
2590
- console.error(
2591
- `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
2592
- );
2593
- await delay(waitMs);
2594
- }
2595
- }
2596
- }
2597
- async function initializeSqliteStore(factory, dataDir, label) {
2598
- try {
2599
- loadBetterSqlite3();
2600
- } catch (error) {
2601
- throw new SqliteUnavailableError(error);
2602
- }
2603
- return retrySqliteBusy(async () => {
2604
- const store = factory();
2605
- await store.init(dataDir);
2606
- return store;
2607
- }, label);
2608
- }
2609
- var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
2610
- var init_sqlite_reliability = __esm({
2611
- "src/store/sqlite-reliability.ts"() {
2612
- "use strict";
2613
- init_esm_shims();
2614
- init_sqlite_db();
2615
- SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
2616
- SqliteUnavailableError = class extends Error {
2617
- code = "MEMORIX_SQLITE_UNAVAILABLE";
2618
- constructor(cause) {
2619
- super(
2620
- cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
2621
- );
2622
- this.name = "SqliteUnavailableError";
2623
- }
2624
- };
2625
- }
2626
- });
2627
-
2628
2635
  // src/store/mini-skill-store.ts
2629
2636
  var mini_skill_store_exports = {};
2630
2637
  __export(mini_skill_store_exports, {
@@ -9120,6 +9127,7 @@ async function searchObservations(options) {
9120
9127
  };
9121
9128
  let queryVector = null;
9122
9129
  let embeddingProviderForSearch = null;
9130
+ let embeddingUnavailable = false;
9123
9131
  if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
9124
9132
  try {
9125
9133
  const provider2 = await getEmbeddingProvider();
@@ -9127,6 +9135,7 @@ async function searchObservations(options) {
9127
9135
  embeddingProviderForSearch = provider2;
9128
9136
  const activeVectorDimensions = getVectorDimensions();
9129
9137
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
9138
+ embeddingUnavailable = true;
9130
9139
  rememberSearchMode(
9131
9140
  modeKey,
9132
9141
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
@@ -9166,8 +9175,11 @@ async function searchObservations(options) {
9166
9175
  hybridWeights: isCJKHeavy ? { text: 0.2, vector: 0.8 } : { text: 0.6, vector: 0.4 }
9167
9176
  };
9168
9177
  }
9178
+ } else {
9179
+ embeddingUnavailable = true;
9169
9180
  }
9170
9181
  } catch (error) {
9182
+ embeddingUnavailable = true;
9171
9183
  rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
9172
9184
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
9173
9185
  }
@@ -9175,7 +9187,7 @@ async function searchObservations(options) {
9175
9187
  mark("preSearch");
9176
9188
  let results;
9177
9189
  const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
9178
- const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled);
9190
+ const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled || embeddingUnavailable);
9179
9191
  let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch ? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch) : canUsePersistentLexical ? await searchPersistentLexically(options, projectIds, requestLimit) : null;
9180
9192
  const durableStore = getOptionalObservationStore();
9181
9193
  if (!persistent && queryVector && durableStore?.hasLexicalIndex?.() && count(database) === 0) {
@@ -160092,7 +160104,7 @@ var init_request_context = __esm({
160092
160104
  function getMcpServerInfo() {
160093
160105
  return {
160094
160106
  name: "memorix",
160095
- version: true ? "1.9.6" : "1.0.1"
160107
+ version: true ? "1.9.8" : "1.0.1"
160096
160108
  };
160097
160109
  }
160098
160110
  function createMcpDiscoverResult(capabilities, serverInfo) {
@@ -170081,6 +170093,161 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
170081
170093
  },
170082
170094
  };
170083
170095
  };
170096
+
170097
+ /**
170098
+ * OpenCode v2 compatibility layer. Keep this beside the v1 export so one
170099
+ * generated file can be loaded by both host generations during migration.
170100
+ */
170101
+ const v2HookCommand = ${hookCommand};
170102
+
170103
+ function v2ReportHookFailure(eventName, detail) {
170104
+ if (process.env.MEMORIX_HOOK_DEBUG !== '1') return;
170105
+ console.error('[memorix-plugin] v2 hook delivery failed:', eventName, detail);
170106
+ }
170107
+
170108
+ function v2EventData(event) {
170109
+ return event && typeof event.data === 'object' && event.data !== null ? event.data : {};
170110
+ }
170111
+
170112
+ function v2SessionId(event) {
170113
+ const data = v2EventData(event);
170114
+ const value = data.sessionID ?? data.sessionId ?? data.session_id
170115
+ ?? event?.sessionID ?? event?.sessionId;
170116
+ return typeof value === 'string' ? value : '';
170117
+ }
170118
+
170119
+ function v2RunHook(payload, directory, sessionId) {
170120
+ const nextPayload = { agent: 'opencode', cwd: directory, ...payload };
170121
+ if (sessionId) nextPayload.session_id = sessionId;
170122
+ const eventName = nextPayload.hook_event_name || 'unknown';
170123
+ try {
170124
+ const result = spawnSync(v2HookCommand, ['hook'], {
170125
+ input: JSON.stringify(nextPayload),
170126
+ timeout: 10_000,
170127
+ encoding: 'utf-8',
170128
+ stdio: ['pipe', 'pipe', 'pipe'],
170129
+ shell: process.platform === 'win32',
170130
+ windowsHide: true,
170131
+ });
170132
+ if (result.status !== 0) {
170133
+ v2ReportHookFailure(eventName, {
170134
+ exit: result.status,
170135
+ stderr: (result.stderr || '').slice(0, 200),
170136
+ error: result.error?.message,
170137
+ });
170138
+ }
170139
+ } catch (error) {
170140
+ v2ReportHookFailure(eventName, error?.message ?? error);
170141
+ }
170142
+ }
170143
+
170144
+ function v2SessionState(states, sessionId) {
170145
+ const key = sessionId || '__unknown__';
170146
+ let state = states.get(key);
170147
+ if (!state) {
170148
+ state = { pendingText: '', lastDeliveredKey: '' };
170149
+ states.set(key, state);
170150
+ }
170151
+ return state;
170152
+ }
170153
+
170154
+ function v2FlushAssistant(state, directory, sessionId) {
170155
+ const text = state.pendingText.trim();
170156
+ if (!text || text === state.lastDeliveredKey) return;
170157
+ v2RunHook({ hook_event_name: 'message.updated', ai_response: text }, directory, sessionId);
170158
+ state.lastDeliveredKey = text;
170159
+ state.pendingText = '';
170160
+ }
170161
+
170162
+ function v2FilePath(file) {
170163
+ if (typeof file === 'string') return file;
170164
+ if (!file || typeof file !== 'object') return '';
170165
+ return file.path ?? file.file ?? file.name ?? '';
170166
+ }
170167
+
170168
+ function v2HandleEvent(event, directory, states) {
170169
+ if (!event || typeof event !== 'object') return;
170170
+ const type = typeof event.type === 'string' ? event.type : '';
170171
+ const data = v2EventData(event);
170172
+ const sessionId = v2SessionId(event);
170173
+ const state = v2SessionState(states, sessionId);
170174
+ const run = (payload) => v2RunHook(payload, directory, sessionId);
170175
+
170176
+ switch (type) {
170177
+ case 'session.created':
170178
+ run({ hook_event_name: 'session.created' });
170179
+ break;
170180
+ case 'session.text.delta':
170181
+ state.pendingText += typeof data.delta === 'string' ? data.delta : (typeof data.text === 'string' ? data.text : '');
170182
+ break;
170183
+ case 'session.message.updated':
170184
+ case 'message.updated':
170185
+ if (data.role === 'assistant' && typeof data.text === 'string') state.pendingText = data.text;
170186
+ break;
170187
+ case 'session.step.ended': {
170188
+ const files = Array.isArray(data.files) ? data.files : [];
170189
+ for (const file of files) {
170190
+ const filePath = v2FilePath(file);
170191
+ if (filePath) run({ hook_event_name: 'file.edited', file_path: filePath });
170192
+ }
170193
+ if (typeof data.text === 'string') state.pendingText += data.text;
170194
+ break;
170195
+ }
170196
+ case 'session.idle':
170197
+ v2FlushAssistant(state, directory, sessionId);
170198
+ run({ hook_event_name: 'session.idle' });
170199
+ break;
170200
+ case 'session.compacted':
170201
+ run({ hook_event_name: 'session.compacted' });
170202
+ break;
170203
+ default:
170204
+ break;
170205
+ }
170206
+ }
170207
+
170208
+ function setup(ctx) {
170209
+ const directory = ctx?.location?.directory || ctx?.location?.worktree || process.cwd();
170210
+ const controller = new AbortController();
170211
+ const states = new Map();
170212
+
170213
+ if (ctx?.tool?.hook) {
170214
+ try {
170215
+ Promise.resolve(ctx.tool.hook('execute.after', async (event) => {
170216
+ const sessionId = v2SessionId(event);
170217
+ const data = event && typeof event.input === 'object' ? event.input : {};
170218
+ const tool = event?.tool ?? event?.name ?? '';
170219
+ v2RunHook({
170220
+ hook_event_name: 'tool.execute.after',
170221
+ tool_name: tool,
170222
+ tool_input: data,
170223
+ }, directory, sessionId);
170224
+ if (tool === 'shell') {
170225
+ v2RunHook({
170226
+ hook_event_name: 'command.executed',
170227
+ command: data.command ?? data.cmd ?? '',
170228
+ }, directory, sessionId);
170229
+ }
170230
+ })).catch((error) => v2ReportHookFailure('execute.after', error?.message ?? error));
170231
+ } catch (error) {
170232
+ v2ReportHookFailure('execute.after', error?.message ?? error);
170233
+ }
170234
+ }
170235
+
170236
+ if (ctx?.event?.subscribe) {
170237
+ void (async () => {
170238
+ try {
170239
+ const stream = ctx.event.subscribe({ signal: controller.signal });
170240
+ for await (const event of stream) v2HandleEvent(event, directory, states);
170241
+ } catch (error) {
170242
+ if (!controller.signal.aborted) v2ReportHookFailure('event.subscribe', error?.message ?? error);
170243
+ }
170244
+ })();
170245
+ }
170246
+
170247
+ return () => controller.abort();
170248
+ }
170249
+
170250
+ export default { id: 'memorix', setup };
170084
170251
  `;
170085
170252
  }
170086
170253
  async function installOpenCodeSkills(projectRoot, global2 = false) {
@@ -170906,7 +171073,7 @@ var init_installers = __esm({
170906
171073
  "use strict";
170907
171074
  init_esm_shims();
170908
171075
  init_official_skills();
170909
- OPENCODE_PLUGIN_VERSION = 7;
171076
+ OPENCODE_PLUGIN_VERSION = 8;
170910
171077
  AGENT_SKILL_DIRS = {
170911
171078
  cursor: { project: path34.join(".cursor", "skills"), global: path34.join(".cursor", "skills") },
170912
171079
  windsurf: { project: path34.join(".windsurf", "skills"), global: path34.join(".windsurf", "skills") },
@@ -176430,7 +176597,7 @@ var init_serve_shared = __esm({
176430
176597
 
176431
176598
  // src/cli/version.ts
176432
176599
  function getCliVersion() {
176433
- return true ? "1.9.6" : pkg.version;
176600
+ return true ? "1.9.8" : pkg.version;
176434
176601
  }
176435
176602
  var init_version3 = __esm({
176436
176603
  "src/cli/version.ts"() {
@@ -222,6 +222,74 @@ var init_launch_root = __esm({
222
222
  }
223
223
  });
224
224
 
225
+ // src/store/sqlite-reliability.ts
226
+ function isTransientSqliteError(error) {
227
+ const code = error?.code;
228
+ if (typeof code === "string") {
229
+ return /^SQLITE_BUSY(?:_|$)/i.test(code);
230
+ }
231
+ return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
232
+ }
233
+ function isSqliteUnavailableError(error) {
234
+ return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
235
+ }
236
+ function degradedReadError(storeName) {
237
+ return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
238
+ }
239
+ function degradedWriteError(storeName) {
240
+ return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
241
+ }
242
+ function formatSqliteFailure(error) {
243
+ return error instanceof Error && error.message ? error.message : String(error);
244
+ }
245
+ function delay(ms) {
246
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
247
+ }
248
+ async function retrySqliteBusy(operation, label) {
249
+ for (let attempt = 0; ; attempt++) {
250
+ try {
251
+ return await operation();
252
+ } catch (error) {
253
+ if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
254
+ const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
255
+ console.error(
256
+ `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
257
+ );
258
+ await delay(waitMs);
259
+ }
260
+ }
261
+ }
262
+ async function initializeSqliteStore(factory, dataDir, label) {
263
+ try {
264
+ loadBetterSqlite3();
265
+ } catch (error) {
266
+ throw new SqliteUnavailableError(error);
267
+ }
268
+ return retrySqliteBusy(async () => {
269
+ const store = factory();
270
+ await store.init(dataDir);
271
+ return store;
272
+ }, label);
273
+ }
274
+ var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
275
+ var init_sqlite_reliability = __esm({
276
+ "src/store/sqlite-reliability.ts"() {
277
+ "use strict";
278
+ init_esm_shims();
279
+ init_sqlite_db();
280
+ SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
281
+ SqliteUnavailableError = class extends Error {
282
+ code = "MEMORIX_SQLITE_UNAVAILABLE";
283
+ constructor(cause) {
284
+ super(
285
+ cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
286
+ );
287
+ this.name = "SqliteUnavailableError";
288
+ }
289
+ };
290
+ }
291
+ });
292
+
225
293
  // src/store/sqlite-fts.ts
226
294
  function normalizeFtsToken(token) {
227
295
  return token.replace(/"/g, '""');
@@ -235,22 +303,27 @@ function buildFtsQuery(query) {
235
303
  function initializeObservationLexicalIndex(db2) {
236
304
  const known = ftsAvailability.get(db2);
237
305
  if (known !== void 0) return known;
238
- try {
239
- db2.exec(CREATE_OBSERVATION_FTS);
240
- const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
241
- const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
242
- if (observationCount !== indexedCount) {
243
- db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
306
+ let lastError;
307
+ for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
308
+ try {
309
+ db2.exec(CREATE_OBSERVATION_FTS);
310
+ const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
311
+ const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
312
+ if (observationCount !== indexedCount) {
313
+ db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
314
+ }
315
+ ftsAvailability.set(db2, true);
316
+ return true;
317
+ } catch (error) {
318
+ lastError = error;
319
+ if (!isTransientSqliteError(error) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
244
320
  }
245
- ftsAvailability.set(db2, true);
246
- return true;
247
- } catch (error) {
248
- ftsAvailability.set(db2, false);
249
- console.warn(
250
- `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error instanceof Error ? error.message : String(error)}`
251
- );
252
- return false;
253
321
  }
322
+ ftsAvailability.set(db2, false);
323
+ console.warn(
324
+ `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`
325
+ );
326
+ return false;
254
327
  }
255
328
  function isObservationLexicalIndexEnabled(db2) {
256
329
  return ftsAvailability.get(db2) === true;
@@ -304,11 +377,12 @@ function searchObservationLexically(db2, options) {
304
377
  score: Math.max(0, Number(row.lexical_score ?? 0))
305
378
  }));
306
379
  }
307
- var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS;
380
+ var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS, FTS_INIT_MAX_ATTEMPTS;
308
381
  var init_sqlite_fts = __esm({
309
382
  "src/store/sqlite-fts.ts"() {
310
383
  "use strict";
311
384
  init_esm_shims();
385
+ init_sqlite_reliability();
312
386
  ftsAvailability = /* @__PURE__ */ new WeakMap();
313
387
  FTS_TABLE = "observations_fts";
314
388
  CREATE_OBSERVATION_FTS = `
@@ -342,6 +416,7 @@ CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BE
342
416
  VALUES (new.id, new.entityName, new.title, new.narrative, new.facts, new.filesModified, new.concepts, new.attachments);
343
417
  END;
344
418
  `;
419
+ FTS_INIT_MAX_ATTEMPTS = 3;
345
420
  }
346
421
  });
347
422
 
@@ -3061,74 +3136,6 @@ var init_provider = __esm({
3061
3136
  }
3062
3137
  });
3063
3138
 
3064
- // src/store/sqlite-reliability.ts
3065
- function isTransientSqliteError(error) {
3066
- const code = error?.code;
3067
- if (typeof code === "string") {
3068
- return /^SQLITE_BUSY(?:_|$)/i.test(code);
3069
- }
3070
- return error instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error.message);
3071
- }
3072
- function isSqliteUnavailableError(error) {
3073
- return error instanceof SqliteUnavailableError || error instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error.message);
3074
- }
3075
- function degradedReadError(storeName) {
3076
- return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
3077
- }
3078
- function degradedWriteError(storeName) {
3079
- return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
3080
- }
3081
- function formatSqliteFailure(error) {
3082
- return error instanceof Error && error.message ? error.message : String(error);
3083
- }
3084
- function delay(ms) {
3085
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3086
- }
3087
- async function retrySqliteBusy(operation, label) {
3088
- for (let attempt = 0; ; attempt++) {
3089
- try {
3090
- return await operation();
3091
- } catch (error) {
3092
- if (!isTransientSqliteError(error) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error;
3093
- const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
3094
- console.error(
3095
- `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
3096
- );
3097
- await delay(waitMs);
3098
- }
3099
- }
3100
- }
3101
- async function initializeSqliteStore(factory, dataDir, label) {
3102
- try {
3103
- loadBetterSqlite3();
3104
- } catch (error) {
3105
- throw new SqliteUnavailableError(error);
3106
- }
3107
- return retrySqliteBusy(async () => {
3108
- const store = factory();
3109
- await store.init(dataDir);
3110
- return store;
3111
- }, label);
3112
- }
3113
- var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
3114
- var init_sqlite_reliability = __esm({
3115
- "src/store/sqlite-reliability.ts"() {
3116
- "use strict";
3117
- init_esm_shims();
3118
- init_sqlite_db();
3119
- SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
3120
- SqliteUnavailableError = class extends Error {
3121
- code = "MEMORIX_SQLITE_UNAVAILABLE";
3122
- constructor(cause) {
3123
- super(
3124
- cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
3125
- );
3126
- this.name = "SqliteUnavailableError";
3127
- }
3128
- };
3129
- }
3130
- });
3131
-
3132
3139
  // src/store/file-lock.ts
3133
3140
  import { promises as fs2 } from "fs";
3134
3141
  import { randomUUID } from "crypto";
@@ -7686,6 +7693,7 @@ async function searchObservations(options) {
7686
7693
  };
7687
7694
  let queryVector = null;
7688
7695
  let embeddingProviderForSearch = null;
7696
+ let embeddingUnavailable = false;
7689
7697
  if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
7690
7698
  try {
7691
7699
  const provider2 = await getEmbeddingProvider();
@@ -7693,6 +7701,7 @@ async function searchObservations(options) {
7693
7701
  embeddingProviderForSearch = provider2;
7694
7702
  const activeVectorDimensions = getVectorDimensions();
7695
7703
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
7704
+ embeddingUnavailable = true;
7696
7705
  rememberSearchMode(
7697
7706
  modeKey,
7698
7707
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
@@ -7732,8 +7741,11 @@ async function searchObservations(options) {
7732
7741
  hybridWeights: isCJKHeavy ? { text: 0.2, vector: 0.8 } : { text: 0.6, vector: 0.4 }
7733
7742
  };
7734
7743
  }
7744
+ } else {
7745
+ embeddingUnavailable = true;
7735
7746
  }
7736
7747
  } catch (error) {
7748
+ embeddingUnavailable = true;
7737
7749
  rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
7738
7750
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
7739
7751
  }
@@ -7741,7 +7753,7 @@ async function searchObservations(options) {
7741
7753
  mark("preSearch");
7742
7754
  let results;
7743
7755
  const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
7744
- const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled);
7756
+ const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled || embeddingUnavailable);
7745
7757
  let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch ? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch) : canUsePersistentLexical ? await searchPersistentLexically(options, projectIds, requestLimit) : null;
7746
7758
  const durableStore = getOptionalObservationStore();
7747
7759
  if (!persistent && queryVector && durableStore?.hasLexicalIndex?.() && count(database) === 0) {