two stores

What is worth a page, and what is only worth remembering

The wiki is edited, linked and committed. The memory is written once and searched later. Keeping them separate is the whole point — and the boundary is drawn in prose, not in code.

Most systems that give a model long-term state pick one substrate. This one runs two, with different write disciplines, different retrieval mechanics and different tolerances for noise. A fact about a person belongs in a page that a human will read and correct. A note that a service was restarted on Tuesday belongs in a store that nobody curates.

The rule is stated three times in the source — once in the tool description, once in the instruction block sent to every client, and once more in a patch script that inserted it there. Its repetition is the enforcement mechanism.

Do NOT save transient/one-off details (readings, countdowns, runtime status) — those go to memory_retain only.

src/server.ts:96 · src/tools/wiki_append.ts:98
The memory / wiki split A durable fact arriving in conversation reaches a decision gate. Facts about a person, project, decision or preference go left to wiki_append, which writes a bullet on a curated markdown page under git, retrieved later by index, search, neighbourhood expansion or graph traversal. Transient details go right to memory_retain, which posts to a memory service that extracts entities and indexes for hybrid search, retrieved later by recall or reflect. A comparison strip at the bottom contrasts the two stores on write discipline, structure, retrieval, revision and failure mode. a fact emerges in conversation will this still matter in six months? yes — durable no — episodic the wiki — curated, versioned, human-editable wiki_append({ slug, content, links }) routes · de-duplicates · links · locks · commits <wiki>/wiki/<slug>.md ## Updates - one durable fact [[link]] (added YYYY-MM-DD) one page per person, project or topic git commit — “wiki_append: <slug>” · fully reversible retrieved by wiki_index listing files_search snippet files_neighbors subgraph graph_* multi-hop the memory — append-only, machine-curated memory_retain({ text, tags?, bank? }) no routing, no de-duplication, no revision POST /v1/default/banks/<bank>/memories { items: [ { content, tags? } ] } the service extracts entities and indexes for semantic + keyword search with a reranker bank auto-created on first use — PUT, best effort, swallowed retrieved by memory_recall the matched facts themselves memory_reflect a synthesised narrative write discipline structure revision failure mode source of truth guarded, refuses duplicates explicit [[edges]] between pages edit the file, commit again stale prose a human can fix the files on disk open, accepts anything ≥ 3 chars entities inferred by the service no update tool is exposed accumulating noise the service's own index
Two half-lives. The asymmetry is deliberate: the store that is hard to write to is the one a human maintains, and the store that accepts anything is the one nobody has to read.

Three verbs, one service

The memory tools are a thin, opinionated client over an HTTP API. The base address is configurable and defaults to a loopback service, so the memory store is a process the gateway talks to rather than a database it owns. Every operation is preceded by a bank check.

memory_* tools — src/tools/memory.ts
Tool Request Notable arguments Returns
memory_recall POST …/banks/<bank>/memories/recall query; limit 1–50, default 10; optional tags; bank default personal The matched facts, most relevant first
memory_retain POST …/banks/<bank>/memories text ≥ 3 characters; optional tags for later filtering The service's raw acknowledgement
memory_reflect POST …/banks/<bank>/reflect query; budget ∈ low / medium / high, default low A synthesised answer rather than raw matches

The recall / reflect distinction is stated plainly in the source: “recall returns raw matched facts, reflect returns a synthesized narrative” (src/tools/memory.ts:145). The first is evidence; the second is an opinion about the evidence, and it costs a reasoning budget to produce.

Token discipline, applied twice

A recall request is a negotiation about size. The client computes a token ceiling from the requested result count, asks the service to omit three expensive sections, and then — because the service may include them anyway — deletes them from the response before handing it to the model.

The recall round trip and its two trimming stages A recall call with a limit produces a request body whose max_tokens is the limit multiplied by five hundred, clamped between one thousand and five thousand, with a low budget and an include block asking for no entities, chunks or source facts. The service responds with JSON. The client then parses that JSON, truncates the results array to the requested limit and deletes the entities, chunks, source_facts and trace keys before returning text. If parsing fails, the original text is passed through unchanged. 1 · ask for less 2 · the service 3 · trim what came back memory_recall({ query, limit = 10, tags?, bank? }) request body — memory.ts:72-78 query: <the question> max_tokens: min(max(limit × 500, 1000), 5000) budget: "low" include: { entities: null, chunks: null, source_facts: null } tags are attached only when the array is non-empty bank is auto-created first, best effort the ceiling in practice limit 1–2 → 1,000 · limit 10 → 5,000 · limit 50 → 5,000 memory service semantic + keyword + reranker a separate process, reached over loopback HTTP compactRecallResponse — memory.ts:31-44 JSON.parse(text) results = results.slice(0, limit) delete entities delete chunks delete source_facts delete trace on parse failure: return the text untouched isError: !ok, content: [{ type: "text", text }] the same trimming is applied only on success — an error body is forwarded verbatim
Belt and braces. Asking the service to omit a section and deleting it on arrival are redundant by design — the client does not trust the server's include handling, and says so by implementing both.

Version pins written as comments

Three comments in memory.ts record exactly which API shape the client was written against. They read as scar tissue from a migration, and they are the only version documentation in the repository:

src/tools/memory.ts:20, :118, :162the upstream contract, as remembered
// Hindsight 0.4.x: banks are created/updated via PUT /banks/{bank_id}, not POST /banks
// Hindsight 0.4.x RetainRequest: { items: [{ content, tags? }], async? }
// Hindsight 0.4.x ReflectRequest: { query, budget?, max_tokens?, include? }

The retain client sends items but never async; the reflect client sends query and budget but neither max_tokens nor include. The comments describe the full contract, the code uses the part it needs.

Bank creation is a best-effort side effect

ensureBank runs before every recall, retain and reflect. It lists banks, returns early if the target is present, and otherwise issues a PUT to create it. The entire function is wrapped in a try whose catch block contains only the comment /* best-effort */. If the memory service is down, the failure surfaces from the real operation that follows, with a message the model can act on — not from a setup step it did not ask for.

Banks as namespaces

The default bank is configurable and named personal. Every tool takes an optional bank argument, so an agent can partition memory by context without any server-side configuration — the partition is created on first write. Separate work memory is reached through the federation layer instead, as prefixed tools from a downstream server; see the tool surface.

The boundary is advisory — and that is the interesting part

Nothing stops an agent from calling memory_retain with a durable biographical fact, or wiki_append with a runtime status line. The schemas are nearly identical: both take a string and optional tags or links. What separates them is a rule written in English, in three places, aimed at a reader capable of following it.

This is a real architectural position, and it has a cost. The wiki's guard rails are executable — similarity routing, de-duplication, a lock — because the wiki is the store where a bad write is expensive to undo. Memory has no guard rails because a bad write there is merely noise, and noise is what reranking is for. The system spends its correctness budget where mistakes are permanent.

Avoid storing one-off transactional details. Focus on durable facts, preferences, decisions.

src/tools/memory.ts:106 — memory_retain description

Note that this instruction sits inside memory_retain itself, the store with no curation. Even the loose side of the split asks the model to exercise judgement — the difference is that here, nothing checks.

No memory contents, bank names beyond the documented default, service addresses or ports appear on this site.