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_linkstable, 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: ephemeralfor 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.probesbug that ate our own recall accuracy. The vector index was sized for a much bigger table (lists = 100), but Postgres defaultsprobesto 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 withSET 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.probesbug 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: ephemeralwould 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.
Log in or sign up for Devpost to join the conversation.