@oh-my-pi/pi-mnemopi 17.0.2 → 17.0.4

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.4] - 2026-07-18
6
+
7
+ ### Fixed
8
+
9
+ - Fixed a corrupt cached embedding model (truncated `model_optimized.onnx`, `Protobuf parsing failed` on load) permanently disabling local embeddings: init now quarantines the broken cache file (rename to `*.corrupt-<ts>`, only when the path resolves inside the fastembed cache directory) and retries once so the model re-downloads.
10
+
5
11
  ## [17.0.1] - 2026-07-16
6
12
 
7
13
  ### Fixed
@@ -19,6 +19,27 @@ export type LocalModelInitOptions = {
19
19
  showDownloadProgress?: boolean;
20
20
  };
21
21
  export type LocalModelInitializer = (options: LocalModelInitOptions) => Promise<LocalEmbeddingModel>;
22
+ /**
23
+ * Quarantine the exact ONNX file named by a "Protobuf parsing failed" init
24
+ * error. A truncated cached model blocks local embeddings forever: the
25
+ * downloader treats the existing file as complete, so every init re-parses
26
+ * the same broken bytes. The extracted path is error-message CONTENT, so it
27
+ * is only honored when it resolves inside the fastembed cache directory —
28
+ * never rename an arbitrary file a dependency happens to mention. Atomic
29
+ * rename; losing a concurrent-heal race (file already renamed/removed)
30
+ * still returns true because a retry is safe either way.
31
+ * @internal exported for tests
32
+ */
33
+ export declare function quarantineCorruptModelFile(message: string, cacheDir?: string): Promise<boolean>;
34
+ /**
35
+ * Shared local-model initializer: FlagEmbedding.init with BOTH cache heals.
36
+ * Missing sidecars (config/tokenizer/tokens map) re-fetch and retry; a
37
+ * corrupt model blob (Protobuf parse failure) quarantines the file and
38
+ * retries THROUGH the sidecar heal, so a cache that is broken in both ways
39
+ * still recovers in one pass. Also the initializer the embed worker uses in
40
+ * its subprocess; the in-process seam stays {@link setLocalModelInitializer}.
41
+ */
42
+ export declare function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel>;
22
43
  export declare function embeddingsDisabled(): boolean;
23
44
  /**
24
45
  * Resolve the embedding model name for the currently active runtime scope.
@@ -1,6 +1,6 @@
1
1
  export { configureRecallFeatures, type RecallFeatureFlags } from "../config.js";
2
2
  export * from "./banks.js";
3
3
  export * from "./beam/index.js";
4
- export { type LocalEmbeddingModel, type LocalModelInitializer, type LocalModelInitOptions, type StandardEmbeddingModel, setLocalModelInitializer, } from "./embeddings.js";
4
+ export { defaultLocalModelInitializer, type LocalEmbeddingModel, type LocalModelInitializer, type LocalModelInitOptions, type StandardEmbeddingModel, setLocalModelInitializer, } from "./embeddings.js";
5
5
  export * from "./memory.js";
6
6
  export { addMemory, forget, get, getBank, getContext, getDefaultInstance, getStats, Mnemopi, query, recall, recallEnhanced, remember, resetDefaultInstanceForTests, resetMemoryForTests, resetModuleStateForTests, saveMemory, scratchpadClear, scratchpadRead, scratchpadWrite, search, setBank, sleep, sleepAllSessions, storeMemory, update, } from "./memory.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-mnemopi",
4
- "version": "17.0.2",
4
+ "version": "17.0.4",
5
5
  "description": "Local SQLite memory engine for Oh My Pi agents",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "17.0.2",
43
- "@oh-my-pi/pi-catalog": "17.0.2",
44
- "@oh-my-pi/pi-utils": "17.0.2",
42
+ "@oh-my-pi/pi-ai": "17.0.4",
43
+ "@oh-my-pi/pi-catalog": "17.0.4",
44
+ "@oh-my-pi/pi-utils": "17.0.4",
45
45
  "lru-cache": "11.5.2"
46
46
  },
47
47
  "peerDependencies": {
@@ -1,4 +1,6 @@
1
1
  import { mkdirSync } from "node:fs";
2
+ import * as fsp from "node:fs/promises";
3
+ import * as nodePath from "node:path";
2
4
  import { type ApiKey, getOpenRouterHeaders, withAuth } from "@oh-my-pi/pi-ai";
3
5
  import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
4
6
  import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
@@ -62,21 +64,67 @@ const queryCache = new LRUCache<string, Vector>({ max: QUERY_CACHE_MAX });
62
64
  const providerIds = new WeakMap<object, number>();
63
65
  let nextProviderId = 1;
64
66
 
65
- async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
67
+ /**
68
+ * Quarantine the exact ONNX file named by a "Protobuf parsing failed" init
69
+ * error. A truncated cached model blocks local embeddings forever: the
70
+ * downloader treats the existing file as complete, so every init re-parses
71
+ * the same broken bytes. The extracted path is error-message CONTENT, so it
72
+ * is only honored when it resolves inside the fastembed cache directory —
73
+ * never rename an arbitrary file a dependency happens to mention. Atomic
74
+ * rename; losing a concurrent-heal race (file already renamed/removed)
75
+ * still returns true because a retry is safe either way.
76
+ * @internal exported for tests
77
+ */
78
+ export async function quarantineCorruptModelFile(message: string, cacheDir?: string): Promise<boolean> {
79
+ const match = /Load model from (.+?\.onnx) failed:.*Protobuf parsing failed/i.exec(message);
80
+ if (!match) return false;
81
+ const modelFile = nodePath.resolve(match[1]);
82
+ const cacheRoot = nodePath.resolve(cacheDir ?? getFastembedCacheDir());
83
+ if (!modelFile.startsWith(cacheRoot + nodePath.sep)) return false;
84
+ try {
85
+ await fsp.rename(modelFile, `${modelFile}.corrupt-${Date.now()}`);
86
+ logger.warn("mnemopi: quarantined corrupt local embedding model; retrying init", { modelFile });
87
+ } catch {
88
+ // Concurrent heal or vanished file: the single retry stays safe. A
89
+ // rename that failed with the file still in place just makes the
90
+ // retry surface the original error again.
91
+ }
92
+ return true;
93
+ }
94
+
95
+ const SIDECAR_ERROR_RE =
96
+ /(?:Config file not found at .*config|Tokenizer file not found at .*tokenizer|Tokens map file not found at .*special_tokens_map)/u;
97
+
98
+ /**
99
+ * Shared local-model initializer: FlagEmbedding.init with BOTH cache heals.
100
+ * Missing sidecars (config/tokenizer/tokens map) re-fetch and retry; a
101
+ * corrupt model blob (Protobuf parse failure) quarantines the file and
102
+ * retries THROUGH the sidecar heal, so a cache that is broken in both ways
103
+ * still recovers in one pass. Also the initializer the embed worker uses in
104
+ * its subprocess; the in-process seam stays {@link setLocalModelInitializer}.
105
+ */
106
+ export async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
107
+ const cacheDir = options.cacheDir ?? getFastembedCacheDir();
108
+ const initOptions = options.cacheDir === undefined ? { ...options, cacheDir } : options;
66
109
  const { FlagEmbedding } = await loadFastembed();
110
+ const initWithSidecarHeal = async (): Promise<LocalEmbeddingModel> => {
111
+ try {
112
+ return await FlagEmbedding.init(initOptions);
113
+ } catch (error) {
114
+ const message = error instanceof Error ? error.message : "";
115
+ if (!SIDECAR_ERROR_RE.test(message)) throw error;
116
+ if (!(await ensureFastembedModelSidecars(options.model, cacheDir))) throw error;
117
+ return FlagEmbedding.init(initOptions);
118
+ }
119
+ };
67
120
  try {
68
- return await FlagEmbedding.init(options);
121
+ return await initWithSidecarHeal();
69
122
  } catch (error) {
70
123
  const message = error instanceof Error ? error.message : "";
71
- if (
72
- !/(?:Config file not found at .*config|Tokenizer file not found at .*tokenizer|Tokens map file not found at .*special_tokens_map)/u.test(
73
- message,
74
- )
75
- ) {
76
- throw error;
124
+ if (/Protobuf parsing failed/i.test(message) && (await quarantineCorruptModelFile(message, cacheDir))) {
125
+ return initWithSidecarHeal();
77
126
  }
78
- if (!(await ensureFastembedModelSidecars(options.model, options.cacheDir))) throw error;
79
- return FlagEmbedding.init(options);
127
+ throw error;
80
128
  }
81
129
  }
82
130
 
package/src/core/index.ts CHANGED
@@ -2,6 +2,7 @@ export { configureRecallFeatures, type RecallFeatureFlags } from "../config";
2
2
  export * from "./banks";
3
3
  export * from "./beam/index";
4
4
  export {
5
+ defaultLocalModelInitializer,
5
6
  type LocalEmbeddingModel,
6
7
  type LocalModelInitializer,
7
8
  type LocalModelInitOptions,