docs GitHub

ContributingDraft

Vyre Memory everywhere

The user asked for IQ to be "really smart and designed really well and implemented across every surface". This page is the map. It lists each surface, what IQ does there today, what it should do, and the gap. The gaps are ranked by value for cost at the end, with an owner for each.

One rule holds on every surface: one ask path. A question goes to memory.ask (ADR 0034) and comes back as an answer with its sources, or "not sure yet" with what memory does know. No surface writes its own question rules, its own prompt or its own model choice. The hard rules come with it:

  • Grounded or abstain. An answer names the sessions or facts it stands on. Code checks every name, number and path against what it cites. A surface never shows an answer without its sources one tap away.
  • Source trust. Personal facts come only from the user's own words about their life, never from Claude's turns, tool output, pasted text or a Vyre dev session (core/memory/personal/trust.js).
  • Determinism. The same question over the same memory gives the same answer, confidence and sources. Replies are kept by the prompt's hash.
  • The graph is first-class. People, projects and decisions stay visible and editable wherever IQ shows an answer: a source links to the turn, and a person links to the Memory view.
  • Caps in plan terms. Questions have their own daily share of the person's Claude plan (about 150 questions at the default share). At the cap memory.ask returns limited: true and a message in plan terms, and every surface shows that message. No surface ever shows a dollar figure (cost_usd is internal): nothing is a charge, the reads run on the person's Claude plan.

The contract every surface uses

memory.ask { question, context?: { project?, thread? }, project_cwds?, stream?, id? }
  -> { id?, answer, confidence, abstained, known[], sources[{ session, seq, name, quote, ts }],
       via: fact|retrieval|null, latency_ms, cost_usd, limited?, message? }
events (stream: true):
  memory.thinking { id, stage: understanding|searching|reading|checking }
  memory.answered { id, abstained, limited }

A surface makes its own id (letters, digits, _, -), sends stream: true, and shows a thinking state from the first memory.thinking event. The events never carry the question or the answer. A personal fact answers in under 50 ms with no model. Otherwise the fast model reads 8 passages: about 3.8 s at p50 through claude -p today (measured on the open world), and under 3 s once sessions' warm quick session lands (work/sessions, not on main yet).

How it looks is app-design's: an "Answer" variant of the result card (docs/design/system/components/result-card), confirmed 28 Sep. It uses the result card's shell, with no header row and never a primary action. Sources are source chips (a 1px strong rule, no fill, a mono 12 meta line). Confidence is plain text in the meta line, never a colour. The abstain and limited states use the same shell and padding, never a red or amber treatment. Above the search hits there is a 12 px gap (space-3), full width. The shape every surface shares:

  1. The answer, one or two sentences, in the user's words for names and files.
  2. One line under it: how sure ("confidence 0.8") and where from ("from 2 sessions", or "from what you have said" for a personal fact).
  3. Up to three sources: the session's name, when, and the quoted words. A tap opens that turn (recall.thread at seq). "More" shows the rest.
  4. Abstained: "Not sure yet." then "What memory does know:" and the known lines, if any. Never an empty card.
  5. Limited: the message as it came, with its link to Settings.

The stream cutover (0.1.1)

The lead's decision (28 Sep, docs/design/interaction.md): memory.ask { stream: true } is the default wherever it is called, from one shared cutover, coordinated here. No surface builds an interim path.

  • The contract is final as on work/memory-iq: memory.ask { question, stream: true, id, context?, screen? }. The surface makes the id, shows a thinking state from the first memory.thinking { id, stage }, and draws the reply: { id, answer, answer_id, confidence, abstained, known, sources: [{ session, seq, role, name, quote, ts }], via, limited?, message? }. memory.answered { id, abstained, limited } closes it. Events carry no question or answer.
  • Cutover: 30 Sep 2026, in the integrator's first 0.1.1 batch. memory-iq's 0.1.1 sha lands first in that batch; the Capsule (capsule-pro), the Deck and PWA card (chat-core), and the phone's Find card (mobile) land in the same batch, built against work/memory-iq until then. A surface that meets an older vyred (no id in the reply) draws the reply without the thinking stages.
  • Owners: capsule-pro (the Capsule), chat-core (Deck chat and PWA, paused: this spec), the Deck and mobile (Find cards, paused: this spec), app-design (the Answer card, interaction.md section 6). The correction gesture ("Wrong?") ships on each surface as it relaunches.

Surface by surface

The Capsule

  • Today. The Capsule calls memory.relevant and recall.search itself (local/capsule/native/Sources/Host/CapsuleModel.swift:729-762), then starts a lean session whose system prompt is composeIq (core/sessions/iq-prompt.js) over those facts. Said.swift keeps its own question rules and shows memory.answer's sureness. Nothing checks the answer against its sources, session transcripts are only raw hits, and there is no "what memory does know".
  • Should. When the box decides the words are a question (cohesion item 5), the Capsule calls memory.ask { question, stream: true, id, context: { project } } and draws the result card above. "Ask Claude instead" stays as the second action, for a question memory cannot answer. Said.swift and its rules go.
  • Gap. The largest one: the Capsule is where the user asks most, and it runs a second answer path. Owner: capsule-pro (active), with sessions for retiring the capsule purpose's IQ prompt. memory-iq's part (streaming, the id, the cap message) is built.

Chat

  • Today. The per-prompt hook (harness/hooks/hook.js, harness.enrich) adds up to five graph facts from memory.relevant to each prompt, marked as memory with their source. The assistant can call memory_ask over MCP (every module tool is offered), but nothing told it when, so it said "I don't remember" about things memory holds.
  • Should.
    1. memory.ask's description now tells the assistant to ask it before saying it cannot remember something from earlier sessions, and to name the session it cites (built).
    2. about.md (core/about) gains one line: "For a question about your past work or life, call memory_ask and name the session it cites." Static, so it costs nothing per prompt.
    3. The hook, for the user's own interactive session only (not an agent, not a project-scoped session), adds a personal fact when the prompt asks one (memory.answer's rules, no model, under 50 ms): "what's my wife's name, and book us a table" arrives with "Your wife is Juno (from what you have said)". Never a model call per prompt.
    4. When the assistant cites memory in its answer, the Deck's chat renders "(from <session>)" as a link to that turn.
  • Gap. 1 is built. 2 and 3 are small and belong to the harness (cc-plugin). 4 belongs to chat. A module caller (module:harness) passes memory's personal check, so the hook must decide scope itself: it sends agent or project_cwds whenever the session is not the user's own.

The Deck (Find, Memory view, Now)

  • Today. Find (deck/views/find.js:595-597) shows recall.search hits and memory.relevant facts: search results, never an answer. The Memory view browses and edits the graph (memory.graph, memory.facts, memory.why). Now shows what memory learned today. The Ask view uses memory.relevant for hints while typing.
  • Should. Find shows an IQ card on top when the words are a question (they end in "?" or start with who, what, when, where, which, why, how, did, do, is, was), asked on Enter or after 700 ms without typing, never per key. The search hits stay below it, unchanged. The Memory view gets the same card from its search box, and each source links to the turn. Now needs nothing from IQ.
  • Gap. Medium: one card component, one call, debounce. Owner: the Deck (app-design has the result card). Built once in chat-core so the PWA gets it too.

The phone (Find)

  • Today. iOS FindView.swift:607-621 calls recall.search, then memory.relevant with a memory.facts fallback. Android Find.kt follows the same grammar. Search results only.
  • Should. The same IQ card as the Deck, same question test, same debounce, called over the tailnet as the owner (tailnet:<login> passes memory's personal check). Events may not reach the phone, so it shows a plain thinking state until the reply.
  • Gap. Medium, twice (iOS and Android). Owner: mobile.

The CLI (recall, memory, why)

  • Today. vyre memory ask called memory.answer: personal facts only, no sessions. vyre recall searches turns. vyre why shows where a fact came from.
  • Should. vyre memory ask is IQ: the answer, "confidence 0.8 · from 2 sessions", three sources (--sources for all), "not sure yet" with what memory knows, the cap message, exit 1 when there is no answer, and --json for scripts.
  • Gap. Built (this branch). A vyred without memory.ask still answers from personal facts.

Suggest and predictive text

  • Today. suggest (core/suggest) ranks agents, projects, threads, accounts and times per key. memory.suggest existed but nothing asked it.
  • Should. The people and things memory knows complete as the user types: "my wi" offers "wife" with "Juno" beside it, "Jun" offers "Juno". Personal names only on the user's own surfaces. IQ answers are never suggestions: they come on pause, through the card.
  • Gap. Built: memory offers memory.suggest to suggest at start and again on the new suggest.ready event, in the offer's items shape. One line in core/suggest (the event), listed in memory-iq's changed contracts.

The assistant's own system prompt

  • Today. about.md (core/about/index.js) holds up to 600 characters of memory.profile lines (work, places, preferences) and the agents and projects, read at SessionStart. No personal people facts, and nothing says memory can be asked.
  • Should. The same, plus the one line about memory_ask (see Chat). People facts stay out of about.md: they enter a prompt only when the prompt asks about them.
  • Gap. Small. Owner: cc-plugin (core/about).

Teammates' notes

  • Today. A teammate's notes file is its own memory of record and is deliberately not fed into memory (ADR 0031). Corrections go through learn.add.
  • Should. Notes stay out of personal facts: they are an agent's words, and source trust says so. A teammate asks project history with memory_ask, scoped to its projects by the guard, so it never reads personal facts or another client's sessions. Its brief gains one line saying so.
  • Gap. Small. Owner: teammates.

Project graphs: a session that joins a project later

Cohesion writes the product spec. This is what IQ needs from it and what it costs.

  • Today. Memory's rooms already follow projects: project.created, project.changed, thread.picked and thread.unpicked mark the rooms stale, and the next pass files a picked session's facts in that project's room (the ids come from projects.list picks). Personal facts never move: they are the user's, not a project's.
  • Gap. IQ's retrieval scopes a project by its folders. recall.search has project_cwds and no session filter, so a session picked into a project from outside its folders is in the project's graph but not in the passages IQ reads for that project, and a project-scoped agent cannot read it.
  • Should. recall.search takes sessions: string[] next to project_cwds, and a turn matches if either holds. memory.retrieve and memory.ask scope a project by its folders plus its picked session ids. A session unpicked leaves at the next pass. An agent's grants follow the same union.
  • Built (398f6156). recall.search { sessions } keeps those sessions as well as the folders' (keywords and meaning both); only modules and the person's surfaces may name sessions, so a model cannot widen its scope. memory.retrieve and memory.ask scope a project by its folders plus its picked threads. Cohesion's spec decides the user-facing words.

Host to server: the Mac's sessions build the box's graph

Federation owns the transport (its plan: "3. Session sync") and memory-iq the indexing. Agreed with federation on 28 Sep. ADR 0008 is amended (approved by the user: a per-device switch, off by default, in the settings hub, turned on by a person). e2e approved the direction with conditions, now in ADR 0008: the box names the machine from the verified peer and keeps its own copy of the switch; UUID file names, temp and rename, no symlinks, a quota; trust is the lower of the folder rule and mac-sync; the secret scrub runs at ingest; every derived row carries its machine so the delete is complete; unpairing deletes too. It is 0.2 work.

  • Consent. One switch per Mac in the settings hub (ADR 0035), off by default, turned on only by a person. Vyre's own folders, <home>/quick and folders the person excludes never leave the Mac. Turning it off, unpairing, or replacing the device deletes nothing: what came from it is the person's (ADR 0008, amendment item 5). "Delete everything that came from <device>" is the person's own action: federation deletes <home>/synced/<machine>/ and emits sync.deleted { machine }, and memory drops that machine's Recall rows and everything derived, and says how much went (memory.forgot); memory.device is the preview.
  • What moves. Raw session files (JSONL), encrypted in transit over the tailnet, into <home>/synced/<machine>/ in the Mac's own projects layout. The file's own folder (cwd) is kept as it was: source trust keys off it, never off the synced path.
  • Labels. A synced session is source: "mac-sync" with its machine; the live federated read stays source: "mac" (ADR 0021). When both exist for one session id, the catalogue shows the live one; IQ and the graph always read the resident copy.
  • Dedupe. One file per session id, replaced whole when it changes. A shrunk or compacted file is a rewrite: Recall sees fewer turns and memory reads that session again, as today.
  • What is indexed where. Recall on the box adds the synced root to its transcript folders: keywords and embeddings (box CPU), the graph (rules) and personal facts (the model reader) all build there. When a Mac syncs, that Mac's own model reader turns off (it reads the hub setting), so no turn is paid for twice; the Mac keeps its own index for offline search, and its surfaces ask the box's memory.ask, falling back to the Mac's own.
  • Load. Batches only while the Mac is idle, never more often than every 60 s.
  • Cost. Transport and storage: text, tens of MB for a year of sessions. Embeddings and the graph: box CPU, no money. The model reader: a one-time backfill of about $1.50 to $2.60 in reported usage, then at most $0.25 a day, on the box's Claude login (never API dollars since ec097430). With no login on the box, reading waits and nothing else breaks.
  • Size. Federation L (the long pole: upload service, consent, delete). memory-iq M (synced root in Recall, labels, delete by machine, reader location, a two-machine eval fixture).

Correct IQ where it appears

The user: "Make it so IQ can be updated where it appears, so you can update it and it remembers that for the future and improves." Built on work/memory-iq (95b2b891).

  • One tool, extended. memory.correct { answer: <answer_id>, action, object? }. Every memory.ask reply, a "not sure" too, carries answer_id (the same answer to the same question has the same id). The person's own surfaces (Deck, CLI, Capsule, local), or their phone or paired device with a person session (a passkey, ADR 0032), with no Touch ID (the no-nag rule); agents never. Graph corrections follow the same rule (e2e, 28 Sep).
    • replace with object (the right answer, the person's words): the same question gets it at once (via: "corrected", source "your correction"), no model call. For a personal answer the text is also told to memory (memory.remember, highest trust), so "who is my wife" has it too.
    • wrong: that answer is never given to that question again; IQ says "You said ... is wrong." and tries again when memory learns something new.
    • forget: the personal facts behind it stop counting, and the turns it cited never ground an answer again.
    • memory.uncorrect { fix } undoes any of them, told note included.
  • It remembers. A correction is the person's own words: it outranks every other source, keeps its history, and takes effect on the next ask (the correction is read before any kept reply).
  • It improves. The fixes are the person's local log, in their own vyred's database, never in the repo or an eval: memory.corrections { answers: true } and memory.stats().iq give "you corrected 3 answers this week" by kind of question (people, who, date, file, decision, bug, config, personal). eval-iq --fix applies a person's correction to every wrong answer on the synthetic worlds and asks again: open 20 of 20 now right, sealed 38 of 38, none regressed.
  • The card (app-design, 28 Sep; folded into result-card's Answer variant after the RC). "Wrong?" is quiet label text at the end of the meta line ("confidence 0.8 · from 2 sessions · Wrong?"), underlined on hover, never a chip or a button. A tap grows the row in place, never a sheet or a modal: two small ghost buttons ("That's wrong", "Forget this"), then a one-line text field prefilled with the answer. Enter sends the edit, Esc collapses with nothing sent, nothing fires on blur. A "not sure" card has no "Wrong?": the field is always open under it, empty, with the placeholder "Know it? Tell me". After a fix the answer changes in place, and the meta line reads "you corrected this · Undo", with no colour. Undo stays for as long as the card is on screen. A corrected reply has known: [], and its one source ("your correction") is not drawn as a chip. On the phone it is the same inside the Find row; buttons stack only when both do not fit at 44 pt.
  • Specs for the paused owners.
    • chat (Deck chat and the PWA through chat-core): the same three choices on an IQ card inside a conversation, calling memory.correct { answer }; "Undo" calls memory.uncorrect { fix }.
    • pwa: nothing of its own beyond chat-core's card.
    • mobile: Find's card gets the same line and choices. Without a person session the reply is person_session_required: offer the passkey sign-in, then send again.

An agent corrects memory from the person's own words

The user: when he tells an agent in chat "no, my wife is Juno", the agent must be able to fix memory. Built on work/memory-iq (core/memory/iq/heard.js), 0.1.1, for e2e's review.

  • Evidence or nothing. An agent (a model's caller: mcp, mcp:thread:, mcp:agent:) calls memory.correct as the person would, plus from_turn: { seq }: the person's turn that says it. The thread is the one vyred verified for the call (meta.thread, from the session key or the agent key); a thread named in the input is ignored. Memory asks the switchboard who wrote that turn (threads.said, below), and applies only when all of these hold (e2e's review, 28 Sep):
    • the switchboard says role: "user" and by: "person";
    • the turn is fresh: one of the person's latest three in the thread (back 0 to 2) and at most 10 minutes old, never history;
    • the turn names what is corrected: a word of the fact's subject or relation, or of the IQ question;
    • the correction is in the person's words, injected blocks removed: every name, number or content word of the new value (replace, add), or, for wrong, forget and ended, a "no" within six words of the old value's words ("I'm not sure" is not "wrong");
    • that turn has not made a correction already, and the thread has made fewer than 3 this hour.
  • Applied as the person's. It goes through the same path as a correction from the Deck: highest trust, told to memory for a personal answer, attributed heard:<thread>#<seq>, undoable with memory.uncorrect. It emits memory.updated { by: "agent", thread, fix | correction } so the person's surfaces show "Memory updated · Undo". No Touch ID and no confirm: they said it.
  • Everything else waits. No verified thread, no from_turn, a turn that is not the person's (tool output, a web page, a file, a launch brief, another agent), a stale turn, a turn that names something else, a value not in their words (a paraphrase), a project-scoped agent, or an action only the person takes (confirm, merge, split): kept as a suggestion (memory_iq_suggested), never applied. The agent gets { applied: false, suggestion: { id, why } }. The same suggestion again is the same row. At most 5 wait per thread and 50 in all; past that nothing more is kept (dropped: true), so an agent in a loop never fills "waiting on you". A suggestion expires after 14 days, or at once when what it targets changes before the person decides; expired ones stay listed and never apply. The person lists them (every field plain text) with memory.corrections { suggested: true }, accepts with memory.correct { suggestion } and dismisses with memory.uncorrect { suggestion }; an agent can do neither. The same listing's heard rows show what agents applied from the person's words this week ("what is my wife's name: Your wife is Juno"), each with its undo, for a "Kit changed ... · Undo" row on every surface.
  • Tests (core/memory/iq/heard.test.js): a tool result saying "the user's wife is X", another agent quoting a web page, a launch brief, an injected system reminder, pasted content, a paraphrase, another thread's turn, no thread, no from_turn and a project-scoped agent are all kept as suggestions; the person's own turn is applied and answers at once; undo restores; an agent cannot accept a suggestion.
  • Needs from the switchboard (sessions): threads.said { thread, seq }, internal (modules only), -> { role: "user"|"assistant"|"tool", by: "person"|"agent"|"module"|"program", text, ts, back }, from its own records, never from the caller. back counts the person's turns after this one in the thread (0 for their latest). by: "person" only for a prompt the person typed: in their own interactive terminal session (not claude -p), or sent with threads.send by a person caller after the daemon's downgrade (cli, local, deck, capsule, or tailnet or device with a person session). Never by: "person", whatever the transcript says (e2e's list, 28 Sep):
    • tool_result entries;
    • isMeta and isCompactSummary entries;
    • isSidechain turns (a subagent's prompt is written by the parent model);
    • slash-command expansions, skill and hook text (UserPromptSubmit context, system reminders);
    • ! shell output and <local-command-stdout>;
    • inbox deliveries: threads.post (teammates' results), threads.notice, and any queued row whose kind is not a person's send;
    • a launch brief, and threads.send from an agent or a module. When in doubt, it is not the person. This list goes into its doc and its tests. Until it exists, every agent correction waits as a suggestion, which is safe.
  • Needs from waiting (cohesion): a fifth source, memory (kind memory), read from memory.corrections { suggested: true }, refreshed on memory.suggested. The title stays generic ("An agent suggests a memory update"), never the value: it reaches the lock screen.

Ranked

Value is how often the user meets it and how wrong things are today. Cost is the work to build and test it.

RankGapOwnerCostValueState
1The Capsule answers through memory.ask, streamed; Said.swift goescapsule-pro, sessionsMhighestmemory-iq side built; spec sent
2The assistant knows to ask memory (tool description)memory-iqShighbuilt
3about.md line: ask memory_ask, name the sessioncc-pluginShighspec sent
4vyre memory ask is IQmemory-iqSmediumbuilt
5Predictive text knows memory's namesmemory-iq (+1 line in suggest)Smediumbuilt
6Chat hook adds a personal fact when asked, no modelcc-plugin (harness)Smediumspec sent (0.1.1)
7Deck Find and Memory view IQ carddeck, app-designMhighspec sent (0.1.1)
8Phone Find IQ cardmobileMmediumspec sent (0.1.1)
9Chat renders "(from <session>)" as a linkchatSmediumspec sent (0.1.1)
10Teammates' brief: memory_ask for project historyteammatesSlowbuilt (work/teammates 7eb7ffb8; reaches teammates with core/team step 3)
11Project graphs: IQ reads a project's picked sessions, not only its foldersmemory-iq, recallShighbuilt (398f6156)
15Sessions start knowing the project (memory.today in the brief)memory-iq, harnessSmediumbuilt (df22ca0c)
12Mac sessions build the box's graph (consent, dedupe, one reader)federation, memory-iqL+Mhighest for a box usercontract agreed; ADR 0008 amended; 0.2
14An agent corrects from the person's own words; anything else waitsmemory-iq; threads.said: sessions; waiting: cohesionS+S+Shighmemory side built; e2e review asked
13Correct IQ where it appears, rememberedmemory-iq; card: capsule-pro, app-design, chat, mobileS+Mhightool, CLI and eval built; card specs sent

What makes IQ smarter, whatever the surface

These are memory-iq's own, measured on the synthetic worlds before they ship:

  • Prompt v2 (each passage names its project; answer exactly what was asked). Open world: accuracy 0.778 and 1 confident-wrong, against v1 re-recorded the same day at 0.767 and 2. One question apart on 90 is within the model's run-to-run spread, so it is not a measured win yet.
  • The second look for answers about people (ADR 0034 phase 4).
  • Latency. 3.8 s p50 through claude -p. The warm quick session in work/sessions is the fix.
  • "Before" for places: a past place is answered with the current place's confidence (sealed place-history).

Specs for the owners

Each owner builds against memory.ask as it is on work/memory-iq (streaming and id from 6adfc4b6). Tests use the fixtures and temp homes only; under node --test memory has no model, so a question that no personal fact answers comes back abstained, which is the state to test.

  • capsule-pro. On a question, call memory.ask { question, stream: true, id: "cap_<n>", context: { project } }. Show the thinking state from the first memory.thinking with that id, the stage as a word ("searching", "reading"). Draw the result card. A source tap opens the turn. Abstained: "Not sure yet." plus known, then "Ask Claude instead". limited: show message. Remove Said.swift's question rules and the memory.relevant plus composeIq path. Where the box has no memory.ask (no_such_tool), keep today's path.
  • sessions. Once the Capsule has moved, the capsule purpose's IQ prompt (core/sessions/iq-prompt.js) is used only by old Capsules; retire it a release later. Keep the quick session for purpose memory warm: it takes IQ from 3.8 s to under 3 s.
  • cc-plugin (paused). core/about: add "For a question about your past work or life, call memory_ask and name the session it cites." to about.md, inside the 600-character budget. harness.enrich: when the session is the user's own and interactive (no agent, all projects), and memory.answer { q: prompt } returns kind: "fact" at confidence 0.5 or more, add its answer as one memory line "(from what you have said)". Otherwise nothing new. No model call per prompt.
  • Deck and app-design. Find: the IQ card above the hits for question-shaped words, on Enter or after 700 ms still, one call per settled question, the previous one's events ignored by id. Memory view: the same card from its search box. Put the card in chat-core so the PWA gets it.
  • mobile (paused). iOS FindView and Android Find: the same card over the tailnet, a plain thinking state (no events), the same question test and debounce.
  • chat (paused). Render "(from <session name>)" in an assistant message as a link to that session when the session exists in recall.
  • teammates. One line in a teammate's brief: "For what was decided or done before in your projects, call memory_ask; it sees only your projects." Notes stay out of memory.