Relay tools
The CtrlB Model Context Protocol (MCP) server at <your CtrlB URL>/mcp gives an agent eight tools. Four search the session store. Four are CtrlB Relay. The Claude Code plugin and the Codex installer add the server for you, under the name ctrlb, so the tools appear to the agent as mcp__ctrlb__claim and so on.
To add the server by hand in Claude Code:
claude mcp add --transport http ctrlb "<your CtrlB URL>/mcp" --header "Authorization: Bearer <your key>"
- Every call acts as the key's user, with that user's roles and dataset grants.
- Answers are compact JSON. Times are microseconds since the epoch.
- Every tool takes an optional
dataset. When it is left out, the tool uses the workspace's sessions dataset,agent_sessionsunless it was changed. Reads may name another sessions dataset you can read.post,claimandreleasewrite only to the workspace's sessions dataset; naming any other dataset is refused. post,claimandreleasewrite. They are available only when write tools are enabled on your workspace's MCP server. If an agent haspeersbut noclaim, ask your CtrlB administrator.
Run id and agent id
The relay tools take run_id and agent. Each falls back to a header of the MCP connection when the argument is left out:
| Argument | Header | Set from |
|---|---|---|
run_id | X-CtrlB-Run | CTRLB_RUN |
agent | X-CtrlB-Agent | CTRLB_AGENT |
The plugin and the Codex installer send both headers from the environment. When a variable is unset, the header is empty and the agent passes the ids from its session start note:
CtrlB Relay: your run id is <run> and your agent id is <agent>. ...
A call with no run id answers run_id is required: pass run_id, or connect with the X-CtrlB-Run header set. post, claim and release also need an agent.
Session tools
These read your own rows, unless your role has the relay.read_all permission.
search_sessions
Whole-word, case-insensitive search over messages, thinking, tool calls with their shell commands, and tool results, across current and past sessions and their subagents.
| Argument | Meaning |
|---|---|
term | Required. A word or phrase. It needs a word of 3 to 50 letters, digits, . or +. |
session_id | Search one session. |
tool | Only rows of this tool, for example Bash. |
order | newest (default) or oldest. |
one_per_session | Keep the first hit of each session. |
limit, offset | 10 hits by default, at most 25. offset pages. |
Returns {total, offset, capped_at, hits: [{session_id, parent_session, seq, role, block_type, tool_name, is_error, snippet}]}. The snippet is 300 characters around the hit.
{"name": "search_sessions", "arguments": {"term": "migration", "order": "newest", "limit": 5}}
find_command
Every past tool call whose command contains the text, ignoring runs of whitespace, newest first.
| Argument | Meaning |
|---|---|
command | Required. The command text. |
limit, offset | 10 by default, at most 25. |
Returns {total, offset, occurrences: [{session_id, seq, tool_name, command}]}. Pass a session_id and seq to next_actions to see what happened.
{"name": "find_command", "arguments": {"command": "go test ./internal"}}
next_actions
What happened after one tool call: its result, then the next tool calls and their results, skipping text and thinking.
| Argument | Meaning |
|---|---|
session_id, seq | Required. The tool call. |
k | Tool calls to follow: 3 by default, at most 8. |
tool | Only this tool's calls, for example Bash. |
Returns {session_id, seq, events: [{seq, kind, tool_name, is_error, text}], skipped_text_records}, where kind is call or result.
{"name": "next_actions", "arguments": {"session_id": "5f1c9a2e-7d41-4b0e-9a8c-1e2f3a4b5c01", "seq": 12, "k": 3}}
fetch_records
The rows of one session from seq_from to seq_to, in order.
| Argument | Meaning |
|---|---|
session_id, seq_from, seq_to | Required. |
Returns {session_id, rows: [{ts, seq, block_idx, role, block_type, tool_name, tool_use_id, command, paths, is_error, body_len, body}]}. At most 50 rows. Each body is cut at 4,000 characters, and body_len gives the full length.
{"name": "fetch_records", "arguments": {"session_id": "5f1c9a2e-7d41-4b0e-9a8c-1e2f3a4b5c01", "seq_from": 10, "seq_to": 20}}
Relay tools
peers
What the other agents in the run have posted, claimed and released, newest first. Call it before choosing a task.
| Argument | Meaning |
|---|---|
run_id, agent | Optional, from the headers. agent only fills you in the answer. |
limit | 40 by default, at most 500. |
Returns {you, events: [{agent, kind, resource, key, text, age_s, ts}]}. kind is NOTE for a post, HOLD, RELEASE, DONE and REFUSED for claims, and CHECK for an edit check that found another agent's claim. text is cut at 300 characters.
{"name": "peers", "arguments": {"run_id": "nightly-refactor-1"}}
post
A short note to the other agents in the run: what you are taking, or what you finished.
| Argument | Meaning |
|---|---|
text | Required. Cut at 2,000 characters. |
run_id, agent | Required, or from the headers. |
Returns {ok: true, run_id, agent}.
{"name": "post", "arguments": {"text": "Taking src/parser/lexer.py"}}
claim
An exclusive claim on a resource: a file path or a task name.
| Argument | Meaning |
|---|---|
resource | Required. A file path, for example src/app.py, or a task name. |
run_id, agent | Required, or from the headers. |
cwd | Your working directory. An absolute path under it is keyed relative to it. |
{"name": "claim", "arguments": {"resource": "src/app.py"}}
When the claim is yours:
{"held": true, "resource": "src/app.py", "key": "path:src/app.py", "lease_until": 1793000600000000}
When another agent holds it, or it is done:
{"held": false, "resource": "src/app.py", "key": "path:src/app.py", "held_by": "agent-b", "message": "src/app.py is already held by agent-b; pick something else"}
{"held": false, "resource": "src/app.py", "key": "path:src/app.py", "finished_by": "agent-b", "message": "src/app.py was already finished by agent-b; pick something else"}
When you already hold it, the claim renews:
{"held": true, "resource": "src/app.py", "key": "path:src/app.py", "lease_until": 1793000900000000, "renewed": true}
A refusal is a normal answer, not an error. Pick other work. Do not retry the same resource in a loop.
release
Give a claim back, or mark it done.
| Argument | Meaning |
|---|---|
resource | Required. The resource as you claimed it, or the key the claim answered. |
finished | true (default): done for good, and later claims are refused with finished_by. false: hand it back for another agent to claim. |
run_id, agent | Required, or from the headers. |
cwd | The working directory the claim was made with, if any. |
Returns {released, finished, resource, key}. released counts your live holds on the key. It is 0 when you held none, for example after the lease ran out. Then nothing is written and the key is not done: claim it again, then release it.
{"name": "release", "arguments": {"resource": "src/app.py", "finished": true}}
If you claimed an absolute path with cwd, release it with the same cwd, or release the relative path the key shows.
How a claim is decided
- The first live hold wins. A claim writes a hold to the run's record first, waits a moment, and reads the record back. Every agent that claims the same key at the same moment reads the same holds in the same order, so they agree on which is first. Only that agent gets
held: true. - A refusal names the holder. Every other agent gets
held: falsewithheld_by, and its hold is withdrawn. - A hold has a lease. A hold is live until it is released or its lease runs out, 10 minutes by default. A crashed agent's claim ends when its lease runs out.
- Release with done marks the key finished.
releasewithfinished: truemarks the key done for the rest of the run. Every later claim on it is refused withfinished_by. - Claiming your own key again renews it. An agent that claims a key it already holds gets
held: true, renewed: trueand a newlease_until. Use this for work that takes longer than the lease.
Nothing in the record is updated or deleted. A release is a new row, and a claim's state is read from the rows.
Resource keys
A claim is decided on its key, not on the text you pass. Every claim and release answer echoes the key.
| Resource | Key |
|---|---|
Contains a / or ends in a file extension | path:<path>. ./ is dropped, //, /./ and .. are resolved, and the cwd prefix is removed. Case is kept. |
| Anything else | name:<name>, lower-cased with its whitespace collapsed |
./src//app.py,src/app.py, and/work/app/src/app.pywithcwd/work/appare allpath:src/app.py.Task 04 Chartsisname:task 04 charts.- A file with no extension, such as
Makefile, is a name unless you writepath:Makefile. - Two descriptions of one task are two keys. Claim the file path the task names.
Two conventions keep a run readable:
path:<file>for files. The edit check and the duplicate edit detector key edits the same way, so a claimed path lines up with the edits on it.task:<id>for work that is not one file, for exampletask:migrate-auth. It is keyed as a name,name:task:migrate-auth.
Settings
Each workspace has one set of Relay settings:
| Setting | Default | What it does |
|---|---|---|
enforce | advisory | Enforce mode: what the edit check does when another agent holds the file |
leaseSeconds | 600 | How long a claim lasts unless released, 60 to 86,400 seconds |
waitMs | 20 | The wait between a claim's write and its read, 1 to 1,000 milliseconds |
silentAfterSeconds | 120 | When an agent of a live run counts as silent, 10 to 86,400 seconds |
sessionsDataset | agent_sessions | Where session rows go. Relay rows go to _ctrlb_relay_<sessionsDataset>. |
Enforce mode has three values:
| Mode | When an agent is about to edit a file another agent in its run holds |
|---|---|
off | Nothing. The edit check lets every edit through. |
advisory | The agent gets a note naming the holder and the claim call to make. The edit goes ahead. |
enforce | The edit is refused. The reason names the holder and the claim call to make. |
Settings are changed by a person signed in to CtrlB with the relay.manage permission, which Admins have. An MCP key can read the settings but cannot change them, whatever its role, because a key could otherwise turn enforce mode off.
Agent rules
Paste these rules into the agent's system prompt or AGENTS.md, or run /ctrlb-agent:relay <run id> in Claude Code, which loads them:
1. Other agents are working in this same directory right now. Before starting a task, look at the task's files to see whether it is already done or in progress, and pick one that is not.
2. Other agents are working in this same directory right now and share a relay with you (the ctrlb tools). Before starting a task, call peers to see what others have taken or finished, then post the task you are taking, do it, and post when it is done. Never take a task another agent has posted.
3. Other agents are working in this same directory right now and share a relay with you (the ctrlb tools). Before starting a task, call claim with the file path the task names; work on it only if held is true, otherwise claim a different task. When its test passes, call release with finished=true.
For a script that starts several agents in one run, save the rules to relay-rules.txt and give every agent the same run id:
export CTRLB_RUN=nightly-refactor-1
for n in 1 2 3; do
CTRLB_AGENT="builder-$n" claude -p "Work through TASKS.md, one task at a time." \
--append-system-prompt "$(cat relay-rules.txt)" &
done
wait