Give your agent harness a memory
A hands-on ENGRAM walkthrough · July 23, 2026
1. Why this tutorial exists
An agent harness — OpenClaw, NVIDIA NemoClaw, Claude Code's subagent system, Cowork, or one you wrote yourself — owns the loop. It spawns workers, routes tasks, sandboxes execution, enforces budgets. What it does not own is what its agents know, because a harness process dies at the end of a run and takes its context with it.
That layer is horizontal to every harness, and it's the one ENGRAM provides. The arrow points inward: a harness reaches into ENGRAM over MCP to remember and recall. ENGRAM never spawns, routes, or supervises anything.
Two claims follow, and this tutorial demonstrates both on a live instance:
- A harness gets a brain between sessions. Its operating knowledge persists, stays current, and corrects itself without losing history.
- The coordination is the memory. If every agent writes findings to one associative store and reads back under governed scope, you get coordination without a board, a mailbox, or a broadcast channel.
There's also a governance boundary worth naming up front. Your harness governs what an agent may do and spend. ENGRAM governs what an agent may know and pass on. Those are different planes, and conflating them is how knowledge leaks between tenants.
Prior tutorials: Tutorial 1 (peer sharing), Tutorial 2 (sub-agents and PATs), Tutorial 3 (fact update). This one assumes you've skimmed them.
2. What you'll build
A three-worker fleet under one orchestrator, working two services, coordinating entirely through memory:
user_003 (you — human root: quota, audit, revocation)
└── claude-orchestrator agent, Agent Manager, shareable
├── Scout researcher → checkout-service
├── Forge coder → checkout-service
└── Warden reviewer → billing-service
By the end: Scout diagnoses a bug, Forge fixes it, neither ever talks to the other, and the orchestrator recalls both findings — isolated per project — then hands the whole thing back to you for review.
3. Prerequisites
| Requirement | Notes |
|---|---|
| ENGRAM v1.12.8+ | earlier builds lack the project anchor, the UI history view, or correct hand-off scope resolution |
SHARING_ENABLED | otherwise grants are created but inert |
| Sub-agent delegation enabled | for create_sub_agent |
FACT_UPDATE_ENABLED=hybrid | for Part 1 |
| An MCP client | Claude Desktop, Claude Code, or any MCP host |
| Two repo URLs | identifiers only — ENGRAM never clones anything |
Each identity — you, the orchestrator, each worker — is a separate MCP server entry carrying its own personal access token. Identity is pinned server-side from the token, so an agent can never act as another.
Part 0 — Seed the vocabulary
This step is not optional, and skipping it is the single most common way to lose an afternoon.
ENGRAM's recall is entity-seeded: a query is resolved to entities in the graph, and memories surface by association. A memory about a concept that doesn't yet exist in the graph will be stored — and will not come back.
A freshly provisioned agent has an empty graph. Delegation gives it an identity and credentials; it does not give it vocabulary. And because sharing is fail-closed, your private content does not flow down the tree automatically.
So write a short brief to the KB before any agent runs. This is ordinary Layer 1 work with tools you already have — no new machinery.
A human (or the orchestrator) writes the brief: the services in play, the worker → repo assignment, the operating knobs, and references to any SSH keys or secrets. References only — ENGRAM never sees credentials. Your harness resolves them at dispatch; the memory layer just needs to know they exist and what they're called.
Two ways to create it, whichever fits your workflow:
- In the ENGRAM UI — Knowledge Base → new article. Fine for a one-off brief a human is writing anyway.
- Over MCP —
create_articlefor a chat- or agent-authored draft, orsync_articlewhen the brief lives in a repo and should stay keyed to(repository, file_path)so re-runs supersede cleanly rather than piling up duplicates.
Then make it readable by the agents that need it. A brief is private by default, and private
content does not reach your sub-agents; set it to public so the whole fleet
resolves the same vocabulary. (Visibility is a one-way private → public toggle, so decide
deliberately.)
The orchestrator reads it back with get_article or recall — the brief is both
the fleet's instructions and the seed for everything they'll recall later.
Ingestion runs as a background job (parsing → extracting → complete), so poll with a
recall until the entities resolve before moving on.
Part 1 — The harness's operating memory
Before any agents exist, put the harness's own operating facts somewhere that outlives the process.
Everything below is an MCP tool call — the same remember /
revise / forget verbs your agents will use, invoked here from your own MCP
server entry so the facts land in your store. That matters: a human root sees only their own
memories, so authoring these under your identity is what makes them visible in the Memory Explorer and
eligible for your contradiction detection. (Code blocks show argument shape, not any particular language
binding.)
remember(content="The harness assigns Claude Sonnet to coder agents and "
"Claude Haiku to test-runner agents.", scope="long_term")
remember(content="The harness caps concurrent sub-agents at 5 per run.",
scope="long_term")
remember(content="The per-agent token budget for checkout-service agents "
"is 100k tokens per run.", scope="long_term")
remember(content="The deployment target for checkout-service agents is staging.",
scope="long_term")
Authored memories are active and recallable immediately — no consolidation wait.
AUTHORED · ACTIVE.Correcting a fact on purpose
The budget was raised. Don't edit it — supersede it:
revise(memory_id=<budget-100k>,
new_content="The per-agent token budget for checkout-service agents "
"is 250k tokens per run.")
Recall now returns only 250k. The 100k belief is retired, not destroyed:
via revise · manual · conf 1.00 — a deterministic, by-id correction.Correcting a fact without saying so
Now the interesting one. The deployment target moved to production. Write it as an ordinary note — no
revise, no hint that anything conflicts:
remember(content="The deployment target for checkout-service agents is production.",
scope="long_term")
Within a few seconds, ENGRAM embeds the new belief, finds the conflict with "staging" via near-neighbour search, judges it a direct contradiction at high confidence, and retires the old one. No human step. No inbox entry.
via auto · T1 · conf 0.98 — the automatic lane, next to the manual one for contrast.Capture these two History tabs side by side. They're the same supersede-don't-delete mechanism arriving by two different governance paths, and the UI says which decided.
deterministic · conf 1.00.
Asking what you believed last Tuesday
Because every version carries validity bounds, the same query answers differently depending on when you ask (note, this is internal API used for illustration purposes):
GET /api/memory/long-term/point-in-time?user_id=<you>&as_of=2026-07-22T00:52:00Z
→ 100k · staging
GET /api/memory/long-term/point-in-time?user_id=<you>&as_of=2026-07-22T00:57:00Z
→ 250k · staging ← a state no single memory ever held
GET /api/memory/long-term/point-in-time?user_id=<you>&as_of=<now>
→ 250k · production
The middle probe is the argument for the whole model: it composes across two independent supersession chains at one instant. No per-memory history view can express that.
Z-suffixed UTC. In a query string, + is the encoded form
of a space, so …T00:57:00+00:00 arrives as …T00:57:00 00:00 and fails to parse.
Use Z, or percent-encode as %2B00:00.
Undoing a retraction
forget retires a belief without a successor; revert brings it back with content,
title, and provenance intact. This is what makes trusting the automatic lane reasonable — anything the
detector retires is one call from returning.
Part 2 — Grow the team
Now provision workers. Each becomes a real ENGRAM identity, not a label.
create_sub_agent(name="Scout", agent_role="researcher") # → user_008
create_sub_agent(name="Forge", agent_role="coder") # → user_009
create_sub_agent(name="Warden", agent_role="reviewer") # → user_010
issue_sub_agent_pat(user_id="user_008", pat_name="scout-mcp-server")
# → raw_key returned ONCE
Register each PAT as its own MCP server entry (engram-scout, engram-forge,
engram-warden). Rotate with rotate_sub_agent_pat if a key passed through
anywhere it shouldn't have.
Every sub-agent records owner_user_id (its provisioner) and owner_root_user_id
(you), with delegation depth capped at two. You keep quota, audit, and a kill switch
over the whole subtree — disabling an agent revokes all its tokens.
The grantee opt-in that trips everyone up
Automated agents are not valid share targets by default. Someone must opt them in.
Direction matters: in this design the workers grant up to the orchestrator, so it's the
orchestrator that must be shareable: true. The workers can stay
default-deny. Get that backwards and every share_memory returns 403.
list_grantees() # → user_id + email for each eligible recipient
Grantee identity is a user_id or email — not a display name.
list_grantees is how a worker resolves "the orchestrator" to something it can actually pass.
Provision the projects
New in v1.12.x: the orchestrator agent sets up the team's projects from its plan. A project is a scoping anchor — ENGRAM tracks it, but doesn't create the repo, hold credentials, or run anything.
list_projects() # ALWAYS first — see the warning below
create_project(name="checkout-service",
repo_url="github.com/pvelua/checkout-service")
create_project(name="billing-service",
repo_url="github.com/pvelua/billing-service")
The repo_url is the shared key that makes "the project you created" the same thing
as "the project the worker works in."
:Project for it already exists — reuse it (just declare
active_repo) rather than minting a second. Note list_projects is
own-scoped: you see only projects you provisioned, so it won't reveal one
created under another identity. Cross-owner reuse goes through active_repo resolution, which
fails loud on ambiguity.
repo_url workers will declare.Part 3 — Coordination is the memory
Dispatch is your harness's job — git checkouts, branches, credentials, sandboxes. ENGRAM's job starts when a worker learns something.
Fan-out: workers record, project-bound
# as Scout, on checkout-service
remember(
content="In checkout-service the token-refresh retry loop has no jitter, "
"so parallel runs collide and the auth suite flakes intermittently.",
owner_scope="project",
active_repo="github.com/pvelua/checkout-service",
scope="long_term",
)
share_memory(selector_kind="memory", selector_id=<mem id>,
grantee="user_007", bound_scope="project")
Forge does the same on checkout after shipping a fix; Warden on billing.
Two details carry real weight:
owner_scope="project"requires an anchor. Omitactive_repo/active_project_idand you get a 400 telling you exactly what's missing. Earlier builds accepted the write and silently stranded it — a memory that existed but could never be recalled. Fail-loud is a feature; don't route around it.bound_scope="project"confines where the grantee may use what it borrows. A finding shared for checkout cannot surface while the orchestrator works on billing — even if it asks directly.
Fan-in: the orchestrator recalls per project
recall(active_repo="github.com/pvelua/checkout-service",
query="auth suite flake token refresh retry jitter backoff")
| memory | owner | via |
|---|---|---|
| token-refresh retry has no jitter | Scout (user_008) | shared |
| added jittered backoff, 50 clean runs | Forge (user_009) | shared |
Switch projects, and the world changes:
recall(active_repo="github.com/pvelua/billing-service",
entities=["project-billing-service"])
| memory | owner | via |
|---|---|---|
| refund path re-enters on retry | Warden (user_010) | shared |
Then try to break it. Ask for checkout's findings while anchored to billing:
recall(active_repo="github.com/pvelua/billing-service",
entities=["project-checkout-service"],
query="auth suite flake token refresh jitter backoff")
# → []
Nothing. The grant's bound_scope holds against a direct request. That empty result is the
security property, demonstrated rather than asserted.
Step back and notice what just happened: Scout diagnosed a defect, Forge fixed it, and the two never exchanged a message. They shared a project and a graph. The coordination was the memory.
Debugging an empty recall
You will hit an empty result. There are exactly two causes, and confusing them costs hours:
| Cause | Test |
|---|---|
| Wrong project anchor | the memory is scoped to a project you haven't declared |
| Wrong entity seed | your query text didn't resolve to the entities the memory is linked to |
Separate them by pinning entities explicitly. In our run,
recall(active_repo=billing, query="billing-service payment intent idempotency refund dedup")
returned nothing — but the same anchor with entities=["project-billing-service"] returned
Warden's finding immediately. The anchor was fine; the free-text query never resolved. Carry
entity_ids forward from previous results instead of re-describing what you want in
prose.
Part 4 — Hand the work back to a human
The team's findings are owned by agents. To make them yours, hand them off.
suggest_memories(
items=[...], # the synthesis, in the orchestrator's words
context_ref="harness-demo/act3-fanin",
hand_off_to="user_003", # you
task_ref="checkout & billing agent findings",
)
commit_task_memories instead when the agent is reflecting over working memory it
authored itself. An orchestrator whose knowledge arrived through grants has no working memory to
reflect on — suggest_memories takes an explicit list. Hand-off semantics are identical.
Three things are true the moment that call returns:
- The recap is owned by you, not by the agent that wrote it — the id is namespaced to the receiver.
- It lands in your Suggestions inbox, badged
reflection, attributed toclaude-orchestrator (agent). - Nothing is recallable yet. Query the memory's own literal wording and you get nothing back. Suggested means suggested.
Accept it, and three things happen at once.
You become the owner. The memories flip to active under your
user_id — you can now grant them onward, which is only possible because ownership genuinely
transferred.
The memory lands user-scoped. A hand-off to a human is recallable
everywhere, with no project anchor needed — which is the right home for a cross-project summary a person
is meant to act on. (Recaps that stay agent-owned behave differently: a single-project context gets a
stamped project_id, preserving per-project isolation for code sessions.) You don't configure
this; it's resolved from the owner kind at write time.
recall(query="auth-suite flake jittered backoff refund dedup key")
# → the recap, via: own, scope: user
The worker gets it back. An automatic share with origin: grant_back returns
recall to the agent that did the work, so it doesn't strand on a memory it authored:
origin: grant_back — you own a memory an agent authored, and it can still read it.
But only within the context the work happened in. The grant carries
bound_scope: "context", so the orchestrator sees it when working that context and not
otherwise:
# as the orchestrator, context declared
recall(active_context_id="harness-demo/act3-fanin", query="auth-suite flake …")
# → the recap, via: shared, owner_user_id: <you>
# same agent, same query, no context declared
recall(query="auth-suite flake …")
# → []
A borrowed memory stays borrowed. Grant-back returns exactly the access needed to keep working, and nothing wider.
This is the review gate the whole design turns on: ENGRAM never grows your recallable memory behind your back — and never widens anyone else's either.
What this does and doesn't give you
Does: durable operating knowledge across runs · self-correcting facts with full history · per-agent attribution · project-scoped isolation · fail-closed sharing · a human review gate on anything inferred.
Doesn't: spawn, route, sandbox, or budget your agents. That's your harness. ENGRAM holds no credentials and runs no code — it is the layer underneath, not a meta-harness.
Appendix — Gotchas, ranked by how much time they'll cost you
- Agents start with no vocabulary. Seed the graph before workers write, or their memories store and never return.
- Recall is entity-seeded. Pin
entities=[…]; carry ids forward. Free-text alone competes with your whole graph. - Project scope needs an anchor.
active_repooractive_project_id, every time. - Keep repo → project 1:1.
list_projectsis own-scoped and won't show you someone else's. - Grantee opt-in runs one way. The receiver must be shareable.
- Grantee is a
user_id/email, never a display name. - MCP-authored working memory won't appear in the UI Working panel — different session.
- Use
Ztimestamps on the point-in-time API.