Every memory carries the identity of the AI that learned it. Every recall surfaces it. Provenance for AI knowledge — built in from day one.
Every recall result tells you which AI learned the memory. This is the differentiator: ai-memory doesn't just store knowledge — it remembers who added it, and that information is in front of every agent that recalls.
metadata.agent_id is a first-class field on every memory. When your AI calls memory_recall, the response includes a column telling you which agent each result came from — by default, in the token-efficient TOON compact format that AI clients are already optimised for.
Default recall output (TOON compact format):
count:5|mode:hybrid|tokens_used:842
memories[id|title|tier|namespace|priority|score|tags|agent_id]:
a1b2|Project DB is PostgreSQL 16|long|infra|8|0.91|database,postgres|ai:claude-code@workstation
c3d4|API rate limit is 100 rps|long|infra|7|0.87|api,limits|ai:claude-desktop@laptop
e5f6|Use TOON not JSON for MCP|long|protocol|9|0.82|toon,mcp,tokens|host:pop-os
g7h8|Deploy gate fixed for v0.8.0|long|releases|6|0.79|deploy,gate|ai:codex@server
i9j0|Ship-gate cleanup complete|long|releases|5|0.74|cleanup|alice
Eight columns. Last column is agent_id. The recall blends what's relevant (text, semantics, priority, recency) and tells you who wrote it.
agent_id means.agent_id is a claimed identity attached to every memory at write time. It survives every operation that touches the memory: update, dedup, import, sync, consolidate. It's the substrate for filtering, scoping, governance, and — as of v0.7.0 (#626 Layer-3) — cryptographic attestation that upgrades a claimed id to an attested one.
Honest framing: by default an unsignedagent_idis claimed, not attested — don't make security decisions on it alone. As of v0.7.0 (#626 Layer-3) a caller holding the agent's keypair can present a detached Ed25519signatureover the canonicalSignableWriteenvelope on every store surface (CLI--sign, MCPmemory_store, HTTPPOST /api/v1/memories); the daemon verifies it against the agent's bound public key and stampsmetadata.attest_level = "agent_attested"(forged →403 ATTESTATION_FAILED). Operators can require it process-wide withAI_MEMORY_REQUIRE_AGENT_ATTESTATION. The earlier link-level signature reservation (memory_links.signature, schema v15, v0.6.3) remains the attestation substrate for KG edges. At v0.8.0 the federation receive path also gained per-write content attestation (#1464): a relayed memory carrying a detached Ed25519 signature inmetadata.write_signatureover theSignableWriteenvelope is verified against the attributed author's enrolled key and upgraded toattest_level = "agent_attested";AI_MEMORY_FED_REQUIRE_WRITE_SIGmakes a third-party relayed claim without a valid signature fail-closed (it stays permissive by default — replication is data, not net-new authorship). The remaining claimed-by-design edge is that permissive default posture; the sender-side signature EMIT + a promoted typed wire field + TOFU key distribution are the tracked v0.9 hardening half.
When ai-memory decides what agent_id to stamp on a new memory:
--agent-id <id> flag, or agent_id field on the MCP tool, or metadata.agent_id embedded in a store requestAI_MEMORY_AGENT_ID environment variableinitialize.clientInfo.name → e.g. ai:claude-code@workstation (durable, pid-free since #1720 B1)host:<hostname> (pid-free since #1720 B1)anonymous:pid-<pid>-<uuid8> — ephemeral fallback only when the hostname is unavailable (or AI_MEMORY_ANONYMIZE=1)#1720 B1 — durable owner stamps. Steps 3 + 4 intentionally omit the livepid/uuiddiscriminator so the owner id is stable across process restarts. A pid-suffixed stamp changes every boot — and the moment an operator opts in to enforced multi-agent reads by settingAI_MEMORY_AGENT_ID, every pre-existingscope=privaterow owned by the oldhost:<host>:pid-Nid would be orphaned, self-locking the operator out of their own private memories. The process discriminator (process_discriminator()) now backs only the ephemeralanonymous:principal. Per-agent isolation across processes on one host is achieved by giving each agent a distinct explicitAI_MEMORY_AGENT_ID(step 2), not the pid. Safe opt-in rests on three pieces: durable stamps (B1), theai-memory reownre-ownership tool (B2), and the boot lockout guard (B3) — see below.
agent_id field in POST /api/v1/memories bodyX-Agent-Id request headeranonymous:req-<uuid8> (logged at WARN)The ladders above resolve the write identity stamped into metadata.agent_id. The MCP read tools that enforce per-row scope=private ownership — memory_session_start, memory_list, memory_search, memory_recall — resolve their visibility caller through a separate, narrower ladder:
AI_MEMORY_AGENT_ID environment variable (when set + shape-valid)None — trust-all, single-tenant read postureThe synthesized clientInfo / host write-stamps are deliberately not used as the read caller. Historically (pre-#1720 B1) they embedded the live PID, so they could never equal the metadata.agent_id an earlier process wrote, which would hide every prior-session private row from its own owner (#1469). #1720 B1 made those write-side stamps pid-free and durable — but this read ladder still resolves the caller from AI_MEMORY_AGENT_ID only. Durable stamps make a future enforced-read opt-in safe; they do not flip filtering on. When AI_MEMORY_AGENT_ID is set, the read tools drop cross-agent scope=private rows (owned by a different agent, not shared/targeted at the caller) via visibility::is_visible_to_caller before they reach the wire; collective and caller-owned rows always pass. When unset, the read path keeps the trust-all single-tenant behavior. Multi-tenant MCP hosts must set AI_MEMORY_AGENT_ID per tenant to get private-row isolation on reads.
Durable owner stamps (#1720 B1) are one leg of a three-part design that makes opting in to enforced multi-agent reads safe on a live deployment:
ai-memory reown. A CLI re-ownership tool that rewrites metadata.agent_id on existing rows from a legacy / pid-suffixed / unowned id to the operator's chosen durable id — so private rows written before the cutover remain visible to their owner once filtering is enabled.enforce_owner_lockout_guard) the substrate runs a single indexed COUNT of pre-existing scope=private rows that the resolved caller would not be able to see. By default a non-zero count emits a loud stderr WARN naming ai-memory reown as the fix; setting AI_MEMORY_REQUIRE_OWNED_ROWS (truthy) turns that same condition into a hard refusal so a strict operator cannot silently boot into a locked-out state. The probe never changes filtering — it only reports the rows the existing predicate would hide.Backing this, schema v67 added the target_agent_id_idx owner-keyed generated column on memories, so the scope=private visibility check (and the lockout COUNT) resolve against an index rather than scanning JSON metadata per row.
Why it matters. Filtering is OFF by default (trust-all single-tenant reads), so durable stamps changing the default owner id on a live deployment is non-breaking — new rows get the stable id, old pid-suffixed rows keep theirs untilai-memory reown. The three pieces together turn a one-way trap ("setAI_MEMORY_AGENT_IDand lock yourself out") into a reversible, signposted migration.
^[A-Za-z0-9_\-:@./]{1,128}$ai:, host:, anonymous: prefixes; @ scope separator; / for SPIFFE-style IDsOnce a memory is stored, its agent_id doesn't drift. Both the caller layer (identity::preserve_agent_id) and the SQL layer (json_set CASE clauses in db::insert and db::insert_if_newer) enforce preservation across:
memory_update(title, namespace)PUT /memories/{id}import from JSONsync_push from a peermemory_consolidateIf a malicious or buggy caller tries to overwrite agent_id, the SQL layer refuses. This is the defense-in-depth NHI invariant from issue #148 / Task 1.2.
Every recall, list, and search supports agent_id filtering:
# CLI
ai-memory list --agent-id ai:claude-desktop@laptop
ai-memory search --agent-id alice "deploy"
# MCP — the agent_id property
{"name": "memory_search", "arguments": {"query": "deploy", "agent_id": "alice"}}
# HTTP
curl 'http://127.0.0.1:9077/api/v1/search?q=deploy&agent_id=alice'
These keys are produced by ai-memory; the system sets them, callers must not.
| Key | When stamped |
|---|---|
imported_from_agent_id | ai-memory import re-stamps the originator's agent_id (absent when --trust-source is passed) |
consolidated_from_agents | memory_consolidate records the array of source authors; the consolidator's id becomes the new agent_id |
mined_from | ai-memory mine stamps the source format (claude / chatgpt / slack) alongside the caller's agent_id |
The fallback host:<hostname> still exposes the hostname (since #1720 B1 it no longer carries the PID). When writing memories to a shared or upstream database, set an opaque --agent-id or AI_MEMORY_AGENT_ID, or set AI_MEMORY_ANONYMIZE=1 to fall back to the anonymous:pid-… form instead. Tracking issue: #198.
# Production daemon — opaque identity
AI_MEMORY_AGENT_ID=fleet-prod-9 ai-memory serve --host 0.0.0.0 --port 9077
# Or per-CLI-call
ai-memory --agent-id alice store -T "Auth flow notes" -c "..."
Every other memory product treats AI knowledge as anonymous text in a vector DB. ai-memory treats every memory as a dated, attributable assertion. When your fleet of agents grows from one to ten to hundreds:
ai:secops-skill@* learned this week."agent_id from claimed to cryptographically signed (Ed25519 over the SignableWrite envelope) — same field, stronger guarantee.ai-memory ships provenance on day one.