About the Project

Inspiration

The idea started from a very concrete frustration with how long-running LLM agents actually behave once a conversation stretches past a single session. Four specific failure modes kept showing up, and they became the project's real problem statement:

  • The recompute tax. Re-sending full conversation history on every turn to preserve state means quadratic token growth, and once the context window fills, naive truncation forces a full prefix re-process — latency spikes exactly when you can least afford it.
  • Temporal blindness. A stock vector database retrieves purely on semantic similarity. It has no concept of when something was true, so a stale trace log from yesterday and a load-bearing architectural decision from three weeks ago get treated as equally authoritative.
  • Unresolved factual conflicts. LLMs are bad at noticing when a newly retrieved fact contradicts something they were already told, and they'll often default back to a stale prior instead of the correction.
  • Memory poisoning (MINJA-style indirect prompt injection). Persistent memory is an attack surface. If an agent silently absorbs anything it reads from a tool, a file, or a URL into long-term memory with the same authority as something the user said, an attacker doesn't need to jailbreak the model at all — they just need to get a sentence in front of it once.

The name captures the thesis: memory that is governed, not just stored, where trust is a first-class, provenance-derived property of a fact, not an afterthought bolted onto retrieval.

What it does

Governed ChronoMemory-OS is a memory layer for LLM agents built around one governing rule: a fact is only as trustworthy as where it came from. Every memory carries a provenance tag — user turn, agent turn, tool output, third-party message, external doc, or web content — which sets a base trust score before the content is ever evaluated for truthfulness. Anything below a trust threshold is held for human review instead of silently entering active memory. On top of that admission gate, memory decays on a Weibull-shaped forgetting curve, contradicting facts are resolved deterministically via LLM-driven natural-language-inference classification, and every decision — held, promoted, superseded — is backed by a full audit trail.

How we built it

We split the system into a read/write decoupled control loop, so that answering the user and updating memory never compete for the same latency budget.

Read path: synchronous, cache-optimized retrieval

  • Dual-recall search: pgvector cosine similarity plus a one-hop relational spreading-activation walk over a relational_links table, avoiding the need for a separate graph database.
  • A decay score computed at query time:

$$ \text{score} = \text{importance} \times \text{relevance} \times \exp!\left(-\left(\frac{\text{elapsed_days}}{\eta_{\text{eff}}}\right)^{0.8}\right) $$

where the effective half-life $\eta_{\text{eff}} = 7 \times \min(1 + 0.5 \cdot \text{access_count},\ 5)$ days. Memories that get retrieved often earn a longer effective lifespan — up to 5× — which is the reinforcement half of a spaced-repetition loop. Anything scoring below $\theta = 0.15$ is archived automatically, as a side effect of the very query that touches it — there's no separate cleanup job.

  • Middle-out context trimming: the system prompt and a long pinned profile stay fixed at the top (wrapped in cache_control: ephemeral for prompt caching), recalled memories fill the middle and get trimmed from the center outward under budget pressure, and live chat history stays untouched at the footer.

Write path: asynchronous, non-blocking, governed

  • Every turn (user and agent both) is dispatched to a background thread that extracts durable, standalone facts via a dedicated LLM role, structured as JSON with an importance score.
  • VIGIL, the trust gate: each candidate's trust score is set purely by provenance:

$$ \text{trust}(\text{user_turn}) = 1.0,\quad \text{trust}(\text{agent_turn}) = 0.7,\quad \text{trust}(\text{tool/stdout}) = 0.6 $$

$$ \text{trust}(\text{third_party}) = 0.55,\quad \text{trust}(\text{external_doc}) = 0.4,\quad \text{trust}(\text{web_content}) = 0.3 $$

Anything under 0.5 is held in a physically separate SQLite audit database and never touches the trusted Postgres store — the only way out is an explicit, logged, human-clicked "Promote to Postgres."

  • A contradiction gate runs an NLI classification (contradiction / entailment / neutral) against the top-k nearest existing memories. A contradiction deterministically resolves by max(serial_no) — the newer fact always wins, the older one is marked superseded with a pointer forward, and the decision is logged. Entailment/neutral results become corroborating edges instead, feeding back into a read-time composite trust score:

$$ \text{trust}{\text{composite}} = \text{clamp}\Big(\text{trust}{\text{base}} + \min(0.05 \cdot n_{\text{corroborating}},\ 0.15) - \big[\,\text{access} \ge 3 \wedge \text{freshness} < 0.3\,\big] \cdot 0.10,\ 0,\ 1\Big) $$

a MemGuard-style adjustment that penalizes facts that are confidently wrong: retrieved often, but decayed stale.

We built this on Postgres + pgvector for trusted active memory, a separate SQLite file purely for the audit trail and VIGIL-held candidates, sentence-transformers (all-MiniLM-L6-v2, 384-dim, CPU-only) for embeddings, and Qwen Cloud's OpenAI-compatible chat API across three distinct model roles: qwen3.6-plus for the user-facing agent, qwen3.6-flash (thinking forced off, temperature pinned to 0) for the high-frequency extractor and scorer roles — routed through two separate API keys so background load never competes with conversational latency. The whole thing runs behind a Streamlit UI with password auth and per-user isolation, deployed on Alibaba Cloud ECS.

Challenges we ran into

  • A silent ivfflat.probes bug that ate our own recall accuracy. The vector index was sized for a much bigger table (lists = 100), but Postgres defaults probes to 1 — meaning every nearest-neighbor query, in recall and in the contradiction gate, was silently searching roughly 1% of the data. We only caught it by running the identical query at different probe values and watching the result count jump from 1 row to 5. Fixed with SET ivfflat.probes = 10 (√lists) on every connection — but it's the kind of bug that doesn't throw an error, it just quietly makes the system worse at its one job.
  • Our own demo panel contaminated production data. An early "fake injection" demo button ran the real write path on a background thread, and during testing someone promoted a fabricated poisoning string straight into active memory — precisely the failure VIGIL exists to prevent. We rebuilt it as a genuinely side-effect-free sandbox, verified by checking row counts were byte-identical before and after running it, so it's structurally impossible to repeat.
  • Flaky tests that turned out to be three separate, stacked bugs, not one flaky test: no HTTP retry logic (a single transient 429 was fatal), test fixtures that never cleaned up after themselves and crowded out the current run's rows from top-k neighbor search, and the ivfflat.probes bug above compounding both.
  • The infra shifted under us mid-build. The original design assumed a local quantized model server with real KV-cache slot eviction tied directly to decay scoring. Moving to Qwen Cloud meant "erase the KV slot" wasn't a real operation anymore, and we had to consciously redefine "eviction" as a DB-level status change instead of chasing a local-inference concept that no longer applied.
  • Prompt caching had an undocumented minimum. We assumed cache_control: ephemeral would just work once wired up; empirically bisecting it against the live endpoint showed a hard cliff around ~1024 tokens — anything shorter got zero caching benefit, which is why the pinned profile ended up as dense as it is rather than a short blurb.

What we learned

Researching how to turn a static, provenance-based trust score into something that adapts at read time sent us through a surprising amount of prior art — MemGuard, RA-RAG, Beta reputation systems, Dempster-Shafer combination, Knowledge Vault, CaMeL, and PageRank/EigenTrust-style propagation — before landing on the lowest-effort fit: reusing signals the system already computed for other purposes (corroboration from the relational-links graph, staleness from the decay scorer) rather than building a new trust-propagation subsystem from scratch.

The bigger lesson underneath that: the hardest bugs in a system like this aren't in the LLM calls — they're in the boring infrastructure underneath them. A default index parameter, a missing retry, a test fixture that doesn't clean up — because those fail silently and erode exactly the guarantees (accurate recall, isolated poisoning, deterministic resolution) the whole project claims to provide.

Built With

Share this project:

Updates