Per-seat terminal delivery
The typing guard has six modes. off and hold are the two ends. Draft-aware adds an
opt-in check for unfinished input without a second control or daemon-owned retry
scheduler. Between off and hold, three interruption levels choose which automatic messages may
type: openrig-urgent, urgent-only and sends-only. Sibling seats keep their own mode.
| Mode | Terminal input | When input is unavailable |
|---|---|---|
off (default) |
Existing delivery and prompt checks | Existing send result |
draft-aware |
Paste into a recognized empty composer; check ownership again before Enter | Hold input and retain this attempt for inspection when bookkeeping is available |
openrig-urgent |
OpenRig's own messages (watchdogs, scheduled wakes, notices) and anything marked urgent type, never over a draft | Other agents' messages are held, the sender is told, and held messages stay readable |
urgent-only |
Only messages marked urgent type (rig send --urgent, or a queue item at priority urgent or critical), never over a draft |
Everything else is held, the sender is told, and held messages stay readable |
sends-only |
Deliberate sends from people and agents type, as with off; a terminal member's typing isn't protected, because its shell input can't be read |
OpenRig's own notices are held. A terminal member defaults to this level |
hold |
No automatic terminal input, even at an empty prompt | Retain for inspection outside the pane |
Lifecycle work is refused at openrig-urgent and urgent-only, as at hold.
A terminal member (runtime: terminal) defaults to sends-only, which holds OpenRig's own notices. Set one
to off and those notices, such as park reminders and other watchdog messages, type into whatever the pane
is running, and a shell prompt runs each line as a command, From and To lines included: OpenRig doesn't check
a terminal pane's foreground process. openrig-urgent and draft-aware type only into a recognized empty agent composer, so
a shell prompt holds them. Keep the default for a seat that may fall back to a shell.
Runtime permissions are separate. Recipient and interactive-prompt checks still
apply. --raw and --force do not bypass the typing guard. A deliberate
--dangerously-interact --reason remains available at every interruption level
and uses the existing-input check below. Direct human input remains available.
A message sent to a busy Codex seat joins its running turn and may redirect it.
For separate work, create a queue row instead:
rig queue create --destination <seat> --summary "<one line>" --body-file <file>.
A row that's saved keeps the work apart from its wake, the short pointer typed
into the seat. The wake is still typed input, and it isn't proof that the seat
claimed or finished the work: the row's state says that.
rig send <seat> "<message>" --wait-for-idle <seconds> suits a sender who
watches the result. If the seat is still busy, or its activity is unknown, when
the wait ends, nothing is sent (exit 1, "No text was sent.") and OpenRig keeps
nothing to send later.
Configure and inspect
Who may intervene
rig seat status <seat> shows the seat's terminal interaction account alongside
its native permissions and interruption level. These choices are independent.
The presets no-peer-interventions, help-within-instructions and full-control
set the peer row. Self and OpenRig retain their existing authority unless an
explicit exception changes it. Named helpers and the delegating instruction are
shown; OpenRig does not interpret free-text instructions as executable rules.
rig seat set-terminal-policy reviewer@my-rig --preset help-within-instructions \
--helper lead@my-rig --peer-model deny \
--reason "Help with compaction; keep the review model"
rig seat status reviewer@my-rig
Each --peer-compaction, --self-compaction, --openrig-compaction,
--peer-model, --self-model or --openrig-model exception takes allow or
deny. Model includes reasoning effort. Exceptions apply to the whole actor
row: --peer-compaction allow also allows unnamed peers. A choice with
exceptions reads as Custom. The setter replaces the complete choice; omit
exceptions to return to the preset's defaults. Outside a managed seat, use --operator <address> for
audit attribution. This does not establish an authenticated identity.
Enforcement covers managed compaction and saved model/effort changes only.
The latter take effect at the next managed continuation, not in the current
process. Other interventions remain guidance-only. Managed compaction applies
to Claude seats; it is not offered for Codex or Pi. A policy change during
preparation is checked before further input. It does not undo a submitted
/compact or abandon an already-owed restore/audit.
Existing seats retain current behaviour on upgrade. To choose a default for
newly added seats in a rig, add --rig-default to the setter above: existing
seats keep their own copied choices. OpenRig's standing managed-compaction
authority stays active unless its exception is deliberately changed.
Caller attribution is cooperative. An unattributed caller retains current
behaviour rather than being labelled the person or a peer. The existing
rig send --dangerously-interact --reason path remains available; this policy
adds no restriction there. Its existing-input check applies independently of the
typing mode. Plain tmux is not prevented.
Deliberate interaction and existing input
A managed rig send <seat> "<answer>" --dangerously-interact --reason "<why>" can drive a recognized native menu as before. Otherwise, with the delivery guard attached, it reads the visible composer under the seat's lease immediately before pasting, after buffer preparation. Empty input proceeds. A recognized dim suggestion is displayed separately and counts as empty; typed or pasted text (author unknown) stops the send with override_input_present. An unreadable input stops with override_input_unknown, never an empty-input claim. Both return single-send HTTP 409, sent: false, and caller-only existingInput` evidence (visible rows, suggestion
rows, cursor/dimensions and target binding). A collapsed paste shows its label,
not the hidden contents. OpenRig neither holds nor retries this stopped override.
Inspect the shown input in the native terminal, deliberately clear the prompt with
native/tmux controls, then issue the send again. Manual clearing runs outside
OpenRig's lease and observation checks and is not recorded. For a deliberate
unchecked write, add --type-without-checking to that same command. It skips the
read and does not clear input: the text may join or submit what is already there.
The result names that choice. It requires --dangerously-interact and --reason.
Observed input is returned only to the caller, never saved in an outbox, held message, capture-observer record or override audit. The audit keeps classifications, counts and the choice, without input contents. The lease serializes OpenRig writes; human typing can still race the capture and paste. Ordinary sends and wakes keep their existing behaviour.
Terminal delivery
Run from a seat shell with OPENRIG_SESSION_NAME set; the daemon derives the
audit actor from transport identity.
rig seat set-typing-guard dev-impl@my-rig --mode draft-aware \
--reason "preserve my unfinished input"
rig seat status dev-impl@my-rig
rig seat held-messages dev-impl@my-rig --json
rig seat held-messages dev-impl@my-rig --id <outbox-id> --json
rig seat retire-held-message dev-impl@my-rig <outbox-id> --reason "read outside the pane"
Use exactly one of --mode off|draft-aware|openrig-urgent|urgent-only|sends-only|hold or the compatible
--enabled true|false: true selects hold, false selects off. There are no
retry settings or deadlines. Activation waits for an already-started seat operation
to finish. The API may return HTTP 202 with pending: true; wait for
pending: false before relying on the requested mode. Status reports desired
and effective modes and retains the text Typing guard: on (hold) for hold.
A held send immediately returns outcome: "retained", sent: false, its exact
reason (such as draft_input_busy) and the saved attempt's outboxIds. No automatic
retry is scheduled. Each explicit draft-aware request is a new attempt, including
one with a reused deliveryId; the result warns older callers that this field no
longer deduplicates input. The CLI does not generate a deduplication key.
Clearing a draft, changing modes or restarting does not flush saved messages or callbacks. To try again, send a current request: it rechecks the recipient and input. A caller abort before paste cancels input; after a write may have started, the result preserves that uncertainty rather than claiming nothing was sent.
Bookkeeping is observational. If its write fails, an otherwise valid input attempt
still proceeds and the response includes a warning instead of a saved receipt.
Actual transport success stays success; an unknown write stays unknown. If input
was held and no record could be saved, the response reports failure with sent: false and the protection reason, rather than claiming durable custody.
Reading never sends. Retirement frees that record's active retention quota while preserving evidence; it does not deliver a message or close queue work.
Input evidence and outcomes
The cursor-aware reader lives in session-transport.ts and shares the composer
matchers with ordinary delivery. It recognizes framed Claude input and Codex input
with a known footer. The visible input containing the cursor is checked independently
of activity hooks, again after buffer preparation, and immediately before Enter.
Copy mode, an unreadable capture and an unsupported layout cannot authorize input.
The final check preserves authored whitespace: the cursor distinguishes trailing spaces from screen padding or omitted blank capture cells. A numbered message is recognized as this operation's own text only within its previously empty composer; a newly observed numbered menu is still refused. Human edits after paste stay visible and Enter is refused.
Faint placeholders and autocomplete suffixes are display hints only when the capture preserves their styling. Typing the same words and moving the cursor to the start still counts as a draft. Current footer and indentation anchor the Claude frame, so quoted prompt/frame examples inside a draft do not become proof of empty input.
A large paste can appear as several labels plus a literal tail. The existing
hasExpectedStagedText check validates the tail and the labels' combined source
boundary. That complete rendering must be observed after this operation's paste
and remain unchanged before Enter. Labels alone do not prove arbitrary content.
| Historical delivery state | Meaning |
|---|---|
sending |
This saved attempt has an unresolved write outcome |
held |
No input was written; this attempt remains retained or has been retired |
complete |
Transport execution succeeded, including render verification if requested; native consumption is not asserted |
indeterminate |
Input may have occurred or requested render verification failed |
failed |
A definite failure before input, such as caller cancellation |
Migration 100_typing_guard_modes extends the existing guard/audit rows and
adds receipt metadata to the original outbox rows; it creates no tables, retry
indexes or executable requests. Combined wakes retain each original ID and body.
A held queue wake stays on the existing queue recovery path. Interrupted historical
writes become indeterminate at startup. That history never suppresses a current
explicit request and is not consulted to determine its result.
At startup, the queue recovery pass retries still-current failed or unconfirmed wake notifications. It preserves their original envelopes and warns that delivery may repeat. Each notification is attempted once per pass, including coalesced wakes; a failed attempt cannot spin in that pass. A no-write failure on a later retry does not resolve an earlier unknown delivery. Prior-process claims are reconciled before new startup sends can begin. Known deliveries and held or retired messages stay untouched. Closed, claimed or reassigned work is not woken, and the retry never repeats the queue mutation or a lifecycle action.
The existing retention limits apply: 100 records and 8 MiB per seat, with a 1 MiB message limit. Hold mode cannot retain another message at capacity. Draft-aware input attempts continue with a bookkeeping warning if a record cannot be saved; already-committed queue intent remains durable.
Rollback and lifecycle
Persisted legacy guard bits are on for both hold and draft-aware, so an older daemon holds messages instead of typing into a protected seat. On re-upgrade, the most recent guard audit row and change timestamp determine whether the saved mode is still authoritative. A later boolean write by the older daemon takes precedence, including a same-value on-to-on write in the same clock tick. Without a later setting change, the draft-aware mode survives re-upgrade. Held and uncertain deliveries are never replayed by this reconciliation.
Current API desired and effective booleans continue to describe hold-all;
mode fields describe draft-aware protection. Storage's fail-safe legacy bits
are deliberately broader.
With hold, writing lifecycle operations refuse before effects. With draft-aware, an active seat requires an explicit switch to off before writing lifecycle work. Fresh startup is exempt only after creating a new pane or successfully respawning a dead pane within the same lifecycle operation. Adopting or rebinding an existing pane grants no such exemption.
Terminal capture is observational, not an atomic editor API. A direct tmux writer can still act between the last observation and paste. The resulting changed input refuses Enter and is not replayed. Unsupported provider layouts remain held. Use off when that tradeoff is unsuitable.
HTTP configuration uses POST /api/seat/set-typing-guard/:seatRef with
{mode, reason} or {enabled, reason}. Status and held-message routes are
unchanged; no RigSpec field is required.
Verification scope
Regression tests exercise rollback/re-upgrade, same-value legacy writes, exact retained outcomes, current requests after restart, coalesced queue bodies, trailing spaces, numbered bodies, real-menu refusals and multi-placeholder ownership. Native tests use private real tmux panes with scripted editable composers. These verify the transport mechanics; they are not live Claude/Codex provider sessions.
Watchdog reminders
A currently due watchdog reminder makes a new delivery attempt, including after an interrupted attempt or job accounting. Historical outbox receipts do not suppress a lone reminder or prevent it from joining a digest. The message tells its recipient that delivery may repeat after an unconfirmed attempt. Its job history records the current transport result; an unknown write is not proof that nothing arrived.
The transport still protects approval/question prompts, human drafts and recipient identity. Scheduler timing, current parked-row eligibility and stop checks still apply. This changes message retries, not the idempotence of structured continuity actions such as creating a cutover baton.
The parked-owner supervisor also retries current obligations despite an earlier failed, unknown or unrecorded attempt. Its first reminder to a seat waits until the seat's waiting state has been unchanged for 5 minutes, then goes on the next watchdog tick, so a seat that just handed work off or asked a person isn't interrupted; a seat still holding the work after that is reminded as before. A seat at an approval or question prompt gets no grace, so the prompt-blocked alert isn't delayed. A recorded success to the same known occupant waits for a change in its obligations. Rotation includes retained attempts, so a held input cannot starve another owner. Interactive prompts stay with the existing prompt-refusal recovery. Its own reminders wait while managed compaction owes the turn boundary, restore or audit, including a fresh check at the paste and Enter boundary. A stage that begins after paste leaves an honest partial-input result; OpenRig does not erase or replay that text. Peer messages and unrelated reminder policies keep their existing behavior.