Skip to content
BlogOctober 29, 2026

Why I Built OpenLTM Around Memory I Can Maintain

Rohi Rikman
Why I Built OpenLTM Around Memory I Can Maintain
An agent that remembers the wrong thing can be harder to work with than one that forgets. Suppose an agent remembers where authentication happens in a project. You move that logic. The old memory still matches every question about authentication, so the agent keeps retrieving it. The memory is relevant. It is also wrong. A bigger context window does not settle that disagreement. Neither does putting the same sentence in a vector database. I built OpenLTM, and the choices I made in it start from that problem: memory needs a way to change its standing when the world changes. Saving a sentence is the easy part. Deciding whether it belongs in the next task is the work. The earlier OpenLTM post covered release mechanics. This one is about why I think this shape of memory is worth using, and where it still needs another check. OpenLTM is for durable knowledge: a preference, an architectural decision, a gotcha worth avoiding. It is not intended to preserve every conversation as a transcript archive. That changes what gets stored. The core gives a memory a category such as architecture, gotcha or constraint, along with importance, confidence and project scope. Project goals and progress also have their own context records. A temporary update does not have to become a permanent rule merely because the agent wrote it down. Consider the difference between "the build failed" and "this integration test needs a running database." The first describes an event. The second might be a reusable gotcha. Neither becomes true because a category field says so, but the structure gives capture and retrieval something better to work with than a pile of paragraphs. I want the agent to retrieve knowledge for the task in front of it. The core's project filter includes that project's memories and global ones rather than treating every project's conventions as interchangeable. That still depends on sensible scoping when a memory is written. A database cannot repair a bad decision to make a local convention global. SQLite keeps the durable store on disk. FTS5 supplies full-text retrieval without requiring an embedding service. The graph is stored as relations in that database, not in a mandatory graph server. That is a dependency choice, not a claim that SQLite wins every storage problem. In the core, semantic recall can run alongside full-text search and the results are fused. If the embedding path is disabled, fails, or takes longer than its two-second budget, recall falls back to full-text search alone. I like that failure boundary: losing an optional retrieval improvement should not make stored knowledge inaccessible. The current full-text migration uses Porter stemming, so it does more than compare literal strings. That is still not the same as understanding a paraphrase. Semantic retrieval earns its place when the wording changes enough that keyword search misses the connection. It is an optional capability with an actual provider to configure, not something the database filename grants you. Plain Markdown notes remain a good answer when what you need is a short, explicit document. I am not arguing that a preference needs a database just to be readable. What a notes file alone does not supply is OpenLTM's implemented lifecycle: record-level status, filtered recall, file anchors and reviewable relations. You can build those around files. Then the lifecycle code, rather than Markdown itself, is doing that work. I apply the same test to built-in assistant memory. If it already meets your needs, use it. My requirement here is a memory store that can be inspected and shared across supported agent hosts, without making one assistant's interface the only way to reach it. For a vector-database-backed memory service, the question I ask before comparing search quality is a different one: what handles scope, invalidation and corrections? Those are separate requirements from nearest-neighbor retrieval. OpenLTM puts implementations of them beside the local store. That is a comparison of requirements, not a claim that every competing service lacks them. Importance and decay make retention an explicit policy. Lower-importance memories can lose standing as they age; the janitor refreshes decay scores and can deprecate weak entries. Importance 5 is exempt from age decay. The benefit is a store that has machinery for reducing accumulated noise, instead of assuming every saved observation deserves equal attention forever. It is not automatic fact-checking. An old fact can remain correct. A new one can be nonsense. There is also a feedback problem. Recall updates usage timestamps. Repeating a memory can reinforce it. Neither action independently verifies its content. That is why code-anchored staleness matters more to me than a promise to "remember everything." A memory can name the files it depends on. The Claude Code commit hook can flag anchored memories when those files change, preserving the reason and lowering their recall standing. Revalidation clears the flag after review. A changed file does not prove the memory is false. It proves there is a reason to look again. The boundary is narrow. Unanchored memories do not get this signal. The core deliberately exempts importance 5 from automatic file-change flagging too, which is a real cost of marking something permanent. And a revalidation call records a decision to clear the flag; it does not inspect the code on the caller's behalf. I prefer a visible maintenance signal with those limits to letting popularity stand in for correctness. A flat search result can return a decision without the constraint that produced it. OpenLTM's relations include depends_on, supports, refines and contradicts. Graph traversal follows those recorded connections. For example, a design choice might depend on a compatibility constraint. If the constraint changes, the connection gives a reviewer somewhere specific to investigate. Similar wording alone does not express that dependency. The edge is still an assertion. Optional model-inferred relations do not become verified reasoning just because they appear in a graph. What the graph does give is a connection that is explicit and inspectable. Corrections need the same care. The core has a staged-conflict workflow with accept, reject and coexist outcomes. Accepting a staged replacement marks the earlier memory superseded and preserves the relation to its replacement. Coexist keeps both active. The regression tests define rollback behavior if acceptance fails partway through. There is an important unfinished edge here: the documented automatic detection path does not currently feed that staging queue. The review machinery exists; it is not a universal contradiction detector running over every new fact. Near-duplicate reinforcement is a separate mechanism, not proof that conflicting statements have been understood. The design is valuable because it gives a correction somewhere to live without silently replacing the earlier record. It does not relieve the caller of recognizing that a correction is needed. Origin labels record who said what: an owner-statement is different from an agent-observation, and historical-approval describes the past. A write that does not say where it came from is recorded as an agent-observation. The caller supplies those labels. They are provenance metadata, not authenticated identity or permission to act. A local database can still feed a remote model. An embedding provider or maintenance LLM is an outgoing path, even when the original row never leaves the disk as a file. OpenLTM's core puts handling rules inside that path. Private-tagged memories are excluded from default recall, and learn skips embedding and automatic relation processing for a private-tagged write. Graph traversal also excludes private nodes. Secret scrubbing runs before core learning stores content, and the scrubber returns a redacted placeholder rather than the original text if it fails. Those controls matter because they sit where memory is written or consumed. A sentence in a setup guide asking the agent to be careful would not provide the same behavior. They are not a vault. The database stores plaintext. The private tag is a visibility rule, with explicit opt-in access on supported read paths. Pattern-based scrubbing cannot recognize every sensitive fact. Nor does choosing local storage prove that the installed host, provider configuration and every outgoing path have been audited. Credentials do not belong in it, scrubber or no scrubber. The shared database matters because knowledge does not have to belong to the interface that first captured it. The documented Hermes and Claude Code integration can point both at the same store. But Hermes uses a native Python implementation over the shared database, not the TypeScript core wrapped in Python. Its capture behavior differs from Claude Code's session-end proposal workflow. Shared data is not identical lifecycle behavior. The integration guide is appropriately specific about the cost. Back up before the first shared-database migration. Update the Hermes plugin before core changes its embedding layout. Align embedding models if you expect both writers' vectors to be comparable. Even exact-text deduplication differs between the writers, so an identical sentence can be stored twice. Those are not reasons to abandon sharing. They are reasons not to advertise it as "point at a file and forget about it." I think OpenLTM is good for an engineer who wants persistent agent knowledge with a local retrieval baseline and explicit ways to inspect, age and correct it. If your short notes file is doing the job, keep it. If you need a distributed memory service, this article is not evidence that a shared SQLite file meets those requirements. The memory I want to keep is the one I can question later. Every link points at revision a0c8b41.
Share this post: