@lotargo/memory_plugin 1.6.1 → 1.6.3

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.
@@ -138,6 +138,21 @@ const MIGRATIONS = [
138
138
  `);
139
139
  },
140
140
  },
141
+ {
142
+ version: 5,
143
+ name: "005_retrieval_policy",
144
+ up: async (db) => {
145
+ try {
146
+ await db.exec(`ALTER TABLE micro_chunks ADD COLUMN retrieval_policy TEXT DEFAULT 'micro_chunk';`);
147
+ } catch (e) {}
148
+ try {
149
+ await db.exec(`ALTER TABLE micro_chunks ADD COLUMN policy_source_id TEXT;`);
150
+ } catch (e) {}
151
+ await db.exec(`
152
+ CREATE INDEX IF NOT EXISTS idx_micro_chunks_retrieval_policy ON micro_chunks(retrieval_policy);
153
+ `);
154
+ },
155
+ },
141
156
  ];
142
157
 
143
158
  export async function runMigrations(db) {
@@ -36,7 +36,8 @@ const CLI_COMMANDS = new Set([
36
36
  "identity",
37
37
  "migrate_titles",
38
38
  "enable-prompt",
39
- "disable-prompt",
39
+ "disable-prompt",
40
+ "doctor",
40
41
  ]);
41
42
 
42
43
  function printUsage() {
@@ -51,7 +52,8 @@ Usage:
51
52
  memory_plugin auth-status
52
53
  memory_plugin link|unlink|relink|identity [--dir <path>] [--remote <url>]
53
54
  memory_plugin migrate_titles [--key <key>]
54
- memory_plugin enable-prompt | disable-prompt
55
+ memory_plugin enable-prompt | disable-prompt
56
+ memory_plugin doctor --codex
55
57
 
56
58
  Options:
57
59
  -h, --help Show this help text
@@ -5,6 +5,159 @@ export function estimateTokens(text) {
5
5
  return Math.ceil(text.length / 4);
6
6
  }
7
7
 
8
+ const CODE_SIGNATURE_REGEX = /^\s*(?:export\s+|async\s+)?(?:function|class|def|pub\s+fn|fn|struct|interface|enum)\s+/;
9
+
10
+ function _classifyLine(line) {
11
+ const t = line.trimStart();
12
+ if (t.startsWith("/**")) return "jsdoc_start";
13
+ if (t.startsWith("*/")) return "jsdoc_end";
14
+ if (t.startsWith("*")) return "jsdoc_mid";
15
+ if (t.startsWith("//")) return "line_comment";
16
+ if (t.startsWith("#")) return "hash_comment";
17
+ if (t.startsWith("'''") || t.startsWith('"""')) return "py_docstring";
18
+ return "code";
19
+ }
20
+
21
+ export function extractCodeSignatures(codeContent) {
22
+ const lines = codeContent.split("\n");
23
+ const signatures = [];
24
+
25
+ const fenceMatch = lines[0] && lines[0].match(/^(\s*)(```|~~~)/);
26
+ const bodyStart = fenceMatch ? 1 : 0
27
+ const bodyEnd = fenceMatch && (lines[lines.length - 1].startsWith("```") || lines[lines.length - 1].startsWith("~~~")) ? lines.length - 1 : lines.length;
28
+ const bodyLines = lines.slice(bodyStart, bodyEnd);
29
+
30
+ let i = 0;
31
+ while (i < bodyLines.length) {
32
+ const line = bodyLines[i];
33
+ const type = _classifyLine(line);
34
+
35
+ if (type === "jsdoc_start") {
36
+ const jsdocBlock = [line];
37
+ let j = i + 1;
38
+ while (j < bodyLines.length) {
39
+ jsdocBlock.push(bodyLines[j]);
40
+ if (_classifyLine(bodyLines[j]) === "jsdoc_end") {
41
+ j++;
42
+ break;
43
+ }
44
+ j++;
45
+ }
46
+
47
+ if (j < bodyLines.length && CODE_SIGNATURE_REGEX.test(bodyLines[j])) {
48
+ const sigLines = [bodyLines[j]];
49
+ const pyDocResult = _tryPyDocstring(bodyLines, j + 1);
50
+ if (pyDocResult.docLines.length > 0) {
51
+ sigLines.push(...pyDocResult.docLines);
52
+ }
53
+ const endIdx = pyDocResult.docLines.length > 0 ? pyDocResult.endIdx : j;
54
+
55
+ signatures.push({
56
+ signature: [...jsdocBlock, ...sigLines].join("\n").trim(),
57
+ line_number: j + bodyStart + 1,
58
+ });
59
+ i = endIdx + 1;
60
+ continue;
61
+ }
62
+
63
+ i = j;
64
+ continue;
65
+ }
66
+
67
+ if (type === "line_comment" || type === "hash_comment") {
68
+ const commentBlock = [line];
69
+ let j = i + 1;
70
+ while (j < bodyLines.length && _classifyLine(bodyLines[j]) === type) {
71
+ commentBlock.push(bodyLines[j]);
72
+ j++;
73
+ }
74
+
75
+ if (j < bodyLines.length && CODE_SIGNATURE_REGEX.test(bodyLines[j])) {
76
+ const sigLines = [bodyLines[j]];
77
+ const pyDocResult = _tryPyDocstring(bodyLines, j + 1);
78
+ if (pyDocResult.docLines.length > 0) {
79
+ sigLines.push(...pyDocResult.docLines);
80
+ }
81
+ const endIdx = pyDocResult.docLines.length > 0 ? pyDocResult.endIdx : j;
82
+
83
+ signatures.push({
84
+ signature: [...commentBlock, ...sigLines].join("\n").trim(),
85
+ line_number: j + bodyStart + 1,
86
+ });
87
+ i = endIdx + 1;
88
+ continue;
89
+ }
90
+
91
+ i = j;
92
+ continue;
93
+ }
94
+
95
+ if (CODE_SIGNATURE_REGEX.test(line)) {
96
+ const sigLines = [line];
97
+ const pyDocResult = _tryPyDocstring(bodyLines, i + 1);
98
+ if (pyDocResult.docLines.length > 0) {
99
+ sigLines.push(...pyDocResult.docLines);
100
+ }
101
+ const endIdx = pyDocResult.docLines.length > 0 ? pyDocResult.endIdx : i;
102
+
103
+ signatures.push({
104
+ signature: sigLines.join("\n").trim(),
105
+ line_number: i + bodyStart + 1,
106
+ });
107
+ i = endIdx + 1;
108
+ continue;
109
+ }
110
+
111
+ i++;
112
+ }
113
+
114
+ return signatures;
115
+ }
116
+
117
+ function _tryPyDocstring(bodyLines, startIdx) {
118
+ let k = startIdx;
119
+ while (k < bodyLines.length && bodyLines[k].trim() === "") k++;
120
+ if (k >= bodyLines.length) return { docLines: [], endIdx: startIdx - 1 };
121
+
122
+ const line = bodyLines[k];
123
+ const tripleDouble = /^\s*"""/.test(line);
124
+ const tripleSingle = /^\s*'''/.test(line);
125
+ const marker = tripleDouble ? '"""' : tripleSingle ? "'''" : null;
126
+ if (!marker) return { docLines: [], endIdx: startIdx - 1 };
127
+
128
+ const docLines = [line];
129
+ if (line.includes(marker.repeat(2)) && line.indexOf(marker) !== line.lastIndexOf(marker)) {
130
+ return { docLines, endIdx: k };
131
+ }
132
+
133
+ for (let m = k + 1; m < bodyLines.length; m++) {
134
+ docLines.push(bodyLines[m]);
135
+ if (bodyLines[m].includes(marker)) {
136
+ return { docLines, endIdx: m };
137
+ }
138
+ }
139
+ return { docLines, endIdx: k };
140
+ }
141
+
142
+ export function generateTableSummary(tableContent, breadcrumbs = "") {
143
+ const lines = tableContent.split("\n").filter((l) => l.trim().length > 0);
144
+ if (lines.length === 0) return null;
145
+
146
+ const headerLine = lines[0];
147
+ const columns = headerLine
148
+ .split("|")
149
+ .map((c) => c.trim())
150
+ .filter((c) => c.length > 0);
151
+
152
+ const separatorLine = lines[1] || "";
153
+ const hasSeparator = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(separatorLine);
154
+ const dataLines = hasSeparator ? lines.slice(2) : lines.slice(1);
155
+ const rowCount = dataLines.length;
156
+
157
+ const contextPart = breadcrumbs ? ` Context: ${breadcrumbs}.` : "";
158
+ return `Table with columns [${columns.join(", ")}] containing ${rowCount} row${rowCount !== 1 ? "s" : ""}.${contextPart}`;
159
+ }
160
+
8
161
  // 1. BIG LEVEL: Heading & Section Hierarchy Parser
9
162
  export function parseSections(markdown, docTitle = "Document") {
10
163
  const lines = markdown.split("\n");
@@ -72,7 +225,7 @@ export function extractMediumBlocks(section, sectionId, docId) {
72
225
  let blockIndex = 0;
73
226
 
74
227
  let currentLines = [];
75
- let currentBlockType = "paragraph"; // 'paragraph', 'code', 'table', 'list', 'blockquote'
228
+ let currentBlockType = "paragraph";
76
229
 
77
230
  function pushCurrentBlock() {
78
231
  const blockContent = currentLines.join("\n").trim();
@@ -97,7 +250,6 @@ export function extractMediumBlocks(section, sectionId, docId) {
97
250
  for (let i = 0; i < lines.length; i++) {
98
251
  const line = lines[i];
99
252
 
100
- // Check code fence
101
253
  const fenceMatch = line.match(/^(\s*)(```|~~~)/);
102
254
  if (fenceMatch) {
103
255
  if (!inFencedCode) {
@@ -121,7 +273,6 @@ export function extractMediumBlocks(section, sectionId, docId) {
121
273
  continue;
122
274
  }
123
275
 
124
- // Check table line
125
276
  const isTableLine = /^\s*\|.*\|\s*$/.test(line);
126
277
  if (isTableLine) {
127
278
  if (currentBlockType !== "table" && currentLines.length > 0) {
@@ -134,7 +285,6 @@ export function extractMediumBlocks(section, sectionId, docId) {
134
285
  pushCurrentBlock();
135
286
  }
136
287
 
137
- // Check list item line
138
288
  const isListLine = /^\s*([*+-]|\d+\.)\s+/.test(line);
139
289
  if (isListLine) {
140
290
  if (currentBlockType !== "list" && currentBlockType !== "paragraph" && currentLines.length > 0) {
@@ -145,7 +295,6 @@ export function extractMediumBlocks(section, sectionId, docId) {
145
295
  continue;
146
296
  }
147
297
 
148
- // Check empty line
149
298
  if (line.trim().length === 0) {
150
299
  if (currentLines.length > 0) {
151
300
  pushCurrentBlock();
@@ -169,6 +318,7 @@ export function createSmallChunks(mediumBlock, sectionId, docId) {
169
318
 
170
319
  function makeChunk(chunkText, extraMeta = {}) {
171
320
  if (!chunkText || chunkText.trim().length === 0) return;
321
+ const { retrieval_policy, policy_source_id, ...rest } = extraMeta;
172
322
  smallChunks.push({
173
323
  id: `${mediumBlock.id}_s${smallIdx++}`,
174
324
  medium_id: mediumBlock.id,
@@ -177,14 +327,24 @@ export function createSmallChunks(mediumBlock, sectionId, docId) {
177
327
  content: chunkText.trim(),
178
328
  breadcrumbs: mediumBlock.breadcrumbs,
179
329
  token_count: estimateTokens(chunkText),
180
- ...extraMeta,
330
+ retrieval_policy: retrieval_policy || "micro_chunk",
331
+ policy_source_id: policy_source_id || null,
332
+ ...rest,
181
333
  });
182
334
  }
183
335
 
184
336
  // RULE FOR TABLES
185
337
  if (mediumBlock.block_type === "table") {
338
+ const summary = generateTableSummary(content, mediumBlock.breadcrumbs);
339
+ if (summary) {
340
+ makeChunk(summary, {
341
+ retrieval_policy: "table_summary",
342
+ policy_source_id: mediumBlock.id,
343
+ });
344
+ }
345
+
186
346
  if (tokenCount <= 350) {
187
- makeChunk(content);
347
+ makeChunk(content, { retrieval_policy: "micro_chunk" });
188
348
  return smallChunks;
189
349
  }
190
350
 
@@ -205,15 +365,23 @@ export function createSmallChunks(mediumBlock, sectionId, docId) {
205
365
  for (let i = 0; i < dataLines.length; i += chunkSize) {
206
366
  const rowBatch = dataLines.slice(i, i + chunkSize);
207
367
  const tableChunkText = `${headerStr}\n${rowBatch.join("\n")}`;
208
- makeChunk(tableChunkText);
368
+ makeChunk(tableChunkText, { retrieval_policy: "micro_chunk" });
209
369
  }
210
370
  return smallChunks;
211
371
  }
212
372
 
213
373
  // RULE FOR CODE BLOCKS
214
374
  if (mediumBlock.block_type === "code") {
375
+ const signatures = extractCodeSignatures(content);
376
+ for (const sig of signatures) {
377
+ makeChunk(sig.signature, {
378
+ retrieval_policy: "code_signature",
379
+ policy_source_id: mediumBlock.id,
380
+ });
381
+ }
382
+
215
383
  if (tokenCount <= 350) {
216
- makeChunk(content);
384
+ makeChunk(content, { retrieval_policy: "micro_chunk" });
217
385
  return smallChunks;
218
386
  }
219
387
 
@@ -243,7 +411,7 @@ export function createSmallChunks(mediumBlock, sectionId, docId) {
243
411
 
244
412
  for (const block of astBlocks) {
245
413
  const fullChunk = fenceHeader ? `${fenceHeader}\n${block}\n${fenceFooter}` : block;
246
- makeChunk(fullChunk);
414
+ makeChunk(fullChunk, { retrieval_policy: "micro_chunk" });
247
415
  }
248
416
  return smallChunks;
249
417
  }
@@ -265,8 +433,7 @@ export function createSmallChunks(mediumBlock, sectionId, docId) {
265
433
 
266
434
  if (currentTokens + sTokens > TARGET_WINDOW_TOKENS && currentWindow.length > 0) {
267
435
  makeChunk(currentWindow.join(" "));
268
-
269
- // Safe Overlap: Keep the last sentence of the previous window if available
436
+
270
437
  const lastSentence = currentWindow[currentWindow.length - 1];
271
438
  currentWindow = [lastSentence, sentence];
272
439
  currentTokens = estimateTokens(lastSentence) + sTokens;
@@ -129,8 +129,8 @@ export async function ingestDocument({
129
129
  }
130
130
 
131
131
  const insertMicroStmt = db.prepare(`
132
- INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id)
133
- VALUES (?, ?, ?, ?, ?, ?, ?);
132
+ INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id, retrieval_policy, policy_source_id)
133
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
134
134
  `);
135
135
  const insertFtsStmt = db.prepare(`
136
136
  INSERT INTO micro_chunks_fts (id, content, breadcrumbs)
@@ -138,7 +138,7 @@ export async function ingestDocument({
138
138
  `);
139
139
 
140
140
  for (const micro of hierarchy.microChunks) {
141
- await insertMicroStmt.run(micro.id, micro.section_id, micro.doc_id, micro.content, micro.vector, micro.token_count, micro.medium_id || null);
141
+ await insertMicroStmt.run(micro.id, micro.section_id, micro.doc_id, micro.content, micro.vector, micro.token_count, micro.medium_id || null, micro.retrieval_policy || "micro_chunk", micro.policy_source_id || null);
142
142
  await insertFtsStmt.run(micro.id, micro.content, micro.breadcrumbs);
143
143
  }
144
144