Write modes, capture & recall
This page covers the two halves of the memory loop: how the host writes each completed turn (memory.writeMode) and how the agent reads memory through targeted MemoryRecall search or bounded local MemoryJournal chronology. Both are driven by the single config.memory block — there is no separate .mcp.json entry to hand-wire.
For tier selection (lite / journal / bujo) and embeddings setup, start at the Memory overview and Embeddings. Recall is the read path; scheduled consolidation is the maintenance path covered in Consolidation.
Write modes (memory.writeMode)
Section titled “Write modes (memory.writeMode)”memory.writeMode controls how the host runtime persists each completed turn. It is independent of the tier’s recall capability. Coverage: config .
| Mode | What it does | Backend / tiers | LLM |
|---|---|---|---|
disabled | Never persist turns. Recall still works over existing memory state. | Lite/Journal/BuJo | no |
append-host-summary | Admit one deterministic host observation by provider run id, then fsync and project it. | Lite/Journal/BuJo | no |
capture | Admit the host summary plus full approved capture text by provider run id, then curate it in the background. | BuJo | configured chat model |
The host deliberately skips memory writes for one low-signal successful turn, in every write mode: final answers that are NOTHING_TO_REPORT (the cron/webhook no-op sentinel), or that end with it on their final line. There is no host word list for probes or filler such as test / ping; those turns reach capture, whose extraction may return an empty plan.
Memory is not a task list. Automatic capture never writes an open BuJo task ([ ]): a line the extractor types as a task is stored as a dated history note, and it gains no host-derived coarse fact. Plans, requests and scheduled work live in the agent’s task tools; memory keeps only their durable history. Capture never rewrites an existing task line in place either: when reconciliation would update one, the task stays as written and the new information becomes its own note, always threaded to that task. A supersede is the intended exception: the old task is marked invalidated and its replacement is a note, because memory is not a task tracker and a superseded task becomes history. A capture plan retained before this rule is recovered with the same task-to-note mapping. Explicit task lines written by an operator are unchanged.
Cron and webhook turns omit their trigger prompt and webhook pre-instructions from memory. Intelligent capture receives an honest scheduled-task or webhook source label (not User) plus the assistant answer; the deterministic host summary remains assistant-answer-only. The extractor must not treat an assistant recap as a first-party report. There is no deterministic recap classifier, so model-guided capture may still make mistakes; the no-op sentinel remains the deterministic skip.
Memory persistence is host-owned. When a user says “remember this,” an allowed Remember tool can write a deliberate fact immediately; otherwise the configured write mode determines completed-turn persistence. In BuJo, Remember({ text, about?, supersedes?, replaceable? }) optionally links an existing person entity (about) and replaces a recalled current ordinary note (supersedes), leaving the old state in history. It cannot create a person or replace owner-stated facts, preferences, lessons, or text-only Remember lines; supersession is limited to captured notes/events and enhanced Remember writes. Source, author, observation date and uncertainty belong in the self-contained sentence; enhanced writes carry assistant-noted, not verified owner, attribution. Lite and Journal accept text-only writes. For scan observations without a known person ID, use replaceable: true on the first write so a later change can supersede it; plain text-only Remember writes are protected from scan replacement. Exact repeats are idempotent; recall and compare before writing paraphrases. If a later plain Remember repeats the exact text of an existing replaceable write, it reports a duplicate and does not remove its replaceable marker; use distinct dated/source-specific wording when a protected owner-stated fact must be recorded. Do not use shell, filesystem, or database tools to edit .mono-agent/memory, canonical Markdown, SQLite rows, manifests, generations, or indexes directly. Operators should stop the agent and use the mono-agent memory ... maintenance commands when they need to rebuild, migrate, audit, or repair memory state. memory export is the one exception: it is strictly read-only and can back up a live agent. memory import apply and import restore do require a stopped agent.
{ "memory": { "mode": "journal", "writeMode": "append-host-summary", "path": "./.mono-agent/memory" }}{ "memory": { "writeMode": "append-host-summary" } }Durable completed-turn admission
Section titled “Durable completed-turn admission”The built-in store implements the harness’s strong completed-turn write. Before a successful
turn reaches terminal reporting, the harness awaits an owner-only, fsynced record under
memory.path/.capture-intake/pending/. The filename is the SHA-256 hash of the stable provider
run id; retrying the same run and payload is a successful duplicate, while reusing that run id
with different bytes fails closed. An admission failure does not replace the provider’s answer,
but it emits an explicit memory-degradation warning instead of pretending the turn was saved.
Only a turn that reaches the successful commit boundary qualifies. A cancelled
or failed non-isolated turn may publish a bounded continuity account into
canonical conversation history, but neither account nor its partial
assistant/tool content is admitted to append-host-summary or capture.
Hard-interrupted, isolated, and never-started turns remain excluded as well.
The admitted record is the restart boundary. It contains the bounded deterministic summary and,
for writeMode: "capture", the bounded host-approved capture text. Projection and BuJo curation
may run after the reply, but a process restart resumes them from the durable record. Pending work
uses 16 bounded exponential-backoff attempts (one minute initially, capped at six hours, spanning
more than 24 hours) before moving to a durable dead letter. Resolved receipts are retained in a
bounded rich set, while an exact content-free id + payload-hash commitment remains permanently
in one of 256 cataloged compact append-only ledger shards. A sibling owner-only integrity catalog
commits every shard’s byte high-water mark and SHA-256 after the shard append is fsynced. Missing,
truncated, or valid-looking replacement shards therefore fail closed. A crash between shard append
and catalog commit can advance the catalog only when every suffix entry still has an exact
materialized intake receipt and after the exact shard inode is fsynced again. A separate
owner-only schema marker at the memory root proves that the catalog has existed, so deleting both
the ledger and catalog cannot masquerade as a pre-ledger upgrade. Receipt pruning therefore cannot make an old run
admissible again or create another raw-audit line/curated fact. The intake directories are 0700
and records/ledger shards are 0600; symlinks, ownership changes, conflicting payloads, malformed
records, partial ledger writes, and unsafe crash transitions are rejected or deterministically
recovered before admission.
A safe rebuild may change the BuJo embedding provider or dimension while a run-owned semantic plan is retained: the candidate is rebuilt from canonical source with the new embedding identity, and the exact plan remains available until intake resolution. Rebuilding that retained plan into Lite or Journal is refused before source mutation, because those tiers cannot preserve the same BuJo run-derived ids and provider-bound replay contract. Start the current BuJo configuration to finish the durable intake before changing tiers.
External writable MemoryStore implementations must implement persistCompletedTurn
and own stable runId admission and deduplication semantics. Read-only stores may
omit it; the harness rejects enabled writing when the method is absent. Mono-agent
does not infer, provision, or configure an external service. The optional
captureSpeakerKind on completed turns is host-verified outer-turn provenance,
not a label extracted from text or metadata.source: human-turn, trigger, or
unknown (the default for legacy and direct callers). An external store must
preserve the distinction without silently upgrading unknown/trigger assertions
into first-party evidence. Owner-authorized operator turns, including keyless
loopback-only clients, are treated as the human owner; automation using that
interface is consequently attributed as the owner. A keyless non-loopback
operator endpoint is not trusted for human attribution. Channel adapters must
verify the sender before setting human-turn; a copied payload field is not
verification. This provenance contract does not itself produce typed graph facts.
Direct integrations
Section titled “Direct integrations”The harness, direct embedders, and offline calibration use persistCompletedTurn.
Capture mode reaches strict extraction after durable, run-idempotent admission.
The legacy capture queue, direct capture() method, and loose capture exports have
been removed. See the migration guide.
Strict tier write behavior
Section titled “Strict tier write behavior”- Lite: projects the admitted normalized host observation to
daily/YYYY-MM-DD.mdand indexes it for FTS. It never embeds and never calls a chat model. - Journal: projects the admitted observation with a case-preserving, NFKC/whitespace-normalized SHA-256 identity, then makes it available to FTS. Semantic indexing is queued in batches of up to 32, so Ollama/LM Studio/OpenAI embedding latency is not on the provider-success critical path. Repeated content converges on one markdown/index identity.
- BuJo: projects each admitted compact host observation to
audit/YYYY-MM-DD.md, outside curated recall. OnlywriteMode: "capture"asks the memory model to promote durable facts into canonicaldaily/notes and the graph. A model outage therefore cannot turn an uncurated raw transcript into recalled fact.
Exact BuJo replay projection
Section titled “Exact BuJo replay projection”BuJo capture can also create replay-owned state that canonical daily markdown
and graph.jsonl cannot reconstruct by themselves: thread edges, supersession
lifecycle/edges, and terminal timestamps from an explicit migration forget.
The owner-only memory.path/.replay-projection-v1.json is the exact canonical
authority for that state. It contains only metadata—memory ids, timestamps,
thread weights, authority kinds, and content-free SHA-256 authority digests;
it contains no memory text or model output.
Capture and migration publish their exact projection delta while the durable
capture intent or migration marker still exists, prove that SQLite matches it,
and only then retire that durable authority. A crash at any boundary therefore
leaves enough state for an idempotent replay. Strict health compares the full
sidecar and SQLite projection exactly; structurally plausible lifecycle or
edges written directly to SQLite are canonical_mismatch, not trusted history.
The sidecar itself is strict canonical JSON, an owner-owned single-link regular
file with mode 0600, and is bounded to 32 MiB / 131,072 entries.
Lite and Journal do not consume the BuJo sidecar and reject replay-owned
lifecycle or edges in their SQLite index. BuJo also never infers a nonempty
projection from SQLite: if a legacy managed generation or unmanaged memory.db
has replay state but no sidecar, keep the agent stopped and use the explicit
metadata-only mono-agent memory adopt-replay --json trust-on-first-use flow
before rebuild. Adoption can safely attest multiple disjoint capture
intents/receipts and at most one migration marker. Mutable pending capture work
and a pending migration are mutually exclusive; immutable completed capture
receipts may coexist with the later migration, and retained receipts remain
until intake resolves.
Rebuild must immediately follow adoption and completes any attested protocol
without repeating provider work. Building the replacement semantic generation
still uses the configured embeddings provider before activation. Never start
the service between those steps.
See Validation & CLI.
The durable intake and both downstream paths are bounded and observable. Intake admits at most
4,096 active pending/dead records of at most 640 KiB each and retains at most 4,096 resolved
receipts. Its permanent content-free ledger grows by one fixed 129-byte entry per distinct run id
across up to 256 lazily created shard files plus one fixed-size 256-slot integrity catalog. Journal
indexing holds at most 256 items / 2 MiB. Each capture-model completion is
rejected before parsing when it exceeds 262,144 JavaScript characters. Runtime snapshots report
content-free intake pending/dead/resolved/due/transition counts alongside downstream queue and
shutdown state. The store republishes that snapshot immediately after admission and every durable
intake transition, so a strict health audit sees newly accepted work as in_progress without
waiting for the periodic heartbeat. Notification failures never roll back or misreport the durable
transition; the on-disk intake remains authoritative. Shutdown gives work up to 10 seconds to
drain; after that deadline it aborts the cooperative active attempt and returns while the intake
record remains pending for restart.
Operators can inspect that intake without reading its summaries or capture text:
mono-agent memory inspect --jsonmono-agent memory inspect <64-character-id> --jsonInspection returns only ids, states, timestamps, attempts/revisions, due flags, bounded failure
categories, and aggregate counts. With the matching agent stopped, memory retry [<id>] makes
dead/delayed work due for processing after restart. memory resolve <id> <reason-slug> is the
explicit loss-accepting path: it records operator_resolved without claiming capture succeeded,
preserves permanent duplicate protection, and refuses recoverable retained semantic plans. See
Validation & CLI for
the liveness fence, exact inputs, and no-op semantics.
capture — per-turn intelligent capture (bujo)
Section titled “capture — per-turn intelligent capture (bujo)”capture fsyncs the completed turn into durable intake, then projects its compact raw audit and
runs curation in the background, except for the low-signal skipped turns described above. The plan
uses exactly one chat-LLM call to extract up to eight atomic memories plus their precise
entities/relations, then at most one small review call over the admitted lines (below), then at
most one additional batched call to classify close existing candidates as ADD / UPDATE /
SUPERSEDE / NOOP: at most three calls per turn. Turns without review-eligible lines skip the
review, and clearly novel candidates skip the reconcile call. Entity extraction is part of the
first call, not a separate pass.
Key properties:
- Local admission before terminal status. The provider call is already complete; terminal reporting waits only for the bounded filesystem admission, never for embeddings or the chat model.
- Restartable background work. Raw-audit projection and curation resume from pending intake after restart or provider recovery.
- Serialized per store. Captures do not race each other against the same memory root.
- Bounded shutdown without admitted-work loss. A normal stop drains accepted work with a 10-second safety deadline. If a provider ignores cancellation, stop still returns and the durable pending record resumes on restart.
- Strict model contracts. Extraction and reconciliation accept one exact, bounded JSON value with complete arrays/decisions and no duplicate keys, unknown fields, partial filtering, unsafe text, or ambiguous target collisions. Reconciliation spells out the exact per-action shape:
ADDhas index/action only;NOOPrequires a supplied target id;UPDATEandSUPERSEDErequire that target plus complete replacement text. Invalid output retries and never counts as successful capture. - Reconcile is intelligent, not append-only: the pipeline classifies each observation as
ADD/UPDATE/SUPERSEDE/NOOPagainst existing memories to avoid duplication. - The user’s own word wins. When a
user-sourced candidate contradicts an existing line, the classifier supersedes that line with the user’s statement rather than updating it or adding beside it. This includes a recorded assistant inference, estimate or recommendation whose claim or premise the user corrects (the assistant recommended resuming a game because it was unfinished; the user says they finished it). Other details about the same subject stay separate lines. This is a model rule, in any language. A preference line is replaced only when it is the owner’s own (scopeagent, or the owner turn’s ownuser:sender scope), on a host-verified owner turn, by a memory the model sources to the user; the old line stays as superseded history. A peer’s or project preference is never replaced this way. Otherwise a preference or lesson line is never superseded, and the change is added beside it. - Duplicates are checked structurally. Two memories with the same words in the same order are a duplicate; there is no word-shape near-duplicate rule. Whether differently worded lines say the same thing is the model’s judgement, at extraction and against the store at reconciliation.
- Crash-idempotent semantic commit. Run-derived fact ids, a retained semantic plan, and the exact replay projection make a post-commit/pre-receipt replay converge without another model call, duplicate fact, or unattested lifecycle/edge.
- The assistant’s own low-salience lines are not kept. The extraction model marks each memory’s
sourceand scores its salience. A memory whose host-bounded source isassistant(including auserclaim on a turn without human user text) needs a salience of at least0.5; below that it is dropped before reconciliation, together with any entity or relation only it named. This removes the assistant’s progress and status reports and generic advice without word lists, in any language. User, tool and document lines, and retained plans without asource, have no floor. - One constrained review pass after extraction. When a turn has assistant-sourced lines, or owner user lines without a preference label, one small model call reviews only those admitted lines. It may add a preference label to a line that records the owner’s own stated like, dislike or taste; the label goes through the same host gates and sits beside the coarse owner fact. It may drop an assistant line that is general information about the world unrelated to the user, or a transient report about the assistant’s own process or tooling; user-specific findings and consequential outcomes stay, and it keeps when uncertain. It never changes text, source, entities or salience and never drops user, tool or document lines. Entities and relations named only by dropped lines are pruned. The review runs before
capture.onlyfiltering, and its decisions are retained with the capture plan, so a retry never reviews again. Turns with no eligible line make no call. The review must return one allowed decision for every listed line; a missing, extra or disallowed decision (or a model failure) rejects the whole review and the capture retries. On the final durable attempt the unreviewed plan is kept, so an uncertain or failed review always means keep. - Associations are precise. Each curated fact carries only the entity IDs explicitly extracted for that fact; the implementation never creates a turn-wide memory/entity Cartesian product.
- Time is observation-grounded. Capture asks the model to resolve unambiguous relative calendar dates against the immutable host-owned UTC admission instant, retain broad intervals when precision is unavailable, and express an age snapshot as historical (
was 14.5 months old as of 2026-09-08), not a permanent current age. When the anchor is absent it must not fabricate one. Reconciliation converts an UPDATE whose text carries an ISO date into a dated supersession, rather than rewriting an older daily bullet, when its target is undated or carries an earlier date; other time sensitivity is the classifier’s observation-date rule, with no age or “currently” word lists. Reconciliation also offers up to three of a candidate’s same-entity lines (those sharing a structured fact key first, then the newest) without a vector score, so the classifier can recognise a changed state. These are model instructions plus structural guards, not factual verification.
This path uses a chat LLM, so writeMode: "capture" requires
mode: "bujo" and fails config validation otherwise—there is no silent fallback
or tier downshift.
{ "memory": { "mode": "bujo", "writeMode": "capture", "path": "./.mono-agent/memory", "embeddings": { "provider": "ollama", "model": "nomic-embed-text:v1.5", "dim": 768 }, "llm": { "provider": "agent-host", "model": "openai-codex:gpt-5.6-terra" } }}{ "memory": { "mode": "bujo", "writeMode": "capture" } }To use a different model only for the reconciliation classifier, set
memory.capture.reconcileModel to a validated agent-host runtime model reference
(for example, openai-codex:gpt-6-sol). Unset means all capture stages use
memory.llm as before. Extraction and review always use memory.llm;
classification alone uses the optional model. The alternate is not a fallback:
its failure retries capture, and on the final durable attempt nonduplicate
candidates are added rather than dropped while exact duplicates stay no-ops.
The optional model uses the agent-host memory runtime and inherits applicable
timeout and recording settings when the capture LLM is also agent-host. With
an Ollama capture LLM, extraction/review remain on Ollama and the classifier
runs through agent-host, so configure that provider’s credentials separately.
No tools or channel fallback are added. A separate classifier effort key is not
supported.
On a fictional EN/PL/ES correction set (12 expected corrections, 6 unrelated
cases, repeated twice), missed corrections changed from 3 to 1 and from 3 to 0,
with zero unrelated false supersedes in either condition. These are variable,
bounded observations, not a correctness guarantee; classifier median call time
rose from about 3.1–3.3 seconds to 7.3–7.5 seconds in the fictional trials.
Provider costs and latency may vary.
Set memory.capture.cron: false or memory.capture.webhook: false (BuJo capture
mode) to skip automatic admission for host-identified cron firings (including
run-now) or webhook turns, respectively. Only host metadata counts, never a
conversation-ID prefix. Unset/true keeps the existing default. Other turns,
history, recall and explicit Remember calls are unchanged; raw summaries in
audit/ for skipped turns are also omitted. Previously admitted work still drains.
When the allowed Remember tool is enabled, the host injects brief manual-write
guidance into the per-turn session context on affected runs, not the system prompt.
BuJo receives detail-aware guidance; text-only stores receive text-only guidance.
If Remember is disabled or not allowed, no manual-write guidance is injected.
An agent can optionally narrow automatic capture with memory.capture.focus (up to
2048 UTF-8 bytes of operator-written guidance, inside a delimited extraction-prompt
section) and memory.capture.only (a subset of fact, preference, lesson).
For example, a fictional coding agent might use:
{ "memory": { "mode": "bujo", "writeMode": "capture", "path": "./.mono-agent/memory", "embeddings": { "provider": "ollama", "model": "nomic-embed-text:v1.5", "dim": 768 }, "llm": { "provider": "agent-host", "model": "openai-codex:gpt-5.6-terra" }, "capture": { "focus": "Keep durable coding preferences and lessons; skip PR and CI status.", "only": ["preference", "lesson"] } }}focus guides selection, but cannot override the strict JSON contract, attribution
or host safety checks. only is deterministic: after host label validation and
reconciliation, capture stores a memory only if one accepted label has an allowed
kind; unrelated graph nodes are also dropped. An empty only list suppresses
all automatic capture; unset preserves existing capture behavior. These settings
require BuJo capture mode, and never affect explicit Remember writes or the
compact host audit of admitted turns.
The BuJo chat model used by capture comes from the tier’s required memory.llm block. Scheduled consolidation keeps that strict tier contract but is projection-only and makes no LLM call. With memory.llm.provider: "agent-host", capture can point at an SDK runtime model reference (e.g. openai-codex:gpt-5.6-terra). The extraction prompt explicitly states exact fields, array bounds, identifier/reference grammar, lowercase relations, and the canonical 0..1 salience range. The provider-neutral strict parser remains authoritative and never clamps, rescales, or coerces model values. Standalone migrate remains Ollama-only; legacy reflect is a read-only due-state report and needs no model.
The MemoryRecall tool
Section titled “The MemoryRecall tool”The agent performs targeted durable-memory search through the read-only MemoryRecall tool: hybrid keyword (FTS) + vector search over the same memory it writes to. Coverage: config .
MemoryRecall runs no chat LLM — recall is embeddings + full-text search only. Durable writes stay in-app on the agent-host LLM via per-turn capture; recall just reads. Search one short, specific topic per call; use several calls for several topics instead of a keyword list. Leave the owner’s name out unless the question is specifically about the owner. Weak or related hits alone are not answers.
Local labelled Preferences & lessons appear only when their memory lines rank in the effective query’s top eight retrieved candidates and clear the automatic background guidance score floor. Within that window, higher-scoring lines rank first within each scope; scope precedence is user, then conversation, then agent, with six lines at most. Explicit kind: "preference" or kind: "lesson" requests keep the query ranking and scope precedence but may display retrieved lines below the score floor. If no eligible lines were retrieved, the section is omitted. The entity-driven fact sheet and ordinary dated hits are unchanged; about requests have no guidance section.
Recall returns live records, which includes completed, scheduled, and migrated
items — not only open ones. Terminal dropped/invalidated records stay
excluded. So that a finished or deferred item cannot read as a current fact, a
result whose status is not open is prefixed with that status, for example
0.800 [recorded 2026-07-06T12:00:00.000Z] [done] Ship the 0.9 release.; an ordinary open record also includes the recorded timestamp when supplied. Explicit tool hits keep the backend’s ranking. Structured results carry optional type, status, createdAt, validFrom, validTo, and dueAt alongside id, score, and text whenever the backend supplies them — a remote backend that reports none keeps its previous result shape unchanged. createdAt is the recording instant, not necessarily the event date.
Questions about the active chat belong to current conversation history, not durable
memory. The tool description tells the model to use that history rather than
MemoryRecall for the current or last message. If explicitly called, however,
MemoryRecall searches without English-only grammar that would suppress some
languages but not others. The automatic possibly-relevant block likewise uses
scores and host-stamped owner status, not an English last-message gate; the
main model decides whether the lines are relevant. Hits remain evidence, not asserted answers; only
structured conflicting current facts produce an automatic conflict note. A broad
explicit-period question belongs to MemoryJournal when available.
Interrupted-run recovery is also not a durable-memory query. For a request to
pick up, continue, or recover interrupted work, the model-facing tool contract
routes to RunHistory {} first when available. An empty MemoryRecall response
repeats that conditional exact handoff so the agent does not keep rephrasing
memory searches for evidence owned by run history.
How it is provisioned
Section titled “How it is provisioned”The configured harness auto-provisions MemoryRecall from the single config.memory block unless config.memory.recallTool.enabled is explicitly false. This default applies both to config loaded from disk and to direct createConfiguredConversationHarness / createConfiguredAgentResponder composition whose typed memory block omits recallTool. It exposes a request-scoped loopback MCP endpoint backed by the same open store and retrieval service as automatic recall. Identical normalized automatic/tool queries share one per-turn lookup; a different tool query may search again. No second SQLite handle, embedding request, or hand-maintained MCP config is involved. Caller-supplied request extensions are composed with the default tool instead of replacing it.
On this request-scoped configured-harness path, the tool also offers a deliberate
useOriginalQuery: true mode. It reuses the bounded direct lookup already made for
the current logical turn’s original user question, even when the automatic
score window shows no lines and a later rephrased search would retrieve a
different set. Existing deliberate-recall graph expansion still applies when
supported; it can add related evidence within the existing result limit. The mode
does not combine results from different queries, widen automatic injection, or
repeat the direct backend lookup. Supply either query for an ordinary
query-local search or useOriginalQuery: true, never both. The mode is unavailable
after an in-turn nonduplicate memory write, for empty questions, after turn
cleanup, and on standalone or capability-free
programmatic recall servers that do not own the bound automatic lookup.
The shared writable store counts only delivered memory IDs, once per logical turn, in either mode, including tiers without graph expansion.
For the local store’s hybrid results, MemoryRecall returns fewer weak hits and
says when the evidence is weak. Scores are ranking evidence, not probabilities:
- Tail cut. A hit more than
0.15below the best hit is dropped. When the best hit clears the calibrated0.65automatic-recall floor, hits below that floor are dropped too. The best hit is always kept. Direct hits are cut before one-hop graph expansion and the final list once more after it. - Source date and currentness. Each hit shows its recorded date and any
validity interval. Superseded or invalidated records are
superseded; a past structuredvalidToor eventdueAtisended <date>; current values stay unmarked. Date-only values close after the host’s local calendar day; timestamped values close when their ISO instant passes, preserving its offset. Local means the host process timezone: a daemon running in UTC uses UTC. Structured hits carrycurrentness: "current" | "superseded" | "ended". - Evidence note. When the fact sheet holds conflicting current values, or
the top candidates give different values for the same subject, property and
scope, the result starts with
Conflicting values:. Conflicts are checked on at least the top eight candidates before the tail cut, so the note stays even when the cut removes one of the values; structured content carriesevidence: "conflicting". When the best hit is below the floor it starts withInsufficient evidence:and carriesevidence: "insufficient".
The change is additive. The existing text and structured fields keep their
meaning; currentness and evidence are new optional fields. Original-query
mode keeps the automatic lookup’s hit list uncut but adds the same notes. A
degraded lexical-only result and array-only backends that do not report a
retrieval mode keep their previous output unchanged.
The endpoint is allocated only after the turn acquires a provider-concurrency slot, so queued turns do not accumulate listeners. If endpoint startup fails, the host warns and omits the explicit tool for that turn; automatic recall and the provider response continue. If the memory backend itself fails during a tool call, MemoryRecall returns an explicit degraded result instead of fabricated hits.
{ "memory": { "mode": "bujo", "path": "./.mono-agent/memory", "embeddings": { "provider": "ollama", "model": "nomic-embed-text:v1.5", "dim": 768 }, "recallTool": { "enabled": true } }}{ "memory": { "recallTool": { "enabled": true } } }recallTool.enabled default | Condition |
|---|---|
| on | every configured tier: Lite (FTS), Journal/BuJo (hybrid), and external backends |
| off | only when set explicitly to false |
This replaces the retired standalone @mono-agent/memory-mcp package (which also shipped memory_capture / memory_note write tools — both dropped, since in-app capture now covers durable writes). To build a recall server directly in your own code, compose @mono-agent/memory/bujo (createBujoMemoryStore) with @mono-agent/memory/search (createEmbeddingProvider) — exactly what the bundled server does. See Programmatic composition.
Recall scoring
Section titled “Recall scoring”Recall fuses two retrievers and re-ranks the result:
- BM25 keyword (FTS) over the markdown entries.
- Vector similarity over the configured embeddings.
- With embeddings, ranking is embedding-first: every candidate from either retriever is scored by its stored vector’s cosine similarity (below
0.5counts as no semantic evidence). Shared words add nothing by themselves; only exact names, numbers and dates earn a bounded bonus: each query anchor carries an equal share of up to0.15for numbers and dates and0.08for names. The embedding already reflects a name, so the smaller name bonus keeps records that only share a name from being lifted as far. A query word is such an anchor when it is a whole number, date or numeric identifier (1988-11-02never matches1988-12-02), or a word of at least three letters in the name of an entity associated with a candidate record; there is no question-word or stop-word list; matching normalizes Unicode and ignores case and accents (ZoematchesZoë). A small Reciprocal Rank Fusion (RRF) rank hint breaks ties; salience/insight are small tie-breakers.lastAccessedAtand access counts are telemetry only and never affect ranking. - Without embeddings (Lite, or a temporary embedding outage), and for a record still waiting for its vector, evidence remains lexical term overlap as before.
On web turns, recall and capture use the model-visible owner’s unprefixed text rather than the host’s project, tag, or conversation-marker envelope. Recall also uses the extracted attachment text, while capture keeps only redacted attachment metadata. The model still receives that envelope, and canonical history retains the dispatched text. This applies to bound live follow-ups too; other hosts use their existing message text. An authenticated direct adapter client can supply a matching trailing owner text that omits earlier model-visible context from capture; the web console always sends the whole owner message.
Automatic recall at the start of a turn shows a small possibly relevant block. The main agent model decides what matters; the block never claims to answer:
## Memory (possibly relevant — may be unrelated; verify before relying)
- – Morgan joined the Maple book club. (recorded 2026-03-01; superseded)- – Morgan prefers green tea. (recorded 2026-05-01; current; you said)Selection uses scores only, with no question grammar or word lists, so it works the same way in any language:
- the strongest hybrid (embedding-first) hit must reach
0.62; - further lines must score within
0.04of it; - at most three lines are shown, and identical text appears once;
- tasks remain historical context without a live
[ ]marker; current lines saytask/plan recorded, while closed lines keep their ended or superseded status, attribution and insight marker. Use a task tracker for current status; - current lines come before superseded or ended ones (date-only
validToor eventdueAtbefore the host’s local day; timestamps before the host instant). Events without a structured date are not inferred from prose; - the chosen lines are listed oldest first, so the latest statement reads last.
Each line shows when it was recorded and whether it is current, superseded or ended <date>. When every label on the line agrees, it also shows who said it: you said, assistant noted or from a document.
The block has a 1.5 KB budget. Each line’s text is capped at 360 bytes, and a line that doesn’t fit is left out. The whole automatic context, including the background card below, stays under about 2.5 KB. Opposite statements can appear together; the model weighs them.
Automatic possibly relevant lines are suppressed when host-verified owner text on web, TUI or ACP is at most 16 Unicode code points after NFC normalization and trimming. The person card and labelled background remain available. The backend lookup remains available through explicit MemoryRecall; this structural gate uses no word lists and does not apply to standalone BujoMemoryStore.load(). Dense scripts can convey more in fewer code points.
Two cases show nothing automatically:
- Lexical-only (degraded) results. The host warns instead.
- Turns that are not host-verified owner turns: group chats, other senders, cron triggers, and peers. Host-owned process-job completion wakes suppress both the possibly-relevant block and the automatic memory background card even when routed through an authenticated web channel, without changing capture attribution. This is a privacy default; these turns can still use
MemoryRecalldeliberately.
The floor, window and line count were measured on three real stores that use nomic-embed-text:v1.5, with English, Polish and Spanish questions. They were chosen to keep the answer present as often as possible while negative and near-miss probes average at most two lines.
Retrieval quality still limits other languages. With an English-centred embedding model and English memory text, Polish or Spanish questions often don’t retrieve the answer at all. A multilingual embedding model is the lever for that.
Programmatic BujoMemoryStore.load() now uses the same hybrid-only, score-based selector and three-line bound. It lacks the app’s verified-owner privacy gate and richer attribution; standalone callers must gate injection by audience themselves. Lite and degraded lexical-only stores return no automatic block; use recallWithOutcome() for deliberate retrieval.
Labelled background stays language-neutral too. Preferences and verified lessons in the turn’s scopes join when their memory ranks among the top retrieved hits and clearly leads the candidate median. Opposite advice is shown together. A person card appears when the message contains an exact person name or person: id, such as Morgan, ¿Dónde trabaja Morgan? or Gdzie pracuje Morgan?. The card shows that person’s current user-stated or document facts with the recording date, in the form home town: Maple Harbor (you said, recorded 2026-09-06), plus age for a birth date. It does not filter keys by the question’s wording. Two or more current distinct values for one key show as conflicting values — ask. First-person wording no longer selects the owner’s card. Keys drop the other: namespace and values render as text, not JSON, in both the automatic card and the explicit MemoryRecall fact sheet.
You can exercise the same hybrid scoring config-aware from the agent folder with mono-agent memory search:
mono-agent memory search "what did we decide about the rollout?"Entity graph (bujo auto)
Section titled “Entity graph (bujo auto)”The BuJo tier also maintains a lightweight entity graph beside the curated daily notes. During writeMode: "capture", the first bounded extraction plan records people, projects, organizations, concepts, precise per-memory associations, and directed relationships in graph.jsonl under memory.path.
There is no separate config switch. The graph is built only for a valid configured bujo tier: memory.mode: "bujo" plus embeddings and memory.llm. The lite and journal tiers do not build it. Capture is serialized per store and runs after durable local admission, so graph extraction never blocks on the provider-success path; if the memory LLM fails or times out, the pending intake retries without publishing partial graph state.
Only an explicit MemoryRecall call uses the graph, and expansion is deterministic and limited to one hop: direct BM25/vector seeds contribute their associated entities, and one directly related entity may pull in neighboring memories. Automatic prompt injection stays direct-only and never synthesizes a graph answer in the background. Lite and Journal never expand the graph. The living index.md, regenerated by the in-app consolidation scheduler, includes a bounded top-entity preview so the graph is inspectable as plain markdown. That projection scans deterministic source pages through inventory exhaustion or a 10,000-row safety ceiling, retaining only bounded reconciliation state before rendering at most 50 lexical groups. It filters ephemeral calendar/time nodes and collapses lexically equivalent display names without changing canonical graph state; it is not canonical entity resolution.
The MemoryJournal chronological tool
Section titled “The MemoryJournal chronological tool”MemoryJournal answers broad retrospectives over explicit calendar dates from
the curated local index. It complements MemoryRecall; it is not another
search index, does not copy or auto-inject journals, and is not exact execution
evidence. Use RunHistory/SessionHistory to substantiate exact tool inputs,
results, failures, or interrupted execution.
The first call requires all three range fields:
{ "fromDate": "2026-09-01", "throughDate": "2026-09-07", "timeZone": "CET", "limit": 10}Dates are inclusive local civil dates in the explicit IANA zone. The resolver
echoes the resulting half-open UTC instant interval, rejects nonexistent dates
or zones and ranges over 31 days, and never guesses the host zone or defaults
to UTC. limit defaults to 10 and caps at 25. Continue a frozen per-run
snapshot only with { "cursor": "..." }; the request-private authenticated cursor
accepts only its exact issued offset, and range or limit overrides are not accepted
on continuation.
Each request can hold four ephemeral snapshots. A snapshot caps at 1,000
entries and 2 MiB; each page caps at 25 entries/8 KiB and each text at 2 KiB.
Coverage says whether the range scan completed and why it stopped. A complete
empty range is successful with noData: true; backend failure is the generic
journal_unavailable error; unsupported backends do not advertise the tool.
Lite, Journal, and BuJo are supported, including read-only local stores. Custom stores must affirm chronological support before the tool is advertised. Results include canonical daily source references and stored validity/supersession state at snapshot time. Dropped records, raw BuJo audit observations, session ids, memory-root paths, embeddings, salience, access telemetry, and raw backend errors are excluded. Unsafe text and ids are replaced with fixed markers, and all returned content is untrusted historical evidence.
The Remember write tool
Section titled “The Remember write tool”Remember stores one explicitly stated fact directly, without waiting for the
capture pass. It writes the curated daily/ source on every tier and indexes in
the same critical section, so the fact is recallable immediately.
It requires a writable bujo-backend store and memory.rememberTool.enabled
(default on), and — unlike recall — it is gated by tools.allowedTools. Writes
are idempotent: the bullet id derives from the content hash, and a canonical
bullet whose index row is missing is completed rather than duplicated. Text
carrying a credential is rejected and nothing is written. See
MCP servers.
Tool policy for explicit memory reads
Section titled “Tool policy for explicit memory reads”memory.recallTool.enabled is the shared opt-out for both explicit read tools.
It defaults on for configured memory. MemoryRecall is gated by that
declaration rather than tools.allowedTools, so a restrictive or empty
allowlist still leaves targeted search available. MemoryJournal enumerates a
date range and has the additional normal app-tool policy gate: under a
restrictive allowlist, include MemoryJournal; exact/server/global deny wins.
See Tool policy and MCP tools for how MCP-provided tools differ from the built-in allowlist.
Environment variables
Section titled “Environment variables”| Env var | Config key | Notes |
|---|---|---|
| — | memory.writeMode | disabled / append-host-summary / capture; capture requires mode: bujo |
| — | memory.recallTool.enabled | Explicit memory-read family: targeted MemoryRecall plus policy-allowed MemoryJournal on local tiers; default on |
| — | memory.mode | lite / journal / bujo |
| — | memory.llm.model | Chat model for the capture pipeline |
| — | memory.llm.endpoint | Ollama chat endpoint (default http://localhost:11434) |
| — | memory.llm.timeoutMs | Per-call in-app chat-LLM timeout, default 60000. See Validation & CLI. |
| — | memory.embeddings.provider | ollama / lmstudio / openai; defaults to ollama once the required Journal/BuJo embeddings block is present; no cross-provider fallback |
| — | memory.embeddings.model | Defaults by provider (nomic-embed-text:v1.5 for Ollama; text-embedding-nomic-embed-text-v1.5 for LM Studio) |
| — | memory.embeddings.dim | Defaults to 768; set it when the model output dimension differs |
See Environment variables for the full table and precedence rules.
Journal and BuJo require an explicit, non-empty memory.embeddings block, but they do not require every field in that block. Provider, model, and dimension use the defaults above; even a block that only overrides dim is valid.
Related pages
Section titled “Related pages”- Memory overview — tier matrix and the single
memoryconfig block - Embeddings — the provider/model behind vector recall
- Consolidation — scheduled projection refresh and duplicate-group counting, without canonical-memory mutation
- Validation & CLI —
mono-agent validatechecks andmono-agent memorymaintenance
Optional BuJo owner context
Section titled “Optional BuJo owner context”memory.profile.enabled defaults to false and requires memory.mode: "bujo":
{ "memory": { "profile": { "enabled": true } }}Add this block to an otherwise valid BuJo configuration. Automatic context is still restricted to host-verified owner web, TUI and ACP turns; Telegram, other speakers and triggers receive no automatic profile or recall block.
memory.profile.enabled derives a profile from active owner-stated owner facts
and agent-scope preferences in the existing label projection. It uses current,
open notes only, omits conflicting/ended sources, dated intentions and episodes,
and selects whole entries in newest-date then stable source-ID order. Timestamp
expiry uses the host observation instant; civil end dates remain inclusive
through the host’s local day. The read-only profile CLI uses the same rule. The entire
payload, including its heading, is at most 600 Unicode code points and also
respects memory.maxBytes. Missing labels produce no profile. No model call,
new file, store, version or history field is involved. Embedding failures do not
erase the profile and emit only memory_recall_unavailable.
With the profile enabled, a confirmed retained provider session excludes unchanged already-served recall/background lines. A profile enters a cold reseed or when its supported contents change. Preparation alone does not count: only a successful retained provider invocation establishes receipts. Isolated turns do not replace shared-session receipts, and Remember writes invalidate searches without discarding pending receipts. Host continuations do not load memory. The bounded volatile cache keeps at most 256 conversations and 256 served-line fingerprints per conversation. Receipts disappear on reset, disposal or restart. If receipt knowledge is lost while the host still confirms retained provider context, automatic reinjection is conservatively suppressed until a cold reseed; deliberate memory tools remain available. A full receipt set also suppresses further automatic warm injection rather than forgetting served lines.
Opt-in automatic blocks are omitted from content-bearing turn_context traces;
only their byte count is recorded. The model/provider still receives the block.
Disabling the profile restores the existing automatic retrieval path, but cannot
erase blocks already retained by a provider session.
Optional intention lifecycle and deliberate recency
Section titled “Optional intention lifecycle and deliberate recency”Three independent BuJo-only switches default off. Add them to an otherwise
valid BuJo configuration; capture still requires writeMode: "capture":
{ "memory": { "capture": { "intentLifecycle": true }, "recall": { "intentExpiry": true, "recency": true } }}Tasks remain authoritative in the task app. Memory is not a second task
list. memory.capture.intentLifecycle lets the extraction model propose a
consequential intention only when the outer host-verified owner’s user turn
supports it. The sentence retains the intention meaning and state, in any
language. Optional intentState maps planned/pending/done/abandoned to the
existing note statuses scheduled/open/done/dropped. Ordinary open facts do not
become pending intentions. Other speakers, triggers and assistant/tool/document
claims cannot supply these proposals.
Optional extraction/API validTo is an inclusive civil end, stored as existing
due= / dueAt, never generic MemoryRecord.validTo or SQLite valid_to.
Undated plans/pending intentions acquire no date. Relative ends use the immutable
trusted admission context’s UTC calendar, preserving stated timezone and
uncertainty; no unstated date or timezone is invented. A supported explicit
zone may use its inclusive last-millisecond end with the applicable ISO offset
(YYYY-MM-DDT23:59:59.999±HH:MM, or Z); an uncertain offset means omit the
proposal. Without a stated zone, keep the civil date only. Split or clamped
sentences lose lifecycle proposals rather than transferring them to unsupported
siblings. Supported intention changes use reconcile SUPERSEDE, including a
classifier UPDATE or a NOOP whose state/end changes: the old sentence remains
history. Repeating an intention without its end is not withdrawal: a NOOP
retains the existing end, and replacements inherit it unless a supported new end
is supplied. Deliberate fact-sheet currentness combines label validity with the
source note’s currentness, retaining expired rows as historical. A passed end
never implies completion and never mutates a status.
memory.recall.intentExpiry is a conservative automatic read policy:
exclude all dated notes, even before their end, plus done/dropped notes,
from similarity, scoped guidance and person cards. The optional profile already
omits dated/closed notes. There is no persisted intention marker,
so an open pending note’s date cannot be distinguished from an ambiguous legacy
due=. Neither is automatically asserted. Unexpired dated plans also stay in
the task app; this policy does not promise automatic reminder coverage.
Under this option, deliberate MemoryRecall currentness treats a reviewed
note dueAt as an inclusive validity end, just like the existing event end.
Date-only ends remain current through the host’s local civil day (daemon
process timezone); offset ends remain current through their inclusive instant.
Task deadlines retain their existing meaning. Closed statuses still show their
status prefix. Generic supersession/terminal validity is unchanged.
Before enabling, drain pending capture and use the existing private
reviewed curate audit.
There is no backfill, automatic legacy reinterpretation/mutation, new field,
marker or migration. Review/remove/retype ambiguous dated sources before using
policy-aware deliberate currentness; automatic exclusion alone does not prove
what an old date meant. Retained extraction plans/outbox actions finish their
already-admitted proposals rather than being retrospectively reclassified.
intentExpiry does not require intentLifecycle: reviewed legacy reads and
capture rollback must remain independent.
Separately, memory.recall.recency adds at most 0.02 as a secondary ordering
term with a 30-day half-life, only in deliberate hybrid BuJo recall (including
original-query mode). It applies to events without fact/preference labels and
unlabelled notes, not facts, preferences, labelled notes or tasks. Future,
missing and invalid recording instants earn no recency term. Qualification uses
the unchanged base-score floor 0.65 and tail margin 0.15; below-floor hits
cannot be rescued. Returned scores and candidate membership stay unchanged;
original-query mode keeps its uncut below-floor tail in place. Automatic lookup,
profile, guidance, journal chronology, salience and access telemetry do not
change. The bounded fifty-hit deliberate superset remains the limit.
Flags absent/off preserve capture prompts, schemas, outputs and canonical bytes; Lite/Journal do not adopt these policies. Disable switches independently to roll back reads/new capture, but an older reader can surface dated or completed notes again, and no switch removes blocks already retained by a provider. Retained-session de-duplication uses actual invocation receipts; a profile source ending sends one changed/empty profile replacement, not repeated text. Keep recency off until separately reviewed usefulness evidence supports it; scripted regressions are not real-model extraction or retrieval-quality proof.
Automatic recall and conversation provenance
Section titled “Automatic recall and conversation provenance”Automatic similarity recall excludes records whose known source.session
matches the current logical conversation, including different daily
#YYYY-MM-DD buckets. This prevents an earlier status from the active dialogue
being reintroduced as current background. Filtering occurs before score/line
selection; explicit MemoryRecall, including its original-query mode, remains
unchanged. The standalone local store’s automatic load() follows the same rule.
Journal/Lite completed-turn projection and explicit Remember preserve the host
conversation id in source.session. Web turns already use web:<thread-id>;
no bare-thread-id alias is guessed. New canonical bullets also record it durably
as optional source_session=<encoded id> metadata at the end of their
<!--mem ...--> comment: BuJo capture ADD and SUPERSEDE stamp the capturing
conversation, a text-changing UPDATE names the conversation that changed it, and
NOOP and the superseded old line keep the provenance they had. The value is the
most recent capture session that contributed to the line, not proof of who
authored every historical claim, and it comes only from the host-supplied
conversation id, never from model output. Rebuild restores it from Markdown, so
it survives restart, reconcile and safe rebuild.
The index mirrors each line’s own recorded value: a Remember retried from another conversation after a partial write keeps the original session, and an id that cannot be stored is unknown in Markdown and SQLite alike. Memory health reports an index session that disagrees with the Markdown provenance.
Records without a known source conversation (lines written before this metadata,
ids longer than 512 UTF-8 bytes, or malformed/duplicated metadata) stay eligible
for automatic and explicit recall. They are labelled source conversation unknown beside, and independent of, any semantic attribution such as you said.
Known other-conversation hits render as before, and a known source conversation id
is never shown to the model. Old lines are not backfilled: nothing is guessed from
text, file names or earlier index rows. Label/card readers do not expose
provenance. This is not a content-based staleness detector.
Daily files that are not rewritten stay byte-for-byte unchanged. Rewriting a daily file re-serializes all of its bullets, which (as before) also normalizes legacy one-line bullets to the two-line form.
Downgrade limitation: older releases ignore source_session when reading, but
any rewrite of a daily file by an older writer (for example a status change,
curation or migration) re-serializes its bullets without the metadata. Those
lines then read as source conversation unknown after upgrading again.