Give your agent harness a memory

A hands-on ENGRAM walkthrough · July 23, 2026

Provisioning an orchestrator and its workers in ENGRAM, then letting them coordinate through shared, governed memory instead of a message bus.

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:

  1. A harness gets a brain between sessions. Its operating knowledge persists, stays current, and corrects itself without losing history.
  2. 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.

Background reading: The Layer a Harness Can't Own.
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

RequirementNotes
ENGRAM v1.12.8+earlier builds lack the project anchor, the UI history view, or correct hand-off scope resolution
SHARING_ENABLEDotherwise grants are created but inert
Sub-agent delegation enabledfor create_sub_agent
FACT_UPDATE_ENABLED=hybridfor Part 1
An MCP clientClaude Desktop, Claude Code, or any MCP host
Two repo URLsidentifiers 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:

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.

The ENGRAM Knowledge Base Library showing the seed brief article with provenance back to the conversation that produced it
The brief in the Knowledge Base, with provenance back to the conversation that produced it.
The Graph Explorer showing the entity cluster extracted from the seed brief — the vocabulary every agent will recall against
The same brief after extraction — this is the vocabulary every agent will recall against.
Alternative: if a public article is unacceptable, give each worker its own seed content. More faithful to a fleet that earns its own vocabulary, at the cost of more setup.

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.

A summary view of the four operating facts stored as long-term memories The Memory Explorer long-term memory list showing the four operating facts, each badged AUTHORED and ACTIVE
Four operating facts, each badged 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:

The version history of the token-budget memory showing a manual revise supersession at confidence 1.00
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.

The version history showing an automatic T1 supersession of the staging fact at confidence 0.98, next to the manual revise for contrast
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.

A third path exists: revise an article, and the memory imprinted from it is superseded deterministically — the version bump is authoritative, nothing is inferred. It renders as 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
Point-in-time reconstruction: one store, one query, three different answers depending on the as_of timestamp
One store, one query, three answers.

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.

⚠️ Use 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 Admin delegation tree showing one orchestrator, three workers, and one human root
One orchestrator, three workers, one human root.

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."

⚠️ Keep repo → project 1:1. Repo resolution needs a single match. If a repo was already ingested from a code session, a :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.
The list_projects output showing both projects, each with the repo_url workers will declare
Two projects, each with the 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:

The Memory Explorer showing three worker findings with three distinct owners — attribution survives the share
Three findings, three owners. Attribution survives the share.

Fan-in: the orchestrator recalls per project

recall(active_repo="github.com/pvelua/checkout-service",
       query="auth suite flake token refresh retry jitter backoff")
memoryownervia
token-refresh retry has no jitterScout (user_008)shared
added jittered backoff, 50 clean runsForge (user_009)shared

Switch projects, and the world changes:

recall(active_repo="github.com/pvelua/billing-service",
       entities=["project-billing-service"])
memoryownervia
refund path re-enters on retryWarden (user_010)shared
Fan-in recall results showing the via: shared column — one orchestrator, two projects, strict isolation
One orchestrator, two projects, strict isolation.

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:

CauseTest
Wrong project anchorthe memory is scoped to a project you haven't declared
Wrong entity seedyour 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",
)
Use 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:

  1. The recap is owned by you, not by the agent that wrote it — the id is namespaced to the receiver.
  2. It lands in your Suggestions inbox, badged reflection, attributed to claude-orchestrator (agent).
  3. Nothing is recallable yet. Query the memory's own literal wording and you get nothing back. Suggested means suggested.
The Suggestions inbox showing a hand-off awaiting review, with the authoring agent named
A hand-off awaiting review, with the authoring agent named.

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:

The Sharing tab showing a grant-back share with origin: grant_back — you own a memory an agent authored, and it can still read it
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

  1. Agents start with no vocabulary. Seed the graph before workers write, or their memories store and never return.
  2. Recall is entity-seeded. Pin entities=[…]; carry ids forward. Free-text alone competes with your whole graph.
  3. Project scope needs an anchor. active_repo or active_project_id, every time.
  4. Keep repo → project 1:1. list_projects is own-scoped and won't show you someone else's.
  5. Grantee opt-in runs one way. The receiver must be shareable.
  6. Grantee is a user_id/email, never a display name.
  7. MCP-authored working memory won't appear in the UI Working panel — different session.
  8. Use Z timestamps on the point-in-time API.