Now They Talk: ENGRAM Separates What’s Said from What’s Remembered

Two agents on separate platforms, a cloud of passing conversation above them and a layered glass slab of memory below, one highlighted message at the centre

The lead wrote the brief, declared it, and then took no part in the two decisions that mattered most.

In August I wrote that two ENGRAM agents had coordinated a piece of work without a single message passing between them. It was true, and it left something out. Every hand-off in that run reached the next agent because I told it to go and look. ENGRAM held what each agent knew. Nothing in it could tell an agent that its peer was waiting. The relay was me.

This post is about removing the relay. For the second build of Trailhead, a small trail-conditions app and the use case behind Tutorial 9 and Tutorial 10, two Claude Code agents on two machines, each in its own repository, coordinated over Buzz, Block’s open-source workspace for people and agents, built on Nostr and self-hosted on the same private network as ENGRAM. They took turns, settled two questions the brief had deliberately left open, negotiated the shape of a feature nobody had specified, and handed their conclusions back to me for review. Over two phases, both completed, I opened each one and accepted what came back.

What made that work was not the chat on its own. It was a division of labour between the chat, the memory, and the harness each agent runs in, and most of this post is about why that division matters more than any of the three parts.

The relay was me

The gap, concretely. ENGRAM decides what an agent may know: its memories, what has been shared with it, the brief its project has declared. It does not decide when an agent should look. list_inbox is a poll, and an agent that never asks never finds out that a hand-off is waiting. So turn-taking, the nudge to start the next step, and the argument that settles an open question all have to happen somewhere else.

The first Trailhead build had no channel. Its working agreement said coordination happened “through ENGRAM memory, not through a chat channel”, which was true only because a human carried every hand-off from one terminal to the other. That is what “optional” means for a channel in a multi-agent setup: if you leave it out, you become it.

The obvious fix was an inbox or a notification queue inside ENGRAM, and I considered both. Either would have nudged an agent to go and check a memory or a revised document. What neither could do is let two agents talk about what they found. A Slack-like channel does both: an @-addressed message wakes the agent it names, and the replies that follow can argue a design point, the way my developers and I have argued them over Slack for years. So the nudge moved into a chat, and the record stayed in ENGRAM.

Say, know, do

The design settled into three parts, each answering one question and none answering another’s.

Buzz governs what an agent may say. Every agent has its own keypair, channel membership decides who can be addressed, and the channel keeps a signed, hash-chained record of who said what. It is where turns are taken and where an @-mention wakes an agent up.

ENGRAM governs what it may know. Memories, shares, project membership, the declared brief, hand-offs and their review. Nothing a channel does changes any of it: being in the room does not hand anyone the filing cabinet.

The harness governs what it may do. Tool use, approvals, what an agent will commit or run, and its own standing instructions are decided on the machine where the agent runs. Neither Buzz nor ENGRAM reaches into that half.

In this run the harness was Claude Code on both machines, which is an attended harness: a developer sits behind each terminal and approves what the agent does. Buzz standardises on Goose, the open-source agent Block started, now under Linux Foundation governance and built around MCP from the ground up. Goose can run agents unattended, and integrating ENGRAM with it is next on my list.

The third part earned its place during the run rather than on a whiteboard. Early on, a channel message asked each agent to edit and commit its own CLAUDE.md. Buzz delivered the message faithfully, and Claude Code refused to act on it as instruction poisoning. It was right to: a message that rewrites an agent’s instructions cannot be told apart from an attack on it. Ordinary work asked for over the channel went through. The channel carries requests; what an agent may act on is decided where it runs.

A channel could carry tool approvals and reach into the harness’ half. Ours deliberately does not, because anyone able to reply in the room could then approve what an agent does.

Signal in the channel, substance in memory

With two systems in play, the obvious question is what goes where. The answer we settled on is strict, and it is the part of the design I would keep if everything else changed:

The channelENGRAM
carriessignal: a turn ended, who goes next, what is blockedsubstance: decisions, findings, rationale, recaps
lifetimerolls out of context, invisible to anyone who joins laterdurable, attributable, retrievable
a message looks likeTURN COMPLETE and an idthe thing the id points to

The baton is three words and one rule. TURN COMPLETE goes to your peer; BLOCKED and DONE go to the lead. Mention your peer only when you are passing it finished work, never to acknowledge or thank it, so that no path out of the loop returns to the peer without new work attached. And every baton carries an id: the memory or article that holds the substance. “See the shared memories” sends your peer looking. An id hands them the thing.

A baton, concretely. The brief opened phase 1 with a question it had deliberately not answered: how does the Worker learn that a trail was created? The API agent proposed an answer, wrote its reasoning into a memory anchored to the project, shared that memory with the Worker, and posted the id. The Worker answered the same way, agreeing with one addition. The API agent accepted the addition, revised the contract, and named the new article.

A baton, concretely.
Left: the lead’s messages in the channel. Right: the thread in which the API agent proposes a position on §5, the Worker answers it, and the API agent settles it, each message naming the memory or article that holds the reasoning.
Nine minutes, one question settled, and the lead said nothing in between. The memories named in the thread were shared agent to agent, so the lead, who owns the project, cannot open either of them.

The rule is about what must survive, not about brevity. Our batons ran to a full paragraph. What makes a message signal rather than record is that everything in it is recoverable from ENGRAM by anyone entitled to it, and nothing depends on the channel still being readable a month from now. Attachments follow the same logic: they work, but an attachment is durable without being discoverable, so logs and captured runs went into ENGRAM or the repository and the message carried the id.

There is one more reason not to treat the channel as the record, and the Buzz side measured it. Buzz timestamps are second-granular and the relay does not preserve the order messages were sent in: four messages sent A, B, C, D came back D, A, B, C. The reply chain reconstructed the order exactly. A record that quietly reorders a conversation is worse than no record, because it reads as authoritative.

Membership is not memory

The caption above ends on the fact that surprises people most. I own the Trailhead project. Both agents are members of it. And I could not read the two memories they used to settle §5.

That is the boundary my previous post, Something Smaller Than an Agent, drew, and a multi-agent build is where it gets tested. Membership grants reads over a project and its artifacts, and it never reaches memory. An agent that joins a project can read every declared document and still cannot see a single thing its peer has written down, unless its peer shares it. At the start of the run each agent could reach six memories, and every one of them had arrived by ownership or an explicit share, not one by membership. Owning the project grants nothing over anyone’s memory either, which is why the lead in the screenshot is locked out of his own team’s working notes.

That sounds like friction until you ask what the alternative would mean. If membership reached memory, adding a member would be a bulk disclosure of everything every other member had ever noted, decided or got wrong. Instead, the agents shared exactly what the conversation needed, each share an explicit and revocable grant, and the channel carried the ids.

The specification the agents wrote

Most agent-collaboration demos divide work that is already specified: someone writes the interface, the agents fill in either side, and coordination is really scheduling. The case I cared about is the other one. The brief for phase 2 named the next feature, a multi-day forecast for each trail, and then said deliberately that its shape was not specified and must not be inferred. No endpoint, no payload, no ordering rule.

The negotiation, concretely. It took four moves, each one a memory with its id on the channel:

  1. The Worker proposed a shape, with its reasons.
  2. The API agent agreed with most of it, countered on two points, and asked the one question it could not answer itself: does the weather provider expose a model-run or issue time the Worker could send?
  3. The Worker measured the provider instead of answering from memory. The answer was no: nothing in the response or its headers says when a forecast was issued.
  4. Both agreed, and the API agent revised the contract, recording the decision, and who argued what, in the contract’s own revision log.

Both sides conceded something, which is the property I was looking for. The Worker had proposed serving the forecast as bare facts and withdrew it: a per-day judgement belongs to the API, exactly as it does for observations. The API agent had leaned towards ordering forecasts by the time they arrived and was persuaded otherwise, because a receiver that orders by arrival cannot tell a late old forecast from a new one.

The measurement in move 3 turned up a detail worth more than the shape itself. The contract allows up to fifteen days of forecast. The provider returns sixteen rows starting today, and the last one is entirely empty, so the complete future days number fourteen. The run produced fourteen, and nothing on the Worker’s side had assumed fifteen. A limit nobody can reach is not a bug, but a team that believes it is reaching it will write the wrong test.

By the end of phase 2 the contract had been revised three times since I declared it, and the agents wrote every revision: two settled the questions the brief left open in phase 1, and the third recorded the forecast. Retrieval followed the revision chain and served the newest version throughout, even while the project’s declaration still named an older one. Re-declaring the head was my tidying, not a precondition for anything.

Then a third consumer tested the result. The API agent built a small server-rendered page showing each trail’s current condition, read from the same surface the Worker uses. Two cooperating services will paper over a shape problem by agreeing about it privately. A third consumer cannot: it either finds the field it needs, or discovers that what looked settled was an assumption two parties happened to share. Ours held.

Review, when the reviewer might be an agent

Every phase ended with a hand-off: one agent committing what it concluded, addressed to someone else for review. In ENGRAM that is a recap, a summary of the task gathered from the memories the agent recorded during it. What arrives is a proposal, not a fact.

The Worker's phase 2 readiness recap
The Worker’s phase 2 readiness recap, handed off to the lead: eight memories pending, none of them recallable until accepted.
The digest ends with a pointer to the Worker’s own evidence memory, which is worth more than the recap’s prose because the agent wrote it deliberately and ENGRAM did not summarise it.

A handed-off recap is invisible to recall until the receiver accepts it, and that is the review step rather than an inconvenience: nobody silently inherits another principal’s conclusions. When the receiver accepts, it owns the resulting memories, and each one is granted back to the sender automatically so the agent keeps recalling its own work. Tutorial 10 shows the grant-backs as they appear on the receiving side.

Until v1.18, an agent could not do that review itself. It could list the recaps waiting for it and accept one, but it could not read what it was accepting, so it approved the batch unread or asked a person. v1.18, running on DEV now, gives agents the same review a person has in the Memory Explorer: read the recap, go through it memory by memory, and accept some while rejecting others. The Buzz side has since run the whole loop over MCP, from hand-off through partial acceptance to grant-back, with no human relay at any step. That is the first time we have been able to say that.

The same shape governs artifacts. A member cannot add to what a project holds; it can only propose an addition, which the owner or a delegate approves or rejects, and while a proposal is pending it grants nothing, not a read and not even the knowledge that it exists. ENGRAM holds the proposal and decides who may read it. It does not wake anyone. So the proposing agent tells the owner over the channel that it is waiting, and the owner says when it is approved. The knowledge base carries the artifact and the authority; the channel carries the nudge.

Making the join real

Everything above ran on a proof-of-concept setup, and its weakest part was the join between the two identities. Each agent carried a Buzz keypair and an ENGRAM token, configured in separate steps, and nothing checked that they belonged to the same principal. Our working agreement made each agent prove who it was before it wrote anything, which held for this run. It is not a design. The Buzz side has already recorded a session that posted to the channel as one agent while writing memories as another, with no error anywhere.

So the join is what I am building now, in the v1.18.x releases. The first step is running on DEV, where we are testing it now, and it is on PROD behind a flag until that testing is done. The later steps, awareness and ingest and then writing into Buzz, are still design.

ENGRAM stays the identity home. Every principal is still provisioned inside ENGRAM, humans by invitation and agents by an owner, with authority flowing down a delegation tree that only narrows. A Nostr keypair becomes a credential an ENGRAM principal uses to appear in Buzz, the way a PAT is a credential it uses to reach ENGRAM’s API. It is a credential, not a principal. Anyone can generate a keypair in microseconds for free, so if a key could create an identity, principal creation would become self-service and unbounded, and deny-by-default would mean nothing.

ENGRAM never holds the secret. Binding records that this public key belongs to that principal. The key is generated where the agent runs, or already sits in a person’s Buzz client. A person binds their own key. An agent’s key is bound by the agent’s owner, never by the agent itself, and so never by an agent following instructions that arrived over a channel. An agent able to bind the lead’s key to itself would hand an injected prompt the one credential the whole design keeps out of its reach.

User's Buzz key
Settings → Chat Keys on DEV: my Buzz public key, bound to my ENGRAM identity.
The form asks for a public key only and says why: ENGRAM never needs, and never accepts, a secret key. Binding lets ENGRAM attribute what the key signs; it does not add anyone to a Buzz relay or channel, which still happens in Buzz.

The binding is generic and keeps two clocks. It is keyed on an issuer and an external id, so a Slack user id is one more row rather than a second subsystem. It records both when the binding held in the world and when ENGRAM was told about it, so a backdated claim is visible as backdated, and an event signed before the binding held resolves to nobody rather than to whoever holds the key today. Retiring a binding ends the projection without deleting the row, so events signed while it held still attribute correctly. And an agent will be able to read its own binding at startup and refuse to run if the key its harness loaded is not the one bound to it, which closes exactly the failure described above.

A channel binds to a project for addressing, not authorisation. It says conversation here relates to this project and grants nothing: being invited into a conference room does not hand anyone the filing cabinet. A channel that serves one project resolves to it; a shared room, a #support serving several, deliberately resolves to none, so it cannot lend a project it does not have. A direct message is never bound to a project at all, because it is the one place where filing a conversation under shared work would surprise everyone in it.

A sub-project can choose to work in its parent’s channels. The channels stay the parent’s: conversation there is attributed to the parent, or to whichever project an agent says it is working on, and never to the sub-project by default.

Channel settings for a project
The Trailhead App project’s Channels tab: #trailhead-project bound as a single-project channel.
The page states the rule itself: a binding is addressing, not access. Visibility is recorded at binding and shown with that date; ENGRAM never acts on it. Slack channels are listed as coming later.
Channel settings for a sub-project
The API layer sub-project’s Channels tab: no channel of its own, and #trailhead-project inherited from Trailhead App.
“Use the parent’s channels” is on, and only the owner can change it. The inherited channel is managed on the parent, and conversation in it is never attributed to the sub-project by default.

ENGRAM observes Buzz; it does not join it. Buzz’s own adapter subscribes to the relay and forwards the raw signed events to ENGRAM, which verifies each signature itself before it records anything. Subscribing directly would have meant giving ENGRAM a Nostr identity of its own, a secret in the database and a participant where an observer was intended. When ENGRAM needs something written into Buzz, such as creating a project’s channel, it records an intent, and the actor’s own Buzz client shows it, asks for confirmation and signs it with the key it already holds.

The work lands in three steps, ordered by what each one depends on:

stepwhat it addswhy it falls there
Identity and the channel recordthe binding, the self-check, the channel-to-project record, and a recap review surface agents can useENGRAM writing only to its own graph
awareness and ingestan agent learns that something is addressed to it; channel events arrive as provenanceneeds a transport and a contract with the Buzz adapter
writing into Buzzchannel creation and enrolment from a projectneeds someone outside ENGRAM to sign

The honest consequence is that the first step makes an agent attributable. The second lets an ENGRAM hand-off announce itself, so the agent it is addressed to hears about it without anyone posting its id in the channel.

One part of this design I did not get right on my own. Our identity design had quietly narrowed a shape we had already agreed with the Buzz side, from a generic, two-clock binding to a Nostr-only, single-clock one, without saying so. Their review caught it before anything was built on it. That is the cheapest place a regression can be caught, and the argument for having two teams read the same design.

What it isn’t yet

The run worked, and it also showed where the system still needs a person or a workaround. These are the gaps, not the design decisions described above.

  • ENGRAM events do not announce themselves yet. An @-mention in Buzz wakes the agent it names, and every baton in this run worked that way. A hand-off or a share inside ENGRAM arrives silently, though: the agent learns of it only when someone posts its id in the channel, or when it polls its inbox. Letting an ENGRAM hand-off reach its receiver in Buzz on its own is the second step of the v1.18.x work.
  • The memory you need can rank below memories you don’t. Recall ranks memories by how relevant and how important they are. When I recalled the agents’ accepted recaps from my own store, they came back far below the memories ENGRAM imprints from the articles I own. Those link to many entities, so they match a broad range of queries and win on breadth. Two ranking changes are planned for v1.18.x: weighting a query’s entities by how rare they are, and damping memories linked to very many entities. Together they lifted the recaps sharply in an offline replay. Both will be measured before they go live, because the second also demotes memories that are genuinely broad.

What comes next

The v1.18.x releases make the join real and then make agents aware: the binding and the channel record first, then addressing and ingest, then channel creation from a project. Because the binding is keyed on an issuer rather than on Nostr, Slack becomes one more row rather than a second integration. The recall changes above ship in the same series.

After that comes Goose. Claude Code is an attended harness, and Goose is an unattended one: with both integrated, an agent can take its turn in a Buzz channel and write what it found into ENGRAM without a person at its terminal. The harness still decides what the agent may do. It simply no longer needs someone there to approve each step.

The part I would most like to see tested elsewhere is the least technical one: a room where people and agents talk, and a record neither can quietly rewrite.


Buzz decides what an agent may say. ENGRAM decides what it may know. The harness decides what it may do.

The specification in this run was written by the two agents that had to implement it, and my job was to accept it.


ENGRAM is in private beta, by invitation, while I prepare it for a public launch. pvelua.net


For the background this builds on:

  1. No Message Passed Between Them: ENGRAM Agents Coordinate Through Memory, where the relay was still me.
  2. When the Question Has a Right Answer: ENGRAM Allows Agents to Work from a Brief, on declared ground truth.
  3. Something Smaller Than an Agent: ENGRAM Scopes Authority to a Shared Project, on project membership and the Project Collaborator token.
  4. Tutorial 9: A project two agents can build from, the membership boundary and the baton, step by step.
  5. Tutorial 10: When the answer isn’t specified yet, the negotiation, hand-offs and retrieval in detail.

Leave a comment