memorix 1.1.11 → 1.1.13

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.
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.1.13] - 2026-07-17
8
+
9
+ ### Added
10
+ - **Codex installation proof** -- `memorix doctor agents --agent codex --scope global` now checks the local plugin bundle, Personal marketplace entry, five declared and trusted lifecycle hooks, and the installed/enabled state reported by `codex plugin list`.
11
+
12
+ ### Fixed
13
+ - **Codex plugin version drift** -- Global setup now stamps the copied Codex plugin manifest with the installed Memorix version, so Codex no longer reports the old template version after an upgrade. The source template is covered by a release regression test as well.
14
+ - **Workspace package publish trap** -- `@memorix/ai`, `@memorix/agent-core`, `@memorix/tui`, and `@memorix/memcode` are now explicitly internal workspaces. The root `memorix` package ships their bundled runtime, and the release workflow publishes only that supported public package.
15
+
16
+ ## [1.1.12] - 2026-07-17
17
+
18
+ ### Added
19
+ - **Codex lifecycle hook capture** -- The Memorix Codex plugin now bundles the documented `SessionStart`, `UserPromptSubmit`, `PostToolUse`, `PreCompact`, and `Stop` hook events, with a Windows-safe `commandWindows` command override.
20
+
21
+ ### Fixed
22
+ - **Codex hook normalization and injection** -- Codex lifecycle payloads now preserve their event names, `SessionStart` returns Codex-native `hookSpecificOutput.additionalContext`, and capture-only events stay quiet instead of filling the agent context with save-status messages.
23
+ - **OpenCode hooks on Windows** -- The generated plugin resolves the installed npm `memorix.cmd` shim while setup runs and uses that stable path inside OpenCode, instead of relying on OpenCode's inherited `PATH`. Hook delivery stays quiet unless `MEMORIX_HOOK_DEBUG=1` is explicitly set. Fixes #125.
24
+ - **Setup scope isolation** -- Project-level setup no longer installs Claude, Codex, or Copilot plugin bundles in the user home. Use `--global` for user-level plugins, skills, and lifecycle hooks.
25
+ - **TUI embedding startup** -- TUI health, chat, and quick-search paths now use the same lexical-ready startup flow as MCP instead of making a remote dimension probe during the first screen or search.
26
+ - **Semantic recall after restart** -- API-backed embeddings now restore compatible vectors from the local cache during index hydration, without placing remote embedding calls on the MCP startup path. Redundant namespace-scoped cache metadata survives a lost dimensions cache; cache misses enter the existing bounded background recovery lane, and the first successful vector upgrades a cache-less lexical index in the same process. Based on the diagnosis in #126 by @Tom-Ma-Ming.
27
+ - **Concurrent index startup** -- Cache-only startup and ordinary database callers now share one Orama initialization, so a later vector-schema attempt cannot replace an already hydrated lexical index. Resets also discard obsolete in-flight instances.
28
+ - **Lazy API vector-cache I/O** -- A cache-only start with no trusted dimension metadata now skips parsing the potentially large API vector cache entirely; the cache is loaded only after compatible local metadata or a normal embedding initialization establishes its dimensions.
29
+
5
30
  ## [1.1.11] - 2026-07-16
6
31
 
7
32
  ### Added
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@memorix/memcode",
3
- "version": "1.1.11",
3
+ "version": "1.1.12",
4
+ "private": true,
4
5
  "description": "Memorix-native coding agent CLI with terminal chat, project memory, hooks, and session management",
5
6
  "type": "module",
6
7
  "piConfig": {
@@ -34,9 +35,9 @@
34
35
  "prepublishOnly": "npm run clean && npm run build"
35
36
  },
36
37
  "dependencies": {
37
- "@memorix/agent-core": "1.1.11",
38
- "@memorix/ai": "1.1.11",
39
- "@memorix/tui": "1.1.11",
38
+ "@memorix/agent-core": "1.1.12",
39
+ "@memorix/ai": "1.1.12",
40
+ "@memorix/tui": "1.1.12",
40
41
  "@opentui/core": "^0.3.4",
41
42
  "@opentui/react": "^0.3.4",
42
43
  "@silvia-odwyer/photon-node": "0.3.4",
package/dist/sdk.js CHANGED
@@ -2813,6 +2813,45 @@ function dimsCacheKey(config) {
2813
2813
  config.requestedDimensions ?? "native"
2814
2814
  ].join("|");
2815
2815
  }
2816
+ function isValidDimension(value) {
2817
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
2818
+ }
2819
+ async function loadCachedVectorDimensions(config) {
2820
+ try {
2821
+ const raw = await readFile2(CACHE_META_FILE, "utf-8");
2822
+ const data = JSON.parse(raw);
2823
+ const namespace = cacheNamespace(config);
2824
+ if (!Array.isArray(data?.entries)) return null;
2825
+ const entry = data.entries.find(
2826
+ (candidate) => typeof candidate === "object" && candidate !== null && candidate.namespace === namespace && isValidDimension(candidate.dimensions)
2827
+ );
2828
+ return entry?.dimensions ?? null;
2829
+ } catch {
2830
+ return null;
2831
+ }
2832
+ }
2833
+ async function saveCachedVectorDimensions(config, dimensions) {
2834
+ if (!isValidDimension(dimensions)) return;
2835
+ try {
2836
+ await mkdir2(CACHE_DIR2, { recursive: true });
2837
+ let entries = [];
2838
+ try {
2839
+ const raw = await readFile2(CACHE_META_FILE, "utf-8");
2840
+ const data = JSON.parse(raw);
2841
+ if (Array.isArray(data?.entries)) {
2842
+ entries = data.entries.filter(
2843
+ (candidate) => typeof candidate === "object" && candidate !== null && typeof candidate.namespace === "string" && isValidDimension(candidate.dimensions) && typeof candidate.ts === "number"
2844
+ );
2845
+ }
2846
+ } catch {
2847
+ }
2848
+ const namespace = cacheNamespace(config);
2849
+ entries = entries.filter((entry) => entry.namespace !== namespace);
2850
+ entries.push({ namespace, dimensions, ts: Date.now() });
2851
+ await writeFile2(CACHE_META_FILE, JSON.stringify({ version: 1, entries }));
2852
+ } catch {
2853
+ }
2854
+ }
2816
2855
  async function loadCachedDims(config) {
2817
2856
  try {
2818
2857
  const raw = await readFile2(DIMS_CACHE_FILE, "utf-8");
@@ -2875,6 +2914,7 @@ async function saveCachedDims(config, dimensions) {
2875
2914
  await writeFile2(DIMS_CACHE_FILE, JSON.stringify({ entries }));
2876
2915
  } catch {
2877
2916
  }
2917
+ await saveCachedVectorDimensions(config, dimensions);
2878
2918
  }
2879
2919
  async function saveDiskCacheNow() {
2880
2920
  if (!diskCacheDirty2) return;
@@ -2957,7 +2997,7 @@ async function fetchWithRetry(url, apiKey, body, attempt = 0) {
2957
2997
  const errorText2 = await response.text().catch(() => "unknown error");
2958
2998
  throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
2959
2999
  }
2960
- var CACHE_DIR2, CACHE_FILE2, DIMS_CACHE_FILE, cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES, BASE_DELAY_MS, APIEmbeddingProvider;
3000
+ var CACHE_DIR2, CACHE_FILE2, DIMS_CACHE_FILE, CACHE_META_FILE, cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES, BASE_DELAY_MS, APIEmbeddingProvider;
2961
3001
  var init_api_provider = __esm({
2962
3002
  "src/embedding/api-provider.ts"() {
2963
3003
  "use strict";
@@ -2965,6 +3005,7 @@ var init_api_provider = __esm({
2965
3005
  CACHE_DIR2 = process.env.MEMORIX_DATA_DIR || join3(homedir7(), ".memorix", "data");
2966
3006
  CACHE_FILE2 = join3(CACHE_DIR2, ".embedding-api-cache.json");
2967
3007
  DIMS_CACHE_FILE = join3(CACHE_DIR2, ".embedding-dims-cache.json");
3008
+ CACHE_META_FILE = join3(CACHE_DIR2, ".embedding-api-cache-meta.json");
2968
3009
  cache3 = /* @__PURE__ */ new Map();
2969
3010
  MAX_CACHE_SIZE3 = 1e4;
2970
3011
  diskCacheDirty2 = false;
@@ -2991,13 +3032,22 @@ var init_api_provider = __esm({
2991
3032
  this.dimensions = detectedDimensions;
2992
3033
  this.name = `api-${config.model.replace(/\//g, "-")}`;
2993
3034
  }
2994
- static async create() {
3035
+ static async create(options = {}) {
2995
3036
  const config = _APIEmbeddingProvider.resolveConfig();
2996
- startDiskCacheLoad();
3037
+ const allowNetworkProbe = options.allowNetworkProbe !== false;
2997
3038
  let probeDimensions = await loadCachedDims(config);
3039
+ let dimensionSource = "dims-cache";
3040
+ if (probeDimensions === null) {
3041
+ probeDimensions = await loadCachedVectorDimensions(config);
3042
+ if (probeDimensions !== null) dimensionSource = "vector-cache";
3043
+ }
2998
3044
  if (probeDimensions !== null) {
2999
- console.error(`[memorix] API embedding: ${config.model} @ ${config.baseUrl} (${probeDimensions}d) [cached dims]`);
3045
+ console.error(`[memorix] API embedding: ${config.model} @ ${config.baseUrl} (${probeDimensions}d) [${dimensionSource}]`);
3046
+ if (dimensionSource === "dims-cache") {
3047
+ void saveCachedVectorDimensions(config, probeDimensions);
3048
+ }
3000
3049
  } else {
3050
+ if (!allowNetworkProbe) return null;
3001
3051
  probeDimensions = await _APIEmbeddingProvider.probeAPI(config);
3002
3052
  console.error(`[memorix] API embedding: ${config.model} @ ${config.baseUrl} (${probeDimensions}d)`);
3003
3053
  saveCachedDims(config, probeDimensions).catch(() => {
@@ -3006,6 +3056,7 @@ var init_api_provider = __esm({
3006
3056
  if (config.requestedDimensions) {
3007
3057
  console.error(`[memorix] Dimension shortening: ${config.requestedDimensions}d requested`);
3008
3058
  }
3059
+ startDiskCacheLoad();
3009
3060
  return new _APIEmbeddingProvider(config, probeDimensions);
3010
3061
  }
3011
3062
  static resolveConfig() {
@@ -3184,6 +3235,13 @@ var init_api_provider = __esm({
3184
3235
  scheduleDiskSave();
3185
3236
  return results;
3186
3237
  }
3238
+ async getCachedEmbeddings(texts) {
3239
+ await ensureDiskCacheLoaded();
3240
+ return texts.map((text) => {
3241
+ const hash = textHash2(normalizeText(text), this.cacheKeyNamespace);
3242
+ return cache3.get(hash) ?? null;
3243
+ });
3244
+ }
3187
3245
  getStats() {
3188
3246
  return {
3189
3247
  totalTokens: this.totalTokensUsed,
@@ -3292,10 +3350,10 @@ async function createTransformersProvider() {
3292
3350
  return null;
3293
3351
  }
3294
3352
  }
3295
- async function createAPIProvider() {
3353
+ async function createAPIProvider(options) {
3296
3354
  try {
3297
3355
  const { APIEmbeddingProvider: APIEmbeddingProvider2 } = await Promise.resolve().then(() => (init_api_provider(), api_provider_exports));
3298
- return await APIEmbeddingProvider2.create();
3356
+ return await APIEmbeddingProvider2.create(options ?? {});
3299
3357
  } catch (e) {
3300
3358
  warnOnce(`[memorix] Failed to init API embedding: ${e instanceof Error ? e.message : e}`);
3301
3359
  return null;
@@ -3350,11 +3408,24 @@ function wrapProvider(candidate) {
3350
3408
  }
3351
3409
  throw error;
3352
3410
  }
3353
- }
3411
+ },
3412
+ ...candidate.getCachedEmbeddings ? {
3413
+ async getCachedEmbeddings(texts) {
3414
+ return candidate.getCachedEmbeddings(texts);
3415
+ }
3416
+ } : {}
3354
3417
  };
3355
3418
  }
3356
- async function getEmbeddingProvider() {
3419
+ async function getEmbeddingProvider(options = {}) {
3357
3420
  if (provider) return provider;
3421
+ if (options.allowNetworkProbe === false) {
3422
+ const mode = getEmbeddingMode2();
3423
+ if (mode !== "api" && !(mode === "auto" && hasAPIEmbeddingConfig())) {
3424
+ return null;
3425
+ }
3426
+ const cached = await createAPIProvider({ allowNetworkProbe: false });
3427
+ return cached ? wrapProvider(cached) : null;
3428
+ }
3358
3429
  if (lastInitWasTemporaryFailure) {
3359
3430
  const elapsed = Date.now() - lastFailureTimestamp;
3360
3431
  if (elapsed < RETRY_COOLDOWN_MS) {
@@ -4867,12 +4938,15 @@ __export(orama_store_exports, {
4867
4938
  batchGenerateEmbeddings: () => batchGenerateEmbeddings,
4868
4939
  generateEmbedding: () => generateEmbedding,
4869
4940
  getDb: () => getDb,
4941
+ getDeferredCachedVectorHydration: () => getDeferredCachedVectorHydration,
4870
4942
  getLastSearchMode: () => getLastSearchMode,
4871
4943
  getObservationCount: () => getObservationCount,
4872
4944
  getObservationsByIds: () => getObservationsByIds,
4873
4945
  getTimeline: () => getTimeline,
4874
4946
  getVectorDimensions: () => getVectorDimensions,
4947
+ hasObservationVector: () => hasObservationVector,
4875
4948
  hydrateIndex: () => hydrateIndex,
4949
+ hydrateIndexForStartup: () => hydrateIndexForStartup,
4876
4950
  insertObservation: () => insertObservation,
4877
4951
  isEmbeddingEnabled: () => isEmbeddingEnabled,
4878
4952
  makeOramaObservationId: () => makeOramaObservationId,
@@ -4933,11 +5007,10 @@ function stripVectorSearchParams(params) {
4933
5007
  const { mode, vector, similarity, hybridWeights, ...rest } = params;
4934
5008
  return rest;
4935
5009
  }
4936
- async function getDb() {
4937
- if (db) return db;
4938
- const provider2 = await getEmbeddingProvider();
4939
- embeddingEnabled = provider2 !== null;
4940
- embeddingDimensions = provider2?.dimensions ?? null;
5010
+ async function initializeDb(options, generation) {
5011
+ const provider2 = await getEmbeddingProvider({ allowNetworkProbe: options.allowNetworkProbe });
5012
+ const nextEmbeddingEnabled = provider2 !== null;
5013
+ const nextEmbeddingDimensions = provider2?.dimensions ?? null;
4941
5014
  const baseSchema = {
4942
5015
  id: "string",
4943
5016
  observationId: "number",
@@ -4960,15 +5033,39 @@ async function getDb() {
4960
5033
  documentType: "string",
4961
5034
  knowledgeLayer: "string"
4962
5035
  };
4963
- const dims = embeddingDimensions ?? 384;
4964
- const schema = embeddingEnabled ? { ...baseSchema, embedding: `vector[${dims}]` } : baseSchema;
4965
- db = await create({ schema });
4966
- return db;
5036
+ const dims = nextEmbeddingDimensions ?? 384;
5037
+ const schema = nextEmbeddingEnabled ? { ...baseSchema, embedding: `vector[${dims}]` } : baseSchema;
5038
+ const nextDb = await create({ schema });
5039
+ if (generation !== dbGeneration) {
5040
+ return getDb(options);
5041
+ }
5042
+ indexEmbeddingProvider = provider2;
5043
+ embeddingEnabled = nextEmbeddingEnabled;
5044
+ embeddingDimensions = nextEmbeddingDimensions;
5045
+ db = nextDb;
5046
+ return nextDb;
5047
+ }
5048
+ async function getDb(options = {}) {
5049
+ if (db) return db;
5050
+ if (dbInitPromise) return dbInitPromise;
5051
+ const initPromise2 = initializeDb(options, dbGeneration);
5052
+ dbInitPromise = initPromise2;
5053
+ try {
5054
+ return await initPromise2;
5055
+ } finally {
5056
+ if (dbInitPromise === initPromise2) {
5057
+ dbInitPromise = null;
5058
+ }
5059
+ }
4967
5060
  }
4968
5061
  async function resetDb() {
5062
+ dbGeneration++;
4969
5063
  db = null;
5064
+ dbInitPromise = null;
4970
5065
  embeddingEnabled = false;
4971
5066
  embeddingDimensions = null;
5067
+ indexEmbeddingProvider = null;
5068
+ deferredCachedVectorHydration = null;
4972
5069
  lastSearchModeByProject.clear();
4973
5070
  docByObservationKey.clear();
4974
5071
  }
@@ -4996,14 +5093,64 @@ async function batchGenerateEmbeddings(texts) {
4996
5093
  return texts.map(() => null);
4997
5094
  }
4998
5095
  }
4999
- async function hydrateIndex(observations2) {
5000
- const database = await getDb();
5096
+ async function getCachedEmbeddings(texts) {
5097
+ if (!embeddingEnabled || texts.length === 0) return texts.map(() => null);
5098
+ const provider2 = indexEmbeddingProvider;
5099
+ if (!provider2?.getCachedEmbeddings) return texts.map(() => null);
5100
+ try {
5101
+ return await provider2.getCachedEmbeddings(texts);
5102
+ } catch {
5103
+ return texts.map(() => null);
5104
+ }
5105
+ }
5106
+ function observationEmbeddingText(observation) {
5107
+ return [
5108
+ observation.title ?? "",
5109
+ observation.narrative ?? "",
5110
+ ...Array.isArray(observation.facts) ? observation.facts : []
5111
+ ].join(" ");
5112
+ }
5113
+ function documentEmbeddingText(document) {
5114
+ return [document.title ?? "", document.narrative ?? "", document.facts ?? ""].join(" ");
5115
+ }
5116
+ function isCompatibleCachedVector(vector) {
5117
+ return Boolean(
5118
+ vector && embeddingDimensions !== null && vector.length === embeddingDimensions && vector.every(Number.isFinite)
5119
+ );
5120
+ }
5121
+ async function attachCachedVectors(database, candidates) {
5122
+ const cachedVectors = await getCachedEmbeddings(
5123
+ candidates.map(({ observation }) => observationEmbeddingText(observation))
5124
+ );
5125
+ if (db !== database) return;
5126
+ for (let index = 0; index < candidates.length; index++) {
5127
+ const vector = cachedVectors[index];
5128
+ if (!isCompatibleCachedVector(vector)) continue;
5129
+ const existing = getByID(database, candidates[index].id);
5130
+ if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(candidates[index].observation)) continue;
5131
+ try {
5132
+ await update(database, candidates[index].id, { ...existing, embedding: vector });
5133
+ } catch {
5134
+ }
5135
+ }
5136
+ }
5137
+ async function hydrateIndex(observations2, options = {}) {
5138
+ const database = await getDb({ allowNetworkProbe: options.allowNetworkProbe });
5139
+ const candidates = [];
5140
+ for (const observation of observations2) {
5141
+ if (!observation || !observation.id || !observation.projectId) continue;
5142
+ const id = makeOramaObservationId(observation.projectId, observation.id);
5143
+ if (getByID(database, id)) continue;
5144
+ candidates.push({ observation, id });
5145
+ }
5146
+ const deferCachedVectors = options.deferCachedVectors === true && Boolean(indexEmbeddingProvider?.getCachedEmbeddings);
5147
+ const cachedVectors = deferCachedVectors ? candidates.map(() => null) : await getCachedEmbeddings(candidates.map(({ observation }) => observationEmbeddingText(observation)));
5001
5148
  let inserted = 0;
5002
- for (const obs of observations2) {
5003
- if (!obs || !obs.id || !obs.projectId) continue;
5149
+ for (let index = 0; index < candidates.length; index++) {
5150
+ const { observation: obs, id } = candidates[index];
5004
5151
  try {
5005
- const id = makeOramaObservationId(obs.projectId, obs.id);
5006
- if (getByID(database, id)) continue;
5152
+ const vector = cachedVectors[index];
5153
+ const compatibleVector = isCompatibleCachedVector(vector) ? vector : null;
5007
5154
  const doc = {
5008
5155
  id,
5009
5156
  observationId: obs.id,
@@ -5022,7 +5169,8 @@ async function hydrateIndex(observations2) {
5022
5169
  status: obs.status ?? "active",
5023
5170
  source: obs.source || "agent",
5024
5171
  documentType: "observation",
5025
- knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source)
5172
+ knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
5173
+ ...compatibleVector ? { embedding: compatibleVector } : {}
5026
5174
  };
5027
5175
  await insert2(database, doc);
5028
5176
  rememberObservationDoc(doc);
@@ -5030,8 +5178,24 @@ async function hydrateIndex(observations2) {
5030
5178
  } catch {
5031
5179
  }
5032
5180
  }
5181
+ deferredCachedVectorHydration = deferCachedVectors ? attachCachedVectors(database, candidates).catch(() => {
5182
+ }) : null;
5033
5183
  return inserted;
5034
5184
  }
5185
+ async function hydrateIndexForStartup(observations2) {
5186
+ return hydrateIndex(observations2, {
5187
+ allowNetworkProbe: false,
5188
+ deferCachedVectors: true
5189
+ });
5190
+ }
5191
+ function getDeferredCachedVectorHydration() {
5192
+ return deferredCachedVectorHydration;
5193
+ }
5194
+ function hasObservationVector(projectId, observationId) {
5195
+ if (!db || !embeddingEnabled || embeddingDimensions === null) return false;
5196
+ const document = getByID(db, makeOramaObservationId(projectId, observationId));
5197
+ return Array.isArray(document?.embedding) && document.embedding.length === embeddingDimensions && document.embedding.every(Number.isFinite);
5198
+ }
5035
5199
  async function insertObservation(doc) {
5036
5200
  const database = await getDb();
5037
5201
  await insert2(database, doc);
@@ -5565,7 +5729,7 @@ function formatTime(isoDate) {
5565
5729
  return isoDate;
5566
5730
  }
5567
5731
  }
5568
- var db, embeddingEnabled, embeddingDimensions, docByObservationKey, NON_CJK_HYBRID_SIMILARITY, lastSearchModeByProject, SEARCH_MODE_DEFAULT_KEY, COMMAND_LOG_TITLE2, COMMAND_STYLE_TITLE, COMMAND_LIKE_QUERY2, COMMAND_INTENT_QUERY, NATURAL_LANGUAGE_MARKERS;
5732
+ var db, dbInitPromise, dbGeneration, embeddingEnabled, embeddingDimensions, indexEmbeddingProvider, deferredCachedVectorHydration, docByObservationKey, NON_CJK_HYBRID_SIMILARITY, lastSearchModeByProject, SEARCH_MODE_DEFAULT_KEY, COMMAND_LOG_TITLE2, COMMAND_STYLE_TITLE, COMMAND_LIKE_QUERY2, COMMAND_INTENT_QUERY, NATURAL_LANGUAGE_MARKERS;
5569
5733
  var init_orama_store = __esm({
5570
5734
  "src/store/orama-store.ts"() {
5571
5735
  "use strict";
@@ -5577,8 +5741,12 @@ var init_orama_store = __esm({
5577
5741
  init_intent_detector();
5578
5742
  init_query_expansion();
5579
5743
  db = null;
5744
+ dbInitPromise = null;
5745
+ dbGeneration = 0;
5580
5746
  embeddingEnabled = false;
5581
5747
  embeddingDimensions = null;
5748
+ indexEmbeddingProvider = null;
5749
+ deferredCachedVectorHydration = null;
5582
5750
  docByObservationKey = /* @__PURE__ */ new Map();
5583
5751
  NON_CJK_HYBRID_SIMILARITY = 0.45;
5584
5752
  lastSearchModeByProject = /* @__PURE__ */ new Map();
@@ -7359,7 +7527,18 @@ async function bindObservationCodeRefsBestEffort(observation) {
7359
7527
  function isVectorCompatibleWithCurrentIndex(embedding) {
7360
7528
  if (!embedding) return false;
7361
7529
  const vectorDimensions = getVectorDimensions();
7362
- return vectorDimensions === null || embedding.length === vectorDimensions;
7530
+ return vectorDimensions !== null && embedding.length === vectorDimensions && embedding.every(Number.isFinite);
7531
+ }
7532
+ async function upgradeVectorSchemaAfterFirstEmbedding(embedding) {
7533
+ if (isVectorCompatibleWithCurrentIndex(embedding)) return true;
7534
+ if (getVectorDimensions() !== null || !embedding.every(Number.isFinite)) return false;
7535
+ const targetProjectDir = projectDir;
7536
+ if (!vectorSchemaUpgradePromise) {
7537
+ vectorSchemaUpgradePromise = reindexObservations().then(() => projectDir === targetProjectDir && isVectorCompatibleWithCurrentIndex(embedding)).catch(() => false).finally(() => {
7538
+ vectorSchemaUpgradePromise = null;
7539
+ });
7540
+ }
7541
+ return vectorSchemaUpgradePromise;
7363
7542
  }
7364
7543
  async function initObservations(dir) {
7365
7544
  if (projectDir === dir) return;
@@ -7369,6 +7548,7 @@ async function initObservations(dir) {
7369
7548
  nextId = await store.loadIdCounter();
7370
7549
  projectDir = dir;
7371
7550
  searchIndexPrepared = false;
7551
+ vectorSchemaUpgradePromise = null;
7372
7552
  }
7373
7553
  async function ensureFreshObservations() {
7374
7554
  if (!projectDir) return false;
@@ -7548,6 +7728,9 @@ async function storeObservation(input) {
7548
7728
  const searchableText = [input.title, input.narrative, ...input.facts ?? []].join(" ");
7549
7729
  generateEmbedding(searchableText).then(async (embedding) => {
7550
7730
  if (embedding) {
7731
+ if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7732
+ await upgradeVectorSchemaAfterFirstEmbedding(embedding);
7733
+ }
7551
7734
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7552
7735
  const vectorDimensions = getVectorDimensions();
7553
7736
  console.error(
@@ -7658,6 +7841,13 @@ async function upsertObservation(existing, input, now) {
7658
7841
  vectorMissingIds.add(obsId);
7659
7842
  generateEmbedding(searchableText).then(async (embedding) => {
7660
7843
  if (embedding) {
7844
+ if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7845
+ await upgradeVectorSchemaAfterFirstEmbedding(embedding);
7846
+ }
7847
+ if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7848
+ queueVectorBackfill(existing.projectId);
7849
+ return;
7850
+ }
7661
7851
  try {
7662
7852
  const { removeObservation: removeObs } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
7663
7853
  await removeObs(makeOramaObservationId(existing.projectId, obsId));
@@ -7802,14 +7992,21 @@ async function reindexObservations() {
7802
7992
  let embeddings = observations.map(() => null);
7803
7993
  const provider2 = await getEmbeddingProvider();
7804
7994
  const canBatchEmbedAtStartup = provider2 !== null && !provider2.name.startsWith("api-");
7995
+ await getDb();
7996
+ const texts = observations.map(
7997
+ (obs) => [obs.title, obs.narrative, ...obs.facts].join(" ")
7998
+ );
7999
+ if (provider2?.getCachedEmbeddings) {
8000
+ try {
8001
+ embeddings = await provider2.getCachedEmbeddings(texts);
8002
+ } catch {
8003
+ }
8004
+ }
7805
8005
  if (provider2 && !canBatchEmbedAtStartup) {
7806
- console.error("[memorix] Startup reindex: skipping synchronous API embeddings; background backfill will hydrate vectors");
8006
+ console.error("[memorix] Startup reindex: restored cached API embeddings; uncached vectors stay in background recovery");
7807
8007
  }
7808
8008
  if (canBatchEmbedAtStartup) {
7809
8009
  try {
7810
- const texts = observations.map(
7811
- (obs) => [obs.title, obs.narrative, ...obs.facts].join(" ")
7812
- );
7813
8010
  embeddings = await batchGenerateEmbeddings(texts);
7814
8011
  } catch {
7815
8012
  }
@@ -7862,16 +8059,31 @@ async function reindexObservations() {
7862
8059
  }
7863
8060
  async function prepareSearchIndex() {
7864
8061
  if (searchIndexPrepared) return 0;
7865
- const count2 = await hydrateIndex(observations);
8062
+ const count2 = await hydrateIndexForStartup(observations);
7866
8063
  if (count2 === 0) {
7867
8064
  searchIndexPrepared = true;
7868
8065
  return 0;
7869
8066
  }
7870
- vectorMissingIds.clear();
7871
- if (isEmbeddingEnabled()) {
7872
- for (const obs of observations) {
7873
- vectorMissingIds.add(obs.id);
8067
+ const hydratedProjectDir = projectDir;
8068
+ const queueMissingVectors = () => {
8069
+ if (projectDir !== hydratedProjectDir) return;
8070
+ vectorMissingIds.clear();
8071
+ if (!isEmbeddingExplicitlyDisabled()) {
8072
+ const projectsWithMissingVectors = /* @__PURE__ */ new Set();
8073
+ for (const obs of observations) {
8074
+ if (!hasObservationVector(obs.projectId, obs.id)) {
8075
+ vectorMissingIds.add(obs.id);
8076
+ projectsWithMissingVectors.add(obs.projectId);
8077
+ }
8078
+ }
8079
+ for (const projectId of projectsWithMissingVectors) queueVectorBackfill(projectId);
7874
8080
  }
8081
+ };
8082
+ const cacheHydration = getDeferredCachedVectorHydration();
8083
+ if (cacheHydration) {
8084
+ void cacheHydration.finally(queueMissingVectors);
8085
+ } else {
8086
+ queueMissingVectors();
7875
8087
  }
7876
8088
  searchIndexPrepared = true;
7877
8089
  return count2;
@@ -7931,6 +8143,9 @@ async function backfillVectorEmbeddings(options = {}) {
7931
8143
  try {
7932
8144
  const embedding = await generateEmbedding(text);
7933
8145
  if (embedding) {
8146
+ if (!isVectorCompatibleWithCurrentIndex(embedding)) {
8147
+ await upgradeVectorSchemaAfterFirstEmbedding(embedding);
8148
+ }
7934
8149
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7935
8150
  const vectorDimensions = getVectorDimensions();
7936
8151
  console.error(
@@ -7993,7 +8208,7 @@ async function backfillVectorEmbeddings(options = {}) {
7993
8208
  }
7994
8209
  return { attempted: ids.length, succeeded, failed };
7995
8210
  }
7996
- var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
8211
+ var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
7997
8212
  var init_observations = __esm({
7998
8213
  "src/memory/observations.ts"() {
7999
8214
  "use strict";
@@ -8011,6 +8226,7 @@ var init_observations = __esm({
8011
8226
  searchIndexPrepared = false;
8012
8227
  vectorMissingIds = /* @__PURE__ */ new Set();
8013
8228
  vectorBackfillRunning = false;
8229
+ vectorSchemaUpgradePromise = null;
8014
8230
  lastVectorBackfill = null;
8015
8231
  embeddingFailureLogTimestamps = /* @__PURE__ */ new Map();
8016
8232
  EMBEDDING_FAILURE_LOG_COOLDOWN_MS = 3e4;
@@ -16434,6 +16650,7 @@ __export(installers_exports, {
16434
16650
  getProjectConfigPath: () => getProjectConfigPath,
16435
16651
  installAgentGuidance: () => installAgentGuidance,
16436
16652
  installHooks: () => installHooks,
16653
+ resolveOpenCodeHookCommand: () => resolveOpenCodeHookCommand,
16437
16654
  uninstallHooks: () => uninstallHooks
16438
16655
  });
16439
16656
  import * as fs14 from "fs/promises";
@@ -16446,6 +16663,22 @@ function resolveHookCommand() {
16446
16663
  }
16447
16664
  return "memorix";
16448
16665
  }
16666
+ function resolveWindowsMemorixShim() {
16667
+ try {
16668
+ const output = String(execSync3("where.exe memorix.cmd", {
16669
+ encoding: "utf-8",
16670
+ stdio: ["ignore", "pipe", "ignore"],
16671
+ windowsHide: true
16672
+ }));
16673
+ return output.split(/\r?\n/).map((candidate) => candidate.trim()).find((candidate) => path19.isAbsolute(candidate)) ?? null;
16674
+ } catch {
16675
+ return null;
16676
+ }
16677
+ }
16678
+ function resolveOpenCodeHookCommand(platform = process.platform, resolveWindowsShim = resolveWindowsMemorixShim) {
16679
+ if (platform !== "win32") return "memorix";
16680
+ return resolveWindowsShim() ?? "memorix.cmd";
16681
+ }
16449
16682
  function generateClaudeConfig() {
16450
16683
  const cmd = `${resolveHookCommand()} hook`;
16451
16684
  const hookEntry = {
@@ -16645,6 +16878,7 @@ async function installOfficialSkillsForAgent(agent, projectRoot, global = false)
16645
16878
  return skillPaths;
16646
16879
  }
16647
16880
  function generateOpenCodePlugin() {
16881
+ const hookCommand = JSON.stringify(resolveOpenCodeHookCommand());
16648
16882
  return `/**
16649
16883
  * Memorix - Cross-Agent Memory Bridge Plugin for OpenCode
16650
16884
  * @generated-version ${OPENCODE_PLUGIN_VERSION}
@@ -16663,6 +16897,16 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
16663
16897
  const sessionId = \`opencode-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2, 8)}\`;
16664
16898
  let pendingAssistantResponse = null;
16665
16899
  let lastDeliveredAssistantKey = '';
16900
+ let hookFailureReported = false;
16901
+ const hookCommand = ${hookCommand};
16902
+
16903
+ function reportHookFailure(eventName, detail) {
16904
+ // OpenCode renders console.error output inside the conversation. Delivery is
16905
+ // best-effort, so keep normal sessions quiet while retaining opt-in diagnostics.
16906
+ if (process.env.MEMORIX_HOOK_DEBUG !== '1' || hookFailureReported) return;
16907
+ hookFailureReported = true;
16908
+ console.error('[memorix-plugin] hook delivery failed:', eventName, detail);
16909
+ }
16666
16910
 
16667
16911
  /**
16668
16912
  * Send event JSON to \`memorix hook\` via child_process.spawnSync.
@@ -16678,8 +16922,7 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
16678
16922
  const data = JSON.stringify(payload);
16679
16923
  const eventName = payload.hook_event_name || 'unknown';
16680
16924
  try {
16681
- const cmd = process.platform === 'win32' ? 'memorix.cmd' : 'memorix';
16682
- const result = spawnSync(cmd, ['hook'], {
16925
+ const result = spawnSync(hookCommand, ['hook'], {
16683
16926
  input: data,
16684
16927
  timeout: 10_000,
16685
16928
  encoding: 'utf-8',
@@ -16687,12 +16930,14 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
16687
16930
  shell: process.platform === 'win32',
16688
16931
  });
16689
16932
  if (result.status !== 0) {
16690
- console.error('[memorix-plugin] hook failed:', eventName,
16691
- 'exit=', result.status,
16692
- 'stderr=', (result.stderr || '').slice(0, 200));
16933
+ reportHookFailure(eventName, {
16934
+ exit: result.status,
16935
+ stderr: (result.stderr || '').slice(0, 200),
16936
+ error: result.error?.message,
16937
+ });
16693
16938
  }
16694
16939
  } catch (e) {
16695
- console.error('[memorix-plugin] hook delivery failed:', eventName, e?.message ?? e);
16940
+ reportHookFailure(eventName, e?.message ?? e);
16696
16941
  }
16697
16942
  }
16698
16943
 
@@ -17519,7 +17764,7 @@ var init_installers = __esm({
17519
17764
  "use strict";
17520
17765
  init_esm_shims();
17521
17766
  init_official_skills();
17522
- OPENCODE_PLUGIN_VERSION = 6;
17767
+ OPENCODE_PLUGIN_VERSION = 7;
17523
17768
  AGENT_SKILL_DIRS = {
17524
17769
  cursor: { project: path19.join(".cursor", "skills"), global: path19.join(".cursor", "skills") },
17525
17770
  windsurf: { project: path19.join(".windsurf", "skills"), global: path19.join(".windsurf", "skills") },
@@ -21554,7 +21799,7 @@ The path should point to a directory containing a .git folder.`
21554
21799
  };
21555
21800
  const server = existingServer ?? new McpServer({
21556
21801
  name: "memorix",
21557
- version: true ? "1.1.11" : "1.0.1"
21802
+ version: true ? "1.1.13" : "1.0.1"
21558
21803
  });
21559
21804
  const originalRegisterTool = server.registerTool.bind(server);
21560
21805
  server.registerTool = ((name, ...args) => {