📖 Prerequisite: Read the Quick Start Guide first for system overview.
This document explains how Chronicle stores and retrieves memories.
Chronicle has one memory provider: chronicle. It is an agentic Markdown vault — a directory of Obsidian-style notes that is the single source of truth for memories. There is no separate vector database, no embeddings, and no hybrid search index. Memories are plain Markdown files; writing and reading each have an independently selectable agent backend.
Code References:
- Provider:
src/advanced_omi_backend/services/memory/providers/chronicle.py - Memory agents:
src/advanced_omi_backend/services/memory/agent/(write agent + read/retrieval agent) - Memory extraction job: runs in the post-conversation RQ chain (
memory_extraction_job), callsmemory_service.add_memory() - Configuration:
config/config.yml(memory + LLM sections) +src/model_registry.py
Conversation transcript
│
▼ memory_extraction_job → memory_service.add_memory()
┌─────────────────────────────┐
│ Write agent (_add_memory_ │ direct / Codex / Pi
│ agent) │ • record conversation note
│ │ • surgically edit People/
│ │ Topics/Category notes
└──────────────┬──────────────┘
▼
data/conversation_docs/<user_id>/ ← the vault (source of truth)
Conversations/<id>.md
People/<name>.md
Topics/<topic>.md
<Category>/<name>.md
▲
│ ripgrep (grep / glob / read_note tools)
┌──────────────┴──────────────┐
│ Read agent (_search_vault_ │ direct / Pi
│ grep) │ • greps the vault
│ │ • reads relevant notes
│ │ • synthesizes an answer
└─────────────────────────────┘
▲
/api/memories/search and chat `search_memories` tool
The vault lives on disk at:
data/conversation_docs/<user_id>/
It is per-user (keyed by the MongoDB ObjectId user_id) and organized into note types:
| Note type | Path | Contents |
|---|---|---|
| Conversations | Conversations/<conversation_id>.md |
One note per conversation — the record of what was discussed. |
| People | People/<name>.md |
A durable semantic profile for a person when captured evidence establishes reusable facts about them. |
| Topics | Topics/<topic>.md |
A note per recurring topic. |
| Categories | <Category>/<name>.md |
Other category notes (e.g. places, projects, preferences). |
These are ordinary Markdown files — readable, editable, and grep-able. Because the vault is the system of record, memories survive as durable text rather than as opaque vector rows.
People/ is deliberately not the enrolled-speaker roster or a count of successful
voice identifications. The speaker service owns enrollment, and active transcript
segments retain every recognized name whether or not a person note exists. Timeline
episodes may carry those names as entities. A memory write creates a person note only
when it has durable person-specific knowledge to put in the note; a routine appearance
or a recognized name alone remains a transcript/timeline fact instead of producing a
thin placeholder profile.
If an Immich photo library is configured (IMMICH_URL/IMMICH_API_KEY, offered by the setup wizard), the person_photos cron job (services/person_photos.py) matches each People/<name>.md note against Immich's people API, stores the person's face-crop thumbnail content-addressed under the vault's _media/ directory, and embeds a small photo at the top of the note.
The write and search paths are selected independently under memory.agents:
| Backend | Write | Search | Model/auth source |
|---|---|---|---|
direct |
Yes | Yes | Built-in tool-calling loop using the model resolved by llm_operations.memory_write or memory_search. |
codex |
Yes | No | Codex CLI and ChatGPT subscription auth from the CODEX_HOME mount. |
pi |
Yes | Yes | Pi CLI using a Chronicle model-registry entry; local llama.cpp, Ollama, and remote OpenAI-compatible models all use the same path. |
Writes also declare recovery_backend. It defaults to direct, so a failed Codex or
Pi run gets one direct-agent recovery attempt. When direct is already the primary,
the recovery attempt uses defaults.fallback_llm. Set it to null to disable agent
recovery. The setup wizard preserves an explicitly configured value on reruns rather
than silently resetting it to direct.
Codex subscription authentication is also a readiness requirement when Codex is the
configured primary writer. A recovery backend handles a write attempt that fails after
the service is ready; it does not make an unauthenticated Codex primary ready. Run
codex login in the host CODEX_HOME before starting that configuration.
Pi is installed in the backend image and runs non-interactively with isolated runtime
configuration. Chronicle resolves memory.backends.pi.model through its model
registry, including the upstream model ID, URL, and API key. No host-side Pi login,
~/.pi directory, auth volume, or hand-written models.json is required. The shipped
default is Pi 0.83.0 on Node 22.19.0.
The Pi process is not given Pi's built-in shell or filesystem tools. Chronicle disables
them and loads a generated extension containing only the canonical vault tool schemas;
calls cross a short-lived, bearer-authenticated loopback gateway into VaultTools.
The search extension receives only the read-only search schemas.
Write loops are bounded at 48 model/tool rounds. Pi additionally enforces an atomic
192-call write cap at the gateway. Search is bounded at 6 tool rounds and 24 calls.
When that tool budget is exhausted, the direct backend gets exactly one completion with
no tool schemas so it can synthesize from evidence already in its conversation. Pi gets
one fresh, isolated no-tool process only when it has already read note evidence; that
process receives the selected evidence but no Chronicle extension or Pi built-in tools.
Neither final-synthesis path can perform another vault operation. A
truncated, stalled, timed-out, or process-failed write retains every audited partial
mutation but is not reported as complete: Chronicle invokes the configured recovery
backend even when the partial run already produced a valid conversation note. New
People and Topic notes are also checked at the tool boundary for every canonical
section and aggregation embed, so a smaller local model gets a recoverable tool error
instead of silently leaving a malformed long-lived note. A second deterministic guard
rejects a new Topic when at least two and three quarters of its substantive About
bullets are already contained by one peer Topic; the same check is repeated after native
filesystem agents finish.
Memory extraction runs as part of the post-conversation RQ pipeline. After a conversation closes, memory_extraction_job calls memory_service.add_memory(), which invokes the write agent (_add_memory_agent in providers/chronicle.py).
Given the conversation transcript and metadata, the selected write backend:
- Records the conversation as a new
Conversations/<conversation_id>.mdnote. - Surgically edits existing People / Topics / Category notes — adding or updating facts in place rather than blindly appending — and creates a new semantic note only when the evidence establishes reusable, durable knowledge for it.
This is LLM-driven extraction: the agent decides what is worth remembering and where it belongs in the vault.
Continuous ScreenPipe audio does not take this path. Capture is profiled in windows capped at two hours; detected Conversation claims prefer quiet seams near 30 minutes but may stay longer while speech is continuous. Remembering per claim would inherit those operational bounds, while a Timeline episode already carries the semantic bounds.
add_day_memory therefore records one settled local day of episodes in a single
write, anchored on Daily/<local_date>.md rather than under Conversations/, which
stays one note per conversation. Chronicle first writes a concise, deterministic
episode index (range, kind, salience and title); the agent then considers only durable
People/Topic/Category facts. It shares the conversation path's executor selection,
recovery backend, bounded rounds, Langfuse spans, and audit ledger
(MemoryCause.DAY_EPISODES).
Person notes deliberately separate durable identity from provenance. ## About holds
stable/current facts such as relationship, work and enduring preferences; it is not a
dated activity log. Conversation-scoped writes may use ## Mentions as a compact source
pointer, but settled-day writes cannot modify that section at all: Daily/Timeline owns
chronology. The same proposition therefore cannot be copied into both sections by a day
run. Topic/category ## About sections likewise describe the recurring thing rather
than repeating each day's episode summary.
The vault owner's own Person note is specifically not a second Daily log: speaking,
building, or testing something on a day does not earn a dated self-mention. The owner
note changes only when the day establishes a durable personal fact, placed in About.
Other people's mentions remain sparse relationship/provenance pointers rather than
episode synopses on conversation writes; day writes put a materially clarified durable
relationship or role in About and leave Mentions unchanged. One-off implementation
phrases and events do not mint Topic notes unless they establish durable state that is
likely to matter across days. A fact belongs to one canonical Topic; a narrower note
cannot substantially repeat the About scope of a broader peer.
The day digest contains only bounded episode summaries, entities, attributes, and
role/confidence assertions. Raw transcripts and deterministic Episodes/*.md artifacts
remain outside the vault. See Memory segmentation and storage
for the complete evidence-to-vault contract.
The episode index has a deterministic source-preserving representation and does not depend on the model. Semantic People/Topic/Category extraction still has no safe fallback: a day whose agent does not deliberately finish stays unwritten and is retried with its diagnostic rather than pretending the missing judgement succeeded.
A ScreenPipe recording that the timeline agent judged conversational — a standup, a 1:1 — is separately promoted back into the Recordings list and search. See Semantic timeline episodes.
A completed write always passes deterministic gates before the run is accepted. When
memory.agents.write.review is enabled, a separate semantic reviewer also checks what
was added; the two checks address different failure classes.
Structure is decided by a function. vault_verify.verify_vault_changes diffs the
vault against a pre-run snapshot and reports illegal paths, a note missing its canonical
sections or aggregation embed, a newly duplicated ## Section, a case-only collision, a
day write that minted a Conversations/ note, and a captured-content note at the vault
root (where only complete category hubs belong). It also treats People Mentions as an
immutable section for day runs and rejects newly-created Topics whose factual scope is
mostly already carried by another Topic. The Daily episode index is separately
verified against the active Timeline digest and restored mechanically if the model
touches it. Each Finding carries a fix instruction addressed to a model. The same
function is offered to the agent as the verify_vault tool so it can self-correct
in-run, and re-run server-side so correctness does not depend on it choosing to.
Deterministic findings that survive the bounded repair pass fail the day and leave it
retryable; they are never merely logged and latched as written. The unattended rebuild
finisher also scans the complete regenerated vault as one final structural gate.
Redundancy cannot be. Structural verification passes on a perfectly well-formed
bullet that re-records something the vault already holds — which is exactly how a
DeepSeek V4 Pro day write finished with Vault verification passed after restating the
phone stand, the chai, and the air-fryer fries that People/alex.md and
People/blair.md already carried. Deciding that means reading the surrounding notes
and judging whether two differently worded sentences carry the same fact, so a second
agent does it (agent/review_agent.py):
- read-only —
grep/glob/read_noteand areport_findingstool, so a review cannot mutate the vault it judges; - fresh context — it sees the source and the lines actually added, never the writer's reasoning, so it cannot inherit the writer's conviction that the work was done;
- narrow — only
redundant(a note of the same kind already records this) andunsupported(the source does not say this). Off-vocabulary verdicts are dropped;Daily,People, andTopicsmay cover the same evidence at different semantic levels, but a People/Topic bullet that merely rephrases the Daily activity log is redundancy; - never judging what it cannot see — the source is bounded at the day digest's own
budget, and if it still had to be cut,
unsupportedis withdrawn for that run. A reviewer shown part of a source cannot tell "the source never said this" from "the source said it in the part you were not given", and left to judge anyway it picks the former in confident detail: cutting a 39,563-char digest at 24,000 hid a gaming session, and a true bullet about it was flagged as invented; - advisory — its findings are the same
Findingtype and flow into the same bounded repair pass. A reviewer that fails, stalls, or returns nothing parseable yields no findings, because a broken reviewer must never block a good write.
It ends with a forced verdict: when the round or tool-call budget runs out, the search tools are withdrawn and the model is asked to report from what it has already read. Measured on the live vault, that step is what makes the reviewer usable at all — the first runs exhausted six rounds with the right answer already written in their own prose ("no mention of Tokyo … this is unsupported") and returned nothing, because they never got a round in which to report. With nothing left to call but the verdict, there is no next search to narrate.
Measured on the live vault with scripts/probe_write_review.py, which injects bullets
whose verdict is known into a copy of the real notes: 28/32 over two days and eight
trials on Qwen 3.6 27B. Genuinely-new bullets were left alone 8/8 and invented ones
caught 8/8. All four misses are the same borderline bullet, where the reviewer finds the
overlap and then rules it a new detail about an already-recorded event — the exception
its own instructions grant — rather than a duplicate.
The reviewer is not an ontology gate. An exact replay of the bad 2026-06-17 write read
both Agent Control and Policy Store, spent 41,887 tokens over five rounds/eight tool
calls, and returned no finding even though all four Policy Store facts were contained by
Agent Control. That failure is why Topic-scope containment is enforced deterministically
at the write boundary instead of paying the reviewer and hoping it notices.
Disable per deployment with memory.agents.write.review: false; it costs one extra
agent run per write that changed anything (~38s against a ~190s day write here).
Search is served by the read agent (_search_vault_grep). Both the direct and Pi
search backends are read-only and operate over the vault with four tools:
grep— full-text ripgrep across the notesglob— find notes by path/name patternread_note— read a specific note's contentssearch_images— rank saved images by what they look like, returningManual Memories/note paths forread_note. Backed by the optional ColPali service; when that is unreachable it returns a plain sentence saying so rather than raising, and the images remain findable by grep over their descriptions.
Given a query, the agent greps the vault, reads the relevant notes, and synthesizes an answer. The result returned to the caller is:
- the synthesized answer as the top result, plus
- the notes it read (cited note paths) as supporting context.
There is no vector similarity score — relevance comes from the agent's reasoning over the text it retrieves.
Chat is always agentic / tool-calling. The chat LLM is given a search_memories tool; when it needs context about the user it calls that tool, which runs the same agentic vault search and returns the synthesized answer plus the cited note paths. The chat model then incorporates that into its reply.
When Chronicle's existing LANGFUSE_HOST, LANGFUSE_PUBLIC_KEY, and
LANGFUSE_SECRET_KEY variables are configured, memory work is exported to the same
local Langfuse OTLP endpoint as the rest of the pipeline. A write trace shows primary
and recovery attempts, the selected executor, model calls, canonical vault-tool calls,
deterministic fallback, latency, token usage, and completion state. A search trace shows
the executor, rounds, tool calls, notes-read count, cap recovery, warnings, usage, and
whether the final answer was usable. Pi's Node subprocess emits equivalent manual model
usage spans, so it is visible beside Direct and Codex rather than becoming a telemetry
blind spot. Langfuse OTLP export is batched, keeping network export off the vault-tool
mutation path even when an agent emits many tool observations. Chronicle explicitly
flushes the completed trace tree at the common async_job boundary before a forked RQ
work-horse exits. The flush belongs to the job wrapper rather than an individual
memory decorator because the final model/agent spans end only moments before RQ calls
os._exit(); relying on the batch exporter's timer would selectively lose those late
spans while retaining earlier Timeline calls from the same job.
Chronicle's manual memory spans are metadata-only by default: they retain lengths and
SHA-256 fingerprints but omit transcripts, queries, answers, note paths/bodies, tool
arguments, and raw provider errors. Set LANGFUSE_MEMORY_CAPTURE_CONTENT=true only
when Langfuse is trusted and local and that personal content is useful for a bounded
debugging session. Content-bearing fields are length-limited; API keys, model endpoints,
and Pi gateway tokens are never emitted by the memory tracer.
The Direct executor also has native child spans from Chronicle's global OpenInference
OpenAI instrumentation. Those spans have independent privacy controls and include model
inputs/outputs by default. Set both OPENINFERENCE_HIDE_INPUTS=true and
OPENINFERENCE_HIDE_OUTPUTS=true to redact them; because instrumentation is global,
those settings apply to every OpenAI-client call in Chronicle, not only memory. Pi and
Codex subprocess spans use the memory-specific content toggle above.
Pi's learned operating guidance is private per-user state under
data/pi_operating_memory/<user_id>/, outside the semantic Obsidian vault. Production
loads only the active AGENTS.md snapshot at the start of a run; Pi still chooses which
vault files to search, read, and edit. Generated guidance is not fixed file routing.
Both the writer and read-only retriever load the same stable snapshot. Their completed
pi_memory and pi_memory_search inference artifacts feed bounded, content-free outlines
to the optimizer. It runs after 25 new traces and has a daily backstop. In the default
shadow mode it may write one AGENTS.md proposal or one inert skill/script candidate per
run, but cannot change active guidance or production code. AGENTS.md proposals require distinct
development and holdout evaluation artifacts, review, and promotion. Promotion refuses
a candidate when active guidance changed since that candidate was generated. Every
activation records the prior text, and a later rollback creates another revision instead
of deleting history. Skill
and script candidates remain inspectable but non-executable.
GET /api/memories/search?query={query}&limit={limit}— runs the agentic vault search and returns the synthesized answer plus the notes the read agent consulted.GET /api/memories/people/suggestions— ranks conservative deterministic duplicate-person candidates for review; it never merges automatically.POST /api/memories/people/identity— records or clears a symmetricdistinct_fromdecision in two People notes, with optional stale-revision protection.POST /api/memories/people/merge/previewandPOST /api/memories/people/merge— preview and apply a locked deterministic merge. Adistinct_fromdecision blocks preview.GET /api/memories/operating-memory— returns active Pi guidance, content-free candidate metadata, revision metadata, and optimizer progress for the current user.GET /api/memories/operating-memory/candidates/{id}— reads one bounded candidate for human inspection.POST /api/memories/operating-memory/candidates/{id}/reviewand/promote— record an evidence-backed decision, then explicitly activate an approved, non-stale AGENTS.md candidate.POST /api/memories/operating-memory/revisions/{id}/rollback— restores the state preceding a selected revision while retaining rollback history.- Other
/api/memories/*management endpoints operate over the vault notes.
The vault is designed to be edited and viewed directly. The optional vault sync feature (in the cross-platform desktop tray, extras/chronicle-tray/) syncs data/conversation_docs/ to an Obsidian vault via Syncthing, so you can browse and hand-edit your memory notes in Obsidian. Human edits made in Obsidian sync back into the vault. This sync is independent of the memory provider itself — the vault on the backend remains the source of truth.
The optional Chronicle Companion plugin adds explicit, deterministic maintenance actions such as merging duplicate people. The UI previews and confirms the action, while the backend performs the locked mutation; no LLM participates in execution.
For historical context, the previous architecture used FalkorDB hybrid search (vector + BM25 + entity-graph BFS over ConvDoc/ConvChunk/ConvEntity nodes and a knowledge graph), plus alternative providers (OpenMemory MCP, Graphiti) and Qdrant/Mem0 vector storage. All of these have been removed. There is now a single chronicle provider backed entirely by the Markdown vault; the falkordb container and FALKORDB_* environment variables no longer exist.
The setup wizard asks for write and search backends separately. This nested structure is the only supported configuration shape:
memory:
provider: chronicle
timeout_seconds: 1200
agents:
write:
backend: pi
recovery_backend: direct
review: true # read-only review agent over what the write added
search:
backend: pi
backends:
direct: {}
codex:
model: gpt-5.6-terra
reasoning_effort: low
sandbox_mode: workspace-write
timeout_seconds: 900
max_used_percent: 80
limit_id: ""
pi:
model: muse-glimmer-llm # Chronicle model-registry entry, not upstream model ID
timeout_seconds: 900
context_window: 131072
max_tokens: 4096 # capped generally; leaves most context for prompts/tools
thinking: high
llm_operations:
memory_write:
reasoning_effort: high
max_tokens: 8000
memory_search:
reasoning_effort: high
max_tokens: 8000The Pi model may be any OpenAI-compatible LLM entry in the effective registry formed by
config/defaults.yml plus name-based overrides from config/config.yml. The wizard
rejects missing entries, embeddings, and non-OpenAI API families. For the local Muse
Glimmer service, setup selects muse-glimmer-llm, records llama.cpp's exact upstream
Hugging Face identity, and records the context actually selected for that service. API credentials,
when a selected registry model needs them, continue to come from the model definition's
environment-variable reference.
No vector store, embedding model, or graph database is part of memory storage or
retrieval.
Pi limits are derived per selected model rather than pinning every backend to one
machine's context profile. The wizard uses a declared context_window (including the
context written by local llama.cpp setup), otherwise a conservative 32K fallback. New
output limits are one quarter of the context up to 4096 tokens, leaving most of the
window for Pi's system prompt, tool schemas, transcript, and multi-round results.
Existing explicit Pi limits are preserved on rerun.
The built-in Qwen 3.8 27B profile declares a 98,304-token context and uses llama.cpp's OpenAI-compatible route and Qwen chat-template thinking control. Chronicle's Pi memory workload is text-only even though the registry model can also serve vision. Keep the 4,096-token output cap so the system prompt, transcript, tool schemas, and multi-round results retain headroom. Thinking is a per-memory-agent setting, not an assumption made from the registry model's general capabilities. Increase output limits or enable thinking only after the isolated vault benchmark passes; a model fitting in GPU memory does not show that its agent loop completes valid writes.