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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ All notable changes to this project will be documented in this file.
6
6
 
7
7
  No unreleased changes.
8
8
 
9
+ ## [1.9.8] - 2026-10-02
10
+
11
+ ### Fixed
12
+ - **OpenCode v2 plugin compatibility** -- generated plugins retain the v1
13
+ named export while also exposing the v2 default `id`/`setup` contract,
14
+ including event subscriptions, tool hooks, cleanup, and host session IDs.
15
+
16
+ ## [1.9.7] - 2026-09-28
17
+
18
+ ### Fixed
19
+ - **SQLite FTS5 startup resilience** -- transient `SQLITE_BUSY` contention during
20
+ lexical-index initialization is retried before FTS5 is marked unavailable.
21
+ - **Large-store search fallback** -- runtime embedding failure, provider
22
+ disappearance, and vector-dimension mismatch now all fall back to the durable
23
+ SQLite lexical index instead of returning a silent empty result.
24
+ - **Release metadata** -- synchronized the npm, MCP Registry, and packaged
25
+ integration metadata for the 1.9.7 patch release.
26
+
9
27
  ## [1.9.6] - 2026-09-22
10
28
 
11
29
  ### Added
package/dist/cli/index.js CHANGED
@@ -3232,7 +3232,7 @@ ${import_picocolors.default.gray(m3)} ${s4}
3232
3232
 
3233
3233
  // src/cli/version.ts
3234
3234
  function getCliVersion() {
3235
- return true ? "1.9.6" : pkg.version;
3235
+ return true ? "1.9.8" : pkg.version;
3236
3236
  }
3237
3237
  var init_version = __esm({
3238
3238
  "src/cli/version.ts"() {
@@ -9349,6 +9349,74 @@ var init_launch_root = __esm({
9349
9349
  }
9350
9350
  });
9351
9351
 
9352
+ // src/store/sqlite-reliability.ts
9353
+ function isTransientSqliteError(error40) {
9354
+ const code = error40?.code;
9355
+ if (typeof code === "string") {
9356
+ return /^SQLITE_BUSY(?:_|$)/i.test(code);
9357
+ }
9358
+ return error40 instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error40.message);
9359
+ }
9360
+ function isSqliteUnavailableError(error40) {
9361
+ return error40 instanceof SqliteUnavailableError || error40 instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error40.message);
9362
+ }
9363
+ function degradedReadError(storeName) {
9364
+ return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
9365
+ }
9366
+ function degradedWriteError(storeName) {
9367
+ return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
9368
+ }
9369
+ function formatSqliteFailure(error40) {
9370
+ return error40 instanceof Error && error40.message ? error40.message : String(error40);
9371
+ }
9372
+ function delay(ms2) {
9373
+ return new Promise((resolve9) => setTimeout(resolve9, ms2));
9374
+ }
9375
+ async function retrySqliteBusy(operation, label) {
9376
+ for (let attempt = 0; ; attempt++) {
9377
+ try {
9378
+ return await operation();
9379
+ } catch (error40) {
9380
+ if (!isTransientSqliteError(error40) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error40;
9381
+ const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
9382
+ console.error(
9383
+ `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
9384
+ );
9385
+ await delay(waitMs);
9386
+ }
9387
+ }
9388
+ }
9389
+ async function initializeSqliteStore(factory, dataDir, label) {
9390
+ try {
9391
+ loadBetterSqlite3();
9392
+ } catch (error40) {
9393
+ throw new SqliteUnavailableError(error40);
9394
+ }
9395
+ return retrySqliteBusy(async () => {
9396
+ const store2 = factory();
9397
+ await store2.init(dataDir);
9398
+ return store2;
9399
+ }, label);
9400
+ }
9401
+ var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
9402
+ var init_sqlite_reliability = __esm({
9403
+ "src/store/sqlite-reliability.ts"() {
9404
+ "use strict";
9405
+ init_esm_shims();
9406
+ init_sqlite_db();
9407
+ SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
9408
+ SqliteUnavailableError = class extends Error {
9409
+ code = "MEMORIX_SQLITE_UNAVAILABLE";
9410
+ constructor(cause) {
9411
+ super(
9412
+ cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
9413
+ );
9414
+ this.name = "SqliteUnavailableError";
9415
+ }
9416
+ };
9417
+ }
9418
+ });
9419
+
9352
9420
  // src/store/sqlite-fts.ts
9353
9421
  function normalizeFtsToken(token) {
9354
9422
  return token.replace(/"/g, '""');
@@ -9362,22 +9430,27 @@ function buildFtsQuery(query) {
9362
9430
  function initializeObservationLexicalIndex(db2) {
9363
9431
  const known = ftsAvailability.get(db2);
9364
9432
  if (known !== void 0) return known;
9365
- try {
9366
- db2.exec(CREATE_OBSERVATION_FTS);
9367
- const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
9368
- const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
9369
- if (observationCount !== indexedCount) {
9370
- db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
9433
+ let lastError;
9434
+ for (let attempt = 1; attempt <= FTS_INIT_MAX_ATTEMPTS; attempt++) {
9435
+ try {
9436
+ db2.exec(CREATE_OBSERVATION_FTS);
9437
+ const observationCount = Number(db2.prepare("SELECT COUNT(*) AS count FROM observations").get()?.count ?? 0);
9438
+ const indexedCount = Number(db2.prepare(`SELECT COUNT(*) AS count FROM ${FTS_TABLE}`).get()?.count ?? 0);
9439
+ if (observationCount !== indexedCount) {
9440
+ db2.exec(`INSERT INTO ${FTS_TABLE}(${FTS_TABLE}) VALUES ('rebuild')`);
9441
+ }
9442
+ ftsAvailability.set(db2, true);
9443
+ return true;
9444
+ } catch (error40) {
9445
+ lastError = error40;
9446
+ if (!isTransientSqliteError(error40) || attempt === FTS_INIT_MAX_ATTEMPTS) break;
9371
9447
  }
9372
- ftsAvailability.set(db2, true);
9373
- return true;
9374
- } catch (error40) {
9375
- ftsAvailability.set(db2, false);
9376
- console.warn(
9377
- `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${error40 instanceof Error ? error40.message : String(error40)}`
9378
- );
9379
- return false;
9380
9448
  }
9449
+ ftsAvailability.set(db2, false);
9450
+ console.warn(
9451
+ `[memorix] SQLite FTS5 unavailable; using the existing search fallback: ${lastError instanceof Error ? lastError.message : String(lastError)}`
9452
+ );
9453
+ return false;
9381
9454
  }
9382
9455
  function isObservationLexicalIndexEnabled(db2) {
9383
9456
  return ftsAvailability.get(db2) === true;
@@ -9431,11 +9504,12 @@ function searchObservationLexically(db2, options) {
9431
9504
  score: Math.max(0, Number(row.lexical_score ?? 0))
9432
9505
  }));
9433
9506
  }
9434
- var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS;
9507
+ var ftsAvailability, FTS_TABLE, CREATE_OBSERVATION_FTS, FTS_INIT_MAX_ATTEMPTS;
9435
9508
  var init_sqlite_fts = __esm({
9436
9509
  "src/store/sqlite-fts.ts"() {
9437
9510
  "use strict";
9438
9511
  init_esm_shims();
9512
+ init_sqlite_reliability();
9439
9513
  ftsAvailability = /* @__PURE__ */ new WeakMap();
9440
9514
  FTS_TABLE = "observations_fts";
9441
9515
  CREATE_OBSERVATION_FTS = `
@@ -9469,6 +9543,7 @@ CREATE TRIGGER IF NOT EXISTS observations_fts_au AFTER UPDATE ON observations BE
9469
9543
  VALUES (new.id, new.entityName, new.title, new.narrative, new.facts, new.filesModified, new.concepts, new.attachments);
9470
9544
  END;
9471
9545
  `;
9546
+ FTS_INIT_MAX_ATTEMPTS = 3;
9472
9547
  }
9473
9548
  });
9474
9549
 
@@ -10724,74 +10799,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
10724
10799
  }
10725
10800
  });
10726
10801
 
10727
- // src/store/sqlite-reliability.ts
10728
- function isTransientSqliteError(error40) {
10729
- const code = error40?.code;
10730
- if (typeof code === "string") {
10731
- return /^SQLITE_BUSY(?:_|$)/i.test(code);
10732
- }
10733
- return error40 instanceof Error && /\bdatabase is locked\b|\bdatabase is busy\b/i.test(error40.message);
10734
- }
10735
- function isSqliteUnavailableError(error40) {
10736
- return error40 instanceof SqliteUnavailableError || error40 instanceof Error && /SQLite is not available|better-sqlite3.*failed|node:sqlite.*failed|bun:sqlite.*failed/i.test(error40.message);
10737
- }
10738
- function degradedReadError(storeName) {
10739
- return new Error(`[memorix] Cannot read ${storeName}: SQLite backend unavailable (memory is not loaded)`);
10740
- }
10741
- function degradedWriteError(storeName) {
10742
- return new Error(`[memorix] Cannot write ${storeName}: SQLite backend unavailable (degraded mode; read-only)`);
10743
- }
10744
- function formatSqliteFailure(error40) {
10745
- return error40 instanceof Error && error40.message ? error40.message : String(error40);
10746
- }
10747
- function delay(ms2) {
10748
- return new Promise((resolve9) => setTimeout(resolve9, ms2));
10749
- }
10750
- async function retrySqliteBusy(operation, label) {
10751
- for (let attempt = 0; ; attempt++) {
10752
- try {
10753
- return await operation();
10754
- } catch (error40) {
10755
- if (!isTransientSqliteError(error40) || attempt >= SQLITE_BUSY_RETRY_DELAYS_MS.length) throw error40;
10756
- const waitMs = SQLITE_BUSY_RETRY_DELAYS_MS[attempt];
10757
- console.error(
10758
- `[memorix] ${label} SQLite busy (attempt ${attempt + 1}/${SQLITE_BUSY_RETRY_DELAYS_MS.length + 1}), retrying in ${waitMs}ms`
10759
- );
10760
- await delay(waitMs);
10761
- }
10762
- }
10763
- }
10764
- async function initializeSqliteStore(factory, dataDir, label) {
10765
- try {
10766
- loadBetterSqlite3();
10767
- } catch (error40) {
10768
- throw new SqliteUnavailableError(error40);
10769
- }
10770
- return retrySqliteBusy(async () => {
10771
- const store2 = factory();
10772
- await store2.init(dataDir);
10773
- return store2;
10774
- }, label);
10775
- }
10776
- var SQLITE_BUSY_RETRY_DELAYS_MS, SqliteUnavailableError;
10777
- var init_sqlite_reliability = __esm({
10778
- "src/store/sqlite-reliability.ts"() {
10779
- "use strict";
10780
- init_esm_shims();
10781
- init_sqlite_db();
10782
- SQLITE_BUSY_RETRY_DELAYS_MS = [50, 150, 400, 1e3, 2500];
10783
- SqliteUnavailableError = class extends Error {
10784
- code = "MEMORIX_SQLITE_UNAVAILABLE";
10785
- constructor(cause) {
10786
- super(
10787
- cause instanceof Error && cause.message ? cause.message : "[memorix] SQLite is unavailable (install a supported SQLite runtime)"
10788
- );
10789
- this.name = "SqliteUnavailableError";
10790
- }
10791
- };
10792
- }
10793
- });
10794
-
10795
10802
  // src/store/file-lock.ts
10796
10803
  import { promises as fs2 } from "fs";
10797
10804
  import { randomUUID } from "crypto";
@@ -188224,6 +188231,7 @@ async function searchObservations(options) {
188224
188231
  };
188225
188232
  let queryVector = null;
188226
188233
  let embeddingProviderForSearch = null;
188234
+ let embeddingUnavailable = false;
188227
188235
  if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
188228
188236
  try {
188229
188237
  const provider2 = await getEmbeddingProvider();
@@ -188231,6 +188239,7 @@ async function searchObservations(options) {
188231
188239
  embeddingProviderForSearch = provider2;
188232
188240
  const activeVectorDimensions = getVectorDimensions();
188233
188241
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
188242
+ embeddingUnavailable = true;
188234
188243
  rememberSearchMode(
188235
188244
  modeKey,
188236
188245
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
@@ -188270,8 +188279,11 @@ async function searchObservations(options) {
188270
188279
  hybridWeights: isCJKHeavy ? { text: 0.2, vector: 0.8 } : { text: 0.6, vector: 0.4 }
188271
188280
  };
188272
188281
  }
188282
+ } else {
188283
+ embeddingUnavailable = true;
188273
188284
  }
188274
188285
  } catch (error40) {
188286
+ embeddingUnavailable = true;
188275
188287
  rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
188276
188288
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
188277
188289
  }
@@ -188279,7 +188291,7 @@ async function searchObservations(options) {
188279
188291
  mark("preSearch");
188280
188292
  let results;
188281
188293
  const persistentCorpusEligible = await shouldUsePersistentSearch(projectIds, options.projectId);
188282
- const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled);
188294
+ const canUsePersistentLexical = persistentCorpusEligible && Boolean(hasQuery) && !queryVector && (quality === "fast" || tier === "fast" || !embeddingEnabled || embeddingUnavailable);
188283
188295
  let persistent = persistentCorpusEligible && queryVector && embeddingProviderForSearch ? await searchPersistentSemantically(options, projectIds, requestLimit, queryVector, embeddingProviderForSearch) : canUsePersistentLexical ? await searchPersistentLexically(options, projectIds, requestLimit) : null;
188284
188296
  const durableStore = getOptionalObservationStore();
188285
188297
  if (!persistent && queryVector && durableStore?.hasLexicalIndex?.() && count2(database) === 0) {
@@ -195655,6 +195667,161 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
195655
195667
  },
195656
195668
  };
195657
195669
  };
195670
+
195671
+ /**
195672
+ * OpenCode v2 compatibility layer. Keep this beside the v1 export so one
195673
+ * generated file can be loaded by both host generations during migration.
195674
+ */
195675
+ const v2HookCommand = ${hookCommand};
195676
+
195677
+ function v2ReportHookFailure(eventName, detail) {
195678
+ if (process.env.MEMORIX_HOOK_DEBUG !== '1') return;
195679
+ console.error('[memorix-plugin] v2 hook delivery failed:', eventName, detail);
195680
+ }
195681
+
195682
+ function v2EventData(event) {
195683
+ return event && typeof event.data === 'object' && event.data !== null ? event.data : {};
195684
+ }
195685
+
195686
+ function v2SessionId(event) {
195687
+ const data = v2EventData(event);
195688
+ const value = data.sessionID ?? data.sessionId ?? data.session_id
195689
+ ?? event?.sessionID ?? event?.sessionId;
195690
+ return typeof value === 'string' ? value : '';
195691
+ }
195692
+
195693
+ function v2RunHook(payload, directory, sessionId) {
195694
+ const nextPayload = { agent: 'opencode', cwd: directory, ...payload };
195695
+ if (sessionId) nextPayload.session_id = sessionId;
195696
+ const eventName = nextPayload.hook_event_name || 'unknown';
195697
+ try {
195698
+ const result = spawnSync(v2HookCommand, ['hook'], {
195699
+ input: JSON.stringify(nextPayload),
195700
+ timeout: 10_000,
195701
+ encoding: 'utf-8',
195702
+ stdio: ['pipe', 'pipe', 'pipe'],
195703
+ shell: process.platform === 'win32',
195704
+ windowsHide: true,
195705
+ });
195706
+ if (result.status !== 0) {
195707
+ v2ReportHookFailure(eventName, {
195708
+ exit: result.status,
195709
+ stderr: (result.stderr || '').slice(0, 200),
195710
+ error: result.error?.message,
195711
+ });
195712
+ }
195713
+ } catch (error) {
195714
+ v2ReportHookFailure(eventName, error?.message ?? error);
195715
+ }
195716
+ }
195717
+
195718
+ function v2SessionState(states, sessionId) {
195719
+ const key = sessionId || '__unknown__';
195720
+ let state = states.get(key);
195721
+ if (!state) {
195722
+ state = { pendingText: '', lastDeliveredKey: '' };
195723
+ states.set(key, state);
195724
+ }
195725
+ return state;
195726
+ }
195727
+
195728
+ function v2FlushAssistant(state, directory, sessionId) {
195729
+ const text = state.pendingText.trim();
195730
+ if (!text || text === state.lastDeliveredKey) return;
195731
+ v2RunHook({ hook_event_name: 'message.updated', ai_response: text }, directory, sessionId);
195732
+ state.lastDeliveredKey = text;
195733
+ state.pendingText = '';
195734
+ }
195735
+
195736
+ function v2FilePath(file) {
195737
+ if (typeof file === 'string') return file;
195738
+ if (!file || typeof file !== 'object') return '';
195739
+ return file.path ?? file.file ?? file.name ?? '';
195740
+ }
195741
+
195742
+ function v2HandleEvent(event, directory, states) {
195743
+ if (!event || typeof event !== 'object') return;
195744
+ const type = typeof event.type === 'string' ? event.type : '';
195745
+ const data = v2EventData(event);
195746
+ const sessionId = v2SessionId(event);
195747
+ const state = v2SessionState(states, sessionId);
195748
+ const run = (payload) => v2RunHook(payload, directory, sessionId);
195749
+
195750
+ switch (type) {
195751
+ case 'session.created':
195752
+ run({ hook_event_name: 'session.created' });
195753
+ break;
195754
+ case 'session.text.delta':
195755
+ state.pendingText += typeof data.delta === 'string' ? data.delta : (typeof data.text === 'string' ? data.text : '');
195756
+ break;
195757
+ case 'session.message.updated':
195758
+ case 'message.updated':
195759
+ if (data.role === 'assistant' && typeof data.text === 'string') state.pendingText = data.text;
195760
+ break;
195761
+ case 'session.step.ended': {
195762
+ const files = Array.isArray(data.files) ? data.files : [];
195763
+ for (const file of files) {
195764
+ const filePath = v2FilePath(file);
195765
+ if (filePath) run({ hook_event_name: 'file.edited', file_path: filePath });
195766
+ }
195767
+ if (typeof data.text === 'string') state.pendingText += data.text;
195768
+ break;
195769
+ }
195770
+ case 'session.idle':
195771
+ v2FlushAssistant(state, directory, sessionId);
195772
+ run({ hook_event_name: 'session.idle' });
195773
+ break;
195774
+ case 'session.compacted':
195775
+ run({ hook_event_name: 'session.compacted' });
195776
+ break;
195777
+ default:
195778
+ break;
195779
+ }
195780
+ }
195781
+
195782
+ function setup(ctx) {
195783
+ const directory = ctx?.location?.directory || ctx?.location?.worktree || process.cwd();
195784
+ const controller = new AbortController();
195785
+ const states = new Map();
195786
+
195787
+ if (ctx?.tool?.hook) {
195788
+ try {
195789
+ Promise.resolve(ctx.tool.hook('execute.after', async (event) => {
195790
+ const sessionId = v2SessionId(event);
195791
+ const data = event && typeof event.input === 'object' ? event.input : {};
195792
+ const tool = event?.tool ?? event?.name ?? '';
195793
+ v2RunHook({
195794
+ hook_event_name: 'tool.execute.after',
195795
+ tool_name: tool,
195796
+ tool_input: data,
195797
+ }, directory, sessionId);
195798
+ if (tool === 'shell') {
195799
+ v2RunHook({
195800
+ hook_event_name: 'command.executed',
195801
+ command: data.command ?? data.cmd ?? '',
195802
+ }, directory, sessionId);
195803
+ }
195804
+ })).catch((error) => v2ReportHookFailure('execute.after', error?.message ?? error));
195805
+ } catch (error) {
195806
+ v2ReportHookFailure('execute.after', error?.message ?? error);
195807
+ }
195808
+ }
195809
+
195810
+ if (ctx?.event?.subscribe) {
195811
+ void (async () => {
195812
+ try {
195813
+ const stream = ctx.event.subscribe({ signal: controller.signal });
195814
+ for await (const event of stream) v2HandleEvent(event, directory, states);
195815
+ } catch (error) {
195816
+ if (!controller.signal.aborted) v2ReportHookFailure('event.subscribe', error?.message ?? error);
195817
+ }
195818
+ })();
195819
+ }
195820
+
195821
+ return () => controller.abort();
195822
+ }
195823
+
195824
+ export default { id: 'memorix', setup };
195658
195825
  `;
195659
195826
  }
195660
195827
  async function installOpenCodeSkills(projectRoot, global2 = false) {
@@ -196480,7 +196647,7 @@ var init_installers = __esm({
196480
196647
  "use strict";
196481
196648
  init_esm_shims();
196482
196649
  init_official_skills();
196483
- OPENCODE_PLUGIN_VERSION = 7;
196650
+ OPENCODE_PLUGIN_VERSION = 8;
196484
196651
  AGENT_SKILL_DIRS = {
196485
196652
  cursor: { project: path31.join(".cursor", "skills"), global: path31.join(".cursor", "skills") },
196486
196653
  windsurf: { project: path31.join(".windsurf", "skills"), global: path31.join(".windsurf", "skills") },
@@ -209007,7 +209174,7 @@ var init_tool_profile = __esm({
209007
209174
  function getMcpServerInfo() {
209008
209175
  return {
209009
209176
  name: "memorix",
209010
- version: true ? "1.9.6" : "1.0.1"
209177
+ version: true ? "1.9.8" : "1.0.1"
209011
209178
  };
209012
209179
  }
209013
209180
  function createMcpDiscoverResult(capabilities, serverInfo) {