{"slug": "agent-memory-that-commits-the-write-before-it-tries-to-embed", "title": "Agent memory that commits the write before it tries to embed", "summary": "A developer released hinterland, a zero-dependency agent memory library for Node.js that commits writes to SQLite before attempting to embed them, ensuring durability on unreliable connections. The library offers lexical fallback search when embeddings are unavailable and a backfill operation to catch up on missing vectors, addressing a common assumption in agent memory systems that embedding APIs are always reachable.", "body_md": "I build on a connection that drops for hours at a time. That exposed an assumption nearly every agent memory library makes without stating it.\n\nIn most of them, `remember()` calls an embedding API, so the write fails when the connection does.\n\nIt is an ordering decision, made once, that everything else follows from.\n\n```\n// The row lands first, unconditionally.\nthis.db.prepare(`INSERT INTO memories (...) VALUES (...)`).run(...);\n\n// Then we try. This is allowed to fail.\nawait this.#tryEmbed([{ id: memoryId, content }]);\n```\n\nThe write is durable before anything touches the network. If the embedder is unreachable, unconfigured or slow, the memory is still there. It gets its vector later, whenever there is a link.\n\nCommitting the write is half of it. If recall then requires vectors, the failure has moved rather than gone.\n\nSo retrieval runs whatever it can:\n\n``` js\n// Lexical always runs. It needs nothing.\nif (mode !== 'lexical') {\n  const vectors = await this.embedder.embed([query]);\n  if (vectors && vectors[0]) {\n    // Semantic joins in when it can.\n  }\n}\n```\n\nWith embeddings present you get semantic search fused with full text search. With none, you get full text search alone. That is worse, and it is the same API returning the same shape.\n\nSo your code has no `if (hasVectors)` in it. That property matters more than the retrieval quality, because a fallback you handle explicitly is a second code path, and second code paths drift from the first.\n\nMemories written offline need to catch up, so that is a named operation:\n\n```\nconst { embedded, remaining } = await memory.backfill();\n```\n\nIt embeds everything without a vector for the current model, in batches, and reports what is left. `memory.pending()` tells you how many are waiting.\n\nTwo details worth stealing.\n\nBackfill checks availability first and returns a reason instead of throwing. Being offline is the expected state, so `{ embedded: 0, remaining: 42, reason: 'no embedder' }` is a normal answer.\n\nChanging a memory's content deletes its vector immediately:\n\n```\nthis.db.prepare('DELETE FROM embeddings WHERE memory_id = ?').run(memoryId);\n```\n\nAn embedding describes text. If the text changed, the vector describes something that no longer exists, and nothing will ever tell you it is wrong. Deleting it puts the memory back in the backfill queue.\n\nLocal search on npm is not an unfilled gap. [Orama](https://www.npmjs.com/package/@orama/orama) is a complete in-process search engine and RAG pipeline. The `sqlite-vec` bindings put vector search inside SQLite directly. Both are good and both are more capable than this at what they are for.\n\nThe difference is what they assume about the embedding. They treat it as present, or as somebody else's problem. This one is built for the case where it is neither.\n\nWith reliable connectivity you probably do not need this. If you have had a write fail because a model was unreachable, you know what it is for.\n\n```\nnpm install hinterland\n```\n\nZero dependencies, one SQLite file, Node 22.5+. [hinterland](https://github.com/catidegla/hinterland).\n\nThe storage side has its own article, because `node:sqlite` removes almost all of the usual setup.", "url": "https://wpnews.pro/news/agent-memory-that-commits-the-write-before-it-tries-to-embed", "canonical_source": "https://dev.to/catidegla/agent-memory-that-commits-the-write-before-it-tries-to-embed-nck", "published_at": "2026-09-09 15:08:02+00:00", "updated_at": "2026-09-09 15:20:12.949589+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "machine-learning"], "entities": ["hinterland", "Orama", "sqlite-vec", "Node.js", "SQLite"], "alternates": {"html": "https://wpnews.pro/news/agent-memory-that-commits-the-write-before-it-tries-to-embed", "markdown": "https://wpnews.pro/news/agent-memory-that-commits-the-write-before-it-tries-to-embed.md", "text": "https://wpnews.pro/news/agent-memory-that-commits-the-write-before-it-tries-to-embed.txt", "jsonld": "https://wpnews.pro/news/agent-memory-that-commits-the-write-before-it-tries-to-embed.jsonld"}}