Skip to main content

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_sessions unless it was changed. Reads may name another sessions dataset you can read. post, claim and release write only to the workspace's sessions dataset; naming any other dataset is refused.
  • post, claim and release write. They are available only when write tools are enabled on your workspace's MCP server. If an agent has peers but no claim, 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:

ArgumentHeaderSet from
run_idX-CtrlB-RunCTRLB_RUN
agentX-CtrlB-AgentCTRLB_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.

ArgumentMeaning
termRequired. A word or phrase. It needs a word of 3 to 50 letters, digits, . or +.
session_idSearch one session.
toolOnly rows of this tool, for example Bash.
ordernewest (default) or oldest.
one_per_sessionKeep the first hit of each session.
limit, offset10 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.

ArgumentMeaning
commandRequired. The command text.
limit, offset10 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.

ArgumentMeaning
session_id, seqRequired. The tool call.
kTool calls to follow: 3 by default, at most 8.
toolOnly 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.

ArgumentMeaning
session_id, seq_from, seq_toRequired.

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.

ArgumentMeaning
run_id, agentOptional, from the headers. agent only fills you in the answer.
limit40 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.

ArgumentMeaning
textRequired. Cut at 2,000 characters.
run_id, agentRequired, 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.

ArgumentMeaning
resourceRequired. A file path, for example src/app.py, or a task name.
run_id, agentRequired, or from the headers.
cwdYour 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.

ArgumentMeaning
resourceRequired. The resource as you claimed it, or the key the claim answered.
finishedtrue (default): done for good, and later claims are refused with finished_by. false: hand it back for another agent to claim.
run_id, agentRequired, or from the headers.
cwdThe 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: false with held_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. release with finished: true marks the key done for the rest of the run. Every later claim on it is refused with finished_by.
  • Claiming your own key again renews it. An agent that claims a key it already holds gets held: true, renewed: true and a new lease_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.

ResourceKey
Contains a / or ends in a file extensionpath:<path>. ./ is dropped, //, /./ and .. are resolved, and the cwd prefix is removed. Case is kept.
Anything elsename:<name>, lower-cased with its whitespace collapsed
  • ./src//app.py, src/app.py, and /work/app/src/app.py with cwd /work/app are all path:src/app.py.
  • Task 04 Charts is name:task 04 charts.
  • A file with no extension, such as Makefile, is a name unless you write path: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 example task:migrate-auth. It is keyed as a name, name:task:migrate-auth.

Settings​

Each workspace has one set of Relay settings:

SettingDefaultWhat it does
enforceadvisoryEnforce mode: what the edit check does when another agent holds the file
leaseSeconds600How long a claim lasts unless released, 60 to 86,400 seconds
waitMs20The wait between a claim's write and its read, 1 to 1,000 milliseconds
silentAfterSeconds120When an agent of a live run counts as silent, 10 to 86,400 seconds
sessionsDatasetagent_sessionsWhere session rows go. Relay rows go to _ctrlb_relay_<sessionsDataset>.

Enforce mode has three values:

ModeWhen an agent is about to edit a file another agent in its run holds
offNothing. The edit check lets every edit through.
advisoryThe agent gets a note naming the holder and the claim call to make. The edit goes ahead.
enforceThe 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