The Nexus Tuple Space
The tuple space is where your Claude Code session, its agents, and the other sessions on your machine leave records for each other: a report to wait for, a message to deliver, a request to answer. This page shows the three things you say to use it, what you see when you do, and how it works underneath.
Words in <ORANGE CAPITALS IN ANGLE BRACKETS> are placeholders. The prompts are examples, and your own words work. Everything on this page is live in the current release. The reference, with the exact parameters of every operation, is docs/tuple-space.md, the walkthroughs draw each use as a sequence, RDR-205 records the design, and Linda in Nexus explains the thinking behind it: what was borrowed, what was left out, and why.
Terms used on this page
| Term | Meaning |
|---|---|
| tuple | One record in the tuple space. It has keys, dimensions, and a body. |
| keys | The fields a take operation matches on. A take operation must give every key exactly. |
| dimensions | Fields that describe the tuple without being part of a take operation's match. A read can match on them. |
| subspace | A named part of the tuple space, such as the mailbox of one agent. Every tuple belongs to exactly one subspace. |
| template | The rules for one kind of subspace: required keys, allowed dimensions, lifetime, and whether take operations are allowed. |
| claim | The state of a tuple between a take operation and its ack. The taker holds the claim. |
| lease | The time limit on a claim. When the lease ends, the tuple becomes available again. |
| ack, nack | Ack ends a claim and marks the tuple consumed. Nack ends a claim and returns the tuple to the space. |
| dead letter | A tuple that failed three delivery attempts. Nobody can take it, but anybody can read it. |
| agent | A process that a Claude Code session starts to do one task. |
| instance | One Claude Code session, under the name the harness gives it. |
| engine | The Nexus service that owns the database and answers every tuple call. |
| hook | A script that the harness runs automatically around an event, such as an agent starting or stopping. |
| tenant | One customer of the engine. Every store is separated by tenant, and one credential gives access to one tenant. |
| read-many store | A store where any number of readers see the same entry and nobody can claim it. |
| bead | One tracked unit of work in the project's issue tracker. |
| correlation id | An id that a request carries and its reply repeats, so the two can be paired. |
| claim log | A table with one row for every claim, ack, nack, lease end, and dead letter. Rows are never edited. |
| digest | A short fingerprint of a set of templates. It changes when any template changes. |
What it does for you
5 minA tuple is a small typed record in a shared space. Any process can add one, read one, or take one, and a take is exclusive: when two processes reach for the same tuple, one gets it and the other does not. A read can wait for a tuple that has not arrived yet. Those two operations, exclusive take and wait, are what the harness, memory, and scratch do not give you, and they are enough for the three situations below.
Wait for an agent to report
Wait until <THE AGENT> reports, then continue with its result.When the agent stops, a hook writes a report tuple to the session's ledger. Claude reads that tuple with a timeout and wakes the moment it lands. The agent does nothing to make this work, and if it never reports, the missing tuple is the evidence.
Tell an agent something while it works
Tell the agent that works on <THE TASK> that the schema changed. Put the message in its mailbox.Claude writes a message tuple to the agent's mailbox. The agent's instructions tell it to read its mailbox before it returns its result, so it can act on the message first. The message reaches exactly one reader.
Ask another session for something
Ask the <PEER SESSION> session to rebuild its gate jar. Wait for its answer.Claude writes a request tuple to the other session's mailbox and waits on its own mailbox for the answer. The other session receives the request, does the work, and writes the answer. Nobody relays anything by hand, and an unanswered request stays visible with its age.
The harness, hooks, memory, and scratch keep doing what they do. The tuple space is built from them: hooks write its records, the ids the harness assigns are its addresses, and Claude reads it with the same kind of tools it already calls. Lessons 2 to 4 show each situation in more detail, lesson 5 shows how to look at the space, and the appendix explains the operations underneath.
Send a message to an agent
10 minEvery agent has a mailbox, and a message to the agent is a tuple in it. The agent's instructions tell it to read its mailbox before it writes its report, so a correction you send while it works can change that report. No hook reads the mailbox for the agent.
The address
When Claude starts an agent, a hook gives the agent an id and tells it its mailbox address. You never need the id yourself: describe the agent by its task, as in lesson 1, and Claude resolves it. To see the addresses that exist, ask "List the mailboxes in the tuple space", and Claude runs a list by prefix.
What the agent receives at start
Claimant id: a3f7c2e1d09b4c551 — mailbox: mailbox/a3f7c2e1d09b4c551
Send
One tuple_out call to the agent's mailbox with the message body and a message id that Claude creates. The id becomes the identity of the tuple, so if the connection fails and Claude sends again, the second send lands on the same tuple and the agent never sees the message twice. The tool returns the tuple's id.
What you see
tuple_out subspace: mailbox/a3f7c2e1d09b4c551 keys: {to: a3f7c2e1d09b4c551}
dims: {from: <YOUR SESSION>, kind: directive} nonce: msg-17
→ e4175991d6ad…The body is a pointer, not the content. It names the memory entry, the document, or the scratch note that holds what the agent needs, and the agent opens that item. The appendix says why.
What the agent sees
On its next mailbox read, the agent takes the message with tuple_in, which returns the tuple and a claim id. It acts on the message, then acks the claim.
What you see
tuple_in subspace: mailbox/a3f7c2e1d09b4c551 keys: {to: a3f7c2e1d09b4c551} claimant: a3f7c2e1d09b4c551
{
"tuple": {
"keys": {"to": "a3f7c2e1d09b4c551"},
"dims": {"from": "<YOUR SESSION>", "kind": "directive"},
"body": "The schema changed. Read memory <PROJECT>/<TITLE> before you write the migration.",
"claim_state": "claimed", "lease_until": "…T16:17:51Z", "attempts": 0
},
"claim_id": "ede42f56-3a09-4897-86ee-856b9209ec9c"
}
tuple_ack claim_id: ede42f56-… claimant: a3f7c2e1d09b4c551
→ Acked claim ede42f56-…Delivery
Each message reaches exactly one reader. When two readers take from the same mailbox, the engine hands each message to one of them, and the other does not receive it. A reader that takes a message and then crashes never acks it, so its lease ends, the message becomes available again, and the next reader gets it. A reader whose work is still running renews its lease with tuple_renew before the lease ends, so the message stays with that reader. A nack counts as one failed attempt, and so does a lease that ends without an ack. After three failed attempts, the engine marks the message as a dead letter. Nobody can take it, but anybody can read it, so a message that makes every reader crash stays visible. An unclaimed or dead-lettered message survives at most seven days.
Neither side has to know when the other is running. The agent reads the space, the sender writes the space, and that is why a shared space works better than a direct message between two processes on different schedules.
Wait for a report or a reply
10 minA read with a timeout waits for a tuple that does not exist yet and wakes when it is written. This is the rendezvous row of the uses table in the appendix, and it is already at work in two places: waiting for an agent's report, and waiting for the reply to a request.
A report
When an agent stops, a hook adds a report tuple to the session's ledger with the agent's id as the key. Behind the instruction in lesson 1, Claude calls tuple_rd on the ledger with that id and a timeout. If the tuple is already there, the read returns immediately. If not, the read waits inside the engine and wakes when the tuple is written or the timeout ends.
What you see
tuple_rd subspace: ledger/<SESSION-ID> keys: {agent_id: a3f7c2e1d09b4c551, kind: report} timeout_s: 25
{
"keys": {"agent_id": "a3f7c2e1d09b4c551", "kind": "report"},
"dims": {"agent_type": "conexus:developer"},
"created_at": "…T16:15:49Z"
}Each call waits for at most twenty-five seconds and then returns what it found, so a wait of minutes is a loop of these calls. The limit keeps every wait shorter than every network timeout between the client and the engine, so a wait can never become a dropped connection.
A reply
A request and its reply share a correlation id, which is what pairs them. After sending a request, Claude calls tuple_in on this session's own mailbox with a timeout and wakes when a tuple with that correlation id arrives. Lesson 4 shows the whole exchange.
The engine wakes a waiting read with a signal, not by polling. When a tuple is written, the engine signals every reader waiting on that subspace once the write is committed, and readers on other subspaces are not signaled. When the engine stops for a deploy, it wakes every waiting read and each returns what it has, so a deploy costs one retry and never a stall.
Ask another session for something
10 minEvery session has a mailbox addressed by its session id, and that mailbox is a channel between any two sessions that share the same engine and the same tenant. That is every session on one machine, and in cloud mode every session on every machine. You name a peer by the name that the harness shows. The send resolves that name to the session that holds it and delivers there.
The exchange
Behind the instruction in lesson 1, Claude sends a request to the peer's name with kind "request" and a new correlation id, then waits on its own mailbox. The peer receives the request, opens what it names, does the work, and answers with a reply that carries the same correlation id. The request names its subject with a pointer, whether a bead id, a memory title, or a document address, and if the work produced something, the reply names where it was written.
What you see, on your side
mailbox_send to: nexus-8c kind: request correlation_id: r-41
body: Confirm you're subscribed to the channel. Ack when done.
→ {"to": "cdeb12b5-…", "address_kind": "session", "from": "5b354c36-…"}
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": "Subscribed. I am on the channel now …"
},
"claim_id": "784e820d-…"
}
tuple_ack … → AckedThe peer receives the request in one of two ways. If its Claude Code session has the channel enabled, its own nexus MCP server announces a reference to it, never the body, and never takes the request itself. That reference wakes the same prompt's hook, which usually claims, acks and shows the request there, before the peer's turn; the peer acts on it and claims nothing. Only a peer without the plugin's hooks claims it itself with tuple_in. The peer then does the work and calls tuple_ack with the reply. The reply and the ack are then one call, so a crash cannot deliver the reply and leave the request to run again. When the hook shows the request, it has already claimed and consumed it. The peer then sends the reply as a new message with the same correlation id, as in the output above. An unanswered request is a tuple with an age that the census in lesson 5 shows, not a line in a chat window that nobody reads.
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.
To find a peer's name, ask Claude to list the sessions on the machine. Send to the name it shows, such as nexus-8c. The send resolves the name in the session directory and delivers to that session's mailbox.
See what is coordinating
10 minTuples are rows with registered shapes, so you can query the coordination state of the whole system: which agents started and never reported, which messages wait, which requests have no answer.
Which agents started in this session and did not report?
Claude reads the session's ledger with an empty pattern, which returns every live tuple in it, and compares the start tuples with the report tuples by agent id. This read is the census. Only tuple_rd can do it, because a take operation must give every key exactly and can never claim by accident with a wide pattern.
What you see
tuple_rd subspace: ledger/<SESSION-ID> keys: {} n: 300
{agent_id: a3f7c2e1d09b4c551, kind: start, agent_type: conexus:developer}
{agent_id: a3f7c2e1d09b4c551, kind: report, agent_type: conexus:developer}
{agent_id: a9b04d8aa1f3e6c27, kind: start, agent_type: conexus:test-validator}
→ a9b04d8aa1f3e6c27 started and has not reportedThree kinds of question
- What is in a subspace. The census above. For a mailbox it returns every waiting message, including tuples under a live claim and dead letters, each with its state.
- Which subspaces exist, and how full they are. A list by prefix returns every subspace with counts of available, claimed, dead, and consumed tuples and the oldest and newest timestamps. "Every session that started an agent in the last ninety days" is one call.
- What happened to a tuple. Every claim, ack, nack, lease end, and dead letter writes one row to the claim log, which is never edited. The history of a message is in that log.
What you see
tuple_stats subspace: mailbox/nexus-19
{"total": 0, "available": 0, "claimed": 0, "dead": 0, "consumed": 1,
"expired_unpurged": 0, "oldest_created_at": "…T15:54:21Z", "newest_created_at": "…T15:54:21Z"}Six rows in nx doctor watch the space: the age of the oldest unclaimed tuple in each subspace, the health of the database table, the age of the last sweep, how full the engine's wait slots are, how deep a work queue has grown, and whether this session's push-delivery channel is alive. The sweep is the job that ends leases nobody renewed, removes expired tuples, and removes old claim-log rows. A healthy space shows small numbers across all six. What these rows catch is a sweep that has stopped, a subspace filling faster than it drains, or a session whose channel push has gone quiet.
Tools and commands
5 minMost of the time you use none of these directly: the hooks write the ledger, the agents read their mailboxes, and Claude calls the tools when you ask it to wait or to send. These are the surfaces for when you want to check for yourself.
| Surface | What it is |
|---|---|
| MCP tools | mailbox_send, tuple_out, tuple_rd, tuple_in, tuple_ack, tuple_nack, tuple_renew, tuple_release, tuple_registry, tuple_list, tuple_stats, plus three that manage what a session's own channel delivers (Coordination covers those). Claude calls these inside a session. |
nx tuple | The same operations from the terminal: out, rd, in, ack, nack, renew, release, templates, list, stats. Hooks and scripts call these. One more: directory, which shows the session that holds a name. release ends a claim without counting it as a failed attempt, unlike nack — the right call for a task or a lock handed back in good order. |
| Hooks | The agent-start hook gives each agent its id and mailbox address. The start and stop hooks write the ledger's start and report tuples without blocking the agent. |
| Skills | /conexus:mailbox holds the rules for sending and reading messages. /conexus:orchestration holds the rule for waiting on a report. |
nx doctor | The six rows described in lesson 5. |
nx tuple templates
nx tuple list --prefix ledger/
nx tuple rd ledger/<SESSION-ID> -n 300The first command shows the templates and their digest. The second shows every session's ledger with counts. The third is the census of one session.
What you see
$ nx tuple rd mailbox/5b354c36-… -n 300
id: adb98e40…
subspace: mailbox/5b354c36-…
keys: {'to': '5b354c36-…'}
dims: {'from': 'cdeb12b5-…', 'kind': 'reply', 'address_kind': 'session', 'correlation_id': 'r-41'}
body: Subscribed. I am on the channel now …
claim_state: claimed
created_at: 2026-09-15T13:49:27ZAppendix: how it is built
referenceNothing here is needed to use the tuple space. It is here for the reader who wants to know why the operations behave as the lessons say.
The three operations
Add a tuple. The tuple's identity comes from fields that the caller supplies, so sending the same tuple twice stores it once.
Read matching tuples. The tuples stay in the space. Any number of readers can read, and with a timeout the read waits until a tuple arrives.
Claim one matching tuple. Exactly one caller wins. The winner holds a lease and ends it with ack, returns the tuple as a failed attempt with nack, or, if the work was not a failure, hands it back clean with release.
A tuple has keys, dimensions, and a body. The keys are what a take operation matches on, and the dimensions describe the tuple without being part of that match. Subspaces divide the space into named parts, and the engine registers a template for each kind of subspace: which keys are required, which dimensions are allowed, how long a tuple lives, and whether take operations are allowed. The engine checks every out against its template and refuses one that does not fit. Only the engine holds the templates, so every client sees the same shapes.
Three operations with exact meanings are easier to build correctly, easier to analyze, and easier to combine than a larger interface. Each is one short database transaction, so the space scales as the database scales, and a busy subspace does not slow a quiet one. A take operation matches on keys alone and always ends in an ack, a nack, or a release, so you can say exactly what happens to any tuple under any sequence of calls, including a crash between a take operation and its ack.
Two more operations exist beneath these three, for the session's own delivery rather than for you to call directly. wait parks one call across several subspaces at once, so a session watching many mailboxes and topics spends one slot, not one per thing it watches. For a mailbox, the same call carries the announce cadence. The engine stamps each row it hands back, so the session keeps no cursor for its mail. park_stats reports how full those wait slots are. Neither has an MCP tool or a CLI verb.
Uses of the same operations
| Use | Tuple | Operations | Today |
|---|---|---|---|
| ledger | tuples that are read and counted, never taken | Hooks call out. A reader calls rd with an empty pattern to see every tuple. | built |
| mailbox | one tuple per message, keyed on the recipient | A sender calls out to the address. The recipient calls in before it acts. | built |
| request and reply | a request in the peer's mailbox and a reply in yours, sharing an id | You call out for the request, then in on your own mailbox for the reply. | built, on the mailbox |
| rendezvous | a tuple that means "I am here" or "I am done" | One process calls out when it arrives. The other calls rd with a timeout and wakes. | built, on the ledger and the mailbox |
| work queue | one tuple per task | A producer calls out for each task. A worker calls in, then ack when the task is done or release to hand it back for another worker without a failed attempt. | built |
| lock | one tuple per resource | A process calls out once to make sure the lock exists, in to hold it, and release to hand it to the next holder. ack is refused on a lock, since an acked tuple could not be taken again. | built |
| announcement | many tuples in one topic, none of them taken | Any session calls out to post. Readers call rd with a timeout to wake on the next post, or with a cursor to see what they missed. | built |
Six subspace kinds exist: the ledger, the mailbox, and the directory, which maps a session's name to its session id, plus a board for announcements, a work queue, and a lock, added under RDR-211. Every use in this table is now built. A new use is a new composition of the three operations rather than new code in the engine, and any use beyond these needs a design record of its own, as RDR-205 says.
The tuple space and the three stores
| Store | Holds | Lifetime | Readers |
|---|---|---|---|
| Scratch (T1) | Working notes of one session and its agents | The session | Many |
| Memory (T2) | Decisions, findings, and the state of the work, by project and title | The project | Many |
| Knowledge (T3) | Documents, chunks, the catalog, and its links | Permanent | Many |
| Tuple space | Coordination: who is present, who owes a report, a message, a request | Days, set by the template | Many read, one takes |
All four stores share one database and one tenant, so a tuple can name an item in any of the other three, and the reader opens it with the credential it already has. A tuple is a signal about data, so a message carries a pointer to its content, not the content itself: a content hash for a chunk, a catalog address for a document, a project and title for a memory entry. The content is already stored once in the right store, a copy inside a message becomes wrong the moment somebody corrects the original, and a pointer can be checked where a copied value cannot.
Inside the engine
- One Postgres table holds the tuples. Subspace, keys, dimensions, body, expiry time, and claim state are columns, and a claim log table beside it records every claim event. The row-level security that separates one tenant's memory from another's applies to tuples unchanged.
- A take is one statement. In one short transaction, the engine selects the oldest available row for the given keys, skipping any row another transaction holds, then locks it, marks it claimed, and logs the claim. A row someone else holds is skipped rather than waited for.
- A lease bounds every failure. Every claim has an end time. A reader that crashes loses its claim when the lease ends, and a sweep releases claims that nobody retook. A claim can never outlast its tuple.
- Every out is idempotent: sending the same one twice has no extra effect. The engine computes a tuple's identity from fields the caller supplies, never from the insert time, so a retry needs no protocol of its own.
- Waiting happens inside the engine. A waiting read waits on a signal for its subspace, which the engine sends after commit. The client does not poll, the wait holds no connection, and each wait is bounded so the client's own timeouts never fire first.
- Templates are delivered with the engine. They are files the engine loads and checks at start. A client can ask the engine for the templates and their digest and detect a mismatch instead of guessing.
A claim can be extended. A holder that is still working calls tuple_renew before its lease ends. The engine moves the lease forward, but never past the tuple's own expiry. A renew that asks for more than the template's longest lease is refused. A renew does not count as a failed attempt.
A reply can travel with the ack that closes a take operation, instead of as a separate call. tuple_ack takes an optional reply. The engine writes the reply and consumes the request in one transaction. Both take effect or neither does, so a crash cannot fall between them.