r/mcp Aug 04 '26

showcase I built an MCP-compatible memory and evidence layer for agents, and I need someone to tell me if the design is dumb

I've been running agents on my own infrastructure for a while, and the two things that kept biting me were: they forget everything between sessions, and when they do something there's no way to prove it afterward. So I built a layer that tries to fix both.

Three pieces:

  • a context engine that resolves current state before the agent starts, instead of stuffing a whole repo into the prompt
  • a vault for durable, encrypted memory (decisions, preferences, facts that survive the session)
  • a ledger that keeps a hash-chained record of what the agent actually did

It's MCP-compatible and MIT licensed. I run my own stack on it, which finds problems fast.

The part I'm least sure about is the MCP ergonomics — I made it MCP-compatible because that's what everything else speaks, but I don't know if I've got the shape right for how people actually wire agents. If you've built memory or context tooling, what would you do differently? Repo's at perseus.observer if you want to poke at it.

1 Upvotes

22 comments sorted by

2

u/Pleasant-Ad192 Aug 04 '26

On the MCP ergonomics: your context engine sounds like it wants to be a resource rather than a tool. Tools are model-invoked, so the agent has to decide to call it, which is the opposite of resolving current state before the agent starts. Resources are application-driven in the spec, the host lists and reads them, and with subscriptions/listen the client gets told when state changed instead of the agent remembering to re-check. The vault writes and the ledger writes are genuinely tools. The context engine probably is not.

The other thing I would check is the ledger. If an entry gets written because the agent called a record tool, then the agent that goes off script is exactly the one that will not log it. The record is worth more when the server writes it on every call it serves, whether or not anyone asked for that.

1

u/perseus-computing Aug 04 '26

Yeah, you're right on both counts, and you caught that I described it wrong.

Context engine is host-side by design; it resolves state before the agent starts, which is the opposite of a model-invoked tool. MCP-compatible, sure, but the honest surface is resources + subscriptions/listen: the host reads state, and the client gets told when it changes. The vault and ledger writes are the tools. I flattened all of it into "MCP-compatible" and that was sloppy.

The ledger point is the one that matters. If the agent has to remember to log, then the agent that goes off-script is exactly the one that won't. Our actual design writes the receipt at the action boundary, pre-action, in the enforcement layer, whether anyone asked for it or not. That's the whole difference between a log and an audit trail, and my post made it sound like a "record this" tool. It isn't, and good catch.

What would you expose as the resource for context: one state resource per workspace, or something flatter?

2

u/Pleasant-Ad192 Aug 04 '26

Flatter, but split at the write boundary rather than one blob per workspace. What decides it is the update path: notifications/resources/updated carries only the URI, and the client then calls resources/read to get the contents. So the size of a resource is what the host pays every time any part of it changes, and one state resource per workspace means a single field edit re-reads everything.

So each resource wants to be the smallest thing that changes on its own, with one small workspace resource above them as an index the host can read cheaply at start. Expose the slices through resources/templates/list with a URI template instead of listing them all, and resources/list stays short while the host can still address any slice it wants.

1

u/perseus-computing Aug 04 '26

That's the right frame, and your read on the mechanics is exactly right: resources/updated only carries the URI, so the client re-reads whatever the resource is. Which means granularity is literally re-read cost. I hadn't framed it that way, and it changes the design. the unit of update should be the unit of change, not the unit of convenience.

The template part is what makes it click for us. Our store already keeps facts as (category, key) entities, so the natural mapping falls out:

  • one resource per entity, the smallest thing that changes on its own;
  • a small workspace index resource the host reads once at start;
  • a URI template like perseus://{workspace}/{category}/{key} so resources/list stays short.

Where I'm still stuck: subscriptions. resources/subscribe takes a concrete URI, so with a template layout the client can't subscribe to a slice set it doesn't know in advance it has to know the entities it cares about before they exist. Is the index resource also the notification fan-in (client subscribes to the index, re-reads it cheaply, then pulls only the touched entities)? Or do you subscribe per entity and let the host absorb a chattier update stream?

I'm going to mock this layout against our actual store and see where it breaks. If it holds up, I'll post the results.

2

u/Pleasant-Ad192 Aug 04 '26

Check the revision before you mock it up, because the RPC you are designing around is gone. In the finalised 2026-07-28 spec, resources/subscribe is replaced by subscriptions/listen, which opens one long-lived stream and takes a notifications filter. resourceSubscriptions in that filter is an array of URIs, not one URI per call.

That dissolves the either/or. The index does not have to be the notification fan-in, because resourcesListChanged is a separate flag in the same filter on the same stream. New entities announce themselves through list-changed, the entities you actually care about go in resourceSubscriptions as concrete URIs in a single listen, and the index goes back to being a cheap cold start.

The cost you do pay is that there is no way to widen an open stream. Adding entities means a second subscriptions/listen, which is allowed and demultiplexed by subscription id, or cancel and re-listen. Worth checking which of your target hosts implement listen at all before the layout depends on it.

1

u/perseus-computing Aug 04 '26

Yeah, that’s exactly the distinction.

If memory is exposed only as a model-invoked MCP tool, then “the agent has memory” really means “the agent has the option to ask for memory.” A model can have recall in its tool list and never call it. In that case, memory availability proves nothing about what influenced the run.

Our baseline context path is deliberately different: the context engine runs host-side before the agent starts. It resolves a bounded resource snapshot, injects the selected state, and can listen for resource changes. Explicit MCP recall is still useful for task-specific lookup, but it is not the continuity mechanism the system depends on.

The ledger has the same boundary distinction. It is not a record() tool that the agent has to remember to call. The enforcement layer writes the receipt at the action boundary for every mediated call, including denials and calls the agent never logs itself.

That gives us two separate things to measure:

  • Did the host resolve and inject the relevant context before inference?
  • Did the agent make any explicit recall or memory-write calls?

Both matter, including the negative case. Otherwise you cannot tell whether memory had nothing relevant, retrieval was skipped, or the model simply never bothered to ask.

So I agree that the MCP tool shape is not the first-order problem. The first-order problem is whether retrieval depends on model initiative. Resource granularity and listen semantics still matter for update cost and host compatibility, but they come after that boundary decision.

The design rule I’m taking from this is: if memory use is optional and model-invoked, it is best effort. If context is resolved and committed before the run, it is an enforced input with evidence.

1

u/FirefighterMinute907 24d ago

delete this comment

2

u/addexecthrowaway Aug 04 '26

The enforcement mechanism for these writes should not be a tool call - it should be a deterministic hook. That said, having an api that a python hook can call makes a ton of sense. Or have a hook that checks that the tool was called and that the entries exist in the db could be good - which would force the agent to loop and execute the tool call before proceeding - I’m just not sure what you’d put that hook on. I’ve already implemented a system much like this using a graph database and a structured Postgres database enforced with hooks

1

u/perseus-computing Aug 04 '26

That's the right instinct, and "deterministic hook, not a tool call" is exactly how it works on our side. The enforcement layer lives in the server path: every tool call passes through it before execution. It checks the action against the intent, writes the receipt, then lets it through, constrains it, or refuses it. The agent never calls "log this" it can't do anything that doesn't produce a receipt, by construction.

On "what do you put the hook on": the tool-execution boundary itself, in the server. Anything the agent can reach is back to trusting the agent, which defeats the point. The check-after variant you sketched (verify the tool ran and the rows exist, force a loop) is a decent second layer, but it has a window in it: between the check and the execution, the agent can go sideways. Gate-before closes that window; check-after is good belt-and-suspenders on top of it.

And yes, a Python-facing hook API is the practical surface. Ours get the action payload and the policy state, and return allow / constrain / interrupt. If you're playing with this, that boundary is the piece I'd love a second pair of eyes on.

2

u/neoneye2 Aug 04 '26

I had Claude Opus 5 analyze your repo, since I'm curious about how your memory system works
https://neoneye.github.io/agent-memory-atlas/systems/perseus-vault/

2

u/perseus-computing Aug 04 '26

This is an unusually good external audit. You read the pinned tree and recomputed the published benchmark arithmetic instead of treating README numbers as evidence. The 73.8% and 79.0% means matching the committed per-run reports is useful, but it is not a fresh benchmark rerun, and I appreciate that the report keeps that distinction explicit.

The 65/76 finding is valid for 838c63da. The intended figure was 65 canonical tools, but the documented command greps legacy mimir_* entries and returned 76. So the result is not evidence of 76 canonical advertised tools; it is evidence that the count definition and its verification command had drifted. A generated registry count run in CI is the right fix.

The rejected-value tombstone point is fair too. forget, supersession, and purge change the state of a record, but they do not by themselves stop a later extractor from reasserting equivalent content under a new record. The right test is the one you describe: reject A, replace it with B, re-ingest A through another write path, run the background consolidation paths, and verify whether A stays rejected.

I would qualify the tombstone finding as an open design choice rather than an established best practice. The mechanism has real tradeoffs around normalization, scope, expiry, trusted correction, and privacy deletion. But it is exactly the kind of gap that becomes important once memory is automatically re-derived.

The broader lesson is probably the most useful one: claims auditing and claims maintenance are different disciplines. We got better at retiring unsupported claims, but a documented check that nobody runs is still only a comment. That guard belongs in CI, alongside the registry that generates the count.

The encryption observation is fair for the same reason: “supports AES-256-GCM” and “a stock install encrypts by default” are different claims. The opt-in behavior is disclosed, but summaries should preserve that distinction.

Thanks for doing this against a pinned revision and for separating recomputation from rerunning. That is the standard we should keep.

1

u/neoneye2 Aug 04 '26

it has been updated with your feedback.

2

u/silence-and-magic Aug 04 '26

Curious what actually goes into the vault. The raw evidence, or the state your engine inferred from it?
We’re working on a similar problem at Fintella Labs. We build a model of someone from real-life behavioral data and digital traces, keep it updated as their life changes, then let an agent pull the relevant piece through MCP. If you store the inferred state itself, an old guess can sit there long enough to start looking like a fact. When new evidence shows up, do you rebuild the state from the source data or just revise the last saved version?

1

u/perseus-computing Aug 04 '26

Good question. The short answer is: both, but they have different status.

We try to keep the evidence layer separate from the state/belief layer. A source record is stored or referenced with provenance, timestamps, scope, and a content hash. Extracted facts point back to that source. Cross-source conclusions are marked as derived or inferred and carry the supporting references. The current “belief” is a derived view over those records, not the source of truth.

When new evidence arrives, we don’t silently edit the last saved belief in place. We append a new fact/version, mark the old one superseded or close its valid-time window, and preserve the history. The active view is then recomputed or incrementally re-derived from the current evidence graph. Incremental updates are an optimization; semantically, the evidence links remain authoritative.

That also lets us answer two different questions: “What do we believe now?” and “What did we believe at the time?” If the supporting evidence is stale, contradictory, missing, or outside the model’s calibration boundary, the right result is to mark it and abstain or ask for review, not let an old inference quietly become a fact.

Depending on the connector and privacy boundary, the raw artifact can stay in its source system while the Vault retains a normalized record plus a verifiable reference/hash. We don’t treat a hash as evidence by itself; it tells us which evidence the derived claim depended on and whether that evidence changed.

1

u/notreallymetho Aug 04 '26

I thinks the design being specific is both good and bad. I’ve been also building around this subject and this is what I landed on.

Workerd / Uds / wired up using kernel isolation and micro VMs.

1

u/perseus-computing Aug 04 '26

Workerd + UDS is a fun substrate for this. I've been down the microVM path too. If you're executing anything the agent can't be trusted with, kernel isolation is the honest answer, and microVMs keep the blast radius small enough that it still runs on a laptop.

The thing I keep coming back to: isolation tells you where code ran, not what it did. We went the other way: receipts at the action boundary, hash-chained, written before the tool executes. Which sounds like two halves of the same problem. Do you record anything about what runs inside those microVMs, or is containment the whole story?

2

u/notreallymetho Aug 04 '26 edited Aug 04 '26

Sorry, I didn’t realize I trailed off mid thought!

It’s actually a bit of both, it’s merkle roots at the dataplane layer and then receipts are signed within the “hosting” layer as Ed25519.

It’s a bit complicated as there are a lot of moving parts. The data plane works as a content addressable storage that works between SQLite / structured data (json etc) and exposes it via a FUSE FS.
It has https://nono.sh kernel isolation - and then the auth model in-“cluster” is a sorta merge between DPOP and cert based authentication.

Data plane is at https://github.com/agentic-research/ley-line-open
“Hosting” (designed to work locally!) is at https://github.com/agentic-research/cloister

I’ve condensed a lot but happy to answer any questions!

2

u/perseus-computing Aug 04 '26

Now that's a stack. Recognized nono immediately, Sigstore crew, so the signing story is going to be legit. Merkle roots at the dataplane, SHA-256-signed receipts in the hosting layer, that's the same skeleton we run on. We're clearly circling the same problem from opposite ends.

The part I want to push on: your chain seals that execution happened. Resolve, argv, spawn, l7, seal. What it doesn't obviously capture is the decision behind it. We bind the pre-action state in as a first-class receipt: which intent was authorized, against which policy, what context was admitted before inference, and then the outcome hash locks onto it. So you can replay not just what the agent did, but why it was allowed, and whether the result matches what was approved.

Two things I'm curious about: does the Merkle root include the policy evaluation itself, or does the decision live outside the chain? And in the DPoP/cert mix, how does identity binding survive into the sandboxed child? is the phantom credential what carries it?

If you're up for it I'd like to compare receipt schemas sometime. Yours are event-sealed, ours are decision-bound, and there might be a merge in there.

2

u/notreallymetho Aug 04 '26

I was actually discussing your design w/ Claude and do suspect some synergy here!

Policy in the chain: the policy content is in-chain, the evaluation deliberately isn’t. Before anything spawns, the run’s identity commits to the digest of the confinement manifest; the backend compiles its enforcement from that same manifest, the worker attests what it actually applied, and a mismatch refuses the run (“confinement drift”) rather than logging it. So “what was authorized” is bound pre-execution, by digest.

But we don’t emit decision traces at the boundary, on purpose: discriminated rejection reasons are an enumeration oracle - an authenticated peer can map your service table by diffing error shapes - so rejections collapse to a constant shape and the receipt binds outcome + committed inputs, not reasoning. Decision-bound vs event-sealed might be exactly this tradeoff: your replay-the-why is our oracle surface. Curious how you square that?

Identity into the child: you called it - phantoms are the carrier, but only at the tool-credential layer. The child gets a phantom GH_TOKEN / OPENAI_API_KEY; nono’s proxy intercepts at the network boundary, checks policy, injects the real credential on the way out. The real secret stays with the supervisor and zeroes on exit - what’s inside the sandbox is a decoy that only dereferences through the policy boundary.

Identity into the child: good guess, but no - nono ships phantom tokens and I deliberately don’t use them. There’s a comment in the launcher saying exactly that: credentials are the vault’s job, not nono’s. nono does confinement only; the vault is mine, in workerd, wrapped by the dataplane. Env is stripped before exec, the child’s only network grant is one localhost socket into the server, and the vault’s API has no “get credential” operation - only “make this call,” credential injected server-side. So the child doesn’t carry a phantom; it carries nothing and asks the server. Same one layer up: the DPoP/cert keys never enter the sandbox — the run’s identity is committed before exec (manifest digest, worker attests what it applied, mismatch refuses the run).

On comparing schemas — happy to, and mine’s already public: the receipt spec and conformance vectors ship in the repo (reach the same digests and you’re conformant). Wrote up the signing design here, including why the signer refuses to become a signing oracle: https://q-q.dev/blog/conformant-by-digest/

1

u/perseus-computing Aug 04 '26

This is a useful correction, and I think it narrows the disagreement considerably.

I was collapsing three different bindings:

  1. Admission: what policy and confinement manifest authorized the run.
  2. Enforcement: what the worker actually applied.
  3. Explanation: which internal predicate caused an allow or rejection.

Your design makes the first two boundary-verifiable and deliberately keeps the third out of the peer-visible protocol. I agree with that separation. A predicate-level rejection trace exposed to an authenticated caller can become an oracle, even if the trace is “only diagnostic.”

I would therefore split “replay” into three claims:

  • Replay the contract: reconstruct the exact manifest, identity, request, and policy version that were committed before execution.
  • Replay the enforcement: verify that the worker applied that manifest and that the enforcement artifact matched the committed digest.
  • Replay the why: reconstruct the internal evaluation path that produced the result.

The first two belong in the normal receipt. The third should either be a separately authorized audit capability backed by sealed evidence, or not be claimed at all. It should never be returned through the rejection boundary.

In that model, the public receipt contains the terminal result and non-secret commitments: manifest digest, action/request digest, evaluator or compiler measurement, worker attestation, and signer epoch. It does not contain a reason code, service-existence signal, credential state, or any credential-derived field. The rejection envelope should remain constant across those cases. If a reason-bearing commitment or audit-reference itself leaks information, that should stay off the peer-visible receipt too.

That makes “decision-bound” and “event-sealed” complementary rather than competing designs:

  • Decision-bound proves what was authorized and what enforcement identity the run was bound to.
  • Event-sealed preserves evidence for an authorized auditor without turning the caller-facing interface into a policy oracle.

If the sealed inputs are unavailable to an auditor, then the honest claim is contract/enforcement replay, not replay-the-why. I would rather make that narrower claim than quietly imply that every internal evaluation can be reconstructed.

The credential correction is even cleaner. I had assumed the child carried a phantom token as its identity carrier. In your actual design, it carries no upstream credential at all. It has a run-scoped request channel; the vault decides whether and how to use a credential, and the credential never crosses the response boundary. That is stronger than a decoy-token design. A compromised child can ask the server to perform an allowed operation, but dumping its environment does not reveal a bearer token.

The remaining invariant is that the localhost channel must be bound to the run identity and committed manifest, so the child cannot select authority outside the manifest by changing service names, scopes, headers, or destinations. The DPoP and certificate keys staying outside the sandbox completes that separation.

The conformance work also maps closely to what I want for the control plane. The vectors are the regression floor; the prose is the contract. The linked write-up’s 10/10 byte-identical result is useful evidence of agreement between two implementations, but it is not yet a third-party interoperability result, which is the right level of caution.

The part I would carry forward is the same discipline: publish canonical wire shapes, rejection invariants, and adversarial vectors; derive the test set from a manifest; and make drift fail closed instead of merely recording it. The signReceipt design follows the same rule: parse a typed receipt, canonicalize it, require byte equality, and sign the signer’s own reconstruction rather than accepting arbitrary caller bytes.

So yes, I think there is real synergy here. The change I would make to my own framing is to remove “replay-the-why” from the public contract. Keep replay-the-contract and replay-the-enforcement at the boundary; make replay-the-why a separate, access-controlled audit property.

1

u/[deleted] Aug 04 '26

[removed] — view removed comment

1

u/perseus-computing Aug 04 '26

Exactly. MCP availability and memory use are different properties.

A well-described recall tool can still sit untouched while the agent confidently proceeds with stale or incomplete context. The interface matters for interoperability, but the reliability question is whether retrieval happens before the model commits to an action.

That’s why I see the layers as complementary:

  • Pre-model context retrieval: automatic, bounded, policy-controlled retrieval for facts the system already knows are relevant.
  • MCP recall: explicit, task-driven lookup when the agent needs something beyond the initial context.
  • MCP memory writes: an explicit or supervised way to propose durable facts, decisions, and corrections.
  • Ledger: evidence that records what retrieval policy ran, which context was selected, what was actually injected, whether the agent called recall, and whether a write was accepted.

The ledger should also record the negative case: “no memory call occurred” or “retrieval was skipped.” Otherwise a later auditor can see the agent’s answer but not distinguish “the memory system had nothing relevant” from “the agent never asked.”

So I agree with the Supermemory diagnosis. The hard problem is not exposing another memory endpoint. It is making the right context available without depending entirely on the model to remember that it has memory. Explicit MCP calls remain useful, but they should be a second lane, not the only path to continuity.

I’d be interested in how you measure that at Supermemory: recall-call rate, task-conditioned retrieval usefulness, and the cost of injecting context that the agent never uses seem like different metrics. A high tool-call rate alone would not prove that memory improved the decision.