Nexus · coordination← The tuple space

Coordination Between Sessions and Agents

This page shows how you coordinate Claude Code sessions and their agents. You can ask a session in another repository to do a job, correct a background agent while it works, and check that every agent reported. You ask Claude in your own words. Claude writes each request and each correction as a record in the Nexus tuple space, and the hooks record each agent's report there. So your session continues its own work, and the other session acts when it is ready.

Words like <PEER NAME> are placeholders, and the prompts are examples. If tuples are new to you, read The Nexus Tuple Space first, because it explains the operations underneath.

Terms used on this page
TermMeaning
engineThe Nexus service that owns the database and handles every tuple call.
tenantOne customer of the engine. Every store is separated by tenant, and one credential gives access to one tenant.
session idThe id of one Claude Code conversation, a UUID. It is the session's mailbox address.
nameThe short label the harness shows for a session, such as nexus-4a. It can change when a session resumes.
agent idThe id the harness gives an agent: the letter a and 16 hexadecimal digits. It is the agent's mailbox address.
tupleOne record in the tuple space. A message, a directory entry, and a ledger record are each one tuple.
subspaceA named part of the tuple space, such as the mailbox of one agent. Every tuple belongs to exactly one subspace.
templateThe schema for one kind of subspace: required keys, allowed dimensions, lifetime, and whether take operations are allowed. The engine refuses a tuple that breaks it.
messageOne tuple in a mailbox. It has a sender, a kind, a correlation id, and a short body.
mailboxThe part of the tuple space that holds the messages for one address.
directoryThe part of the tuple space that maps each name to the session id that holds it.
ledgerThe part of the tuple space where hooks record that an agent started and that it reported. Any session can read it.
dispatch fileA local file for each session, outside the tuple space. Hooks add a row when an agent is expected, when it starts, and when it reports.
censusThe nx-hook expectations_census report. It compares the agents a session expected with the agents that started and the agents that reported.
hookA script that Claude Code runs at a fixed point: session start, each prompt, and agent start and stop.
drain hookThe hook that runs at each prompt. It takes and acks whatever is still waiting and shows it to Claude, whether or not the channel delivered it first.
SendMessageThe Claude Code tool that sends a message to an agent by name or to a live session.
channelThe push path from a session's own nexus MCP server to Claude, opt-in per launch. Once the session subscribes, it notifies Claude when a new message or board post arrives.
pingThe channel notification for a new message or board post. It names where the tuple is, never the body. For a message, claiming and acting on it is the reader's own next step.
rendezvousA meeting through a tuple: one side writes it, and the other reads it or waits for it. The two sides need not be running at the same time.
take operationA tuple_in call. It gives a message to one reader only, for a limited time called the lease.
ack, nacktuple_ack removes a message that the reader took. tuple_nack returns the message to the mailbox.
dead letterA message that failed three times. Anybody can read it, and nobody can take it.
correlation idAn id that a request carries and its reply repeats, so that the two can be paired.
1

What it gives you

3 min · reading only

For work that must not get lost, or that other sessions need to see, Claude uses the tuple space, the coordination part of Nexus persistent memory. Its records live in the engine, outside every conversation, so they survive compaction and /clear, and every session on the engine can read them.

your session engine peer session request waits reply waits writes a request keeps working ping: the reply is there takes the reply busy with its own work takes the request does the work acks with a reply time →
Your session writes a request and keeps working. The request waits in the engine until the peer is ready. The reply waits until your channel pings.

Claude uses SendMessage to inject something into a live agent or session, and it uses SendMessage together with the tuple space. When the harness delivers an agent's report, the hooks also record that report in the ledger, as a tuple. What the tuple space adds is this persistent record of an interaction. Nexus keeps long-form content in its memory, and a tuple points to that content by reference, not by carrying it as its own value. So a reader learns that something is there without having to consume the thing itself. The exchange has three steps: send, continue, and react to the answer.

What you can rely onHow it works
You can see the coordinationEvery live message, name, and agent record is a row that every session on the engine can read. From any session you see what waits, what is in progress, and who reported.
The records surviveTuples live in the engine, so a restart or the end of a session does not erase them either. What a session wrote stays there for anyone who asks for it by address: messages for seven days, ledger records for 90.
The other side need not be thereA writer can end before any reader starts. An agent's report record stays in the ledger after the agent is gone. A message stays in the mailbox after the session that sent it ends. A reader can also wait for a tuple that nobody has written yet: a rendezvous. This holds for sessions and agents alike.
Every record has a schemaEach kind of tuple has a template registered in the engine: its keys, its dimensions, how long it lasts, and whether a reader can take it. The engine refuses a tuple that does not match, so a reader knows what it will get. A SendMessage message is plain text.

Claude uses both, because they do different jobs:

  • Claude uses SendMessage when the reader is live and the exchange is quick: steering a running agent, or a question for a live session. It can also ask for one notice when a session on this machine goes idle.
  • Claude uses the tuple space when the work must outlast the conversation, because others must see it or the other side may not exist yet.

One record also serves every reader. A directory entry or a ledger record is written once, and every session that reads it sees the same row. A SendMessage call reaches one recipient. The appendix compares the two tools point by point.

The mailbox and the ledger deliver to one reader or record one fact. Three more subspaces build on the same three operations for coordination they do not cover. A board posts one announcement to every session watching a topic. A queue hands each task in a shared list to exactly one worker. A lock lets one process at a time hold a named resource. The Nexus Tuple Space covers all three. This page stays with sessions and agents.

2

Ask a peer, and keep working

4 min

This is the everyday exchange. Claude sends a request to a peer by name and continues its own work, and the reply arrives later as a ping.

You might say
Ask <PEER NAME> to <DEPLOY THE SERVICE>. The details are in memory <PROJECT>/<TITLE>. Tell me when the reply arrives.
Send <PEER NAME> a note that <THE RELEASE IS PUBLISHED>.
What happens

Claude calls mailbox_send with the peer's name, the kind request, and a new correlation id. The tool looks the name up in the directory and writes to the mailbox of the session that holds it. The request waits there until the peer is ready. The peer takes it, does the work, and acks it with a reply. The reply keeps the correlation id, which is how your session pairs the answer with its question.

Your session does not wait. When the reply arrives, minutes or days later, the channel pings, and the drain hook usually claims, acks and renders it with that same prompt; Claude acts on the reply, claiming nothing. Without the plugin's hooks, Claude claims it itself with tuple_in, then acks. If the channel never reaches Claude, the drain hook delivers it at your next prompt. A note needs no reply: it waits in the peer's mailbox for up to seven days, and the peer reads it when its own work allows.

What you see
The send
mailbox_send  to: nexus-8c   kind: request   correlation_id: r-41
→ {"to": "cdeb12b5-…", "address_kind": "session", "from": "5b354c36-…"}
The reply, taken and acked
tuple_in   subspace: mailbox/5b354c36-…   keys: {to: 5b354c36-…}   timeout_s: 25
{
  "tuple": {
    "dims": {"from": "cdeb12b5-…", "kind": "reply", "correlation_id": "r-41", "address_kind": "session"},
    "body": "…"
  },
  "claim_id": "784e820d-…"
}
tuple_ack  … → Acked

Other machines

Nothing in the exchange depends on where the peer runs. Machines share one tuple space when they use one engine, as they do in cloud mode. So a session on a laptop can hand a long job to a session on a server. In local mode each machine has its own engine and its own space. Only sessions of one tenant ever share a space.

Ask <REMOTE NAME> to run <THE FULL TEST SUITE> and send me the result.
A ping from a session on another machine
# the send, on the second host
→ {"tuple_id": "8ce10d62…", "to": "5b354c36-d317-4a00-aa15-f567e65a7209", "address_kind": "session", "from": "5cab61d9-3c73-435c-b2f8-c75546df8db7"}

# the notification, in this session
nexus mailbox message: subspace mailbox/5b354c36-…, tuple 8ce10d62…. If its body is rendered with this message, the mailbox hook already claimed and acked it: act on it, claim nothing. If not, claim it yourself with tuple_in("mailbox/5b354c36-…", {"to": "5b354c36-…"}), then act: tuple_ack (with a reply for a request), tuple_nack, or tuple_release with the claim id. The waiter holds no claim.

Good practice

  • Send, then continue, because the channel pings you when the reply arrives.
  • Put long content in memory and send its title. A message is a pointer, and its body holds at most 4096 bytes.
  • Use a new correlation id for every request, so that the reply pairs with it.
  • Send to a name only when one live session holds it, because the name is looked up at send time. Otherwise, send to the session id.
  • Use the kind to tell the reader what to do. Use request for a message that needs a reply and notice for one that needs none. The engine does not check which kind you use.
3

Correct an agent, and check the reports

3 min

A background agent works while your session continues, and every agent has its own mailbox at its agent id. You can correct an agent while it works, and at the end you can check that every agent reported.

You might say
Tell the agent that works on <THE TASK> that <THE CHANGE>.
Start one agent for each <MODULE>, and check that every one reported.
What happens

To correct an agent, Claude sends the correction to the agent id with mailbox_send and continues. The correction waits in the agent's mailbox, so the agent does not have to be listening at that moment. The mailbox skill tells the agent to read its mailbox with tuple_in before it reports. That is how a correction can reach the agent before its report is final, though no hook enforces it.

The hooks record every agent's start and report, so a missing report shows up in the census instead of being forgotten. They keep one copy in a local file for this session, and one in the tuple space, where any session can read it for 90 days.

What the agent sees when it starts
Claimant id: a4873be6337bcac8c — mailbox: mailbox/a4873be6337bcac8c
To check the agents of a session
nx-hook expectations_census <SESSION-ID>
What you see
…
ROWS        expect=28 start=28 reported=28 …
…

The session expected 28 agents, and all 28 started and reported.

4

See what every agent did

2 min

The ledger keeps one start record and one report record for each agent, and every session on the engine can read it. Claude answers from those records, not from what the conversation still remembers.

You might say
Which agents did this session start, and which reported?
Which agents did <PEER NAME> start, and which reported?
Which session holds the name <NAME>?
What happens

For this session, Claude reads ledger/<session id> with tuple_rd. For a peer, it first finds the peer's session id in the directory and then reads that session's ledger. One set of records answers every session that asks, and nobody has to relay it. The directory answers the third question, and every session on the engine sees the same entry.

What you see
The ledger records for one agent
tuple_rd  subspace: ledger/5b354c36-…   keys_pattern: {agent_id: a52466bfafa7e0d74}
  keys: {kind: start,  agent_id: a52466bfafa7e0d74}   dims: {agent_type: Explore}                   created_at: 15:07:32Z
  keys: {kind: report, agent_id: a52466bfafa7e0d74}   dims: {agent_type: Explore, verify: absent}   created_at: 15:12:09Z
The directory entry for a name
tuple_rd  subspace: directory/nexus-4a
{
  "keys": {"name": "nexus-4a"},
  "dims": {"session_id": "5b354c36-d317-4a00-aa15-f567e65a7209"},
  "expires_at": "2026-09-15T15:12:13Z"
}
5

Watch it and steer it

3 min

Because every tuple is a row in the engine, you can look at the coordination directly. Ask Claude, or read the engine from a terminal. Either way you only read, so looking never takes a message.

You might say
Is my channel push working?
What is waiting in your mailbox?
What happens

For the first question, Claude reports what nx doctor's channel row shows for this session: whether the waiter is alive, when it last woke, how many messages it has announced in total, and how many mailboxes had a reference sent and not yet superseded at its last wake. That last count is the waiter's own bookkeeping, not a live query, so a message acked after that wake still counts until the next one. For the second, it reads its mailbox with tuple_rd and takes nothing.

What you see: the channel row
✓ tuples.channel_delivery: waiter alive; last wake 7s ago; announced=0, pending=0

The channel needs a flag on every launch: claude --channels plugin:conexus@nexus-plugins or claude --dangerously-load-development-channels server:nexus. Getting started, "Turn on push delivery" has the exact steps, including how to make it stick with an alias.

From a terminal
nx tuple list --prefix mailbox/  # every mailbox, with counts
nx tuple stats mailbox/<SESSION-ID>  # one mailbox
nx tuple rd mailbox/<SESSION-ID> -n 20  # read without taking
nx tuple directory <NAME>  # who holds a name
nx doctor  # includes six tuple rows
What Claude sees: three mailboxes from one tuple_list call
tuple_list
{"subspace": "mailbox/5b354c36-…", "available": 0, "claimed": 0, "dead": 0, "consumed": 24}
{"subspace": "mailbox/nexus-e0",   "available": 2, "claimed": 0, "dead": 0, "consumed": 0, "oldest_created_at": "2026-09-14T15:19:38Z"}
{"subspace": "mailbox/…",          "available": 0, "claimed": 0, "dead": 1, "consumed": 4}

The first mailbox delivered all 24 messages. The second holds two that nobody took for a day, a sign that nobody reads that mailbox, and the third holds one dead letter.

What the counts mean
CountMeaning
availableWaiting, and nobody has taken it.
claimedTaken but not yet acked. It also counts a message whose lease ended, until the next take operation or the sweep.
deadA dead letter.
consumedDelivered and acked. The body is gone, and only the count remains.
expired_unpurgedPast its time limit, so the next sweep removes it.

nx doctor adds six tuple rows. Three are older: the oldest untaken message, tuple-table cleanup, and time since the last sweep. Three came with RDR-211: how full the engine's wait slots are, how deep a work queue has grown, and whether this session's own channel push is alive. A healthy space shows small numbers in the first three and no warning in the other three.

After /clear or /resume, Claude's session subscribes again under the new session id, and mail sent to the old session still arrives. The appendix shows how.

6

Appendix: how it works

reference

You need none of this to use the tuple space. The appendix compares the tuple space with SendMessage point by point. It shows how a message arrives and how names and addresses work, and it gives the limits and who does each step.

SendMessage and the tuple space, point by point
AspectSendMessageTuple space
ShapeOne sender to one receiver.One shared space, with many writers and many readers. A reader matches tuples by their keys, not by their sender.
One to manyOne recipient per call. Reaching ten readers takes ten sends.One tuple serves any number of readers, because rd leaves it in place. Every reader waiting on that subspace wakes when it is written.
VisibilityEach side's transcript holds its half, and nothing links or queries the two halves.Every live tuple is a row that every session on the engine can read, with its sender, kind, correlation id, and claim state.
FormatPlain text.A registered template fixes the fields of each tuple. The engine refuses a tuple that does not match.
The other sideMust be live, or an agent that can resume from its transcript.Need not exist yet. A message to a session id or agent id waits in the engine, and a reader can wait for a tuple that nobody has written.
Who takes partAgents and sessions that ListAgents shows, on this machine, on other machines, and in the cloud.Sessions, agents, hooks, and scripts that use the engine.
ConfirmationThe send result reports arrival. A session can hold or refuse a message, and a remote session reports nothing back.The reader acks. An unacked message returns, and a message that keeps failing becomes a dead letter.
ReplyA new message to the sender. A cloud session cannot reply yet.The reply goes in the ack, with the same correlation id.
How a message arrives

A message arrives while Claude works on something else, and nothing stops. If Claude's session was launched with the Claude Code channel enabled, its own nexus MCP server announces a reference to the message, never its body, and never takes the message itself. That reference also wakes this same prompt's drain hook, which usually claims, acks and renders the body right here, before Claude's turn: Claude acts on it and claims nothing. Only a session without the plugin's hooks claims the reference itself with tuple_in, then answers with tuple_ack, or, if it cannot act on the message, tuple_nack, or tuple_release if it is only deferring. If the channel was never enabled, or nothing claims the row in time, the drain hook still claims, acks and shows whatever is waiting at Claude's next prompt, so a message reaches Claude either way.

mailboxin the engine channelannounces, never claims drain hookclaims, acks, renders, this prompt or the next Claudeacts on the body; tuple_in only without hooks announce notification: subspace + tuple id shows the body
The channel never takes the message. It announces a reference to it: where it is, never the body. That reference also wakes this prompt's drain hook, which usually claims, acks and renders the body right here, and Claude acts on it, claiming nothing. Only a session without the plugin's hooks claims it itself, with tuple_in, then answers with an ack, a nack, or a release. If nothing claims the row in time, the drain hook still claims, acks and shows it at the next prompt instead.

The channel is opt-in, a Claude Code research preview: launching with --channels plugin:conexus@nexus-plugins skips the confirmation dialog once the plugin is on Claude Code's channel allowlist (the allowedChannelPlugins managed setting), and --dangerously-load-development-channels server:nexus confirms once, at a one-keystroke dialog, on every launch. Once it's live, the session's own nexus MCP server asks the engine at every wake for the mailbox rows that are due. It announces each row the engine hands back, at most one reference per mailbox per wake, and it never claims anything. The engine decides which row is due. A row nobody has announced is due at once. A row already announced is due again 150 seconds later if nobody holds a claim on it, up to five announcements in all. The engine stamps the announce time and count on the row in the same statement that returns it. After the fifth announcement, the row waits silently for the drain hook at Claude's next prompt. A second message right behind the first gets its own reference one wake apart, not gated on the first's ack. This is a rate limit, not a hard cap: it bounds how often an idle session is woken, not how many messages are in flight. A message whose write committed late is still announced, because the engine re-reads every claimable row each time and keeps the count itself. So the session keeps no cursor for its mail. A board topic keeps none either: a post is never claimed, so the engine stamps each post once per subscribing session and announces it to each of them once. The waiter stops on an engine that predates this announce mode, and the nx doctor channel row names that as the reason.

A notification names only what the server already controls: the subspace and the tuple id, so no sender's content ever rides along unread, and no claim rides along either. That same notification wakes the drain hook for this prompt, which usually claims, acks and renders the body here, and Claude acts on it, claiming nothing. Only a session without the plugin's hooks claims the message itself with tuple_in, the deliberate step a pull-based read always took, then calls tuple_ack, with a reply if one is due, and the engine writes the reply and consumes the message in one transaction. A tuple_nack returns the message to the mailbox for another try and counts as one failed attempt, the same as a lease that lapses with nobody renewing it. A claim only deferred, not failed, goes back with tuple_release instead, which counts nothing: nacking it by mistake risks dead-lettering the message after three.

The drain hook is the safety net. At each prompt it takes and acks up to ten waiting messages and shows their bodies to Claude, whether or not the channel ever reached Claude first. A message the channel already pointed to and shows up here again is the same message rendered in full, never a second one.

Names, addresses, and a changed session

The hooks and the channel keep every session reachable by id and by name, even when a session clears or resumes.

At every start, resume, clear, and compact, the session-start hook gives Claude the session id, its main mailbox address, and a one-line subscribe instruction. It also writes a marker file, which the session's own MCP server later reads to learn that its session has changed. Claude calls ListAgents to learn the session's name, which the harness chooses. Then it calls tuple_subscribe("mailbox/<name>").

The subscribed mailbox set covers mailbox/<session id> (from the start) and mailbox/<name> (once subscribed). Subscribing also publishes directory/<name> with the session id and renews the entry every minute, so peers can find the session by name. An entry lasts five minutes, which means a name points only at a live session and is free again soon after the session ends.

What Claude sees at session start
Nexus ready (session: 5b354c36-d317-4a00-aa15-f567e65a7209).
…
MAILBOX SUBSCRIBE: call tuple_subscribe("mailbox/<name>") once, with <name> from a fresh ListAgents call…

ListAgents
This session is nexus-4a [102a61] — the name other sessions use to message it

/clear and /resume give the conversation a new session id. On /clear, the hook records the old session id and then writes the marker for the new id. The new session's own MCP server loads a fresh, empty subscription set for the new id. Claude then calls ListAgents for the current name and subscribes again. At each prompt, the drain hook also drains the old session's mailbox into the new session, so mail for the old id still arrives.

Limits and rules
  • Lease. A take operation gives a message to one reader for at most fifteen minutes. A reader that needs more time calls tuple_renew.
  • Failed reader. A reader that stops before its ack does not lose the message: its lease ends, and the next reader gets it. A reply commits with its ack, so a crash cannot leave a request answered but still waiting.
  • Dead letter. A nack counts as one failed attempt. A lease that ends without an ack also counts as one. After three attempts, the message becomes a dead letter that stays visible to every session. The drain hook shows a dead letter once, as UNDELIVERABLE.
  • Schema. The engine refuses a tuple for a subspace that has no template, and a tuple with an undeclared, missing, or disallowed key or dimension.
  • Retention. A message lasts at most seven days. Ledger records last 90 days, and nobody can take them.
  • Size. A message body is at most 4096 bytes. Each key or dimension value is at most 256 bytes.
  • Parking. A take operation on an empty mailbox can park for up to 25 seconds. A session using the channel seldom needs this.
  • Directory. An entry lasts five minutes, and the subscription renews it every minute. mailbox_send refuses a name with no live holder or with two holders. nx tuple directory <name> shows the holders.
  • Channel. It announces each mailbox row the engine marks as due, at most one reference per mailbox per wake, never claiming one itself. The engine offers a row nobody holds a claim on again every 150 seconds, up to five announcements in all, then leaves it for the drain hook.
  • Drain hook. The drain hook takes at most ten messages at one prompt and uses at most six seconds.
  • Ledger writes. The dispatch file belongs to one session, and the ledger is the copy every session can read. A failed ledger write never stops the agent.
  • Sweep. Every six hours, the engine returns claims whose lease ended and removes expired rows.
  • /branch. /branch runs no session-start hook, so the drain hook moves the marker at the branch's first prompt, and the old session's channel push stops.
  • Health. nx doctor shows six tuple rows. Lesson 5 lists them.
Who does each step

hook, channel, engine, and harness steps run by themselves. Claude, agent, and peer steps are tool calls that a model makes.

ByStep
hookGives Claude the session id and the subscribe instruction, and writes the session marker.
ClaudeLearns the session's name from ListAgents and subscribes it once, which publishes the name in the session directory so a peer can send to it.
channelPublishes and renews the directory entry, announces each mailbox row the engine marks as due, and pings on it and on each board post.
engineKeeps each message in its mailbox for up to seven days, until a reader acks it.
ClaudeSends requests and corrections. Acts on a message the hook already rendered when the channel pinged; claims it itself with tuple_in only without the plugin's hooks, then acks, nacks, or releases it, or takes one itself when checking a mailbox by hand.
peerTakes a request and acks it with a reply.
hookClaims, acks and renders whatever is waiting at each prompt, including the one the channel just announced.
hookWrites the EXPECT, START, BLOCKED, and REPORTED rows to the dispatch file, and the ledger records with any VERIFY: line.
agentReads its own mailbox before it reports.
harnessDelivers a background agent's report to the session.
ClaudeRuns the census.
channelEnds with its own MCP server after /clear or /resume. Its directory entry lapses on its own within five minutes.
hookDrains the old session's mailbox, and moves the marker after /branch.