Skip to content

Agent guide

Operational guide for AI agents (Claude Code, Cursor, OpenCode, Codex, Antigravity, custom MCP clients, Go callers) using GrayMatter as long-term memory.


GrayMatter is your long-term memory. Unlike conversation context, which disappears at the end of a session, GrayMatter facts persist across sessions, projects, and agent restarts. Use it to accumulate knowledge that makes you more effective over time.

Key principle: Store conclusions, not conversations. A good memory is something you would want injected into your system prompt on day 1 of a new session.


Seven tools are registered by graymatter mcp serve (see cmd/graymatter/internal/mcp/server.go). Parameter names are not uniform — check the table before calling.

Tool Required params Optional params Returns
memory_search agent_id (string), query (string) top_k (int, default 8), explain (boolean, default false), min_confidence, confidence_weight Numbered fact list with a count header (deduped), or a “No memories found” notice
memory_search_batch agent_id (string), queries (string array) top_k (int, default 8), min_confidence, confidence_weight Merged deduped block plus per-query lists
memory_add agent_id (string), text (string) confidence Confirmation string; an explicit label includes the committed fact ID and label
memory_alias agent_id (string), term (string), equivalents (string array) — Confirmation naming the term and its equivalents
checkpoint_save agent_id (string) state (JSON-encoded string) Confirmation containing the checkpoint ID
checkpoint_resume agent_id (string) on_missing ("error" | "empty", default "error") Checkpoint "id" restored + Created: (RFC3339) + indented State: JSON; by default an error result when none exists — with on_missing: "empty" a successful {"found": false, "agent_id"} result
memory_reflect action (add|update|forget|link|pin|unpin), plus at least one explicit valid agent_id (canonical) or agent (deprecated alias; agent_id wins when both are valid) text (string), target (string — old fact text for update/forget/pin/unpin; target node ID for link) Confirmation string

ℹ️ memory_reflect accepts both agent_id (canonical) and agent (deprecated alias). At least one explicit valid identity is required at runtime, including over stdio. Its flat input schema requires only action to preserve alias-only clients; it has no root combinator. When both fields are valid, agent_id wins (ADR-014). New integrations spell it agent_id, matching every other tool.

memory_reflect additionally accepts confidence on add and update.

The options below are available since v0.20.0 and absent from v0.19.1. Product and low-level confidence weights remain zero by default. A positive product default requires explicit notice in a published minor release, followed by promotion in a later minor release after successful frozen quality gates. v0.20.0 does not announce a positive default.

Labels are exact strings: unverified < inferred < verified. They describe the writer’s stance; GrayMatter does not establish truth or assign probabilities. An old fact with an absent/empty label is effectively inferred. An unknown historical label is preserved in provenance and treated as unverified for filtering and scoring. New interfaces reject explicit null, empty/unknown labels, surrounding whitespace and incorrect types before effects.

Operation Label omitted Label supplied
memory_add, reflect add, CLI remember Keep legacy empty storage, effectively inferred Commit the validated label with the exact new fact
reflect update, CLI revise Minimum of inferred and every validated live target’s effective confidence Commit the requested label on the exact replacement
Generated consolidation summary Minimum of inferred and the valid consumed sources No automatic promotion
RememberExtracted LLM result Unverified, even if the extracted text equals the input Original-text fallback remains ordinary legacy storage
forget, pin, unpin, link Preserve labels confidence is invalid on these actions

Revision commits the replacement’s text and metadata atomically, then retires the targets. A changed source is revalidated before the replacement commit. The workflow is not one transaction: a later retirement failure is reported and can leave the committed replacement alongside a still-live target. A failed replacement write leaves the targets live. No pin is inherited. Pin/unpin, decay, access bookkeeping, export and vector reconciliation preserve confidence.

Search and batch accept global min_confidence and confidence_weight options:

  • Omitted minimum includes every live content fact. unverified includes all effective categories, inferred includes inferred/legacy and verified, and verified includes verified only. Aliases and tombstones never rank.
  • Eligibility is applied before IDF, signal ranks, recency, relevance floor, deduplication and top-k, in both scan and indexed retrieval. Historical tombstones remain available for explain lineage.
  • A finite weight in [0, 0.5] applies final = B * (1 + weight * c), with c = +1 for verified, 0 for inferred/legacy and -1 for unverified/unknown. B is the existing three-signal RRF score with k = 60. Zero retains legacy ranking and does not disable a filter. A label cannot create a positive score from zero or guarantee priority over every more relevant fact.
  • Final score determines ordering and MinRelevance: descending score, then CreatedAt ascending, then ID ascending. Text duplicates use the selected eligible ID’s receipt. Shared/all and batch preserve their existing fusion policies after each input query applies its options.
  • Any explicit minimum suppresses graph neighbor labels, including unverified. Those labels lack confidence receipts. Without a filter, up to three existing graph hints may be appended; explain covers ranked facts only. Weak-match vocabulary uses the eligible corpus, and filtered calls do not learn aliases.

Options or a positive configured default add retrieval metadata to successful results, including empty ones, and the same effective policy in text. Explain keeps ranks.fused_score as B; its optional ranking object reports base and final scores, factor, effective confidence, weight and confidence-v1 policy. Original confidence remains in provenance.

Filtered vector retrieval reuses one query embedding. The native backend intersects eligible IDs before truncation. Custom backends must implement EligibleVectorStore or ExhaustiveVectorStore; the latter expands a stable prefix until 2 * topK eligible neighbors or explicit exhaustion. A short response alone is not exhaustion. Unsupported/incomplete search, no progress, errors and cancellation fail explicitly rather than returning a partial success. Keyword-only retrieval requires no vector capability. Concurrent updates must not mix eligibility with a different confidence in the receipt. Once a query embedding exists, an overlapping alias edit that changes the effective query fails explicitly and can be retried; it does not generate a second embedding.

New RPC methods negotiate versioned confidence capabilities. A legacy daemon may serve an effectively legacy request; explicit new options require an update/restart error when unsupported. An uncertain write is never replayed automatically. Older RPC endpoints and the REST API retain zero preference. Authenticated MCP over HTTP supports the same options as stdio.

{ "tool": "memory_add", "args": {
"agent_id": "backend", "text": "The reviewed archive retention is 30 days",
"confidence": "verified"
}}
{ "tool": "memory_search", "args": {
"agent_id": "backend", "query": "archive retention", "explain": true,
"min_confidence": "inferred", "confidence_weight": 0.2
}}
{ "tool": "memory_search_batch", "args": {
"agent_id": "backend", "queries": ["archive retention", "routing cutoff"],
"min_confidence": "verified", "confidence_weight": 0
}}

CLI equivalents are remember/revise --confidence and recall --min-confidence/--confidence-weight. Options apply to --shared, --all (agent plus shared, merged) and repeated --query. Explain supports one agent-scoped query; combining it with --shared, --all or batch is rejected. --shared and --all remain mutually exclusive. There is no confidence option on forget/pin/unpin/link, and a search option used on writes is rejected.

Claude Code hooks and MCP are complementary. When a hook actually injects recalled facts, the block begins with a bracketed marker that names the hook’s real agent_id.

Before the first substantive reply, inspect only the newest hook block available for the session’s initial turn; quoted examples and blocks from older turns do not count. Reuse its non-empty sections only when the marker’s id equals the agent_id you intended to search. If the ids differ, run both project and __shared__ searches: cross-namespace deduplication may have placed a shared duplicate under ## Memory. With matching ids, run only the search for each missing section. Continue to use MCP for checkpoint_resume, focused or batch lookups, writes, corrections, aliases, and checkpoint saves. An empty, failed, absent, or throttled hook emits no fresh block, so ordinary MCP startup searches remain the fallback.

Every success result carries both a structuredContent object (declared in the tool’s outputSchema) and the same human-readable text in content — machine-readable and text-parsing clients are both first-class (ADR-013).

// memory_search — content (text)
"Found 3 relevant memories for agent \"frontend-agent\":\n\n1. User prefers TypeScript with strict mode\n2. Project uses pnpm, not npm\n3. Auth tokens live in HttpOnly cookies"
// memory_search — structuredContent
{ "agent_id": "frontend-agent", "query": "css tooling", "count": 3, "facts": ["User prefers TypeScript with strict mode", "Project uses pnpm, not npm", "Auth tokens live in HttpOnly cookies"] }
// empty state: count 0, facts [] — plus the "No memories found ..." notice as text
// memory_add — structuredContent
{ "agent_id": "frontend-agent", "stored": true }
// checkpoint_save — structuredContent
{ "agent_id": "migration-agent", "checkpoint_id": "01JZK7...", "created_at": "2026-04-28T13:42:11Z" }
// checkpoint_resume — content (text)
"Checkpoint \"01JZK7...\" restored for agent \"migration-agent\".\nCreated: 2026-04-28T13:42:11Z\nState:\n{\n \"task\": \"db migration\",\n \"step\": 3\n}\n"
// checkpoint_resume — structuredContent (state/message_count omitted when empty)
{ "id": "01JZK7...", "created_at": "2026-04-28T13:42:11Z", "state": { "task": "db migration", "step": 3 } }
// checkpoint_resume with no checkpoint — isError=true, text content only:
"no checkpoint found for agent \"migration-agent\": no checkpoints for agent \"migration-agent\""
// (content keeps the historical "no checkpoint found for agent ..." prose)
// checkpoint_resume with no checkpoint and on_missing="empty" — success:
{ "found": false, "agent_id": "migration-agent" }
// text: "No checkpoint saved for agent \"migration-agent\" yet."
// Without on_missing the default "error" keeps the historical isError result
// above. A possible v0.21.0 default of "empty" depends on real-client validation
// and the published compatibility notice. Storage and daemon failures stay
// prose-only errors in both modes.
// memory_reflect — structuredContent
{ "action": "update", "agent": "backend-agent", "ok": true }

  • User preferences — coding style, communication preferences, tool choices
  • Project conventions — “this repo uses tabs not spaces”, “never use X library”
  • Architecture decisions — “chose PostgreSQL over MySQL because…”
  • Bug fixes & workarounds — “fixed by upgrading to v2.3, don’t downgrade”
  • Recurring patterns — “user always asks for TypeScript examples first”
  • Environment quirks — “needs NODE_OPTIONS=--max-old-space-size=4096”
  • Stakeholder info — “CTO prefers detailed explanations, CEO wants summaries”
  • Conversation logs — raw back-and-forth without conclusions
  • Duplicate information — already in README, AGENTS.md, or code comments
  • Speculative thoughts — “maybe we should try X” (store after the decision)
  • Secrets or credentials — use proper secret management
  • Large outputs — store the insight, not the 500-line stack trace

(Transient session state goes in a checkpoint, not a memory — see Anti-Pattern §5.)

About to store something?
├── Is it a conclusion / fact / preference? → YES, store it
├── Is it raw conversation without insight? → NO, extract insight first
├── Is it already documented in code/README? → NO, reference docs instead
├── Will this still matter in 10 sessions? → YES, store it
├── Is it temporary debugging state? → NO, use checkpoint
└── Is it a secret / credential? → NO, never store in memory

Use when you have a single, atomic, well-formed fact.

Good:

{ "tool": "memory_add", "args": {
"agent_id": "frontend-agent",
"text": "User prefers Tailwind CSS over styled-components"
}}
{ "tool": "memory_add", "args": {
"agent_id": "backend-agent",
"text": "API rate limit: 100 req/min — exceeded returns 429 with Retry-After header"
}}

Bad:

// Too vague
{ "agent_id": "agent", "text": "user likes things" }
// Conversation log
{ "agent_id": "agent", "text": "User: Can you help? Agent: Sure, what do you need?" }
// Duplicate (already in README)
{ "agent_id": "agent", "text": "Project uses React" }

memory_search — retrieve relevant context

Section titled “memory_search — retrieve relevant context”

Always search before acting on ambiguous requests. Phrase the query as the task you’re trying to do, not as keywords.

Good queries:

{ "agent_id": "frontend-agent",
"query": "how should I style this component",
"top_k": 5 }
{ "agent_id": "backend-agent",
"query": "authentication middleware patterns for this project",
"top_k": 8 }

How retrieval works

GrayMatter ranks facts via Reciprocal Rank Fusion (RRF) over three independent signals (see pkg/memory/recall.go:14):

  1. Vector similarity (cosine, pluggable VectorStore) — when embeddings are available
  2. Keyword relevance (TF-IDF approximation over bbolt facts)
  3. Recency (exponential decay from CreatedAt)

Each signal produces an independent ranking; RRF fuses the ranks (not the scores) into a single base score. Optional confidence preference adjusts that score as described above. Returns top-K, deduplicated by text. One batched transaction updates only returned facts’ access metadata (AccessCount++, AccessedAt = now).

Facts marked superseded are dropped before any of this — a fact an agent has corrected or forgotten never competes for a slot. Graph neighbours of the top hit would also be appended, but nothing wires the graph into the store in shipped builds, so in practice that step never runs (ADR-003).

RRF means rank position matters, not raw scores — a fact’s contribution depends on where it placed in each ranking, not on how close the numbers were. confidence_weight is an optional bounded adjustment after this fusion; it does not alter the three signal weights.

A Go caller configuring the store does. StoreConfig.SignalWeights sets how much each signal contributes (default vector 1.0, keyword 1.0, recency 0.5) and MinRelevance drops results below a fraction of the best score in the same result set. Both default to the behaviour described here. See ADR-006.

Query strategies:

// Strategy 1: Broad context gathering at session start
{ "agent_id": "agent", "query": "<current task description>", "top_k": 8 }
// Strategy 2: Focused lookup mid-task
{ "agent_id": "agent", "query": "<specific question>", "top_k": 3 }
// Strategy 3: Several open questions at once — one batch call
{ "agent_id": "agent", "queries": ["<question 1>", "<question 2>", "<question 3>"], "top_k": 3 }

The most powerful tool. Use it to maintain memory quality over time.

Parameter is agent_id (canonical, since ADR-014). agent remains accepted as a deprecated alias. Supply at least one explicitly; the server never derives it from the working directory. When both fields are valid, agent_id wins. New integrations spell it agent_id.

Each supplied identity field must be a non-empty string with at least one non-whitespace character. Null, incorrect types, empty strings, or whitespace-only strings cause a tool error before any backend call, even if the other field is valid. Omit an unused spelling instead of sending it empty. Valid values are used unchanged: " project " and "project" are distinct namespaces, and Unicode and __shared__ work normally. A call with neither identity may pass the flat input schema, but the server rejects it at runtime.

Action Param meaning of text Param meaning of target
add The new fact (required) (unused)
update The corrected fact (required) The old fact text to supersede (required)
forget The fact to remove (alternative to target) The fact to remove (wins when both are set)
link Source node ID (required) Target node ID in the knowledge graph (required)
pin The fact to pin (alternative to target) The fact to pin (wins when both are set)
unpin The fact to unpin (alternative to target) The fact to unpin (wins when both are set)

Pin/unpin: a pinned fact is exempt from decay, pruning and summarisation (ADR-010) — use it when the user declares something permanent: a standing obligation, an architecture decision, a security policy. Pins are visible (star in the TUI, counted by status, flagged in exports). Unpinning restores normal decay; a fact pinned for a long time inherits the accumulated staleness when unpinned, which is honest rather than silently reset.

Update workflow:

// 1. Find the old fact
{ "tool": "memory_search", "args": {
"agent_id": "backend-agent", "query": "API base URL", "top_k": 3
}}
// 2. Supersede it
{ "tool": "memory_reflect", "args": {
"action": "update",
"agent_id": "backend-agent",
"text": "API base URL is https://api.v2.example.com",
"target": "API base URL is https://api.v1.example.com"
}}

The old fact is tombstoned and stops being recalled from the very next search — not on the next consolidation pass, and not eventually. It is not deleted: it stays visible to graymatter export, the TUI and any List call, with its superseded_by pointing at the fact that replaced it, so the correction can be audited later. Ordinary decay and pruning collect it in due course.

Before v0.10.0 this action set the old fact’s weight to 0 and reported success, and recall does not read weight — so the superseded fact kept coming back alongside its own correction. If you are running an older binary, update does not do what this page says.

Forget workflow:

// Pass the fact in text — or in target; both are accepted.
// If both are set, target wins.
{ "tool": "memory_reflect", "args": {
"action": "forget",
"agent_id": "backend-agent",
"text": "Workaround for Node 14 bug (project now on Node 18)"
}}

Link workflow (knowledge graph):

⚠️ link writes to the knowledge graph, and it does work in shipped builds: both the daemon and the --no-daemon direct store open a real graph and serve the write. It can still fail if the graph cannot be opened, in which case the tool returns knowledge graph not available. Call link opportunistically and degrade gracefully — never make it a hard prerequisite for a workflow.

What does not happen is automatic population: nothing extracts entities from stored facts on its own, so the graph contains exactly what agents put in it by calling link. See ADR-003.

{ "tool": "memory_reflect", "args": {
"action": "link",
"agent_id": "backend-agent",
"text": "depends_on",
"target": "user-database"
}}

checkpoint_save / checkpoint_resume — session continuity

Section titled “checkpoint_save / checkpoint_resume — session continuity”

Use for long-running tasks that might span multiple sessions or be interrupted.

Since v0.20.0, checkpoint_resume accepts on_missing: "empty" to return a successful {"found": false, "agent_id"} result when no checkpoint exists. The default remains "error"; storage, daemon and corrupt-record failures remain errors in either mode. A possible default change to "empty" in v0.21.0 is conditional on real-client validation and the published compatibility notice. Pass on_missing: "error" explicitly if your client depends on absence being an error. See compatibility and rollout.

What checkpoints capture:

  • A JSON object (string-encoded at the MCP layer) — validated on save, rejected otherwise
  • An ID + RFC3339 timestamp

What they DON’T capture:

  • Memory facts (separate system — use memory_add)
  • Filesystem state
  • External-service state

Pattern: task-progress tracking

// Before starting
{ "tool": "checkpoint_save", "args": {
"agent_id": "migration-agent",
"state": "{\"task\":\"db migration\",\"step\":0,\"tables_done\":[]}"
}}
// After each step
{ "tool": "checkpoint_save", "args": {
"agent_id": "migration-agent",
"state": "{\"task\":\"db migration\",\"step\":3,\"tables_done\":[\"users\",\"orders\"]}"
}}
// On session start
{ "tool": "checkpoint_resume", "args": { "agent_id": "migration-agent" } }
// → parse the returned `state` JSON, continue from step

state is a string at the MCP layer — encode/decode JSON yourself. The CLI (graymatter checkpoint resume) does the same.


Before storing, verify the fact:

  • Atomic — one idea per fact, not a paragraph
  • Timeless — still true in 3 months
  • Actionable — helps future-you make better decisions
  • Specific — “prefers tabs”, not “has preferences”
  • Self-contained — readable without conversation context

Facts decay. A fact you never recall will eventually be pruned.

Mechanics (defaults from config.go):

  • Initial weight = 1.0
  • Exponential decay based on time since last access
  • Half-life = 30 days (DecayHalfLife = 720h)
  • Pruned when weight < 0.01
  • Recall resets the decay clock for that fact
  • Consolidation triggers when an agent has ≥ ConsolidateThreshold (default 20) facts; runs async unless AsyncConsolidate = false; up to MaxAsyncConsolidations (default 2) goroutines concurrently

Implications:

// Anti-pattern: store once, never reference → pruned after ~199 days
// (6.64 half-lives to fall below the 0.01 floor, at the default 30-day half-life)
{ "tool": "memory_add", "args": { "agent_id": "agent", "text": "Critical security policy: …" }}
// Then never search for it.
// Better: keep important facts warm by including them in routine context-gathering.
// Best: the user declared it permanent (standing obligation, architecture
// decision)? Pin it — memory_reflect action=pin exempts it from decay,
// pruning and summarisation entirely (ADR-010).
// ```
// { "tool": "memory_reflect", "args": { "action": "pin", "agent_id": "agent",
// "text": "Critical security policy: …" }}
// ```
// A pinned fact never decays and is never pruned or summarised away;
// unpin only restores normal decay — it does not retire the fact. A fact
// that is wrong or superseded is fixed with memory_reflect update (or
// forget); unpin is for a fact that is still true but no longer needs
// permanence.

Every 10–20 sessions, sweep:

Terminal window
# 1. List everything for an agent
graymatter recall <agent_id> "*" --all
# 2. Identify low-quality entries (vague, outdated, duplicate)
# 3. Clean up via memory_reflect (forget / update)

GrayMatter reserves the agent ID __shared__ (the constant SharedAgentID in pkg/memory/store.go:40) for facts every agent in this workspace should see — project conventions, team rules, security policies.

There is no magic routing at the MCP layer. To write or read shared memory, just pass __shared__ as the agent_id parameter exactly like any other agent ID:

// Write a project-wide rule
{ "tool": "memory_add", "args": {
"agent_id": "__shared__",
"text": "Project convention: all timestamps stored as UTC ISO-8601 strings"
}}
// Read it
{ "tool": "memory_search", "args": {
"agent_id": "__shared__",
"query": "timestamp conventions",
"top_k": 5
}}

Per-agent + shared in one shot: issue two calls (one with the agent’s own ID, one with __shared__) and merge the results. The Go library exposes a RecallAll(agentID, query) helper that does this for you (graymatter.go) — there is no MCP equivalent.

Shared-memory best practices:

  • Store project-wide conventions, not agent-specific preferences
  • Prefix shared facts with intent: "Project convention: …", "Team rule: …", "Security policy: …"
  • Keep it small and high-signal (≲ 50 facts)
  • The CLI --shared flag on graymatter remember / graymatter recall writes/reads this namespace directly

// 1. Was I interrupted?
{ "tool": "checkpoint_resume", "args": { "agent_id": "my-agent" } }
// 2. Pull relevant memories for the current task
{ "tool": "memory_search", "args": {
"agent_id": "my-agent",
"query": "<current task description>",
"top_k": 8
}}
// 3. Pull shared context
{ "tool": "memory_search", "args": {
"agent_id": "__shared__",
"query": "<current task description>",
"top_k": 5
}}
// 4. Concatenate into the system prompt and proceed.

After significant interactions, extract atomic conclusions and memory_add them. Don’t store the conversation; store what you learned.

// Agent-A discovers a convention
{ "tool": "memory_add", "args": { "agent_id": "agent-a",
"text": "Use async/await, not callbacks" }}
// Promote it to shared so Agent-B sees it on their next recall
{ "tool": "memory_add", "args": { "agent_id": "__shared__",
"text": "Project convention: use async/await, not callbacks" }}
// Agent-B picks it up via shared search
{ "tool": "memory_search", "args": {
"agent_id": "__shared__",
"query": "async patterns" }}

Every memory operation is also available from the terminal — useful for scripts, CI hooks, and debugging.

MCP tool CLI equivalent
memory_add graymatter remember <agent_id> "<text>" (or --shared for __shared__)
memory_search graymatter recall <agent_id> "<query>" (--all to dump every fact, --shared to query __shared__)
checkpoint_save (library/MCP only — no CLI)
checkpoint_resume graymatter checkpoint resume <agent_id> (lists most recent)
— graymatter checkpoint list <agent_id> (history)
memory_reflect (MCP only — no CLI)

Other useful subcommands:

Command Purpose
graymatter init Wire MCP into Claude Code, Cursor, Codex, OpenCode, Antigravity (see README.md); --kg persists graph auto-population, --hooks installs Claude Code memory hooks
graymatter init --store-only [--dir <path>] Prepare the selected local store directory only: create MEMORY.md if neither it nor a regular gray.db exists; do not wire clients, open the DB, change PATH, or verify runtime health. --quiet suppresses the success message; --json returns a machine-readable result.
graymatter demo Seed a scratch multi-agent store, run consolidation, open the TUI — one command, no keys
graymatter hooks install / uninstall / doctor Manage Claude Code automatic memory hooks (per-turn injection of agent facts + __shared__ conventions, remember: / remember shared: instant-save, /compact survival); every hook failure degrades silently
graymatter recall <agent> "<query>" --explain Receipts per fact: per-signal RRF ranks, fused score, weight, age, provenance (fact_id, written_at) — same JSON shape as the MCP explain payload
graymatter consolidate <agent_id> Run one consolidation cycle through the daemon’s policy
graymatter kg render --out graph.html Self-contained force-graph page (offline, tooltips carry fact-ID receipts); --out graph.dot for Graphviz
graymatter mcp serve Start the MCP server (stdio default, --http 127.0.0.1:8080 for HTTP; the HTTP transport requires a bearer token)
graymatter tui Six-view memory workbench; see controls and scope
graymatter usage Configure, import and inspect quota/cost/context observations; see Usage
graymatter export --format obsidian --out vault/ Dump all memories to a Markdown vault
graymatter run <skill.md> Execute a SKILL.md agent file
graymatter sessions list List managed agent sessions
graymatter plugin {install,list,remove} Manage local plugins

If you’re embedding GrayMatter directly in a Go program (not via MCP), see examples/agent/main.go for the canonical pattern:

  1. graymatter.Open(graymatter.DefaultConfig()) — open the store
  2. mem.Recall(ctx, agentID, query, topK) — pull context before the LLM call
  3. Inject the recalled facts into the system prompt
  4. After the LLM responds, mem.Remember(ctx, agentID, conclusion) (or RememberExtracted to let GrayMatter pull atomic facts via Anthropic Haiku)
  5. defer mem.Close() to flush + release the bbolt lock

For the public API surface and stability promises, see docs/api-stability.md.


GrayMatter persists to bbolt, a single-writer embedded DB. Only one process may hold the write lock at a time. This shows up the moment you run two MCP-aware agents in the same workspace (e.g. Claude Code + OpenCode + the graymatter tui dashboard).

Normal CLI, TUI and MCP processes connect to one store daemon, which owns the write lock. Clients can operate concurrently; ordinary clients auto-start the daemon when necessary. --no-daemon opts into direct access and reintroduces file-lock constraints. See ADR-002.

tui --read-only requires an existing store and connects only to an already running daemon. It blocks mutations for that TUI client without changing other clients’ permissions. Explicit --no-daemon --read-only opens the store directly without creating it; that reader still cannot coexist with a direct writer.

If a lock or connection error occurs, preserve unsaved state and report the specific failure. Do not replay a write whose outcome is unknown.


// BAD
{ "agent_id": "agent", "text": "User said hello" }
{ "agent_id": "agent", "text": "User asked about weather" }
{ "agent_id": "agent", "text": "I responded with the forecast" }
// → 1000 low-signal facts, important ones buried
// GOOD
{ "agent_id": "agent",
"text": "User is planning outdoor event, needs weather updates" }
// BAD: never updating
{ "agent_id": "agent", "text": "User likes X" }
// User changes preference; you keep recalling and acting on the stale fact.
// GOOD: update on change
{ "tool": "memory_reflect", "args": {
"action": "update",
"agent_id": "agent",
"text": "User now prefers Y (changed from X)",
"target": "User likes X" }}
// BAD: no context
{ "agent_id": "agent", "text": "Blue" }
// GOOD: contextual
{ "agent_id": "agent", "text": "User's preferred UI theme: blue" }
// BAD: rotting timestamp & location
{ "agent_id": "agent",
"text": "On 2026-04-15 at 3:42pm, fixed bug in line 47 of auth.js" }
// GOOD: generalised learning
{ "agent_id": "agent",
"text": "auth.js: JWT validation fails when clock skew > 5 minutes" }

5. The Memory Leak (transient state as a fact)

Section titled “5. The Memory Leak (transient state as a fact)”
// BAD
{ "tool": "memory_add", "args": {
"agent_id": "agent",
"text": "Current file being edited: src/components/Button.tsx" }}
// GOOD: that's checkpoint territory
{ "tool": "checkpoint_save", "args": {
"agent_id": "agent",
"state": "{\"current_file\":\"src/components/Button.tsx\"}" }}
// BAD: every agent stores the same convention
{ "agent_id": "agent-a", "text": "Use TypeScript" }
{ "agent_id": "agent-b", "text": "Use TypeScript" }
{ "agent_id": "agent-c", "text": "Use TypeScript" }
// GOOD: write once, all agents see it
{ "agent_id": "__shared__",
"text": "Project convention: use TypeScript" }

memory_reflect link only works when the host has wired a knowledge-graph linker. If your agent loop requires link to function, it’ll break in stock deployments. Treat it as optional enrichment, not infrastructure.


Sessions Full history GrayMatter Savings
1 ~80 ~80 0%
10 ~630 ~550 12%
30 ~1,880 ~550 71%
100 ~6,960 ~670 ~90%

GrayMatter pays off after roughly 10 sessions. For one-shot agents, the overhead may not be worth it. See docs/benchmarks.md for the full methodology.

Operation Typical Notes
memory_add 5–20 ms bbolt write + optional vector upsert
memory_search 10–50 ms Keyword + vector + RRF fusion
checkpoint_save 5–15 ms Single bbolt transaction
checkpoint_resume 5–10 ms Direct key lookup

Safe to call multiple times per turn. No need to batch.

Per fact: text_bytes + (embedding_dim × 4 bytes)
With nomic-embed-text (768-dim): ~3 KB / fact
1000 facts: ~3 MB on disk

Even very large memory stores stay tiny. Don’t pre-optimise for storage.


Terminal window
# Embedding providers (auto-detected in this order: Ollama → OpenAI → Anthropic → keyword)
export OPENAI_API_KEY=sk-... # OpenAI embeddings
export ANTHROPIC_API_KEY=sk-ant-... # Anthropic embeddings + consolidation LLM
# Or run Ollama locally (default, recommended)
ollama pull nomic-embed-text
export GRAYMATTER_OLLAMA_URL=http://localhost:11434 # optional override
export GRAYMATTER_OLLAMA_MODEL=nomic-embed-text # optional override
export GRAYMATTER_OLLAMA_CONSOLIDATE_MODEL=llama3.2 # local consolidation summariser (ADR-011)
export GRAYMATTER_OPENAI_MODEL=text-embedding-3-small # optional override

Consolidation (ConsolidateLLM) accepts "anthropic" (needs ANTHROPIC_API_KEY) or "ollama" — fully local, no key. With Ollama, each applied cycle replaces the weakest half of an agent’s facts with one summary; the consumed facts stay auditable as tombstones pointing at the summary, and status reports the running totals (consolidations, facts_consumed).

Field Default When to tune
DataDir .graymatter Move out of the workspace if you don’t want it tracked
TopK 8 ↑ to 12 for very dense memory; ↓ to 5 if facts are highly specific
EmbeddingMode EmbeddingAuto Force EmbeddingKeyword to skip vector search entirely
DecayHalfLife 720h (30 d) ↓ to 7 d for fast-changing domains; ↑ to 90 d for stable conventions
ConsolidateThreshold 20 ↓ to 10 for aggressive consolidation; ↑ to 50 for retention
AsyncConsolidate true Set false only in tests / deterministic CI
MaxAsyncConsolidations 2 Concurrency cap on background consolidation
ReadOnly false Set true to open the store without taking the write lock

Is it a conclusion / decision / preference?
├── YES → Is it already in code/README?
│ ├── YES → Don't store (reference docs instead)
│ └── NO → Store it
└── NO → Is it temporary state?
├── YES → Use checkpoint
└── NO → Don't store
Need to store a fact?
├── Atomic fact ready → memory_add
├── Long LLM response, multiple → graymatter.RememberExtracted (Go) or extract yourself
│ insights inside before calling memory_add
├── Fix / replace existing fact → memory_reflect action=update
├── Remove a bad fact → memory_reflect action=forget
└── Connect two entities (KG) → memory_reflect action=link (host must wire SetKGLinker)
Need to retrieve context?
├── Agent-specific only → memory_search (agent_id=<your-id>)
├── Shared only → memory_search (agent_id="__shared__")
├── Both merged → two calls, merge yourself (or use RecallAll in Go)
└── Resume after interruption → checkpoint_resume
  • checkpoint_resume — was I interrupted?
  • Inspect only the newest hook block available for this session’s initial turn
  • Marker id differs: run both project and __shared__ searches
  • Marker id matches: search only the scopes whose sections are missing
  • Concatenate into system prompt
  • Proceed with task
  • Extract key learnings from the session
  • memory_add for each atomic insight
  • memory_reflect action=update for any preferences that changed
  • checkpoint_save if the task is incomplete
  • memory_reflect action=forget for any temporary / transient facts that slipped in


Good memory makes good agents. Store conclusions, not conversations.

Adapted and extended from a draft by MikeCase.