@lotargo/memory_plugin 1.2.0 → 1.2.2

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/mcp-server/cli.js CHANGED
@@ -1109,13 +1109,26 @@ export async function runCli() {
1109
1109
  break;
1110
1110
  }
1111
1111
 
1112
- const docItems = docs.map((doc) => ({
1113
- label: doc.title || doc.path || "Untitled Document",
1114
- badge: doc.created_at ? doc.created_at.substring(0, 16) : "",
1115
- hint: doc.id ? `ID: ${doc.id.substring(0, 8)}...` : "",
1116
- info: `Path: ${doc.path || "N/A"}`,
1117
- value: doc,
1118
- }));
1112
+ const docItems = docs.map((doc) => {
1113
+ const rawDate = doc.created_at || doc.updated_at || "";
1114
+ let formattedDate = "";
1115
+ if (rawDate) {
1116
+ try {
1117
+ const d = typeof rawDate === "number" ? new Date(rawDate) : new Date(String(rawDate));
1118
+ formattedDate = isNaN(d.getTime()) ? String(rawDate).substring(0, 16) : d.toISOString().replace("T", " ").substring(0, 16);
1119
+ } catch (e) {
1120
+ formattedDate = String(rawDate).substring(0, 16);
1121
+ }
1122
+ }
1123
+ const docIdStr = doc.id != null ? String(doc.id) : "";
1124
+ return {
1125
+ label: doc.title || doc.path || "Untitled Document",
1126
+ badge: formattedDate,
1127
+ hint: docIdStr ? `ID: ${docIdStr.substring(0, 8)}...` : "",
1128
+ info: `Path: ${doc.path || "N/A"}`,
1129
+ value: doc,
1130
+ };
1131
+ });
1119
1132
  docItems.push({ label: "< Back to Main Menu", value: "back" });
1120
1133
 
1121
1134
  const docRes = await selectSimpleMenu({
@@ -23,6 +23,25 @@ function getOptimalThreadCount() {
23
23
  return Math.max(1, Math.min(totalCores, 8));
24
24
  }
25
25
 
26
+ export function ensureValidModelDirectory() {
27
+ try {
28
+ if (!fs.existsSync(MODELS_DIR)) {
29
+ fs.mkdirSync(MODELS_DIR, { recursive: true });
30
+ }
31
+ const testFile = path.join(MODELS_DIR, ".path_check");
32
+ fs.writeFileSync(testFile, "ok");
33
+ fs.unlinkSync(testFile);
34
+ return MODELS_DIR;
35
+ } catch (err) {
36
+ console.warn(`[Self-Healing] Model directory path "${MODELS_DIR}" is inaccessible or invalid (${err.message}). Falling back to standard default model storage path...`);
37
+ const fallbackDir = path.join(MODELS_DIR, "..", "models");
38
+ try {
39
+ fs.mkdirSync(fallbackDir, { recursive: true });
40
+ } catch (e) {}
41
+ return fallbackDir;
42
+ }
43
+ }
44
+
26
45
  export async function getExtractor(modelName = null, progressCallback = null) {
27
46
  const targetModel = modelName || getConfig().embeddingModel || "Xenova/multilingual-e5-small";
28
47
 
@@ -43,9 +62,12 @@ export async function getExtractor(modelName = null, progressCallback = null) {
43
62
  return extractorInstance;
44
63
  }
45
64
 
65
+ // Ensure valid writable storage directory
66
+ const cacheDir = ensureValidModelDirectory();
67
+
46
68
  const { pipeline, env } = await import("@huggingface/transformers");
47
69
 
48
- env.cacheDir = MODELS_DIR;
70
+ env.cacheDir = cacheDir;
49
71
  env.allowLocalModels = true;
50
72
  env.allowRemoteModels = true;
51
73
  env.remoteHost = "https://huggingface.co";
@@ -93,18 +115,56 @@ export async function getExtractor(modelName = null, progressCallback = null) {
93
115
  } catch {}
94
116
  }
95
117
  } catch (err) {
118
+ const isNetworkError = err.message && (
119
+ err.message.includes("fetch") ||
120
+ err.message.includes("network") ||
121
+ err.message.includes("ETIMEDOUT") ||
122
+ err.message.includes("ENOTFOUND") ||
123
+ err.message.includes("503") ||
124
+ err.message.includes("502") ||
125
+ err.message.includes("504")
126
+ );
127
+
128
+ if (isNetworkError) {
129
+ console.warn(`[Model Manager] ⚠️ Network interruption while loading "${targetModel}": ${err.message}. Retrying / Resuming download...`);
130
+ } else {
131
+ console.warn(`[Model Manager] ⚠️ Unrecoverable file error for "${targetModel}": ${err.message}. Purging corrupted cache...`);
132
+ deleteModelCache(targetModel);
133
+ }
134
+
96
135
  if (targetDevice !== "cpu") {
97
- console.warn(`[GPU Engine] GPU initialization (${targetDevice}) failed: ${err.message}. Falling back to CPU...`);
136
+ console.warn(`[GPU Engine] GPU initialization (${targetDevice}) failed. Retrying on CPU...`);
98
137
  pipelineOpts.device = "cpu";
99
138
  pipelineOpts.session_options.executionMode = "sequential";
100
- extractorInstance = await pipeline("feature-extraction", targetModel, pipelineOpts);
101
- loadedModelName = targetModel;
102
- loadedDevice = "cpu";
103
- } else if (targetModel !== "Xenova/multilingual-e5-small") {
104
- console.warn(`HuggingFace model load failed for ${targetModel}: ${err.message}. Falling back to default Xenova/multilingual-e5-small...`);
105
- extractorInstance = await pipeline("feature-extraction", "Xenova/multilingual-e5-small", pipelineOpts);
106
- loadedModelName = "Xenova/multilingual-e5-small";
107
- loadedDevice = targetDevice;
139
+ try {
140
+ extractorInstance = await pipeline("feature-extraction", targetModel, pipelineOpts);
141
+ loadedModelName = targetModel;
142
+ loadedDevice = "cpu";
143
+ return extractorInstance;
144
+ } catch (err2) {
145
+ if (!isNetworkError) deleteModelCache(targetModel);
146
+ }
147
+ }
148
+
149
+ if (targetModel !== "Xenova/multilingual-e5-small") {
150
+ console.warn(`[Fallback] Reverting to standard default model "Xenova/multilingual-e5-small"...`);
151
+ resetExtractor();
152
+ try {
153
+ const fallbackOpts = {
154
+ quantized: true,
155
+ dtype: "q8",
156
+ device: "cpu",
157
+ session_options: { graphOptimizationLevel: "all", executionMode: "sequential" },
158
+ };
159
+ if (progressCallback) fallbackOpts.progress_callback = progressCallback;
160
+
161
+ extractorInstance = await pipeline("feature-extraction", "Xenova/multilingual-e5-small", fallbackOpts);
162
+ loadedModelName = "Xenova/multilingual-e5-small";
163
+ loadedDevice = "cpu";
164
+ } catch (err3) {
165
+ console.error(`[Fatal] Could not load default fallback model: ${err3.message}`);
166
+ throw err3;
167
+ }
108
168
  } else {
109
169
  throw err;
110
170
  }
@@ -119,6 +179,38 @@ export function resetExtractor() {
119
179
  loadedDevice = null;
120
180
  }
121
181
 
182
+ export function deleteModelCache(modelName) {
183
+ const info = getModelStorageInfo(modelName);
184
+ if (info.status === "not_downloaded" || !fs.existsSync(info.dir)) {
185
+ return { deleted: false, reason: "Model directory not found" };
186
+ }
187
+
188
+ resetExtractor();
189
+ if (global.gc) {
190
+ try { global.gc(); } catch (e) {}
191
+ }
192
+
193
+ try {
194
+ fs.rmSync(info.dir, { recursive: true, force: true });
195
+ const parentDir = path.dirname(info.dir);
196
+ if (fs.existsSync(parentDir) && fs.readdirSync(parentDir).length === 0) {
197
+ fs.rmdirSync(parentDir);
198
+ }
199
+ return { deleted: true, modelName, freedMB: info.sizeMB };
200
+ } catch (err) {
201
+ try {
202
+ const corruptPath = `${info.dir}.corrupt_${Date.now()}`;
203
+ fs.renameSync(info.dir, corruptPath);
204
+ setTimeout(() => {
205
+ try { fs.rmSync(corruptPath, { recursive: true, force: true }); } catch (e) {}
206
+ }, 1000);
207
+ return { deleted: true, modelName, freedMB: info.sizeMB };
208
+ } catch (renameErr) {
209
+ return { deleted: false, reason: `${err.message} (Rename fallback: ${renameErr.message})` };
210
+ }
211
+ }
212
+ }
213
+
122
214
  export function formatInputText(text, isQuery = false, modelName = null, instruction = null) {
123
215
  if (!text) return "";
124
216
  const targetModel = modelName || getConfig().embeddingModel || "Xenova/multilingual-e5-small";
@@ -296,6 +388,8 @@ export async function getReranker(modelName = "Xenova/bge-reranker-base", progre
296
388
  return rerankerInstance;
297
389
  }
298
390
 
391
+ checkAndSelfHealModel(modelName);
392
+
299
393
  const { pipeline, env } = await import("@huggingface/transformers");
300
394
  env.cacheDir = MODELS_DIR;
301
395
  env.allowLocalModels = true;
@@ -338,7 +432,8 @@ export async function getReranker(modelName = "Xenova/bge-reranker-base", progre
338
432
  rerankerInstance = await pipeline("text-classification", modelName, pipelineOpts);
339
433
  loadedRerankerName = modelName;
340
434
  } catch (err) {
341
- console.warn(`Failed to load reranker model ${modelName}: ${err.message}`);
435
+ console.warn(`Failed to load reranker model ${modelName}: ${err.message}. Purging corrupt files...`);
436
+ deleteModelCache(modelName);
342
437
  return null;
343
438
  }
344
439
  return rerankerInstance;
@@ -451,25 +546,6 @@ export function getModelStorageInfo(modelName) {
451
546
  return { status: "partial", sizeMB, bytes: totalBytes, dir: modelDir };
452
547
  }
453
548
 
454
- export function deleteModelCache(modelName) {
455
- const info = getModelStorageInfo(modelName);
456
- if (info.status === "not_downloaded" || !fs.existsSync(info.dir)) {
457
- return { deleted: false, reason: "Model directory not found" };
458
- }
459
-
460
- try {
461
- fs.rmSync(info.dir, { recursive: true, force: true });
462
- const parentDir = path.dirname(info.dir);
463
- if (fs.existsSync(parentDir) && fs.readdirSync(parentDir).length === 0) {
464
- fs.rmdirSync(parentDir);
465
- }
466
- resetExtractor();
467
- return { deleted: true, modelName, freedMB: info.sizeMB };
468
- } catch (err) {
469
- return { deleted: false, reason: err.message };
470
- }
471
- }
472
-
473
549
  export function listAllCachedModels() {
474
550
  const result = [];
475
551
  if (!fs.existsSync(MODELS_DIR)) return result;
@@ -8,7 +8,9 @@ const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
8
8
 
9
9
  export const PROMPT_BLOCK = `${START_MARKER}
10
10
  [SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
11
- When starting a session or when personal/project context is relevant, use \`recall\` from \`memory-agent\` to load saved facts, user preferences, and project guidelines. Use \`remember\` to save any durable high-signal facts provided by the user.
11
+ 1. MANDATORY FIRST STEP (SESSION INITIALIZATION): At the very start of every session or conversation, your VERY FIRST ACTION MUST BE to execute \`recall\` from \`memory-agent\` to load all saved facts, user preferences, personal context, and project guidelines BEFORE performing any other tasks or reading code.
12
+ 2. PROACTIVE SAVING DIRECTIVE: You MUST automatically and proactively call \`remember\` from \`memory-agent\` whenever the user shares durable facts, personal preferences, coding guidelines, technology choices, or project architecture decisions. Do NOT wait for explicit user commands like "remember this".
13
+ 3. SIGNAL FILTER: Save only high-signal facts (name, language, roles, constraints, tech stack preferences, architecture decisions, conventions). Translate facts into clear, concise English when saving. Do NOT save transient details or one-off conversation turns.
12
14
  ${END_MARKER}`;
13
15
 
14
16
  export function getGlobalPromptTargets() {
@@ -74,12 +74,15 @@ async function notify(client, message, variant = "success") {
74
74
  }
75
75
 
76
76
  const MEMORY_INSTRUCTION =
77
+ "MANDATORY FIRST STEP (READ MEMORIES FIRST):\n" +
78
+ "At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
79
+ "PROACTIVE MEMORY DIRECTIVE:\n" +
80
+ "You MUST automatically and proactively call `remember` whenever the user shares durable facts, personal preferences, coding guidelines, tech stack choices, architecture decisions, or project conventions.\n" +
81
+ "Do NOT wait for explicit user commands like \"remember this\". Automatically capture high-signal facts in real time.\n" +
77
82
  "Use `remember` only for important, durable facts about the user and project.\n" +
78
- "Save high-signal things like: name, language, role/goals, constraints, tech\n" +
79
- "stack preferences, architecture decisions, project conventions.\n" +
80
- "DO NOT save: transient details, one-off statements, full conversation turns,\n" +
81
- "or anything unlikely to be useful in future sessions.\n" +
82
- "When saving, translate the fact into English and keep it concise.\n" +
83
+ "Save high-signal items: user role, goals, constraints, tech stack preferences, architecture decisions, project conventions.\n" +
84
+ "DO NOT save: transient details, one-off statements, full conversation turns, or anything unlikely to be useful in future sessions.\n" +
85
+ "When saving, translate the fact into clear, concise English.\n" +
83
86
  "Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.";
84
87
 
85
88
  function buildMemoryContext(globalFacts, projectFacts, projectKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotargo/memory_plugin",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Persistent memory agent for coding AI tools — remembers user preferences and project context across sessions. Works with Antigravity, OpenCode, Claude Code, and Codex.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: using-memory
3
- description: Comprehensive guide for using the Memory & Hybrid RAG Knowledge Engine tools (remember, recall, forget, ingest_document, query_knowledge_base, manage_knowledge_base). Trigger whenever remembering user preferences/conventions, querying ingested documentation, indexing files/repos, or managing persistent knowledge.
3
+ description: Comprehensive guide for using the Memory & Hybrid RAG Knowledge Engine tools (remember, recall, forget, link_knowledge, ingest_document, query_knowledge_base, manage_knowledge_base). Trigger proactively whenever user preferences, project conventions, technology stack choices, or architecture decisions are introduced, or when querying ingested documentation, indexing files/repos, or managing persistent knowledge.
4
4
  ---
5
5
 
6
6
  # Using Memory & Hybrid RAG Knowledge Engine
@@ -116,7 +116,8 @@ In such cases, use the **Full Raw Document Reading** mechanism:
116
116
 
117
117
  ## 4. Core Directives for AI Agents
118
118
 
119
- 1. **Be Proactive**: When the user mentions a durable preference or constraint, save it immediately using `remember`.
120
- 2. **Check Knowledge Base First**: If a user asks how a specific module, API, or project architecture works, call `query_knowledge_base` using concept-dense search phrases.
121
- 3. **Inspect Ambiguous Docs Directly**: If querying produces low relevance scores on abstractly-named documents, call `manage_knowledge_base(action: "read_document")` to inspect the full text directly.
122
- 4. **Keep Memory Clean**: If a preference changes, call `forget` on the outdated entry before saving the new one.
119
+ 1. **Read Memories First (MANDATORY)**: At the very start of any session or conversation, your VERY FIRST STEP MUST BE to execute `recall` to load all saved facts, user context, and project guidelines BEFORE performing any other task or code analysis.
120
+ 2. **Be Proactive**: When the user mentions a durable preference, personal fact, or constraint, save it immediately using `remember`. Do not wait for explicit user commands.
121
+ 3. **Check Knowledge Base First**: If a user asks how a specific module, API, or project architecture works, call `query_knowledge_base` using concept-dense search phrases.
122
+ 4. **Inspect Ambiguous Docs Directly**: If querying produces low relevance scores on abstractly-named documents, call `manage_knowledge_base(action: "read_document")` to inspect the full text directly.
123
+ 5. **Keep Memory Clean**: If a preference changes, call `forget` on the outdated entry before saving the new one.