diff --git a/CHANGELOG.md b/CHANGELOG.md index 57098831..d2a87355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,13 @@ the end of this file for historical context. - Did not claim the unrelated `longmemory` PyPI project. New Python installs use `longmemory-sdk`. +### Fixes + +- Embedded project-scoped writes. `longmemory_ingest` with a `project_id`, + `longmemory_remember_decision` and connector imports go through + `applyImportPlan`, which stored every node without a semantic vector, so + those memories never received a vector score in recall (#216). + ## [1.0.0] - 2026-08-31 LongMemory 1.0 is a ground-up architecture rewrite rather than an incremental diff --git a/benchmarks/src/check.ts b/benchmarks/src/check.ts index 92b977ff..9db37104 100644 --- a/benchmarks/src/check.ts +++ b/benchmarks/src/check.ts @@ -35,6 +35,7 @@ import { create_named_embedding_provider } from '../../src/core/embeddings/provi import { prepare_evidence_query, evidence_support, evidence_adjustment, order_evidence, query_calendar_window, calendar_relevance } from '../../src/core/recall/rerank.js'; import { associative_recall } from '../../src/core/recall/associative_recall.js'; import { answer_from_evidence, type evidence_reader_model } from '../../src/answering/evidence_reader.js'; +import { createProjectMemory as create_project_memory } from '../../src/core/project/project_memory.js'; const directory = temp(join(tmpdir(), 'longmemory-bench-check-')); const memory = create_memory({ embedding_dimension: 2 }); @@ -451,6 +452,19 @@ try { else process.env[key!] = value; } } + { + // Project writes and decisions go through applyImportPlan, not ingest(); they must still be embedded. + const embedded = create_memory({ embedding_dimension: 4, embedding_provider: { embed: () => [0.5, 0.5, 0.5, 0.5] } }); + try { + const manager = await create_project_memory({ memory: embedded, tenant_id: 'default', project_id: 'check-project', name: 'check' }); + const id = await manager.ingestProjectEvent('check-project', { kind: 'decision', text: 'Staging credentials rotate every 45 days.' }); + const node = (await embedded.explain(id)).node; + assert.ok(node); + assert.equal(node.vectors.semantic?.length, 4); + } finally { + await embedded.close(); + } + } if (!process.argv.includes('--unit-only')) { const smoke = await run_benchmark({ providers: ['longmemory'], datasets: ['smoke'], cutoffs: [5], output_dir: join(directory, 'smoke'), resume: false }); assert.equal(smoke.report.providers[0].failed_questions, 0); diff --git a/src/core/create_memory.ts b/src/core/create_memory.ts index 8ef5f5a8..1b1e9113 100644 --- a/src/core/create_memory.ts +++ b/src/core/create_memory.ts @@ -550,6 +550,12 @@ export function create_memory(config: memory_config = {}): long_memory { throw new Error(`connector node ${node.key} is missing source identity, recorded_at, or provenance`); } } + // The import transaction is synchronous, so embed before entering it, as ingest() does. + const vectors = new Map<(typeof plan.nodes_to_create)[number], number[] | null>(); + for (const node of plan.nodes_to_create) { + const language = detect_language(node.content).language ?? cfg.default_language; + vectors.set(node, await embed(cfg.embedding_provider, multilingual_embeddings, embedding_cache, node.content, language, 'document')); + } return import_transaction().run(() => { const before_nodes = new Set(engine.graph.node_list().map((node) => node.id)); const before_edges = new Set(engine.graph.edge_list().map((edge) => edge.id)); @@ -611,6 +617,7 @@ export function create_memory(config: memory_config = {}): long_memory { id: node.id, user_id: cfg.user_id, text: node.content, + vector: vectors.get(node), at: node.recorded_at, observed_at: node.observed_at, valid_from: node.valid_from,