Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Interrupting or waking another session (the two confirmed escalations)

Two opt-in escalations when one agent messages another: kill a busy target's turn instead of waiting for it to finish, and wake a paused, archived, ended or errored target instead of being refused. Both are off by default because both have real consequences, and both must be asked for explicitly. Also the per-target circuit breaker, which stops a sender looping against one target after three terminal refusals and tells it to re-route instead.

What it is

Two opt-in, confirmed escalations on POST /sessions/:id/peer-message: kill a busy target's in-flight turn instead of waiting for it (confirmInterruptTurn), and wake a paused / archived / ended / errored target instead of being refused (confirmInactiveTarget). Both are shipped, tested, and already live — this page exists because neither was documented anywhere before now.

Where to find it

Agent-facing rather than user-facing: these are two fields an agent adds to the peer-message route on the local control server. Nothing in the UI exposes them, deliberately — a session interrupts or wakes another one on purpose, from code.

How it behaves

The short version

You want to… Send Default behavior without it
Interrupt a target mid-turn right now confirmInterruptTurn: true Message is held on the turn-boundary queue, delivered when the turn ends
Wake a paused / archived / ended / errored target confirmInactiveTarget: true 409 inactive_target_needs_confirmation — nothing is touched

Both fields go on the same route, POST /sessions/:id/peer-message — see that page for the base mechanics (auth, provenance header, rate limiting, idempotency). This page covers only the two escalations and one sharp edge.

Auth reminder: you send this as your own session, with your scoped $AMC_CLI_TOKEN and your own X-AMC-Source-Session-Id. The route admits the agent-session tier (admitAgentSession + allowAgentSession) because a session reaching a peer is the whole point of it.

The reverse is what is refused: the GLOBAL ~/.amc/cli-token acting as a named session — an owner-file caller that declares a source session id is turned away here. That pairing is what let a caller send a message wearing a live session's identity, attributed to it and billed to it, so the route's own guard rejects it.

The owner credential cannot use this route at all — in either shape. The obvious workaround is to drop the header and call as the owner alone, and it does not work: this route also requires an identified sender, and a header-less owner proves no session, so it is refused 400 as source-less. The two refusals point at each other, and that is deliberate — a cross-session delivery that spends a paid turn must be attributable to a real session. A session reaches a peer with its own token, which is why the scoped tier is admitted here in the first place.

1. Interrupt a busy target — confirmInterruptTurn

By default, peer-message never interrupts a target that is mid-turn: the message is written to that session's durable turn-boundary queue and the call returns 200 { queued: true }. That is correct almost all of the time — nothing is lost, nothing is destroyed. confirmInterruptTurn: true is the deliberate override for the rare case where your message matters more than the work the target is doing right now: it SIGKILLs the in-flight turn and respawns with your message, exactly like a human clicking "Send now (interrupts the current step)" in the composer. If messages are already queued for the target — including the held copy of this one from a first plain send — the interrupt delivers them in the same turn, oldest first and yours last: a message never jumps the queue.

Native targets only. An external-engine target (gemini/codex/pi/openclaw/…) has no kill-and-respawn path, so a busy one still queues even with the flag set.

Recipe

TOKEN=$AMC_CLI_TOKEN

# Step 1 — a plain send. If the target is mid-turn, this is HELD, not dropped.
curl -s -X POST "http://127.0.0.1:19519/sessions/<target-id>/peer-message" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"text":"Master moved under you — rebase before you tag ready-to-merge."}'
# → 200 { "ok": true, "delivered": true, "wokenFrom": null, "queued": true,
#          "detail": "...re-send with confirmInterruptTurn: true..." }

# Step 2 — only if it genuinely cannot wait: kill the in-flight turn and deliver now.
curl -s -X POST "http://127.0.0.1:19519/sessions/<target-id>/peer-message" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"text":"Master moved under you — rebase before you tag ready-to-merge.","confirmInterruptTurn":true}'
# → 200 { "ok": true, "delivered": true, "wokenFrom": null }

A queued: true response is a success — do not retry the plain call. Only send Step 2 when you have decided the interruption is worth it.

Idempotency caution. If you attach X-Client-Request-Id to Step 1, its queued: true response is a success and is cached for ~10 minutes. Reusing that same key on the Step 2 (confirmed) call replays the cached queued result instead of actually interrupting — mint a fresh X-Client-Request-Id for Step 2. (This is the opposite of the wake recipe below, where reusing the same key on the confirmed retry is explicitly safe — the difference is that Step 1 here succeeds, while a dormant-target refusal does not.)

The sharp edge: a second route interrupts with no flag at all

POST /session/:id/message — the "send the user's next turn" route — has no interrupt guard at all. It calls sendResponse with isAutoResponse: false, and the class-wide mid-turn guard that protects every mechanical sender only fires when isAutoResponse is true. So this route always behaves like a human hitting "Send now": if the target is mid-turn, it kills and respawns it immediately, unconditionally — no confirmInterruptTurn, no queueing, no opt-out.

This is intentional for a human-typed message (that IS what "Send now" means), but it means an agent calling POST /session/:id/message gets the destructive behavior by default, while peer-message requires you to opt in. If you want the safe-by-default behavior (hold until the turn ends, unless you explicitly say otherwise), use peer-message, not this route. This asymmetry is a known, accepted design tradeoff — not a bug, and not something this page changes.

2. Wake a dormant or archived target — confirmInactiveTarget

peer-message's target can be in any status. For a target that is paused, archived, ended, or error (collectively "dormant"), waking it is a real, visible side effect — it re-enters the live session list and starts spending money again — so by default the call refuses:

409 { "ok": false, "error": "inactive_target_needs_confirmation",
      "targetStatus": "archived",
      "detail": "\"<name>\" (<id>) is ARCHIVED — delivering will pull it back into the live session list and it will start working again. Nothing has been sent. If you meant to wake it, re-send the same request with \"confirmInactiveTarget\": true." }

Nothing is unpaused, unarchived, spawned, or written on the refusal. Re-sending the identical request with confirmInactiveTarget: true proceeds and wakes it.

Recipe

TOKEN=$AMC_CLI_TOKEN

# Step 1 — plain send to a target you believe may be dormant.
curl -s -X POST "http://127.0.0.1:19519/sessions/<target-id>/peer-message" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"text":"New findings on the auth bug — can you take another look?"}'
# → 409 { "ok": false, "error": "inactive_target_needs_confirmation", "targetStatus": "archived", "detail": "..." }

# Step 2 — confirm and proceed. Safe to reuse the SAME X-Client-Request-Id here if you used
# one on Step 1 — a 409 releases the idempotency claim (unlike the interrupt recipe above).
curl -s -X POST "http://127.0.0.1:19519/sessions/<target-id>/peer-message" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{"text":"New findings on the auth bug — can you take another look?","confirmInactiveTarget":true}'
# → 200 { "ok": true, "delivered": true, "wokenFrom": "archived" }

wokenFrom tells you what you just did: "paused" (unpaused), "archived" (unarchived), "ended" / "error" (a fresh CLI process was spawned). null means the target was already live — or that a PAUSED target HELD your message instead of being woken. queued: true tells those two apart, and it is the only difference between "the receiver has it now" and "the receiver will get it when a person unpauses it". See the pause rule below.

A paused target is HELD, not woken (2026-09-28)

Your confirmation is not enough to unpause a session. A pause on this box is a person's own command — nothing automatic writes it, and the reaper's floors, the adopt gate and the stranded triage all read it as a standing human hold. So a delivery from an agent, a cron or a machine notice to a paused target is written to that session's OWN turn-boundary queue and handed over when a person unpauses it:

# → 200 { "ok": true, "delivered": true, "queued": true, "wokenFrom": null }

confirmInactiveTarget: true does not change that — it only clears the 409. Nothing is lost, so do not retry: the row is durable and drains on the unpause. Only a PERSON's own send (the composer, or an operator broadcast) still unpauses a paused target.

Why: the wake used to be unconditional, so any machine traffic undid a person's pause — measured 2026-09-28, 25 sessions were paused at 05:15Z and three were running again 1.5 to 26 minutes later with no human resume, each flipped by its own next gate verdict or sibling message. A pause that holds only until the next notification is not a load lever.

For a Claude target, the 200 comes back once the message is accepted — not once the target is running again (2026-09-26). Waking a target with no running process means restarting its CLI, which can take a couple of minutes on a busy machine, so the route no longer holds your call open for it. If that restart then fails, you are still told: a "was NOT delivered" row lands in your own conversation and the full text is kept (readable from GET /peer-dead-letters), exactly as for a send that fails outright.

Two exemptions, and one internal override, skip the confirmation gate

  • Sending to yourself (senderSessionId === targetSessionId) — the confirmation this gate asks for can never arrive from a session that is asleep, so a self-wake (a wake schedule, a heartbeat, an in-session cron addressing its own id) delivers to a paused, ended or errored session.
  • Replying to a session that messaged you first — if the target sent you a peer-message within the last 24 hours, your reply wakes it without confirmation. Provable only from your own transcript (the target's session id must appear there), so this can't be talked into by a caller-supplied claim.

Neither exemption reaches an ARCHIVED session (the owner's rule, 2026-09-24: "Agents can force through if important otherwise block"). A reply or a self-send to an archived session gets the same 409 inactive_target_needs_confirmation as any other send, and so does POST /session/:id/message. Archived means the conversation is over — re-send with confirmInactiveTarget: true only when it genuinely matters.

  • The statuses a mechanical sender has ALREADY confirmed (wakeConfirmedInactiveStatuses) — an internal option, not in the route's request body, so you cannot set it from peer-message. It names dormant statuses the caller has decided to wake, and the guard skips exactly those. It exists for the one sender that can never answer the confirmation: the Review Service delivering a finished review's whole result back to the session that asked for it (review-service/deliver-review.ts). There the sender is the app, every reviewer behind the result is already archived, and the delivery is attempted once — so a refusal is not a prompt, it is a silent loss. It names archived, ended and error — a finished review wakes the session that asked for it however it went to sleep — and never paused, which on this box is always a person's command. A named list is that caller's whole policy: the self-send and reply exemptions above never widen it.

Two guards that beat even a confirmed request

  • user_closed always wins. If a person closed the target session, confirmInactiveTarget does not reopen it — you get 409 { "error": "user_closed" } regardless. (A different opt-in, allowReopenUserClosed, exists for that specific case and is currently reserved for the Auto-lander's hand-back flow.)
  • noWakeFromArchived hard-refuses an archived target with { "error": "session_archived" }. You cannot set this yourself from peer-message — it isn't in the route's request body. It's an internal option several automated senders set on themselves (the broadcast fan-out, the cron wake adapter, worktree-ledger nudges) so routine automation can never resurrect a session someone deliberately archived. If you're calling peer-message directly with confirmInactiveTarget: true, this guard does not apply to you. The Review Service's own delivery deliberately does not set it — a finished review has to reach the session that asked for it, and that session is routinely one the app archived after it finished; that caller uses the per-status list above instead, which is what keeps a paused session out of its reach.

A guard that beats REPEATED attempts: target_circuit_open

Sending to a target that refuses you is a normal, recoverable event — a target can be busy, dormant, or closed. Sending to it three times in a row and getting a terminal refusal every time is a loop, and it is the one failure this route will not keep carrying for you.

Once your session has collected three terminal refusals (user_closed, session_archived, session_ended, target_gone, session_not_found, or an unconfirmed dormant target) against one target inside two hours, the next send is refused before anything is attempted:

409 { "ok": false, "error": "target_circuit_open",
      "detail": "3 messages in a row to that session have now been refused, the last because that session was closed. Stop re-sending: send this to a different session, or raise it with your overseer. That session will keep refusing until 2026-09-21T15:00:00.000Z. Nothing was sent this time, and nothing was lost — every earlier attempt is kept in full, so the text does not need re-sending to keep it.",
      "circuit": { "count": 3, "lastReason": "user_closed", "resetsAt": "2026-09-21T15:00:00.000Z" } }

What to do when you see it:

  • Do not re-send. Every attempt until circuit.resetsAt is refused the same way, and the refusal is written into your own transcript so a later reading of it cannot mistake the send for a success.
  • Re-route the work to a session that is actually reading, or raise it with your overseer — the two moves the refusal names.
  • Nothing is lost. Every attempt the count is made of was preserved in full, so the text does not need re-sending anywhere to keep it. The refusal itself adds no new copy.

The count is per SENDER and per TARGET: another agent's failures against that session do not count against you, and your failures against other sessions do not count against this one. A wait that resolves on its own — a busy target, a stalled one, a Do-Not-Disturb hold, an over-full queue — is never counted, and a message that does get through clears the count back to zero. The breaker closes by itself when the oldest counted refusal leaves the two-hour window.

For agents

Implementation notes (for agents touching this code)

  • confirmInterruptTurn: field + doc block in peer-message-service.ts (DeliverPeerMessageOpts, around line 280); normalised into allowInterruptInFlightTurn and logged at log.warn in deliverPeerMessage (~688-697). (It used to also bypass a needs_you inbox-card dwell hold; that hold was retired 2026-09-08 — an idle target is delivered to immediately again. See message-queue-contract I15.) Route schema + wiring in cli-server-peer-routes.ts (schema ~165, wiring ~675); the 200 { queued: true } response's detail that advertises the flag is built at ~493-501.
  • confirmInactiveTarget: DORMANT_TARGET_STATUSES and the confirmation gate in peer-message-service.ts (statuses ~37, gate ~1179-1197, exemptions ~947-971 and ~1179, user_closed ~999-1009, noWakeFromArchived enforcement ~1231-1241). Route schema + inversion into requireInactiveTargetConfirmation in cli-server-peer-routes.ts (schema ~158, inversion ~670); the release-on-refusal idempotency behavior is documented in that file's header comment (~88-98).
  • The sharp edge: POST /session/:id/message's sendResponse call with isAutoResponse: false is in cli-server-session-action-routes.ts (~410-427); the class-wide guard it skips is in user-message-router.ts (~211-223), with the kill-and-respawn mechanism it protects (killInFlightTurnForNewMessage) described just above it.
  • Contract: agent-messaging-contract.md — never-destroys-a-turn and never-resurrects name both of these as the sanctioned, explicit, recorded exceptions to the default "never interrupt, never resurrect" behavior.

Related

See also

  • cross-session-messaging.md — the base peer-message / peer-broadcast mechanics: auth, provenance, rate limiting, idempotency, dead letters, and how an arrival renders
  • queue-a-message.md — the human-facing composer equivalent of "don't interrupt, deliver when free," and POST /session/:id/queue as a durable alternative to retrying a busy peer
  • send-a-message.md — the composer's normal send, which interrupts a running turn the same way confirmInterruptTurn does, by design, for a human

Last verified 2026-10-05