- TypeScript 95.1%
- JavaScript 4.2%
- Shell 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Adds `src/claude/`, a thin adapter around the same `MemoryEngine` the Pi extension uses, plus the `plugin/` manifest that wires it up. The repository doubles as a plugin marketplace. - SessionStart hook injects the epistemic policy, profile, daily note and tool guidance, and runs again after compaction - UserPromptSubmit hook carries passive activation, trimming its own low-ranked tail to fit the host's 10,000 character cap - a dependency-free stdio MCP server behind a transport seam exposes the nine memory tools; the reference SDK would have brought seventeen transitive dependencies into a package that has three - writes stay confirmation-gated through MCP elicitation, and refuse outright on a host that cannot ask the user - stored SOP skills are symlinked into the host's skills directory, never overwriting a name the user owns `resolveDataDirectory` and the epistemic policy text now have one home each, so the two adapters cannot drift. `task claude:dev` loads the plugin from a clone with no configuration; `task claude:install` installs it. README records what the host cannot do: no runtime system-prompt seam, no composable status line, no tools on the first turn of a headless run, and no semantic search inside the plugin sandbox. |
||
| .claude-plugin | ||
| evals | ||
| plugin | ||
| src | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| eslint.config.mjs | ||
| LICENSE | ||
| mise.toml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| Taskfile.yml | ||
| tsconfig.json | ||
| vitest.config.ts | ||
@morten-olsen/pi-memory
Durable personal memory for Pi and Claude Code. Before every turn it tells the agent what it knows something about, so the agent never has to guess whether consulting memory is worth it.
One archive serves both. The memory behaviour lives behind a single MemoryEngine; the Pi extension, the Claude Code plugin, and the CLI are thin adapters around it, and they can read the same directory at the same time.
Why this exists
The obvious starting point for agent memory is the "LLM wiki" pattern: let the agent consolidate what it learns into a folder of Markdown documents. That is where this project started, and two pieces of it survive — Markdown as canonical, human-readable storage, and a wiki you can inspect, edit, and keep in Git.
But a wiki only solves retention. When memory is a pile of documents, retrieval depends on the model deciding to search, guessing the right query, and wording that happens to match. The failure mode is the unknown unknown: you ask for a dinner recommendation, and the agent has no idea a stored constraint should influence the answer, so it never looks.
Two systems, not one
This package is really two cooperating systems, and most of its design follows from keeping them apart.
The archive retains without limit. Storage is cheap and expandable, so unlike human memory there is no reason to forget in order to make room. Nothing is overwritten: a superseded fact keeps its record and gains an end date. The wiki is the source of truth, and SQLite is a derived index that can be deleted and rebuilt at any time.
The attention layer treats context as the scarce resource. An agent has the constraint a filing cabinet does not — a bounded context window and bounded attention within it. So a second system decides what surfaces, given relevance, recency, lifecycle, and a fixed budget. Superseding a fact is an attention decision, not a storage one: the old employer is not deleted, it stops being presented as current.
Separating them means retention policy never has to trade against context policy. You can keep everything and still say little.
Cues, not contents
Passive activation surfaces pointers, not documents. It names what is held — title, key, kind, date, an optional one-line description — and the agent loads what it decides it needs with memory_recall or memory_follow. Roughly the way human memory offers a sense that you know something before you have actually recalled it.
This is a deliberate trade. Injecting whole documents bets that selection was correct, and at any useful wiki size that bet loses often. A cue costs a fraction of a body, so the same budget carries several times as many pointers and the odds that the thing you needed was at least mentioned go up sharply. The exception is memory whose omission makes an answer wrong rather than merely incomplete — a critical allergy, a hard policy — which is supplied in full.
Above the cues sits a layer that cannot omit anything: counts of what was not shown. Cue selection can still miss; a count over everything held cannot. "23 memories about Sam, 4 still open" is what stops the agent assuming the cues are all there is.
Search prepares candidates; it does not decide
Lexical and hybrid search are candidate generators. They do not produce perfect results and are not expected to — that is precisely what an agentic setting makes tolerable. The retrieval layer narrows a large archive to a plausible working set and reports honestly on what it left out; the agent, which can read, reason, and ask again, does the deciding. Nothing here is meant to stand on its own.
Every output is shaped for that: provenance on each record, a stated reason for each match, explicit counts of the unshown, and Markdown you can read yourself. It is built for agent and human introspection, not as an oracle.
What it does
- Loads a compact
profile.mdonce per session as stable always-on context - Loads today's daily note once per session as a stable scratchpad for the day's orchestration
- Cues task-relevant memory before each turn, in Pi and in Claude Code
- Widens retrieval with the recent conversation when the prompt is too thin to stand on its own, so short follow-ups such as "book it" still activate, without letting unrelated chatter bury the question
- Reports counts of everything held on the subjects and domains in play, so the agent knows what it has not been shown
- Loads in full only what would make an answer wrong if omitted — high-priority constraints and policies, including ones reachable through a relation
- Stores atomic facts, state, constraints, preferences, commitments, and episodes
- Ranks by blending attention, relevance, importance, and recency, and reserves slots so open loops are never crowded out
- Says nothing at all when nothing clears the relevance floor, rather than offering the nearest weak match
- Withholds memory marked
sensitiveunless the turn is clearly about it, or an explicit trigger or activation rule fires - Supersedes prior values through stable keys such as
person:morten.current-employment, and flags likely replacements stored under a different key for confirmation - Reviews stale memory in bounded, cursor-based batches rather than scanning everything with an LLM
- Reports redundancy, stale open loops, broken relations, and never-confirmed inferences through
pi-memory consolidate, without editing anything - Offers agent-maintained SOP skills from the memory directory, so procedures live next to the memory they operate on
- Exposes confirmation-gated
memory_remember,memory_forget,memory_profile_update, andskill_update, plus the read-onlymemory_recall,memory_follow,skill_list, and thedaily_note_get/daily_note_updatescratchpad tools
Kinds describe lifecycle semantics. Domains, triggers, and relation predicates are user data; the package ships with no assumptions about food, employment, home automation, or other personal domains.
Requirements
- Node.js 24
- Pi 0.83 or newer, for the Pi extension
- Claude Code 2.1.220 or newer, for the plugin (older versions lack
reloadSkillsand MCP elicitation)
Install it only in a personal Pi profile, or configure the Claude Code plugin at user scope only, if coding sessions should not receive personal memory.
Install from npm
Once published:
PI_CODING_AGENT_DIR="$HOME/.pi/personal-agent" \
pi install npm:@morten-olsen/pi-memory
npx @morten-olsen/pi-memory semantic enable
Omit PI_CODING_AGENT_DIR to install it in the default profile. The second command enables local semantic retrieval and downloads an embedding model on first recall. Skip it only if lexical recall is intentionally preferred.
Install from a local clone
git clone <repository-url> ~/Projects/ai/memory
cd ~/Projects/ai/memory
mise trust mise.toml
mise install
task install
task ci:quality
task build
PI_CODING_AGENT_DIR="$HOME/.pi/personal-agent" \
pi install "$PWD"
pnpm memory semantic enable
Pi records the clone path and loads dist/pi/pi-extension.js. After changing source:
task ci:quality
task build
Then run /reload in Pi.
Install in Claude Code
The repository is also a plugin marketplace, so plugin/ can be loaded straight from a clone or installed like any other plugin.
From a clone, while iterating
task claude:dev # builds, then starts Claude Code with the plugin
That is claude --plugin-dir "$PWD/plugin", which loads the plugin in place rather than copying it. Nothing needs configuring: ${CLAUDE_PLUGIN_ROOT} is the real directory inside the clone, so the launcher finds ../dist/cli/pi-memory.js on its own. Extra flags pass through — task claude:dev -- --model opus.
The loop after that:
| You changed | To pick it up |
|---|---|
src/ — hooks, activation, formatting |
task build; the next prompt uses it, because each hook is a fresh process |
src/ — MCP tools |
task build, then /reload-plugins, since the server is long-lived |
plugin/ — hooks.json, .mcp.json, plugin.json |
restart the session; manifests are read once at startup |
From a clone, installed permanently
task claude:install
This adds the clone as a marketplace and installs the plugin, so it loads in every session without a flag. Installing copies plugin/ into ~/.claude/plugins/cache/, which has two consequences worth knowing:
-
The copy has no
dist/beside it, so tell it where the clone is. In~/.claude/settings.json:{ "env": { "PI_MEMORY_BIN": "/path/to/pi-memory/dist/cli/pi-memory.js" } }Settings
envreaches hook and MCP processes, so this is all it takes.npm linkfrom the clone works too — it putspi-memoryonPATHpointing at the same build. Either waytask buildis live, since nothing is copied but the manifest. -
Editing anything under
plugin/needs a re-copy.claude plugin updatewill not do it: the version is unchanged, so it reports "already at the latest version" and keeps the stale manifest.task claude:installreinstalls, which does re-copy.task claude:uninstallremoves both the plugin and the marketplace.
From a published package
Once @morten-olsen/pi-memory is on a registry:
claude plugin marketplace add ssh://git@code.olsen.cloud:2206/ai/pi-memory && \
claude plugin install pi-memory@pi-memory
Adding an npm lockfile next to plugin/package.json at that point makes Claude Code install the runtime itself (npm ci --ignore-scripts in the plugin cache), and no PI_MEMORY_BIN or global install is needed.
How the runtime is found
The plugin's hooks and MCP server both go through plugin/scripts/pi-memory-runtime, which takes the first of:
$PI_MEMORY_BIN../dist/cli/pi-memory.js— the clone case above; impossible for an installed copy, which has no repository around itnode_modules/@morten-olsen/pi-memoryinside the plugin, from the published-package casepi-memoryonPATH, fromnpm i -gornpm link
If none exist it says so once per session, and every turn proceeds normally without memory rather than failing.
What the plugin wires up
| Piece | Mechanism |
|---|---|
Session-stable context — epistemic policy, profile.md, today's daily note |
SessionStart hook returning additionalContext, re-run after compaction |
| Passive activation before every turn | UserPromptSubmit hook returning additionalContext |
memory_recall, memory_follow, memory_remember, memory_forget, memory_profile_update, daily_note_get, daily_note_update, skill_list, skill_update |
bundled MCP server (pi-memory mcp) |
| Confirmation before any write | MCP elicitation — the host renders the dialog and hands back the answer |
| Stored SOP skills | symlinked into ~/.claude/skills/, with reloadSkills so they load in the same session |
Tools arrive prefixed, as mcp__plugin_pi-memory_memory__memory_recall and so on. Claude Code asks permission the first time each is used; add the ones you trust to permissions in settings.json to avoid being asked for both permission and confirmation on every write.
Configuration
Run /plugin and configure pi-memory, or pass values at install time:
claude plugin install pi-memory@pi-memory --config data_dir=~/memory --config status_messages=true
data_dir— the archive. Defaults to~/.local/share/pi-memory. An ambientPI_MEMORY_DIRtakes precedence, so one shell variable can point Pi, the CLI, and Claude Code at the same directory.status_messages— prints one line per turn, such asmemory: 3 cued, 1 loaded. Off by default, because it prints on every prompt.
Coexisting with Claude Code auto memory
Claude Code has its own memory: it writes notes to ~/.claude/projects/<project>/memory/ and loads MEMORY.md into every session. The two systems are left to compete, and both will save things. They differ in kind — auto memory is per repository with no retrieval step, while this archive is personal, has an attention layer, and surfaces provenance and counts.
To run only this one, disable auto memory in ~/.claude/settings.json:
{ "autoMemoryEnabled": false }
What Claude Code does not support
Everything the Pi extension does has an equivalent except these, and each is a host limitation rather than a missing feature here:
- No runtime system-prompt seam. Pi appends the epistemic policy to the system prompt. Claude Code can only add context to a turn, so the policy and the tool guidance are injected as session context instead. They can be dropped by compaction; the
SessionStarthook re-injects them when that happens. - Injected context is capped at 10,000 characters per hook. Cues are ranked, so the adapter trims its own low-ranked tail and states how many lines it dropped rather than letting the host silently truncate.
- No composable status indicator. Pi's status is namespaced per extension; Claude Code has a single
statusLineowned by the user, sostatus_messagesuses a per-turn notice instead of a status bar. - Writes need MCP elicitation. A host that does not support it gets no write path at all: the tool refuses and says so, rather than writing memory the user never confirmed.
- The first turn of a headless
-prun has no tools. MCP servers may still be connecting when that turn is built. Activation is unaffected — it is a hook, not a tool — and interactive sessions connect long before anyone types. - Semantic search needs a global install. Claude Code installs plugin dependencies with
--ignore-scripts, soonnxruntime-nodecannot build there. Lexical recall works everywhere; for embeddings, install the runtime globally and runpi-memory semantic enable. - Conversation-window activation is Claude Code only. Short follow-ups such as "book it" activate against the recent turns, read from the session transcript. Pi's
before_agent_startevent carries no history, so its adapter cannot do the same.
To debug a hook, set PI_MEMORY_HOOK_DEBUG=/tmp/pi-memory-hook.log — every event, the resolved directory, and the response are appended. This exists because Claude Code does not surface hook stderr; claude --debug hooks shows only whether context was delivered.
Data directory
The default is ~/.local/share/pi-memory; override it with PI_MEMORY_DIR, the CLI's --data-dir, or the plugin's data_dir option, in that order of precedence.
One archive can be open in several processes at once — a Pi session, a Claude Code MCP server, a hook process per prompt, and the CLI. The derived index runs in WAL mode with a busy timeout so readers are never blocked out; the canonical Markdown is only ever written through confirmed tool calls.
~/.local/share/pi-memory/
├── .gitignore # Ignores the derived SQLite and model files
├── activation.yaml # Optional shared activation rules; empty by default
├── config.yaml # Semantic-search settings
├── daily/ # One Markdown scratchpad per day (YYYY-MM-DD.md)
├── index.sqlite # Derived local FTS/vector index
├── models/ # Derived local model cache; ignored by Git
├── profile.md # Compact always-loaded, session-stable context
├── skills/ # Agent-maintained SOP skills (one directory per SKILL.md)
└── wiki/ # Canonical Markdown memories
Initialization creates .gitignore without overwriting an existing file, so the remaining files can be managed with Git:
cd ~/.local/share/pi-memory
git init
git add .gitignore activation.yaml config.yaml profile.md daily skills wiki
git commit -m "chore: initialize personal memory"
After pulling changes on another machine, rebuild the derived index:
pnpm --dir ~/Projects/ai/memory memory rebuild
Memory is plaintext and may contain sensitive information. Use a private encrypted remote, or do not push it off the machine.
Session-stable profile
profile.md contains only information likely to matter in almost every conversation, for example a preferred name, language, or timezone. It is loaded once when the session starts and placed before dynamic recall to preserve a stable prompt-cache prefix.
The agent can replace it through memory_profile_update, but the change requires confirmation and is intentionally not applied to the current session. Run /reload or start a new session. The file is limited to 8 KiB so it cannot become a second memory archive.
Transient projects, detailed history, and selectively relevant sensitive facts belong in wiki/, not the profile.
Daily notes
daily/YYYY-MM-DD.md is a per-day scratchpad for light orchestration: the day's tasks, meetings, and a small cache of links to relevant documents. It is explicitly not durable memory — it is the agent's working surface for the day, not a place to capture knowledge.
Only today's note is injected into context, and only when it is non-empty. Like the profile, it is snapshotted once at session_start so it stays stable for the whole conversation: every turn sees the same note, and the prompt-cache prefix is preserved. The agent can read and update any day's note through daily_note_get and daily_note_update, but edits the agent makes mid-conversation apply to disk and to the next session's injection — the cached snapshot is not re-injected, which is intentional and matches how memory_profile_update behaves.
On /resume, today's note is read fresh as part of the base context. There is no diff against the original conversation's snapshot; conversations are not expected to span long enough for that to earn its complexity.
The file is hard-capped at 8 KiB. If a day's note cannot fit, it is no longer light orchestration — it is memory, and belongs in wiki/ through memory_remember.
Skills
skills/ holds standard operating procedures — how the user wants a recurring task done, rather than what is true. Memory records state; a skill records procedure. Keeping both in one directory means a procedure can be versioned, synced, and reviewed alongside the memory it operates on.
Each skill is a directory containing a SKILL.md with name and description frontmatter, following the Agent Skills layout that Pi already loads:
skills/
└── weekly-review/
└── SKILL.md
The Pi extension contributes the directory through the resources_discover hook, so stored skills appear in the ordinary skill list and are loaded on demand by description — the body never enters context until Pi decides the task matches. This keeps the cost of a large SOP library proportional to what is actually used, matching how recall itself avoids sending the archive to an LLM.
Claude Code discovers skills from fixed locations instead, so the plugin symlinks each stored skill into ~/.claude/skills/ at session start and asks the host to re-scan. Links are only created or removed: if a skill name is already taken by something the user owns, the plugin leaves it alone and says which one it could not offer.
Two tools manage the collection:
skill_listreports the stored skills with name, description, and pathskill_updatecreates or replaces one after confirmation, validating the name and description against the spec so a written skill is always loadable
Skills are written only when the user explicitly asks to capture a repeatable procedure; the agent does not infer one from observed repetition. Like the profile, a new or renamed skill is picked up after /reload in Pi, or at the next session start in Claude Code, since discovery runs once at startup. Deleting a skill is a plain rm of its directory — it is ordinary Markdown with no derived index behind it.
Memory format
Each file represents one independently activated and updated memory:
---
schemaVersion: 2
id: current-focus-a1b2c3d4
key: person:morten.current-focus
title: Current focus
kind: state
subjects: [person:morten]
domains: [work, projects]
triggers: [working on, current project, current focus]
relations:
- predicate: working-on
target: project:beacon
cardinality: single
priority: normal
sensitivity: personal
attention: ambient
status: active
recordedAt: 2026-08-16
reviewAfter: 2026-09-01
supersedes: []
source: user
---
Morten is currently working on Project Beacon.
Kinds
constraint— a hard restrictionpolicy— a rule for decisions or actionsstate— a current value that may become stalefact— stable reference knowledgecommitment— unfinished work or obligationpreference— a soft preferenceepisode— a dated historical eventinference— unconfirmed interpretation
Attention
ambient— current context worth keeping in viewopen-loop— unfinished or unresolvedclosed-loop— completed or cancelled; excluded by defaultreference— consulted when relevant
Relations
Relations point at stable keys, so superseding a record does not break the link. Recalled memories surface their links as one-line descriptors — direction, predicate, target key, and its status — without loading any related content. The agent then decides whether to follow:
memory_follow({ keys: [...] })loads up to 10 related memories in one call- One hop per call; deeper traversal requires another visible call
- Following defaults to
scope: all, because the agent is navigating a link it has already been shown; a narrower scope can still be requested - A key that yields nothing comes back in
unavailablewith the reason it was withheld — scope, staleness, or status — so a filtered link is never mistaken for a missing one
Filters staying authoritative on recall while following defaults to all is deliberate: passive recall must not volunteer a closed loop or a stale value, but a link the agent has already seen and chosen to open should not silently return nothing. The reason string keeps the distinction visible instead of collapsing "filtered" and "absent" into the same empty result.
There is no automatic expansion. Semantic search finds an entry point; relations are explicit, agent-initiated navigation.
memory_recall accepts scope: default, open, closed, or all. Historical wording also makes closed and superseded records eligible.
Updating and review
For a single key, a changed value supersedes the previous active record. Repeating the same value refreshes its confirmation metadata instead of creating a duplicate.
reviewAfter does not delete memory. Overdue state and commitments stop answering current questions confidently and enter the review queue:
pnpm memory review 20
The queue is ordered and bounded. Closed loops and archival episodes are not repeatedly reviewed.
Forgetting
Superseding is the normal way a value changes, and it preserves history. Deletion is for the case supersession cannot express: reshaping information into differently-shaped memories, where the key itself changes and the old record has no successor to point at.
memory_forget({ ids: [...] }) requires confirmation, then removes the canonical Markdown files and their index rows, including the relations sourced from the deleted memories. Links from surviving memories that pointed at a forgotten key are left in place and simply stop resolving — the descriptor still shows direction, predicate, and key, but no title or status. Nothing else is rewritten, and ids that do not exist come back in missing rather than failing the call. If the data directory is a Git repository, deleted files remain recoverable from history.
Semantic retrieval
Retrieval always combines deterministic per-memory triggers and SQLite FTS. Optional shared rules in activation.yaml can map vocabulary to domains, but no domain-specific rules are installed by default.
Local semantic search is recommended for robust passive activation but requires an explicit opt-in because first use downloads a model:
pnpm memory semantic enable
pnpm memory semantic status
pnpm memory semantic disable
Without it, passive recall still runs automatically but depends on per-memory trigger or text overlap. It will miss more paraphrases and unfamiliar vocabulary.
Semantic retrieval uses @huggingface/transformers for local embeddings and sqlite-vec for nearest-neighbour search. The first recall after enabling caches the model under models/, then embeds existing memories in bounded batches. Later writes are embedded incrementally. Change the model in config.yaml if needed; changing it rebuilds the derived vector index.
Semantic similarity only generates candidates. Temporal validity, open/closed-loop scope, and status remain hard eligibility filters, so a similar old closed loop cannot become a current answer.
CLI
From a local clone:
pnpm memory init
pnpm memory validate
pnpm memory rebuild
pnpm memory recall "what am I working on?"
pnpm memory review 20
pnpm memory consolidate
pnpm memory semantic status
Manual Markdown edits require pnpm memory rebuild. Writes made through the Pi extension or the Claude Code plugin update the index incrementally.
Two further commands exist for the Claude Code plugin and are not meant to be typed by hand: claude-hook answers a hook event read as JSON on stdin, and mcp serves the memory tools over stdio.
consolidate is read-only. It reports possible duplicates, open loops that are overdue for review but still being injected as current, relations pointing at keys no memory defines, and inferences that have sat unverified for more than ninety days. It never edits the wiki — the output is meant to be read like a diff and acted on deliberately, which is the property that automatic conflict resolution in other memory systems gives up.
Activation quality
evals/ holds deterministic scenarios — fixture wikis, scripted turns, and assertions about what was surfaced. They run in CI with no API key and no model download, using a hashed bag-of-words embedding so the hybrid lexical/vector path is exercised without run-to-run variance.
task evals
The headline metric is cue recall: of the memories that mattered, how many did the agent at least get told about. Precision is deliberately not the target — surfacing some irrelevant pointers is the trade being made, so noise is tracked and reported but never asserted on. Cost in tokens per turn is asserted, because coverage that is not affordable is not an improvement over injecting whole documents.
Violations are the hard failures: something needed was never surfaced, something forbidden was, a stale record was presented as current, or a correctness-critical memory was cued instead of loaded.
Tuning constants live in one place each — DEFAULT_WEIGHTS in src/memory/memory.ranking.ts, and the cue and load thresholds in src/memory/memory.activation.ts — so a change in behaviour can be attributed to a number rather than argued about.
Synthesized answers are deliberately not cached
A popular LLM-wiki pattern is to persist the result of a multi-document synthesis as a new document, so the same join is not repeated later. This package does not do that. A cached answer is a materialized view with no key lifecycle: the moment a source memory is superseded, the cached document silently disagrees with current state, and it can then be recalled as evidence and re-synthesized into further syntheses. That would break the staleness guarantees that current-state recall is built on.
Future: reinforcement, not repetition
Spaced repetition exists because human retention decays — read the book the night before and you keep the highlights; space the reading out and you keep the material. An agent has no such decay. One write is permanent and perfect, so repetition can buy nothing in the archive.
What repetition should buy is salience. Which memories matter is not knowable at write time; it emerges from how often a fact turns out to be worth restating, re-confirming, or loading. So the planned feature is a reinforcement signal that feeds the attention layer's ordering — not a retention mechanism, and never a forgetting one. A memory that has not been touched in two years sinks in the cue ordering; it is never removed.
The two-system split decides where that data lives:
- Re-confirmation is archive data. The user restating a fact is an observation about the world, and it already has a home:
confirmedAt, refreshed when an identical memory is re-proposed. It belongs in Markdown. - Access telemetry is attention data. How often the system surfaced a memory, and how often the agent chose to load it, says nothing about the world — only about what proved useful. It belongs in the derived index, where losing it to a rebuild is acceptable degradation rather than data loss.
The seed of this is already present and unused: confirmedAt is maintained on re-proposal but read only as a date fallback, never as a signal of standing.
Future: synthesis as derived artifacts
A synthesis feature is planned, with build-system semantics rather than wiki-document semantics:
- A
synthesiskind recordsderivesFromsource keys alongside its content. - Eligibility is computed, not guessed: if any source key's active record changed after the synthesis was recorded, the synthesis is automatically ineligible for recall.
- Syntheses restate sources; they never introduce new facts. New information goes through
memory_remember. - They are excluded from key uniqueness and supersession chains, and always have
referenceattention — a cached conclusion is never current state. - They are generated only on repeated demonstrated need, not on every good answer.
This is deferred until real usage shows repeated expensive joins — evidence before cache.
Limitations
There is not yet an interactive review UI, encryption, a dedicated close/reopen-loop tool, or the synthesis system described above. Consolidation reports but does not apply; that is deliberate, not a gap to close by adding automatic edits.
Relevance is lexical coverage plus optional embedding similarity. Lexical coverage cannot tell homonyms apart, so a note about library books can still surface for "book a table" when the prompt is short and semantic search is disabled. Enabling semantic retrieval reduces this; it does not eliminate it.