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
| Term | Meaning |
|---|---|
| engine | The Nexus service that owns the database and handles every tuple call. |
| tenant | One customer of the engine. Every store is separated by tenant, and one credential gives access to one tenant. |
| session id | The id of one Claude Code conversation, a UUID. It is the session's mailbox address. |
| name | The short label the harness shows for a session, such as nexus-4a. It can change when a session resumes. |
| agent id | The id the harness gives an agent: the letter a and 16 hexadecimal digits. It is the agent's mailbox address. |
| tuple | One record in the tuple space. A message, a directory entry, and a ledger record are each one tuple. |
| subspace | A named part of the tuple space, such as the mailbox of one agent. Every tuple belongs to exactly one subspace. |
| template | The 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. |
| message | One tuple in a mailbox. It has a sender, a kind, a correlation id, and a short body. |
| mailbox | The part of the tuple space that holds the messages for one address. |
| directory | The part of the tuple space that maps each name to the session id that holds it. |
| ledger | The part of the tuple space where hooks record that an agent started and that it reported. Any session can read it. |
| dispatch file | A 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. |
| census | The nx-hook expectations_census report. It compares the agents a session expected with the agents that started and the agents that reported. |
| hook | A script that Claude Code runs at a fixed point: session start, each prompt, and agent start and stop. |
| drain hook | The 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. |
| SendMessage | The Claude Code tool that sends a message to an agent by name or to a live session. |
| channel | The 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. |
| ping | The 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. |
| rendezvous | A 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 operation | A tuple_in call. It gives a message to one reader only, for a limited time called the lease. |
| ack, nack | tuple_ack removes a message that the reader took. tuple_nack returns the message to the mailbox. |
| dead letter | A message that failed three times. Anybody can read it, and nobody can take it. |
| correlation id | An id that a request carries and its reply repeats, so that the two can be paired. |
What it gives you
3 min · reading onlyFor 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.
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 on | How it works |
|---|---|
| You can see the coordination | Every 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 survive | Tuples 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 there | A 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 schema | Each 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.
Ask a peer, and keep working
4 minThis 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.
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>.
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.
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 … → AckedOther 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
requestfor a message that needs a reply andnoticefor one that needs none. The engine does not check which kind you use.
Correct an agent, and check the reports
3 minA 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.
Tell the agent that works on <THE TASK> that <THE CHANGE>. Start one agent for each <MODULE>, and check that every one reported.
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
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.
See what every agent did
2 minThe 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.
Which agents did this session start, and which reported? Which agents did <PEER NAME> start, and which reported? Which session holds the name <NAME>?
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.
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:09ZThe 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"
}Watch it and steer it
3 minBecause 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.
Is my channel push working? What is waiting in your mailbox?
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.
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
| Count | Meaning |
|---|---|
| available | Waiting, and nobody has taken it. |
| claimed | Taken but not yet acked. It also counts a message whose lease ended, until the next take operation or the sweep. |
| dead | A dead letter. |
| consumed | Delivered and acked. The body is gone, and only the count remains. |
| expired_unpurged | Past 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.
Appendix: how it works
referenceYou 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
| Aspect | SendMessage | Tuple space |
|---|---|---|
| Shape | One 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 many | One 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. |
| Visibility | Each 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. |
| Format | Plain text. | A registered template fixes the fields of each tuple. The engine refuses a tuple that does not match. |
| The other side | Must 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 part | Agents and sessions that ListAgents shows, on this machine, on other machines, and in the cloud. | Sessions, agents, hooks, and scripts that use the engine. |
| Confirmation | The 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. |
| Reply | A 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.
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_sendrefuses 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.
/branchruns 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 doctorshows 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.
| By | Step |
|---|---|
| hook | Gives Claude the session id and the subscribe instruction, and writes the session marker. |
| Claude | Learns 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. |
| channel | Publishes and renews the directory entry, announces each mailbox row the engine marks as due, and pings on it and on each board post. |
| engine | Keeps each message in its mailbox for up to seven days, until a reader acks it. |
| Claude | Sends 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. |
| peer | Takes a request and acks it with a reply. |
| hook | Claims, acks and renders whatever is waiting at each prompt, including the one the channel just announced. |
| hook | Writes the EXPECT, START, BLOCKED, and REPORTED rows to the dispatch file, and the ledger records with any VERIFY: line. |
| agent | Reads its own mailbox before it reports. |
| harness | Delivers a background agent's report to the session. |
| Claude | Runs the census. |
| channel | Ends with its own MCP server after /clear or /resume. Its directory entry lapses on its own within five minutes. |
| hook | Drains the old session's mailbox, and moves the marker after /branch. |