Sending Messages to a Running Agent
An agent that is in the middle of a turn is not frozen. You can still reach it — stop it, steer it, or leave a note for it to read when it is done. SCORPIOX CODE gives you one shared submission path for all of it, and it works the same whether you are typing at the keyboard, dropping a file on disk, calling the API, or embedding the agent in your own application.
This page covers the three submit modes, exactly how each behaves in the terminal, the drop-in session inbox, remote control through SCORPIOX BOT, and the public embedding API.
Docs for SCORPIOX CODE @ 94275ad.
The three submit modes
Every message you send to a running agent is classified as one of three modes. The mode is not a transport choice — it is a timing choice: when should the agent read this.
| Mode | When the agent is idle | When the agent is busy | Use it for |
|---|---|---|---|
auto |
Starts a new turn. | Behaves like interrupt. |
The default. "Just send it; do the sensible thing." |
interrupt |
Starts a new turn. | Injected into the current run — the agent reads it at the next tool-call boundary, mid-turn. | Steering: "keep going, but now do X instead." |
queue |
Starts a new turn. | Stashed and held. Delivered as a fresh run once the current turn finishes. | "Don't bother it now — run this next." |
Two facts fall out of the table immediately:
autois the workhorse. It resolves to "new turn" when nothing is running and to "interrupt" when something is. That is exactly what pressing Enter does in the terminal — there is no separate auto/interrupt/queue key in the TUI, just Enter and a~prefix (below).interruptandqueueonly differ while the agent is busy. When the agent is idle, all three modes do the same thing: they start a new turn.
interrupt messages accumulate. If you interrupt twice before the agent reaches a tool-call boundary, both notes are joined and delivered together at the next boundary. queue messages likewise accumulate — every queued message is joined and sent as one follow-up run when the turn ends.
In the TUI
The terminal gives you the three modes with two keys and one prefix.
Enter
Pressing Enter submits with auto. So:
- Agent idle → your text starts a new turn immediately.
- Agent busy → your text becomes an interrupt. The TUI echoes it as
[Interrupt] …and the agent picks it up at the next tool-call boundary, mid-turn.
There is no "are you sure the agent is busy?" prompt. Enter-while-busy is a feature, not a mistake — it is the fastest way to redirect a running turn.
The ~ prefix: queue it
If you type a message while the agent is busy and you want it to wait its turn rather than cut in, prefix it with a tilde:
~ once this test passes, update the changelog
The leading ~ (and any spaces after it) are stripped, and the remainder is submitted as queue. The agent keeps running its current turn, and when that turn ends, SCORPIOX CODE sends the queued text as a fresh run. You will see a [Queued N] … line confirming it is waiting, and a [Sending queued message(s)] line when it is finally dispatched.
Queue multiple notes by sending several ~-prefixed messages in a row — they are all held and delivered together when the turn completes.
ESC: pure cancel
The ESC key is not a submit mode. Pressing ESC while the agent is running cancels the agentic loop entirely — the turn stops, the agent returns to the prompt, and nothing queued or interrupted is sent as a replacement. Use it to stop, not to say.
| You want… | In the TUI |
|---|---|
| Stop it dead, say nothing | ESC |
| Keep it going, but redirect | Just type and press Enter (interrupt) |
| Let it finish, then do this | Type ~ your note and press Enter (queue) |
The drop-in session inbox
The TUI, the API, and any external program all share one other input channel: a folder on disk. Every session has an inbox directory, and the agent polls it as part of its normal loop.
.scorpiox/sessions/<session-id>/inbox/
<stem>.json ← a message the agent has not read yet
<stem>.png ← optional image sidecar (same stem)
done/
<stem>.json ← messages the agent has already consumed
A message is a small JSON file. The agent looks for the text field (or content, the accepted alias) and an optional mode field:
{ "mode": "queue", "text": "Once the build is green, run the full test suite." }
mode accepts auto, interrupt, or queue. If you omit it, it defaults to auto.
The handshake: write .tmp, then rename
The inbox is a coordination point between two processes, so the delivery is made atomic. You do not write <stem>.json directly. You write the fully-formed message to a temporary name and rename it into place:
<stem>.json.tmp → <stem>.json
The agent only ever sees complete files, never a half-written one. Use any stem you like — a timestamp, a sequence number, a UUID — as long as it is a safe filename (letters, digits, _, -, .). The agent sorts pending stems and processes one per poll pass, in name order, so the stem doubles as a priority/sort key.
Images
A message can carry an image. Put an image file in the inbox with the same stem as its JSON and one of these extensions: png, jpg, jpeg, gif, webp. The agent reads it in alongside the text. There is a hard cap — an image larger than about 8 MB is ignored, and a JSON larger than about 64 KB is skipped — so keep payloads in the message, not in the file.
Consumption
Once the agent has read a message, it moves the file (and any image sidecar) into inbox/done/. The done/ folder is your audit trail of what the agent has actually acted on. An empty inbox/ is a no-op, so it is safe to point the poller at a session that is about to start.
The SCORPIOX_INBOX gate
The whole inbox channel is behind a single config flag, SCORPIOX_INBOX, which is on by default. Set it to 0 to disable polling entirely. As with every other setting, the value follows the standard configuration cascade — built-in default, then global, user, project, profile, then the real environment variable. See Configuration and Profiles for the cascade and where each file lives.
Because the inbox is just files, it is the most portable input channel: a build script, a cron job, another machine, or another agent can reach a running session by dropping one file.
From SCORPIOX BOT
SCORPIOX BOT is the layer that drives a session without a keyboard. To send a message to a session that is already alive, you hit the same inbox over HTTP:
POST /inbox?id=<session-id>&mode=auto
Content-Type: text/plain
Fix the null check in auth.c and run the tests
The mode query parameter is the same auto / interrupt / queue set as everywhere else. The body is plain text — the SCORPIO BOT layer writes it into the session inbox for you, so the atomic-rename handshake is handled for you rather than something you have to do by hand. A tilde prefix on the body is read as queue, which is the same ~ shorthand the TUI uses.
The write is atomic, and the route waits up to two seconds for the agent to actually consume the message before it answers, so the response tells you whether delivery happened:
{ "ok": true, "inbox": true, "stem": "1759000420_000000123", "mode": "auto", "session": "fix_auth", "consumed": true }
"consumed": false means written, not yet picked up — not a failure. The agent was mid-turn; the file is in the inbox and will be read at the next poll pass. For the full remote-control surface — session dispatch, the live terminal stream, escape/interrupt keystrokes, and the fleet dashboard — see Remote Agent Control and Fleet Management with SCORPIOX BOT.
The embedding API
When you embed the agent in your own application through the public DLL, the same three modes are three exported functions. These are the public entry points — you call them from your host (C# P/Invoke, Swift, or any FFI host) and the agent handles the rest.
| Export | What it does |
|---|---|
sx_interrupt() |
Cancels the agent immediately — the programmatic equivalent of pressing ESC in the TUI. |
sx_interrupt_message(message) |
Injects a message into the running turn. The agent reads it at the next tool-call boundary — a steer, not a stop. Returns -1 if the agent is not busy. |
sx_enqueue(message) |
Queues a message for after the current turn. If the agent is already idle, it is sent immediately as a new turn. |
[DllImport("sx.dll")] static extern int sx_interrupt();
[DllImport("sx.dll")] static extern int sx_interrupt_message(string message);
[DllImport("sx.dll")] static extern int sx_enqueue(string message);
sx_interrupt() and sx_interrupt_message() are the two halves of steering: the first says stop, the second says stop and say this first. sx_enqueue() is the "do this next" path — the same end-of-turn delivery the TUI's ~ prefix uses.
How this compares to other harnesses
The question every agent harness has to answer is: what happens to a message the user sends while the agent is already working? The honest answers differ in one dimension — where the waiting message lives.
OpenCode v2
OpenCode's v2 model is the most developed of the public alternatives. It separates steer (a note the agent should fold into the current turn) from queue (a note to run after the current turn), and it does not keep that state in volatile memory. Queued and steered messages are written into a durable, database-backed queue, and delivery is coordinated by a leader / queue-management layer (leader-q) that owns which message goes out and when. That coordination survives process restarts and lets a client inspect or mutate the pending queue. For the "stop it" case, OpenCode exposes a dedicated interrupt key that halts the active turn.
SCORPIOX CODE
SCORPIOX CODE takes the opposite trade. The interrupt and queue state is in-memory: two small, process-local buffers — one for the next tool-call boundary, one for end-of-turn — drained by the agent's own loop. There is no separate coordinator process and no database. What you get in exchange:
- No moving parts. Delivery is the agent polling its own buffers and its own inbox directory. Nothing to deploy, nothing to keep in sync, nothing to restart.
- A durable channel without a database. The part that must survive across processes — "a message from another program or machine" — is the filesystem inbox, not a queue table. The inbox is the durable surface; the in-memory buffers are just the fast path between polls.
- One submit path everywhere. TUI Enter, the
~prefix, a dropped file, the SCORPIO BOT HTTP route, and the DLL exports all funnel into the same classification. There is no per-surface queue to reason about.
The consequence of the trade: SCORPIOX CODE's live interrupt/queue state is volatile — it lives as long as the process. If the process dies, in-flight notes in the buffers are lost, but anything already written to inbox/ is not, because the filesystem is the source of truth for cross-process delivery. OpenCode's database-backed queue makes the live state durable at the cost of a persistent store and a leader to manage it.
| Dimension | OpenCode v2 | SCORPIOX CODE |
|---|---|---|
| Where waiting messages live | Durable database-backed queue | In-memory buffers + filesystem inbox |
| Coordination | Leader / queue-management layer (leader-q) |
The agent's own loop polls buffers + inbox |
| Cross-process delivery | Via the durable queue | Via the inbox/ drop-in folder |
| "Stop it" | Dedicated interrupt key | ESC in the TUI, sx_interrupt() in the API |
| Durability of live notes | Survives restart (database) | Lost if the process dies; inbox files survive |
| Per-surface queues | Central queue | None — one shared submit path |
Choosing a mode
- Default to
auto(or just press Enter). It does the right thing in both states and there is almost never a reason to think harder. - Interrupt when the agent is going down the wrong road and you want to redirect now — mid-turn, at the next tool call. In the TUI that is just Enter; in the API that is
mode=interruptorsx_interrupt_message. - Queue when the current work is on track and you have a follow-up. In the TUI that is
~ note; in the API that ismode=queueorsx_enqueue. - Cancel (ESC, or
sx_interrupt()) when you do not want the current turn to finish at all — it stops the turn and sends nothing in its place.