Client interface architecture
Zuno's TUI, headless CLI, HTTP server, ACP adapter, and future GUI clients are views over the same durable agent runtime. No client owns a private turn loop.
Shared model
Every client uses shared runtime surfaces:
- Commands admit user intent through the same command registry and real handlers.
- Session events are the durable record of prompts, model output, tools, retries, questions, approvals, subagent reports, and lifecycle changes.
- Projections derive current conversation and status state from those events.
- The durable inbox accepts prompts, live steering, and
reportDelivery: nextStepreports before work is scheduled. - Durable human requests own ordinary and Goal questions and permissions across process restarts; process-local channels only notify already-running surfaces after commit.
The shared projection vocabulary includes:
ActivityProjectionfor one model step's commands, reads, searches, images, delegations, and other tool activity;WorkStateProjectionfor the active goal, todos, durable jobs, memory candidates, and resident entries;LearningStateProjectionfor message feedback, Experience records, mined patterns, and reviewed Skill candidates;SessionUsagefor cumulative provider-accounted tokens and context-window state;BackgroundExecutionProjectionfor process-owned terminal state and bounded output.
The server exposes cursor-based replay followed by live delivery. A reconnect sends the last committed cursor, receives every later event in order, then joins the bounded live stream. A client that misses live events must replay; it must not infer the missing state.
The only HTTP event operations are GET /api/event for live process-wide notifications and GET /api/session/{sessionID}/event for durable session replay plus live delivery. The session operation emits sessionID:sequence as the SSE id and accepts that value through Last-Event-ID on reconnect. Zuno does not mount an unscoped /event adapter or a second event envelope.
Subscribing to the session stream of a session the database has never seen is a 404 with the error code not_found, not a 200 with an empty stream. A stream that could never produce an event would leave a client waiting on a session that does not exist, with no way to tell that wait from a healthy idle one.
Routes and OpenAPI operations exist only when a real handler exists. Optional provider, credential, or client capabilities register their operations with their implementation rather than exposing a permanent placeholder that can only fail.
Learning uses the same rule. GET /api/session/{sessionID}/learning reads the durable LearningStateProjection; learning.state.changed announces a committed change. ACP carries the same value in _meta.zuno.learning during replay and live updates. Neither surface invokes extraction or pattern mining while reading the projection.
Process lifetime
One invocation of the zuno executable is one process on every supported platform. A client that spawns it — an editor over ACP, a service supervising the HTTP server, a script — holds the pid that runs the command: ending that process ends the command, and its stdout and stderr reach end of file when it exits. No platform hides a second process behind the pid the client holds, so no client has to discover a second pid or terminate a process tree to stop one invocation.
Startup resolves global options and ZUNO_* variables into one value the command reads directly, so completing that resolution never requires writing the process environment. Unix additionally replaces the image once, keeping the same pid, so launched processes inherit the resolved values. Windows has no equivalent operation, so there the values stay process-local and a program Zuno launches does not read them from its own environment. A platform without exec must not buy inheritance by spawning a second process: the client would then hold a waiter, killing it would leave the command running with the inherited pipe write ends, and the client's read would never reach end of file. See One invocation, one process.
Client capabilities
A client handshake should advertise only presentation and transport capabilities:
- supported event and projection versions;
- inline image and attachment rendering;
- terminal, diff, location, and generic tool renderers;
- interactive question, approval, and permission controls;
- maximum accepted snapshot and event batch sizes.
Capability negotiation must not alter agent semantics. An unsupported renderer falls back to a generic event view; it does not hide the durable event or change the tool call.
Input admission
Every submitted input receives an admission identifier before execution. A client may optimistically render a pending row keyed by that identifier, then replace it when the committed event arrives. Reconnecting with the same identifier must not create a duplicate input.
Input also names its session target. A root composer uses the mounted root target; an attached child composer sends the durable child session id and never tunnels the message through the parent transcript. The runtime validates that target, writes the message to that child's inbox, and supervises delivery independently of the render loop. A completed child acquires an idle run lease and reopens its TurnHost; a running child receives the same admitted input as a soft steer. The wake coordinator closes the active-to-idle race, so a message that misses the running turn remains pending and becomes the next child turn instead of being lost.
During an active turn, ordinary text and rich content target the nearest safe step as steering. Commands and explicit next-turn work remain queued. A steer interrupts a provider wait or provider-retry delay, checkpoints partial assistant output without ending the turn, and starts the next model step with the promoted input. It does not cancel a side-effecting tool already in flight. A steer that misses the final safe point stays durably pending and becomes the next FIFO turn; client channel capacity or reconnect timing never decides its fate.
An explicit interrupt applies to a live turn only. POST /api/session/{sessionID}/interrupt on an idle session is a 204 no-op; it never arms the next turn, because a marker with no expiry would cancel whichever ordinary turn starts next, including an explicit resume of deliberately queued input.
Input admitted while a compaction holds the session lease stays in the durable inbox and is driven as soon as the compaction releases the lease. A compaction is a lease holder like any turn, so the input it delays needs no unrelated external event to wake the session.
The TUI projects committed pending inputs into a fixed FIFO dock above the root composer. Queue admission is not rendered as transcript history. Each row states whether it targets the next turn or the active turn, and the footer resolves the actual force-submit and queue-manager bindings. Promotion moves the semantic input into history; cancellation removes it without inventing a user message.
Human input has priority over an automatic goal retry. The client may show the persisted retry deadline and reason, but cancellation, pause, and resume are explicit commands rather than local timer changes.
Questions and permissions are durable rows, not the lifetime of an RPC or tool future. QuestionPort separates publication, lookup/list, command application and waiting. QuestionService owns the transaction joining request CAS, command receipt, FIFO input, session wait and any matching Goal transition. The model sees receipt-only tool output; the actual response enters history through the inbox. Clients preserve stable item IDs and partial answers. Defer is pending, not an answer; draftAnswers are stored separately and never copied into model input. Reconnect re-presents pending rows in creation order without repeating a tool call. Ordinary sessions use the same wait contract as Goals.
plan_exit is a deferred approval publisher, not a mode-switch implementation. Its host-created binding freezes the Plan ID/revision, Work identity and review gate. The user submits an explicit typed decision. Early approval remains waiting_for_handoff; source-turn completion and Start Work admission commit together. Changed Plan/review/identity or interruption cannot authorize Work.
Scheduling metadata lives on session_execution_state: ready, waiting for an exact human request, waiting for an external source/cycle, paused, or completed. Admission is separate from eligibility to call the model. A user status query can be answered without clearing a pause. Callbacks and recovery preserve the gate and session-level no-progress fingerprint. FIFO promotion rechecks eligibility under its database transaction; gated automatic items do not block later human input. Explicit /resume creates a durable Work control, not synthetic user text or Plan consent.
Backpressure and disconnects
- Durable writes complete before a success response is returned.
- Bounded live channels may drop a subscriber, never a committed event.
- Slow clients resume from their last cursor.
- Large projections use a snapshot envelope plus later events.
- Background reports settle durably before they wake a parent.
- A client disconnect never cancels an active goal unless it issued an explicit interrupt.
ACP child-session projection
ACP always exposes delegation through the stable task tool-call projection. That card is sufficient for clients which know nothing about child sessions and is retained even when a richer projection is active.
The draft native-subagent extension is opt-in presentation, not a different execution path. After direct clientCapabilities.subagents negotiation, a foreground child's durable session becomes an additional ACP update route: spawn and terminal state are sent to its direct parent, while replay and live turn events are sent to the child id. The adapter drains the bounded child projection queue before returning the parent prompt response. If high-frequency updates must be omitted under backpressure, it emits a typed omission notice; spawn and terminal transitions are protected.
Historical child trees are reconstructed from durable parent edges and replayed parent-before-child. Their terminal state is disconnected, because durable history proves identity and content but not process liveness. Background children remain jobs: projecting them as foreground sessions would erase their independent cancellation, report-delivery, and uncertain-outcome semantics.
Human requests follow the same routing contract. A negotiated native child uses its own session id. A compatibility client receives the ask on the known root session with _meta.zuno.childSessionId, so it never has to accept an unknown route. Reusable permission grants are still owned by the root ACP session and are cleared when it closes.
An autonomous Goal may yield WaitingForHuman through goal_request_input. Plan may ask synchronous clarifications, and ordinary Work may use question for required input. Root sessions can use question_async for optional input without delaying their current summary. A delegated child reports blockers to its parent rather than contacting the user directly.
Design sources and deliberate differences
The implementation was based on Zuno v0.10.29, with Codex 9ba1d9eb5bbbd87ba2fc528d91ad239eea975ee9 and oh-my-openagent 44c95e976dfd13b911de7988872fc2302f2b1092 as design references. Request/user-input lifetimes, optional follow-up and explicit collaboration boundaries inform the question interface. Responsibility-oriented prompts, bounded specialist delegation and evidence-driven execution inform the native Agent catalog. These are design references, not wire/configuration compatibility targets; no upstream prompt body is copied.
Zuno keeps all fifteen native roles. The parent passes its current effective rules, resource contract and complete authorized tool registry separately from the provider-visible schema subset. Explicit child-role restrictions narrow this authority; a new child invocation does not reuse an obsolete child permission snapshot or gain authority from newly discovered global configuration.
The follow-up runtime-consistency work starts from Zuno v0.10.30 and pins Codex eaa8b6d91701d6cabe464141facc677e5915fbfc. Its per-issue source mapping and intentional differences are in the six-part audit.
Database format 15 retains the format-14 InputAdmissionReceipt, GoalResumeRequest and ContextUsageSnapshot contracts, adding work-cycle scope and keyed Goal-turn observations/settlement. Supported formats 5–14 migrate through one guarded transaction, retain original session/message/memory values, and advance the marker last. Legacy unbound completions remain evidence; a migration never invents a work cycle, model application, verified Memory source or Goal resume.
Input admission, history recording, model application and terminal processing are distinct native facts. Standard ACP prompt waits for its associated outcome; steer returns immediate admission. All clients consume the same Context snapshot, not independent guesses. A paused Goal is offered a skippable host-owned resume choice; its exact Goal revision and original input are validated atomically.
TUI
The TUI favors dense, keyboard-first operation:
- stable transcript and status-strip dimensions;
- a composer that uses the available left pane with only a one-column gutter;
- multiline question input with bounded growth that replaces the composer area while the tool waits;
Esccancels it as a refusal rather than returning a synthetic answer; - visible permission, retry, diagnostics, and background-job states. TUI presentation settings live in
tui.json; authorization remains a runtime concern in the mainzuno.jsonpermissionblock.mode: allow_allskips every Zuno tool-approval ask, while explicit denies and catastrophic Shell denials remain authoritative. Root and child asks share one foreground queue, but every request retains its trusted session, assistant-message, and tool-call origin. Closing the attached TUI refuses outstanding asks, and anAlwaysgrant is scoped to the originating session; - explicit
working,awaiting approval, andawaiting answerstates. During a running turn the firstEscarms interruption and shows the confirmation immediately above the composer; the second within the confirmation window cancels it. If a modal or autocomplete surface owns the first press, that same press closes the surface and arms the turn, so two physical presses remain sufficient.Stopping the active turn…remains visible until the terminal turn event clears it;TurnInterruptedthen appends one session-owned interruption marker, and replay reconstructs that marker from a persisted assistant abort rather than nesting it inside partial assistant output; - liveness animation advances from a bounded UI clock, not from provider event arrival. A slow provider, shell command, or MCP call therefore keeps moving while producing no output; the clock stops while waiting for human input and at the completed or interrupted turn boundary;
- generic rendering for unknown future events;
- a
systemtheme that reads non-invasive terminal color hints when available and otherwise preserves the terminal's foreground and background defaults. Its unordered-list marker uses muted text without bold emphasis, while ordered enumerations retain their own emphasized token. Ordinary content, reasoning, service names, and sidebar metadata use neutral foreground hierarchy; semantic green is reserved for compact success or health glyphs rather than complete sentences or rows; - a full-height ambient sidebar outside the transcript, prompt, status, and info column, so no left-hand band renders underneath it;
- a visible transcript scrollbar with wheel and thumb dragging, plus application-owned text selection that is clipped to the transcript. Releasing a drag copies automatically, right-click copies the retained selection, and success or failure is reported without clearing the highlight. Transcript clipboard text comes from semantic message content: role chrome, borders, padding, and visual soft wraps are excluded, while explicit source newlines are preserved. Root and attached-child composers use the same captured drag selection and clipboard path, and render an inverse theme-derived caret on both empty and populated input buffers; pasting one supported local image path, or image bytes supplied by the clipboard backend, inserts a visible
[Image #N]draft handle while retaining filename, MIME, and bytes as separate typed content.@project/pathresolves bounded text or images below the project root, and the same rich submission shape survives queue, steer, child continuation, durable replay, and provider encoding. See images and file references;mouse: falseopts back into terminal-native selection and alternate-scroll translation; - ordinary modal overlays are centred in both axes; composer-owned questions remain anchored to the composer region. Leader continuation help is a compact, centred, titled, bordered overlay: it preserves readable cell widths and reports
+N moreinstead of filling the frame with clipped descriptions. Numeric quick-session bindings remain active but are omitted from this help surface so nine repeatedSwitch to session in quick slotrows do not obscure higher-value commands. The default leader timeout is five seconds, so the continuation overlay does not vanish before it can be read. An open modal captures pointer input so clicks cannot activate covered transcript or sidebar content. Picker rows and confirmation buttons accept left-click selection as well as keyboard actions. Slash autocomplete, structured questions, permissions, sessions, models, agents, themes, MCP servers, subagents, background jobs, and memory entries are mouse-selectable; list and reference panels accept the wheel as well as keyboard navigation; - clicking a user prompt opens its message actions.
Copy messagewrites the complete prompt through the shared clipboard path and reports the result.Revert this turnis offered only when the live newest prompt has a restorable boundary; choosing it opens the same explicit Restore/Keep confirmation as/undo, and only Restore admits the typed undo command; - step-level activity summaries for completed routine commands, reads, searches, images, and delegations. Running work, approvals, failures, and important results remain visible. A folded group retains one bounded identifier line per call: shell command text, read path and requested window, search query, or the tool-specific summary. Full arguments and results remain behind disclosure rather than flooding the main answer.
Ctrl+Topens the complete scrollable durable transcript and preserves manual scroll position;Alt+Tchanges reasoning effort. Each thinking block remains folded by default, uses muted styling in both states, and owns an independent disclosure target. Its header is explicitly labelled◇ Thought; tool rows are explicitly labelledTool, so the two secondary content types do not share an ambiguous visual shape. Clicking a thought header opens the complete persisted body without changing sibling blocks./thinkingremains the keyboard-wide fallback. If replay contains both the visible reasoning event and an identical provider-reasoning capsule, the client projects one block while retaining both durable records; - user and assistant prose use the same CommonMark renderer, including GFM tables, lists, headings, code, quotes, and links. Rendering never changes the durable source text. The transcript adds hierarchy without adding saturated panels: a speaker title is followed by a weak neutral rule, Markdown headings use the neutral title role, and list/quote/rule chrome uses the muted role while prose remains primary. Structure therefore remains visible without relying on purple, green, or colour alone;
- per-call tool disclosure in the complete transcript, with subagent rendering selected by persisted
ToolUiIntent::Subagentrather than hard-coded tool names. The collapsed row stays summary-only; expanding a call renders a pretty-printedArgumentssection and a distinctResultsection, with explicit bounded-overflow notices for pathological inputs. A call refused before execution is projected asblockedwith warning styling and a durable block kind; only a call that actually ran and failed is projected as an error. Tool headers are composed from separate theme roles: disclosure and separators are muted,Tooland the tool identity are neutral titles, the argument summary is secondary, and warning/error/running emphasis is confined to the status glyph. Overflow hints are muted rather than rendered as primary actions. These span-level distinctions retain the existing per-call disclosure target and remain legible in monochrome terminals; - one subagent overview for native child sessions and configured Codex or Claude Code product agents. It shows product/target, objective, status, elapsed time, session/run, job, report delivery, result, and safety diagnostics without exposing product-internal reasoning or tool streams. Enter toggles details; each compact row separates focus, status glyph, product title, target, and muted objective, with the selection background applied across the complete row. Expanded details use a weak
Detailsdivider, muted field labels, readable values, and per-child status glyphs instead of tinting whole rows; pressingxtwice requests cancellation of a running job and keeps the list mounted for consecutive cancellations. Native child hosts additionally publish a full main-pane session projection:Ctrl+X Downenters the first direct child,Ctrl+X Upreturns to its parent, andCtrl+X Left/Rightremain available for sibling navigation. While the attached child's composer is empty, plain Left/Right cycle siblings directly; once a draft exists, those same keys return to ordinary cursor movement rather than stealing text editing. Mouse-wheel events scroll the attached child's own transcript instead of being dropped at the child composer. The fixed two-row child footer shows the resolved Agent/model, current context occupancy, direct-child position such as3/8, sibling keys, the parent key, and whether Enter will steer or continue. The parent host remains mounted and running while the child transcript receives live events, so child progress is visible before the foregroundtaskcall completes. Every attached child owns its ownInputEditordraft. Enter steers a running child and continues a completed child; the text is admitted to the child inbox before execution. Durable child rows and retained history are projected again when a TUI resumes the parent after a process restart. The child continuation identity stores its resolved Agent, model, effort, parent Attempt, and workflow lineage in session metadata, so a restored child is not view-only. Child input reopens the same fullTurnHostused by a root session, including tools, permissions, cancellation, lifecycle reporting, usage, and automatic context compaction. Child input is literal text, so/helpand other slash-looking strings are not dispatched as root or host commands. Switching siblings or returning to the parent preserves each child draft. A child may delegate only when its selected Agent exposestask, names the target indelegates, passes permission, and remains belowsubagent_depth; using the same host does not bypass those limits. Historical navigation follows durable direct-child session edges rather than inferring a child from ordinary tool activity. When a retained session has no delegated child,Ctrl+X Downleaves the transcript in place and directs the user to the transcript details instead of implying that history restoration failed; - a skill census that separates discovery from use: the heading reports
loaded/discovered, and only a successfully completedskilltool call marks a row✓ skill-name · loaded. Expanded skills are grouped with loaded skills first and not-loaded skills second, with each group sorted by name and source. Same-named skills display their source locator, and completion marks only the exact source selected by the tool call; - an independently scrollable and selectable sidebar whose current-session title is a fixed header and participates in the same application-owned drag-selection and clipboard path as the body; its location/version footer stays fixed. Only the projection body scrolls. Foreground transcript-backed delegations appear immediately under
Agents; once a call acquires a matching durable job it yields to the richerJobsprojection rather than appearing twice. The sidebar also projects goal, todos, pending memory, user learning, token usage, LSP, MCP, and skills from shared state rather than polling; /psfor process-owned background terminals,/memoryfor resident Memory,/memoriesfor the current session's use/generation policy, and/learnfor Experience, patterns, feedback, and reviewed Skill candidates./reflectmanually admits the same no-tools extraction job used by the post-turn path;- the welcome screen owns only a prepared process identity. It creates no durable session until the first model-bound submission commits the session and user message together.
session.materializedupdates the in-place session catalog, so/sessionsees the new row without remounting; - usage comes from the durable
SessionUsageprojection on resume. A history whose provider accounting cannot be recovered displays an unavailable marker rather than a fabricated zero. Cumulative input/output/cache buckets remain available for accounting and history, but the live sidebar shows only the latest whole prompt, model-window limit, and decimal occupancy percentage; - no empty LSP status or setup prompt; the sidebar adds LSP only for configured services or real diagnostics;
/sessionlists active root sessions from the current durable database and exact working directory. Selecting another session is admitted only between turns and remounts the complete session composition, including transcript replay, permissions, cancellation ownership, LSP/MCP workers, and snapshot history. The physical terminal activation remains mounted throughout, so a switch never leaves and re-enters the alternate screen;/newperforms the same in-process remount to a fresh prepared identity. The command opens a blank conversation shell directly rather than returning to the launch welcome page. It creates no row; the first model-bound prompt materializes exactly one new session through the normal durable-input transaction;/compactinvokes the runtime compaction agent and persists the resulting summary;/goalinvokes the shared durable goal handler. Both are typed native session commands rather than model prompts.compaction.threshold_percentcontrols proactive compaction against the usable model window, whilecompaction.auto: falsedisables that proactive trigger without removing manual compaction or bounded context-limit recovery;/plan,/start-plan, and/start-workare native collaboration-mode controls. Plan entry is idempotent and Agent selection is independent from mode. ACP publishes standard mode/config updates, while the TUI keeps its explicit confirmation surface. Neither path may return from Plan to Work without an exact handoff-ready Plan revision. Entering Plan atomically recordspaused(plan_mode)for an active Goal; Start Work records durable Work authority and aUserControlcontinuation rather than fabricating a user message. A bound Draft review requires explicit persisted risk acceptance;- the same session list owns row actions:
Ctrl+Ropens a pre-filled rename prompt, whileCtrl+Dmust be pressed twice on the same row before deletion. Both actions are revalidated by the host and use the transactional session store; deleting the current session remounts the most recent remaining session in that directory, or creates a new one when none remains. A successful delete reopens the refreshed session list on that replacement so users can delete several sessions without invoking/sessionagain; a refused delete leaves the existing list mounted. A current session with background subagents still running is refused rather than deleting state those tasks can still write. When derived Experience exists, the TUI asks whether to keep it or clean it. Cleaning first creates pending-review Memory and Skill revocations, rejects pending candidates that cite the evidence, and only then marks the Experience forgotten. ACP requires the same choice as the explicitcleanupDerivedExperiencesfield; - warning and error notices wrap inside the viewport and remain visible long enough to inspect or select. Ephemeral command guidance, such as an unknown slash command, is a short-lived toast and does not become durable transcript content;
- no blocking network, LSP, or provider work in the render loop.
Future GUI
A GUI should reuse the HTTP event replay, projections, command registry, question and approval protocols, and attachment references. GUI-only packages may own layout, theming, caching, and richer tool renderers. They must not import provider implementations or mutate session tables directly.
The first GUI milestone therefore needs no engine fork:
- versioned handshake and session snapshot;
- cursor replay plus SSE or WebSocket live events;
- idempotent input admission;
- command, question, approval, interrupt, goal pause, and goal resume endpoints;
- generic tool rendering with optional specialized renderers.
The HTTP question and permission list/reply operations publish concrete OpenAPI schemas. Their responses represent the same durable request rows used by TUI and ACP, including recovered requests; they are not merely a snapshot of live in-process waiters.
That contract does not extend to standing authorizations, which are process-local rather than durable request rows. Zuno therefore publishes no /api/permission/saved list and no /api/permission/saved/{id} revoke. Both were fixed responses — an always-empty list and an always-successful delete — while a live grant kept authorizing matching calls, so a client auditing its grants was told there were none and a client revoking one was told it succeeded. A missing operation is honest; a published one that can only misreport is not.
A standing authorization installed over HTTP is scoped to the session that granted it, exactly as the ACP one is. An always reply is one session's decision, so it pre-approves later calls only in that session, and only when the ask offered patterns to save: a manual confirmation offers none, is never satisfied by an existing authorization, and never installs one. The authorization appears only once the reply has reached the call that was waiting for it, and it is withdrawn when the session ends — archiving or deleting a session through POST /api/session/prune drops every authorization that session granted. An event-stream disconnect withdraws nothing, because a stream is not the session: a client that reconnects keeps what its user already approved.