Files
tutti/docs/architecture/agent-gui-node.md
T
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

226 KiB
Raw Blame History

Agent GUI Node Architecture

Status: current implemented architecture

This document defines the durable architecture of the Agent GUI system: ownership, dependency direction, core entities, critical flows, and change-routing rules. It is not an implementation plan, feature inventory, or troubleshooting log.

Scope:

  • packages/agent/host: provider-neutral Agent application core
  • packages/agent/store-sqlite and store-sqlite/canonical: canonical contracts and transactional local storage
  • packages/agent/daemon: provider runtimes, adapters, and registry
  • services/tuttid: HTTP, queries, product policy, and Host adapters
  • packages/agent/activity-core: frontend workspace engine
  • packages/agent/gui: Agent GUI, Message Center, and conversation presentation
  • apps/desktop: Electron, Workbench, transport, and concrete host capabilities
  • apps/mobile: React Native presentation, DeviceLink transport adapter, and mobile lifecycle integration

Implementation progress belongs in Git history or an active spec. Debugging procedures belong in Agent Runtime Troubleshooting.

1. Architectural taste

1.1 One fact, one owner

  • durable lifecycle: packages/agent/host
  • canonical vocabulary: packages/agent/store-sqlite/canonical
  • canonical frontend state: workspace AgentSessionEngine
  • DOM, focus, scroll, menus, and temporary disclosure: UI only

Do not solve cross-layer coordination by copying state. Consumers read projections/selectors and write semantic commands.

Device-global quick prompts are not Session or Turn state. AgentGUI consumes the optional AgentHostApi.quickPrompts capability, preserves the host's canonical list order, and delegates create/update/delete/move effects to the host adapter. Reordering is presentation plus intent only: AgentGUI emits a moved prompt, nullable beforePromptId anchor, and moved version; Desktop may show an optimistic projection, while tuttid remains the durable order owner. Hosts that omit the capability must hide the entire entry rather than expose a partial or disabled library.

Desktop AgentGUI reports quick-prompt engagement through the typed panel engagement sink. Opening the picker from the Composer emits agent.quick_prompt_engagement with action=panel_opened and source=composer_input; successfully inserting a saved prompt or a saved recommended template into the Composer emits the same event with action=prompt_used and the corresponding prompt_type. Engagement is buffered until the containing panel satisfies its exposure gate. The action-discriminated contract forbids prompt_type on panel-open events. Events carry only stable AgentGUI context and never prompt titles or prompt bodies, while the action keeps picker exposure distinct from successful use.

The native Mobile composer consumes the same device-global list through its authenticated Desktop connection rather than creating Mobile-owned prompt state. Its authenticated-device service reads the canonical quick-prompt list through the generated tuttid client. The Mobile + menu offers a searchable, read-only selector; choosing a prompt adds its text at the current plain-text input position without replacing the existing draft, restores input focus, and never sends automatically. Create, edit, delete, and reorder remain Desktop management actions. DeviceLink permits only exact GET /v1/agent-quick-prompts reads for this flow; mutation and per-prompt routes remain blocked.

The native Mobile new-conversation Composer presents Agent Target and working directory as peer context selectors directly above the input. The Agent Target catalog remains the launch authority. The working-directory selector reads the daemon-owned registered project catalog through the exact read-only GET /v1/user-projects DeviceLink route and also offers an explicit no-project choice; Mobile does not browse the Desktop filesystem, accept arbitrary paths, or mutate the registered project catalog. The authenticated-device scope keeps the last loaded project metadata across workspace activity replacement, while the selected path remains process-only draft state and disappears after activation. Changing either selector reloads Composer options for the exact Target and optional cwd; creation waits while those exact options are loading and submits only sparse user-selected settings rather than cached effective defaults. An outcome-unknown activation locks both context selectors so an idempotent retry cannot change its launch identity. Activation with a registered project carries both its path as cwd and its versioned canonical project railPlacement; no-project activation omits cwd so the daemon allocates an isolated Session directory and carries the canonical conversations placement. Existing Sessions never expose these selectors as mutable settings.

Use the closed-surface test when assigning ownership: if state must survive or continue progressing after every Agent GUI surface closes, it belongs to Host/store or the workspace engine. State that should disappear with the surface belongs to UI.

1.2 Semantics before screens

Session, Turn, Interaction, Goal, and operation are domain facts. Rail, timeline, dock, toast, and Message Center are projections of those facts; they do not define lifecycle.

Transcript is historical presentation. It is not authoritative for approvals, questions, Turn state, or submit availability.

1.3 Ports and adapters

Core layers declare narrow contracts and ports. HTTP, Electron, filesystem, provider wire, authorization, VM, and process details stay in adapters.

A reusable boundary needs a real responsibility and consumer. Do not create vague common, utils, or shared core modules merely to look reusable.

1.4 Provider-neutral does not mean provider-blind

A provider adapter may understand its own wire protocol. Shared business code reads descriptors, strategies, capabilities, and canonical payloads.

AgentGUI, Message Center, composer, and shared services must not choose behavior by names such as Codex, Claude Code, Cursor, or OpenCode.

1.5 Events are hints; canonical reads reconcile

Realtime events reduce latency but are not automatically complete truth:

  • normalized provider text/reasoning streams and explicitly appendable textual tool output arrive as optimistic message_delta payloads on the /v1/events/ws business-event WebSocket
  • canonical tool output/error bodies bound each text, stdout, and stderr field to 1 MiB, then fit formal output streams into a 768 KiB aggregate durable payload budget, including nested tool steps, by retaining a valid UTF-8 prefix and the fixed [Output truncated] marker
  • terminal command snapshots may omit text only when it exactly equals the trimmed stdout or stderr; Reporter resolves the alias before independent field truncation and clears an earlier running text, while the raw stream remains canonical and explicit non-command tools retain provider-neutral text
  • continuous, version-complete message_update events may merge inline
  • terminal message_update is the durable confirmation; message version gaps, invalid/unanchored deltas, nonterminal deltas after known terminal message truth, reconnects, Turn, Interaction, and state changes trigger authoritative reconciliation
  • a focused conversation holds a synchronization lease. A terminal initial reconcile failure is observable through that lease and receives one bounded retry; releasing the lease prevents further focus-owned recovery work
  • event publication or observer failure cannot roll back a committed canonical transaction
  • message projections preserve the original textual content, including leading and trailing whitespace; trimmed or whitespace-collapsed copies may decide emptiness, synthetic-control suppression, or duplicate identity, but must not replace the body passed to renderers. Markdown renderers continue to own soft-break presentation.

1.6 Identity and correlation are explicit

Cross-boundary work uses stable identifiers:

  • workspace: workspaceId
  • session: agentSessionId
  • Turn: turnId
  • Interaction: requestId
  • submit: clientSubmitId
  • UI Agent: agentTargetId

Never infer identity from titles, timestamps, array positions, provider names, the latest transcript row, or runtime instance IDs.

1.7 Fail closed

When authoritative identity, Turn, or Interaction is missing, return unsupported/loading/error. Capability presence is explicit: a null session capability snapshot means no authoritative session snapshot is available (not yet reported or legacy-ambiguous), while a non-null snapshot is complete and may explicitly contain no supported capabilities. Presentation may fall back from a null session snapshot to the authoritative provider composer descriptor, but an explicit session false always wins. Do not choose the first provider, manufacture a Turn, treat an ambiguous legacy empty array as loaded, or hide contract drift behind an ad hoc UI fallback.

Compatibility paths require evidence of existing data or a release window. Keep them isolated from canonical writes.

1.8 Contract first

Change OpenAPI before HTTP contracts, then generate Go and TypeScript types. Internal domain types cross layers through explicit projections; do not maintain handwritten transport mirrors.

Identity, time, and state use canonical representations. Unknown enum values produce an explicit unsupported/error path; widening them to arbitrary strings is not compatibility.

2. System shape

2.1 Command path

AgentGUI / Message Center / host surface
  -> semantic AgentSessionEngine operation or typed intent
  -> workspace AgentSessionEngine
  -> shared typed lifecycle effect projection
  -> Desktop or Mobile AgentSessionEffectPort
     (host-only commands use the narrow EngineExtensionCommand adapter)
  -> tuttid HTTP and product adapter
  -> packages/agent/host
  -> canonical store transaction + provider runtime port

2.2 Observation path

provider runtime observation
  -> packages/agent/host + store-sqlite canonical transaction
  -> CommittedDelta / CommitObserver
  -> tuttid ActivityProjection and event publication
  -> Desktop business-event bridge or Mobile DeviceLink live lane
  -> workspace AgentSessionEngine reducer
  -> memoized AgentActivitySnapshot
  -> selectors / pure projections
  -> AgentGUI / Message Center / host chrome

2.3 Session capability snapshots

Provider adapters report canonical session capabilities through a typed, complete snapshot. They do not place capability ids in provider-private RuntimeContext. A missing snapshot means no authoritative session snapshot is available because it has not been reported yet or legacy data is ambiguous; an empty reported snapshot means no canonical session capabilities are supported. Partial state reports omit the snapshot and preserve the last reported value.

Persistence uses a private JSON compatibility carrier for capability ids plus explicit report presence, then exposes only the typed optional snapshot at the store boundary. Legacy non-empty lists are reported snapshots; legacy empty lists without presence evidence remain unknown. The daemon API projects unknown as null and reported snapshots as a closed boolean record. This does not require a database schema migration: the presence marker lives inside the existing metadata JSON column, and decoding normalizes old rows.

activity-core owns the presentation resolution rule: a reported session value overrides composer options, while a null session snapshot falls back to the authoritative provider composer descriptor. AgentGUI consumes that result and does not add provider-specific or control-specific fallback logic. Host and runtime operation handlers remain authoritative when an action is submitted.

Runtime context transport also distinguishes a complete snapshot from an explicit top-level set/unset patch. Omission preserves existing context; patches may update only provider-private keys and must never replace unrelated session metadata.

New-session launch settings

Remembered composer defaults are target-scoped preferences, not active Session state. AgentGUI reads them only while composing a new Session and sends the resolved sparse settings through the normal activation command. A host-owned entry capability may additionally hide or disable an experimental control; the activation boundary must fail closed as well, so a remembered true value cannot outlive a disabled host entry. Provider support comes from the resolved composer descriptor rather than provider-name checks in shared UI code. Provider and extension model catalogs can change independently of target-scoped remembered defaults. On Create, the daemon treats such a default as a fallback preference and resolves an obsolete value to the current catalog default from the same target. Because AgentGUI also sends its presented effective value, the activation request carries explicit provenance bits for model and reasoning; Engine and the tuttid adapter must preserve them through the HTTP boundary. Legacy direct Create callers that omit provenance retain the compatibility rule that a supplied non-empty value is explicit. Non-explicit model-dependent settings, such as reasoning effort, resolve against that effective model rather than remaining bound to the obsolete preference. A strict per-model catalog never forwards an inherited value that the target model did not advertise. A model or dependent setting explicitly supplied by the caller remains strict. If the runtime rejects an explicit model selection, startup fails rather than continuing with an undisclosed provider default.

Create resolves and freezes the effective launch cwd before the first cwd-sensitive model-catalog lookup. A no-project session therefore queries the catalog from its allocated session directory, which is also passed to runtime; it must not inherit the tuttid process cwd. Worktree creation intentionally queries from the selected source checkout before creating the isolated checkout. The no-project directory remains a provisional allocation until Host creates the Session (or delivery becomes uncertain); any earlier catalog, plan, reasoning, preparation, or runtime failure releases it so failed requests cannot consume the allocator's daily name space.

Composer model catalogs are daemon-owned snapshots. A provider auth/config change invalidates the snapshot and closes its provider app-server session, but the daemon may return the last known list as stale while refreshing it in the background. AgentGUI may render that stale projection with its loading state; model validation must wait for an authoritative snapshot. The daemon shares concurrent catalog loads, reuses a warm app-server session, and publishes the existing catalog invalidation hint after a successful refresh so open composers can converge without owning provider or cache policy.

Connector capability catalogs follow the same projection boundary. Each host reads its authoritative local Connector Market snapshot, then uses packages/agent/daemon/composercatalog to map manifest identity, icon, installation, compatibility, and authorization state into the closed Composer capability contract. Host transports only serialize that projection and route semantic open intents; they must not duplicate Connector readiness rules or derive Composer entries from renderer-local market state. The snapshot uses the host's current account scope so Composer status and prompt admission include the same authorization overlay as Connector Market management. Connector Market change events invalidate retained Composer options, while a menu open may still force an authoritative reread as a recovery path.

Hosts may project a Connector catalog as inspectable but read-only. In that mode AgentGUI keeps the menu and host-provided readiness descriptions visible, but suppresses selection, setup or authorization opens, selected chips, and the optional management footer. Read-only presentation never becomes execution authority; the Host remains responsible for Connector admission at submit or runtime preparation.

Settings that affect provider preparation are immutable after launch. The daemon validates them against current product policy and resolved provider capability before runtime preparation; an active Session cannot reinterpret them through an in-place settings update.

RTK saver mode follows this launch-only path for every resolved Agent provider. The remembered composer value is only an opt-in; provider-neutral runtime preparation resolves the pinned Tutti-bundled or managed-runtime rtk executable, copies it and the canonical RTK.md into the exact Session runtime, places an absolute reference to that file first in session-private AGENTS.md providers, retains the inline RTK rule as a compatibility fallback through the provider's existing instruction channel, installs provider-native rewrite hooks or plugins where available, and prepends only the private binary directory to the Session environment. Tutti never runs an RTK package manager or global installer. RTK usage data, tee output, and telemetry policy are also Session-scoped, so enabling the mode cannot change another Agent's instructions, executable search path, or tracking state. The independent rtkSaverMode property carries this provider-neutral setting, while the existing codexSaverMode property remains Codex-only and continues to control the Luna subagent workflow. Tutti terminals receive the bundled RTK directory in their child PATH for explicit use, while the user-global PATH and external Agent processes remain unchanged.

2.4 Ownership map

Layer Owns Must not own
store-sqlite/canonical canonical phase, outcome, origin, Interaction, capability vocabulary, and pure projections HTTP, provider processes, React
store-sqlite canonical transactions, SQLite repositories, durable tombstones/outbox participation product UI, transport policy
packages/agent/host create/resume/send/cancel/fork, lineage, Interaction, Goal, operation, and recovery lifecycle HTTP DTOs, Electron, concrete provider wire
packages/agent/daemon provider registry, runtime mechanics, wire normalization AgentGUI policy, cross-provider UI branches
services/tuttid/service/agent Host adapters, HTTP/query/composer/product policy, provider preparation reimplementation of Host lifecycle
tuttid ActivityProjection canonical read projection, commit observation, event publication/repair lifecycle decisions, React state
agent-activity-core workspace engine, canonical frontend entities, pending intents, queue, selectors HTTP, Electron, React
agent-gui runtime contract, projections, controllers, views, UI-local state daemon truth, a second session store
apps/desktop tuttid client, business-event WebSocket, preload, Workbench, windows, file/OS capabilities a second Agent business core
apps/mobile DeviceLink adapter, Native renderer, app lifecycle, navigation and drafts a second Agent business core or Session DTO cache

services/tuttid/api/openapi/tuttid.v1.yaml is authoritative for HTTP request/response contracts. It projects the canonical domain; it does not replace store-sqlite/canonical.

2.5 Ephemeral observation gaps

A Host may expose an ephemeral observation gap for one exact Session and Turn when its caller-side projection may be stale. The gap is presentation-only: AgentGUI pauses the processing animation, keeps recovery chrome visible, and blocks commands that require current state until the Host removes the gap. A Host may additionally classify the gap as peer-offline or synchronizing; while that classification is present, AgentGUI replaces the numeric live duration with the matching status copy. An older Host that omits the classification retains the frozen-duration fallback.

An observation gap does not settle, interrupt, or otherwise rewrite the canonical Turn. The Host owns reconnect and catch-up fencing and must remove the gap only after the same Turn is authoritative again. When the capability is absent, AgentGUI preserves its existing lifecycle presentation.

2.5.1 Live Side transient lane

Side is a provider-neutral, surface-owned live conversation fork. It is intentionally separate from the durable command and observation paths:

AgentGUI Side pane
  -> AgentSideConversationRuntime port
  -> Desktop generated tuttid client adapter
  -> tuttid Side HTTP handlers
  -> packages/agent/host Side lifecycle
  -> packages/agent/daemon provider Side adapter

provider Side runtime observation
  -> SideStreamObserver
  -> tuttid agent.side.updated publisher
  -> Desktop business-event adapter
  -> AgentGUI Side event normalizer
  -> activity-core ephemeral conversation projector
  -> shared AgentConversationVM projection
  -> Side timeline + composer instances

The runtime SideStreamObserver seam includes both event observation and explicit ephemeral-identity cleanup. Host calls the cleanup operation when a Side closes, is removed, or expires, so bridge-local ordering state cannot outlive the Side identity.

The provider adapter owns provider-specific context snapshot mechanics. Host owns Side identity, idempotent open, exact-Turn commands, scope isolation, and cleanup. tuttid projects those commands through generated OpenAPI contracts and publishes only the transient agent.side.updated envelope. Desktop is a thin transport adapter. AgentGUI owns capability gating, the transient event projection, and presentation; it contains no provider-name branches.

Side has no persistent button in the main composer. Desktop exposes Side and the transcript text-selection action toolbar only after the user enables the default-off lab.agentSideConversation developer preference. The host passes that preference as an explicit AgentGUI capability and omits the Side runtime while it is disabled; AgentGUI therefore does not resolve provider support, show the toolbar or slash entry, or intercept typed /side input. After the host opt-in, AgentGUI injects the provider-neutral /side entry into the slash command palette with a local submitImmediate selection effect only when the exact source Session reports every required Side capability. Selecting the entry immediately submits the local /side intent, ensures the source runtime is available, and opens Side; typed /side <prompt> opens it and sends the prompt in one action. After capability discovery has enabled /side, submission creates the local opening projection immediately and renders the Side shell plus its empty temporary-conversation explanation before the provider fork finishes. It does not perform another renderer capability request on the open path; Host and Runtime remain the authoritative live-source validators. Capability resolution is fail-closed: pending, unavailable, and unsupported providers do not show /side, including when a provider advertises a command with that name. A typed /side invocation remains a Side-only intent in those states: AgentGUI reports the unavailable operation and never forwards its text through the main conversation submit path. A released canonical provider connection may be resumed from the source Session before capability resolution; the runtime remains the final authority for whether the operation can open.

Desktop supplies a Side runtime factory rather than a workspace singleton. Each mounted embedded or detached AgentGUI surface creates and disposes its own runtime instance, so transient identity and cleanup ownership cannot race between windows.

The Side controller is not an AgentSessionEngine and must never dispatch Side events into the durable workspace engine. AgentGUI normalizes the transient envelope into the React-free activity-core ephemeral conversation projector, which emits the same AgentActivitySnapshot/Turn vocabulary consumed by the shared conversation projection. This reuse is projection-only: it does not load, retain, reconcile, or persist a workspace Session.

The controller retains only the selected surface's active Side identity, ephemeral projection, pending Interaction, sequence, and error. Embedded AgentGUI keeps Side attached to its source identity when the selected source changes and shows it again when that source is reselected. A host that injects the optional external Side presentation bridge opts into fail-closed source switching: changing the selected canonical Session closes the old Side. The owning AgentGUI surface otherwise closes it only through the explicit Side close action or when the surface-owned runtime is destroyed. Its external-store subscription owns the transient event and connection subscriptions. Once the last surface subscriber is gone, those subscriptions are released. Disconnect, source-identity mismatch, or a sequence gap expires local state and prevents further rendering; no canonical reconciliation fallback exists for this ephemeral lane.

The shared Side surface creates a second instance of the conversation timeline and composer. Embedded AgentGUI wraps it as a docked sibling of the main detail surface, not an overlay or nested durable Session route. Standalone AgentGUI publishes the exact surface projection through an optional presentation-only external store; Desktop maps that projection to a contextual side tab in the shared right tool sidebar and suppresses the inline wrapper. The bridge carries only exact source/Side identity, surface props, and the close intent. It owns no Side lifecycle or durable state. Each surface instance owns its own draft, scroll-follow state, focus/shortcut scope, Turn gate, and interrupt action. The Side composer inherits the provider/model/cwd boundary captured at open time and presents those settings read-only. A normal main-composer submission always stays on the main Session; only the explicit /side command opens or targets the Side lane.

The transcript selection toolbar stages selected text as a removable quote in the target composer. Add to conversation updates only the main draft. Ask in Side opens an empty Side when needed and updates only its draft; it never creates a Turn until the user explicitly submits that composer. Quote blocks are UI-local draft state and materialize as Markdown blockquote text at the existing submit boundary, so the Side transport remains text-only. When a typed question accompanies those quotes, Side keeps the blockquotes in provider content but projects only the typed question as the visible user message; a legacy quote-only submission keeps the existing blockquote display fallback; only a future explicit selected-text content kind can make that ambiguous shape safe to render as a chip. Focus ownership follows the currently visible Side pane: an unmounted or source-mismatched Side cannot keep the main composer inactive, and pane cleanup must release its focus scope.

Side capability is enabled only when the exact selected source runtime, after any required canonical-session resume, reports all mandatory facts: native support, active-source-Turn snapshot support, ephemeral lifetime, hidden inherited history, and an injected model boundary. The Side may be opened after the source has started and may continue after the source Turn settles; the source Turn only determines whether the provider has live context to snapshot, not the lifetime of an already-open Side. Attachments are rejected at the AgentGUI controller and Desktop adapter until the transport contract can preserve them without degradation. Approval and question state remains Side-local and is answered through the exact (workspaceId, sideAgentSessionId, turnId, requestId) command.

2.5.2 Interaction admission and execution presentation

An exact pending Interaction is a separate admission scope. When its Host supplies interaction readiness, that exact result owns transport presentation and early write admission for the pending card. An observation gap may still govern the active Turn before or after that Interaction, but it cannot override the exact ready or blocked readiness result while the card is presented.

When an exact Interaction is blocked, AgentGUI keeps the pending action visible but disables its controls. The disabled action group remains focusable and hoverable so the same readiness reason is discoverable in the conversation chrome and Message Center; this is presentation only and does not change the Host-owned admission decision.

Message Center and attention-card consumers preserve this separation in their presentation contract: isSubmitting describes an in-flight response, while an independently supplied interaction-disabled state blocks the nested prompt controls without hiding the pending card or its conversation navigation. A host-provided blocked reason is exposed through a focusable, hoverable description so a blocked card remains understandable without making Tooltip the command authority. If the Host omits the reason, the disabled wrapper stays out of the tab order and does not create an empty Tooltip. The final semantic command admission check remains in the consumer/controller path.

Submission and execution presentation

Submission acknowledgment and execution are separate presentation signals:

  • isSubmitting, an unconfirmed submit, and a viable initial launch that expects a Turn drive an isAwaitingTurnStart Composer spinner immediately because they prove local submission intent or expected launch work
  • a reconnect-only activating state belongs to connection chrome and command admission; without submission intent it must not create a sending spinner, working status, Stop target, or queue eligibility
  • Conversation working status and queue eligibility require canonical execution evidence: the fenced Engine display status or a non-settled active Turn; pending activation or submission alone must remain ready, both before and after canonical Session confirmation
  • canonical completed, failed, or canceled activity stops the execution indicator even if a lower-level runtime signal has not caught up yet
  • transport recovery chrome, such as connecting or unavailable, takes priority over normal working copy
  • Stop is available before Turn identity only when the Engine exposes an exact pending-submit stop target; reconnect and other connection state never invent one

This keeps feedback optimistic for responsiveness while keeping claims about Agent work grounded in the canonical Turn/runtime projection.

The Composer also hands the exact draft used to build a submission to the AgentGUI controller. The controller snapshots that handoff for conditional post-submit clearing; it must clear only when the current draft still matches the submitted snapshot, so a queue admission or accepted send cannot erase a newer edit made while the request is in flight. For image blocks, provider locators and preview/upload-progress metadata may settle asynchronously and do not count as user edits; upload errors and unknown fields remain part of the comparison.

2.6 On-demand status

AgentGUI owns one provider-neutral AgentStatusController for /status, Agent Info, and Agent Config. These surfaces are explicit bounded reads; mounting an AgentGUI node must not start background status polling.

The host injects an AgentStatusSource. AgentGUI treats scopeKey as opaque and never resolves provider, account, local-vs-remote transport, or owner identity. The controller owns only loading/ready/error presentation, 30-second request cancellation, a one-hour retained UI snapshot, 5-second manual-refresh debounce, and fencing callbacks from closed or replaced requests. Opening any status surface uses the same controller snapshot so the three views cannot drift into separate state machines.

Every production host, including Tutti Desktop, injects this controller. A host adapter resolves the exact Agent Target to its provider, verifies an optional Session belongs to the same workspace/target/provider, and performs the bounded status read. AgentGUI has no legacy probe-backed status state or provider-derived fallback. The active conversation id is the request Session identity because it exists before detail hydration; raw Session chrome remains presentation data.

Tutti Desktop creates one status source per workspace renderer and injects it into every AgentGUI surface in that workspace. The source shares the one-hour Agent Target snapshot, five-second refresh debounce, and in-flight status probe by exact agentTargetId, while each surface keeps its own controller and therefore its own query, loading, close, and stale-response state. Sharing the controller itself is invalid because one surface could replace or close another surface's active request. Standalone renderer processes have their own source; the Electron main process remains the cross-window short-lived exact-target cache.

Account-usage probing for an Agent Extension is an optional, versioned, target-scoped capability declared by the signed Extension. The provider-owned Helper owns provider config, credentials, trusted origins, private endpoints, and response parsing; tuttid runs only the exact companion script installed and fingerprinted in that Target's independent companion runtime through a separately verified Node interpreter. Companion availability never changes ACP readiness. A daemon-owned reconciler installs the companion outside setup actions, wakes after runtime or Extension activation, retries failures with bounded backoff, and rechecks persisted activation on restart. Account-usage requests are joined and cached for a short TTL by exact agentTargetId; the TTL starts when execution completes, and Node identity derivation is reused while the executable file identity is unchanged. The daemon accepts existing v1 helper output and returns the normalized provider-neutral tutti.agent.account-usage.v2 discriminated result. Desktop validates the schema and echoed Target/provider identity, then projects billing identity, quota completeness, and optional exact Provider-neutral amounts without changing ACP readiness. A complete quota set is distinct from a known account whose quota is unavailable and from API billing where quota is not applicable; only the complete state may carry rows. Presentation maps API not_applicable to a resolved empty limits row (—), while unavailable uses localized account-quota-unavailable copy; neither becomes a refresh failure. Missing profiles and older Extensions are unsupported; unknown schemas, enums, shapes, partial exact amounts, or a claimed complete result without quotas fail closed as parse_failed. No provider message, path, endpoint, response body, credential, account ID, or raw account record crosses the port or enters logs.

A source emits at most one cached snapshot followed by at most one refreshed value, then completes. Backend probing may continue independently to fill a host-owned cache after the presentation request is canceled; late frames from the canceled request must not mutate AgentGUI. Errors crossing the port are structured codes, never provider stderr, account material, endpoints, or transport diagnostics. The limits projection preserves stable codes such as auth_required, session_expired, and subscription_required through AgentStatusValue.limitsErrorCode; AgentGUI owns their localized presentation and maps unknown codes to one generic failure label.

A provider quota may carry an optional exact provider-neutral amount and unit when a percentage alone would hide useful account information. The host owns the provider request, credential handling, package aggregation, and percentage normalization; AgentGUI only renders the projected amount and uses the optional percentage for progress presentation. Raw account and package records never cross this boundary.

An explicit unsupported usage probe is a successful bounded read with no quotas and limitsState: unavailable; it must not become a refresh failure. Only real authentication, transport, parsing, timeout, or execution failures project limitsState: error.

A Custom Agent bound to a Model Plan does not use the Agent Runtime provider's native account for model access, so provider-native account usage is not applicable to that exact target. Desktop projects providerAccountUsageApplicable: false from the Workspace Agent binding; AgentGUI preserves the optional capability through its target projection. The Desktop status source then completes the bounded read with unavailable limits without calling the provider usage probe, and AgentGUI disables manual usage refresh for that target. An absent capability keeps the existing provider account behavior for other hosts and ordinary Runtime targets.

Account-config refresh placement is also host-scoped presentation. Tutti Desktop opts into accountUsageRefreshInline so the refresh control shares the limits header row. The capability defaults to false; TSH/VN and other hosts that omit it retain the established standalone menu row.

Closing /status, Agent Info, or Agent Config cancels only the request owned by that surface. Replaced requests remain fenced. A stream that completes without a frame is a failed refresh: a retained value may remain visible, but the UI must show the refresh failure rather than treating the old value as a new success.

Desktop availability analytics reads the renderer's loaded managed-provider snapshot and must not turn a pageview or focus event into a daemon request. A stale visibility snapshot may reconcile through a non-forced tuttid status read; tuttid owns cache validity, credential-fingerprint checks, and single-flight probing. Forced detection is reserved for explicit provider refresh and the provider-scoped confirmation after an install, login, or update action. A confirmed new-conversation creation failure is the only analytics exception: Desktop force-refreshes only the exact target's provider, then reports the fresh availability snapshot with a failure-specific trigger.

Cold workspace initialization may request all managed providers together so the first useful snapshot arrives quickly. tuttid may detect different providers in parallel, but it applies one process-wide limit to auth, version, and adapter probe subprocesses because a single provider can otherwise fan out into several CLI processes. Later visible-window stale reconciliation is best-effort background work and checks providers serially; another focus cannot overlap the same reconciliation run.

Provider detection separates volatile readiness from stable executable facts. An ordinary read may reuse the full provider snapshot. A forced read bypasses that snapshot and rechecks current authentication/readiness, including the provider credential marker, but may reuse a successful CLI version or adapter launch result while the resolved executable fingerprint is unchanged. Failed version reads and failed adapter launches are never cached. OpenCode and Tutti Agent use their validated local credential files as the primary auth signal; malformed files may fall back to one CLI check. Cursor's single about --format json result supplies both auth and version when available.

Authentication status is evidence-based and provider-neutral. A local status command, marker, token, or API key proves only configured. A provider-backed account probe or successful agent request proves authenticated. An explicit remote authentication failure proves required and outranks stale local credentials until credentials change or a later provider request succeeds. Conversation failures may update this provider-global state only for a provider-native runtime and only from the typed completed root-provider Turn: auth_required records required, while a completed outcome records success. Model Plan and Agent Extension failures are connection-scoped and never mutate the provider-global login state. Shared layers do not infer authentication from provider prose, HTTP 403, gateway timeouts, billing, quota, or model errors. packages/agent/daemon/providerstatus owns evidence reduction; tuttid owns the live outcome store, cache invalidation, and public status projection. Tutti Desktop management surfaces and AgentGUI consume that same status. They may keep a configured provider launchable when availability is ready, but must not relabel it as connected or authenticated.

For subscription OAuth, provider descriptors select the remote-auth strategy. Claude Code uses GET https://api.anthropic.com/api/oauth/usage; Codex uses the provider runtime's account/rateLimits/read. These checks are attempted on ordinary status detection and become stale after 15 minutes. Visible, focused Desktop windows reconcile stale providers on focus/visibility activation and on the 15-minute poll. API-key configuration skips subscription OAuth checks. Explicit authentication rejection is authoritative; rate limits, server errors, and network failures leave the weaker configured state intact. Conversation error adapters preserve typed provider code, HTTP status, retryability, origin, and sanitized upstream detail. AgentGUI shows sanitized provider-origin detail directly; Tutti-owned transport and lifecycle failures continue to use localized product copy. Secrets are redacted before logging, persistence, or presentation. These checks validate current access but do not rotate refresh tokens; refresh ownership requires a separate serialized credential lifecycle that can gate on live provider processes.

2.6 Developer cassette replay

The developer-only agent.sessionRecording desktop preference defaults off. When enabled, Desktop injects its recording and replay controls through generic AgentGUI render slots. AgentGUI contains no recording/replay API, controller, state, provider branch, component, or copy.

The provider-neutral renderer contract is published separately as @tutti-os/agent-session-replay. It owns the portable activity event type and interaction contract shared by Desktop and TSH; product adapters still own scope mapping, persistence, HTTP/Electron integration, replay runners, and provider/runtime setup.

packages/agent/session-replay owns the provider-neutral Recording/Cassette workflow, status transitions, portable contracts, and validation policy. services/tuttid/service/agentsessionreplay is Tutti's HTTP/product adapter and composition surface; Desktop is the Electron/renderer adapter and composition surface. Desktop reads the authoritative recording list after mounting and projects commands; React state is never the source of an active Recording. packages/agent/daemon/runtime retains only concrete recording and scripted replay transport mechanics.

The default-off preference is a startup composition gate, not only a UI gate. The renderer awaits initial persisted preference hydration before it constructs the Workspace service container, so composition never reads transient in-memory defaults. The preferences module owns this readiness seam; Replay remains a consumer of the hydrated feature flag and does not add a parallel event center. The main-process Replay composition module owns manager/access/control creation and all Replay IPC bindings; general runtime IPC supplies only Electron and daemon adapters. When disabled, Desktop main does not create the Replay process manager, access adapters, control writer, or Replay IPC handlers, and the renderer keeps the replay activity bridge inert: it creates no recording binding, recorder map, or Engine observer. When enabled, the agent-session-replay feature-local activity bridge owns the recording binding, recorder map, and Engine observer fan-out; WorkspaceAgentActivityService remains the Activity Engine/reconcile facade and delegates that replay boundary to the bridge. The renderer creates recorder state only for the lifetime of an active Recording and mounts isolated Replay observers only inside the Replay runtime. Changing the preference does not claim to recompose a running daemon or renderer; the next process composition applies the new value.

A Recording captures a time window over the root SessionGraph. Root Turn settlement, child creation, and Goal continuation do not complete it. Explicit stop waits for a stable boundary, detaches every graph connection, and exports expected graph state. Daemon restart recovery marks an interrupted capture incomplete and exposes it through the same recording list.

The cassette runner drives these controls through a real Electron AgentGUI. Replay mode keeps the current provider adapter but replaces only its Agent SessionGraph ProcessConnection set; current adapter outbound bytes must match the recorded Session/provider/launch-ordinal connection before inbound frames are released. The Desktop feature apps/desktop/src/renderer/src/features/agent-session-replay owns the recording toolbar, recording list, replay-window controls, feature gating, product copy, and daemon client adapter. Recording names default to their UTC creation timestamp. A completed row supports inline rename; the daemon writes the name into cassette.json, recalculates the manifest hash, and commits matching Recording and Cassette metadata. The list header can import one or more portable Cassette directories through a native Desktop file selection. tuttid validates every manifest, file inventory, hash, portable state, identity, and size limit before copying the batch into daemon-owned storage and projecting each valid Cassette as a completed Recording in the current Workspace. Invalid or conflicting members report per-Cassette failures without rolling back other successful imports; source directories are never mutated. The Desktop Replay feature service owns the native import adapter call, loading and error state, and the authoritative Recording refresh after any successful member. React dispatches that semantic command and renders its localized outcome toast; it does not compose preload and refresh operations. A completed recording exposes Play directly in its list row. When at least two distinct root Sessions are available, the list may enter temporary batch-selection mode and submit their exact Cassette IDs in one Replay Workspace launch. Selecting one Recording disables other Recordings for the same root Session; this selection disappears when the list closes and is never Recording or Cassette domain state. The primary workspace recording list exposes Delete for inactive rows and requires a portaled destructive confirmation dialog before removing the Recording and its Cassette. Active rows remain protected by the daemon workflow. The primary workspace window never renders Replay pause or checkpoint controls. Those controls belong only to the isolated Replay Electron surface and become enabled only when the daemon core exposes the corresponding stable commands; the UI must not simulate them by dropping chunks or changing a local cursor.

While a Recording is active, Desktop observes the workspace AgentSessionEngine boundary rather than AgentGUI. Public replayable intents enter a synchronous renderer buffer; matching external command settlements become effect events linked to the intent that caused them. Reducer-generated follow-up intents are not recorded a second time, and effects without a recorded cause are discarded. Desktop flushes this ordered stream through the recording API before asking the daemon to complete the Recording, and drops the buffer after a successful cancellation. AgentGUI remains unaware of recording and replay contracts. The isolated Replay Workspace reuses the Workspace window composition and mounts one AgentGUI Workbench Node per Replay Cassette. Desktop installs one Workspace replay binding, not one binding per Node. The binding maps generated Engine commandId values to the exact Cassette; command results never fall back to recorded correlation IDs across Cassettes.

Desktop mounts Cassette-scoped status observation and playback controls through each AgentGUI Node's composer footer slot, while Replay runtime state keeps them empty in normal windows. AgentGUI lays this accessory slot out as a full-width wrapping row below the primary composer controls so Replay transport does not compete with handoff, model, or reasoning controls in narrow Nodes. Replay speed is a daemon-owned command. The isolated Node resolves its Cassette from exact Node identity before reading or updating playback through Desktop IPC. Replay status remains visible independently of provider transport readiness: the footer renders with disabled transport controls until playback becomes active, and terminal completion produces a success toast in the isolated Electron window. The primary recording list asks for automatic or manual playback when Play is selected. Automatic mode preserves continuous playback. Manual mode enters the isolated runtime paused at the first inspectable checkpoint, so no initial Activity interval can race a later renderer Pause command. Pause, resume, and checkpoint movement use a versioned replay-control.json handoff at TUTTI_AGENT_SESSION_REPLAY_CONTROL_PATH, because pausing only the provider transport would still allow the runner to dispatch later business stimuli. The runner publishes daemon-confirmed semantic checkpoint state and playback mode beside phase in replay-status.json; Desktop observes that file but does not manufacture playback state in React. A checkpoint becomes inspectable only after the exact Node is mounted, its logical Session is selected, current detail is hydrated, and the renderer has observed the satisfying canonical version. Controls remain forward-only; Desktop does not expose previous-checkpoint or restart. The primary window owns only Replay launch feedback. Main supervises the runner by (launchId, cassetteId) and keeps Cassette milestones in memory plus the temporary Surface-status handoff. It does not persist playback execution, checkpoint, completion, failure, or cancellation into the primary daemon. The cassette runner writes Cassette-scoped replay, verification, success, or failure events, and a failure in one Surface keeps the Replay Workspace open while other Cassettes continue.

The runner uses isolated daemon state and Electron userData. CDP opens AgentGUI, selects the provider and exact Session, and verifies the rendered result. Recorded business stimuli are sent through the isolated daemon HTTP contract; they are not reconstructed from composer clicks. Cassette transport selection is daemon composition and does not change Session, Turn, Interaction, Goal, or runtime-operation lifecycle ownership in Host. The semantic Replay runtime creates the isolated Workbench snapshot with onboarding already auto-opened and no restored nodes, so the runner can open only AgentGUI. Create-session stimuli persist the effective launch settings returned by the recorded Session; Replay must not recompute model, reasoning, permission, or speed defaults from the current machine. The runner preserves lifecycle ordering by waiting for the canonical Session to become idle before dispatching each recorded session.send; HTTP status retries are not lifecycle authority.

Managed Replay launch has two Cassette-scoped milestones. surface-ready requires the exact Node to be mounted, its root Session selected, and that Session's detail hydrated; it closes launch feedback but does not complete the Cassette. replay-complete is emitted only after that Cassette's stimuli, transport check, and expected-state check succeed, and only that milestone resolves the Cassette's local completion as complete. A create-session Replay restores its recorded rail Project, waits for the create command, and selects the exact canonical Session without reloading the page.

3. Domain model

3.1 Session

A Session holds identity, target, provider metadata, cwd, title, settings, resume information, a Goal reference, and the current active Turn reference.

Session title has an explicit ownership rule in the runtime. A user title (explicit rename) is immutable from the turn-execution path: a running turn may fold provider/event titles as candidates, but the controller's current Session is the only accepted state, and the turn-completion commit merges only the turn-owned lifecycle/status fields back. A title the user set is never overwritten by a late provider title or by a stale turn-completion snapshot, and neither the stream projection nor the durable report may carry a stale provider title over an established user title. On resume the runtime fails closed and treats a persisted title as user-established. Metadata commands issued while a new Session is still activating wait for the canonical Session; an activation failure or bounded wait timeout is an explicit user-visible error and never a silent no-op.

A Session does not copy Turn phase/outcome, own pending Interactions, or persist lifecycle inferred from transcript.

If provider-native compaction fails because the current context is already over its hard limit, the provider adapter projects one typed context-handoff-required system notice with error severity. AgentGUI localizes the failure and tells the user to start a new conversation and mention the exhausted Session there. Tutti does not replace the provider session or automatically redispatch the user's next message: the fresh root Session and its canonical agent-session mention make the handoff explicit and preserve the user's control over what continues.

Provider-native subagents use child Sessions:

  • rootAgentSessionId / rootTurnId: root execution
  • parentAgentSessionId / parentTurnId: direct parent
  • parentToolCallId: delegation card correlation
  • child messages, Turns, and Interactions retain the child owner

A provider adapter must normalize explicit spawn evidence into both the parent delegation call and the canonical child Session/Turn. AgentGUI attaches the child lane only through the immutable parentToolCallId; it must not parse provider-native spawn events, invent a missing parent card, or create a presentation-only child lane.

The parent-side child lane preserves the latest canonical assistant message as complete Markdown both while it streams and after the child settles. The same safe streaming Markdown/link boundary used by ordinary assistant messages owns that presentation. Bounded plain-text activity is only a fallback before the child emits assistant content; character-level activity truncation must never corrupt a generated-file link or drop earlier assistant context.

User-initiated Fork creates a new root Session rather than a provider-native subagent. The child records durable lineage to the source Session and inclusive boundary Turn, but receives a caller-reserved canonical Session id and a provider-created Session id. Canonical Session, Turn, Message, and Interaction identities are session-scoped and are remapped during the atomic clone. Fork titles are allocated across the complete lineage family, so a source titled Session produces Session (2), Session (3), and so on even when a later Fork starts from an earlier child.

3.1.1 Session Fork

throughTurn means the boundary Turn and all earlier canonical history are included. AgentGUI emits only the canonical Turn id. Host resolves its durable provider root Turn id and invokes the exact provider adapter selected by the runtime registry; shared UI and Host code never branch on provider names.

The projected Session capability is provider/runtime-level:

provider registry declares native Session Fork
  AND the exact adapter/version attests throughTurn support
  AND provider state has an explicit host-copy or provider-owned binding mode

Only that conjunction projects lifecycleCapabilities.forkThroughTurn=true. Projection consumes a live observation or persisted driver attestation only; it does not prepare or launch a runtime. The full preparation and exact driver revalidation happen after the user requests Fork. Turn-level eligibility is the single providerForkBindingAvailable fact projected from a non-empty canonical rootProviderTurnId, plus the canonical Turn being settled. Host eligibility remains optimistic and verifies only those selected-Turn facts. AgentGUI exposes Fork only for settled, provider-bound Turns. This per-Turn presentation rule is shared by every Agent provider. Source activity, pending Interactions, descendants, and historical prefix provenance do not hide Fork actions for earlier settled Turns. Only an in-flight Fork for that exact canonical Turn disables its button.

Session Fork is available whenever the exact provider/runtime and selected Turn project the capabilities described above. Desktop exposes that capability directly, and tuttid verifies the same provider and Turn facts when executing a new Fork write; no separate feature preference gates the action.

Execution accepts a worktree-isolated source. A provider-native Fork retains the provider cwd, and Tutti freezes the same cwd and isolation coordinates into the target snapshot without creating or transferring worktree ownership. Other runtime facts are frozen into the target snapshot as well. Only attachments referenced by that snapshot are staged into the target namespace; the source attachment directory is never copied wholesale.

Fork is a durable Host-owned saga:

prepared -> dispatching -> provider_accepted -> committed
                  \-> unknown
           \-------------------------> failed

requestId is the caller-stable replay identity and targetAgentSessionId is reserved at prepare. Prepare freezes the complete canonical snapshot through the selected Turn, the selected provider binding, driver kind/version, runtime context, settings, and a deterministic attachment manifest. Referenced attachments are staged into the target namespace before provider dispatch. The source remains writable; only physical deletion retains the frozen resources. A second explicit Fork from the same boundary is valid. The provider call begins only after the dispatching marker commits.

The provider adapter verifies only that the selected provider Turn exists in the provider source. Tutti deliberately trusts earlier provider and canonical history to represent the same conversation. Provider acceptance and the child provider Session id are persisted before host-copy binding and canonical materialization. A binding or materialization failure therefore leaves provider_accepted; retry continues only local work and never reissues the provider mutation.

prepared recovery safely continues dispatch because the durable marker proves the provider call has not begun. A crash in dispatching becomes unknown and is never automatically redispatched. provider_accepted recovery retries only local binding and materialization. The canonical commit consumes the frozen snapshot without re-proving the live prefix, remaps all session-scoped ids, normalizes a live boundary to settled/interrupted, supersedes copied pending Interactions, persists lineage, and emits the complete committed delta.

3.2 Turn

One user submission or provider continuation belongs to one canonical Turn.

submitted -> running -> waiting -> running -> settling -> settled

Terminal outcome is independent from phase:

completed | failed | canceled | interrupted

Cancellation targets an exact Turn. cancel_requested, provider confirmation, and canonical settlement are distinct facts; UI must not manufacture an early terminal outcome.

3.3 Interaction

An Interaction represents an approval, question, or plan confirmation that requires user handling:

pending -> answered | superseded

Actionable UI reads canonical pending Interactions only. A transcript tool row showing waiting_input does not create answerable state.

A provider adapter must publish interaction.requested with the complete immutable input required by the action surface. When a provider splits one request across ordered wire frames—for example permission identity and options first, then the matching question body in a tool update—the adapter correlates them by exact Turn and tool-call identity and delays Interaction publication until the input is complete. A later transcript tool update cannot repair an already-persisted incomplete Interaction. The adapter must also validate that the provider wire can represent every answer allowed by the published surface; an unsupported shape fails before publication instead of exposing a lossy Interaction. This publication gate applies equally to emitted events and SessionState.PendingInteractive; a snapshot must not leak the incomplete request while the adapter waits for the later frame.

Approval detail presentation is provider-neutral. The daemon carries generic url/uri and prompt/instruction display fields into the canonical toolCall.input when a provider reports them only at the tool-call root, and AgentGUI renders those fields from either canonical nested input or compatible root input. Shared and local surfaces use this same projection; they must not branch on a provider or tool name to recover web-action details.

A child Interaction may appear in the root conversation, but submission carries the exact (agentSessionId, turnId, requestId) tuple. Every AgentGUI, Message Center, Desktop notification, and Mobile action submits that tuple plus the user's current answer through engine.submitInteractionResponse. The Engine allocates command identity, applies the shared 30-second timeout, rejects non-pending or in-flight targets, and recognizes an explicit exact repeat after a confirmed failure. It does not silently reuse the previous answer when a caller omits fields, and it does not retry delivery-unknown responses. Plan-implementation actions remain their specialized plan-decision workflow rather than masquerading as ordinary Interaction responses.

Free-text approval feedback is a non-terminal rejection. When an approval offers both deny and a terminal deny_and_stop/abort option, the feedback surface submits the non-terminal deny option and preserves the full feedback payload; abort remains a separate stop action. A provider adapter first acknowledges that exact approval response, then carries the feedback through the provider's active-turn guidance protocol using the exact provider thread and Turn identities. Waiting for that guidance acknowledgement must not turn a successfully answered Interaction into a failed runtime operation.

3.4 Goal and operations

Goal is a Session-level durable entity, not a Turn command. It owns desired/observed state, revision, and an independent operation.

A Goal operation may produce zero or more provider Turns, but it cannot reserve or fabricate Turn IDs. Goal control bypasses the prompt pipeline and does not create a user transcript Turn message. AgentGUI dispatches goal/controlRequested; the workspace Engine owns the provider-neutral goal/control command, typed effect execution, and exact Session-scoped result reconciliation. AgentGUI may project its durable session audit as a dedicated goal-control timeline row; that row has no Turn ID and does not participate in Turn counts, processing ownership, cancellation, or settlement. When the audit carries a user-visible command body, the dedicated row also uses the shared user-message action surface, including copy.

When a session-level timeline row occurs chronologically between two rows from the same Turn, transcript presentation keeps one Turn group and renders the session-level row as an interstitial item. This presentation grouping does not assign the row a Turn ID or make it lifecycle-owned by that Turn.

Host owns recovery for runtime operations, Goal operations, and the reconcile inbox. An adapter must not start a second worker or state machine.

On daemon restart, Host recovery first restores durable operations, then settles unrecoverable active Turns as settled/interrupted and supersedes pending Interactions.

Codex's restored Full access warning is presentation-only, device-local safety chrome. Show it only when an empty home composer restores an unacknowledged Full access target default; do not show it for another provider or permission mode, an active or historical Session, or while defaults are loading. Explicit Full access confirmation and “Don't show again” persist the same browser-local acknowledgement, while the close action affects only the current mount. This acknowledgement must not enter Session lifecycle, target defaults, Workbench node data, or AgentGUIRuntime state.

3.5 Edit retry and effective history

Edit retry is a Host-owned lifecycle operation, not a transcript mutation and not a GUI-orchestrated pair of provider calls. It is available only for the latest settled root user Turn when the provider exposes authoritative effective history and the Session has no active or child work. The command carries the exact Turn id, expected history revision, edited text, and a caller-stable operation id.

AgentGUI edit intent
  -> agent-activity-core typed command + stable client operation id
  -> Desktop transport adapter
  -> tuttid HTTP adapter
  -> Host durable edit-retry operation
  -> provider rollback/read/start capability
  -> SQLite effective-history transaction
  -> committed activity invalidation
  -> Desktop semantic event normalization
  -> agent-activity-core state-and-message reconcile
  -> AgentGUI canonical projection

Host checkpoints before provider mutation and treats provider thread/read as the authority after an unknown result. Confirmed rollback retracts one complete Turn from effective history but retains its audit row and file-change record. The replacement reuses the persisted structured submission and changes only its first text block, preserving attachments and other non-text input. Rollback is never represented as natural-language model input.

AgentGUI projects only three presentation states: ready, processing, and needs-action. It may expose edit controls only on the exact eligible Turn. The workspace AgentSessionEngine owns command identity, pending/failure state, recovery-action dispatch, and command-result reconciliation; React keeps only the unsent editor draft and awaits the engine-owned command settlement. An accepted transport result enters an engine-owned reconciling state and does not overwrite canonical availability. Editing stays blocked until a session-detail read confirms the returned history revision and recovery state; Desktop must not manufacture recovery actions from the command response. resend_pending and recovery_required are explicit recovery states rather than indefinite loading. GUI recovery commands delegate to Host and never choose whether rollback or replacement should be repeated. Completion and terminal Turn events request both state and message reconciliation because events are latency hints; the authoritative detail and message reads repair missed WebSocket delivery without requiring a Session switch.

3.6 Messages and ordering

A durable message has two independent ordering values:

  • sequence: presentation order assigned at creation; streaming updates do not change it
  • version: per-session mutable change cursor used for incremental updates and gap detection

Lifecycle timestamps describe occurrence time; they do not replace durable sequence. A live message with unknown Turn ownership must be completed or rejected at the boundary, never assigned an owner in GUI. Optimistic prompt and Goal-control echoes occupy a separate overlay ordering domain. They remain after all currently durable messages until canonical confirmation replaces them; their earlier submission timestamps must not move them back through durable transcript history during message projection or timeline merging.

Neither value is a history-coverage signal. A newest-to-oldest message read projects an explicit Session message window into the frontend engine: oldestLoadedVersion is the next paging cursor and the response's hasMore becomes hasOlderMessages. AgentGUI may request an older page only from that authoritative window. It must not infer older history from a non-one minimum version, a version gap, sequence, timestamps, or transcript shape; streaming updates can raise a message version without creating any older row.

The same rule applies to imported transcripts. When the provider transcript contains trustworthy user-message boundaries, the import adapter persists stable settled Turn identities before the data reaches AgentGUI. A page that starts midway through a long imported Turn must therefore retain that Turn id; AgentGUI must not depend on the leading user message being present in the currently loaded window. Imported content before the first trustworthy boundary remains explicitly session-scoped.

4. Workspace frontend engine

One (workspaceId, runtime origin) maps to one AgentSessionEngine. Panel unmount, Workbench node reconstruction, and standalone window switching must not change its lifecycle.

The engine owns:

  • canonical Session, Turn, Interaction, and Message indexes
  • pending activation/submit intents and optimistic projections
  • prompt queue, send-now, and cancel-then-send coordination
  • session mutation, Goal Control, settings, composer options, and operation state
  • workspace/session reconciliation state
  • ephemeral per-Session runtime command availability projected by the host
  • attention/read state and cross-surface selectors

The engine does not own daemon persistence, provider transport, DOM, or permanent UI layout.

When a model change invalidates canonical context-window usage, AgentGUI keeps the last rendered usage for that exact Session visible until a fresh canonical usage value arrives. This retained value is presentation-only: it is keyed by Session, never crosses into another conversation, and is never written back to the workspace engine.

Runtime command availability is session-scoped whenever one workspace engine can contain Sessions backed by different transports or access policies. The host projects available, transport/capability blocking, or the explicit agent_sharing_revoked state with the owner display label. The engine uses that single fact to gate sends, cancellation, settings, and Interaction or plan responses. AgentGUI preserves the composer draft content but disables editing and runtime-dependent actions. A revoked shared Session keeps its history and renders the package-owned owner-specific notice; it becomes available again only when the host projects the sharing relationship as active. It must not reuse the engine-wide connection state for this case, because one remote Session losing its owner or access must not disable Local Agent or another remote Session.

Mobile projects pending Interactions from the root conversation plus its child Sessions and reads each exact Engine response record for submitting/failure state. Native cards call engine.submitInteractionResponse; they do not construct response intents, recover previous answer payloads, or keep a parallel Promise lifecycle. After a confirmed failure, Mobile keeps the canonical prompt and its explicit answer controls visible instead of replacing them with a payload-free retry button. Missing provider-authored Plan options fail closed, and runtime unavailability disables the exact response without discarding composer drafts or Interaction identity.

Device connection presentation is target-scoped rather than Session-scoped. The host exposes a target connection source keyed by agentTargetId with the current status and retry attempt, and AgentGUI reads the active conversation target or the selected Home target. This lets a new-conversation composer show and enforce connection state before any Session exists. Session runtime availability remains the independent command safety gate for existing Sessions; it is not the source of device connection presentation.

Sharing eligibility is a parent gate for device connection. When an owner has revoked a Session's shared Agent relationship, the host projects agent_sharing_revoked and does not manufacture a target transport state for that relationship. Device reconnect and automatic retry presentation begins only while the sharing relationship is active.

Outside an exact pending Interaction, AgentGUI projects a blocked target connection through the chrome above the composer and gives it precedence over other non-interaction recovery notices because ordinary Composer writes cannot complete while the target is blocked. An explicitly terminal unavailable state appears immediately. Initial connecting appears only after a 300-millisecond controller delay so short background connections do not flash. A recoverable host retry, including a dormant low-frequency retry, remains a neutral connecting presentation and updates the visible retry attempt without restarting the delay. During the initial delay, the raw target state already blocks commands, but AgentGUI keeps the existing non-interaction recovery chrome visible until the connection notice replaces it. Recovery removes the notice without a success banner. The notice does not offer a manual retry because transport recovery is host-owned.

When the Host supplies readiness for the exact approval or interactive prompt being presented, that readiness result is the sole transport-chrome and early write-admission authority for the card. Target connection and exact-Turn observation gaps continue to gate ordinary Composer commands, but neither may hide the card or override an exact ready or blocked interaction result.

Session presentation derives one canonical Composer gate from target connection, Session runtime availability, provider readiness, ownership, activation, Interaction, and busy/queue facts. The gate is one atomic projection with separate editor, submission, and runtime-command decisions; AgentGUI must not place canSubmit, target-connection blocking, or Session-runtime blocking in independent memoized view slices and recombine them later. The editor, send action, keyboard submit paths, Stop availability, and Interaction submission consume that same gate snapshot. Busy work may project queue submission while keeping the editor editable. Draft emptiness, upload progress/failure, project existence, and other draft-local conditions may disable submission, but must not change editor editability.

Engine submitting and unconfirmed-submit selectors remain Turn-admission facts after a canonical Session first appears. Session existence or an available runtime must not create an idle frame in the Composer spinner before the exact Turn claims the submission, but these facts do not create execution busy, Conversation working, or queue eligibility. A viable new-Session activation with initialTurnExpected remains the same Turn-start bridge while no canonical latest Turn exists. Goal-only activation deliberately does not set initialTurnExpected, because Goal Control does not synchronously create a Turn. A viable initial Goal set whose projected Goal is still active remains a Turn-start bridge only while the Goal is optimistic or the host supplies an exact pending/applying/unknown goalSyncState with a pending operation identity. A synced Goal proves only that the Goal mutation converged; it does not prove that a future Turn will exist. The first canonical Turn, a synced or non-active Goal, failed/diverged synchronization, pending/applying/unknown without an operation identity, or a canceled/failed activation releases the bridge; initial Goal clear never creates it. A host that omits goalSyncState cannot prove post-create execution and therefore fails closed to the canonical availability projection instead of keeping the Composer spinner active indefinitely. When a provider exposes an exact session-level running/idle observation before Turn identity, the daemon projects that typed, non-persistent runtime activity through agent.activity.updated after the associated state report wins canonical ordering. Desktop and Mobile Live consume the same event variant. The workspace Engine uses its occurrence time to reject reordering and prevents an older running observation from overriding a settled Turn. A disconnect clears the ephemeral observation, while canonical Turn state remains authoritative once it exists. AgentGUI consumes the Engine's fenced Session display status after the canonical Session exists; it must not recompute Composer busy state from the raw runtime activity flag, because that would let an older running observation outlive a completed or failed latest Turn. Raw runtime activity may bridge presentation only before that canonical Session projection exists.

4.1 Read/write rules

  • reads use exported selectors or memoized AgentActivitySnapshot
  • an engine subscriber notification is an invalidation signal, not a render command. Concurrent AgentGUI surfaces subscribe through exact Session-family or target selectors; a selector preserves its selected reference when another root Session changes
  • AgentGUI transcript presentation subscribes to projected messages for only the selected root Session and its current child Sessions. This projection includes optimistic message_delta text before durable confirmation, while preserving message-array references for every unrelated Session
  • whole-workspace AgentActivitySnapshot projections remain valid for bounded aggregate reads, but do not belong in high-frequency AgentGUI render paths. Event callbacks that need current canonical data read the engine snapshot at event time instead of retaining a whole-workspace render snapshot
  • optimistic message_delta invalidations are coalesced on the host-injected scheduler; the Desktop bridge queues matching session-message events and flushes them at the same boundary so the rendered snapshot and event fan-out cannot observe different prefixes. The external-store subscribe and getSnapshot callbacks must remain referentially stable across renders
  • lifecycle writes use semantic Engine operations or typed intents/commands
  • composer-option reads use engine.loadComposerOptions; the Engine owns request identity, signature-aware cache reuse, identical in-flight joining, supersession, exact settlement, caller abort, and disposal. Desktop and Mobile retain transport execution in their EngineExtensionCommand adapters and delegate pure generated-DTO projection to @tutti-os/agent-activity-tuttid-adapter
  • A composer-option failure with no signature-matching cached value remains an explicit Engine error state. AgentGUI renders a compact failure and retry action in the existing options slot instead of presenting an empty list or a stale/default selection; a last valid cached value remains renderable while its load status is reconciled. The retry is the same semantic engine.loadComposerOptions operation with force left false, so request identity, cache reuse, and identical in-flight joining remain Engine-owned; a view must not call the transport adapter directly.
  • an acknowledged home Composer default remains an optimistic draft until a later authoritative Composer-options response reports the same effective field value. A successful read alone must not retire the draft because a slow or overlapping provider discovery may still return an older default. When a settled response omits that field, AgentGUI keeps the optimistic intent but releases its confirmation marker so providers that cannot project the field do not remain on permanent forced, uncached discovery. Concrete conflicting authority is retried only for a bounded number of confirmation reads; if a provider keeps normalizing, rejecting, or overriding the default, AgentGUI protects the optimistic intent from generic option sanitization only while that confirmation marker remains active. Releasing the marker lets later settled options apply normal sanitization and return to the Engine's signature-aware cache
  • the Engine alone translates shared activation, prompt send, settings update, Goal Control, turn cancel, Interaction response, rename, pin, and batch-delete commands into AgentSessionEffectPort calls. Desktop and Mobile effect ports retain transport, product integration, and host-specific DTO mapping. Their create/send request and Turn response projections delegate to @tutti-os/agent-activity-tuttid-adapter; they must not duplicate a command-type switch for these shared effects. Host activity facades call engine.activateSession, engine.submitPrompt, engine.controlGoal, engine.stopSession, engine.updateSessionSettings, engine.renameSession, engine.setSessionPinned, and engine.deleteSessions; these deep methods own the applicable workspace projection, protocol or mutation identity, timeout, cancellation, settlement, and canonical result projection. Settings update is fire-and-observe intent admission rather than a settlement Promise: the existing settings-operation selector remains the source of pending, failed, and unknown state. Hosts must not reconstruct mutation protocol with reducer intents and snapshot reads or construct raw session/settingsUpdateRequested fields for an existing Session. Session stop is also fire-and-observe admission: the Engine owns its command identity, 30-second cancellation timeout, duplicate fence, and 30-second first-Turn waiting window. Desktop AgentGUI and Native Mobile call engine.stopSession and never construct raw session/stopRequested fields. Existing-Session Goal Control is also fire-and-observe admission. The caller proposes one stable clientSubmitId; the Engine admission returns the effective identity used for settlement. The Engine owns workspace scope, command identity, the 30-second transport timeout, one in-flight operation per Session, optimistic Goal projection, typed result validation, and delivery-unknown reconciliation. AgentGUI reads selectSessionGoalControlPresentation and never stores a parallel optimistic Goal or settles the transport Promise itself. Desktop and Mobile effects send the request and map the authoritative Session/Goal/operation evidence only. Session activation enters through engine.activateSession. Desktop AgentGUI and Native Mobile keep product-specific target selection, placement, initial content/settings, and stable request identities, while the Engine owns workspace scope, timestamps, construction of activation/requested, the accepted result, and one 120-second confirmation window. The confirmation deadline is later than the 90-second new-Session command timeout so a valid slow runtime startup cannot expire while the command may still succeed. The typed activation effect returns either the created authoritative Session or the resumed Session's authoritative detail aggregate. The Engine validates the versioned activation-v1 result contract, result scope, mode, and every nested Session/Turn/Interaction entity, then projects the aggregate in its own drain. Desktop and Mobile effects retain transport, DTO mapping, and product-local integration/observability concerns, but must not pre-dispatch those projections. Canonical monotonicity guards prevent a late activation response from regressing newer realtime state. A failed new-Session activation is a rollback signal only when the Engine's canonical Session selector confirms that no Session entity exists. If the Session exists but runtime startup or the initial Goal failed, the Session remains selectable and the failure is rendered as Session detail state. An explicit historical-row selection has higher priority than stale activation failure metadata; it first requests detail reconciliation and only returns home after an authoritative not-found result. Surfaces clear a new-Session draft only after activation admission succeeds. If an admitted new-Session activation is canceled before canonical Session confirmation, the surface restores the submitted draft only while the current draft is still empty, releases the pending clientSubmitId, and allocates a fresh identity for the next explicit submission. Existing-Session Prompt submission enters through engine.submitPrompt. The surface keeps the stable clientSubmitId used by its draft-recovery and idempotent-retry bookkeeping; the Engine owns workspace scope, timestamps, routing protocol, the shared 120-second confirmation window, and the accepted/queued admission result. Desktop AgentGUI and Native Mobile clear a submitted draft only after that result confirms admission, and never construct raw submit/requested fields or rebuild admission from selectors. New-Session initial content continues to travel with activation. Provider acceptance protects provider-turn identity and fork safety, but it is not the prompt's durability boundary. If runtime delivery is explicitly rejected or times out with an unknown outcome, Host persists the submitted content and clientSubmitId independently of the canceled request. Existing sessions then receive a terminal failed Turn or an uncertain-delivery claim; only an unobserved provisional activation may be compensated away. Platform-only commands remain in each host's EngineExtensionCommand adapter. Every effect propagates its typed Engine origin and Engine-owned AbortSignal to its transport. Rename, pin, and delete settle through the shared Session-mutation state. Rename and pin may update canonical Session state only from a validated authoritative Session result. Delete may remove canonical Sessions only from a validated SessionDeleteMutationResult, projected as session/removed tombstone intents. Caller cancellation aborts the host effect, but an already accepted write remains delivery-unknown until later canonical reconciliation; it is never converted into a confirmed failure. Direct settings changes, post-activation persistence, and prompt-required settings share one per-Session Engine lane. Owner boundaries are serialization barriers rather than coalescing opportunities. A validated precondition updates canonical Session state before the Engine starts send, while a failed or timed-out precondition prevents delivery. A timed-out settings write remains delivery-unknown and does not release queued writes automatically. A fresh explicit settings selection is the user's retry: engine.updateSessionSettings recognizes the exact Engine settings-operation state, so Desktop AgentGUI and Native Mobile do not derive retry flags independently. Ordinary approval and question answers similarly enter through engine.submitInteractionResponse; the Engine owns exact Interaction identity, command identity, timeout, deduplication, and confirmed-failure retry admission. Surfaces submit the current explicit answer and never dispatch interaction/responseRequested or reconstruct a previous answer themselves
  • consumers do not read reducer maps directly
  • consumers do not create canonical session/message mirrors
  • optimistic records define confirmation, rejection, timeout, and uncertain-delivery paths
  • business command completion returns to the engine as a result intent; controllers do not rebuild lifecycle with Promise/effect chains

Edit-and-retry availability is an exact SessionEngine projection, not a fact inferred from transcript order. AgentGUI edits only the authoritative eligible latest Turn, preserves attachment and non-text blocks through the Host-owned submission envelope, and never optimistically splices the transcript. After the command is accepted, Desktop applies effective history through the composite authoritative snapshot; AgentGUI only renders that canonical result. An authoritatively retracted initial optimistic prompt is marked on its pending activation so it cannot be materialized again while activation metadata and turnless controls remain intact.

4.2 Historical pull and realtime push

  • list/history reads use session/snapshotReceived and do not create unread completion
  • newest-to-oldest reads attach their authoritative message-window coverage to the same snapshot intent; incremental/realtime updates preserve that coverage
  • realtime authoritative entities use upsert intents
  • optimistic message_delta updates invalidate the exact Session projection; they do not write an unconfirmed message into canonical Engine state. Terminal message_update reconciliation replaces that optimistic projection
  • an authoritative Session detail result enters through session/detailSnapshotReceived; agent-activity-core expands the root Session, Turns, child Sessions, and optional message coverage in one engine drain and one subscriber notification. Desktop and Mobile use the same @tutti-os/agent-activity-tuttid-adapter aggregate mapper and must not dispatch each entity independently
  • Desktop and Mobile execute Engine-owned session/reconcile commands through the same activity-core Session reconcile executor. The executor owns scope, cursor/window, pagination, double-detail race closure, cancellation/deletion fences, and atomic application; hosts own transport, DTO mapping, diagnostics, polling, and presentation side effects. A shared-agent detail open also re-materializes a durable binding into the local Engine when its cached binding exists but the local runtime projection is absent; this repairs local state and does not mutate historical data.
  • every daemon Session response carries the required messageVersion high-water cursor. Daemon and renderer ship as one protocol unit, so the shared adapter rejects a missing or invalid cursor instead of fabricating zero or entering a compatibility read path
  • Desktop and Mobile use the same host-neutral event-observation helper to decide whether normalized entities can apply inline and whether the exact Session needs an authoritative state or message reconcile; partial parsing, identity mismatch, count mismatch, or cursor disagreement always reconciles
  • message updates fold inline only when unseen versions are continuous
  • version gaps and reconnects trigger incremental message reconciliation for hydrated Sessions
  • Turn, Interaction, and legacy state invalidation trigger authoritative Session reconciliation
  • realtime provenance survives until the authoritative result reaches the engine; fetch failure must not downgrade it to historical

4.3 Root and child hydration

Workspace lists show root Sessions only. A root detail read also returns nested child Sessions; the engine stores every entity, Rail selects roots, and timeline/Message Center selectors aggregate descendants.

Child Session discovery does not require every host to prefetch child transcript pages. A surface that does not render child conversations keeps the hierarchy and pending Interaction state canonical without paying for unused child message history.

The shared reconcile executor exposes an explicit message-hydration policy. Desktop selects Session-hierarchy hydration for its aggregate projections; Mobile selects requested-Session hydration while it does not render child transcripts. The first read remains a bounded newest-first page. Later detail reconciliation compares each hydrated Session's required messageVersion with the largest locally cached durable message version; an unchanged child performs no message request. Newly discovered children use the bounded newest-first read, while already hydrated children that advanced use incremental reads. A known empty child window is an authoritative durable cursor at zero, so its first later messages are drained incrementally from afterVersion=0; it is not treated as an unknown window. The second detail read catches a root or child that advances while the first message pass is in flight. Transient optimistic rows never advance this cursor. This child policy is separate from the root conversation's user-boundary repair policy, which may intentionally restart an incremental read at zero.

The discovery detail request uses the daemon's messageHydration projection. That projection retains the hierarchy and messageVersion cursors needed by the shared executor but does not resolve provider-backed lifecycle capabilities. The final detail request uses the full projection and is the only read in one combined reconcile that may perform a historical provider capability probe. Desktop and Mobile derive this choice from the same activity-core request purpose; host adapters must not infer it from timing or add a TTL cache for capability results. The detail response echoes the selected projection and an explicit lifecycle-capability projection flag. The shared tuttid client rejects mismatched responses, and consumers must not treat false-valued capabilities from an unresolved hydration projection as authoritative.

A waiting Turn does not imply user action. Only a pending Interaction produces approval/question attention.

4.4 Prompt queue

The busy-session prompt queue is ephemeral durable-intent coordination in the workspace engine. It is neither a daemon queue nor component state.

  • a normal prompt waits for canonical availability
  • a queued-prompt Send next action uses native guidance when the provider supports it, even when the provider also supports interruption; guidance stays on the same canonical Turn and follows the provider's native semantics
  • when an active-turn Send now arrives before the Session has an authoritative capability snapshot, the workspace Engine retains the exact prompt and send-now decision without dispatching guidance or cancellation. A later authoritative snapshot resolves that same intent to native guidance or cancel-then-send. An explicit complete snapshot that supports neither keeps the prompt in the ordinary queue until canonical availability returns; a missing snapshot must never discard the prompt or be treated as unsupported. Each queued prompt owns its deferred decision and exact target Turn; a later Turn must not inherit guidance or cancellation intended for its predecessor
  • Claude SDK guidance acknowledges delivery only after its SDK interrupt has succeeded and the guidance prompt has been enqueued; a failed interrupt is a failed guidance request and must not allow the old response to keep running. The confirmed interruption settles the old response's streaming projections while the canonical Turn remains active for the guidance response
  • Codex/Tutti Agent guidance sends turn/steer with the exact active provider Turn identity. Steering inserts the guidance into the current provider Turn; it does not interrupt the response or start a replacement provider Turn. If the provider response has already ended while the canonical Turn remains active for child work, guidance may start a provider continuation with turn/start on that same canonical Turn
  • guidance captures the canonical activeTurnId at the interaction boundary and carries it through the queue, activity adapter, daemon API, Agent Host, and runtime Controller; turnId is required for every cross-process guidance request and is never inferred from the latest Session snapshot
  • Host and the runtime Controller compare that target with the live active Turn while holding the lifecycle admission lock. A mismatch is a typed pre-provider rejection (NotDispatched), and a prepared submit claim is removed so the failed guidance cannot strand the queue or be redirected to a newer Turn
  • otherwise send-now performs exact cancel-then-send
  • user Stop pauses the queue; cancellation must not leak the next prompt
  • a durable pending-submit record does not by itself make queued backlog stoppable. A queued prompt without a Turn becomes a pending-submit Stop target only while that exact queue prompt is in flight or has uncertain delivery; visible or suspended backlog must not keep the Composer Stop spinner alive
  • a prompt settings precondition is an explicit preparation stage, not a nested host effect. It serializes with direct and post-activation settings writes, updates the canonical Session on success, starts send before releasing later settings writes, and fails the logical prompt without delivery when the settings result is not valid
  • a visible failed queue entry continues to own its submitted content for retry; draft settlement must not duplicate that content back into the composer
  • uncertain delivery reconciles by clientSubmitId and exact turnId; it never resends merely because the Session appears idle
  • editing a queued prompt restores its stable attachment references, then rehydrates missing image previews through AgentGUIRuntime with the exact workspace and Session identity; renderer-inaccessible paths never become image URLs, and late reads may update only the matching restored draft image
  • rebuilding a Composer draft from queued canonical Markdown resolves mention labels and icons in each AgentMentionNodeView through the nearest workspace-scoped RichTextMentionService; every NodeView subscribes only to its canonical mention identity and keeps presentation out of the Tiptap document, while missing, failed, or removed services fall back to the canonical label and semantic icon. Agent target catalog presentation remains authoritative over service fallbacks
  • the delivery barrier serializes new-Turn sends only; a guidance head steering the running barrier Turn is exempt and may steer it repeatedly, while in-flight, uncertain-delivery, suspension, and failed-head blockers still gate guidance sends
  • drain readiness is one pure decision over the queue record and canonical availability; a new blocker joins that single decision with an explicit priority against every existing blocker, never as another independent pre-check in the drain path

4.5 Rail query and presentation state

The headless AgentGUIConversationRailQueryController is the single cross-platform owner of Rail query scope, first-page refresh, cursor pagination, stale-request fences, membership reconciliation, and Engine ingestion. Desktop and Native Mobile both construct it through createAgentGUIConversationRailQueryController, the canonical factory exported by @tutti-os/agent-gui/conversation-rail-controller; a host must not instantiate the internal implementation or recreate that state machine in its app layer.

The Rail query cache stores section metadata, ordered Session IDs, cursors, and totals only. Each first-page or pagination response passes Session DTOs transiently through the host mapper into Engine upserts, while the Rail snapshot retains only memberships, ordered IDs, cursors, totals, loading, and failure state. Refreshing a bounded Rail page is not deletion evidence and must not replace or prune canonical Engine entities.

The public headless snapshot contains query and membership state only. Desktop-localized conversation summaries are projected from the snapshot plus canonical Engine state outside the controller, so the Native entrypoint does not depend on Desktop presentation or locale bundles. The public factory accepts only the Engine, active-conversation identity getter, canonical runtime queries, workspace identity, and small scheduling/page-size ports; cache records, diagnostic trackers, and request-generation seams remain package internals. Surface identity such as a Desktop AgentGUI nodeId is adapter-owned diagnostic context: the Desktop runtime adapter enriches diagnostic payloads instead of passing it into the headless controller interface.

The full AgentGUI surface reads the optional AgentGUIRuntime.conversationRailQueryLimits.sectionRefreshLimitMax backend contract and passes it to that same controller. A host whose section endpoint accepts less than AgentGUI's default refresh limit must declare its positive maximum instead of relying on transport rejection or adapter-side truncation.

Resolved query results may be reused from the workspace cache. In-flight first-page entity payloads are controller-generation scoped and must not be shared across mounted controllers: detach, pause, or a scope change must fence both Engine ingestion and cache writes from the obsolete request. The canonical factory owns one resolved-query cache per workspace Engine, so Desktop and Mobile receive the same remount semantics without exposing cache access through AgentGUIRuntime or a host adapter.

Hosts own the transport adapter, DTO mapping, runtime-availability policy, and surface lifecycle. For example, Mobile owns disconnected polling and foreground/background pause-resume around the shared controller. Native hosts also own their renderer, localized status projection, and interaction layout; those host concerns must not leak back into the shared query controller.

Mobile DeviceLink recovery is application-scoped rather than screen-scoped. MobileApplicationService is the single owner of the paired-device connection phase (idle, reconnecting, synchronizing, connected, or failed). Native stops Agent Live immediately on background entry while retaining the underlying DeviceLink for its background grace interval. Every Native-to-JS live delivery carries the caller-owned subscription generation; the bridge and workspace live lane reject deliveries from a closed generation before they can reach the coordinator. JavaScript records the background entry time and uses elapsed time on foreground instead of relying on a timer that the runtime may suspend. Foreground resume queues canonical workspace and selected-Session reconciliation before opening the replacement live lane. An unexpected live-lane disconnect first allows one bounded stream retry, then rebuilds DeviceLink if the stream remains offline. Runtime commands stay blocked until the replacement live lane reports ready.

Recovery retains the current device, workspace, navigation, and drafts behind a global blocking presentation. A replacement workspace scope is started and authoritatively hydrated before it atomically replaces the paused scope; a failed candidate cannot clear the visible conversation. The App-root overlay only projects the service phase and exposes retry or explicit return-to-device commands. It does not own timers, transport checks, retry policy, or copied connection state.

Cross-platform hosts may reuse the DOM-free canonical Rail summary projection from @tutti-os/agent-gui/conversation-rail-projection. They must still obtain ordered membership, project labels, totals, and cursors from the authoritative section query and join those IDs to canonical engine Sessions. Native hosts own their renderer and interaction layout; they must not import Desktop or Web components, infer project membership from cwd, or create a second Session lifecycle store.

AgentGUI's existing-Session project projection uses the authoritative railSectionKey as its only join key: it matches that value exactly against a registered user project's sectionKey. The key is immutable under runtime reports, cwd changes, and user-project list updates. The conversations key, a missing key, or an unknown key fails closed to no project label even when the Session cwd equals or sits below a registered project path. Conversely, a recognized project key keeps its project identity when the runtime cwd is an isolated worktree or another detached execution directory. Path matching is reserved for resolving the user's explicit project selection before a new activation; it is not an existing-Session compatibility fallback. The Remove project action delegates one awaited operation to the daemon. The daemon deletes every unpinned root through the canonical Host batch path, keeps pinned Session trees, atomically rehomes retained rows to Chats, and only then removes the user-project row. Failure keeps the project visible for retry; the renderer does not compose candidate-query, Session-delete, and metadata-delete requests itself. Native Mobile applies the same rule to Activity labels and Rail placement. Section sessionIds remain query membership, page-boundary, initial-order, and hydration data. After joining those memberships to current Engine entities, hosts sort loaded ordinary sections by the canonical conversation sort key: the latest Turn start time, falling back to Session creation time. Streaming freshness, status, and completion updates do not change that key, so parallel running Turns do not churn Rail positions. Pinned sections preserve the query's pinned order. If a section disagrees with a Session's canonical railSectionKey, the host rehomes the row by the Session key instead of accepting the stale membership; presentation reordering must not mutate cached memberships or pagination cursors.

Cross-platform hosts may also reuse the DOM-free canonical transcript projection from @tutti-os/agent-gui/conversation-projection. It accepts the canonical activity snapshot, selected Session id, and known Turns. The projection derives both the Session and its messages from that one snapshot, hides intermediate AgentGUI activity-card/timeline-item construction, then returns the same AgentConversationVM that AgentGUI renders for message merging, thinking, tool groups, processing, notices, and turn summaries. Its portable navigation resolver covers only external URLs and Agent Session mentions and depends on focused host-neutral parsing primitives shared with the broader Workspace resolver; file, local-asset, app, issue, and custom-mention actions remain host capabilities. Projection code in this entry must depend on focused pure helpers for persisted pasted-text presentation and file-path validation; importing the interactive Composer model or the broad Workspace link resolver would pull Web-only editor, UI System, issue, and file-manager types into Native consumers. Native hosts own their renderer, localized fallback copy, scroll position, and temporary disclosure state; they do not reinterpret raw message kinds or import the DOM transcript. Approvals, Plan choices, and questions remain canonical pending Interactions, are projected through the shared Interaction-to-Prompt seam, and continue to submit exact option ids through semantic Engine commands rather than through transcript rows.

Cross-platform hosts may reuse the DOM-free Composer policy from @tutti-os/agent-gui/composer-projection. Composer-option loading remains an AgentSessionEngine semantic operation keyed by the exact Agent Target. Both Desktop and Mobile call engine.loadComposerOptions; the generated tuttid DTO is mapped once by @tutti-os/agent-activity-tuttid-adapter into AgentActivityComposerOptions, while the host extension adapter remains the transport seam. Hosts render provider-authored options and use the shared support projection, but keep Native/DOM menus and temporary open state local. Existing-Session setting changes enter through engine.updateSessionSettings. The Engine allocates command identity, fixes timeout and retry policy, and translates the semantic call to its internal session/settingsUpdateRequested intent. New-Session draft settings travel on the activation intent instead; provider-independent draft projection and Desktop-persisted defaults remain surface policy until activation. A renderer must not call the settings endpoint from a component or invent a provider-specific settings schema. Desktop and Mobile project the broader Engine settings through one generated-contract allowlist before composer-option or existing-Session settings requests. Both preserve supported fields such as browserUse; neither sends computerUse until OpenAPI adds that request field. Existing-Session Prompt sends similarly enter through engine.submitPrompt; Desktop and Mobile provide content plus a stable client submit identity, while the Engine owns common routing, confirmation expiry, and admission projection. New- and existing-Session activation enters through engine.activateSession. The surfaces retain target selection, placement, initial draft projection, and stable request identity; the Engine derives workspace and timing fields, projects the typed intent, and reports whether it was admitted. Actual Session creation, resume, and initial Turn lifecycle remain owned by Agent Host behind the host effect port.

An activation intent's shared Session settings are not an HTTP create-field allowlist. Each host must construct a typed CreateWorkspaceAgentSessionRequest and forward only fields present in the generated contract. Both hosts preserve browserUse, which is a supported create field. In contrast, computerUse is a default-on runtime setting but is not currently a create-request field; neither host may add it as an extra property. Supporting an explicit first-Turn opt-out requires changing OpenAPI and the create adapters first.

Desktop and Mobile construct the headless controller through @tutti-os/agent-gui/conversation-rail-controller and supply its narrow query/diagnostic runtime port. The shared factory owns the workspace-scoped cache lifetime while transport adapters own only protocol mapping, authorization, and host-specific diagnostic context. Hosts that expose the full AgentGUI mutation surface additionally install the complete query/mutation cohort from @tutti-os/agent-gui/conversation-rail-runtime. Batch deletion requires both authoritative section candidate lookup and the batch mutation. AgentGUI fails that paired capability closed when either method is absent, so the view cannot expose an action that will resolve to an empty optional-method path.

Conversation deletion is transactional from the renderer's perspective. The canonical delete command must succeed before AgentGUI clears the active Session, changes Rail selection, unactivates runtime state, removes rows, or discards local drafts. The same rule applies to batch deletion: a protected Tutti execution conflict leaves the whole selection and local collection unchanged. The conflict parser recognizes only the daemon's typed tutti_execution_active payload; archive/view commands remain an optional host capability until the generated execution adapter is present.

Conversation Rail Activity View

AgentGUIRuntime.conversationActivityViewEnabled is the fail-closed host boundary for the Conversation Rail Activity View. Desktop opts in explicitly; external hosts that omit or disable it retain the ordinary Rail unchanged.

The view is a presentation-only activation over the current workspace Engine: it reads visible root Session summaries, each root Session's own lifecycle status, root-conversation attention/read state, and already-cached Session messages. Descendant lifecycle activity remains available in the conversation detail and interaction surfaces, but it does not change the root row's Rail status. A pending descendant Interaction still contributes canonical root-conversation attention so an approval or question cannot disappear with the filtered child row; submission retains the exact child identity. Opening Activity View must not call list pagination or transcript hydration. Its membership input is the canonical mounted Engine summary collection, never the ordinary Rail's transient runtimeRailConversations overlay; stale page projections therefore cannot leak into Activity View. Membership and recency are snapshotted when the view opens; subsequent Engine pushes reconcile incrementally, while deletion removes a member immediately. The Activity controller owns that activation and its retained row cache; React render only consumes the controller snapshot and projects live row facts. Existing Priority member IDs and their relative order survive ordinary Rail refreshes that temporarily omit a summary; the controller uses the last known row summary until the next toggle or an explicit canonical tombstone. Selecting a historical Session that is temporarily injected into the visible summary list does not make an idle Session a newly discovered Activity task; only its live waiting, unread, or active facts can admit it to Priority. The Engine preserves this distinction at the Turn boundary: turn/upserted uses live provenance. Historical detail hydration writes canonical Turns with live: false and cannot create new unread attention; realtime projections and live reconcile writes use live: true. Omitted live remains compatible with older hosts and is treated as live, while historical producers must pass false. Existing durable read and unread markers remain authoritative during hydration; only a read marker synthesized by the current historical observation may be upgraded by a later live observation for the same completion. The activation is scoped by the workspace, authenticated user, rail filter, AgentGUI node, and Engine identity, not by the currently selected Session's provider or target; selecting a row must not rebuild a cross-provider Activity queue. Search temporarily takes over the content area and clearing search restores the same activation. Closing the view discards the activation and its retained row cache. Existing workspace, authenticated-user, rail-filter, AgentGUI-node, and Engine identities fence the activation internally; the selected Session's provider and target do not. No Activity filter or additional host scope contract is introduced. Disabled hosts use an empty Activity selector and do not scan root Sessions. Activity rows omit the minute-clock subscription together with their hidden timestamp. The full product contract is recorded in docs/specs/2026-08-05-agent-conversation-activity-view-prd.md.

The full first-page query is the only Rail read that resolves a navigation scope and clears its pending state. Targeted section refresh and pagination may update only an already-resolved matching scope. A subordinate result must not cancel the full query, publish partial membership for an unresolved scope, or unlock Rail interactions.

When Activity View has a non-null in-memory projection, that projection remains visible while the unrelated initial Rail membership query is pending; Rail loading, empty, or failure placeholders must not replace it. A refresh or page failure after Rail rows already exist preserves those rows and reports a non-blocking error through the host toast capability, falling back to the UI System toast when that optional capability is absent. An unresolved empty Rail scope retains the centered error state and its retry action.

AgentGUI owns one bounded retry for first-page and targeted membership reads so every host receives the same recovery behavior across daemon restarts, transient upstream failures, and transport loss. A fast transient failure retries once after a short deterministic jitter while publication remains pending. A request that already reached its timeout publishes the retained or empty failure state, releases the interaction lock, and performs its one retry after a longer background delay. Cancellation and permanent authorization, parameter, or other non-retryable client failures never retry. Scope replacement and surface detach abort both the active read and any scheduled retry; hosts must propagate the controller-owned AbortSignal while retaining their own request timeout.

Presentation-invisible Sessions remain canonical engine entities and stay available through exact Session selectors for trusted open, reconcile, and command flows. Plural consumer selectors exclude them before Rail and Message Center collection projection; a hidden Session must not become a list row just because it is resumable or receives later canonical updates.

When runtime sections are enabled, projection unions IDs from the current section, search, reconciliation, and every in-progress root Session in the exact target scope, then joins canonical Sessions. In-progress Sessions remain display overlays until the bounded section query contains them; overlay rows do not alter cursors, hasMore, or server totals. Unchanged summaries preserve structural sharing so unrelated engine updates do not rebuild the whole Rail snapshot.

A locally initiated Session creation projects its exact target identity as pending-creation reconciliation evidence until the canonical Session arrives. This covers both new-conversation activation and Session fork, and the marker is independent from any optimistic or runtime-overlay render source. When that identity becomes canonical, the Rail controller refreshes the exact persisted railSectionKey even if the new Session is currently selected. The selected historical-Session fast path remains valid only when no pending-creation evidence exists. An active or running overlay must never mask a missing authoritative section membership that would disappear after the Turn settles.

Scroll, section collapse, visible limits, and search query belong to mounted view scope. Non-search state is isolated by workspaceId + agentTargetId/all; search creates a temporary navigation scope. activeConversationId expresses selection only. Scrolling requires an explicit reveal intent.

On the Home composer, a single-Agent Rail filter follows the effective composer Agent Target whether the change originates inside AgentGUI or from host-owned node data. The all filter remains broad, an open Session keeps its current Rail filter, and unresolved/loading targets neither rewrite presentation state nor expose placeholder target labels in Home chrome.

Rail scroll memory is captured by scroll events and explicit navigation. Effect cleanup must not synchronously read scrollTop: React may already have dirtied the document, turning that read into a full layout inside the interaction task.

An empty bounded Rail result must not unactivate an active or persisted Session. Only an explicit Agent target selection may move the detail to that target's Home composer.

Contain selection and presentation identity at the Rail boundary. Each section receives the active ID only when it owns the canonical or overlay row; unrelated sections receive null so their memoized props remain equal. Rail pane, section, and row receive a dedicated Rail-label projection whose identity changes for locale changes, not provider-specific detail copy. Event handlers shared by every section keep stable identities and read the current scope and lock state when invoked.

Keep section header/action chrome independent from changing item collections. A memoized header receives scalar presentation fields and stable event-time actions; it must not receive the section object or rebuild project/session semantics. Split the header into narrow render islands. Frequently changing derived booleans such as project drag disabled, project action locked, and batch deletion disabled may cross the Section presentation boundary through separate primitive Context projections. The Rail pane owns those providers outside the memoized Section so a projection-only update does not execute item projection; only the frame, forwarded-ref button leaf, or open menu content that renders the value may consume it. Do not combine those values into one Context object or copy them into persistent state. Menu disclosure is view-local state. A conversation row keeps its context-menu root mounted so right-click remains immediate, but may defer its normally hidden direct actions and dropdown root until the row is first hovered, focused, or opened by context menu. Once activated, those controls stay mounted for stable focus and keyboard behavior. Portaled menu content exists only while that menu is open, and a closed menu has no availability-state consumer. The project header remains the native drag source, each project section updates the insertion position across its full area, and the Rail scroll viewport owns the final drop so section gaps cannot discard an already visible insertion target. This is a presentation boundary, not a second Rail or lifecycle store; stable event-time guards remain authoritative for action delivery.

Relative time uses one renderer-realm minute clock. Timestamp leaves subscribe directly; do not thread a tick prop through Rail pane/section/row and rerender the interactive subtree every minute. Fully occluded AgentGUI surfaces do not subscribe to that minute cadence. They retain the last clock snapshot while hidden and refresh from current time when visual exposure resumes.

4.6 Detail and transcript

Rail selection, detail hydration, older-page loading, and transcript projection are separate states.

Desktop and Mobile use the same renderer-neutral AgentGUI conversation-message controller for focused detail message queries. Initial hydration and latest refresh enter the Engine as Session reconcile intents; explicit older-page loads read only the Engine's authoritative message window, share one in-flight/retry/stale-request fence, and apply mapped durable pages back to the same Engine. Switching the focused Session or pausing the host aborts and generation-fences the obsolete older-page request. A failed request remains retryable at the same cursor.

The host owns mapped transport, foreground/background and disconnected-polling triggers, diagnostics enrichment, scrolling, and rendering. Canonical messages, Turns, Interactions, and optimistic prompts still come from the Engine; neither Desktop view state nor Mobile services keep an older-message entity store. An empty message list means neither hydrated nor not-found. Selecting an already hydrated Session must not start another detail reconcile; the selection controller alone decides whether hydration is missing. Composer option synchronization does not own Session detail reloads. On Desktop the selection controller also owns activation guards and Rail projection coordination before requesting idempotent initial hydration. A repeated automatic selection restoration must not become a forced refresh or append pending demand to the in-flight reconcile; explicit user refresh is a separate intent. The message paging adapter owns only focused message-controller lifetime, transport mapping, diagnostics, and initial/older commands; it does not call back into selection or Rail state. There is no separate messages-only Engine reconcile helper.

Timeline projection is pure, deterministic, and provider-neutral. React views render rows/cards and dispatch actions. Transcript Turn membership and order come only from timeline items in the hydrated message window. Session-wide canonical Turn metadata may enrich an already projected Turn by exact turnId—for example, by adding a view-only terminal error when the provider emitted no error message—but it must not create a transcript Turn that has no item in that window. The missing item normally belongs to an older page; the projection adds the error when that page is hydrated. A user_prompt Turn that still has no transcript item after authoritative full hydration is an invariant or data-repair case, not an empty row to manufacture. Goal and provider-initiated activity without transcript items use their dedicated presentation surfaces. Canonical reducers preserve entity and collection references when an accepted Session snapshot is semantically unchanged, while still merging changed Turns and Interactions at the same Session version. The legacy activity snapshot projector preserves each unchanged domain-slice projection so reconcile bookkeeping or composer-option updates do not rebuild transcript inputs.

Turn elapsed-time presentation starts at the client submission timestamp carried by the canonical user-message payload, not at provider runtime start. Historical activity without that timestamp falls back to the leading user-message timestamp.

Completed Turn disclosure collapses only process-oriented rows such as thinking, tool groups, progress, turn-boundary messages, and transient processing. Ordinary assistant content, user messages, and the response-tail file summary remain visible. The file summary owns the diff panel and stays at the end of its canonical Turn after the final assistant reply.

High-frequency transcript updates must not pair DOM mutation with unconditional synchronous reads of the timeline's full scroll geometry. Conversation switches, explicit submit-to-bottom requests, skeleton transitions, and older-page prepend restoration may perform pre-paint scroll correction.

Transcript end-following is one UI-local state machine shared by DOM, TanStack Virtual, and React Native adapters. It has only following and detached modes. User scroll-away intent detaches synchronously, before the first scroll frame. A mounted detail view retains the scroll anchor and follow-end mode for each exact Agent Session it visits. First selection follows the end; returning to a detached Session restores its retained position, while returning to a following Session stays at the end. This mounted-view memory retains at most 64 recently used Sessions and evicts the least recently used entry beyond that limit. It expires with the mounted view and never enters navigation, Engine, or Session state. Prompt submission, an explicit scroll-to-end request, or the user actually reaching the end may reattach. Content growth, layout effects, observers, virtualizer geometry, and near-end thresholds are sensors or executors only; they must not transition the mode.

Turn-level virtualization has one geometry owner. When the transcript is virtualized and the state machine is following, TanStack Virtual owns append following, streaming size adjustments, prepend anchoring, end detection, list height, and item transforms. While detached, AgentGUI disables append following so TanStack cannot bypass the shared intent owner, but retains stable-item mutation anchoring so prepending an older page preserves the visible viewport without reattaching to the end. AgentGUI retains Session selection, explicit user intent, top-page loading, bottom-dock safe-area measurement, and the non-virtualized short-transcript branch. It must not apply native scrollHeight-delta prepend compensation or a content-resize bottom write after the virtualizer accepts ownership.

The detail view reaches that owner through a narrow controller containing the exact agentSessionId, isAtEnd, and scrollToEnd. Every read or write checks the current Session identity; a stale controller is inert. Controller readiness is reported back to the detail scroll controller so initial end positioning still runs when the virtualizer acquires its scroll element after the first detail layout. Virtual measurement keys also include the exact Agent Session id, so replacing a conversation cannot reuse another Session's measured Turn height. The virtual list measures its offset from the timeline scroll origin as scrollMargin, including changes caused by the older-page loading indicator. Direct DOM transform mode owns the virtual sizer height and item transforms; React keeps only row content, measurement refs, cross-axis sizing, and disclosure spacing.

Virtualizer- or layout-driven scroll events do not change the end-following mode or trigger older-page loading without explicit user scroll-away intent. A settled timeline that is too short to fill its viewport may still request older pages.

Composer draft updates cross the view boundary through the stable shell, rail, detail, composer, interaction, readiness, and operations projections rather than one aggregate Detail prop. The active Timeline ScrollArea is a memoized render island and does not consume draft state. TipTap treats a controlled value as a local acknowledgement only when its draft scope and local edit revision match; a scope change or other external replacement may rebuild the document.

A virtualized transcript derives message-locator selection from the virtualizer's measured turn positions and explicit transcript identity. The currently mounted DOM window is rendering output, not a selection source; range changes must not make the locator temporarily select a neighboring message. Recent wheel and keyboard direction fences locator selection from reversing because of estimate-to-measurement scroll compensation or an item-list identity shift; the previous stable locator index is reconciled in a layout effect before paint. A genuine opposite input replaces that intent.

Historical rich text renders from the canonical Tiptap document through a static schema renderer. Only interactive composer surfaces own a Tiptap Editor/ProseMirror EditorView; read-only transcript surfaces reuse the same mention/token presentation without mounting editor lifecycle. Settled transcript messages reuse a bounded cache of pure Markdown ASTs and Tiptap JSON documents keyed by message identity and exact parser input; rendered React elements are never cached, and streaming Markdown bypasses this cache. Conversation titles are a separate plain-text projection: Markdown mention links are normalized to their @label text and never render mention SVGs or interactive rich-text tokens.

Fenced Markdown blocks whose exact language is mermaid use the bundled Mermaid renderer only in non-inline transcript surfaces. While the owning message is streaming, AgentGUI renders a stable diagram frame and placeholder; it does not expose the growing source or repeatedly lay out the diagram. Once the message settles, AgentGUI renders with Mermaid's strict security mode and bounded text and edge limits. Active Turn lifecycle is authoritative over an individual message or tool status: a completed tool must not settle a Mermaid block while the owning Turn is still active. Oversized, invalid, or failed diagrams render a compact failure state with an explicit source-copy action; they do not automatically expose the source in the transcript. A rendered diagram may open a presentation-local viewer with zoom, reset, wheel input, and Space-modified pointer panning. Viewer transform, disclosure, and theme-refresh state are UI-only and must not enter Message, Turn, Session, activity-runtime, or workspace-engine state. The renderer is packaged with AgentGUI and performs no runtime network fetch.

Attachment-only fallback labels such as [Image] may provide title or summary text, but they are not an additional transcript text block when the canonical structured content already renders the same image. Explicit display prompts remain transcript content and continue to replace expanded rich prompt text, except when the structured content carries the composer’s selected-text reference blocks. In that case the conversation projection preserves ordinary structured text and images, groups selected-text blocks into one leading typed compact reference part, and uses displayPrompt only as a compatibility fallback; the renderer owns the chip presentation. The selected reference is presentation-only, so copy/edit actions remain attached to the ordinary typed prompt part. This keeps the durable prompt and editing source unchanged while preventing a display-only flattened prompt from discarding the reference boundary.

Structured user-prompt transcript images keep resource acquisition and browser image decoding as separate presentation states. A URL or hydrated data source is not proof that pixels are ready: AgentGUI reserves the final thumbnail geometry and shows its loading treatment until the exact image element reports load. error replaces that slot with the retryable failure treatment. A retry of the same URL or a replacement effective source starts a new decode attempt instead of reusing stale loaded or failed state. These loading and retry states are UI-local and never enter canonical Message or workspace-engine state.

Standalone hosts may opt a transcript into participant avatars through the agent-conversation entrypoint's explicit presentation contract. Omitted or disabled presentation preserves the existing transcript DOM. Enabled presentation has distinct loading and ready states, so the renderer never infers identity readiness from a missing image URL. The host supplies user and Agent names and optional avatar URLs; AgentGUI owns the UI System Avatar, fixed-size loading slot, fallback initial, and user-right/Agent-left layout. Participant-header placement is projected from presentation turns rather than individual message, tool-progress, or canonical Turn rows. A presentation turn starts at a user message and continues until the next user message, so recovery or provider continuation may span canonical Turns without duplicating the participant header. Each speaker renders at most one header per presentation turn, and a collapsed completed canonical Turn keeps the Agent header on visible reply content. This presentation input is view data only and must not enter canonical Session, Turn, Message, activity-runtime, or workspace-engine state.

Standalone hosts rendering AgentConversationFlow may also pass the current Agent target presentation catalog through the agent-conversation entrypoint. The flow scopes that catalog to all historical rich-text rows, so agent-target and agent-session mentions use the same provider icon and identity presentation as the full AgentGUI node without copying the catalog into canonical activity data. Omitting the catalog preserves an inherited AgentGUI presentation context. The standalone flow also owns its visibility-aware conversation clock, so a hidden host does not keep elapsed-time presentation timers active.

Hosts whose action directories omit valid identities supply the complete presentation-only catalog through mentionAgentDirectory. It retains exact target IDs and unavailable Agents without making them launchable. Hosts that omit this capability keep the existing union of provider-rail and handoff targets. Interactive composer mentions, readonly rich text, and Markdown links must resolve presentation through the shared target-presentation resolver. They must not infer icons from target-id formats such as local:*; serialized mention presentation is only the fallback when the current catalog no longer contains a historical target.

5. Agent identity and provider architecture

5.1 agentTargetId is UI identity

Use agentTargetId for:

  • Agent selection and Rail filtering
  • composer-options cache
  • Workbench node state
  • new-session launch
  • Agent mentions and handoff targets

provider is execution metadata, not UI identity. Multiple Agents may share a provider; UI must not group, deduplicate, cache, or fall back by provider. Ordinary Session selection reads composer options through the existing target/cwd/settings request cache. It does not force a refresh; force is reserved for explicit invalidation, completed Session creation, and documented provider prewarm behavior.

For an active Session, the composer treats agentSessionId, agentTargetId, and provider as one Engine-owned identity projection. It must not combine a target id from the canonical Session with a provider from lagging Workbench node data. If the Engine target is not yet available or belongs to a different selected Session, composer-option loading waits instead of issuing a guessed request. The home composer continues to use its selected Agent Target as one atomic projection.

Trusted host/daemon code resolves a target-backed request through agent_targets, then derives provider and runtime reference. If a client supplies both target and provider, daemon rejects a mismatch.

5.2 Provider strategy

provider ID
  -> daemon providerregistry descriptor
  -> typed strategy / capability
  -> provider-neutral consumer

An unknown provider produces explicit unsupported behavior. Provider adapters normalize their own wire; shared renderers consume canonical message/tool/notice contracts only.

Desktop managed-provider setup reads the generated provider catalog's statusKind strategy before requesting a provider-specific runtime candidate catalog. The setup service and view must not keep a second provider-id list or branch directly on provider names.

Skill invocation follows the same boundary. Filesystem and runtime adapters discover skill identity, source, and plugin ownership; providerregistry projects the provider-authored trigger and invocation strategy. Composer and host adapters consume that projection and must not rebuild $ versus /, plugin namespaces, or prompt-item versus text-trigger behavior from provider names.

For an active Session, composer Skill discovery also scans that Session's isolated provider home. Opening slash search explicitly refreshes this catalog so newly created Skills appear without restarting the Session; runtime-managed Skills remain hidden through their .tutti-managed-skill marker.

App-server-backed skill discovery follows the descriptor boundary. Tutti Agent requests only skills/list and retains the ordinary Skill projection through the shared app-server transport, capability contract, cache, and structured prompt-item submission path.

Model-only app-server catalog probes are a separate read path: they request model/list without skills/list and pass the runtime launch preparation contract's SkipSkills flag. These probes do not create a live Session and must not resolve or materialize provider Skill directories. A Composer-options read that returns both models and Skills keeps the normal Skill preparation path.

Tutti Desktop's slash connector section is a local catalog projection, not a Provider connector catalog. services/tuttid/service/agent reads the daemon-owned connector-market application through its read-only snapshot port; the Agent service never opens or receives the underlying repository directly. It projects manifest presentation (including the catalog icon) plus installation, authorization, and compatibility into a provider-neutral capability option, and replaces any Provider-reported connector capabilities before returning composer options. This keeps installable connectors visible in the slash menu while ensuring that only installed and authorized connectors can be invoked. AgentGUI does not scan external MCP, plugin, or package-manager configuration and does not infer installation from a remote market response.

Authorization and draft selection are separate facts. Selected connectors are stored as semantic draft blocks, rendered as removable chips, and submitted as structured prompt content without synthesizing slash text. The compact Composer trigger previews installed and authorized connectors independently from draft selection, while preserving selected connectors first in that bounded preview. A matching send on a session-scoped draft keeps those connector blocks and clears the rest of the draft; home and other non-session scopes clear the full draft. /clear keeps the session enable set. A new conversation starts empty. The GUI submits the full enable set every turn; the daemon remembers the last announced set and injects only the delta into the provider turn.

The device-global lab.connectors UI-preference flag controls whether that projection is returned. The daemon fails closed when the preference is absent or unreadable and removes Provider-reported connector entries as well as local ones. Desktop invalidates cached Composer Options when the preference changes, publishes the same invalidation through the AgentGUI host event bus, and projects the host capability into the palette so stale cached connector rows are hidden immediately. Workspace and standalone AgentGUI surfaces therefore converge on the next canonical read without exposing stale entries. The flag does not uninstall connectors, stop their runtimes, or reject an already structured connector prompt.

Desktop also projects these flags through AgentGUI's existing host-owned capability-menu state. The footer renders the Connectors menu only when lab.connectors is on. The independent lab.tuttiMode flag defaults off; when enabled, the same footer renders the Tutti Mode activation switch and the slash palette exposes /tutti, including when Connectors are also enabled. When the flag is off, both Tutti Mode entry points are omitted. The same footer serves both the home hero and existing-session dock, so the two AgentGUI contexts cannot drift. The one-time Desktop Preferences migration resets any persisted lab.tuttiMode value to false; its durable migration marker preserves later user opt-ins instead of resetting the preference on every launch. The menu, selection-chip, and Palette-item implementations plus their neutral item contracts belong to @tutti-os/connector-renderer/ui. AgentGUI owns only the React-free capability projection under integrations/connector, Composer placement, draft/prompt semantics, canonical option refresh request, and Tutti Mode fallback. Its existing onCapabilitySettingsRequest host port emits the exact connector key; the host delegates that intent to Connector Market's shared open use case and mounts one window-level Connector Market dialog host. AgentGUI does not load Connector Market state, construct its Root, or implement Connector-specific visuals.

5.3 Agent Directory and setup

The host provides a complete, ordered Agent Directory with this load lifecycle:

idle | loading | ready | error

ready may contain an authoritative empty list. error may retain the last successful snapshot. Components must not infer loading from agents.length.

One-shot Desktop surfaces that emit Agent identity, such as outcome notifications, start and await their own Agent Directory load attempt before resolving presentation. They must not depend on an AgentGUI or Workbench React effect to initialize the directory.

The provider descriptor's target sort order owns the default built-in Agent order. Tutti Agent is the first built-in target; an explicit device-local Provider Rail reorder remains a presentation preference and takes precedence over that default.

Tutti Agent is also the first Desktop default Provider for ambiguous new entries. An exact persisted Agent Target or an explicit launch request still takes precedence. AgentGUI and launch surfaces that require readiness fall back through the authoritative ready Agent Directory when that default is unavailable.

The directory owns Agent presentation. agents[].iconUrl is the primary presentation asset used by conversation identity, Message Center, mentions, and the empty-home carousel and Provider Rail. It is decorative metadata: an Agent with an exact agentTargetId and name remains selectable when the icon is absent. maskIconUrl may supply the monochrome conversation-row glyph. Desktop Workspace Agent projections first inherit the resolved icon of their Harness target by exact target ID, then use the provider/icon catalog fallback. Host projections preserve these roles independently and do not create provider-specific renderer catalogs.

AgentGUI accepts separate directory projections for separate responsibilities: agentDirectory owns the current runtime rail, handoffAgentDirectory owns launch choices, and optional mentionAgentDirectory owns exact rendered identity. The mention directory may include offline or otherwise unavailable Agents and must not participate in selection, admission, setup, or handoff. This keeps availability as action state instead of deleting identity metadata.

Agent presentation images decoded for a canvas-backed texture must use anonymous CORS loading before assigning src. Any host-owned custom protocol that serves those images must return an Access-Control-Allow-Origin response header so the decoded image cannot taint the texture canvas.

When the Desktop host projects built-in Agent mentions into a workspace app, it replaces host-local file URLs with bounded 64px WebP data URLs. The external bridge is the serialization owner: workspace apps must not read host paths, register an Electron-only asset protocol, or re-encode the icon. Remote and already-inline extension icons retain their authoritative URL.

Handoff target menus are an AgentGUI presentation contract. The shared AgentHandoffMenu renders exact agentTargetId rows, ownership metadata, and optional host-resolved ownerDeviceLabel metadata directly from the same target, plus temporary disclosure/icon-motion state; a host supplies its authoritative ready target projection and retains launch orchestration in onSelect. Host surfaces must not reconstruct a second handoff row model, observe the portaled menu DOM, or infer target identity from provider or visible text. The current conversation's composer input availability is independent from this launch surface: a target connection may disable input while Handoff remains available when the host supplies the launch callback. Handoff preserves canonical Rail placement: a source in the Chats section omits its runtime cwd from the destination project selection, while a source in a project section carries that project's path. AgentGUI DOM surfaces render the target's owner badge through the shared UI System Avatar primitive. A failed owner image falls back to the supplied owner label initial instead of exposing a browser broken-image glyph.

For a signed Agent Extension, package icon is the primary identity and optional package maskIcon is the conversation-row glyph. All assets remain pinned to the verified active installation.

Target-managed setup uses exact agentTargetId; daemon persists its state and actions. Setup gates only the empty new-conversation surface. Active/history conversations follow host-projected Session runtime availability for exact-target capability and transport reachability. A blocked Session runtime disables both composer editing and submit until the host reports the Session available again.

For a built-in Runtime that is not installed or still requires authentication, the Desktop readiness projection may additionally declare the host-owned onModelPlanSetup action. AgentGUI then keeps the ordinary install/login action and offers a secondary route to the host's Model Plan settings. Desktop derives the capability from the provider descriptor's Model Plan protocol and injects its managed-model settings route; shared AgentGUI does not name or open that product destination. Unknown or unsupported providers omit it and keep the single setup action. The capability is fail-closed: non-Tutti hosts, including TSH/VN, omit it and never render the Model Plan route when they consume the shared AgentGUI package. This route is navigation only: it does not rewrite readiness, create a Session, or bypass activation-time Model Plan validation.

Provider-declared terminal authentication remains a Host capability, not React or Session lifecycle. AgentGUI's target-setup controller owns the local idle/waiting/error projection and terminal handle, while the Desktop host launches the workspace terminal and monitors the authoritative target-setup watch until it reports ready or the bounded wait expires. The view only renders that controller state and dispatches start/cancel intent; it must not add its own effect or timer polling loop, raise the AgentGUI degradation baseline, or infer authentication from terminal output.

The built-in managed-environment wizard and Agent Extension setup have different owners. Shared UI must not combine their lifecycles by provider name.

See Agent Extensions for the detailed setup contract.

6. Agent GUI composition

6.1 UI chain

AgentGUI
  -> AgentGUINode shell
  -> useAgentGUINodeController
  -> { viewModel, actions }
  -> AgentGUINodeView
  -> shared conversation components

Code uses stable horizontal layers and behavior-oriented vertical modules:

  • shell: host/runtime/i18n/layout composition
  • controller: selector binding, UI-local state, typed command dispatch
  • model/projection: pure derivation
  • view: DOM, focus, scroll, animation, event wiring
  • vertical module: navigation, composer, timeline, Interaction, readiness, Goal, files/mentions

A controller may compose flows but cannot become a second lifecycle state machine. Extract complete behavior first; do not scatter it into a pile of domainless helpers.

Activation and existing-Session submit share a canonical prompt envelope. Submit eligibility includes text and renderable structured content; an individual composer does not redefine it.

@tutti-os/agent-gui/quick-composer is the draft-only public entry for launch surfaces that need the canonical DOM Composer without a Rail or timeline. It may present rich text, image draft blocks, and an exact Agent Target selector, but it owns no Session, Turn, option-loading, or recovery state. The host receives its typed prompt envelope and must route new Session creation through the workspace's existing AgentSessionEngine; the entry must never construct a second Engine or call a lifecycle transport.

The embedding host owns target readiness and capability loading. It passes the canonical agentTargetId plus a capability snapshot for each selectable target. Quick Composer resolves only the exact identifier and fails closed for unknown, disabled, or capability-less targets; it never falls back to array position, legacy targetId, or an inferred provider. Structured image content is accepted only when the selected target declares image support. Submit emits the resolved target identity with the prompt envelope, preserving the target the user actually selected across the host activation boundary. The Quick Composer public target omits legacy targetId and AgentGUI-internal ref; the adapter derives both from canonical agentTargetId for the shared Composer VM. Provider-menu selection therefore cannot cross or collide with a second identifier namespace.

When an embedding host supports pre-session settings, it passes one explicit settings capability containing authoritative AgentActivityComposerOptions, a controlled sparse settings draft, its change callback, and option-loading state for the exact target and project. Omitting any persistence path means omitting the whole capability, so Quick Composer fails closed instead of rendering interactive no-op controls. It projects supported values into the same model, reasoning, speed, permission, plan, and browser controls rendered by the full Composer; it does not maintain a second menu implementation or fetch options itself. computerUse and gated codexSaverMode remain hidden until an embedding activation seam preserves and authorizes them end to end. A settings change returns a typed patch to the host, which refreshes authority and forwards the settled draft through its existing activation command. Only the very first options load for a target locks settings controls and submit; once a good options snapshot exists, background refreshes keep every control interactive and a failed refresh keeps the last good snapshot instead of blanking the menus.

@tutti-os/agent-gui/composer-settings-core is the host-agnostic policy for that settings capability. ComposerSettingsCore is a headless controller (subscribe/getSnapshot, no React, no Engine dependency) that owns the sparse settings draft, a revision-fenced options lifecycle with last-good retention, defaults seeding via the daemon's defaults-merged effectiveSettings, and trailing-edge write-back of explicit picks into the canonical per-target composer-defaults ledger through injected ports (fetchOptions, rememberDefaults). resolveSubmitSettings() returns the exact values the composer displays; hosts must submit them verbatim so the daemon never re-interprets empty fields against another surface's memory. A Quick Composer host embeds this core instead of hand-rolling fencing, failure semantics, or a parallel settings store; the desktop capture window is the first adopter.

The Quick Composer uses the Composer's embedded layout contract. Unlike dock, which intentionally grows attachments and long drafts upward over a conversation timeline, embedded keeps the entire draft in normal document flow. Compact host surfaces must select that layout instead of compensating for dock overhang with consumer-specific offsets or clipping.

Embedding hosts may inject the same RichTextMentionService and workspace reference-picker callback used by a full AgentGUI surface. Quick Composer wraps those inputs in the canonical mention-service boundary and delegates reference insertion to AgentComposer; it does not own a parallel mention catalog or picker. A constrained host may also declare a top viewport inset for portaled menus. Provider Select collision padding and mention-palette geometry must honor that inset so host chrome is never treated as usable menu space. An embedding host may also opt into the canonical project selector with a controlled selected path and an explicit WorkspaceUserProjectApi. That capability supplies the real registered-project catalog, selection preparation, native directory choice, and project registration. A native directory callback alone is insufficient: Quick Composer does not invent a project registry or Session state, so it hides the project selector when no real project capability is available. The host carries the resulting path through its existing activation command as cwd. AgentGUI's standalone locale runtime includes the scoped defaults of package surfaces it mounts, including workspace-user-project. A host-supplied app runtime may override those keys, but a standalone Quick Composer must never render an unresolved scoped key as user-visible copy. Quick Composer also establishes the canonical AgentGUI semantic-token scope, so embedded placeholder, foreground, border, and menu colors never fall back to inherited host text styles. Hosts may provide a compact action accessory; AgentComposer renders it immediately before the primary send/stop action in non-hero layouts instead of requiring consumer CSS to position controls over the Composer DOM. An embedding host with a definite block size may opt into fillAvailableHeight; the embedded Composer then gives its input group and each rich-text editor wrapper the remaining height while keeping attachment and footer rows intrinsic. The entire assigned editor block remains a native contenteditable hit target instead of relying on a host-level click-to-focus proxy. This remains explicit so ordinary launch surfaces keep the default content-sized behavior.

An embedded host may also choose composerActionPlacement="footer". In that mode the same primary action cluster (including an optional host accessory) joins the canonical Composer footer instead of the prompt row. This keeps the reference controls, Agent selector, host modifier, and send button on one alignment baseline without host positioning CSS; the default remains input for existing consumers.

The canonical Composer gate belongs to the Session-presentation projection and travels through the Composer view-model slice as one object. View-local transition or workflow locks may layer on top as explicit presentation locks; they do not copy or reinterpret runtime, connection, provider, ownership, or queue readiness.

The conversation composer area is a stable AgentComposerRegion with explicit floating-control, lifted-interaction, accessory, and primary-composer slots. Workflow features compose through the accessory slot instead of adding another bottom-dock layout or reaching into composer DOM. A disclosure accessory owns only presentation and expanded state; its domain controller owns phase selection and commands.

Tutti plan review and Issue execution project into one composer-anchored TuttiWorkflowDock. The same banner shell expands upward and moves through review, UI-local materializing, execution, or error; there is no top-level Plan/Task tab and no duplicate full plan or Issue card in the conversation timeline. Accepting a plan stores only the checkpoint identity and title needed to bridge the read-model handoff, not an accepted-plan copy. That UI-local marker is scoped by exact Session, workflow, and checkpoint, and is cleared when the matching Issue or materialization failure arrives, the decision fails, or a newer actionable checkpoint supersedes it. It must not survive a completed handoff and later turn an ordinary Session re-entry into a false materializing phase. During a genuine handoff the draft remains editable while submit is disabled. The arrival of a newer actionable plan takes presentation priority over an existing Issue. Each newly identified actionable review starts expanded once; an explicit user collapse remains authoritative for that review, while the disclosure's current expanded state survives materialization and execution handoffs so the UI does not jump. The expanded plan panel starts with the plan title and body; it does not repeat mode, review-kind, or pending-state badges already communicated by the workflow banner.

The reviewed document's effect and speed are immutable plan inputs, while the Composer preferences remain mutable for the next Turn. A change is proven only when the document carries both explicit frozen values and either differs from the current Composer value; legacy documents with a missing pair never infer a change from reasoning or orchestration fields. With a proven change, the empty Composer shows Request changes beside Accept; without one, it shows only Accept. Accept always targets the checkpoint bound to the currently projected revision and therefore executes exactly the visible plan. The daemon rejects a checkpoint that is no longer bound to the workflow's current revision, so a stale render cannot execute replacement content. Typed feedback continues to request changes and includes the current preferences when they diverge.

Task-assignment directories and target option catalogs are workspace query projections, not Plan, Session, or Turn state. The Desktop assignment source retains them in the shared bounded workspace query cache: directories are keyed by workspace, while target options use exact workspace, Agent Target, and provider identity. AgentGUI may synchronously reuse the last successful value, including a stale value, while the source performs a deduplicated refresh. Provider catalog and workspace model-configuration events invalidate only the affected target entries; a generation fence prevents a pre-invalidation response from becoming current. Composer options remain a separate cwd/settings-sensitive projection and do not share this cache instance.

The materialized Issue is also a Tutti-owned aggregate-work projection for the source composer. While dispatch is not paused and any task is nonterminal, an empty composer shows the normal running Stop control even when the source Session has no active Turn; this is presentation state and must not manufacture an Agent Turn or change Host submit availability. Typed input replaces that aggregate Stop with Send. Sending from the ordinary composer always uses the normal source-conversation submit path, including while the Issue and source Turn are active; canonical busy-session availability therefore keeps the prompt in the normal queue. Provider guidance capability only enables an explicit guidance action and never changes ordinary Send semantics. Stop durably pauses Issue dispatch and cancels its running task Sessions, and also sends the ordinary source-Session stop only when that Session has stoppable work. The two idempotent paths are independent because canceling an idle Session merely to trigger the Issue cascade could capture a later Turn.

Task-level accept and rework controls in that projection prepare localized instructions in the exact source Session composer; they preserve any existing draft and never send automatically. They do not call generic Issue Task mutations or impersonate the source Agent's CLI authority. Once the user sends the draft, the normal source-conversation submit path applies and the Agent inspects the canonical Tutti execution before issuing checkpoint- and revision-fenced commands. Other plan-panel interactions should use this prompt-action pattern only when their intended effect requires source-Agent judgment; navigation and existing daemon-owned commands remain direct semantic actions.

An independent-review failure is a distinct workflow presentation, not a generic retry loop. The dock may offer an explicitly user-triggered self-review fallback through an optional host command and shows its pending, success, or failure result together with the audit identifier. It never switches review mode automatically.

An active Tutti Mode composer badge is a preference-settings entry, not a destructive toggle. Clicking it opens the UI-local TuttiBudgetPopover, seeded from the engine-projected effect and speed preferences. Every slider movement sends its selected value directly through the existing Tutti Mode activation command -- there is no draft/confirm step. Turning Tutti Mode off remains a separate adjacent action, and all controls stay disabled while an activation update is unresolved. The Desktop command host and HTTP adapter must preserve the optimistic CAS revision and both optional preferences; dropping any field turns a valid UI intent into a stale or semantically mismatched response. Tutti Desktop advertises the Tutti Mode host capability only while the default-off lab.tuttiMode preference is enabled. Toggling the preference changes presentation without mutating an existing session's durable activation or workflow state.

The preference popup uses two independent 0-100 sliders. effect raises the minimum model capability and task-verification breadth. speed asks the planning Agent to choose the fastest model that still satisfies that effect floor and maps into an upper parallel target: 0-24 -> 1, 25-49 -> 2, 50-74 -> 3, and 75-100 -> 4. They are combined, never averaged: high effect plus high speed means "fastest suitable powerful model" with a target of up to four parallel Agents, not a mid-tier model.

The popup previews a short model strategy and that parallel target. The target is not a concurrency promise: the planning Agent may shape real independent workstreams toward it, while actual scheduling remains bounded by dependencies, ownership, safe isolation, ready work, budget, and the workspace-wide four-Run ceiling. Exact models still come from the live composer catalog. Effect-scaled verification remains planning policy but is not shown as a primary preview metric.

Plan review reads the additive execution.effect and execution.speed snapshots from current tutti-mode-plan/v1 documents. A legacy v1 document without those fields maps its single orchestrationIntensity value to effect and uses the balanced speed default. The existing execution fields keep their original Issue semantics; AgentGUI must not reinterpret orchestrationIntensity as speed.

Home-composer project state distinguishes an unresolved durable default from an explicit selection whose path may be null. The project selector may apply the durable default only while that intent is unresolved. Entering the unscoped conversation section resolves the intent to no project, so remounting the hero composer or refreshing the project list cannot restore a previous project.

New-Session worktree launch is a fail-closed Host capability, not Session lifecycle state in React. The host opts in with sessionWorktreeEnabled, projects a launch preference map for the current workspace keyed by the canonical project sectionKey, persists explicit changes through onSessionLaunchModePreferenceChange, and supplies the exact project-path probe through AgentHostApi.workspace.resolveSessionWorktreeSupport. AgentGUI renders the selector only when all four pieces exist, the selected target is explicitly self-owned, a real project path and section key are selected, and the probe succeeds. Existing Sessions and shared/remote targets never expose it. Tutti Desktop carries the exact agentTargetId and project path through the generated daemon support contract. Tuttid resolves that target through its local launch authority and fails closed for shared-Agent identities. The Session create boundary repeats the same target admission before allocating a worktree and also requires a project railPlacement with a non-empty logical project path. An unscoped conversations Session therefore cannot acquire worktree isolation even through a direct API request, so React visibility is presentation defense rather than the business authorization boundary. The adapter records the selected repository-relative cwd and remaps it under the isolated checkout, so a registered project below the repository root keeps the same working-directory scope. A retry with the same Workspace, Session, repository, and relative cwd reuses its exact metadata-backed checkout before entering Host idempotency; a mismatched identity fails closed.

The stored local | worktree value records future launch intent; canonical Session isolation records what actually launched. Probe failure, temporary repository incompatibility, or a missing host capability makes the effective mode local without overwriting a saved worktree intent. Visiting an old Session also cannot write that preference. Tutti Desktop persists the nested workspaceId -> project.sectionKey -> mode map in daemon-backed desktop preferences and enables the capability. Each change uses the daemon-owned preferences.agent.session.launch.mode.patch.requested workspace/project patch; its SQLite transaction merges against the latest map, and full desktop-preference updates preserve that field, so concurrent windows cannot replace one another's choices. Renderer write failures roll back only the still-current optimistic value and are consumed through the Workbench diagnostic path. Published AgentGUI defaults the capability off so other hosts may opt in independently. The composer only adds isolation: "worktree" to the admitted activation input. Tutti's daemon adapter owns the filesystem checkout and provider-preparation cwd, while Host continues to own the idempotent Session create. The canonical Session projection later carries the created worktree path, branch, and base commit. Rail rows render the worktree glyph from that canonical isolation projection next to relative time in the unhovered row and never infer it from cwd.

A locked Session cwd existence check is UI-local observation, not Session truth. AgentGUI starts it only after pending creation has resolved, scopes its result to the normalized selected path, and discards callbacks from a previous path. Switching between Sessions that share the same cwd must not repeat the same mounted probe. A host probe failure leaves existence unknown; only a successful check that confirms absence may render missing-project chrome.

The empty-home carousel may measure its placeholder synchronously when live alignment first activates. Later React updates coalesce alignment into the next animation frame; ResizeObserver and MutationObserver keep layout roots current.

Composer text transactions may publish the current draft, but the draft value must not drive synchronous pre-paint geometry reads or an urgent AgentGUI tree render. The rich-text editor DOM owns the urgent input transaction. The latest prompt ref updates synchronously for submit and attachment reconciliation, while palette and controlled-draft projections publish in a React transition. The editor recognizes stale controlled echoes from that transition so an older projection cannot overwrite newer local input; a value not emitted locally remains an authoritative external replacement.

An existing-Session composer derives input history from that Session's canonical user-message projection and its turnless Goal-control audit entries; it does not persist a second history store. These sources are merged by their timeline timestamps so Goal commands participate in the same Up/Down sequence as ordinary prompts. The host capability remains explicit so unsupported hosts can fail closed, but Tutti Desktop always supplies sessionInputHistoryEnabled: true; historical lab.agentInputHistory preference values do not hide or disable the feature.

Composer model recents and favorites are separate browser-local menu chrome. The Composer presentation projects the exact Agent Target identity together with narrow provider-native catalog testimony, and one focused controller owns storage reads, writes, cross-window refresh, and reconciliation. An unresolved active-Session target disables history instead of reading or writing a shared fallback bucket. Only an authoritative, settled native catalog may retire a recent model; loading, empty catalogs, requested-origin entries, and selected-model-only echoes remain unverifiable. Favorites preserve explicit user intent even when a model is currently unavailable. The legacy shared default bucket migrates lazily to the first exact target: recents pass through current authoritative testimony before migration, while favorites migrate without availability filtering. Quick Composer projects the same history identity and testimony from its exact selected target and host-owned options capability. Host-level option loading keeps that testimony unsettled, so a retained last-good catalog cannot retire history while the embedding host is refreshing or changing targets.

Bare Up/Down recalls older/newer structured drafts only from an empty composer or an unchanged recalled entry, and only when the collapsed caret is at a whole-document boundary. Palette handling and IME composition take precedence, while editing a recalled draft exits history navigation. Moving past the newest entry clears the composer. Adjacent equivalent submissions collapse, persisted attachments are restored with exact workspace and Session identity, and reaching the oldest loaded entry requests the existing authoritative older-page read while preserving the timeline prepend scroll anchor. The history cursor is UI-local and resets when the draft Session scope changes; Home has no Session history.

The dock observes geometry through one coalesced animation-frame measurement entry point. Editor document updates, attachment membership or intrinsic attachment size changes, and changes to the stable input-shell width may invalidate that entry point. ResizeObserver must not observe the animated input area or editor block size: their height transition is an output of measurement and must never feed back into another measurement cycle. Width observations compare inline size before invalidating, while attachment observers cover asynchronous chip and preview sizes without a duplicate global resize listener.

The measurement pass is read-only. It derives natural text height from the editor document blocks, reads attachment heights, and publishes one atomic composer-metrics snapshot only when the snapshot changes. It must not temporarily collapse or restyle either the editor or transitioned input container. The dock input area establishes a local layout-containment boundary so a composer read cannot invalidate the conversation root. Composer paragraphs and the viewport calculation share the same line-height token so the 3.5-line cap remains exact. Regression coverage must expand across explicit newline rows, delete back to one row, and verify stable action-button placement.

Dynamic Agent surfaces must not use :has() on .workbench-window, the timeline, or transcript rows. Composer and streaming DOM mutations would make those relational subjects candidates for subtree style invalidation. Window header layout and render-error state are projected onto the owning window or header; message footer, speaker, thinking-edge, and row-kind state are projected onto the owning message flow or transcript row. Small, self-contained controls may still use local relational selectors when their subject and mutation scope are bounded.

Composer mention providers own entity presentation metadata, including an optional iconUrl, through their insertion result. AgentGUI mention projections must preserve that metadata for every supported entity kind, and the shared mention row renders the icon only when supplied. A provider-level icon assertion is not sufficient coverage: presentation changes require both a projection assertion and a consuming-row DOM assertion so an intermediate view-model cannot silently discard the icon.

Composer copy and cut write both the canonical prompt Markdown as text/plain and schema-serialized mention markup as text/html. Pasting that markup reparses it through the AgentGUI editor schema, so built-in and currently registered custom mentions retain their canonical href and chip identity without a host DOM event interceptor.

External OS file paste and drop enter one host-injected classification boundary before draft attachment creation. The synchronous resolveExternalPromptEntries port classifies each source index as a live WorkspaceFileReference or a snapshot requiring preparation. AgentGUI owns ordered mention insertion and draft reconciliation: references become ordinary file/folder mentions and never consume prompt-asset slots, while only prepare entries create pending attachment state and enter prepareExternalPromptFiles. A host without the resolver prepares every external entry. The preparer owns native-path or byte lookup, size enforcement, persistence, and remote transport; each prepared input has one sourceIndex result, one failure must not fail siblings, successful results include a provider-readable path or url, and failures carry typed error codes. Hosts that classify path-backed entries as references must reject any such entry that unexpectedly reaches preparation, so classification failure cannot silently create a duplicate snapshot.

Plain-text absolute path paste may enter an optional host-owned workspace.resolvePastedPath boundary. AgentGUI applies only a strict sync candidate gate (one trimmed absolute path with no whitespace or quotes). The host returns a WorkspaceFileReference to insert as a file/folder mention, or null/rejection to fall back to plain text. Hosts that omit the callback keep the previous plain-text paste path; existence checks, host-namespace projection, and Shared-Agent policy remain host-owned.

Workspace picker results and internal workspace-reference drags remain live references. They enter the rich-text document as mentions and never pass through external-file preparation. A picker source whose selected locator is not yet consumer-readable may perform source-owned confirmation preparation before the mention is inserted; the picker waits in a loading state, publishes no partial result on failure, and remains open for retry. This confirmation transaction belongs to the reference source contract and is distinct from the external OS file preparation pipeline. Removing an inline external-file mention removes its draft intent; a later async result must not revive it or lose its error reason when the draft is in another scope.

A host may map a reference-source content error to a labeled recovery action through the optional workspace contract. AgentGUI passes that policy through to the shared picker without copying error state. The picker owns centered error presentation and retries the failed browse or search; the host remains the authority for deciding which structured errors can request authorization or otherwise recover interactively.

6.2 Public node contract

AgentGUINodeProps groups fields by semantic responsibility:

Object Responsibility
identity node, workspace, user, title identity
workspace path, reference, project, Agent settings
frame position, size, visibility, embedding
state persisted Agent GUI node data
runtimeRequests focus, launch, prefill, probe requests
hostCapabilities host catalog, readiness, menus, icons
hostActions host mutations, Workbench/window actions
renderSlots narrow product-neutral presentation slots

AgentToolBrowserPanel owns its BrowserNode feature, surface identity, and tab state. It also owns the single browser chrome instance for that surface. Its controller may activate an existing page by URL inside that one surface; the product Host remains responsible for choosing among Browser surfaces and focusing the owning top-level node or window. Desktop mode adapters register the same semantic Terminal, Browser, and Files launch coordinators in each renderer. A Workspace adapter presents through its Workbench host; a standalone Agent adapter presents through the right-side tool sidebar and waits for the exact mounted controller when necessary. The standalone AgentGUI surface.host represents the Agent node and must not be used as a tool-launch host. After a canonical Turn is created, its originating renderer claims the Browser presentation role through Electron main; Browser automation then selects or creates that exact Agent or Workspace surface. Automation may wait briefly when a provider tool request races ahead of that claim. Unclaimed legacy Turns retain the user/Workspace fallback. Host readiness must not be used to infer Turn origin. These tools remain Desktop chrome and do not enter AgentGUI lifecycle state. Hosts compose window controls into the tab strip through defaultActions, pass draggable-header semantics through dragHandleProps, and use navigationActions for address-row actions. A host must not wrap the panel in a second visible title bar or recreate the browser header outside the panel.

Host-issued runtimeRequests.composerAppend values are one-shot requests. An append may carry prompt text, prepared files, or one semantic connector key. AgentGUI waits until the exact requested Session is the active conversation, applies the append once to the structured draft, and then calls hostActions.onComposerAppendHandled(sequence). A Host that retains routed requests must clear only the acknowledged sequence; it must not let an older open-Session append mask a newer request.

Collaborative Hosts may supply hostCapabilities.interactionReadinessSource for exact pending Interaction write admission. The source is keyed by (workspaceId, agentSessionId, turnId, requestId) and exposes only ready or blocked(synchronizing | owner_offline | binding_revoked). AgentGUI does not compose presence, transport, or lifecycle facts. When the capability exists, a missing exact record fails closed as synchronizing; omitting the capability preserves ordinary local-Host behavior. AgentGUI reads the same source for presentation and again at the event-time submission boundary. This readiness is ephemeral Host policy and must not enter canonical Session, Turn, Interaction, or Engine state.

While an exact approval or server-projected interactive prompt is presented, this capability owns its transport chrome. Target connection and observation gap sources still govern target discovery, ordinary composer writes, and sessions without an exact Interaction, but they must not override an exact ready or blocked result. Synchronizing and owner-offline results keep the pending card visible while disabling its actions; binding revocation is the terminal exception.

If an approval and a server-projected prompt coexist, AgentGUI reads readiness for both exact identities. Each surface consumes only its own admission result; selecting one as the active prompt must not reuse its readiness for the sibling Interaction.

Do not restore flat compatibility props or hide workflow inside a render slot. The optional renderSlots.projectDirectoryPickerHeaderActions slot is limited to host presentation beside the directory picker's title. AgentGUI owns picker state and supplies refresh plus source-located target selection after host mutations; the host owns the action's side effects and must not duplicate picker navigation or selection state. Hosts that render capabilities owned by another device set hostCapabilities.capabilityControlsReadOnly; AgentGUI keeps owner-supported Browser/Computer entries visible but disables their mutation and setup actions. Unsupported capabilities remain absent according to the authoritative composer capability descriptor. A caller host must not open its local device settings as a fallback for a remote owner. Account and Commerce UI is Host chrome rendered through @tutti-os/commerce/react; it is not an AgentGUI node capability. AgentGUI accepts only generic hostCapabilities.visibleErrorPresentationOverrides for structured product errors. The host owns Account/Commerce requests, login/logout, external navigation, reward receipt persistence, clipboard writes, notifications, localization, feature gating, and menu lifecycle. Neither AgentGUI nor the frontend Commerce package may receive a Cookie or start a Commerce request. For a shared Agent conversation, the Host sets the presentation-only hostCapabilities.visibleErrorPresentationScope to shared_caller. AgentGUI preserves the canonical structured reason and existing diagnostic disclosure, substitutes caller-safe contact guidance only when the structured code identifies an Owner-remediable failure, and suppresses current-device and current-account remediation actions. Transport and ambiguous failure copy stays neutral and retry-oriented. Omitting the scope preserves local_owner behavior. The scope must not enter Turn, activity, Engine, event, persistence, or provider contracts. The optional renderSlots.agentConfigAccount is a presentation-only Host chrome seam for the exact selected Agent Target. Its paired hostActions.onAgentConfigMenuOpen notification lets the Host refresh account state without hiding workflow in the render slot. Returning no renderable content keeps the provider account and quota block unchanged. Returning the package-owned AgentGUIConfigAccountFallbackSuppressed marker suppresses those generic fallback rows without adding Host content.

Tutti Desktop fills this seam only for its self-owned local Tutti Agent target. The Desktop Account service remains the source of account, membership, credit, and Commerce-link state; opening the target menu asks that service to refresh, and the render slot stays request-free. Its compact Agent menu omits the redundant Tutti account identity row while retaining credit, membership, and account-center actions. A signed-out local Tutti Agent returns the suppression marker because the generic provider-account and quota rows do not describe its Host-owned account model. Other providers return null and retain AgentGUI's default provider-account and quota presentation. The optional renderSlots.agentTargetInfo seam enriches the exact target icon in the provider Rail and Conversation Rail. The same AgentGUIAgentTargetInfoRenderer may be passed to AgentGuiWorkbenchHeader.renderAgentTargetInfo with its exact conversationAgentTarget. AgentGUI owns Tooltip disclosure, positioning, focus behavior, and the built-in label fallback; the Host renderer receives only { target, surface }, remains presentation-only, and is invoked lazily while the Tooltip content is mounted. Conversation rows resolve the target by canonical agentTargetId from the current Host directory and fail closed when it is absent. They do not copy owner, device, availability, or other target metadata into Session state.

Unavailable Agent targets retain the standard empty-home identity. AgentGUI projects offline transport and revoked-sharing states through the same Home status chrome used by other runtime failures, immediately above the composer; it does not expose a second Host-owned availability container. The canonical composer gate blocks both editing and submission until the selected target is available. Host chrome that aligns to AgentGUI's internal layout must consume explicit package signals such as hostActions.onConversationRailLayoutChange; it must not observe package DOM, CSS variables, or class names with MutationObserver. Composer affordances belong in AgentGUI itself or a narrow renderSlots contract, not in host-owned portals inserted into package DOM. During a Conversation Rail drag, AgentGUI emits this signal with resizing: true; the Workbench and standalone desktop headers apply that ephemeral width only to their own grid alignment, while the existing onUpdateNode path remains the sole persistent width write. Hosts must not write node state for each pointer movement. AgentGUI updates both its grid track (--agent-gui-conversation-rail-width) and the Rail content width (--agent-gui-conversation-rail-content-width) in that same pointer move; updating only one leaves blank space when expanding or overflows detail content when shrinking.

6.3 AgentGUIRuntime and AgentHostApi

AgentGUIRuntime is the runtime surface accepted by AgentGUI. It owns reads, Rail queries, file/upload capabilities, subscriptions, diagnostics, and the workspace AgentSessionEngine lookup. Shared lifecycle writes enter through semantic Engine methods or typed intents; AgentGUI does not require duplicate activateSession, createSession, goalControl, sendInput, submitInteractive, unactivateSession, updateSessionSettings, or updateTuttiModeActivation callbacks.

AgentGUIRuntime is the sole AgentGUI host contract. Hosts must not recreate the removed lifecycle callbacks as no-op or forwarding adapters. Metadata actions that AgentGUI still reads directly remain on this surface until their Engine migration preserves all host observability and product integration.

AgentGUI performance correlation is package-owned through createAgentGUIPerformanceMonitor. A host supplies the workspace Engine, the existing Session-event subscription, a clock, and a product-neutral event sink. The monitor joins AgentGUI's preserved submittedAtUnixMs and queue diagnostics to exact Session and Turn identities, then emits activation settlement, Prompt admission, first-token receipt, Turn settlement, and Composer-options load facts. trackComposerOptionsLoad emits a start fact before calling either the runtime getComposerOptions or exposed Engine loadComposerOptions path, then emits a correlated completed or failed fact with exact duration. An unmatched start therefore remains observable when a Provider options request never settles. The facts distinguish runtime from session-Engine entry, force refresh, directory presence, bounded error category, section/stage, and model count. Settled Composer events may include a bounded, deduplicated, control-character-cleaned allowlist of model identifiers; paths, settings, prompts, credentials, and error messages remain excluded. Hosts map those facts to their own analytics catalogs; they must not copy the Turn-binding, early-event buffering, duration-bucket, or token-classification logic.

First-token time ends when the renderer receives the first non-empty Agent content message_delta; its bounded kind is text, reasoning, plan, or other. It deliberately does not use the producer timestamp, infer a latest Turn, count tool output, or inspect Prompt or response content. Early deltas are retained only until the Engine supplies the exact Turn binding, duplicate deltas emit once, and bounded caches prevent an abandoned Prompt from growing runtime memory without limit. Exact durationMs accompanies the stable buckets lt_1s, 1s_to_3s, 3s_to_10s, 10s_to_30s, 30s_to_60s, and gte_60s.

AgentHostApi supplies host capabilities only: files, clipboard, project/account lookup, Agent Target setup/probes, diagnostics, and OS/Workbench helpers. It must not become a Session, Turn, timeline, or write source again.

Terminal authentication is one such Host capability. AgentGUI receives an optional atomic terminalStartupAction containing a safe slash-command name and literal readiness marker, then returns it unchanged through terminalLogin.run; neither layer accepts or synthesizes raw terminal input. Hosts must explicitly list supported startup action types; an older Host that omits that capability may still expose the manual command fallback but cannot silently launch a typed action it does not implement. The Desktop Workbench presenter owns node launch, exact-session readiness matching, slash submission, diagnostics, and close cleanup. Its startup result must settle before the setup-readiness monitor starts, so timeout, cancellation, or transport failure cannot leave a second polling lifecycle behind.

The optional quick-prompt library follows that host-capability boundary. Tutti Desktop projects the device-global tuttid quick-prompt CRUD service through AgentHostApi.quickPrompts; AgentGUI owns only the picker/editor presentation and inserts a selected prompt into the current TipTap selection without submitting it. The library snapshot and cross-window invalidation are not Session or Turn state and must not enter AgentGUIRuntime or the workspace engine. Hosts that omit the capability render no quick-prompt composer entry. AgentGUI may also present a small, localized set of recommended templates; those only prefill the existing editor and remain client-local until the user explicitly saves them through the CRUD capability.

6.4 Multiple surfaces

AgentGUI, Message Center, dock/header, workspace window, and standalone Agent window consume the same workspace engine.

Opening a panel/window creates presentation state only. It does not clone a Session, copy engine entities, or start another event stream. Standalone tools are Desktop chrome, not AgentGUI lifecycle.

Workbench previews must not mount a second AgentGUI tree. Genie capture prefers the host-provided native image and clones the visible node DOM into a texture only after native capture fails or exceeds its bounded wait. Electron hosts pass the sanitized node region directly to webContents.capturePage, retain the returned region for Genie, and resize a copy for the Dock. The Genie path must not reuse an undersized Dock image. Dock popup cards and minimized slots use the bounded image and its cache. A background or minimized popup node must not request a fresh DOM snapshot: AgentGUI may be unhydrated, and surface.isVisible=false deactivates imperative resources such as the hero Canvas. Those nodes reuse only a previously successful image. If no captured image exists, the Dock shows a static terminal placeholder rather than an in-flight skeleton; it does not mount AgentGUI or render a conversation summary as a fallback. Capture failure is not cached across popup lifetimes, so a later foreground attempt may still populate the image. AgentGUI therefore has no preview-mode rendering contract. When a minimized node has a cached Genie texture, Workbench completes the restore animation before launching the host node, then replaces the final texture frame only after launch settles. The expensive AgentGUI reconstruction therefore stays outside the animation. Workbench also prepares the Genie canvas during browser idle time with representative scanline, scaling, and glow operations, and commits the final minimize state in a later task instead of extending the last animation frame. A later minimize may start from that retained texture while the host refreshes its native capture asynchronously; a native image replaces the retained Genie texture only when it can cover the current window without upscaling. Low-resolution thumbnails update only the Dock preview cache. A late native result updates that Dock cache immediately, but Workbench decodes its full-size image only after the active Genie animation settles and the browser is idle. Without a reusable full-resolution texture, Workbench falls back to the visible DOM capture. The retained Canvas cache is bounded by both entry count and estimated RGBA bytes. Restored nodes keep their texture for the next minimize; controller state changes remove textures only after their nodes are closed. Eviction leaves the separate Dock image cache intact.

Genie node hiding uses a per-node operation token. A newer operation for the same node may supersede an older reveal, but another node's animation may not leave the first node hidden. The global animation generation controls only the shared Genie Canvas.

The empty AgentGUI Hero renders its existing DOM player before initializing the optional WebGL carousel. A host projects whether the normal window presentation is currently visible; the carousel waits for that visibility and then browser idle time before creating its WebGL renderer. This keeps Genie restore work out of the WebGL creation task without exposing Genie state to AgentGUI. A fully occluded presentation releases its scene, decoded images, observer, and wheel listener; exposure restores them after the reveal interaction settles. Once initialized, WebGL renders only for texture or size changes and carousel selection changes; the spring stops requesting frames after it settles. The DOM turntable record uses a compositor animation only in the active visible AgentGUI, and composer prompt tips follow the same active-surface rule. Empty-Hero entry animations may fill backwards across their delay, but must release their final identity transform after completion so every mounted AgentGUI does not retain decorative compositor layers. An embedded AgentGUI skips those internal entry animations because the Workbench shell already owns its appearance transition. Background Workbench AgentGUI bodies remain mounted so drafts and local conversation presentation survive focus changes. Focus does not determine visibility. Workbench frame geometry and z-order classify bodies as fully occluded or visually exposed; partial exposure counts as visible. While Genie replaces a real node with Canvas output, or scale minimize marks the node as pending, Workbench excludes that departing node from geometric occlusion. Covered windows therefore resume presentation during minimize; a restoring Genie node stays non-occluding until its real node is revealed. Scale restore, shell frame transitions, and onboarding entry keep the moving node's own presentation visible while temporarily excluding it as an occluder; DOM transition and animation completion release that state. Portal-backed dialog-popover nodes sort above default-layer nodes before Workbench applies normal stack order. Only fully occluded bodies pause descendant animations and use content-visibility: hidden. Visible Empty-Hero surfaces may own carousel images, alignment observers, wheel input, and a Three.js/WebGL scene. Occlusion releases those resources; exposure restores the DOM presentation immediately and defers WebGL reconstruction until after the reveal interaction settles. Detail timelines follow the same geometric boundary: fully occluded surfaces disconnect dock/timeline resize observation and scroll listeners, while exposure reattaches them and resynchronizes the retained bottom lock. Elapsed Turn, compaction, subagent, and Goal labels subscribe to one shared renderer-realm second clock only while their containing presentation is visible; exposure catches the displayed value up from canonical timestamps. Focus alone must not stop either behavior because multiple exposed AgentGUI windows remain live at the same time. On workspace restore, Desktop mounts the focused AgentGUI body immediately and hydrates inactive bodies sequentially after browser idle, one animation frame at a time. Window shells and persisted geometry remain synchronous; this staging is Desktop presentation work and does not enter engine or Session lifecycle state. WebGL scene readiness remains local presentation state; it must not enter AgentGUIRuntime, the workspace engine, or Workbench node state.

The shared Workbench Header owns conversation-identity visibility. When no Conversation exists, it ignores conversation titles, Agent titles, primary icons, exact Agent targets, target-information renderers, and fallback icons even if a host supplies them. When target information is enabled, only the session icon becomes a no-drag Tooltip trigger; the surrounding title remains owned by the Header's existing drag and menu behavior.

The reusable tool-sidebar contract lives in packages/agent/gui/workbench/tool-sidebar. Hosts provide the supported panel catalog and render adapters; the shared component owns tab selection, picker, sizing, toolbar mechanics, and a structured AgentToolSidebarHeaderLayout. Standalone Desktop registers Side only while the exact external projection exists, opens the matching source-Session tab, and uses the existing Side width bounds (440px default, 360px minimum, 600px maximum). Collapsing the sidebar retains the Side tab/runtime and releases Side focus; closing the exact tab invokes the projected close intent. Stream updates notify only the mounted Side panel, while the sidebar subscribes to a stable identity projection so it does not rerender for each delta. Other hosts keep explicit panel catalogs and do not receive Side implicitly. Hosts may hide the shared toolbar toggle through the explicit showToggleButton presentation input; omission preserves the standard visible entry. Hiding the entry does not duplicate or override sidebar behavior in the host. That layout carries the actions, open state, and reserved width into the one authoritative AgentGuiWorkbenchHeader.

The standalone Files tool reuses the Desktop file-manager pane and its complete context menu. Its Open With submenu keeps Tutti's file viewer and in-app browser alongside system applications, default-browser handling, and the system application picker. Double-clicking a file still delegates to the desktop host's system-default opener; directories continue to navigate inside Files.

Header ownership is explicit. A native standalone window selects the window-owned contract, so the shared sidebar composes the Workbench Header and body frame. An embedded Workbench selects the host-owned contract and projects the same header layout into its existing Header. The sidebar never creates a second visible panel Header. Interactive controls stop host drag gestures, while blank host-owned Header space bubbles to the host drag owner; native window ownership alone uses Electron app-region dragging.

Header layout is independent from ownership. overlay means the Header is layered above a full-height body, so open tool-panel content reserves the shared Header height and the Header stacks above --z-panel. stacked means the host has already placed the body below the Header and no panel spacer is added.

The package owns one responsive Rail presentation policy for every surface. resolveAgentGUIConversationRailPresentation derives the effective Rail width, automatic collapse, and combined collapsed state from container width plus the persisted user preference. Hosts must not supply an alternative breakpoint, copy Rail dimension constants, or restyle AgentGuiWorkbenchHeader to move its controls. The shared Workbench contribution selects an overlay Header with no bottom border, and native standalone composition projects that same explicit window presentation state. Standalone identity remains relevant only to native window ownership such as Electron drag regions and full-viewport sizing; it is not a Header layout or separator switch.

7. Key flows

7.1 New conversation

home composer submit
  -> engine pending activation + optimistic Session/message
     (including the resolved immutable railSectionKey)
  -> Host CreateSession(initial content, clientSubmitId)
  -> provisional runtime + canonical transaction
  -> first Turn accepted
  -> authoritative Session/Turn replaces optimistic projection

Initial content is one user-owned submit flow, but provider acceptance is not the prompt's durability boundary. Once the submitted Turn and prompt are durably recorded, a deterministic provider rejection keeps the visible Session, failed Turn, and user prompt so the failure can be rendered as history. Host then discards the startup runtime without publishing canonical completion. AgentGUI may offer provider login for an authentication failure, but it does not offer manual activation retry for failed or canceled Sessions; the user starts a new conversation instead. Restarted Sessions already project their canonical canceled state, so this presentation rule does not require a separate provider-history query. Only a pre-dispatch startup/validation failure may compensate an empty provisional shell; an outcome-unknown delivery keeps its recovery claim instead of guessing whether the provider ran. The rejected submit claim is terminal and remains bound to that failed Turn, so replaying the same clientSubmitId is an idempotent read of the persisted failure and never dispatches a second provider Turn. The initiating composer snapshots Tutti activation plus effect and speed with that submit. An explicit active or inactive submit snapshot is authoritative over a later read of mutable home-draft state; non-composer callers may fall back to the engine draft when no snapshot exists. capabilityRefs remain independent audit provenance and must never substitute for initialTuttiModeActivation. An activation may instead carry initialGoalControl. In that branch the engine and runtime adapter preserve the structured {action, objective} command. Desktop and Mobile map it to the typed initialGoalControl Create field and send an empty initialContent; the daemon delegates that field to Agent Host, which creates a non-provisional Session and the durable Goal operation without manufacturing a Turn. Typed initial Goal and non-empty initial content are mutually exclusive. The structured field is authoritative; integrations must not reparse the display prompt or forward /goal ... as backend command text to recover Goal semantics. Host retains text parsing only for compatibility callers that omit the structured field. AgentGUI represents the pending control and its durable audit with the same client-submit presentation identity, so canonical replacement does not remove and recreate the visible goal-control row. Hosts that can observe the durable Goal saga may additionally project the optional Session goalSyncState (revision, syncStatus, and pendingOperationId, plus optional executionPending). It is read evidence owned by the Host, not Session-owned Goal lifecycle state. AgentGUI uses an identified pending, applying, or unknown operation while the exact initial Goal mutation remains pending. After mutation convergence, synced retains the bridge only when the Host explicitly reports executionPending=true. The Host clears that proof on the first canonical Turn with exact Goal provenance or on terminal, diverged, failed, or non-active Goal evidence. Omission fails closed, including across mixed-version hosts; AgentGUI never infers future execution from synced alone or uses a UI timeout. The pending activation carries the same resolved project section key as the create command. Exact rail projection therefore shows the conversation as soon as the intent is accepted; it does not wait for provider startup or invent a temporary catch-all section. AgentGUI may select that optimistic conversation in mounted UI immediately, but it must not persist the provisional Session ID into Workbench navigation state. The selection becomes durable only after the workspace engine confirms the activation from an authoritative Session snapshot. On restart, absence from a bounded Rail page is not deletion evidence. A host that proves the selected Session is absent returns typed session.not_found; the activity engine retains that code, and AgentGUI clears only the matching global and per-target navigation memories before returning Home. Timeout, transport, and other reconcile failures preserve the remembered selection. The controller's new-conversation command must distinguish rail placement from the active Session's runtime working directory before entering the home composer. A Session in the Chats section may have a generated cwd, but that path is not a selected user project and must be cleared. A Session in a canonical project section resolves that section key back to the registered project root; its runtime cwd may instead be a nested directory or isolated worktree and is never reused as project identity. “Continue in new conversation” uses the same resolution before moving the mention draft to Home. A command already on the home composer preserves the user's explicit project selection. Views only forward new-conversation intent; unresolved active rail membership fails closed rather than guessing from composer presentation fields. For delegated/shared execution, the initiating caller remains the placement authority: the adapter forwards that caller-selected RailPlacement through the binding to the owner Host. The owner persists the same section key and does not recompute it from the owner's user-project list. The Agent CLI handoff adapter also inherits the caller Session's runtime cwd so the delegate starts inside the same checkout or linked worktree. If a project-backed caller has no runtime cwd, its canonical project path is the fallback. A supplied caller Session ID that cannot be resolved fails the start instead of creating a detached Session in an allocator directory.

7.2 Existing conversation submit

composer submit
  -> engine pending submit / queue
  -> Host SendInput(clientSubmitId)
  -> durable submit claim
  -> provider execution
  -> exact authoritative Turn acknowledgement
  -> event/reconcile confirmation

A successful response includes the exact Turn. Clients must not repair a missing Turn by polling, sleeping, or synthesizing an entity.

AgentGUI-generated Prompt clientSubmitId values are canonical UUID v4 identities. They are opaque correlation and idempotency values because a host may promote the same identity into a cross-device or shared-execution command. Business meaning must not be encoded in or recovered from the UUID. In particular, Plan feedback records its exact turnId and requestId in the pending submit's typed source; Message Center reads that source through the activity-core selector. Activity Core continues to accept caller-supplied submit identities as opaque strings and does not reinterpret or rewrite them.

7.2.1 Existing conversation Goal Control

AgentGUI goal action or structured /goal submit
  -> engine.controlGoal(action, clientSubmitId)
  -> Engine Goal operation + optimistic Goal projection
  -> Desktop or Mobile AgentSessionEffectPort.controlGoal
  -> tuttid HTTP request carrying the same clientSubmitId
  -> Agent Host durable Goal saga
  -> typed Session/Goal/operation result
  -> Engine validates Goal state and applies the returned canonical Session

Every admitted action reaches Host, even when the projected Goal already has the requested value, because Host owns the durable revision, operation, and audit. A typed response with pending or applying Goal state is accepted; synced is succeeded; explicit failed, diverged, and unknown states remain distinct. Only definitive protocol rejections become frontend failed. Timeout, transport loss, or a malformed typed success remains unknown; generic Session reconciliation does not prove the outcome of a particular Goal operation. Retrying the same action from unknown reuses its original clientSubmitId, so Host resolves the same durable operation instead of creating a second one. A canonical Session Goal cannot settle the operation by value equality because it carries no operation identity. At most one operation record is retained per Session and Session removal clears it. The package root exposes presentation and settlement selectors, and the public Engine snapshot contains only their derived state, not the raw Goal operation ledger. These maps are sparse and update only the Session IDs whose Goal, Goal operation, or Goal-bearing activation changed, so Turn streaming and unrelated Session changes preserve the Goal branch and unaffected presentation references. The response goal is Host's durable desired projection; provider observation remains in Goal state and may be empty without clearing the visible Goal. Only a durable tombstone produces goal: null. A definitive failure releases the old identity; only an unknown result retains it for an explicit retry. Goal control enters only through the workspace Engine; AgentGUI has no parallel runtime callback.

7.3 Interaction response

canonical Interaction(pending)
  -> selector projection
  -> inline / Message Center / toast surface
  -> exact interaction response command
  -> Host idempotent transition
  -> answered or superseded projection

Every surface shares the exact interaction identity (workspaceId, agentSessionId, turnId, requestId) and submitting state. Collapsing an ask-user surface back into the conversation is presentation-only: the canonical Interaction remains pending, no response or cancellation is dispatched, and the mounted answer flow retains its in-memory draft for that surface. The collapsed surface keeps a visible resume affordance. Hosts expose this action only when their ordinary Composer can answer the exact pending Interaction; Side and other prompt-only composers do not infer support from a full-size layout. In the supported full conversation surface, a question with structured options does not add a second free-text answer field; users give a new direction through the ordinary composer instead. When the active root Turn is waiting on exactly one pending question whose normalized fields all accept free text, a text-only ordinary composer submit is converted to the canonical answer payload and sent through the existing Engine Interaction response operation for that exact Turn and request. This resolves the provider's pending request so the same Turn can continue; it does not cancel the Turn, enqueue a second prompt, or branch on provider names. The composer draft clears only after Engine admits the response and is restored if the response later fails. Fixed-choice questions and non-text composer content remain on their existing paths. Provider request ids remain unchanged and may repeat across Turns; no adapter may recover a missing Turn by scanning for a session-wide request-id match. An optional Host interaction-readiness capability may block an owner-dependent response while preserving the canonical pending Interaction. Synchronizing uses the existing device-connecting presentation and disables mutation; it is not a terminal Interaction or Session outcome. Host submission adapters must recheck their authoritative projection immediately before transport dispatch; AgentGUI's early check does not replace Host or provider admission. An interactive provider callback must not block the transport's message reader while waiting for user input. If the provider can emit follow-up frames during that wait, the adapter keeps reading and joins them before publishing the canonical Interaction. When the answer is delivered, local call resolution is serialized ahead of provider terminal messages caused by that answer. Codex app-server requests are classified explicitly against the generated server-request surface. A request being schema-known does not make it a background request that may be declined silently. Message-only MCP form elicitations reuse the canonical approval Interaction and preserve the exact accept, decline, cancel, and advertised persistence semantics in the app-server response. Field-bearing forms, URL elicitations, and other request shapes that AgentGUI cannot represent losslessly fail closed before publishing an Interaction; adapters must not coerce them into a partial approval or question surface. When a standard ACP provider bridges a structured question through one session/request_permission as the complete question transaction, one selected permission option is that bridge's entire response capacity. The one-shot adapter therefore publishes only an exact single-question, single-select mapping whose question labels correspond one-to-one with the provider's non-rejection options. It marks that surface as option-only, rejects multi-question, multi-select, free-text, duplicate, or malformed/mismatched shapes before canonical Interaction publication, and never records them as answered. On submission, only the single scalar value in answersByQuestionId is authoritative; the flat answers list is display data and must not select a provider outcome. The adapter preserves the accepted canonical answer payload for local projection but translates its chosen label back to the provider's opaque option ID. The wire response remains ACP-native (outcome=selected plus optionId); renderer actions such as submit are not provider outcome values. A provider may model richer questions as a correlated sequence of permission requests, but Tutti must implement that sequence as an explicit transaction before publishing the richer surface; it must not batch answers into request ids that have not arrived. Non-DOM hosts such as React Native reuse the pure agent-conversation/interactive-answer entrypoint for canonical ask-user payload construction and own-property-safe question-id access. Presentation components remain platform-specific, but they must not copy or reinterpret this cross-provider answer contract.

A synthesized plan decision uses a durable plan_decision operation. A provider-native plan Interaction continues through interactive_response. Similar UI does not justify merging their write paths.

7.4 Resume

select/open existing Session
  -> engine session reconcile
  -> Host GetSession / EnsureRuntimeSession
  -> canonical state + optional live observation
  -> messages/detail hydration

If resume is unavailable, return an explicit state. Do not create a shadow Session.

7.5 Conversation actions and copy

rail row More menu / row context menu / workbench header menu
  -> one shared action-group contract (AgentGUIConversationActionsMenu)
  -> rename | copy as reference | copy as Markdown | open window | mark unread

All three surfaces render the same action groups; the header dispatches one discriminated AgentGuiWorkbenchCommand protocol and the node resolves the target session against canonical rail entities under the rail interaction lock. While either row menu is open the row keeps its hover layout (short title truncation, actions visible) so titles cannot overlap the action cluster. The existing package-owned AgentGUI external-request controller is the only consumer of Workbench commands. It routes by exact Workbench instance and executes new-conversation and Session actions directly, without a host hook, React request state, or sequence projection. Product hosts provide only the Workbench instance identity and an optional host-owned Rail response callback. The reusable Workbench node-state source remains the only embedded Rail state writer; its callback may record a host device preference but must not write the same node state again. A standalone surface may use the callback as its sole Rail state writer because it has no Workbench node-state source. The callback contract contains no Tutti Desktop preference or product policy, so other hosts such as TSH may persist their own Rail state without adopting Desktop behavior.

Attention state is unread-only: a live completion or explicit user unread request creates a durable unread completion, and real user exposure removes it. Absence of an unread completion means read; no separate durable read marker is authoritative. Historical list/detail reads own canonical content but never create, remove, or reconcile attention. Hydration restores only completed.unreadIds and failed.unreadIds; legacy readIds are ignored on read and every write payload explicitly empties both read buckets so stale read markers cannot survive migration.

Selection is node-local while attention state is shared. An active Session may remove unread attention only while its AgentGUI surface is host-focused, host-visible, and the renderer document is visible and focused. A retained hidden, occluded, unfocused, or backgrounded node must preserve unread attention even when its local activeConversationId still names the Session. The read-decision diagnostic records the exact node, completion, host focus, document exposure, visibility, and decision reason without logging on stream updates.

Read-only host surfaces reuse the complete workbench header and declare the session actions they support. Omitting that capability list preserves the full rename-and-copy menu; a copy-only surface does not render rename or an empty separator. Hosts that already own a complete canonical message projection may reuse the pure transcript serializer exported by the agent-conversation entrypoint, while clipboard access, toasts, and session loading remain host-owned capabilities. Window-level Agent chrome applies only when that Header is rendered through the Workbench window's own header slot; a complete Header nested in another host window's body remains ordinary embedded content and must not alter the outer window's layout or drag layer.

Copy as reference copies the session-mention markdown link the @ panel produces, so pasting into any composer reconstructs the session chip; it is synchronous and only requires a writable host clipboard. Copy as Markdown loads every canonical message page and serializes a lean transcript: user inputs blockquoted in full, per-turn final agent replies plain, interim narration collapsed in a <details> block; tool payloads (except image outputs), thinking, agent_system_notice messages, and JSON fallbacks are dropped. The clipboard write is dual-format: text/plain keeps short image references and never carries base64, while text/html embeds images as data URIs hydrated from inline data, the attachment store, and host workspace.readFile — verified empirically: rich-paste targets (Feishu docs) consume data URIs and re-upload them, but never fetch local paths or localhost URLs. Images over the per-image embed cap, or with failed reads, keep the lean reference and surface a toast that counts the omissions and points at per-image copy. Because history loading and image hydration take a noticeable moment on long conversations, selecting copy as Markdown opens one toast immediately (AgentHostToastApi.loading): it shows busy with a spinner and never auto-dismisses, and the handle it returns settles that same toast in place to success, the omitted-images info tone, or failure — at which point it starts auto-dismissing like any other toast. This is one continuous toast, not a loading toast followed by a separate result toast. Hosts without the loading capability get the prior plain info toast instead, and the result lands as an ordinary second toast.

8. Change routing

Answer before editing:

Question Owner
Change when Session/Turn/Goal/operation is created, sent, terminated, or recovered? packages/agent/host; add a conformance scenario first
Change canonical phase/outcome/Interaction vocabulary? store-sqlite/canonical
Change an HTTP request/response? OpenAPI first, then generated clients
Change provider wire normalization? provider-owned daemon adapter
Change cross-provider behavior? registry descriptor/strategy/capability
Change frontend async, optimistic, queue, or reconciliation semantics? agent-activity-core engine
Change projection, interaction, or loading behavior? focused AgentGUI model/controller
Change DOM, focus, scroll, or animation? view/UI-local hook
Change Electron, Workbench, OS, file, or window capability? Desktop host adapter

Diagnose in owner order:

  1. Did the canonical command accept and commit?
  2. Did Host produce the correct lifecycle result/CommittedDelta?
  3. Do tuttid events match the authoritative read?
  4. Did Desktop reconciliation emit the correct engine intent?
  5. Did the engine reducer/selector derive the correct state?
  6. Did projection/view render only its input?

Do not start by adding a fallback to the visible component.

8.1 Agent settings surface

The desktop settings panel's Agent section has General Settings, Agent Runtime, and Custom Agents available by default; Automation remains independently gated. The Agent Runtime tab renders built-in provider rows from the authoritative identity catalog plus the live IAgentProviderStatusService. Stable Agent Extension maturity is declared separately from Early Access activation flags, and those rows consume their live IAgentsService Targets and package-provided identity assets rather than becoming built-in provider descriptors. Its Enable/Disable control reads all Agent Targets from IAgentsService and persists the daemon-owned Agent Target enabled field. Tutti Agent is the built-in exception: its target is always enabled, its row cannot be disabled, and it has no separate developer visibility switch. Other disabled targets remain in this settings control plane so they can be re-enabled, but they are excluded from the AgentGUI agent projection and from CLI discovery and launch. The device-global provider-rail preferences remain presentation-only (ordering and optional sidebar personalization); they do not authorize an Agent Target or replace daemon enablement. Staged (Beta/Preview/in-progress) rows are gated by the lab.previewAgents switch via the provider-neutral agentGuiWorkbenchPreviewProviders predicate; stable built-in and Agent Extension rows always show in settings and launch surfaces. Deep links publish the existing openWorkspaceSettingsPanel intent with optional pane/provider; the Desktop Settings service is the single adapter that resolves legacy aliases and current destinations for workspace and standalone windows. An Agent Runtime destination also bumps agentFocus to scroll and briefly highlight the row; a link to a hidden preview agent surfaces an "enable Preview Agents" hint rather than failing silently. This is a settings surface, not a second Agent Target state store.

New Custom Agent drafts derive an untouched display name from the selected Model Plan and Agent Runtime, joined as Model Plan - Runtime when both are present. Runtime or plan changes may refresh that suggestion only until the user edits the name; after the first edit, the draft preserves the user's value across later selection changes. Description, instructions, and call conditions remain draft fields but are grouped under a collapsed-by-default Advanced options disclosure.

8.2 Deleted-conversation settings surface

Deleted conversations are maintained by a top-level desktop Settings section immediately before About; they do not belong to the Agent subsection. The page is scoped to the current Workspace, while its 15/30-day automatic-cleanup preference is device-global. Its fixed header owns the title-only search, project filter, retention selector, and destructive “delete all” action so the controls remain discoverable while the list scrolls.

Each row represents one topmost deleted Session component—a canonical root or a child whose parent is not deleted—and reuses the two-line Activity View summary convention: title first, then original project identity and the pre-delete updated_at. The row body is not navigation. Restore and permanent delete are the only row actions; restore has no confirmation and does not navigate, while permanent delete uses the ordinary destructive confirmation. Legacy lossy tombstones explain why restore is unavailable. “Delete all” ignores search/project filters, confirms against the Workspace component count with a typed phrase, and remains subject to the daemon idle gate.

The list uses stable cursor paging plus virtual scrolling and automatically loads near its end. Project options include the unscoped case and original paths whose project registration has since disappeared. Renderer state may optimistically remove a row only after a successful restore/purge response; it does not copy lifecycle semantics out of Host or trigger provider resume. The canonical restore commit emits session_restored; the workspace engine clears only that Session's deletion tombstone and performs an authoritative detail reconcile before AgentGUI renders it again. Generic activity events remain unable to resurrect a deleted Session.

9. Folder guide

Path Responsibility
packages/agent/host/** provider-neutral lifecycle application core
packages/agent/store-sqlite/** canonical SQLite transactions/repositories
packages/agent/store-sqlite/canonical/** canonical vocabulary and projection contracts
packages/agent/daemon/** provider runtime, registry, wire adapters
services/tuttid/service/agent/** Host adapters, queries, HTTP/product preparation
services/tuttid/api/openapi/tuttid.v1.yaml daemon HTTP contract
packages/agent/activity-core/src/engine/** frontend workspace engine
packages/agent/gui/agentActivityRuntime.tsx AgentGUI runtime interface
packages/agent/gui/agentGUIPerformanceMonitor.ts product-neutral AgentGUI latency correlation
packages/agent/gui/agent-gui/agentGuiNode/controller/** focused controller modules
packages/agent/gui/agent-gui/agentGuiNode/model/** pure node projection/policy
packages/agent/gui/shared/agentConversation/** reusable transcript projections/components
packages/agent/gui/agent-message-center/** Message Center projection/presentation
apps/desktop/**/workspace-agent/** desktop activity service, adapter, host integration

10. Validation

Follow the repository Validation Selection. The Agent architecture boundary commands available to that workflow are:

pnpm check:agent-host-boundary
pnpm check:agent-activity-runtime-boundaries
pnpm check:agent-provider-strategy-boundaries
pnpm check:agent-gui-degradation
pnpm check:renderer-boundaries

check:agent-gui-degradation is executable architecture. Its business-file 800-line limit and budgets for effects, memoization, render-mirror refs, provider branches, timers, component stores, and module globals may only stay level or decrease. Tighten the baseline when a metric drops; never raise it to merge new drift.

Any change to an owner, data flow, public contract, or recurring trap requires documentation impact:

  • durable architecture rules update this or an adjacent architecture document
  • implementation plans belong in docs/specs or docs/plans
  • symptoms and investigation steps belong in troubleshooting
  • historical migration records do not return to this document