Governed memory for multi-agent production meshes.

Attestor is no longer memory for your coding assistant. When many agents share one memory: who is allowed to write, who is allowed to forget, and can you prove it afterwards.

# identity, role, namespace, budget ctx = AgentContext( agent_id="reviewer-1", role=AgentRole.REVIEWER, namespace="acme-prod", ) # PermissionError: REVIEWER lacks 'write' ctx.add_memory("...") # belief as of then; nothing is deleted mem.recall("...", context=ctx, as_of=last_friday)

The Python library. The REST sidecar and MCP server expose the same calls.

Three things happened.

For a few months this was a memory layer for coding assistants. Then the editor vendors shipped memory for free. The benchmark race became a race about which LLM answers the question. And every serious conversation was about something else entirely: when many agents share one memory, who is allowed to write, who is allowed to forget, and can you prove it afterwards. So that is what Attestor is now.

Who wrote this, on whose behalf?

Every memory carries the agent, session, namespace and role that produced it. Child contexts accumulate the trail. Ed25519 signing makes it non-repudiable.

Who is allowed to forget it?

Six roles, enforced before any backend is touched. Forgetting writes its audit row first, then walks every store and reports per-backend counts.

What did the system believe last Friday?

Contradictions supersede; nothing is deleted. Every fact has valid time and transaction time, and recall(as_of=…) replays any past moment.

What that means in the code, not the deck.

Every row names the file that enforces it, and the limits are written down next to the claims.

Access

Who may do what, decided before any store is touched.

Six roles gate write and forget before any backend is touched.

Reviewers and monitors are read-only, because a reviewer that writes contaminates the evidence trail. ORCHESTRATOR may read, write and forget. PLANNER, EXECUTOR and RESEARCHER may read and write. REVIEWER and MONITOR read only. read_only=True strips write and forget regardless of role. A violation raises before any backend is touched.

attestor/context.py · ROLE_PERMISSIONS

Per-agent budgets and write quotas contain a misbehaving sub-agent by construction.

A token budget bounds what recall() packs for one context. A per-context write cap and a per-user daily quota bound what can be added. The cap is a property of the context, so a misbehaving sub-agent is contained by construction.

attestor/context.py · attestor/core/agent_memory.py

Tenant isolation on all three stores, not only the database.

Postgres row-level security keyed to the current user, with namespace stamped into metadata. Pinecone: one index, a namespace partition on every call. Neo4j: a namespace property with a composite constraint and namespace-scoped traversal. The PageRank projection is global and is a ranking signal only.

attestor/store/schema.sql · pinecone_backend.py · neo4j_backend.py

Truth

What was said, by whom, and what the system believed at any moment.

Every memory carries which agent wrote it, in which session, on whose behalf.

Optional Ed25519 signatures, verifiable later. Child contexts accumulate an agent trail, so "which agent said this, on whose behalf" is always answerable. Rows leave only through an explicit forget, a retention delete policy, or compact(). Signatures are verified with verify_memory().

attestor/context.py · attestor/identity/signing.py

Contradictions supersede instead of overwriting. You can ask what the mesh believed last Tuesday.

Every fact has valid time and transaction time. Supersession never deletes. recall(as_of=…) filters candidates on transaction time at document hydration and in the BM25 lane. The Pinecone lane ignores as_of and says so in the log.

attestor/temporal/manager.py

The read path is deterministic. No model scores anything by default.

Ranking is vector, BM25, RRF, graph, MMR, budget fit, in that order, with no LLM in scoring by default. The optional reranker is off by default. Same query, same order: unit-testable, replayable, explainable to an auditor line by line.

attestor/retrieval/orchestrator/core.py · scorer.py · reranker.py

Deletion

Forgetting that leaves a record. Jobs that finish.

Forgetting a user writes the audit row first, then walks every store and reports per-backend counts.

Partial failure is visible, never silent. forget_user() writes an RLS-exempt audit row before deleting, then walks document, state, vector and graph. Declarative retention policies (archive or delete) apply first and write the audit row after. Full export for subject-access requests.

attestor/compliance/retention.py

The governance jobs themselves are durable.

Consolidation, derived-state repair, the forget saga and retention sweeps run as Temporal workflows with retry policies and an operator UI. Opt-in; job state only. Postgres stays the source of truth. How it is wired.

attestor/durable/workflows/ · attestor/durable/activities/

That is the whole pitch. Every row above names the file that enforces it, and the limits are written down next to the claims. If your memory layer cannot show you the enforcing line, it is a policy document. Hold every option to that, including the temporal-knowledge-graph layers (Zep, Graphiti) and this one.

How a read happens.

Six steps, in this order, every time. No model is asked to score anything unless you turn the reranker on. That is what makes a ranking something you can replay in front of an auditor.

  1. Vector candidates

    Cosine top-K from Pinecone, scoped to the caller's namespace. An optional HyDE lane can widen the net.

  2. Lexical candidates

    BM25 over Postgres full-text search, filtered on transaction time when as_of is set.

  3. Rank fusion

    Both lists fold into one rank with a fixed constant. No weights to tune, nothing learned.

  4. Graph affinity

    A bounded breadth-first walk in Neo4j, inside the namespace, boosts candidates linked to the query's entities.

  5. Diversity and decay

    Maximal marginal relevance removes near-duplicates. Confidence decays with age and supersession.

  6. Budget fit

    Greedy pack to the context's token budget, best first. Flat per call however long the conversation is.

Ask it what it injected.

Every recall can report the packed payload it just handed the model, from the CLI or in the MCP response. The cost of memory is a number you read, not one you estimate. Full-context replay re-sends a growing transcript each turn. This does not.

$ attestor recall ~/.attestor "what did we decide about auth?" --show-tokens [tokens] packed N tokens across M memories (cap budget). This payload is flat regardless of conversation length. Full-context replay grows unbounded.

The read path never waits. The jobs never quit.

Governance work that used to be a cron job and a prayer runs as Temporal workflows: retried by policy, resumed after a crash, inspectable in an operator UI. Turn it on with one flag.

Two hard rules. Recall never touches Temporal and hooks never wait on it; nothing under the retrieval or hook packages may import it, and a test enforces that. Workflow payloads carry ids, never memory content, so Postgres stays the only source of truth.

With durable.enabled: false, or an unreachable server, every job falls back to the in-process path it replaced. Nothing about a memory ever lives only in a workflow history.

TriggerWorkflowWhat it guarantees
On every writeDeriveMemoryVector and graph derivation is idempotent and retried, so derived state cannot silently drift from Postgres.
On each episodeConsolidateEpisodeReplaces the hand-rolled queue and lease; a stuck or failed episode is visible, not lost.
On a scheduleRetentionSweep
SessionSweep
Declarative retention and idle-session cleanup actually run, on a Temporal Schedule you apply once.
On demandRebuildDerived
ForgetUser
Rebuild Pinecone and Neo4j from Postgres. Forget as a saga: audit row first, then each backend with its own retry and result.
# opt in: Temporal server + UI on :8233, then the worker $ attestor quickstart --durable $ attestor worker $ attestor durable status

Built for the day someone asks.

Three situations where "the agent remembered something" is not an answer, and what you can say instead.

A security review is next quarter.

Which of your agents can modify shared state, and how do you know a compromised sub-agent can't wipe it?

Roles are enforced at the context layer, before any backend call. Forget is a separate permission from write. Budgets and write caps are properties of the context, so containment is by construction. The reviewer can read ROLE_PERMISSIONS and stop there.

A regulator asks what the system believed on a date.

Reconstruct the information your agent had when it made this recommendation on the fourteenth.

recall(as_of=…) returns the belief of that moment. Nothing was deleted to get there: later contradictions superseded earlier facts, and both are still in the log with valid time and transaction time. Provenance names the agent and session for every row.

It's 3 a.m. and an agent did something wrong.

Which memory did it act on, who wrote that memory, and why did it rank first?

The read path is deterministic: re-run the query with the same context and you get the same order, step by step. The memory carries its author, its session and, if signing is on, a verifiable signature. Fix the fact, supersede it, and the next call sees the correction.

Runs where you run.

One API across three topologies; only the configuration changes. Postgres is the source of truth. Pinecone vectors and the Neo4j graph are derived state, rebuildable from it.

Embedded library

AgentMemory(config) in process, talking directly to the three stores. The Claude Code plugin and the MCP server run this way. Role enforcement is an AgentContext guarantee and runs in process.

Sidecar

attestor api on localhost. Any language talks HTTP to the same stores.

Shared service

One Attestor in front of a mesh on App Runner, Cloud Run or Container Apps, backed by managed Postgres, Pinecone and Neo4j.

Local, in one command.

Zero questions. It writes the config, brings up Postgres, Pinecone Local and Neo4j in Docker, wires the MCP server and hooks for Claude Code, and runs attestor doctor. attestor teardown reverses it and keeps your data unless you say otherwise.

# macOS / Linux, Docker running $ pipx install attestor $ attestor quickstart
A bound archival ledger lying open, one page pressed with an embossed seal, photographed in black and white.

Provenance, all the way down.

A memory layer that sells auditability should be auditable itself. The repository is held to the same standard as the data it stores.

  1. Every source file carries an SPDX header naming its author and licence, so a copy is a copy with a name on it.
  2. Commits are SSH-signed and the repository runs in vigilant mode, so unsigned history shows as such.
  3. Releases are timestamped on Bitcoin with OpenTimestamps. The .ots proofs ship in artifacts/provenance/ and verify offline against the chain.
  4. No benchmark numbers in the repo. Figures go stale. The harness that produces them is open, and you run it yourself.

If you run agent meshes in a regulated setting and this is the problem you have, I would like to talk.

Self-hosted, MIT, Python. Designed and built by Surendra Singh.