@yamo/memory-mesh 3.2.2 → 3.2.5

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/README.md CHANGED
@@ -47,14 +47,46 @@ The installer will:
47
47
 
48
48
  ### CLI
49
49
 
50
+ The `memory-mesh` CLI provides seven commands for full subconscious CRUD and recall:
51
+
50
52
  ```bash
51
53
  # Store a memory (automatically scrubbed & embedded)
52
- node tools/memory_mesh.mjs store --content "My important memory" --type "insight"
54
+ memory-mesh store --content "My important memory" --type insight
55
+
56
+ # Store with full provenance metadata
57
+ memory-mesh store -c "Insight text" -t decision -r "Improves latency" -h "Caching reduces p95"
58
+
59
+ # Bulk-ingest a directory (recursive, by extension)
60
+ memory-mesh pull ./docs --extension ".md,.yamo" --type documentation
61
+
62
+ # Semantic search
63
+ memory-mesh search "query about orchestration" --limit 5
64
+
65
+ # Retrieve a specific record by ID
66
+ memory-mesh get --id mem_abc123
53
67
 
54
- # Search memories
55
- node tools/memory_mesh.mjs search "query" --limit 5
68
+ # Delete a record by ID
69
+ memory-mesh delete --id mem_abc123
70
+
71
+ # Synthesize insights from recent memories
72
+ memory-mesh reflect --topic "bugs" --lookback 10
73
+
74
+ # Database health and statistics
75
+ memory-mesh stats
56
76
  ```
57
77
 
78
+ **Command Reference:**
79
+
80
+ | Command | Key Options | Description |
81
+ |---------|-------------|-------------|
82
+ | `store` | `-c/--content` (required), `-t/--type`, `-r/--rationale`, `-h/--hypothesis` | Persist a semantic memory |
83
+ | `pull` | `<path>` (required), `-e/--extension`, `-t/--type` | Bulk-ingest a directory |
84
+ | `search` | `<query>` (required), `-l/--limit` | Semantic recall |
85
+ | `get` | `-i/--id` (required) | Fetch a record by ID |
86
+ | `delete` | `-i/--id` (required) | Remove a record by ID |
87
+ | `reflect` | `-t/--topic`, `-l/--lookback` | Synthesize insights from memories |
88
+ | `stats` | — | DB health, count, embedding model |
89
+
58
90
  ### Node.js API
59
91
 
60
92
  ```javascript
@@ -173,7 +205,7 @@ docker run -v $(pwd)/data:/app/runtime/data \
173
205
 
174
206
  ## About YAMO Protocol
175
207
 
176
- Memory Mesh is built on the **YAMO (Yet Another Markup for Orchestration) Protocol** - a structured language for transparent AI agent collaboration with immutable provenance tracking.
208
+ Memory Mesh is built on the **YAMO (Yet Another Model Ontology) Protocol** - a structured language for transparent AI agent collaboration with immutable provenance tracking.
177
209
 
178
210
  **YAMO Protocol Features:**
179
211
  - **Structured Agent Workflows**: Semicolon-terminated constraints, explicit handoff chains
@@ -21,7 +21,7 @@ const program = new Command();
21
21
  program
22
22
  .name('memory-mesh')
23
23
  .description('YAMO Semantic Subconscious - Protocol-Native CLI')
24
- .version('3.2.0');
24
+ .version('3.2.3');
25
25
 
26
26
  // Helper for beautiful logging
27
27
  const ui = {
@@ -194,4 +194,85 @@ program
194
194
  }
195
195
  });
196
196
 
197
+ // 5. Get Command
198
+ program
199
+ .command('get')
200
+ .description('Retrieve a memory record by ID')
201
+ .requiredOption('-i, --id <id>', 'Memory record ID')
202
+ .action(async (options) => {
203
+ const mesh = new MemoryMesh();
204
+ try {
205
+ const record = await mesh.get(options.id);
206
+ if (!record) {
207
+ ui.warn(`Record not found: ${options.id}`);
208
+ process.exit(1);
209
+ }
210
+ const meta = typeof record.metadata === 'string' ? JSON.parse(record.metadata) : (record.metadata || {});
211
+ ui.header(`Memory ${options.id}`);
212
+ console.log(`${pc.bold('ID:')} ${pc.dim(record.id)}`);
213
+ console.log(`${pc.bold('Type:')} ${pc.dim(meta.type || 'event')}`);
214
+ console.log(`${pc.bold('Created:')} ${pc.dim(record.created_at)}`);
215
+ console.log(`\n${pc.white(record.content)}`);
216
+ } catch (err) {
217
+ ui.error(`Get failed: ${err.message}`);
218
+ process.exit(1);
219
+ } finally {
220
+ await mesh.close();
221
+ }
222
+ });
223
+
224
+ // 6. Delete Command
225
+ program
226
+ .command('delete')
227
+ .description('Permanently remove a memory record by ID')
228
+ .requiredOption('-i, --id <id>', 'Memory record ID to delete')
229
+ .action(async (options) => {
230
+ const mesh = new MemoryMesh();
231
+ try {
232
+ await mesh.delete(options.id);
233
+ ui.success(`Deleted record ${pc.bold(options.id)}`);
234
+ } catch (err) {
235
+ ui.error(`Delete failed: ${err.message}`);
236
+ process.exit(1);
237
+ } finally {
238
+ await mesh.close();
239
+ }
240
+ });
241
+
242
+ // 7. Reflect Command
243
+ program
244
+ .command('reflect')
245
+ .description('Synthesize insights from stored memories')
246
+ .option('-t, --topic <topic>', 'Focus the reflection on a specific topic')
247
+ .option('-l, --lookback <number>', 'Number of memories to review', '10')
248
+ .action(async (options) => {
249
+ const mesh = new MemoryMesh();
250
+ try {
251
+ ui.info(`Reflecting on ${options.topic ? `"${pc.italic(options.topic)}"` : 'recent memories'}...`);
252
+ const result = await mesh.reflect({
253
+ topic: options.topic,
254
+ lookback: parseInt(options.lookback),
255
+ });
256
+ ui.header('Reflection');
257
+ if (result.reflection) {
258
+ console.log(pc.white(result.reflection));
259
+ console.log(`\n${pc.bold('Confidence:')} ${pc.cyan((result.confidence * 100).toFixed(0))}%`);
260
+ } else {
261
+ console.log(pc.dim(`Reviewed ${result.count} memories${result.topic ? ` on topic: ${result.topic}` : ''}`));
262
+ console.log(`\n${pc.bold('Prompt for LLM:')}\n${pc.white(result.prompt)}`);
263
+ if (result.context?.length) {
264
+ console.log('');
265
+ result.context.forEach((m, i) => {
266
+ console.log(`${pc.cyan(`Memory ${i + 1}:`)} ${pc.white(m.content.substring(0, 200))}${m.content.length > 200 ? '...' : ''}`);
267
+ });
268
+ }
269
+ }
270
+ } catch (err) {
271
+ ui.error(`Reflect failed: ${err.message}`);
272
+ process.exit(1);
273
+ } finally {
274
+ await mesh.close();
275
+ }
276
+ });
277
+
197
278
  program.parse();
@@ -85,6 +85,11 @@ export declare class LanceDBClient {
85
85
  * @throws {QueryError} If stats query fails
86
86
  */
87
87
  getStats(): Promise<any>;
88
+ /**
89
+ * Compact old data files and prune versions older than 7 days.
90
+ * Best-effort — never throws.
91
+ */
92
+ optimize(): Promise<void>;
88
93
  /**
89
94
  * Sanitize an ID to prevent SQL injection
90
95
  * Removes any characters that aren't alphanumeric, underscore, or hyphen
@@ -201,7 +201,7 @@ export class LanceDBClient {
201
201
  await this.connect();
202
202
  }
203
203
  this._validateVector(vector);
204
- const { limit = 10, nprobes = 20, filter = null } = options;
204
+ const { limit = 10, nprobes = 20, filter = null, refineFactor, timeoutMs } = options;
205
205
  return this._retryOperation(async () => {
206
206
  if (!this.table) {
207
207
  throw new StorageError("Table not initialized");
@@ -217,12 +217,21 @@ export class LanceDBClient {
217
217
  // ignore
218
218
  }
219
219
  }
220
+ // Apply refineFactor for improved ANN recall (fetches N×candidates, reranks)
221
+ if (refineFactor && typeof refineFactor === "number") {
222
+ try {
223
+ query = query.refineFactor(refineFactor);
224
+ }
225
+ catch (_e) {
226
+ // ignore if not supported
227
+ }
228
+ }
220
229
  // Apply filter if provided
221
230
  if (filter) {
222
231
  query = query.where(filter);
223
232
  }
224
- // Execute search with limit
225
- const resultsArray = await query.limit(limit).toArray();
233
+ // Execute search with limit (and optional timeout)
234
+ const resultsArray = await query.limit(limit).toArray(timeoutMs ? { timeoutMs } : undefined);
226
235
  return resultsArray.map((row) => ({
227
236
  id: row.id,
228
237
  content: row.content,
@@ -385,6 +394,22 @@ export class LanceDBClient {
385
394
  };
386
395
  });
387
396
  }
397
+ /**
398
+ * Compact old data files and prune versions older than 7 days.
399
+ * Best-effort — never throws.
400
+ */
401
+ async optimize() {
402
+ if (!this.isConnected || !this.table)
403
+ return;
404
+ try {
405
+ await this.table.optimize({
406
+ cleanupOlderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
407
+ });
408
+ }
409
+ catch (_e) {
410
+ // Best-effort — never block normal operations
411
+ }
412
+ }
388
413
  /**
389
414
  * Sanitize an ID to prevent SQL injection
390
415
  * Removes any characters that aren't alphanumeric, underscore, or hyphen
@@ -201,7 +201,7 @@ export class LanceDBClient {
201
201
  await this.connect();
202
202
  }
203
203
  this._validateVector(vector);
204
- const { limit = 10, nprobes = 20, filter = null } = options;
204
+ const { limit = 10, nprobes = 20, filter = null, refineFactor, timeoutMs } = options;
205
205
  return this._retryOperation(async () => {
206
206
  if (!this.table) {
207
207
  throw new StorageError("Table not initialized");
@@ -217,12 +217,23 @@ export class LanceDBClient {
217
217
  // ignore
218
218
  }
219
219
  }
220
+ // Apply refineFactor for improved ANN recall (fetches N×candidates, reranks)
221
+ if (refineFactor && typeof refineFactor === "number") {
222
+ try {
223
+ query = query.refineFactor(refineFactor);
224
+ }
225
+ catch (_e) {
226
+ // ignore if not supported
227
+ }
228
+ }
220
229
  // Apply filter if provided
221
230
  if (filter) {
222
231
  query = query.where(filter);
223
232
  }
224
- // Execute search with limit
225
- const resultsArray = await query.limit(limit).toArray();
233
+ // Execute search with limit (and optional timeout)
234
+ const resultsArray = await query.limit(limit).toArray(
235
+ timeoutMs ? { timeoutMs } : undefined,
236
+ );
226
237
  return resultsArray.map((row) => ({
227
238
  id: row.id,
228
239
  content: row.content,
@@ -385,6 +396,21 @@ export class LanceDBClient {
385
396
  };
386
397
  });
387
398
  }
399
+ /**
400
+ * Compact old data files and prune versions older than 7 days.
401
+ * Best-effort — never throws.
402
+ */
403
+ async optimize() {
404
+ if (!this.isConnected || !this.table) return;
405
+ try {
406
+ await this.table.optimize({
407
+ cleanupOlderThan: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
408
+ });
409
+ }
410
+ catch (_e) {
411
+ // Best-effort — never block normal operations
412
+ }
413
+ }
388
414
  /**
389
415
  * Sanitize an ID to prevent SQL injection
390
416
  * Removes any characters that aren't alphanumeric, underscore, or hyphen
@@ -444,6 +444,11 @@ export declare class MemoryMesh {
444
444
  * await mesh.close(); // Clean up
445
445
  * ```
446
446
  */
447
+ /**
448
+ * Compact old data files and prune versions older than 7 days.
449
+ * Best-effort — delegates to LanceDBClient.optimize().
450
+ */
451
+ optimize(): Promise<any>;
447
452
  close(): Promise<void>;
448
453
  }
449
454
  /**
@@ -288,8 +288,16 @@ export class MemoryMesh {
288
288
  const skillSchema = createSynthesizedSkillSchema(this.vectorDimension);
289
289
  this.skillTable = await this.client.db.createTable("synthesized_skills", [], {
290
290
  schema: skillSchema,
291
+ storageOptions: { new_table_data_storage_version: "stable" },
291
292
  });
292
293
  }
294
+ // Migrate manifest paths to V2 layout (idempotent)
295
+ try {
296
+ await this.skillTable.migrateManifestPathsV2();
297
+ }
298
+ catch {
299
+ // Already migrated or not a local table — ignore
300
+ }
293
301
  if (process.env.YAMO_DEBUG === "true") {
294
302
  logger.debug("YAMO blocks and synthesized skills tables initialized");
295
303
  }
@@ -1706,6 +1714,13 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
1706
1714
  * await mesh.close(); // Clean up
1707
1715
  * ```
1708
1716
  */
1717
+ /**
1718
+ * Compact old data files and prune versions older than 7 days.
1719
+ * Best-effort — delegates to LanceDBClient.optimize().
1720
+ */
1721
+ async optimize() {
1722
+ return this.client?.optimize?.();
1723
+ }
1709
1724
  // eslint-disable-next-line @typescript-eslint/require-await
1710
1725
  async close() {
1711
1726
  try {
@@ -1808,6 +1823,23 @@ export async function run() {
1808
1823
  else if (action === "stats") {
1809
1824
  process.stdout.write(`[MemoryMesh] Database Statistics:\n${JSON.stringify({ status: "ok", stats: await mesh.stats() }, null, 2)}\n`);
1810
1825
  }
1826
+ else if (action === "get") {
1827
+ const record = await mesh.get(input.id);
1828
+ if (!record) {
1829
+ process.stdout.write(`[MemoryMesh] Record not found: ${input.id}\n${JSON.stringify({ status: "not_found", id: input.id })}\n`);
1830
+ }
1831
+ else {
1832
+ process.stdout.write(`[MemoryMesh] Record ${record.id}\n${JSON.stringify({ status: "ok", record }, null, 2)}\n`);
1833
+ }
1834
+ }
1835
+ else if (action === "delete") {
1836
+ await mesh.delete(input.id);
1837
+ process.stdout.write(`[MemoryMesh] Deleted record ${input.id}\n${JSON.stringify({ status: "ok", id: input.id })}\n`);
1838
+ }
1839
+ else if (action === "reflect") {
1840
+ const result = await mesh.reflect({ topic: input.topic, lookback: input.lookback });
1841
+ process.stdout.write(`[MemoryMesh] Reflection complete.\n${JSON.stringify({ status: "ok", result }, null, 2)}\n`);
1842
+ }
1811
1843
  else {
1812
1844
  logger.error({ action }, "Unknown action");
1813
1845
  process.exit(1);
@@ -320,8 +320,16 @@ export class MemoryMesh {
320
320
  const skillSchema = createSynthesizedSkillSchema(this.vectorDimension);
321
321
  this.skillTable = await this.client.db.createTable("synthesized_skills", [], {
322
322
  schema: skillSchema,
323
+ storageOptions: { new_table_data_storage_version: "stable" },
323
324
  });
324
325
  }
326
+ // Migrate manifest paths to V2 layout (idempotent)
327
+ try {
328
+ await this.skillTable.migrateManifestPathsV2();
329
+ }
330
+ catch {
331
+ // Already migrated or not a local table — ignore
332
+ }
325
333
  if (process.env.YAMO_DEBUG === "true") {
326
334
  logger.debug("YAMO blocks and synthesized skills tables initialized");
327
335
  }
@@ -1793,6 +1801,13 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
1793
1801
  * await mesh.close(); // Clean up
1794
1802
  * ```
1795
1803
  */
1804
+ /**
1805
+ * Compact old data files and prune versions older than 7 days.
1806
+ * Best-effort — delegates to LanceDBClient.optimize().
1807
+ */
1808
+ async optimize() {
1809
+ return this.client?.optimize?.();
1810
+ }
1796
1811
  // eslint-disable-next-line @typescript-eslint/require-await
1797
1812
  async close() {
1798
1813
  try {
@@ -1895,6 +1910,22 @@ export async function run() {
1895
1910
  else if (action === "stats") {
1896
1911
  process.stdout.write(`[MemoryMesh] Database Statistics:\n${JSON.stringify({ status: "ok", stats: await mesh.stats() }, null, 2)}\n`);
1897
1912
  }
1913
+ else if (action === "get") {
1914
+ const record = await mesh.get(input.id);
1915
+ if (!record) {
1916
+ process.stdout.write(`[MemoryMesh] Record not found: ${input.id}\n${JSON.stringify({ status: "not_found", id: input.id })}\n`);
1917
+ } else {
1918
+ process.stdout.write(`[MemoryMesh] Record ${record.id}\n${JSON.stringify({ status: "ok", record }, null, 2)}\n`);
1919
+ }
1920
+ }
1921
+ else if (action === "delete") {
1922
+ await mesh.delete(input.id);
1923
+ process.stdout.write(`[MemoryMesh] Deleted record ${input.id}\n${JSON.stringify({ status: "ok", id: input.id })}\n`);
1924
+ }
1925
+ else if (action === "reflect") {
1926
+ const result = await mesh.reflect({ topic: input.topic, lookback: input.lookback });
1927
+ process.stdout.write(`[MemoryMesh] Reflection complete.\n${JSON.stringify({ status: "ok", result }, null, 2)}\n`);
1928
+ }
1898
1929
  else {
1899
1930
  logger.error({ action }, "Unknown action");
1900
1931
  process.exit(1);
@@ -57,6 +57,20 @@ export declare function createSynthesizedSkillSchema(vectorDim?: number): arrow.
57
57
  * @returns {boolean} True if V2 schema detected
58
58
  */
59
59
  export declare function isSchemaV2(schema: any): any;
60
+ /**
61
+ * Migrate an existing table to V2:
62
+ * 1. Migrate manifest paths to V2 layout (efficient versioning, idempotent)
63
+ * 2. Add nullable V2 columns to memory_entries-style tables if not already present
64
+ *
65
+ * Safe to call on any table — non-memory tables skip the schema column additions.
66
+ */
67
+ export declare function migrateTableV2(table: any): Promise<void>;
68
+ /**
69
+ * Ensure the vector column has an IVF_PQ index.
70
+ * Skipped when: table has too few rows, index already exists, or table is a mock.
71
+ * Called automatically by createMemoryTableWithDimension after migration.
72
+ */
73
+ export declare function ensureVectorIndex(table: any): Promise<void>;
60
74
  /**
61
75
  * Memory table schema using Apache Arrow format (default 384 dimensions)
62
76
  * @deprecated Use createMemorySchema(vectorDim) for dynamic dimensions
@@ -113,6 +127,8 @@ declare const _default: {
113
127
  createMemorySchema: typeof createMemorySchema;
114
128
  createMemorySchemaV2: typeof createMemorySchemaV2;
115
129
  isSchemaV2: typeof isSchemaV2;
130
+ migrateTableV2: typeof migrateTableV2;
131
+ ensureVectorIndex: typeof ensureVectorIndex;
116
132
  getEmbeddingDimension: typeof getEmbeddingDimension;
117
133
  DEFAULT_VECTOR_DIMENSION: number;
118
134
  EMBEDDING_DIMENSIONS: {
@@ -9,6 +9,7 @@
9
9
  * - text-embedding-3-small: 1536 dimensions
10
10
  */
11
11
  import * as arrow from "apache-arrow";
12
+ import { Index } from "@lancedb/lancedb";
12
13
  /**
13
14
  * Default vector dimension (all-MiniLM-L6-v2)
14
15
  */
@@ -112,6 +113,75 @@ export function createSynthesizedSkillSchema(vectorDim = DEFAULT_VECTOR_DIMENSIO
112
113
  export function isSchemaV2(schema) {
113
114
  return schema.fields.some((f) => f.name === "session_id");
114
115
  }
116
+ /**
117
+ * Migrate an existing table to V2:
118
+ * 1. Migrate manifest paths to V2 layout (efficient versioning, idempotent)
119
+ * 2. Add nullable V2 columns to memory_entries-style tables if not already present
120
+ *
121
+ * Safe to call on any table — non-memory tables skip the schema column additions.
122
+ */
123
+ export async function migrateTableV2(table) {
124
+ // Step 1: manifest path migration (idempotent on already-migrated tables)
125
+ try {
126
+ await table.migrateManifestPathsV2();
127
+ }
128
+ catch {
129
+ // Already migrated or not a local table — ignore
130
+ }
131
+ // Step 2: add V2 schema columns if this is a memory_entries-style table (V1)
132
+ // Guard: schema() may not exist on mock tables in tests
133
+ if (typeof table.schema !== "function")
134
+ return;
135
+ let schema;
136
+ try {
137
+ schema = await table.schema();
138
+ }
139
+ catch {
140
+ return; // Can't inspect schema — skip
141
+ }
142
+ if (isSchemaV2(schema))
143
+ return;
144
+ // Only add V2 columns if the table has the V1 memory_entries shape
145
+ const fieldNames = schema.fields.map((f) => f.name);
146
+ if (!fieldNames.includes("content") || !fieldNames.includes("vector"))
147
+ return;
148
+ await table.addColumns([
149
+ { name: "session_id", valueSql: "cast(null as string)" },
150
+ { name: "agent_id", valueSql: "cast(null as string)" },
151
+ { name: "memory_type", valueSql: "cast(null as string)" },
152
+ { name: "importance_score", valueSql: "cast(null as float)" },
153
+ { name: "access_count", valueSql: "cast(null as int)" },
154
+ { name: "last_accessed", valueSql: "cast(null as timestamp)" },
155
+ ]);
156
+ }
157
+ /**
158
+ * Ensure the vector column has an IVF_PQ index.
159
+ * Skipped when: table has too few rows, index already exists, or table is a mock.
160
+ * Called automatically by createMemoryTableWithDimension after migration.
161
+ */
162
+ export async function ensureVectorIndex(table) {
163
+ if (typeof table.listIndices !== "function")
164
+ return;
165
+ try {
166
+ const indices = await table.listIndices();
167
+ if (indices.some((i) => i.columns.includes("vector")))
168
+ return;
169
+ const rowCount = await table.countRows();
170
+ if (rowCount < INDEX_CONFIG.vector.num_partitions)
171
+ return;
172
+ await table.createIndex("vector", {
173
+ config: Index.ivfPq({
174
+ numPartitions: INDEX_CONFIG.vector.num_partitions,
175
+ numSubVectors: INDEX_CONFIG.vector.num_sub_vectors,
176
+ distanceType: INDEX_CONFIG.vector.metric,
177
+ }),
178
+ replace: false,
179
+ });
180
+ }
181
+ catch {
182
+ // Index creation is best-effort — never block table access
183
+ }
184
+ }
115
185
  /**
116
186
  * Memory table schema using Apache Arrow format (default 384 dimensions)
117
187
  * @deprecated Use createMemorySchema(vectorDim) for dynamic dimensions
@@ -153,16 +223,23 @@ export async function createMemoryTable(db, tableName = "memory_entries") {
153
223
  */
154
224
  export async function createMemoryTableWithDimension(db, tableName, vectorDim) {
155
225
  try {
156
- // Check if table already exists
157
226
  const existingTables = await db.tableNames();
227
+ let table;
158
228
  if (existingTables.includes(tableName)) {
159
- return await db.openTable(tableName);
229
+ table = await db.openTable(tableName);
230
+ }
231
+ else {
232
+ // New tables use V2 schema and stable storage format
233
+ const schema = createMemorySchemaV2(vectorDim);
234
+ table = await db.createTable(tableName, [], {
235
+ schema,
236
+ storageOptions: { new_table_data_storage_version: "stable" },
237
+ });
160
238
  }
161
- // Create schema with specified dimension
162
- const schema = createMemorySchema(vectorDim);
163
- // Create table with schema
164
- // LanceDB v0.23.0+ accepts empty array as initial data with schema option
165
- const table = await db.createTable(tableName, [], { schema }); // Cast to any because lancedb types might be strict about options
239
+ // Migrate existing tables to V2 (manifest paths + schema columns, idempotent)
240
+ await migrateTableV2(table);
241
+ // Ensure vector index exists (no-op if already present or insufficient rows)
242
+ await ensureVectorIndex(table);
166
243
  return table;
167
244
  }
168
245
  catch (error) {
@@ -178,6 +255,8 @@ export default {
178
255
  createMemorySchema,
179
256
  createMemorySchemaV2,
180
257
  isSchemaV2,
258
+ migrateTableV2,
259
+ ensureVectorIndex,
181
260
  getEmbeddingDimension,
182
261
  DEFAULT_VECTOR_DIMENSION,
183
262
  EMBEDDING_DIMENSIONS,
@@ -9,6 +9,8 @@
9
9
  * - text-embedding-3-small: 1536 dimensions
10
10
  */
11
11
  import * as arrow from "apache-arrow";
12
+ import * as lancedb from "@lancedb/lancedb";
13
+ import { Index } from "@lancedb/lancedb";
12
14
  /**
13
15
  * Default vector dimension (all-MiniLM-L6-v2)
14
16
  */
@@ -112,6 +114,69 @@ export function createSynthesizedSkillSchema(vectorDim = DEFAULT_VECTOR_DIMENSIO
112
114
  export function isSchemaV2(schema) {
113
115
  return schema.fields.some((f) => f.name === "session_id");
114
116
  }
117
+ /**
118
+ * Migrate an existing table to V2:
119
+ * 1. Migrate manifest paths to V2 layout (efficient versioning, idempotent)
120
+ * 2. Add nullable V2 columns to memory_entries-style tables if not already present
121
+ *
122
+ * Safe to call on any table — non-memory tables skip the schema column additions.
123
+ */
124
+ export async function migrateTableV2(table) {
125
+ // Step 1: manifest path migration (idempotent on already-migrated tables)
126
+ try {
127
+ await table.migrateManifestPathsV2();
128
+ }
129
+ catch {
130
+ // Already migrated or not a local table — ignore
131
+ }
132
+ // Step 2: add V2 schema columns if this is a memory_entries-style table (V1)
133
+ // Guard: schema() may not exist on mock tables in tests
134
+ if (typeof table.schema !== "function") return;
135
+ let schema;
136
+ try {
137
+ schema = await table.schema();
138
+ }
139
+ catch {
140
+ return; // Can't inspect schema — skip
141
+ }
142
+ if (isSchemaV2(schema)) return;
143
+ // Only add V2 columns if the table has the V1 memory_entries shape
144
+ const fieldNames = schema.fields.map((f) => f.name);
145
+ if (!fieldNames.includes("content") || !fieldNames.includes("vector")) return;
146
+ await table.addColumns([
147
+ { name: "session_id", valueSql: "cast(null as string)" },
148
+ { name: "agent_id", valueSql: "cast(null as string)" },
149
+ { name: "memory_type", valueSql: "cast(null as string)" },
150
+ { name: "importance_score", valueSql: "cast(null as float)" },
151
+ { name: "access_count", valueSql: "cast(null as int)" },
152
+ { name: "last_accessed", valueSql: "cast(null as timestamp)" },
153
+ ]);
154
+ }
155
+ /**
156
+ * Ensure the vector column has an IVF_PQ index.
157
+ * Skipped when: table has too few rows, index already exists, or table is a mock.
158
+ * Called automatically by createMemoryTableWithDimension after migration.
159
+ */
160
+ export async function ensureVectorIndex(table) {
161
+ if (typeof table.listIndices !== "function") return;
162
+ try {
163
+ const indices = await table.listIndices();
164
+ if (indices.some((i) => i.columns.includes("vector"))) return;
165
+ const rowCount = await table.countRows();
166
+ if (rowCount < INDEX_CONFIG.vector.num_partitions) return;
167
+ await table.createIndex("vector", {
168
+ config: Index.ivfPq({
169
+ numPartitions: INDEX_CONFIG.vector.num_partitions,
170
+ numSubVectors: INDEX_CONFIG.vector.num_sub_vectors,
171
+ distanceType: INDEX_CONFIG.vector.metric,
172
+ }),
173
+ replace: false,
174
+ });
175
+ }
176
+ catch {
177
+ // Index creation is best-effort — never block table access
178
+ }
179
+ }
115
180
  /**
116
181
  * Memory table schema using Apache Arrow format (default 384 dimensions)
117
182
  * @deprecated Use createMemorySchema(vectorDim) for dynamic dimensions
@@ -153,16 +218,23 @@ export async function createMemoryTable(db, tableName = "memory_entries") {
153
218
  */
154
219
  export async function createMemoryTableWithDimension(db, tableName, vectorDim) {
155
220
  try {
156
- // Check if table already exists
157
221
  const existingTables = await db.tableNames();
222
+ let table;
158
223
  if (existingTables.includes(tableName)) {
159
- return await db.openTable(tableName);
224
+ table = await db.openTable(tableName);
225
+ }
226
+ else {
227
+ // New tables use V2 schema and stable storage format
228
+ const schema = createMemorySchemaV2(vectorDim);
229
+ table = await db.createTable(tableName, [], {
230
+ schema,
231
+ storageOptions: { new_table_data_storage_version: "stable" },
232
+ });
160
233
  }
161
- // Create schema with specified dimension
162
- const schema = createMemorySchema(vectorDim);
163
- // Create table with schema
164
- // LanceDB v0.23.0+ accepts empty array as initial data with schema option
165
- const table = await db.createTable(tableName, [], { schema }); // Cast to any because lancedb types might be strict about options
234
+ // Migrate existing tables to V2 (manifest paths + schema columns, idempotent)
235
+ await migrateTableV2(table);
236
+ // Ensure vector index exists (no-op if already present or insufficient rows)
237
+ await ensureVectorIndex(table);
166
238
  return table;
167
239
  }
168
240
  catch (error) {
@@ -178,6 +250,8 @@ export default {
178
250
  createMemorySchema,
179
251
  createMemorySchemaV2,
180
252
  isSchemaV2,
253
+ migrateTableV2,
254
+ ensureVectorIndex,
181
255
  getEmbeddingDimension,
182
256
  DEFAULT_VECTOR_DIMENSION,
183
257
  EMBEDDING_DIMENSIONS,
@@ -40,15 +40,25 @@ export function createYamoSchema() {
40
40
  */
41
41
  export async function createYamoTable(db, tableName = "yamo_blocks") {
42
42
  try {
43
- // Check if table already exists
44
43
  const existingTables = await db.tableNames();
44
+ let table;
45
45
  if (existingTables.includes(tableName)) {
46
- // Table exists, open it
47
- return await db.openTable(tableName);
46
+ table = await db.openTable(tableName);
47
+ }
48
+ else {
49
+ const schema = createYamoSchema();
50
+ table = await db.createTable(tableName, [], {
51
+ schema,
52
+ storageOptions: { new_table_data_storage_version: "stable" },
53
+ });
54
+ }
55
+ // Migrate manifest paths to V2 layout (idempotent)
56
+ try {
57
+ await table.migrateManifestPathsV2();
58
+ }
59
+ catch {
60
+ // Already migrated or not a local table — ignore
48
61
  }
49
- // Create new table with YAMO schema
50
- const schema = createYamoSchema();
51
- const table = await db.createTable(tableName, [], { schema });
52
62
  return table;
53
63
  }
54
64
  catch (error) {
@@ -40,15 +40,25 @@ export function createYamoSchema() {
40
40
  */
41
41
  export async function createYamoTable(db, tableName = "yamo_blocks") {
42
42
  try {
43
- // Check if table already exists
44
43
  const existingTables = await db.tableNames();
44
+ let table;
45
45
  if (existingTables.includes(tableName)) {
46
- // Table exists, open it
47
- return await db.openTable(tableName);
46
+ table = await db.openTable(tableName);
47
+ }
48
+ else {
49
+ const schema = createYamoSchema();
50
+ table = await db.createTable(tableName, [], {
51
+ schema,
52
+ storageOptions: { new_table_data_storage_version: "stable" },
53
+ });
54
+ }
55
+ // Migrate manifest paths to V2 layout (idempotent)
56
+ try {
57
+ await table.migrateManifestPathsV2();
58
+ }
59
+ catch {
60
+ // Already migrated or not a local table — ignore
48
61
  }
49
- // Create new table with YAMO schema
50
- const schema = createYamoSchema();
51
- const table = await db.createTable(tableName, [], { schema });
52
62
  return table;
53
63
  }
54
64
  catch (error) {
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@yamo/memory-mesh",
3
- "version": "3.2.2",
3
+ "version": "3.2.5",
4
4
  "description": "Portable semantic memory system with Layer 0 Scrubber for YAMO agents (v3 Singularity Edition)",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/yamo-protocol/yamo-memory-mesh"
8
+ },
5
9
  "type": "module",
6
10
  "main": "lib/memory/index.js",
7
11
  "types": "lib/memory/index.d.ts",
@@ -23,16 +27,16 @@
23
27
  "prepublishOnly": "npm run build"
24
28
  },
25
29
  "dependencies": {
26
- "@lancedb/lancedb": "^0.23.0",
27
- "@xenova/transformers": "^2.17.0",
28
- "apache-arrow": "^17.0.0",
30
+ "@lancedb/lancedb": "^0.26.2",
31
+ "@xenova/transformers": "^2.17.2",
32
+ "apache-arrow": "^18.1.0",
29
33
  "commander": "^14.0.3",
30
- "onnxruntime-node": "^1.18.0",
34
+ "onnxruntime-node": "^1.24.3",
31
35
  "pino": "^10.3.1",
32
36
  "pino-pretty": "^13.1.3",
33
37
  "cli-progress": "^3.12.0",
34
38
  "picocolors": "^1.1.1",
35
- "glob": "^13.0.5"
39
+ "glob": "^13.0.6"
36
40
  },
37
41
  "author": "Soverane Labs",
38
42
  "license": "MIT",
@@ -40,9 +44,9 @@
40
44
  "node": ">=18.0.0"
41
45
  },
42
46
  "devDependencies": {
43
- "@types/node": "^25.0.9",
47
+ "@types/node": "^25.4.0",
44
48
  "cohere-ai": "^7.20.0",
45
- "openai": "^6.16.0",
49
+ "openai": "^6.27.0",
46
50
  "typescript": "^5.9.3"
47
51
  }
48
52
  }