TRIAGE · trg · Rust + Ratatui

Triage

trg is a terminal client for Triage, a routing service that turns plain requests (“file a receipt review task”, “what's the status of that build?”) into actions on other systems. The service decides and acts; trg records exactly what you asked, what was accepted and what happened, including when a reply is lost on the network.

trg                                       # the rich terminal; dry-run by default
trg send 'File a receipt review task' --json
trg send 'An explicitly authorised request' --live
trg turn approve PREVIEW_TURN_ID        # a new live action from a frozen preview
trg turn cancel SERVER_TURN_ID          # a fence, with proof, not a rollback
trg resume SESSION_ID

The journal

The dangerous moment in any client is the one after it sends a request and before it hears back. trg closes it from both sides: before posting anything, it records a caller UUID, the session and the exact request in a private local journal, and the router uses that UUID to recognise a repeat. A reply that never arrives leaves the turn unknown and held, never quietly retried and never resubmitted with a fresh ID.

Write it down before you send it What trg does around one request. Before posting, it writes a caller UUID, the target session and the exact request to an owner-private SQLite journal. Then it posts the turn to the router. If the router answers, the admission (accepted or rejected) is recorded against that UUID. If the reply is lost, malformed or ambiguous, the turn is kept as unknown and nothing is posted again automatically. Later, resume reconciles saved identities by reading the router, never by reposting, and turn retry with the same caller UUID rereads the saved request instead of inventing a new one. Exiting stops local waiting, not accepted remote work. “file a receipt review task”1 caller UUID · session · request2 POST turn (caller UUID)3 admission: accepted · server turn ID4 record admission5 or the reply is lost state: unknown · held6 trg turn retry CALLER_UUID7 reread the saved request8 same caller UUID, same body9 the existing turn (server is idempotent)10 youyou trgclienttrgclient journalSQLite, owner-onlyjournalSQLite, owner-only routerowns routing + executionrouterowns routing + execution
Recovery reuses the identity. turn retry rereads the saved request; resume reconciles by reading, never by posting again.

The journal is bound to the router's complete base URL. Pointing the same state directory at a different server fails before a credential is read or a request is sent, so requests can never drift into another endpoint's history.

Outcomes and exit codes

A receipt says what happened, and a finished turn can still mean the action was blocked, rejected or left indeterminate. trg keeps those apart, and its exit codes do too, so a script can tell "the command worked" from "the action worked".

What a turn can end as The outcomes trg distinguishes for a turn. A sent turn is admitted (accepted) or rejected, or its acceptance is unknown. An accepted turn is pending until done, and done is not success: the action may have been blocked, rejected or left indeterminate, and blocked or indeterminate turns stay held rather than being filed as history. A dry-run preview can be approved, which is a new live turn pinned to the preview's current proposal revision. A cancellation succeeds only with the router's proof that the action never started. sentjournalled first acceptedpending rejectedexit 1 unknownheld · exit 3 done≠ success dry-run previewproposal revision r cancelledproof: nothing started succeeded blocked · indeterminatestays held approvea NEW live turn, pinned to r dry run cancel
Nothing ambiguous is tidied away. Blocked and indeterminate turns stay held even when the server presents them as done.
Exit codes
0the command, admission or read succeeded (not necessarily the action)
1an operational failure, or a rejected or unsent admission
2invalid input or configuration
3acceptance unknown, or a watch deadline reached

Dry run is the default. Routing and its mode freeze when a turn is submitted, so changing your mind afterwards changes new work, not old. --live, or a persistent live mode, routes for real; the server still owns authorisation for every action.

Approval is a new action. /approve turns a dry-run preview into one live turn pinned to the preview's current proposal revision, without resending your text. Cancellation is a fence. It succeeds only when the router proves no action command started; it never promises a rollback.

The terminal

The composer is always available: type the next request while earlier turns are still pending, each tracked on its own. An inspector shows a selected turn's real events and receipt, and the conversation can carry on: the router can ask which project you meant, hold the task while you answer, and refine an unconsumed preview with “make it P1”, which comes back as a separate dry proposal.

--plain gives a line REPL for screen readers, and every operation is also a subcommand with versioned JSON for scripts.

With native server-sent events, the terminal keeps one observer for the active session rather than polling every turn, and refreshes receipts only when events say something changed. Events never submit, approve or cancel anything. Watch commands stream JSONL with consumed cursors, so a script resumes from the last complete line.

Replies from agents that a request reached can be observed with trg turn replies; they are shown as the agent's assertion, never as proof of execution and never routed as a new command.