Skip to content

Signed commands

Control tells a rig what to do only through signed commands. Control signs each command with its own Ed25519 key. The rig’s supervisor carries the command to the harness, which checks the signature, the rig’s name and the time before it does anything. The supervisor can carry a command, but it cannot make one or change one.

A signed command
{
"envelope": {
"v": 1,
"id": "3f6c2a8e-5b1d-4c7a-9e2f-0a1b2c3d4e5f",
"rig": "rig-1",
"kind": "pause_run",
"issued_by": "owner:api",
"issued_at": "2026-10-19T09:00:00.000Z",
"expires_at": "2026-10-19T10:00:00.000Z",
"payload": { "run": "run-1", "reason": "checking the kit" }
},
"signature": "<base64 Ed25519 signature>"
}

Commands come from the dashboard, the REST API, the command line and the MCP tools. All of them go through the same code in control, so a command is checked and signed the same way wherever it comes from. For the reasons behind this design, see Trust between control and rigs.

FieldTypeMeaning
envelope.v1The format’s version
envelope.idUUIDThe command’s id. Control makes it
envelope.rigtextThe one rig that may apply it
envelope.kindtextOne of the kinds below
envelope.issued_bytextWho issued it, as control knows them: owner:ui, owner:api, owner:mcp, owner:operator, agent:mcp, or the owner’s actor for a full reset’s steps. See Tokens and roles. The caller cannot choose it
envelope.issued_atISO 8601 timeWhen control signed it, by control’s clock
envelope.expires_atISO 8601 timeWhen it stops being valid
envelope.payloadobjectThe kind’s own fields, checked against the kind’s schema before signing
signaturebase64An Ed25519 signature over the envelope’s canonical JSON

Control’s key pair is control-signing.key and control-signing.pub in control’s data directory (THERMITE_CONTROL_DIR). Control makes the pair the first time it starts. THERMITE_CONTROL_PUBKEY_EXPORT copies the public key to a file for rigs, and the harness reads it from THERMITE_CONTROL_PUBKEY_FILE.

StepWhereWhat happens
1. IssueControlControl checks that it knows the rig (it has reported and is not retired, is enrolled, or is in the rig tokens file), checks the payload against the kind’s schema, signs the envelope and stores the command as pending. For a message_agent, it first trims the text and refuses a message that nothing on the rig would read (see message_agent). A signed command larger than 3.5 MiB is refused: the <kind> command is too large to deliver
2. FetchSupervisorThe supervisor asks control for its rig’s pending commands, oldest first, every THERMITE_COMMAND_POLL_SECONDS seconds (default 5)
3. StageSupervisorIf the command names content to fetch (a project, a kit, a model or another rig’s champion), the supervisor fetches it into the rig’s inbox first. The command stays pending while it waits
4. ApplyHarnessThe supervisor posts the command to the harness, which checks it and applies it once
5. AcknowledgeSupervisorThe supervisor tells control the status and the harness’s answer, which become the command’s status and outcome
6. ConfirmControlThe harness records each command it applied in its ledger. When the rig’s next signed report brings that record, control takes the status and outcome from it, in place of the supervisor’s

GET /v1/commands/<id>, the MCP tool command_status and the dashboard’s Commands page, which lists the newest 200 commands, show a command’s status and outcome.

StatusMeaningOutcome
pendingWaiting for the rig’s supervisor to take it, or for the content it names to arriveNone
deliveredThe harness applied itThe harness’s answer: {"status": "applied", "accepted": true, "result": …}, or "replayed" with the first answer
rejectedThe harness refused it before applying it (HTTP 403): signature, rig, time, payload, or issued before the rig’s state was wiped{"status": "rejected", "reason": "…"}
failedThe harness applied it but it did not succeed, or the harness answered with another error, or its content could not be fetched, or the owner cancelled it{"status": "applied", "accepted": false, "result": {"error": "…"}}; {"http": <status>, "error": "…"}; {"error": "staging: …"}; or {"cancelled_by": "owner:api"}
expiredIt was still pending after expires_atNone

Once control has the harness’s own record of an applied command (step 6), the outcome is {"source": "harness", "kind": …, "accepted": …, "result": …}, and the status is delivered if accepted is true, else failed.

delivered means the harness carried the command out, not that what it started went well. A rig_check, for example, is delivered as soon as the check starts; its finding comes later, as a rig_check_finished event.

Value
Time to live, set by the issuer (ttl_minutes, --ttl)1 to 10080 minutes (7 days). Default 60
propagate, built by control (POST /v1/propagate, propagate_champion, Send champion)24 hours, because the champion may still be on its way to the results repository. A propagate sent through POST /v1/commands takes its ttl_minutes like any other kind
Every other command the dashboard sends60 minutes
A full reset’s steps (pause_run, export_now, wipe_state)30 minutes each
Clock difference the harness allows between control and the rig5 minutes
  • Expiry at control. When the supervisor next asks for commands, control marks each pending command past its expires_at as expired and does not hand it over. Time spent fetching a command’s content counts toward its time to live.
  • Expiry at the harness. The harness refuses a command whose expires_at has passed by the rig’s clock (expired), or whose issued_at is more than 5 minutes ahead of it (issued in the future).
  • Cancelling. The owner can cancel a command while it is pending, with POST /v1/commands/<id>/cancel and the owner’s token. The command becomes failed with the outcome {"cancelled_by": "owner:api"}. A command that is not pending gives 404 {"error":"no such pending command"}. The dashboard, the command line and MCP have no way to cancel. See Cancel a pending command.

The harness checks every command in this order. A command that fails a check marked rejected is not applied: the harness answers with HTTP 403 and records a command_rejected event in the rig’s ledger.

CheckReason givenStatus at control
The command has the right shapemalformed commandrejected
Control’s public key can be readcontrol public key unusablerejected
The signature matches control’s public keybad signaturerejected
The command names this rigcommand is for rig <rig>rejected
It was not applied beforeNone: a command already applied answers replayed with its first answer, even if it has expired sinceAs the first answer
It is not more than 5 minutes in the futureissued in the futurerejected
It has not expiredexpiredrejected
The payload fits the kindbad payload: <field>: <problem>rejected
It was issued after the rig’s state was last wipedissued before this rig's state was wipedrejected
It is not being applied right nowNone: the harness answers in_progress, and the supervisor asks again laterStays pending
An owner-only kind was issued by the owner (issued_by is owner or starts with owner:)<kind> is the owner's to issue, not <issuer>'sfailed

The owner-only kinds are approve_promotion, decline_promotion, accept_review, conclude, wipe_state, message_agent, unkeep_message, rig_settings, and the six kinds that change a run’s experiment queue and its rules: queue_add, queue_move, queue_edit, queue_drop, queue_accept and drop_directive. The owner-only check is the first step of applying a command, so a command it refuses is recorded like any applied command, as not accepted. Every applied command is recorded in the ledger with the actor control:<issued_by>.

KindDashboard (owner:ui)REST and thermite (owner:api)MCP owner (owner:mcp)MCP operator (owner:operator)MCP agent (agent:mcp)Owner only
assign_campaignNew campaign; Start the run in the Start a run panel of a campaign’s pageYes; thermite assignYesYesYes
rig_checkRun it in a rig’s Checks; Check on SettingsYesYesYesYes
pause_runPause, then Pause the run in its menu, in a run’s header, in the header of the run’s Live, and on the run’s card under At work nowYesYesYesYes
resume_runResume in a run’s header, in the header of the run’s Live, and on a paused run’s row under Earlier runsYesYesYesYes
redirectNone: on the dashboard, rules come from Tell the planner, a message_agent to the planner, which makes themYesYesYesYes
reset_workersRestart the lanes, under ⋯ in the header of a running run with agents, on its page and in LiveYesYesYesYes
export_nowExport now on a rig’s pageYesYesYesYes
message_agentSend in the message box of a run’s Live; Send to the planner on a run’s Planner tab, while the planner is onYesYesYesNoYes
unkeep_messageRemove in the 📌 <n> kept list of a run’s LiveYesYesYesNoYes
queue_addAdd under Up next on a run’s Experiments tabYesYesYesNoYes
queue_move↑ Top, or dragging an experiment, under Up next on a run’s Experiments tabYesYesYesNoYes
queue_editEdit, then Save, on an experiment under Up next on a run’s Experiments tabYesYesYesNoYes
queue_dropDrop, then Drop it, on an experiment under Up next; Drop on a lane’s idea under Ideas from the lanesYesYesYesNoYes
queue_acceptQueue it on a lane’s idea under Ideas from the lanesYesYesYesNoYes
drop_directiveRemove on a rule on a run’s Planner tabYesYesYesNoYes
propagateSend champion #<n> on a run’s Planner tab, under Other rigsPOST /v1/propagate, or POST /v1/commandspropagate_championpropagate_championpropagate_champion
approve_promotionNeeds you: Make it the champion, Overrule the auditorYesYesNoNoYes
decline_promotionNeeds you: Reject itYesYesNoNoYes
accept_reviewNeeds you: Accept, RejectYesNoNoNoYes
concludeNeeds you: End it: not possible, Keep goingYesNoNoNoYes
rig_settingsSave changes on Edit rig; GPUs for the judge on New campaign and in the Start a run panelYesNoNoNoYes
wipe_stateOnly as a step of a full resetNo: wipe_state is issued only by a full resetNoNoNoYes

Control’s own lists decide what each MCP role may issue; the harness’s owner-only check is a second gate behind them. The operator acts for the owner, so its owner:operator passes that gate for message_agent, unkeep_message and the six queue kinds. During the owner’s full reset of a rig, MCP’s issue_command refuses an agent’s token for that rig: <rig> is being reset by the owner. Tokens and roles describes each token.

On the dashboard, only an experiment up next has Edit: a lane’s idea’s words change only over the REST API, the command line or MCP. Choose what runs next walks through the Experiments and Planner tabs.

Ids such as run are lowercase ids: letters a–z, digits, ., _ and -, starting with a letter or digit, at most 64 characters. Commits are full commit ids, 40 or 64 hexadecimal characters. A run that does not exist gives no run <run>.

Except where a section says control refuses, the refusals listed under each kind come while the harness applies the command. Such a command is failed, and its outcome’s result is {"error": "<the refusal>"}.

Starts a run of a campaign on the rig. On a rig launched on RunPod whose pod is stopped, control starts the pod first, then queues the command, which waits until the pod runs (A run on a stopped RunPod pod). Only the owner’s token starts a pod.

FieldTypeDefaultMeaning
runlowercase idrequiredThe new run’s id
contractobjectrequiredThe whole contract
contract_sha64 hexadecimal charactersrequiredThe contract’s SHA-256, as thermite contract validate prints it
seed{repo, commit}noneThe repository (a lowercase id) and the commit the run starts from. Required for hill climb and goal campaigns; research campaigns have none. When the contract names a project.folder, commit is the whole repository’s commit, and the run starts from that folder of it
directiontextnoneRecorded with the run
importslist of imports[]What the rig must take in first: the project, the kit, models
agentsbooleanworker agents runfalse: a judge-only run
promotionauto or owner_approvalthe rig’s own settingThe run’s promotion rule
queueup to 500 experiments, each as belownoneExperiments to start the new run’s experiment queue with, in order: usually another run’s untried ones
briefingup to 200 items, each as belownoneBackground from past research for every agent’s brief, in order: findings, claims, notes and reports from the knowledge base
mcp_serversthe kit’s mcp_servers listnoneThe MCP servers the run’s kit names (a research contract’s, for a research run), with the agents that get each. Control fills it in from a kit it keeps, from the page’s reading of a kit in a repository, or from a research contract, and refuses a server Settings lacks. The harness refuses the run when it differs from what the kit names. Control gives the rig only the servers its runs name

Each experiment in queue has these fields:

FieldTypeMeaning
titletext, 1 to 200 charactersWhat to try
detailstext up to 4000 charactersOptional. More about it
familytext, 1 to 80 charactersOptional. Its direction, in words or as a direction family’s name
whytext up to 2000 charactersOptional. Why it is worth trying
bytext, 1 to 80 charactersWho added it in the first place, such as owner, planner, teacher or lane-2. The new run keeps it as the experiment’s author. Over MCP, control replaces it, as below
statequeued or proposedqueued: up next. proposed: a lane’s idea, which stays an idea in the new run
from{run, id}The run and the experiment it was copied from

Over MCP, control does not keep the caller’s words for a copied experiment that it can take from its own record. It takes title, details, family, why and by from its record of the experiment that from names: the rigs’ signed events, with every edit applied. A field that the record does not have is left out. It does this only when the run that from names is filed in the folder of the new run’s campaign (contract.campaign), or of a campaign whose folder that campaign may read (Grants). Any other experiment keeps the caller’s words, and its by is copied: one that control has no record of (no rig reported it, or more than one did), and one from a run in any other folder, or in none. To an experiment taken from its record, control adds the line In <run> (<campaign>) it was <result>. after the details when the experiment’s item in the knowledge base has a result, as Start a run does, and cuts details to 4,000 characters.

Each item in briefing has these fields:

FieldTypeMeaning
texttext, 1 to 4000 characters, trimmedThe item’s words
from{campaign, run, kind, ref}Where it came from: the campaign’s folder (1 to 200 characters), the run (a lowercase id), the item’s kind (1 to 40 characters, such as finding) and its ref in that run (1 to 200 characters)

Over MCP, control does not keep the caller’s words for a briefing either. It looks up the knowledge base’s item that from names and puts that item’s words in text. It leaves out an item it cannot find, one it finds more than once (run names are unique on one rig only), and one from a folder the new run’s campaign may not read. With none left, it sends no briefing. It does this before it looks up the copied experiments. The REST API (POST /v1/commands) and thermite command send briefing and queue as given: control looks nothing up. thermite assign has no option for a briefing. Its --queue-from fills queue with a past run’s untried experiments, as Start a run does below, and takes them from control first (GET /v1/rigs/<rig>/runs/<run>/untried), with the words and authors control’s records hold, as over MCP. See the command line.

Start a run on a campaign’s page also works for a campaign that was assigned over the REST API, MCP or the command line, with no version saved on control. Control then signs again the campaign’s newest assign_campaign whose status is pending or delivered, as a new run: the same contract, seed, imports and direction, with a new run, and agents and promotion from the panel. With Agents on and Copy the untried experiments of ticked, the panel also sends queue: the chosen run’s experiments still up next, in their order, then its lanes’ ideas, at most 500 of them. Its help says “They join the new run’s queue in the same order, up to 500.” With Agents off, the panel hides that field, and control sends no queue even when the form has it: a run without agents has no queue. Its From past research sends the ticked past experiments in queue, after the copied ones (at most 500 in all), and the ticked findings, claims, notes and reports in briefing, each looked up again in control’s knowledge base. With Agents off, the panel hides From past research too and sends none of it. If ticked items reach control all the same (the page shown without its script), control leaves them out, starts the try, and says “A try (agents off) takes no experiments and no background: what you ticked was left out.” Use past research in a new run gives the steps. Start a run’s twins, POST /v1/campaigns/<campaign>/runs, MCP’s start_run and thermite run start, run the panel’s code and send the same assign_campaign; they refuse a run to copy from, a model or past research for a try instead of leaving them out.

The harness creates the run only after every check passes. In order, it refuses:

MessageWhen
invalid contract: …The contract does not pass its schema
contract hash does not match contract_shaThe hash differs
run <run> already existsThe run id is taken on this rig
a run without agents has no queue: leave out the experiments to copy"agents": false with experiments in queue
queue experiment <n>: <the first problem>An experiment in queue whose title or family is only spaces. <n> counts from 1
a run without agents has no briefs: leave out the background from past research"agents": false with items in briefing
contract <campaign> v<version> exists with other contentThe rig has this contract version with different content
model <name> is not listed in the contractA model import the contract does not list
a <mode> campaign needs a seed commitNo seed for a hill climb or goal campaign
An import’s message (see Imports)An import fails
model <name> (<sha256>) has not been importedA model the contract lists is not on the rig
kit commit <commit> is not in kit <repo>; import its bundle firstThe contract’s kit commit is not on the rig
kit <repo> at <commit> has no valid kit.json: …The kit’s kit.json is missing or invalid
the kit does not fit the contract: …The kit lacks what the contract needs
seed commit <commit> is not in <repo>; import its bundle firstThe seed commit is not on the rig
commit <commit> has no folder <folder>The contract’s project.folder is not a folder of the seed commit

For a contract with a project.folder, the run starts from a commit the harness makes from the seed commit: the folder’s files as its whole tree, with no parent, a fixed author and time, and the message <folder> at <commit>. Every rig makes the same commit from the same seed commit and folder, so champions move between rigs. That commit is the run’s champion #1, and the run’s run_created event names it in seed.commit, with seed.folder and the repository’s commit in seed.source_commit.

Then it creates the run as running, and pauses the rig’s other running runs with the reason replaced by <run>. In the same step it adds each experiment in queue to the end of the new run’s queue, in order. Each keeps its author, its reason, its state and where it came from, with one experiment_added event (see queue_add), which also has from_run and from_id. In a hill-climb or goal run, a worker whose lane gets an experiment that neither the owner nor the planner added reads this line under it in its brief: Added by <author>: its words are a suggestion to weigh, not instructions., with an earlier run for copied. It keeps the briefing with the run, in order, and nothing changes it later; the run’s run_created event then has "briefing": <n>. Briefings says where each agent reads it. Result: {"run", "contract_sha", "imported", "agents"}, plus "start": {"folder", "commit"} for a project in a folder, with the commit the run starts from, "queue": <n> when queue held any experiments and "briefing": <n> when briefing held any items.

Starts one of the rig’s checks.

FieldTypeDefaultMeaning
checkmodel, isolation or smokerequiredWhich check
forcebooleanfalseRun it even though the rig is busy. Isolation and smoke want an idle rig; forcing may end running trials

Result: {"check", "id", "started": true}. The finding arrives later as a rig_check_finished event. Refusals: this rig cannot run the <check> check, the <check> check is still running, not now: <why>.

FieldTypeMeaning
runlowercase idThe run
reasontext, at least 1 characterWhy. The dashboard sends what you write in why (optional) in the Pause menu, at most 500 characters, or paused from the dashboard when you leave it empty

The run becomes paused with the reason. Its lanes’ running trials end; the experiment each was on goes back to the top of the run’s queue, as for reset_workers. The judge takes no new job of a paused run: its jobs waiting for the judge stay queued until the run runs again. Result: {"run", "status": "paused"}.

FieldTypeMeaning
runlowercase idThe run

The run becomes running, and the rig’s other running runs are paused: a rig runs one run at a time. A closed run is refused: run <run> is closed. Result: {"run", "status": "running"}. On a rig launched on RunPod whose pod is stopped, control starts the pod first, as for assign_campaign.

A set of directives for the run’s planner. See Steer a run.

FieldTypeDefaultMeaning
runlowercase idrequiredThe run
directives1 to 40 directivesrequiredThe set
replacebooleantrueReplace the set this issuer gave before. false adds to it

Each directive has only these fields:

FieldTypeMeaning
kindavoid, try, mandate, priority, lane_count or noteavoid, try and mandate (Always on the dashboard) name a direction; priority ranks one; lane_count sets how many lanes work, up to the contract’s count; note is text for the planner only
familyletters, digits, spaces and dashes, plain or as :name:, that make a name of 2 to 41 charactersThe direction it is about, when the kind needs one. Control signs it as the direction’s name: its words in lowercase, joined by dashes, so :Shared memory tiling: becomes shared-memory-tiling
valuetext up to 1000 characters, or a numberThe value, when the kind needs one. A research run’s try or mandate may give a question’s number here instead of a family
rationaletext, 1 to 2000 charactersWhy. The planner sees it; workers never do
citationsup to 20 texts of 1 to 500 charactersSources. Default []

Control refuses a directive that breaks these rules before signing, with bad redirect payload: directives.<i>[.<field>]: <message>, <i> counting from 0:

Field and messageWhen
a rule no longer takes match: name its direction in family, in plain words or as :name:The directive has match, a rule in words
family: "<text>" is not a direction's name: write it in letters, digits, spaces and dashes, such as shared memory tiling or :shared memory tiling:Other characters in family
family: "<text>" is too long for a direction's name: use at most 41 letters, digits, spaces and dashesA name over 41 characters
family: an avoid, try or mandate rule names a direction (family)An avoid, try or mandate with no family (and, for try and mandate, no value)

A family may name a direction the run doesn’t have yet. The rig takes it, and the planner’s model designs the direction at its next round: it says what the direction means, in its own words, and the run then has it (Directions). A research run has no planner to design one: control and the rig refuse there any direction but a task kind, A research run's directions are its task kinds: :explore:, :primary:, :contradict:, :consolidate:. It has no planner to design a new direction, so a rule for :<name>: would do nothing. Name a task kind.

A set that both avoids a direction and tries or mandates it is refused: the set both avoids and asks for <directions>. A closed run is refused: run <run> is closed: its queue and rules no longer change. Result: {"run", "directives": [<ids>]}, with "new_directions": [<names>] when the set names directions the run doesn’t have yet.

A rule in words (match) set before rules named directions stays in force: the rig marks it legacy, and only the planner’s model reads it. The fixed rules that give out work without the model (no model, a model answer that code refused, the planner switched off, and every research run) keep only the directives that name a family.

redirect is not the only source of rules. The teacher pair’s set (each teacher’s recommendations, and the combiner’s directives: see What each role must answer) and a rule that the planner adds because an owner’s message asked for it name a direction the same way.

The owner’s redirect, or an operator’s for the owner, can set again a rule that was removed with drop_directive. An agent token’s redirect, the teacher pair’s set and the planner’s cannot.

The dashboard calls directives rules. It sends no redirect: on a run’s Planner tab, Tell the planner sends a message_agent to the planner, and the planner adds the rules the message asks for at its next round, each with from_message set to the message. Remove sends drop_directive, which removes one rule. Control’s own command form refuses a redirect: this command is not available from the dashboard.

FieldTypeMeaning
runlowercase idThe run
reseed_commitcommitAccepted by the schema, but the harness refuses it: re-seeding lanes is not available in this version; reset without it

The run’s running trials end, along with the current epoch (a round, on the dashboard), and every lane starts again from the champion. The experiment each ended trial was on ends with the result interrupted, and a copy of it goes to the top of the run’s queue, with the same author and the reason tried again: its trial on <lane> was interrupted (<old id>), followed by the old reason. The copy’s experiment_added event also has requeued_from: {experiment, trial, lane, result}, the experiment it repeats, its trial, the lane and interrupted. Result: {"run", "reset": true}.

The run page’s History says “You restarted the lanes: their trials ended, and every lane starts again from the champion.” (with the issuer’s name in place of “You” for the agent’s token), and, for each copy, <author>'s experiment "<title>" went back in the queue: its trial was interrupted., such as “The planner’s experiment …” or “Your experiment …”. The dashboard offers Restart the lanes only on a running run with agents: a run without agents (a try, a rig check) has no lanes.

The owner’s message to the run’s agents. The judge is code and takes no messages.

FieldTypeDefaultMeaning
runlowercase idrequiredThe run
toone of the recipients belowrequiredWho gets it
texttext, 1 to 4000 charactersrequiredThe message. Control trims it before it signs it
keepbooleanfalsetrue keeps the message in force: it goes into the brief of every new trial it is for, until an unkeep_message removes it. Only for all or a lane
toWho reads it
allEveryone working: each lane at work, and the planner of a hill-climb or goal run whose planner is on. The lanes at work are the contract’s lanes.count, or fewer when the newest lane_count rule in force says so
lane-1 to lane-16That lane
trial:t- and 8 to 32 hexadecimal charactersThat trial. If it ends before it reads the message, its lane’s next trial reads it, with a note that it was sent to an earlier trial
plannerThe planner. A research run has none
advisorsThe teacher pair, the auditor and the review panel, each once, except those that a refusal below names
teachersThe teacher pair
auditorThe auditor
panelThe review panel

Each reader reads a message once: each lane, the planner and each advisor role. Only a message kept in force is given again, to each new trial. A read is recorded when the reader works with the message, not when the text is only fetched or put in a brief. When each one reads it:

ReaderWhen
A lane with no trial at workIn the brief of its next trial, once that trial’s agent is at work: its first session line, or a model turn or tokens counted. If the trial fails before that, the lane’s next trial gets the message. A message in a trial’s brief is not given to it again during the trial
A worker in PiAt its next step: after its model’s current turn
A worker in OpenCode, Claude Code or CodexWhen its current run ends without a submission. The harness then continues the session with the message
A worker that has submittedIt does not read it: a submission ends the trial, and the lane’s next trial reads the message in its brief
The plannerAt its next plan whose model’s plan is used. While the planner is off, its model can’t be reached, or code refuses its plan on both tries (the fixed rules give out the round’s work), the message waits for the next plan
The teacher pairIn its next round, once at least one teacher has given its review. For a running run, a message starts that round: between rounds the supervisor looks for one every 5 seconds, and a round under way finishes first. When the advisors have reached the day’s spending limit, a message starts no round; the run’s live view says once the advisors reached the day's spending limit (<limit> USD): the teacher pair reads your message after midnight UTC
The auditorWith its next audit, once the audit is posted to the harness
The review panelWith its next review, once the review is posted. If a panelist gives no answer, the review is not posted, and its next try gives every panelist the message again

An advisor’s brief or package puts the owner’s messages first, and they are never cut when it is shortened. The teacher pair, the auditor and the review panel are asked to answer each message they are given, in one or two sentences, and the harness records each answer. An answer with no text is left out, and one malformed answer does not lose the advisor’s others. A message kept in force goes into every new trial’s brief under # Messages from the campaign's owner, in force; the others go under # Messages from the campaign's owner.

Control checks every message before it signs it, wherever it comes from: a run’s Live or Planner tab, the REST API, thermite command or MCP. It trims the text, and refuses a message with no words in it, or one that nothing on the rig would read. It judges that from what the rig last reported: the run, the rig’s settings (Edit rig) and the advisor roles its supervisor has set up. While the rig has not reported the run, or its supervisor has not reported its roles, control does not refuse for that reason, and the harness decides, as below. Control refuses with these messages:

MessageWhen
Write a message first.text is empty, or only spaces
This run has no agents (the judge alone tries the kit), so nothing would read it.all, planner, a lane or a trial, in a run started without agents
A research run plans its rounds with a fixed method: it has no planner to message.planner, in a research run
The planner is off on this rig (Edit rig), so nothing would read it.planner, with the planner switched off on Edit rig
A research run has no attempts to audit.auditor, in a research run
A research run has no conclusions to review.panel, in a research run
The teacher pair is off on this rig (Edit rig), so nothing would read it., and the same with auditor or review panelteachers, auditor or panel, with that role switched off on Edit rig
This rig's supervisor has no teacher pair set up, so nothing would read it., and the same with auditor or review panelteachers, auditor or panel, when the rig’s supervisor reports that it has no such role
This run has no goal the review panel reviews, so nothing would read it.panel, when the run’s contract has no goal checked by review
This rig's supervisor has no advisors set up, so nothing would read it.advisors, when the supervisor reports none of the three roles
No advisor would read it., followed by the reason for each role, in the order teacher pair, auditor, review paneladvisors, when each of the three roles has one of the reasons above

Over the REST API and thermite command, a refusal is 400 {"error":"<message>"}, written as refused. Over MCP, issue_command answers {"error":"<message>"} with isError: true, written as refused. On the dashboard it shows as Not sent. and the message (below), written as refused. A run’s Live also greys out each recipient that control would refuse, with the same words when you point at it (on a run without agents that includes everyone working and the lanes, and the box starts on the advisors), and its message box asks for words before it sends: “Write a message first.”

Control refuses keep with any other to before signing: bad message_agent payload: keep: only a message to everyone working (all) or to a lane can be kept in force. The harness checks again when it applies the command. It refuses a message in these cases, and records no message:

MessageWhen
run <run> is closed: nothing will read this messageThe run is closed
run <run> has no agents (the judge alone tries the kit): nothing would read this messageall, planner, a lane or a trial, in a run started without agents
a research run plans its rounds with a fixed method; it has no planner to messageplanner, in a research run
the planner is off on this rig (Edit rig), so nothing would read this messageplanner, with the planner switched off on Edit rig
the teacher pair is off on this rig (Edit rig), so nothing would read this messageteachers, with the teacher pair switched off
the auditor is off on this rig (Edit rig), so nothing would read this messageauditor, with the auditor switched off
a research run has no audits, so the auditor would never read this messageauditor, in a research run
the review panel is off on this rig (Edit rig), so nothing would read this messagepanel, with the review panel switched off
this run has no goal the review panel reviews, so the panel would never read this messagepanel, in a run with no goal checked by review
none of the advisors would read this message: <each reason, joined by "; ">advisors, when no role would read it
run <run> has <n> lanes, or … has 1 laneA lane beyond the contract’s number of lanes. A lane within it, but above the number a lane_count rule leaves at work, is accepted
no trial t-… in run <run>A trial the run does not have

The run’s Live shows the rig’s refusal of the newest message to the run, from wherever it was sent, as Not sent. The rig refused your last message: <reason> above its message box, the next time the page loads. The run’s Planner tab does the same for the newest message to the planner, above Tell the planner: Not sent. The rig refused your last message to the planner: <reason>. A message from the dashboard that control refuses before it signs it shows in the same place at once, as Not sent. and control’s reason. Control’s audit log records that refusal as refused, with the page’s path and the reason (Audit log actions). While the planner is off on Edit rig, the Planner tab has no form under Tell the planner, only “The planner is off on this rig, so it reads no messages. The lanes take the queue in order and keep to the rules. Switch it on in Edit rig to tell it something.”, with Switch it on in Edit rig as a link to the rig’s Edit rig page.

The harness writes the live line Owner to <to> (#<id>): <text>, with , kept in force after the id for a kept message. A run’s Live shows a signed line as To the recipient and the text. A line for everyone working, a lane, the planner or the advisors shows so at once, in its own words. A line for a trial shows as a plain line until the run’s agent_message event, below, reaches control with the rig’s report; Live then draws it again in place, with the event’s words. Result: {"message": <id>, "to", "keep", "readers", "note"}. readers lists who will read it: lanes, planner, teachers, auditor or panel. note is there only for a trial that has ended: trial t-… has ended; lane-<n>'s next trial reads this.

The run’s ledger records the message with its text (agent_message), each reader’s first read (agent_message_read, {message, by}, with trial for a lane’s read and round for the planner’s: the round whose plan read it) and each advisor’s answer (advisor_answer). A rule the planner makes from the message is a directive whose body has from_message. The run’s Planner tab shows a message to the planner, or to everyone working, under Tell the planner: waiting until the planner’s round reads it, then with the planner’s answer, made from these records: the rules it made from the message, the directions it designed for them, and the round’s notes. The run’s Live shows the message, each read and each answer only from the harness’s signed live lines; an unsigned line that looks like one shows as a plain line. A lane’s read, like a message to a trial, shows plainly until the run’s agent_message event reaches control.

Removes a message from force: new trials’ briefs no longer carry it, and no reader that has not read it yet is given it.

FieldTypeMeaning
runlowercase idThe run
messagepositive whole numberThe message’s id: message in the result of its message_agent

The harness refuses: no message <id> in run <run>, message <id> is not kept in force, and message <id> was already removed from force at <time>. The harness writes the live line the owner's message #<id> is no longer in force, and the ledger records agent_message_unkept. A run’s Live shows that line, when the harness signed it, under You (and the message’s lane, for a message to a lane) as 📌 You removed your kept message: “<text>”. New trials no longer get it., the text cut to 120 characters, or with (#<id>) in place of the text when control does not have the message. Result: {"message": <id>, "unkept": true}.

Adds an experiment to the run’s experiment queue. Each run has its own queue: the experiments still to try, in order (Up next), the lanes’ ideas that wait for a decision, and every experiment tried, with what became of it. At the start of each round the planner gives the experiments at the top to free lanes, never the same one to two lanes. Choose what runs next shows the queue on the dashboard.

FieldTypeDefaultMeaning
runlowercase idrequiredThe run
titletext, 1 to 200 characters, trimmedrequiredWhat to try
detailstext up to 4000 charactersnoneMore about it
familytext, 1 to 80 characters, trimmednoneIts direction: in your own words, or a direction’s name. Written as :name:, it is kept without the colons
whytext up to 2000 charactersnoneWhy it is worth trying. The planner reads it, and so does the worker whose lane gets the experiment
position"top", "end", or a whole number from 0 to 100000"end"Where it goes among the queued experiments

The dashboard’s Add sends Add an experiment as title, and Direction, Why and Details as family, why and details when they are not empty. It puts the experiment at the top, unless Where says at the end. Over the REST API, the command line and MCP it goes to the end unless position says otherwise.

These rules hold for the five queue kinds:

  • An experiment’s id is x- and 12 hexadecimal characters, such as x-3f6c2a8e5b1d. Control refuses another before signing: bad <kind> payload: id: an experiment id (x-...).
  • An experiment’s place is its index among the queued experiments, 0 first. A number puts it at that index among the others; a number past the last puts it at the end.
  • The harness refuses a run it doesn’t have (no run <run>), a closed run (run <run> is closed: its queue and rules no longer change), and an experiment the run doesn’t have (no experiment <id> in run <run>).
  • The harness writes the experiment and one event in one step. The event’s actor is control:<issued_by>, such as control:owner:ui, and its by is owner, whoever issued the command.

The new experiment is up next, with owner as its author. Result: {"experiment": "x-…", "state": "queued", "position": <n>}, position being its place now. Event: experiment_added, {id, title, details?, family?, why?, by, state, position}.

Moves an experiment that is up next to another place.

FieldTypeMeaning
runlowercase idThe run
idexperiment idThe experiment
position"top", "end", or a whole number from 0 to 100000Its new place

↑ Top on the dashboard sends "top"; dragging an experiment sends its new index. The harness refuses an experiment that is not up next: experiment <id> is <state>: only queued experiments move. Result as for queue_add. Event: experiment_moved, {id, by, position}.

Changes the words of an experiment that still waits: one up next, or a lane’s idea.

FieldTypeMeaning
runlowercase idThe run
idexperiment idThe experiment
title, details, family, whyas for queue_addThe new words. Give at least one; the others stay as they are

An empty details or why clears it; title and family cannot be empty.

Edit on an experiment under Up next opens a form with its words: What to try (title), Details, Direction (family) and Why. Save always sends details and why, so a field you empty is cleared. It sends title and family only when they are not empty, so an emptied one keeps its old words. A lane’s idea has no Edit on the dashboard.

Control refuses an edit with none before signing: bad queue_edit payload: payload: change at least one of title, details, family and why. The harness refuses an experiment that no longer waits: experiment <id> is <state>: only proposed or queued experiments can be edited. Result as for queue_add; position is null for a lane’s idea. Event: experiment_edited, {id, by} and the fields that changed.

Drops an experiment that still waits, so no lane tries it. It stays in the run’s record as dropped, with who dropped it and why.

FieldTypeMeaning
runlowercase idThe run
idexperiment idThe experiment
whytext, 1 to 2000 characters, trimmedWhy. Required

Drop it on the dashboard sends what you write in its why field. Drop on a lane’s idea sends the owner dropped this idea. The harness refuses an experiment that no longer waits: experiment <id> is <state>: only proposed or queued experiments can be dropped. Result: {"experiment": "x-…", "state": "dropped", "position": null}. Event: experiment_dropped, {id, by, why}.

Queues a lane’s idea: an experiment a worker proposed (with thermite-trial idea or Pi’s idea tool) goes up next. The planner’s model also queues or drops ideas as it plans. Under Ideas from the lanes, the Experiments tab says: “The planner’s model queues or drops these at its next hand-out, and you can decide first. Without its model (none set, its answer refused, the planner off, or a research run) they wait for you.”

FieldTypeDefaultMeaning
runlowercase idrequiredThe run
idexperiment idrequiredThe idea
position"top", "end", or a whole number from 0 to 100000the endWhere it goes

Queue it on the dashboard sends no position, so the idea goes to the end. The harness refuses anything but an idea: experiment <id> is <state>, not a proposed idea. Result as for queue_add. Event: experiment_accepted, {id, by, position?}.

Removes one directive, a rule for the plan, whoever set it: the owner, the teacher pair or the planner.

FieldTypeMeaning
runlowercase idThe run
idpositive whole numberThe directive’s id: one of directives in its redirect’s result, or id in the run’s rules (run_queue)

The harness refuses: no run <run>, a closed run (run <run> is closed: its queue and rules no longer change), and no rule <id> in force in run <run>. The directive stays in the record with the status dropped, and the ledger records directive_dropped, {id}. Result: {"run", "dropped": <id>}.

A removed rule does not come back from the advisors, the planner or an agent’s token. The teacher pair’s next set of directives replaces its earlier set, the planner adds rules when an owner’s message asks for one, and an agent token may send a redirect. Before the harness records such a set, it leaves out each rule that matches a rule removed from the run with drop_directive: the same kind, and the same family or match words, ignoring case and spaces at the ends. A rule that names neither matches every removed rule of its kind that names neither. The harness records the left-out rules in one event, directives_left_out, {rules, why: "the owner removed these rules"}, whose actor is supervisor, planner or control:agent:mcp, and records the rest of the set. Only a redirect issued by the owner, or by an operator for the owner, sets a removed rule again: its author is control:owner:…, such as control:owner:api for the owner’s REST API or control:owner:operator.

Another rig’s published champion, sent as a candidate for this rig’s judge. Control builds it from the source rig’s signed report. See Run a campaign on several rigs.

FieldTypeMeaning
runlowercase idThe run on this rig
source_rigtextThe rig the champion comes from
source_runlowercase idIts run
source_seqpositive whole numberThe champion’s number in that run
importa champion_patch importThe champion as a patch, with its path propagate/<rig>/<run>/<seq>.patch

Control refuses to build one: propagate between two different rigs, no run <run> on <rig>, the source run has no champion beyond its seed, champion seq <n> is not published yet (it is published with the rig's next export), <run> runs <campaign>, not <campaign>, the target works on <repo>, the champion is in <repo>.

The supervisor takes the patch from the results repository, and waits while it is not there yet. The harness then refuses: research campaigns do not take propagations, propagate takes a champion_patch import (a published champion), the champion is in <repo>; this run works on <repo>, base commit <commit> is not in this rig's repository, the champion patch does not apply to the base: …, tree mismatch: the signed tree is <tree>, the patch gives <tree>. If this rig’s champion already has the same files, the result is {"identical": true, "note": "this rig's champion already has the same tree"}. Otherwise the patch becomes an attempt that this rig’s judge measures against its own champion. Result: {"attempt", "lifecycle", "tree"}.

FieldTypeDefaultMeaning
fullbooleanfalsetrue also writes the research archive and a snapshot of the ledger. A full reset asks for a full export

The harness writes an export to its outbox for the supervisor to push. Result: {"export", "export_id", "through_event", "attempts", "events"}.

Only control’s full reset issues it, as the owner who asked for the reset, after the owner confirmed the wipe. See Stop, restart, remove or reset a rig.

FieldTypeMeaning
confirm"wipe"Must be exactly this
exporttext, 1 to 100 charactersThe full export that reached the results repository
through_eventwhole number, 0 or moreThe last event that export holds

The harness refuses: no export <export> in this rig, export <export> is not a full export, the export does not cover the events, and research was recorded after the export (<what>): export again. Otherwise it pauses the running runs, and the rig’s state is wiped when the harness next starts, a few seconds later. The rig keeps its own signing key. Result: {"wiping": true, "export"}.

FieldTypeMeaning
runlowercase idThe run
attempttext, 1 to 64 charactersThe attempt

Promotes a confirmed win that waits for the owner, or overrules an audit that is pending or that blocked it. The judge’s verdict must still allow promotion, and approving an attempt with declared flaws accepts them. If the champion moved since the attempt was judged, it is measured again against the new champion. Refusals: attempt <attempt> is not in <run>, attempt <attempt> is <lifecycle>, not awaiting approval, no pending promotion for <attempt>.

FieldTypeMeaning
runlowercase idThe run
attempttext, 1 to 64 charactersThe attempt
notetext up to 2000 charactersOptional. Why

The attempt never becomes the champion and stays in the record as declined. Refusals: attempt <attempt> is not in <run>, attempt <attempt> is <lifecycle>: there is nothing to decline. Result: {"attempt", "declined": true, "was"}.

The rig’s own settings, as on its Edit rig page. Only the fields that change are sent, and no other field is allowed.

FieldTypeMeaning
judgebooleanThe judge builds and measures. Off: changes wait in its queue
workersbooleanWorkers start trials. Off: no new trial starts
lanes1 to 16Trials at the same time. More is refused with at most 16: a rig has users for lanes 1 to 16 only
plannerbooleanThe planner’s model gives each lane its experiment at the start of each round. Off: lanes take the queued experiments in order, by fixed rules, with no model
auditorbooleanThe supervisor’s auditor runs, where configured
teachersbooleanThe supervisor’s teacher pair and combiner run, where configured
reviewbooleanThe supervisor’s review panel runs, where configured
exportsbooleanHourly exports for the supervisor to push
trial_minutes1 to 10080A trial’s length when its contract sets none
max_turns1 to 100000, or nullA limit on each trial’s model turns. null: no limit
promotionauto or owner_approvalThe rig’s promotion rule, for runs that set none
judge_gpus1 to 8, or nullOn a rig given its set of GPUs, how many are the judge’s. null: the rig’s default. A change restarts the harness to regroup the cards

The harness keeps the settings, applies them at once and reports them, with a rig_settings_changed event that holds the change and the settings after it. Result: {"settings": <all the rig's settings>}.

The History of the run at work at the time says what changed, such as “You changed the rig’s settings: the planner off, 3 lanes, no turn limit.” Each field reads as the judge, the workers, the planner, the auditor, the teacher pair, the review panel or exports with on or off; <n> lanes (1 lane); trials of <n> min; at most <n> turns a trial or no turn limit; confirmed wins kept at once or confirmed wins wait for you; and <n> GPUs for the judge (1 GPU for the judge) or the judge's usual share of GPUs.

The owner’s final word on a goal that the review panel passed.

FieldTypeMeaning
runlowercase idThe run
reviewpositive whole numberThe review
acceptbooleantrue accepts, false rejects
notetext up to 2000 charactersOptional

The judge then checks the champion’s goals again. Result: {"review", "decision": "accepted"} or "rejected".

The owner’s answer to the harness’s proposal that a goal campaign’s goals cannot be reached.

FieldTypeMeaning
runlowercase idThe run
proposalpositive whole numberThe proposal
acceptbooleantrue closes the run as not possible; false dismisses the proposal and the agents carry on
notetext up to 2000 charactersOptional

assign_campaign lists its imports in imports, and propagate carries one in import. Every path is relative to the rig’s inbox and may not contain ... The harness copies each file into its own store, hashing it as it copies.

KindFieldsUsed in
repo_bundlepath; repo (lowercase id); commit; sha256 (optional: the bundle file’s hash, checked as well as the commit); source (optional)assign_campaign: the project
kit_bundleThe same fieldsassign_campaign: the kit
modelpath; name; sha256 (required); source (optional)assign_campaign: a model the contract lists
champion_patchpath; repo; base_commit; tree; sha256 (all required)propagate only. In assign_campaign it is refused: a champion patch comes only with a propagate command

source tells the supervisor where to fetch the content before it relays the command:

SourceForMeaning
{"git": "<URL>"}repo_bundle, kit_bundleA git repository that has the commit: an https://, ssh://, git:// or file:// address, or user@host:path, up to 2000 characters. The supervisor fetches the commit and writes it to path as a bundle
{"git": "<URL>", "access": "<name>"}repo_bundle, kit_bundleThe same, with Git access that control keeps for a private repository, by name. See Use a private repository
{"control": "kits/<name>"}kit_bundleA kit that control wrote and keeps itself
{"url": "<URL>"}modelAn http:// or https:// address, up to 4000 characters. The supervisor downloads the file and keeps it only with the import’s sha256, reporting its progress. See Model files on a rig

A source that cannot be fetched makes the command failed with {"error": "staging: <reason>"}. Without a source, the file must already be in the inbox.

The harness refuses an import with: path escapes inbox: <path>, not found in inbox: <path>, symlink escapes inbox: <path>, sha256 mismatch for <path>: expected <hash>, got <hash>, or bundle does not contain commit <commit>.

Commands signed before runs and rigs were renamed still verify. Their envelope names unit instead of rig; their payloads may name instance, source_instance and source_unit, read as run, source_run and source_rig; and the kinds pause_instance and resume_instance are read as pause_run and resume_run.