@ai-agent-forge/plugin-memory 0.88.19 → 0.88.20

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.
@@ -1 +1 @@
1
- {"version":3,"file":"vector-index.d.ts","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"AA+CA,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,wBAAwB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IAC5G,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IACpG,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAiK/B","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\tlet closed = false;\n\tlet closePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst withOpenIndex = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tif (closed) return Promise.reject(new Error(\"memory vector index is closed\"));\n\t\tconst call = operation();\n\t\tactiveCalls.add(call);\n\t\tvoid call.then(\n\t\t\t() => activeCalls.delete(call),\n\t\t\t() => activeCalls.delete(call),\n\t\t);\n\t\treturn call;\n\t};\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: (entries) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (entries.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\t\towner: entry.owner,\n\t\t\t\t\tmodelId: entry.modelId,\n\t\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t\t}));\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。\n\t\t\t\tawait current.mergeInsert(\"memoryId\").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);\n\t\t\t}),\n\n\t\tremove: (memoryIds) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (memoryIds.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\t\tawait current.delete(predicate);\n\t\t\t}),\n\n\t\tqueryKnn: (vector, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t\t}),\n\n\t\tqueryFts: (queryText, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t\t}),\n\n\t\tlistMemoryIds: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn new Set<string>();\n\t\t\t\t}\n\t\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t\t}),\n\n\t\tcount: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\treturn current.countRows();\n\t\t\t}),\n\n\t\tgetStoredModelId: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\t\treturn rows[0]?.modelId;\n\t\t\t}),\n\n\t\tclose: () => {\n\t\t\tclosed = true;\n\t\t\tclosePromise ??= (async () => {\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tdb.close();\n\t\t\t})();\n\t\t\treturn closePromise;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"vector-index.d.ts","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"AAyBA,8EAA8E;AAC9E,KAAK,kBAAkB,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAeF;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,2BAA2B;IAC3C,qFAA2C;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,oDAA8B;IAC9B,QAAQ,CAAC,UAAU,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,IAAI,EAAE,SAAS,kBAAkB,EAAE,EACnC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,2BAA2B,CAgB7B;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,wBAAwB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IAC5G,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IACpG,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACpE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAgM/B","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\n/**\n * 同 memoryId 不同 body 的写入 (P5 残留守卫): memoryId 由 owner + observationId\n * 派生、不含记忆域, 而投影表按 owner 共享——跨域/跨进程复用同一 id 时\n * mergeInsert 的 whenMatchedUpdateAll 会静默覆盖另一条 canonical 原子的行,\n * 启动对账只 diff id 集合、不会重建被覆盖的 body (2026-10-27 tick 48 实测)。\n */\nexport interface MemoryVectorCollisionV1 {\n\treadonly memoryId: string;\n\treadonly storedBody: string;\n\treadonly incomingBody: string;\n}\n\nexport interface MemoryVectorIdentitySplitV1 {\n\t/** 行身份与既有行相同 (新 id 或同 body 重投影), 可安全写入。 */\n\treadonly accepted: readonly MemoryVectorRecord[];\n\t/** 同 id 不同 body 的写入, 必须拒绝。 */\n\treadonly collisions: readonly MemoryVectorCollisionV1[];\n}\n\n/**\n * 按既有行 body 切分待写行: 只有 id 未出现或 body 完全一致的写入可以通过。\n * body 变更对同一 id 恒为外来碰撞 (atom 不可变, 同 id 内容变化只可能来自另一\n * 原子), 拒绝该行可保证先写入的原子排序信号不被另一端覆盖。\n */\nexport function partitionIdentitySafeWrites(\n\trows: readonly MemoryVectorRecord[],\n\tstored: ReadonlyMap<string, string>,\n): MemoryVectorIdentitySplitV1 {\n\tconst accepted: MemoryVectorRecord[] = [];\n\tconst collisions: MemoryVectorCollisionV1[] = [];\n\tfor (const row of rows) {\n\t\tconst storedBody = stored.get(row.memoryId);\n\t\tif (storedBody === undefined) {\n\t\t\taccepted.push(row);\n\t\t\tcontinue;\n\t\t}\n\t\tif (storedBody === row.body) {\n\t\t\taccepted.push(row);\n\t\t\tcontinue;\n\t\t}\n\t\tcollisions.push({ memoryId: row.memoryId, storedBody, incomingBody: row.body });\n\t}\n\treturn { accepted, collisions };\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n\t/**\n\t * 身份碰撞上报: 同 memoryId 不同 body 的写入被拒绝时按 (memoryId, 既有 body)\n\t * 去重调用一次。缺省 = 只拒绝不报 (拒绝本身已生效)。\n\t */\n\treadonly onCollision?: (collision: MemoryVectorCollisionV1) => void;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\tlet closed = false;\n\tlet closePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst withOpenIndex = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tif (closed) return Promise.reject(new Error(\"memory vector index is closed\"));\n\t\tconst call = operation();\n\t\tactiveCalls.add(call);\n\t\tvoid call.then(\n\t\t\t() => activeCalls.delete(call),\n\t\t\t() => activeCalls.delete(call),\n\t\t);\n\t\treturn call;\n\t};\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\t/** 既有行的 body (按 memoryId), upsert 前做身份核对: 缺省 = 全新 id。 */\n\tconst storedBodiesFor = async (\n\t\ttarget: LanceDbTable,\n\t\trows: readonly MemoryVectorRecord[],\n\t): Promise<Map<string, string>> => {\n\t\tconst ids = [...new Set(rows.map((row) => row.memoryId))];\n\t\tif (ids.length === 0) return new Map();\n\t\tconst predicate = `memoryId IN (${ids.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\tconst found: { memoryId: string; body: string }[] = await target\n\t\t\t.query()\n\t\t\t.where(predicate)\n\t\t\t.select([\"memoryId\", \"body\"])\n\t\t\t.toArray();\n\t\treturn new Map(found.map((row) => [row.memoryId, row.body]));\n\t};\n\t/** 已上报过的碰撞 (memoryId + 既有 body), 避免逐次重投影刷屏。 */\n\tconst reportedCollisions = new Set<string>();\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: (entries) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (entries.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\t\towner: entry.owner,\n\t\t\t\t\tmodelId: entry.modelId,\n\t\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t\t}));\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { accepted, collisions } = partitionIdentitySafeWrites(rows, await storedBodiesFor(current, rows));\n\t\t\t\tfor (const collision of collisions) {\n\t\t\t\t\tconst key = `${collision.memoryId}\\u0000${collision.storedBody}`;\n\t\t\t\t\tif (reportedCollisions.has(key)) continue;\n\t\t\t\t\treportedCollisions.add(key);\n\t\t\t\t\toptions.onCollision?.(collision);\n\t\t\t\t}\n\t\t\t\tif (accepted.length === 0) return;\n\t\t\t\t// mergeInsert upsert: 同 memoryId 且 body 一致才更新 (身份核对已在上一步\n\t\t\t\t// 排除外来碰撞), 新 id 插入。\n\t\t\t\tawait current\n\t\t\t\t\t.mergeInsert(\"memoryId\")\n\t\t\t\t\t.whenMatchedUpdateAll()\n\t\t\t\t\t.whenNotMatchedInsertAll()\n\t\t\t\t\t.execute([...accepted]);\n\t\t\t}),\n\n\t\tremove: (memoryIds) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (memoryIds.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\t\tawait current.delete(predicate);\n\t\t\t}),\n\n\t\tqueryKnn: (vector, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t\t}),\n\n\t\tqueryFts: (queryText, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t\t}),\n\n\t\tlistMemoryIds: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn new Set<string>();\n\t\t\t\t}\n\t\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t\t}),\n\n\t\tcount: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\treturn current.countRows();\n\t\t\t}),\n\n\t\tgetStoredModelId: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\t\treturn rows[0]?.modelId;\n\t\t\t}),\n\n\t\tclose: () => {\n\t\t\tclosed = true;\n\t\t\tclosePromise ??= (async () => {\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tdb.close();\n\t\t\t})();\n\t\t\treturn closePromise;\n\t\t},\n\t};\n}\n"]}
@@ -5,6 +5,28 @@ const RRF_K = 60;
5
5
  function escapeSqlLiteral(value) {
6
6
  return value.replace(/'/g, "''");
7
7
  }
8
+ /**
9
+ * 按既有行 body 切分待写行: 只有 id 未出现或 body 完全一致的写入可以通过。
10
+ * body 变更对同一 id 恒为外来碰撞 (atom 不可变, 同 id 内容变化只可能来自另一
11
+ * 原子), 拒绝该行可保证先写入的原子排序信号不被另一端覆盖。
12
+ */
13
+ export function partitionIdentitySafeWrites(rows, stored) {
14
+ const accepted = [];
15
+ const collisions = [];
16
+ for (const row of rows) {
17
+ const storedBody = stored.get(row.memoryId);
18
+ if (storedBody === undefined) {
19
+ accepted.push(row);
20
+ continue;
21
+ }
22
+ if (storedBody === row.body) {
23
+ accepted.push(row);
24
+ continue;
25
+ }
26
+ collisions.push({ memoryId: row.memoryId, storedBody, incomingBody: row.body });
27
+ }
28
+ return { accepted, collisions };
29
+ }
8
30
  /**
9
31
  * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.
10
32
  * Table creation is deferred to the first upsert; queries against a
@@ -48,6 +70,21 @@ export async function createMemoryVectorIndex(options) {
48
70
  // 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。
49
71
  }
50
72
  };
73
+ /** 既有行的 body (按 memoryId), upsert 前做身份核对: 缺省 = 全新 id。 */
74
+ const storedBodiesFor = async (target, rows) => {
75
+ const ids = [...new Set(rows.map((row) => row.memoryId))];
76
+ if (ids.length === 0)
77
+ return new Map();
78
+ const predicate = `memoryId IN (${ids.map((id) => `'${escapeSqlLiteral(id)}'`).join(",")})`;
79
+ const found = await target
80
+ .query()
81
+ .where(predicate)
82
+ .select(["memoryId", "body"])
83
+ .toArray();
84
+ return new Map(found.map((row) => [row.memoryId, row.body]));
85
+ };
86
+ /** 已上报过的碰撞 (memoryId + 既有 body), 避免逐次重投影刷屏。 */
87
+ const reportedCollisions = new Set();
51
88
  return {
52
89
  replicaId: "memory-vector-index",
53
90
  upsert: (entries) => withOpenIndex(async () => {
@@ -74,8 +111,23 @@ export async function createMemoryVectorIndex(options) {
74
111
  await createBodyFtsIndex(table);
75
112
  return;
76
113
  }
77
- // mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。
78
- await current.mergeInsert("memoryId").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);
114
+ const { accepted, collisions } = partitionIdentitySafeWrites(rows, await storedBodiesFor(current, rows));
115
+ for (const collision of collisions) {
116
+ const key = `${collision.memoryId}\u0000${collision.storedBody}`;
117
+ if (reportedCollisions.has(key))
118
+ continue;
119
+ reportedCollisions.add(key);
120
+ options.onCollision?.(collision);
121
+ }
122
+ if (accepted.length === 0)
123
+ return;
124
+ // mergeInsert upsert: 同 memoryId 且 body 一致才更新 (身份核对已在上一步
125
+ // 排除外来碰撞), 新 id 插入。
126
+ await current
127
+ .mergeInsert("memoryId")
128
+ .whenMatchedUpdateAll()
129
+ .whenNotMatchedInsertAll()
130
+ .execute([...accepted]);
79
131
  }),
80
132
  remove: (memoryIds) => withOpenIndex(async () => {
81
133
  if (memoryIds.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"vector-index.js","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\tlet closed = false;\n\tlet closePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst withOpenIndex = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tif (closed) return Promise.reject(new Error(\"memory vector index is closed\"));\n\t\tconst call = operation();\n\t\tactiveCalls.add(call);\n\t\tvoid call.then(\n\t\t\t() => activeCalls.delete(call),\n\t\t\t() => activeCalls.delete(call),\n\t\t);\n\t\treturn call;\n\t};\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: (entries) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (entries.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\t\towner: entry.owner,\n\t\t\t\t\tmodelId: entry.modelId,\n\t\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t\t}));\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t// mergeInsert upsert: 同 memoryId 幂等更新, 新 id 插入。\n\t\t\t\tawait current.mergeInsert(\"memoryId\").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(rows);\n\t\t\t}),\n\n\t\tremove: (memoryIds) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (memoryIds.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\t\tawait current.delete(predicate);\n\t\t\t}),\n\n\t\tqueryKnn: (vector, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t\t}),\n\n\t\tqueryFts: (queryText, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t\t}),\n\n\t\tlistMemoryIds: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn new Set<string>();\n\t\t\t\t}\n\t\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t\t}),\n\n\t\tcount: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\treturn current.countRows();\n\t\t\t}),\n\n\t\tgetStoredModelId: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\t\treturn rows[0]?.modelId;\n\t\t\t}),\n\n\t\tclose: () => {\n\t\t\tclosed = true;\n\t\t\tclosePromise ??= (async () => {\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tdb.close();\n\t\t\t})();\n\t\t\treturn closePromise;\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"vector-index.js","sourceRoot":"","sources":["../../src/memory/vector-index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Memory vector index for hybrid retrieval (混合检索 · LanceDB 适配层).\n *\n * POC-verified (2026-09) against @lancedb/lancedb 0.39.0 on a 100k corpus:\n * vector column must be named `vector` (number[] auto-maps to\n * fixed-size-list<float32>), FTS with `Index.fts({ baseTokenizer: \"icu\" })`\n * covers Chinese text, flat-scan KNN on 100k rows takes ~600ms (预期内).\n *\n * Deliberately NOT implemented (POC 结论, 留二期): IVF_PQ ANN index — flat\n * scan is acceptable at current scale; fused hybrid query via\n * `fullTextQuery()` — that chain does not exist in 0.39.0, the caller composes\n * KNN + FTS itself and fuses with RRF (k=60), which is why this adapter's FTS\n * scores use `1/(60 + rank + 1)`.\n *\n * 边界行为 (POC 观察): 空 table 上建 FTS 索引/查询可能抛错 — 空表查询返回\n * 空数组; 建索引包 try/catch 忽略 (注释见 createBodyFtsIndex)。LanceDB 其他\n * 失败原样抛出, 不吞错、不静默降级。\n *\n * Runtime dep @lancedb/lancedb is lazy-loaded via `await import` (惯例参考\n * packages/agent-forge/src/utils/photon.ts); type positions use type-only\n * imports.\n */\nimport type { Connection, Table as LanceDbTable, Query, VectorQuery } from \"@lancedb/lancedb\";\nimport { importHostOrBareModule } from \"./host-module-import.ts\";\n\n/** Stored row shape: 5 columns; `body` is the FTS text (statement + tags). */\ntype MemoryVectorRecord = {\n\tmemoryId: string;\n\towner: string;\n\tmodelId: string;\n\tbody: string;\n\tvector: number[];\n};\n\ninterface MemoryVectorHitRow {\n\tmemoryId: string;\n\t_distance: number;\n}\n\nconst TABLE_NAME = \"memories\";\n/** RRF 常量, 与调用方融合层一致 (queryFts 注释说明口径)。 */\nconst RRF_K = 60;\n\nfunction escapeSqlLiteral(value: string): string {\n\treturn value.replace(/'/g, \"''\");\n}\n\n/**\n * 同 memoryId 不同 body 的写入 (P5 残留守卫): memoryId 由 owner + observationId\n * 派生、不含记忆域, 而投影表按 owner 共享——跨域/跨进程复用同一 id 时\n * mergeInsert 的 whenMatchedUpdateAll 会静默覆盖另一条 canonical 原子的行,\n * 启动对账只 diff id 集合、不会重建被覆盖的 body (2026-10-27 tick 48 实测)。\n */\nexport interface MemoryVectorCollisionV1 {\n\treadonly memoryId: string;\n\treadonly storedBody: string;\n\treadonly incomingBody: string;\n}\n\nexport interface MemoryVectorIdentitySplitV1 {\n\t/** 行身份与既有行相同 (新 id 或同 body 重投影), 可安全写入。 */\n\treadonly accepted: readonly MemoryVectorRecord[];\n\t/** 同 id 不同 body 的写入, 必须拒绝。 */\n\treadonly collisions: readonly MemoryVectorCollisionV1[];\n}\n\n/**\n * 按既有行 body 切分待写行: 只有 id 未出现或 body 完全一致的写入可以通过。\n * body 变更对同一 id 恒为外来碰撞 (atom 不可变, 同 id 内容变化只可能来自另一\n * 原子), 拒绝该行可保证先写入的原子排序信号不被另一端覆盖。\n */\nexport function partitionIdentitySafeWrites(\n\trows: readonly MemoryVectorRecord[],\n\tstored: ReadonlyMap<string, string>,\n): MemoryVectorIdentitySplitV1 {\n\tconst accepted: MemoryVectorRecord[] = [];\n\tconst collisions: MemoryVectorCollisionV1[] = [];\n\tfor (const row of rows) {\n\t\tconst storedBody = stored.get(row.memoryId);\n\t\tif (storedBody === undefined) {\n\t\t\taccepted.push(row);\n\t\t\tcontinue;\n\t\t}\n\t\tif (storedBody === row.body) {\n\t\t\taccepted.push(row);\n\t\t\tcontinue;\n\t\t}\n\t\tcollisions.push({ memoryId: row.memoryId, storedBody, incomingBody: row.body });\n\t}\n\treturn { accepted, collisions };\n}\n\nexport interface MemoryVectorIndexEntryV1 {\n\treadonly memoryId: string;\n\treadonly owner: string;\n\treadonly modelId: string;\n\treadonly statement: string;\n\treadonly tags: readonly string[];\n\treadonly vector: readonly number[];\n}\n\nexport interface MemoryVectorIndexQueryV1 {\n\treadonly owner: string;\n\treadonly limit: number;\n}\n\nexport interface MemoryVectorHitV1 {\n\treadonly memoryId: string;\n\treadonly score: number;\n}\n\nexport interface MemoryVectorIndexV1 {\n\treadonly replicaId: \"memory-vector-index\";\n\tupsert(entries: readonly MemoryVectorIndexEntryV1[]): Promise<void>;\n\tremove(memoryIds: readonly string[]): Promise<void>;\n\tqueryKnn(vector: readonly number[], query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tqueryFts(queryText: string, query: MemoryVectorIndexQueryV1): Promise<readonly MemoryVectorHitV1[]>;\n\tlistMemoryIds(): Promise<ReadonlySet<string>>;\n\tcount(): Promise<number>;\n\tgetStoredModelId(): Promise<string | undefined>;\n\tclose(): Promise<void>;\n}\n\n/**\n * Open (lazily) / create the LanceDB-backed memory vector index at `dbPath`.\n * Table creation is deferred to the first upsert; queries against a\n * not-yet-created table return empty results instead of throwing.\n */\nexport async function createMemoryVectorIndex(options: {\n\treadonly dbPath: string;\n\treadonly dimensions: number;\n\t/** Host-resolved lancedb entry path (install-store visibility); undefined = bare import. */\n\treadonly moduleEntryPath?: string;\n\t/**\n\t * 身份碰撞上报: 同 memoryId 不同 body 的写入被拒绝时按 (memoryId, 既有 body)\n\t * 去重调用一次。缺省 = 只拒绝不报 (拒绝本身已生效)。\n\t */\n\treadonly onCollision?: (collision: MemoryVectorCollisionV1) => void;\n}): Promise<MemoryVectorIndexV1> {\n\tconst dimensions = options.dimensions;\n\tconst lancedb = await importHostOrBareModule<typeof import(\"@lancedb/lancedb\")>(\n\t\toptions.moduleEntryPath ?? \"@lancedb/lancedb\",\n\t);\n\tconst db: Connection = await lancedb.connect(options.dbPath);\n\tlet table: LanceDbTable | null = null;\n\tlet closed = false;\n\tlet closePromise: Promise<void> | undefined;\n\tconst activeCalls = new Set<Promise<unknown>>();\n\tconst withOpenIndex = <T>(operation: () => Promise<T>): Promise<T> => {\n\t\tif (closed) return Promise.reject(new Error(\"memory vector index is closed\"));\n\t\tconst call = operation();\n\t\tactiveCalls.add(call);\n\t\tvoid call.then(\n\t\t\t() => activeCalls.delete(call),\n\t\t\t() => activeCalls.delete(call),\n\t\t);\n\t\treturn call;\n\t};\n\n\tconst ensureTable = async (): Promise<LanceDbTable | null> => {\n\t\tif (table) {\n\t\t\treturn table;\n\t\t}\n\t\tconst names = await db.tableNames();\n\t\tif (!names.includes(TABLE_NAME)) {\n\t\t\treturn null;\n\t\t}\n\t\ttable = await db.openTable(TABLE_NAME);\n\t\treturn table;\n\t};\n\n\t// 空表建 FTS 索引在 LanceDB 0.39 会抛错 (POC 观察); 本适配器仅在首次建表\n\t// (带非空数据) 后建索引, 但仍按契约包 try/catch 容错。忽略建索引失败意味着\n\t// FTS 通道暂不可用 (queryFts 会显式报错), KNN 通道不受影响。\n\tconst createBodyFtsIndex = async (target: LanceDbTable): Promise<void> => {\n\t\ttry {\n\t\t\tawait target.createIndex(\"body\", { config: lancedb.Index.fts({ baseTokenizer: \"icu\" }), replace: true });\n\t\t} catch {\n\t\t\t// 空表/重复建索引等边界忽略; 二期 IVF_PQ 落地时统一补索引管理。\n\t\t}\n\t};\n\n\t/** 既有行的 body (按 memoryId), upsert 前做身份核对: 缺省 = 全新 id。 */\n\tconst storedBodiesFor = async (\n\t\ttarget: LanceDbTable,\n\t\trows: readonly MemoryVectorRecord[],\n\t): Promise<Map<string, string>> => {\n\t\tconst ids = [...new Set(rows.map((row) => row.memoryId))];\n\t\tif (ids.length === 0) return new Map();\n\t\tconst predicate = `memoryId IN (${ids.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\tconst found: { memoryId: string; body: string }[] = await target\n\t\t\t.query()\n\t\t\t.where(predicate)\n\t\t\t.select([\"memoryId\", \"body\"])\n\t\t\t.toArray();\n\t\treturn new Map(found.map((row) => [row.memoryId, row.body]));\n\t};\n\t/** 已上报过的碰撞 (memoryId + 既有 body), 避免逐次重投影刷屏。 */\n\tconst reportedCollisions = new Set<string>();\n\n\treturn {\n\t\treplicaId: \"memory-vector-index\",\n\n\t\tupsert: (entries) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (entries.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (entry.vector.length !== dimensions) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`memory vector dimension mismatch for ${entry.memoryId}: got ${entry.vector.length}, index declares ${dimensions}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorRecord[] = entries.map((entry) => ({\n\t\t\t\t\tmemoryId: entry.memoryId,\n\t\t\t\t\towner: entry.owner,\n\t\t\t\t\tmodelId: entry.modelId,\n\t\t\t\t\tbody: `${entry.statement} ${entry.tags.join(\" \")}`,\n\t\t\t\t\tvector: Array.from(entry.vector),\n\t\t\t\t}));\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\t// mode \"create\" (非 POC 的 \"overwrite\"): 表不存在才建; 并发竞态下\n\t\t\t\t\t// 若表已被他人创建则显式抛错, 而不是静默清空重写。\n\t\t\t\t\ttable = await db.createTable(TABLE_NAME, rows, { mode: \"create\" });\n\t\t\t\t\tawait createBodyFtsIndex(table);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { accepted, collisions } = partitionIdentitySafeWrites(rows, await storedBodiesFor(current, rows));\n\t\t\t\tfor (const collision of collisions) {\n\t\t\t\t\tconst key = `${collision.memoryId}\\u0000${collision.storedBody}`;\n\t\t\t\t\tif (reportedCollisions.has(key)) continue;\n\t\t\t\t\treportedCollisions.add(key);\n\t\t\t\t\toptions.onCollision?.(collision);\n\t\t\t\t}\n\t\t\t\tif (accepted.length === 0) return;\n\t\t\t\t// mergeInsert upsert: 同 memoryId 且 body 一致才更新 (身份核对已在上一步\n\t\t\t\t// 排除外来碰撞), 新 id 插入。\n\t\t\t\tawait current\n\t\t\t\t\t.mergeInsert(\"memoryId\")\n\t\t\t\t\t.whenMatchedUpdateAll()\n\t\t\t\t\t.whenNotMatchedInsertAll()\n\t\t\t\t\t.execute([...accepted]);\n\t\t\t}),\n\n\t\tremove: (memoryIds) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (memoryIds.length === 0) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst predicate = `memoryId IN (${memoryIds.map((id) => `'${escapeSqlLiteral(id)}'`).join(\",\")})`;\n\t\t\t\tawait current.delete(predicate);\n\t\t\t}),\n\n\t\tqueryKnn: (vector, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tif (vector.length !== dimensions) {\n\t\t\t\t\tthrow new Error(`query vector dimension mismatch: got ${vector.length}, index declares ${dimensions}`);\n\t\t\t\t}\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(Array.from(vector)) as VectorQuery)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// 分数口径: 1/(1 + _distance), L2 距离越小分越高, 与 FTS 通道分数同域可融合。\n\t\t\t\treturn rows.map((row) => ({ memoryId: row.memoryId, score: 1 / (1 + row._distance) }));\n\t\t\t}),\n\n\t\tqueryFts: (queryText, query) =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t\tconst rows: MemoryVectorHitRow[] = await (current.search(queryText) as Query)\n\t\t\t\t\t.where(`owner = '${escapeSqlLiteral(query.owner)}'`)\n\t\t\t\t\t.limit(query.limit)\n\t\t\t\t\t.toArray();\n\t\t\t\t// FTS 无距离分: 按返回序 1/(RRF_K + rank + 1) 打分, RRF_K=60 与调用方\n\t\t\t\t// 融合层 (RRF k=60) 一致, 保证 KNN/FTS 两通道分数可融合。\n\t\t\t\treturn rows.map((row, rank) => ({ memoryId: row.memoryId, score: 1 / (RRF_K + rank + 1) }));\n\t\t\t}),\n\n\t\tlistMemoryIds: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn new Set<string>();\n\t\t\t\t}\n\t\t\t\tconst rows: { memoryId: string }[] = await current.query().select([\"memoryId\"]).toArray();\n\t\t\t\treturn new Set(rows.map((row) => row.memoryId));\n\t\t\t}),\n\n\t\tcount: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn 0;\n\t\t\t\t}\n\t\t\t\treturn current.countRows();\n\t\t\t}),\n\n\t\tgetStoredModelId: () =>\n\t\t\twithOpenIndex(async () => {\n\t\t\t\tconst current = await ensureTable();\n\t\t\t\tif (!current) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tconst rows: { modelId: string }[] = await current.query().select([\"modelId\"]).limit(1).toArray();\n\t\t\t\treturn rows[0]?.modelId;\n\t\t\t}),\n\n\t\tclose: () => {\n\t\t\tclosed = true;\n\t\t\tclosePromise ??= (async () => {\n\t\t\t\tawait Promise.allSettled([...activeCalls]);\n\t\t\t\tdb.close();\n\t\t\t})();\n\t\t\treturn closePromise;\n\t\t},\n\t};\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-agent-forge/plugin-memory",
3
- "version": "0.88.19",
3
+ "version": "0.88.20",
4
4
  "description": "First-party memory capability plugin for Agent Forge (memory_write/recall/list/forget over the M5 Memory Foundation)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -48,7 +48,7 @@
48
48
  "@huggingface/transformers": "4.3.0",
49
49
  "@lancedb/lancedb": "0.39.0",
50
50
  "typebox": "1.3.27",
51
- "@ai-agent-forge/plugin-sdk": "^0.88.19"
51
+ "@ai-agent-forge/plugin-sdk": "^0.88.20"
52
52
  },
53
53
  "devDependencies": {
54
54
  "shx": "0.3.4",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "agent-forge.plugin.memory",
3
- "version": "0.88.19",
3
+ "version": "0.88.20",
4
4
  "apiVersion": "1-draft",
5
5
  "entry": "./dist/entry.js",
6
6
  "hostVersion": ">=0.84.4",