IPC and Event Governance
Wardian uses a Bidirectional Event System over Tauri's IPC bridge. This document defines the "Contract" between the Rust backend (Source of Truth) and the React frontend (Observer).
📡 Commands (Frontend to Backend)
The UI invokes these functions via invoke("command_name", { args }).
Agent Lifecycle
list_agents: Returns a list of all active/hibernating agent configurations.spawn_agent: Spawns a new PTY session from anAgentConfig.kill_agent: Internal compatibility alias that force-deletes the owning agent after the UI has confirmed the destructive action.pause_agent: Suspends the PTY (killing the process but keeping the config).resume_agent: Restarts a paused PTY session.send_input_to_agent: Routes raw keystrokes to an agent'sstdin.send_binary_input_to_agent: Routes raw byte sequences from xterm to an agent'sstdinwithout UTF-8 re-encoding.
Automation Governance
automation_run: Initiates an automation blueprint run.automation_resume: Resumes an interrupted durable run.automation_approve: Grants or rejects a paused approval node.automation_cancel: Requests cancellation for a live run.schedule_*: Manages persisted schedule invokers for automation blueprints.listener_*: Manages persisted listener invokers - file watch, inbound webhook, and web poll.
Old automation system commands such as run_automation, stop_all_triggers, pause_all_triggers, and resume_all_triggers are compatibility history only.
Inbox and Readiness
load_queue_items/save_queue_items: Load or persist the legacy completion projection for the active Wardian home. Inbox item identity should be derived from canonical evidence IDs rather than frontend timestamps.dismiss_agent_completions: Writes durable dismissal tombstones for backend-owned provider completion items.load_queue_preferences/save_queue_preferences: Load or persist per-event-type Inbox visibility, desktop alert, and sound alert preferences.list_inbox_notifications/resolve_inbox_notification: Read and resolve durable agent-created updates and manual approval requests from the interaction store.list_automation_inbox_approvals: Project native automation Approval nodes for Inbox while leaving automation state authoritative.list_provider_readiness: Returns install/auth readiness for provider commands before spawn. This is separate from live provider input readiness, which is tracked per agent runtime generation.
Files Resources
The Files IPC contract uses snake-case DTOs nested under request:
open_file_resourcereturns a canonical descriptor, subscription, and monotonic revision after backend authorization.read_file_resource_textrequires that resource, subscription, and revision.issue_file_resource_ticketrequires those fields plus a non-emptyrenderer_lease_idand returns a short-lived, WebView-bound image/PDF URL.close_file_renderer_leaserevokes the matching renderer lease and its tickets without closing the subscription.close_file_resourcereleases one subscription.
For complete request and response JSON, see Tauri Command Reference. Do not persist picker capability IDs, subscriptions, revisions, tickets, or leases in Workbench state. Restoration sends its saved path with both authorization IDs set to null; the backend resolves current authority again from agent roots or its bounded exact-path grant registry and the Workbench converges the tab to the returned canonical resource_id.
🔔 Events (Backend to Frontend)
The UI listens for these events using listen("event-name", (event) => { ... }).
agent-metrics
Pushed every 5 seconds. Provides aggregate resource usage.
[
{
"session_id": "uuid-1",
"cpu_usage": 12.5,
"memory_mb": 450,
"query_count": 5
}
]agent-json-event
Pushed whenever an agent's PTY output contains valid JSON (e.g., from Gemini CLI).
type: "progress": Used for the "Thought Stream" bubbles.type: "alert": Triggers floating UI notifications.type: "info" | "model": Signals the agent has finished a turn (Idle state).
agent-pty-output-ready
Pushed when the PTY reader appends new plain terminal output into an agent's buffered output_buffer.
{
"session_id": "uuid-1"
}The event does not carry terminal text directly. The UI should treat it as a readiness signal and immediately drain the buffer via read_agent_pty until that command returns null.
agent-status-updated
Pushed when the backend accepts a provider runtime status transition for an agent.
{
"session_id": "uuid-1",
"current_status": "action_required"
}Provider runtime status is authoritative for provider-internal states such as idle, processing, action_required, off, and error. Wardian interaction status must not replace provider status. For example, a Codex permission prompt is provider runtime evidence; a queued Wardian ask waiting for delivery remains an interaction delivery state.
file-resource://revision
Subscribe before invoking open_file_resource so the watcher cannot publish a revision between the open and listener setup. The event has no frontend request payload. It is emitted once after 150 ms of stable changed content and carries the complete next descriptor:
{
"schema": 1,
"resource_id": "file:<canonical-path>",
"revision": 2,
"descriptor": {
"schema": 1,
"canonical_path": "<absolute-workspace-path>/README.md",
"display_name": "README.md",
"extension": "md",
"mime_type": "text/markdown",
"encoding": "utf-8",
"renderer_kind": "markdown",
"size_bytes": 2112,
"line_count": 44,
"content_hash": "sha256:<content-hash>",
"modified_at_ms": 1784242800150,
"capabilities": {
"preview": true,
"changes": true,
"draft": true,
"stream": false
},
"unavailable_reason": null
}
}An unchanged content hash emits no event. Consumers ignore unrelated resource IDs and revisions not newer than their snapshot, then reopen through open_file_resource; they do not read a new revision through an old subscription snapshot. A stale read_file_resource_text or stream ticket request fails with stale_revision.
Listener readiness alone does not close the response-publication race. While the initial open is pending, the controller coalesces the highest event per resource. When that event is newer than the returned snapshot, the controller closes the just-opened subscription and opens the resource again instead of grafting event fields onto a snapshot owned by another subscription. The bounded reopen publishes only an authoritative backend snapshot. Its event signal is consumed before a retry, so an old-incarnation revision cannot keep a new revision-one resource in a reopen loop. If every bounded candidate is already behind an observed revision, the controller closes the final candidate and reports a retryable resource-local error instead of publishing known-stale content. A refresh keeps its previous live subscription until a later manual retry publishes an authoritative replacement.
The backend owns one watcher per canonical file, shared by every subscription. Closing a renderer lease revokes only its stream tickets. Closing the last file subscription removes the watcher and any remaining ticket/lease state for that subscription.
Interaction and Delivery Watch Events
The CLI live-control agent_watch, ask, send_message, submit_reply, notify_create, and notify_wait paths expose ordered control evidence. These records are not raw terminal output.
{
"cursor": "uuid-1:0000000000000042",
"kind": "delivery",
"payload": {
"uuid": "uuid-1",
"provider": "codex",
"runtime_state": "live_pty_available",
"delivery_state": "provider_accepted",
"input_mode": "message",
"delivery_phase": "turn_started"
}
}Delivery events describe the transport attempt. Interaction records describe the Wardian-owned communication lifecycle:
{
"id": "ask_0123456789abcdef",
"kind": "task",
"sender_session_id": "source-uuid",
"target_session_ids": ["target-uuid"],
"status": "awaiting_reply",
"trigger_policy": "reply_required",
"body_ref": {
"storage": "file",
"path": "<wardian-home>/agents/target-uuid/mailbox/ask_0123456789abcdef.md"
},
"created_at": "2026-05-25T16:00:00.000Z",
"updated_at": "2026-05-25T16:00:00.000Z"
}Valid interaction kinds are message, task, reply, and notification. Valid interaction statuses are created, queued, delivering, delivered, awaiting_reply, completed, failed, and expired.
Structured replies attach to the parent task interaction and carry the reply status separately:
{
"request_id": "ask_0123456789abcdef",
"status": "done",
"body": "Reviewed the patch. No blocking findings.",
"target_session_id": "target-uuid",
"source_session_id": "target-uuid",
"replied_at": "2026-05-25T16:03:00.000Z"
}Chat transcript provenance
AgentChatEvent.role is the normalized presentation and turn-segmentation role, not a copy of a provider's native message role. Provider adapters must classify provenance before emitting chat events:
- genuine operator or agent prompts are
kind: "message",role: "user", withmetadata.input_originset tohuman_inputoragent_input; - provider-supplied context and provider-internal messages are
kind: "message",role: "system", with an explicitinput_originsuch ascontext_injectionorprovider_internal; - tool results are
kind: "tool_result",role: "tool", and retain their provider tool identity inturn_idor metadata.
Some providers, including Claude, encode all three cases in native user records. Consumers must use the normalized role and provenance metadata rather than the raw provider role or text to identify prompts and turn boundaries. Desktop and remote chat both use the shared backend transcript loader and archive replay path, so provider provenance must be corrected there once. The replay boundary also canonicalizes legacy archived roles from their persisted input_origin metadata; historical archive files do not need a destructive rewrite to display the corrected classification.
Archive replay retains canonical IDs and can enrich provenance from a current, source-bound native observation. Capture-enabled repair persists verified aliases and metadata in the existing archive format; capture-disabled repair changes only the active view. A reconciled broker input retains its broker provenance while exposing the observed native source (provider_log, source, and log path). Equal prompt text alone does not establish identity. See the internal design record at docs/specs/2026-09-07-provider-archive-provenance-repair.md for the publication and compatibility contract. For the verified Antigravity SQLite GENERIC tool result, an identity-bound running-to-DONE observation also refreshes text and native step status in place. The original event ID and narrative sequence remain; DONE does not assert success and a stale running capture cannot replace completed output. Durable repair still requires enabled capture; disabled replay changes only the view.
Provider Input State
Live delivery readiness is tracked per provider runtime generation. The generation increments when a provider process is spawned, resumed, cleared, or reattached. Delivery may drain queued work only after readiness evidence for the current generation.
{
"session_id": "uuid-1",
"generation": 7,
"state": "ready",
"ready_evidence": "prompt_detected",
"observed_at": "2026-05-25T16:00:00.000Z"
}Valid states are unknown, booting, ready, busy, action_required, and unavailable. Valid readiness evidence values are provider_event, prompt_detected, title_detected, and manual_status. Stale readiness from an older generation must be ignored.
Inbox Evidence
Inbox items project canonical live evidence instead of replayed terminal text. The backend owns every agent completion card: it writes the card to the queue before any client sees it, so a card does not depend on a mounted desktop window. agent-turn-completed has two forms:
- Without
inbox_item, it marks a provider turn boundary. The frontend refreshes turn-scoped views such as change review and never builds a card from it. - With
inbox_item, it projects a completion card the backend has already persisted. The item carries its stable ID, backend timestamp,timestamp_source, boundedsummary, and fullresponse_text. The frontend applies it as-is and does not reread the transcript or invent queue identity.
A card is keyed by provider evidence for its turn, agent-completed:<agent-session>:<evidence_id>:
| Provider | Completion source | evidence_id |
|---|---|---|
| Claude | None for interactive turns: Claude Code exposes no supported turn-end verdict | Claude prompt_id |
| Codex | Owned app-server turn/completed with its final_answer, and the rollout task_complete record (last_agent_message) | Codex turn ID, shared by both sources |
| Other providers | The final assistant message of the transcript at the turn boundary | answer: plus a digest of the request and its answer |
Two observers of the same turn converge on one card, and a dismissed card stays dismissed. A turn without non-empty final answer text produces no card: interrupted and failed turns, Claude StopFailure, provider control commands such as /login, and turns whose transcript does not end in an assistant answer to a user request. Rollout records written before the current runtime launched are history and are ignored. A failed queue write is retried while the same agent runtime remains current. Antigravity's transcript marks intermediate planner and script steps as DONE, so Wardian reports its turn boundary only when that provider returns to its visible ready prompt, once per active turn.
Interactive Claude turns create no completion card. Claude Code's Stop hook and its hook_execution_complete telemetry both precede the turn-end decision, and no supported event reports that decision, so the outcome is unknown (Claude Inbox Turn Completions). Committed prompts are recorded per launch, so startup replay is idempotent. Dismissal marks a durable tombstone; whole-queue snapshots cannot delete or revive backend-owned completion records.
Other queue records, such as action-needed cards, have this shape:
{
"id": "queue_0123456789abcdef",
"type": "action_needed",
"timestamp": 1779724800000,
"read": false,
"evidence_id": "provider-event-uuid-1-7-permission-1",
"evidence_source": "provider_runtime",
"agent_session_id": "uuid-1",
"agent_name": "reviewer-a1",
"summary": "Codex is asking for command approval."
}evidence_source is one of provider_runtime, interaction_store, or live_runtime. Hydration may restore existing completion items, but it must not emit new evidence. Replay of provider logs or terminal buffers must not create duplicate Inbox cards. Agent-originated updates and approvals use durable notification interactions rather than this frontend projection.
automation-telemetry
Pushed in real-time as the Automation Engine executes nodes.
{
"automation_id": "uuid-a",
"node_id": "node-1",
"status": "processing | success | error",
"output": { "text": "..." },
"error": "..."
}listeners-updated
Emitted whenever a listener's configuration or runtime state changes: a fire, a run completing, an arming result, or a refused delivery. Signals the UI to refresh via listener_list.
Payload is empty; the UI re-reads rather than trusting a pushed delta.
Note that the supervisor does not re-arm on this event. Arming is gated on a fingerprint of the listener configuration only, so the runtime write-back that follows every fire is invisible to it. Without that split, a listener would tear down and rebuild its own watcher each time it fired.
agents-updated
Emitted whenever the global agent roster changes (spawned, renamed, killed). Signals the UI to refresh its list via list_agents. Also emitted once when startup restoration finishes and agent_roster_restored becomes true, so every window takes one refresh that the backend vouches for as complete.
AgentConfig Provider Config
Provider-specific launch settings are nested under provider_config instead of being authored as new top-level AgentConfig fields. The wire shape is an internally tagged object whose type matches the selected top-level provider:
{
"provider": "codex",
"provider_config": {
"type": "codex",
"sandbox_mode": "workspace-write",
"approval_policy": "never"
}
}Shared launch settings such as model, include_directories, system_include_directories, custom_args, debug, and session_persistence remain top-level. Legacy flat provider fields may still be returned while reading older persisted state, but new spawn, clone, and explicit config update requests should send the nested provider_config shape.
🛠️ Global Governance Contract
- Stop All (Safety-First): Invokes
stop_all_triggers. This affects background automation only. - Pause/Resume All: Globally freezes or thaws the evaluation of cron and file-system triggers in the Heartbeat thread without aborting the background tasks themselves.