Read ARCHITECTURAL-PRINCIPLES.md first if you are deciding where code should live or whether a change is aligned with the repo direction. Read PRODUCT-VALIDATION-OBJECTIVES.md when you are deciding which user frustration a change protects and how much proof it needs. Read UPSTREAM-DIVERGENCE.md when you are porting changes from upstream or explaining why a direct cherry-pick is not appropriate. For the practical contributor workflow around browser terminals, restore, browser-lab validation, and non-obvious terminal lifecycle rules, read TERMINAL-DEVELOPMENT-GUIDE.md. For the durable terminal behavior contract covering byte fidelity, stream messages, recovery, readiness, flow control, parity, and degraded platform behavior, read TERMINAL-CONTRACT.md. For task/worktree-scoped Docker Compose support, inspect semantics, and lifecycle boundaries, read TASK-CONTAINER-ENVIRONMENTS.md. For the current explicit git-isolation model, migration rules, and remaining compatibility cleanup, read GIT-ISOLATION-MODEL-SPEC.md. For the browser-first startup contract and the split between cold bootstrap and reconnect restore, read BROWSER-BOOTSTRAP-REDESIGN.md. For browser-server coordinator mode, hidden subtasks, tool credentials, and prompt-delivery ownership, read COORDINATOR-MODE.md.
This document explains the current Parallel Code architecture after the recent browser control, multi-client, terminal-attach, and browser-lab work.
It is a runtime map, not a design manifesto. It covers:
- what the system is
- how data actually flows today
- which layers are reasonably clean
- where the architecture is still mixed or awkward
Use this as the reference point for current runtime structure and data flow. Use the principles document as the normative guide for ownership, layering, and do/don't rules.
This document owns:
- the current runtime shape
- the current data and control flows
- layer and owner boundaries as they exist today
- the parts of the system that are still mixed or awkward
This document does not own:
- validation policy and quality gates
- terminal/browser-lab runbooks
- review checklists or lessons-learned heuristics
This walkthrough covers:
- the desktop UI shared by Electron mode and browser mode
- the remote/mobile UI
- the shared websocket transport
- the Electron IPC backend
- the standalone browser server shell
- the main domain concepts: projects, tasks, agents, terminals, channels, and control events
Key files:
src/App.tsxsrc/app/terminal-attach-scheduler.tssrc/runtime/*src/lib/ipc.tssrc/lib/websocket-client.tssrc/store/*electron/ipc/*electron/remote/*server/browser-server.tsserver/main.ts
Parallel Code has one shared agent/task core with three runtime shells:
- Electron desktop shell
- Browser desktop shell
- Remote/mobile shell
All three shells operate on the same underlying concepts:
- a
Projectis a repo/worktree root plus defaults;Project.projectModeexplicitly distinguishes git projects from non-git projects when git-owned workflows are unavailable Project.containerConfigis optional repo-scoped configuration for task-owned container environments; it is durable project truth, not live runtime state- a
Taskis the user-facing unit of work and has an explicit execution mode:taskMode: 'agent'owns one or more AI-agent runtimes, whiletaskMode: 'terminal'owns project-backed shell runtimes without an AI agent. Missing persisted modes migrate toagent; an emptyagentIdsarray is never used to infer the mode, including older collapsed snapshots - an
Agentis the long-lived PTY-backed AI worker attached to an agent-mode task AgentSupervisionis the backend-owned supervision snapshot used for attention routingTaskConvergenceis the app-level projection used for review readiness, overlap, and convergence queueingCoordinatoris the browser-server-only backend owner for orchestration runs, hidden subtasks, prompt delivery, startup assignment contracts, Codex readiness detection, subtask inspection, explicit subtask cleanup, self-landing state, and backend-owned workflow DAG execution, including append-only adaptive step mutation, decision-lane workflow actions, execution journaling, join-policy fan-in, branch-bundle iteration limits, repo-review proving templates, durable subtask launch payloads with explicit stale-run resume and credential rotation, backend-owned per-workflow budget enforcement (steps, lanes, retries, wall-clock) with theexecution.budgetprojection consumed by the rail, operator approval gates for decision-lane workflow actions, run pause/unpause admission control, operator lane retry, and compact workflow-state projection- a
Terminalis a standalone scratch shell panel in the UI. It is distinct from both an AIAgentand the task-scoped shells owned by a terminal-modeTask - a
Channelis a transport output stream binding used primarily in browser mode PeerPresenceis ephemeral per-browser-session identity plus focus/control context- a task takeover request is a live control-plane workflow, not persisted workspace state
- a
ServerMessage/ClientMessagepair is the websocket control vocabulary
The architecture is not a classic clean architecture. The current shape is pragmatic:
- shared protocol and transport primitives
- explicit runtime adapters for Electron, browser desktop, and remote/mobile
- explicit workflow modules for multi-step use cases
- store and UI layers that are moving toward projection and presentation
- thin server shells that should compose, not own, the workflow logic
Recent quality work has made the real seams explicit:
- browser mode now has three explicit transport planes
- backend multi-step operations now have named workflow modules
- server-owned state like browser git status now prefers push and replay over client polling
- supervision and attention state are backend-owned and pushed to clients
- lifecycle-heavy transport code is now typed and tested more aggressively
Agent launch definitions may include a command, args, and a small explicit environment map. The UI
can create one-off custom terminal agents from a direct command line, but spawning remains a backend
PTY responsibility and command availability remains a backend check. src/lib/direct-command.ts
parses launch intent, src/lib/agent-spawn-config.ts projects AgentDef.env, backend availability
resolution stays in electron/ipc/command-resolver.ts, and PTY spawn/env filtering stays in the
backend PTY owner.
Task-content reads use a separate backend authority seam. server/task-names.ts owns the canonical
task-root disposition and runtime-local generation; renderer state synchronization may update task
presentation metadata but cannot insert, replace, or resurrect a root. A live task can begin a
one-shot root admission. Closing, removed, tombstoned, and unknown tasks fail closed. The only
unknown-task exception is an exact active Arena PTY whose launch consumed a backend-issued,
one-shot capability bound to the backend-created worktree plus exact task and agent identities.
server/task-content-authority-coordinator.ts linearizes task and PTY authority, while
electron/ipc/task-file-access.ts binds a canonical in-root regular-file descriptor, rechecks its
identity and path, commits the still-current admission, and performs a bounded read. Markdown and
plan owners add their own extension/directory limits on top; renderer requests carry task identity
and a relative path, never an authorization root. The PTY owner advances an immutable revocation
transition under the same coordinator before kill, termination, disposal, or exit teardown; delayed
process exit therefore cannot keep a transient root readable.
Files:
src/App.tsxsrc/components/*src/remote/App.tsxsrc/remote/AgentList.tsxsrc/remote/AgentDetail.tsx
Responsibilities:
- render the desktop or remote UI
- bind DOM events, keyboard shortcuts, drag/drop, dialogs
- subscribe to store and runtime state
- translate user interaction into workflow or store actions
This layer is thinner than it used to be. The main remaining UI hotspots are large screens like
TaskPanel.tsx and transport-aware surfaces like TerminalView.tsx.
The UI shell now also has a small shared presentation-primitives seam for repeated chrome:
src/components/DialogHeader.tsxowns common dialog title/description/close affordancessrc/components/InlineNotice.tsxowns the standard neutral/warning/error/success inline notice treatmentsrc/components/SectionLabel.tsxowns the uppercase muted section-label treatment used across dialogs, sidebars, and settings panessrc/lib/typography.tsowns the semantic typography roles shared by the desktop shell and the remote/mobile shell, whilesrc/styles.cssandsrc/remote/index.htmldefine the actual root type tokenssrc/styles.cssowns the low-specificity app-wide keyboard-focus and reduced-motion contracts; remote and Arena builds keep equivalent rules in their own stylesheet boundaries. Generated xterm/Monaco focus nodes may suppress their internal outline only beneath a first-party shell that renders the replacement perimeter.src/lib/reduced-motion.tssamples the current local preference once for each newly mounted task panel/sidebar row so appearance classes never enter shared state, persistence, media listeners, timers, transport, or backend work.
Use these when the structure and behavior already match. Do not force context-specific workflow or state policy into them.
Two current ownership splits matter in review:
src/App.tsxis the desktop shell composition root: it keeps session/bootstrap wiring, root dialog policy, and takeover/display-name workflow state, whilesrc/components/app-shell/*stays presentational shell chromesrc/app/app-startup-status.tsowns the shared startup summary consumed byDisplayNameDialog.tsxandTerminalStartupChip.tsx, whilesrc/app/desktop-session.tsandsrc/app/desktop-session-startup.tsown the coarse bootstrap/restore lifecycle updates that feed it; the required display-name dialog owns that live region while it is open, and the global chip resumes once the dialog is gonesrc/store/local-shell-preferences.tsowns canonical defaults, resolution, detached snapshots, and boundary-specific encoding for the shared renderer-local shell preference shape.src/domain/new-task-defaults.tsowns the grouped New Task default value and exact per-field migration. Settings is its only durable writer;NewTaskDialog.tsxsamples a detached value once per open and recomputes skip-permission capability at submit. Electron full-state and browser client-session persistence include the value, while workspace snapshots and peer sync exclude it.src/store/sidebar-section-state.tsowns the sidebar collapse sub-fragment defaults and normalization, whilesrc/store/sidebar-sections.tsowns the live store toggle helpers. Browser session storage stays insrc/store/client-session.ts, browser cold-bootstrap resets apply the same closed snapshot insrc/store/browser-cold-bootstrap-projection.ts, the Electron full-state storage boundary stays insrc/store/persistence-codecs.tsandsrc/store/persistence-load.ts, andsrc/components/sidebar/SidebarProjectsSection.tsxplussrc/components/SidebarFooter.tsxonly render and toggle those section states; collapsed secondary sections may stay compact, but the footer still needs to surface peer-session identity cues without requiring an explicit expandstore.terminalHighLoadModeis a renderer-local preference, not backend truth. Browser mode persists through the store/persistence owners, whilesrc/app/terminal-high-load-mode.tsowns the runtime-facing mirror consumed by app/runtime modules. App workflow owners should subscribe to that mirror instead of importingstore/coredirectly, and store writers plus restore paths must sync it explicitly. Omitted persisted state must preserve the current bootstrap-backed valuesrc/components/TaskPanel.tsxnow keeps section composition and task-local refs whilesrc/components/task-panel/task-panel-focus-runtime.ts,src/components/task-panel/task-panel-steps-controller.tsx,src/components/task-panel/task-panel-preview-controller.ts,src/components/task-panel/task-panel-dialog-state.ts, andsrc/components/task-panel/task-panel-permission-controller.tsown the reusable focus, preview, dialog, and permission-flow orchestration seams. Task-scoped terminal switch-window lifecycle for task-panel terminals now lives here as well, so mounted siblingTerminalViews only report readiness/recovery against the shared task window instead of individually starting or cancelling it. The permission controller delegates command response work tosrc/app/task-permission-workflows.tsinstead of resolving it inline.- task steps are backend-owned worktree truth.
electron/ipc/task-steps.tsowns.claude/steps.jsonwatching, normalization, host-stamped timestamp repair, summary projection, and replayable backend events;electron/ipc/task-steps-handlers.tsis the typed fetch seam;src/store/task-steps.tsstores backend snapshots;src/app/task-steps.tsowns prompt seeding, full-snapshot fetch, next-action prefill, and focus/jump behavior; andsrc/components/task-panel/TaskStepsSection.tsxis presentation only behindsrc/components/task-panel/task-panel-steps-controller.tsx. The optional Steps view loads only for tracked tasks, with loading/error containment inside its stable panel. The controller remains eager and bounds requests by task and summary revision: a newer in-flight revision gets one follow-up, unchanged failures do not self-retry, and explicit task revisit permits another attempt. PersistedTaskstate carries onlystepsTracking; full step history stays in the worktree file and backend projections rather than renderer persistence or client-session state - task/project workflow entry points now live in app owners:
src/app/project-workflows.tsowns project picking/removal sequencing, whilesrc/app/new-task-dialog-workflows.tsowns the "open new task dialog" policy and keepssrc/store/navigation.tsfocused on pure dialog state toggles src/components/NewTaskDialog.tsxowns form and DOM interaction state, whilesrc/components/new-task-dialog/task-git-options-controller.tsowns the form-local Git metadata lifecycle. Branch metadata and retry stay independent from ignored-file discovery. The latter is active only while an open dialog can create a managed worktree, clears and aborts on project/mode/ close changes, selects only backend-marked defaults, and exposes bounded loading, unavailable, retry, and truncation state toSymlinkDirPicker.tsx. Its loading state blocks only managed- worktree submit; an advisory failure submits an empty selection. The lifecycle is keyed by project identity, not only repository path or Git configuration, because multiple project records may reference the same path. Creation projects the form into one discriminatedTaskLaunch(agentorterminal) before calling the app workflow, so agent-only prompt, permission, and coordinator fields cannot leak into terminal tasks. Git location remains an independent axis: managed worktree, project root (current-branchinternally), or imported existing worktree.src/components/new-task-dialog/new-task-draft.tsowns semantic prompt/name comparison. The dialog initializes those fields and their baseline once per open transition, routes Cancel, overlay, and Escape through one local discard policy, and bypasses that guard only after a valid submit has entered its explicit submitting state- ignored-file sharing policy is backend-owned by
electron/ipc/git-worktree-symlinks.ts. It owns bounded candidate discovery, canonical request bytes, final fresh admission, source/destination checks, common.git/info/excludeupdates, link creation, and postcondition cleanup. The existingget_gitignored_dirswire channel returns typed candidates for compatibility; renderer names stay untrusted hints. Managed task and Arena creation call the same owner. Optional failures return typedsymlink_warnings;src/app/task-lifecycle-workflows.tskeeps the successful task, logs bounded reason counts without filenames, and shows one user-facing summary src/domain/task-mode.tsowns the canonical task-mode vocabulary and legacy default;src/app/task-lifecycle-workflows.tstranslates creation/collapse/restore intent. Collapse and reopen useelectron/ipc/task-collapse-workflow.tsthrough leasedSetTaskCollapsed, preserving canonical session IDs and delegating durable clean-stop/restart proof to the existing agent and shell session owners instead of constructing replacement renderer identities;src/components/TaskPanel.tsxbranches section composition once so terminal tasks never mount AI terminal, prompt, permission, or coordinator surfaces. The first terminal-task shell explicitly owns task watcher restart after restore; ordinary secondary shells do not restart shared watchers- canonical publication can precede initial session admission. Managed agent and shell attachment join only the exact in-flight creation operation, outside the session operation queue needed by that creator, then revalidate canonical ownership, collapse, and admission. Agent attachment joins only while its original PTY is absent; existing live agents do not wait for creation completion. Persisted-only or ambiguous launch records do not authorize a retry or replacement process
- collapse persists a canonical visibility barrier, closes and drains task-scoped spawn admission, and asks both session owners to stop and retain exact next-generation permits. Reopen retries any retained suspension before making the task active. Hydration keeps canonical IDs while omitting live agent projections for collapsed tasks. Old snapshots that already erased those IDs remain collapsed with a visible recovery error; missing identity is never proof that a replacement process is safe. Generic workspace saves cannot edit collapsed state or move a task across active and collapsed lists. Host shutdown drains this owner before disposing session journals and finishes retained exact clean-stop proof writes, including sessions already stopped by a failed collapse. Verified successful removal retires those task-owned proofs; failed removal retains them
- project-root admission is backend-owned in
electron/ipc/task-workflows.ts. The backend resolves the Git top-level path, reserves its canonical filesystem identity across checkout and runtime registration, and keeps live registrations authoritative over lagging renderer snapshots. This makes the invariant independent of renderer project ids and shared by Electron and browser IPC. Its saved-state mirror registers existing legacy subdirectory paths under the nearest.gitowner root, while preserving nested worktree roots and missing paths as distinct identities. Every rendererCreateTaskrequest carries a client-generated operation id; the backend single-flights concurrent retries and replays the committed result until that task is removed, so an ambiguous browser response cannot duplicate a worktree or strand a project-root registration - task deletion is backend-owned across both managed-worktree
DeleteTaskand state-removingCleanupTaskRuntime.electron/ipc/task-removal-owner.tsfreezes canonical task identity and cleanup inputs before the first external effect, persists ordered evidence for runner, container, runtime-state, coordinator, optional worktree quarantine, optional exact-OID branch release, and shell-preparation steps, and removes canonical membership only after every required step is durable. A restart resumes at the first unfinished step. Root-backed Git and non-Git tasks use the same owner with Git cleanup explicitly preserved; renderer-supplied paths, branches, agent ids, and delete preferences never select generic-owner effects electron/ipc/task-runtime-removal-participant.tsowns infrastructure effects and the terminal shell lifecycle. Managed terminal removal prepares the exact launch operation before canonical absence and finalizes it with the committed workspace revision afterward; agent and legacy terminal tasks durably record that shell cleanup is not required. Owned worktrees are moved under a deterministic, operation-locked.parallel-code-recoveryquarantine. Dirty bytes are retained, foreign or symlink targets are refused, and branch release uses the quarantined HEAD as an exact compare-and-delete proof; the recovery quarantine is never recursively deleted by removal- ordinary legacy deletion remains available only through one process-local
WorkspaceTaskRemovalLegacyWriterGate.TaskStructureMutationServicechooses the generic owner or executes that gated legacy effect while holding one structural admission, so cutover drains an admitted request before disabling the old writer and cannot pause it between destructive cleanup and canonical membership removal. Production participant activation and IPC handlers obtain the exact same gate fromWorkspaceMutationHost src/domain/task-cleanup.tsowns the typed result shared by Electron and browser transports. Generic removal reports a durableremovalState;src/app/task-lifecycle-workflows.tsremoves and persists its local projection only forcompleteor post-commitfinalizer-repair-pending.cleanup-pendingandawaiting-linked-proofretain the task with a retryable close error. Legacy cleanup warnings remain compatibility output during the cutover, while generic cleanup failures remain durable pending work instead of being converted into false-success warnings- task container lifecycle is backend-owned.
electron/ipc/task-containers.tsandelectron/ipc/task-container-identity.tsown Compose support detection, identity, lifecycle, and logs;electron/ipc/task-container-handlers.tsis the typed IPC seam;src/app/task-containers.tsplussrc/components/task-panel/task-panel-preview-controller.tsown the task-level workflow, including inspect/log/action sequencing, stale-request suppression, and explicit error-state ownership; andsrc/components/TaskContainersPanel.tsxis presentation only. Container running/support state is not persisted store truth and must not drift into leaf-component inference - agent runner execution is separate from task-container preview lifecycle.
src/domain/agent-runners.tsowns runner profile/status/identity types,electron/ipc/agent-runner-handlers.tsowns typed profile validation,electron/ipc/agent-runner-docker.tsowns Docker CLI preflight, Dockerfile build/run argument construction, managed labels, bind-mount policy, and exact-label cleanup, andelectron/ipc/task-workflows.tsowns pending-spawn admission, cancellation, per-agent serialization, the global preparation limit, and prepared-launch cleanup. It disposes a prepared launch if spawn admission closes or the PTY attaches to an already-created session, and retains a failed prepared cleanup owner for retry. Per-agent stop is single-flight and keeps that agent's spawn admission closed through pending-spawn drain and runner cleanup; global stop applies the same single-flight admission barrier to every agent. Either failed stop retains closed admission until an explicit retry confirms successful settlement. Independent PTY, prepared-launch, and pending-build cleanup is lazily settled so even a synchronous failure cannot skip a later owner. Electron IPC, browser websocket, and remote websocket kill paths all await that same workflow owner.electron/ipc/agent-handlers.tsis the typed IPC seam; it does not own spawn concurrency.electron/ipc/pty.tsowns live and terminating session identity, normally waits for PTY exit before external cleanup, and retains both a non-exiting PTY and failed external cleanup for explicit retry. After bounded TERM/KILL exit waits are exhausted, it may attempt external cleanup as a force-stop fallback, but the still-live PTY remains owned until an exit is observed. Task deletion and application shutdown close and drain pending spawns, then await runner cleanup. Renderer code may configure and present runner state, but it must not import Docker runtime code or infer Docker truth from settings. Host remains the default runner; Docker container execution is opt-in; Docker sandbox and Docker-backed Hydra adapter launches are explicitly rejected until they have their own backend owner contract. Dockerfile-built images use per-runner tags, cleanup removes only exact-label managed containers, and Docker runner env forwarding blocks process-control variables such asPATH,HOME,SHELL,USER,NODE_OPTIONS, and dynamic library injection hooks - arena competitor readiness is backend-owned.
electron/ipc/arena-competitors.tsowns command availability, auth/env readiness, and quiet-output classification for known competitors;src/arena/command-template.tsowns the shared direct-executable parser/materializer contract;electron/ipc/system-handlers.tsis the typed inspect IPC seam;src/arena/ConfigScreen.tsxrenders readiness and gatesFight!; andsrc/arena/BattleScreen.tsxconsumes the same direct-executable model that preflight validated. Shell wrappers and env-prefixed launch strings are rejected beforeFight!, and battle execution must not drift back to/bin/sh -c. Renderer code must not guess PATH/auth state for local CLI competitors src/components/ReviewPanel.tsxnow keeps rendering, selection, and review-surface composition whilesrc/components/review-panel/review-panel-controller.tsowns the loading/diff request orchestration behind it. The shared review-session owner still lives insrc/components/review-surface-session.tssrc/components/terminal-view/terminal-session.tsstays the public terminal lifecycle facade whilesrc/components/terminal-view/terminal-input-pipeline.ts,src/components/terminal-view/terminal-output-pipeline.ts, andsrc/components/terminal-view/terminal-recovery-runtime.tsown the input, output, and recovery sub-lifecycles behind it. Terminal-specific clipboard image paste is a handler/transport-owned Electron capability surfaced through typed IPC, while terminal-session owns the explicit browser fallback and the final decision to paste the returned temp-file path. Terminal shortcut policy belongs insrc/lib/terminal-shortcuts.ts; terminal-session consumes that policy for keydown sends and non-keydown suppression instead of re-encoding ad hoc key heuristics. Task-scoped switch-window protection remains app-owned shared state: mountedTerminalViews may register participation, but they must not each own cancellation of the shared task window. Likewise, selected-task state is not enough to keep a hidden terminal render-live; hidden siblings must tier as hidden unless they are actually visible, focused, or explicitly protected as the current switch target- typing-priority truth is app-owned shared state in
src/app/terminal-interactivity-governor.ts. Terminal input, output scheduling, and fit/session stabilization must consume that governor instead of each inventing separate "focused-input" heuristics. The governor owns when a terminal is in typing-critical mode; downstream owners may choose how to yield, but they must not redefine who is latency-critical - task activity in
src/app/task-presentation-status.tsis separate from task attention. Attention answers "what needs action"; activity answers "what is the task doing right now". When multiple terminals disagree, current live output should beat unrelated waiting/startup cues, while hard failure and recovery states stay explicit. Short-lived activity cues remain app-owned projection state, so they need explicit invalidation and lifecycle ownership instead of becoming backend truth or leaf-component heuristics - renderer-local terminal output flow control still applies while a terminal is render-hibernating:
suppressed chunks may skip live writes, but
terminal-output-pipelinemust continue accounting those bytes toward pause/resume thresholds so noisy hidden terminals cannot bypass renderer-side backpressure just because their live renderer is asleep - task AI terminal layout is task-level presentation state.
src/store/task-terminal-layout.tsderives the visible agent set from selected agent, task agents, and the layout mode (focused,split,grid, orstacked) instead of persisting a second visibility truth.src/components/task-panel/TaskAiTerminalSection.tsxrenders selected panes as the only command target and visible siblings as passive-visible real terminal surfaces. TaskPanel owns the shared switch-window lifecycle; mounted sibling terminals only report readiness and must not cancel the task-level window independently. Terminal session identity is captured at mount/remount boundaries, so staleonReady/onDisposecallbacks from an old agent session must not clear or focus the current session. Task-agent membership changes are store-level mutations insrc/store/agents.ts: adding an agent selects it and persists membership best-effort, while closing a sibling kills its PTY, clears agent-scoped state, and falls back to a remaining task agent without allowing the last AI agent to be removed - fit/layout correctness is separate from typing priority.
terminal-sessionandterminalFitManagermay yield non-critical stabilization while another terminal is typing, but they must still allow resize/correctness-critical work through instead of letting latency mode create stale geometry bugs - temporary terminal maximize is renderer-local presentation owned by the mounted
TerminalMaximizeControlinsideTerminalView. Exactly one existing surface covers the viewport; neither the surface nor its terminal session is recreated or reparented. Temporary ancestor attributes release CSS containment only while maximized, and disposal or restore removes them. Existing resize/focus paths remain authoritative. Maximize does not persist task layout, collapse tasks, or acquire control. Dialogs and terminal search retain Escape precedence. - transitional lifecycle states must remain owner-backed. Browser/runtime/presentation code may
project
reconnecting,restoring, read-only, or flow-control states, but those projections must not outrun the backend/runtime owner that will clear them, and they need deterministic test coverage for repeated enter/exit churn rather than one-shot happy paths
Files:
src/app/desktop-session.tssrc/app/desktop-session-startup.tssrc/app/browser-cold-bootstrap.tssrc/app/browser-workspace-cold-start-recovery.tssrc/app/browser-startup.tssrc/app/desktop-browser-runtime.tssrc/app/desktop-session-types.tssrc/domain/browser-cold-bootstrap-projection-builder.tssrc/runtime/browser-session.tssrc/store/browser-cold-bootstrap-projection.tssrc/runtime/server-sync.tssrc/runtime/window-session.tssrc/runtime/drag-drop.tssrc/runtime/app-shortcuts.tssrc/lib/ipc.tssrc/remote/ws.ts
Responsibilities:
- adapt the UI to Electron mode vs browser mode vs remote/mobile mode
- coordinate desktop startup and teardown ordering
- distinguish browser cold bootstrap from reconnect restore
- manage websocket lifecycle, browser reconnection, connection banners, queueing
- publish browser-session presence and identity to the control plane
- prioritize active terminal attach over background attach
- apply the typed browser cold-bootstrap projection before browser-local client session restore
- manage window lifecycle in Electron mode
- translate transport events into store updates and workflow refreshes
This seam is now central. Runtime wiring is easier to find than it was before the refactor passes.
Browser startup now has an explicit split:
src/app/desktop-session-startup.tsstartssrc/app/browser-workspace-cold-start-recovery.tsbefore window chrome, then keeps ownership of payload hydration, browser-local client-session restore, and startup-tier sequencing; the recovery owner acquires the dedicated backend payload through the thinsrc/app/browser-cold-bootstrap.tstransport adapter and owns cancellable per-attempt deadlines, bounded retries, and the backend projection -> same-tab handoff -> canonical workspace fallback order. Canonical fallback authority is the persistence-session loaded-snapshot marker, not renderer-local panel or project shape. The backend builds the typed workspace projection throughsrc/domain/browser-cold-bootstrap-projection-builder.tswhile the renderer applies it throughsrc/store/browser-cold-bootstrap-projection.ts- if live canonical workspace publication wins the cold-bootstrap fetch, startup preserves its controllers, focus, and pending edits. Equal/older cold projections do not reset live state; a newer cold revision uses the existing bounded incremental full-state loader and reconciliation
- on the successful backend-projection path, the cold-bootstrap fetch is the only awaited network
round trip before the selected-task startup tier: app shortcuts register before any startup
await (handlers no-op on an empty store), the fetch starts before window chrome and runs
concurrently with the websocket runtime registration, and the payload folds in what used to be
separate round trips — bounded
planContents(exact persistedplanRelativePathreads, visible tasks only, count/byte capped),projectPathsExist(applied throughapplyProjectPathExistencewith a delayed backgroundvalidateProjectPathsfor reconciliation), and agent defs with last-known availability - the speculative selected-terminal attach lifecycle is owned by
src/app/speculative-terminal-attach.ts: startup publishes an intent frompeekClientSessionSelection()(a pure read of the persisted client-session fragment through the same parsersloadClientSessionStateuses) before any network round trip, and must confirm or discard it against the restored selection identity before announcing the selected-task tier; speculation never writes the store, and v1 ships no prewarm consumer (the attach-pipeline item may register one through the resolution callback) - reconnect restore still lives in
src/runtime/browser-session.tsand continues to use the reconnect snapshot path after authenticated control traffic confirms reconnection; if transport churn, auth expiry, or restore failure invalidates that restore,src/app/browser-startup.tscancels the reconnect-startup mode instead of leaving stale or falsely completed restore diagnostics active - the full reconnect snapshot is revision-keyed: the client passes its loaded
knownWorkspaceRevisionintoGetBrowserReconnectSnapshot, the server omits both saved-state JSON payloads on a match (controllers, generations, and running agents still ride along), andsrc/runtime/browser-state-sync-controller.tstreats the payload-free matching snapshot as a verified no-change that still runs client-session reconciliation and project-path validation; revision 0 is the unversioned legacy fallback (legacySaveAppStatemutates the file without a revision bump), so the client never claims it as a known revision and the server never skips on it —appStateJsonis the single legacy payload shipped only when no workspace-state file exists; the handler-side saved-state cache is revision-keyed with no TTL because every save path in the process invalidates it explicitly (hit/miss/invalidation/revision-skip diagnostics keep reporting)
This split keeps first browser loads from behaving like restored Electron sessions.
Files:
src/lib/websocket-client.tselectron/remote/ws-transport.tselectron/remote/protocol.tssrc/lib/client-id.ts
Responsibilities:
- shared websocket client behavior
- shared websocket server behavior
- auth handshake shape
- heartbeat/pong handling
- reconnect + replay cursor behavior
- peer presence snapshots
- task takeover request/result sequencing
- control-event sequencing
- controller lease behavior
This is the most settled part of the current architecture. The transport rules are more centralized, better typed, and better tested than before.
Files:
src/app/task-workflows.tssrc/app/task-lifecycle-workflows.tssrc/app/task-prompt-workflows.tssrc/app/prompt-input-policy.tssrc/app/task-git-action-capability.tssrc/domain/task-prompt-input-admission.tssrc/app/task-shell-workflows.tssrc/app/task-convergence.tssrc/app/remote-access.tssrc/domain/task-closing.tselectron/ipc/task-workflows.tselectron/ipc/git-status-workflows.tselectron/ipc/remote-access-workflows.tssrc/app/task-ai-workflows.tssrc/app/task-close-state.tssrc/app/task-output-channels.tssrc/app/task-command-lease-session.tssrc/app/task-command-lease-runtime.tssrc/app/task-command-lease-runtime-state.tssrc/app/task-command-lease-runtime-subscriptions.tssrc/app/task-command-lease-takeover.ts
Responsibilities:
- own multi-step user-facing operations
- sequence backend mutations plus side effects
- centralize refresh, watcher, and reconciliation behavior
- project backend-owned state like remote access and task attention into UI-facing models
- derive review-ready, stale, and overlap-aware convergence state from canonical git data
- keep transport adapters and handlers thin
This layer is newer than the others, but it is now part of the architecture. It addresses the earlier problem where end-to-end behavior was scattered across handlers, services, store slices, and runtime shells.
Ordinary task prompts use a semantic, single-attempt command rather than exposing raw PTY framing
to the renderer. src/app/prompt-input-policy.ts is the pure presentation policy for editing,
Enter behavior, and focus preservation; it is not an authorization boundary. The Electron owner
captures the current agent lifecycle generation, monotonic supervision version, task-command lease,
and task-closing state, fixes the trusted purpose to ordinary-post-start, and revalidates those
facts in electron/ipc/task-prompt-input-admission.ts immediately before admitting materialized
bytes. Every non-question supervision state remains eligible for an ordinary send. A rejection is
reported as definitely pre-write only while zero bytes are proven; a failure after the adapter
boundary is explicitly ambiguous and is never automatically replayed by browser transport.
task-git-action-capability.ts is the single renderer-side product-capability owner for merge and
push affordances. Its pure decision returns stable reasons for missing, closing, collapsed,
unavailable-project, non-Git, and project-root tasks. The same decision drives title-bar visibility,
shortcut/title intent admission, pending-dialog revalidation, and final workflow revalidation.
Denied explicit intent produces one client-local warning; an allowed decision remains only an
affordance and never replaces backend Git validation or task-command lease authority.
One workflow split worth calling out explicitly now:
src/app/task-command-lease-session.tsowns the public task-command lease API and retained session behavior. Focus loss releases only the shared typing session's own hold; unmount and idle cleanup are idempotent and cannot consume a concurrent command's retained holdsrc/app/task-command-lease-runtime.tsis the public runtime facade for lease acquisition and release behaviorsrc/app/task-command-lease-runtime-state.tsowns local retained-lease maps and invalidator bookkeepingsrc/app/task-command-lease-runtime-subscriptions.tsowns controller/transport subscriptions, takeover-expiry cleanup, and transport-generation invalidationsrc/app/task-command-lease-takeover.tsowns pending takeover decisions and prompt/response policy
That split exists so takeover policy, retained-session lifecycle, and transport/runtime cleanup do not regress one another inside a single file.
Files:
src/store/core.tssrc/store/state.tssrc/store/store.tssrc/store/tasks.tssrc/store/agents.tssrc/store/taskStatus.tssrc/store/agent-output-activity.tssrc/store/agent-ready-callbacks.tssrc/store/agent-question-state.tssrc/store/task-git-status.tssrc/store/task-command-takeovers.tssrc/store/keyed-snapshot-record.tssrc/store/projects.tssrc/store/persistence.tssrc/store/persistence-codecs.tssrc/store/persistence-save.tssrc/store/persistence-load.tssrc/store/persistence-load-context.tssrc/store/persistence-legacy-state.tssrc/store/persistence-agent-defaults.tssrc/store/persistence-projects.tssrc/store/persistence-task-hydration.tssrc/store/persistence-terminal-restore.tssrc/store/persistence-session.tssrc/store/task-state-cleanup.tssrc/store/types.ts
Responsibilities:
- hold the client-side source of truth for UI state
- expose mutations and selectors
- own persistence loading/saving logic
- project ephemeral browser presence and takeover request state
- derive task/agent status for presentation
The renderer's question detector is a fast safety blocker, not a second canonical status owner.
agent-question-state.ts keys its bounded local observation by the authoritative agent generation
and monotonically increasing output-evidence revision. Stale-generation output cannot block a new
session. Backend supervision remains the replayable source for task attention and
awaiting-input; the prompt policy conservatively ORs a matching-generation local blocker only for
dispatch safety.
This layer is cleaner than it was, but it is still not "just state". Some store modules still act
as workflow facades, especially around task and agent behavior.
The current direction is to remove workflow entrypoints from store modules where possible, keep
src/store/navigation.ts and similar files as pure local state mutation owners, and move
multi-step behavior into src/app/* workflow modules. The current intentional exceptions are
src/store/auto-trust.ts, which still owns a narrow lease-driven control path, and
src/store/taskStatus.ts, which fronts the task-presentation projection helpers.
One non-obvious boundary inside this layer now matters in review:
src/store/core.tsis the internal primitive store implementationsrc/store/state.tsis the sanctioned primitive facade for owner modules that really do need direct store reads/writessrc/store/store.tsremains the broader public selector/action barrel for component-facing consumers
That split exists to keep store/core.ts out of app/runtime/component code without forcing
everything through the full public barrel and creating import cycles. App and runtime owners should
prefer src/store/state.ts or the narrow store authority they actually need instead of importing
the broad src/store/store.ts barrel.
Another projection boundary that now matters in review:
src/store/focus.tsowns rawfocusedPanelnormalization and selector policy
App, runtime, and presentation code should read focused-panel state through the named selectors
instead of interpreting store.focusedPanel directly.
Late focus for panels that register after startup also belongs here: terminal/session code may
publish a focus callback, but src/store/focus.ts decides whether a still-current pending panel
focus should replay once that callback exists. Presentation code should not call term.focus() as
its own startup policy.
The same rule now applies to incoming desktop takeover prompts:
src/store/task-command-takeovers.tsowns incoming takeover request ordering and lookup
Desktop UI should consume the named request selectors instead of sorting
store.incomingTaskTakeoverRequests inline.
One newer app projection worth calling out is task convergence:
src/app/task-convergence.ts
It combines existing backend-owned git signals:
- branch diff
- worktree status
- merge status
- branch log
into a UI-facing convergence model:
- review-ready
- needs-refresh
- merge-blocked
- dirty-uncommitted
- overlap-risk
That projection lives above raw git services and below the UI so the sidebar review queue, review panel summary, and post-merge sibling refreshes all use one model.
The closed-domain metadata for review state now lives with that domain:
src/domain/task-convergence.tsowns labels, queue grouping, queue ordering, and review-state tone metadata, including the presentation of typed readiness reasons and actual base-branch factssrc/components/task-review-presentation.tstranslates those shared tone decisions into theme colors for desktop presentation
This keeps queue policy, sidebar badges, and review panel summary color/label behavior aligned when
new review states are added. The backend supplies semantic reasons rather than requiring clients
to parse summary prose. Older cached snapshots remain readable without guessing a branch name or
presenting needs-refresh as a loading operation.
Convergence explanations apply to shared project-root checkouts as well as isolated tasks;
a recorded branch name does not authorize switching or updating that checkout.
Another shared workflow boundary is task closing:
src/domain/task-closing.ts
It centralizes task and terminal closing predicates so workflow modules and screens stop spreading raw close-state checks and git-isolation guards independently.
Tasks now carry a discriminated closeState object instead of a loose closingStatus /
closingError pair. Terminals still use the simpler closingStatus field because they only need
closing versus removing.
Another store-owned cleanup seam worth preserving:
src/store/task-state-cleanup.ts
Task removal and incremental workspace reconciliation now use the same task-scoped cleanup helpers for derived state, agent records, and panel-side state instead of maintaining parallel delete clusters.
Task and standalone terminal removal should clear renderer state immediately after the owning runtime cleanup completes and then persist that removal best-effort. Do not reintroduce delayed store deletion for close animations; a delayed delete can reload stale task or terminal state and looks like a crash loop when cleanup races with browser persistence.
Files:
electron/ipc/pty.tselectron/ipc/git.tselectron/ipc/tasks.tselectron/ipc/storage.tselectron/ipc/git-watcher.tselectron/ipc/plans.tselectron/ipc/agent-status.ts
Responsibilities:
- spawn and manage PTY sessions
- manipulate worktrees, branches, diffs, and commits
- persist and reload app state
- compute backend-owned projections like canonical agent status
These modules are low-level. They should provide capabilities that workflows and handlers compose rather than quietly becoming use-case layers themselves.
electron/ipc/ask-about-code.ts owns every launched provider process from admission through bounded
tree termination. Cancelling or replacing a request removes only the current request-id projection;
the terminating process remains in the global concurrency owner set until cleanup settles. Both
Electron and browser-server shutdown close this admission seam, terminate and drain every owner,
and include any cleanup failure in their labeled runtime aggregate. Confirmed requested termination
is an expected cleanup result; exhausting the bounded close grace without confirming process-tree
release remains a lifecycle failure even though the UI request reports cancellation. Desktop
shutdown exits nonzero after an aggregate failure; it does not merely log the failure and report a
clean quit.
One cross-cutting backend owner is the prioritized work queue:
electron/ipc/backend-work-queue.ts
All deferrable backend recomputation (git-status refresh chains and their convergence / review /
review-signals fan-out, plus background reconciliation) routes through enqueueBackendWork with a
global concurrency cap (default 3, PARALLEL_CODE_BACKEND_WORK_CONCURRENCY) and four priority
lanes: interactive (client-invoked mutation follow-ups) > selected > visible > background.
The queue dedupes pending jobs by key, reprioritizes focus-derived pending jobs when client focus
changes, and ages background jobs into the visible lane after 60s so they cannot starve forever.
Client focus arrives through the fire-and-forget ReportClientTaskFocus IPC channel
({ selectedTaskId, visibleTaskIds, focusedChannelIds? }), owned on the renderer side by the
single reporter src/app/backend-focus-reporter.ts (leading-edge on selection change, debounced
for visibility churn, periodic keepalive against the 60s focus-registry TTL). Identity comes from
the authenticated browser client-id header (or the single Electron renderer), never request JSON,
and a client's focus contribution is cleared on disconnect. The background lane is gated until
releaseBackendBackgroundWork() runs after listen/window-load; it is the single post-listen
background scheduler, and the slow reconciliation sweep (~15s after release, one task at a time,
idle-lanes-only) repairs offline drift for never-focused tasks.
Derived backend snapshots are persisted by their backend owner:
electron/ipc/derived-state-persistence.tswrites<stateDir>/derived-state.json(debounced ~2s, atomic) from the five snapshot sources: git status, task convergence, task review, task review signals, and task step summarieselectron/ipc/saved-state-restore.tsis the shared shell-agnostic boot restore composed by bothserver/browser-server.tsandelectron/application.ts: it syncs metadata registries from one parsedSavedStateDocument, starts watchers without scheduling blanket refreshes, hydrates the persisted derived snapshots behind exact identity filters (taskId + worktreePath + branchName + projectId; git-status entries must belong to a registered watcher worktree), and registers every restored task with the background reconciliation sweep- hydrated snapshots may be stale until a demand refresh runs; the UI already presents
updatedAt-relative recency, the selected task refreshes on first focus, and watchers cover live changes - hydrated snapshots are emitted with fresh per-boot
stateVersioncounters, exactly like recomputed snapshots; all server-state category versions are per-boot, so the reconnect-version handshake carries the server instance identity (getServerInstanceId()inelectron/ipc/server-instance.ts, a random UUID per server process) before it may skip bootstrap categories — presented versions are honored only when the presented instance id matches the current process, so a restarted server always serves the full bootstrap path - the shared resync version vocabulary is
ResyncVersionMapinsrc/domain/server-state-bootstrap.ts: per-category entries are the per-boot bootstrap versions (exposed bygetServerStateBootstrapVersionsinelectron/ipc/server-state-bootstrap.ts),'workspace'IS the persistedworkspaceRevision(the only restart-safe version, already consumed by the revision-keyedGetBrowserReconnectSnapshotskip), and'agents'is the transport agent-list counter carried onagentsmessages; no domain ever gets a second version space
Browser reconnect is version-gated end to end (delta resync):
- the renderer presents its
ResyncVersionMap(collected bycollectServerStateCategoryVersionsinsrc/app/server-state-bootstrap.ts, injected into the control client throughsetBrowserResyncStateProvider), the last observed agents version, and the last observedserverInstanceIdas reconnect socket query params authenticateConnection(server/browser-control-plane.ts) replays the missed window as onecontrol-replay-batchframe, skips the agent list when the agents version is current, and sends astate-bootstrapcontaining only the stale categories (the connection-scopedpeer-presence/remote-statuscategories are always resent); the bootstrap message doubles as the handshake-complete signal and always carriesserverInstanceId. Legacy clients (no presented versions) keep the unconditional full bootstrap and per-event replay- the control-event replay ring compacts by caller-supplied key
(
getControlEventCompactionKey): only full-replace-per-key message classes may compact (coordinator events by entityKey, git-status by worktree, task-ports by task, peer-presences, remote-status); tombstones share the entity's key so they supersede the earlier upsert.run-meta-upsertedis the one partial-payload coordinator event (run scalars only), so it compacts on its own per-run slot instead of the run's entityKey — a blip-window meta mutation can never supersede therun-upsertedcreation snapshot in the ring. The client adopts the batch frame'stoSeqwholesale because compaction makes inner seqs legitimately non-contiguous; mid-stream gap detection for live traffic is unchanged. Per-event (legacy) replay is reserved for gap-free windows: when compaction or eviction left holes in the missed window, the transport sends only thereplay-truncatedsignal instead of a holey per-event replay — gap-detecting legacy consumers would otherwise misfire on the seq jump (the remote shell answers gaps with a hard reconnect whose own churn re-compacts the window, looping forever). Old clients answerreplay-truncatedwith their full restore; the current client core adopts the signal'slatestSeqwholesale so live traffic continues gap-free while the handshake bootstrap repairs state. On the client, the coordinator store applier adopts an event'scategorySeqonly when the event was fully applicable: orphan sub-entity deltas (and the meta-seeds-missing-run repair) leave the presented coordinator version stale so the next resync handshake resends the category - batch-replay frames and conditional bootstraps ride the same strict-FIFO sequenced control send path (lane 0) as live broadcasts; nothing bypasses it
- the renderer no longer gates the reconnect-status skip on a 30s wall-clock window: whenever
sequenced traffic confirmed the reconnect and no mid-stream sequence gap occurred, the cheap
GetBrowserReconnectStatuscontent check (workspaceRevision / taskCommandControllerVersion / agentGenerations) decides; the status payload also carriesserverInstanceId, but the skip comparison does not need it — taskCommandControllerVersion and agentGenerations are per-boot, so an instance change always fails the content check.replay-truncatedno longer forces a full restore (stale categories arrive through the handshake), only a sequence gap or a status mismatch does - per-boot category versions are only comparable within one server instance on the client too:
the browser state-bootstrap applier (
src/app/server-state-bootstrap-registry.ts) tracks theserverInstanceIdcarried on every state-bootstrap message and, when it changes (standalone server restarted under a surviving tab), resets every category's version tracking (resetServerStateVersionTrackingForInstanceChangeinsrc/app/server-state-bootstrap.ts) before hydrating — otherwise the versioned replacement appliers would drop the restarted server's lower-versioned full bootstrap and wedge those categories until a page reload - wake events (
online/pageshow/visible after a >5s hidden gap) probe an OPEN socket with a 2s ping deadline (probeLivenessinsrc/lib/websocket-client.ts, policy inWAKE_LIVENESS_PROBE) instead of trusting it; a miss force-closes the zombie socket so the fast-reconnect table takes over - one degraded-category contract covers bootstrap failures: a throwing category builder yields a
DegradedServerStateBootstrapSnapshotmarker instead of failing the whole bootstrap; the renderer records the degraded category, keeps prior state, and retries it targetedly (browser: therequest-state-bootstrapcontrol message; initial bootstrap: a single delayed refetch insrc/app/session-bootstrap-controller.ts)
Task-command leases get a reconnect grace on both sides:
- the control plane defers the lease release for a disconnected clientId by
TASK_COMMAND_LEASE_RECONNECT_GRACE_MS(30s); presence/takeover cleanup stays immediate, natural lease expiry (15s, non-renewing holder) still governs, and never-connected automation clientIds (coordinator:prefixes) keep immediate-prune semantics. Lease ownership itself is untouched: a peer takeover during the grace window wins and is never resurrected - the renderer suspends (rather than invalidates) lease renewals on transport loss
(
suspendAllTaskCommandLeaseRenewals) and re-claims each suspended lease through the ordinary non-takeover acquire on reconnect, adopting the backend's NEW lease generation; denial invalidates that task's sessions, and a 30s client-side deadline falls back to the old full invalidation. Transport detects, the control plane defers release, andelectron/ipc/task-command-leases.tsis unchanged
Agent availability is backend-owned sticky state:
electron/ipc/agent-availability-state.tsowns per-agent availability snapshots, a monotonic per-boot state version, and background revalidation; probe results are sticky per process and there is no TTL re-probe on any user-facing request path- probe rounds route exclusively through the backend work queue: the boot round is enqueued at
backgroundpriority (so it stays gated behindreleaseBackendBackgroundWork()), and dialog-open / settings-change revalidation runs atinteractivepriority, escalating a pending boot round through the queue's dedupe-by-key path instead of adding a second scheduler; repeat revalidation is throttled (~15s) except when the probe-target key changes electron/ipc/agents.tsis synchronous-from-state:listAgents()andgetAgentDefsWithLastKnownAvailability()mergeDEFAULT_AGENTSwith last-known snapshots (availabilityStatus: 'probing'until the first probe lands) and never spawn a prober inline; the cold-bootstrap handler consumes the same sticky read, so it stays free of process spawns- availability replays as the
agent-availabilityserver-state bootstrap category and pushes asagent_availability_changedthrough the genericipc-eventtransport; live pushes are versioned envelopes (AgentAvailabilityChangedEvent, carrying the same backend state version as the bootstrap snapshot) sosrc/app/agent-availability.tsapplies one stale-version guard to both paths — a pre-snapshot event replayed by startup buffering cannot overwrite newer snapshot truth — and the agent-catalog re-merge preserves applied availability so a stale catalog response cannot clobber newer truth - the new-task dialog opens synchronously from the store catalog (probing agents stay
launchable, with a probing badge in the agent picker) and fires a throttled
RefreshAgentAvailabilityinvoke in the background; spawn-timevalidateCommandremains the launch authority, so a stale badge can never block a launch
One newer backend service worth calling out is agent supervision:
electron/ipc/agent-supervision.ts
It derives task-attention signals from PTY output, pause state, and exits:
awaiting-inputidle-at-promptquietpausedflow-controlledrestoringexited-cleanexited-error
That state is server-authoritative and replayable, just like other backend-owned status.
Prompt and question classification inside supervision must stay tail-local and canonical:
- a bare trailing prompt line by itself means
idle-at-prompt, notawaiting-input - prompt-adjacent interactive choice text such as Hydra selection prompts still counts as
awaiting-inputeven if the operator prompt is already visible - clearing an automatic pause such as
flow-controlorrestoremust reclassify from the saved tail instead of falling back to a genericactivestate - shared renderer helpers that expose question/prompt state must reuse the same interpretation instead of inventing a separate prompt-cancels-question shortcut
- supervision snapshots carry the PTY lifecycle generation and a per-agent monotonic
supervisionVersion; prompt byte admission rejects a stale generation/version even when the renderer's immediate policy check previously passed
Another newer backend service is task port tracking:
electron/ipc/task-ports.ts
It keeps runtime task-port state split into:
- observed ports detected from PTY output
- explicitly exposed ports that the product is allowed to preview
That state is also server-authoritative and replayable.
Files:
electron/ipc/handlers.tselectron/ipc/register.ts
Responsibilities:
- validate request shape
- map IPC names to workflows or low-level services
- bridge runtime-specific invocation into the backend
This layer is thinner than it used to be, but handlers.ts is still a hotspot because it remains the front door for a large backend surface area.
Files:
electron/ipc/register.tsserver/browser-server.tsserver/main.tselectron/remote/server.ts
Responsibilities:
- host the backend service layer in different runtime modes
- adapt the shared transport to each shell
- translate shell-specific requests into backend operations
- serve frontend assets in browser/remote modes
There are now fewer duplicated transport rules across these shells, but the shells still do a lot of work.
The current architecture treats some state as backend-owned:
- git status
- remote access status
- agent supervision / attention
- agent availability (sticky per-process probe results)
- task step summaries derived from
.claude/steps.json - task port observation and exposure
The rule is:
- backend detects or computes the state
- backend pushes or replays it
- clients project it into UI state
- targeted refetch is a fallback, not the ownership model
This keeps reconnect semantics, multi-client behavior, and startup repair logic consistent across Electron and browser mode.
The browser and Electron shells now distinguish between five different ownership modes instead of persisting one shared UI blob:
WorkspaceSharedState- durable workspace-scoped state shared across clients
- examples: projects, task order, collapsed task state, task notes, task metadata
ClientSessionState- browser-local or window-local session state
- examples: selected task, selected agent, sidebar and focus state, panel sizes, font/theme preferences
PeerPresence- live browser-session identity and focus/control context
- examples: display name, active task, focused surface, visibility, currently controlled tasks
- takeover requests
- short-lived control-plane request/response state
- examples: incoming takeover cards, pending requester state, timeout-driven result messages
- task command control
- short-lived task-scoped control leases for high-conflict task actions
- examples: prompt dispatch, merge, push, close, collapse, restore
This split matters for multi-client behavior:
- foreign shared-workspace updates should not overwrite local selection or view state
- reconnect should restore shared workspace state and active task command controllers explicitly
- conflicting task mutations should use typed control leases instead of silent last-write-wins races
Task notes have one implemented semantic writer shared by every production composition.
TaskNotesService uses the canonical workspace mutation authority plus D13's shared
structural/removal authority for Get/Issue/Update, content-version comparison, task-incarnation
binding, durable operation receipts, and mutation admission. Task.notes is protected from broad
renderer saves, and the old public store setter and direct desktop writer no longer exist.
The read/editor path is active and the writer implementation is complete, but the writer cutover is intentionally default-dark. Desktop and remote each require a distinct, immutable entitlement whose archived full-gate report identity exactly equals the independently recomputed promotion identity for that surface. Missing, malformed, cross-surface, unequal, or structurally forged evidence leaves that surface dark. The current production entrypoints load no report or environment boolean and therefore advertise no fresh write capability. Both real composition roots accept immutable entitlements from an external verified promoter; the local capability query reflects the desktop entitlement, while the scoped gateway advertises the remote command set. The external browser-test launcher alone composes explicit test entitlements. A dark surface rejects Issue and first-Update admission without a mutation or lease, while exact terminal replay and already-admitted recovery remain callable so rollback does not strand durable operations.
WorkspaceMutationService also owns Notes' narrow WorkspacePrivateSnapshotAuthority. It returns
only a detached, tentative host snapshot for operation classification before structural admission;
every issued/admitted write is revalidated in the queued mutation. An identity-scoped odd/even
inspection epoch spans mutation, durability repair, and close, and a shared pending-durability gate
rejects inspection before or after the load if a write is in flight, the epoch changed, or a renamed
proposal is not yet directory-durable. This lets terminal receipts replay without a new admission
while a new or resumed write loses to task closing before entering the host queue.
The renderer path has one transport-neutral owner:
src/components/task-notes/task-notes-draft.tsowns pure conflict and recovery transitions;task-notes-controller.tsexecutes typed Get/Issue/Update effects, timeouts, retries, lifecycle ordering, invalidation coalescing, and saved/slow presentation timers;task-notes-registry.tsretains app/task-scoped drafts and bounded acknowledgement proofs in memory and installsbeforeunloadprotection only while a draft is unsafe;- desktop uses a one-second UI debounce, while the lazy remote Notes tab uses explicit Save; neither surface owns operation identity or lifecycle truth;
- desktop read-only recovery keeps real error/conflict messages visible. Its Check status action performs a Get rather than replaying an Issue/Update. Delayed clipboard fallback and recovered-draft discard confirmation revalidate their captured draft before affecting the current editor;
task_notes_changedcontains only task id, revision, and source id. Both shells validate it and publish it throughsrc/runtime/task-notes-invalidation.ts; note content is fetched on demand.
Electron desktop and browser desktop install the same typed handler table before asynchronous runtime startup. Their trusted local principal and read/write grants are fixed by composition; the handler awaits the one production task-experience runtime on first use, so startup cannot expose a missing-channel race or a second service. Authenticated remote clients reach the same service only through the scoped command gateway. Read and write visibility come from the exact frozen remote command/admission snapshot, not request JSON or a UI default.
Both remote hosts compose the same raw command HTTP handler through ScopedRemoteCommandRuntime.
The gateway retains its generic authorization, grant, queue, and result contract; the transport edge
in electron/remote/task-notes-http.ts is the sole owner that unwraps successful Notes dispatches to
a direct, method-guarded TaskNotesWireResponse, assigns the documented HTTP status, and carries
bounded retry advice into Retry-After. Other scoped commands retain the generic gateway envelope.
src/remote/remote-ipc.ts shares one authenticated fetch owner, then validates Notes' direct body and
status together before returning it to the transport-neutral controller. A nested gateway-plus-Notes
success envelope or a status/body mismatch is a protocol failure, not an alternate compatibility
path.
Each host has one content-free Notes event stream beside its renderer event projection. Electron
publishes to its BrowserWindow and Electron-hosted scoped websocket; standalone publishes to the
browser control plane and its scoped remote websocket. Websocket fanout is limited to current
notes:read sessions, subscription cleanup is host-owned, and reconnect performs an authoritative
Get for mounted editors instead of replaying note content. Shutdown closes mutation admission and
drains active commands before revoking the scoped runtime; reads remain available while admission is
draining.
Relevant files:
src/store/persistence.tssrc/store/client-session.tssrc/domain/presence.tssrc/runtime/browser-presence.tssrc/domain/presence-runtime.tssrc/store/peer-presence.tssrc/store/task-command-controllers.tssrc/domain/task-command-controller-projection.tssrc/domain/task-command-owner-status.tssrc/store/task-command-takeovers.tssrc/runtime/browser-state-sync-controller.tssrc/runtime/browser-session.tselectron/ipc/system-handlers.tselectron/ipc/task-command-leases.tssrc/app/task-command-lease.ts
One ownership boundary matters here in review:
src/domain/presence.tsowns the shared connection-status andupdate-presencepayload typessrc/domain/presence-runtime.tsowns the DOM/reactive heartbeat runtime built on top of those shared domain types
That split keeps websocket/server typing out of DOM-bearing runtime modules while preserving one canonical presence contract across browser desktop, remote/mobile, and the backend control plane.
Browser desktop and remote/mobile now share the same backend task-command control model. The UI projections are still different, but ownership truth, takeover sequencing, and controller versioning are no longer desktop-only concerns.
src/runtime/browser-presence.tspublishes the current browser session's:- display name
- visibility
- active task
- focused surface
- currently controlled tasks
server/browser-control-plane.tstracks those snapshots per authenticated browser client and fans out the authoritative presence list throughserver/browser-peer-presence.tssrc/app/server-state-bootstrap.tsandsrc/runtime/browser-session.tsreplay presence on startup and reconnectsrc/domain/server-state.tsowns the shared peer-presence snapshot validator used by both desktop/browser store projection and remote/mobile collaboration projection, so malformed replay entries cannot crash presence sorting or produce bogus owner cuessrc/store/peer-presence.tsprojects that snapshot list into UI-friendly selectors- UI surfaces like
src/components/SidebarFooter.tsx,src/components/TaskTitleBar.tsx, and the terminal/prompt control affordances render those projections src/remote/remote-presence.tspublishes the remote/mobile session's:- display name
- visibility
- active task
- focused surface
- currently controlled tasks
src/remote/remote-collaboration.tsprojects peer presence, controller snapshots, and incoming takeover requests for the remote shell- remote/mobile now uses the same display-name and control cues as desktop, but through its smaller agent-centric projection layer instead of the full desktop store
- task control itself is still enforced by backend task-command leases
- browser takeover request queuing, timeout, and controller-change reconciliation now live behind
server/browser-task-command-takeovers.ts, whileserver/browser-control-plane.tsstays the composition root that wires the backend owner into transport events - browser sessions use
src/app/task-command-lease.tsto request takeover rather than silently stealing control - the browser control plane brokers request/result messages
- the current owner sees stacked takeover request cards through
src/components/TaskTakeoverRequestDialog.tsx - the requester sees pending, approved, denied, forced, or timed-out outcomes projected through the same store/runtime path
- remote/mobile uses:
src/remote/remote-task-command.tsas the public task-command control facadesrc/remote/remote-task-command-state.tsfor retained-lease, pending-takeover, and queued-write statesrc/remote/remote-task-command-subscriptions.tsfor controller/transport subscriptions, transport invalidation, and takeover request lifecyclesrc/remote/remote-ipc.tsfor task-command lease HTTP IPCsrc/remote/ws.tsandelectron/remote/ws-server.tsfor sequenced controller, takeover, presence, and shared task-command lease result messagessrc/remote/RemoteTaskTakeoverDialog.tsxfor the owner-side approve / deny surface, which renders the full pending request queue rather than truncating to the earliest request
- remote/mobile input and resize now follow the same task-command control lifecycle as desktop instead of sending raw terminal writes without ownership
- browser session naming is still App-owned on desktop/browser mode; the browser-only header action
in
src/components/Sidebar.tsxreopenssrc/components/DisplayNameDialog.tsxthrough the shared action registry instead of leaf chrome owning a parallel dialog state
Important property:
- leaf dialogs and banners render takeover state
- the control plane and task-command lease owners decide whether control actually moves
- task-command controller snapshots are backend-versioned so a stale HTTP/IPC lease response cannot overwrite a newer websocket/control-plane ownership change in the renderer
- desktop and remote/mobile now share the same controller snapshot ordering and owner-status derivation through domain helpers instead of maintaining separate projection logic
Terminal attach is no longer a pure "mount means attach immediately" path.
Relevant files:
src/app/terminal-attach-scheduler.tssrc/app/terminal-dense-overload.tssrc/app/terminal-frame-pressure.tssrc/app/terminal-output-scheduler.tssrc/app/terminal-surface-tiering.tssrc/store/terminal-startup.tssrc/components/TerminalStartupChip.tsxsrc/components/SidebarTaskRow.tsxsrc/components/TerminalView.tsxsrc/components/terminal-view/terminal-session.tssrc/components/terminal-view/terminal-input-pipeline.tssrc/components/terminal-view/terminal-output-pipeline.tssrc/components/terminal-view/terminal-recovery-runtime.tssrc/components/terminal-view/terminal-search-runtime.tssrc/lib/terminalFitLifecycle.tssrc/lib/terminal-output-priority.tssrc/lib/webglPool.ts
Current shape:
TerminalViewregisters with the attach scheduler instead of always attaching immediately; cold-hidden non-shell terminals defer their renderer attach entirely until visibility or prewarm intent, keeping the backend session (and supervision) live throughEnsureAgentSessionsBatch(src/app/agent-session-ensure.ts)- the scheduler gives priority to the active task and focused terminal before background terminals; the drain skips ineligible candidates instead of breaking, so one pending foreground candidate cannot collapse background attach concurrency
- attach scheduler slots are released as soon as the attach RPC is dispatched
(
onAttachDispatched), so slots only guard renderer CPU phases, never backend round trips - terminals show explicit
Connecting,Attaching, andRestoringstates while the attach path is still stabilizing - a terminal attach is one pipelined backend round trip. Managed-agent and canonical primary-shell
requests carry only
(taskId, agentId/sessionId)plus channel/recovery transport fields; the backend derives launch policy from canonical state, resolves the exact generation, and performs either an immutable exact attach or an explicitly admitted one-shot restore. It validates that identity before binding the output channel, so an unavailable or stale managed identity creates no channel, process, watcher, pause, or mutation side effect. Compatibility shells and transient Arena competitors are separately discriminated legacy owners and cannot downgrade a canonical managed identity into a generic spawn. The retired renderer-ownedSpawnAgentendpoint remains wire-visible only to return a stable migration error. After a successful exact decision,AttachTerminalSessioncaptures the initial recovery entry in the same backend tick, so no Data frame on the new channel can precede the recovery cursor. A fresh compatibility/Arena spawn uses optimistic geometry (last-known or 80x24), an existing session is never resized by attach, and fit gates paint, never process creation. Reconnect and non-startup recovery still go through the sharedGetTerminalRecoveryBatchcoalescing path, while visible non-shell startup attach uses the backend-ownedGetTerminalStartupRecoveryBatchpath; non-attach recoveries are cursor-first (no rendered-tail upload) with capped snapshots, and atail-neededresponse asks the client for one bounded 64KB tail before any destructive truncated snapshot. The backend owns the restore pause lifetime for batched recoveries: every recovery request (and the attach RPC) holds its own request-scoped pause under a uniquebatchPauseIdeven when the agent is already paused (restore pause leases stack), the client releases every pause id it observed after apply, and a 5s server auto-resume timer covers a lost release; this replaces the per-terminalPauseAgent/ResumeAgentround trips - visible startup recovery is split by terminal type:
- visible non-shell startup attach uses
GetTerminalStartupRecoveryBatch, and the backend prefers serializedterminal-statepayloads from its headless xterm mirror forselectedandvisible-siblingterminals - visible shell attach stays on the ordinary attach/recovery path, but local rendered-tail replay is suppressed so reload/background-switch shell continuity does not fall back to renderer-side request-state overlap
- hidden attach and non-startup restore paths still use the ordinary recovery contract
- visible non-shell startup attach uses
- replay/apply throughput is still paced in the renderer, but visible-startup state reconstruction now lives on the backend so startup no longer depends on renderer-side "smaller replay" heuristics
- fit/restore readiness is explicit before queued output is flushed into xterm
- the loading surface masks the live xterm container until live render is ready, so users do not watch blocking startup snapshot application scroll underneath the startup UI
- terminal presentation truth is explicit at the surface level: a terminal may be
live,loading, orerror; onlyloadingmasks the live xterm container, while visible unfocused terminals stay on the real terminal surface even if their scheduler tier is deprioritized - once attached, terminal output is drained through a shared runtime scheduler instead of each terminal independently racing its own frame/timer path
- WebGL acceleration is a steady-state focused-surface optimization, not a startup/restore renderer: only a focused terminal in ready, non-restore-blocked state may claim WebGL. Once a visible terminal has already claimed WebGL, it retains that renderer while it remains visible so ordinary focus handoffs or sibling-pane changes do not flip it back to the DOM renderer. If all WebGL slots are already held by still-visible terminals, additional visible terminals stay on the DOM renderer instead of evicting a visible WebGL owner and causing renderer churn. Hidden, startup, and restore-blocked paths stay on the real DOM xterm surface unless they are explicitly promoted later. Focused terminals that already own WebGL keep it through committed resize churn so large-buffer resize replay does not fall back to the slow DOM repaint path. The same pool is the only desktop owner of paint-only atlas recovery: macOS foreground and retained-visibility edges plus the cross-platform manual redraw action enqueue exact current generations, focused/recent first. At drain time it resolves current shared-atlas identity and invalidates every sharing model synchronously, then refreshes visible members. One atlas group runs per animation frame, within the six-context bound; grouped pending requests are consumed together. This prevents xterm's per-renderer model cache from retaining coordinates into a cleared shared texture. Hidden sharers receive model invalidation but no explicit refresh. Eligibility and exact entry identity are rechecked. Repair cannot become output replay, terminal recovery, context acquisition, or component-owned global listeners; a throwing renderer alone falls back to DOM.
- terminal-local search stays behind the session facade. Its runtime lazily imports and attaches
@xterm/addon-searchonly for an open overlay with a nonempty query, owns latest-generation result ordering and frame-bounded incremental scans, and disposes the addon/listener with that exact overlay or terminal session. The overlay owns query, result counter, focus, and inline load failure only; no search state enters PTY input, task-command control, persistence, or a remote/backend projection. - queued/background terminal startup now has a shared renderer-side activity owner in
src/store/terminal-startup.ts, so the app can show one subtle aggregate startup indicator and compact per-task sidebar hints without eachTerminalViewinventing its own global status view - the public terminal lifecycle now stays visible in
terminal-session.ts, while input dispatch, output/write flow control, and recovery/rebind behavior live behind the named terminal-view owners instead of re-accumulating in one file - experimental many-terminal heavy-load policy is split cleanly:
src/app/terminal-high-load-mode.tsowns the runtime-facing mirror for the product settingsrc/app/terminal-frame-pressure.tsandsrc/app/terminal-dense-overload.tsown measured pressure and guarded overload detection- scheduler/output owners consume that state as pacing and write-shaping policy only
src/app/terminal-surface-tiering.tsplussrc/components/TerminalView.tsxown presentation/runtime surface policy such as visible-surface truth, render hibernation, selected handoff, and prewarm interpretation- backend recovery truth, task attention, and PTY flow-control ownership stay outside those presentation/runtime seams
Important property:
- visible-startup recovery is now server-owned terminal-state recovery instead of renderer-serialized historical output; if the headless mirror is unavailable, diagnostics count the fallback and the backend returns the ordinary compact snapshot path
- the backend PTY hot loop only records bytes; utf8 decoding plus supervision, port-scan,
input-trace, and terminal-state-mirror writes run once per batch flush (and run before the
no-subscriber early return so unattached coordinator agents keep supervision live).
TerminalStateMirror.serializeLatest()answers against the last applied mirror write with anappliedCursor, and startup recovery composes the remaining ring-buffer delta on top, so a noisy terminal's mirror backlog cannot stall the selected terminal's recovery - the browser channel plane drains through outbound lanes (
server/browser-outbound-lanes.ts). Lane invariant: ALL sequenced control-plane messages — live broadcasts, per-event replay, batch-replay frames, and bootstrap messages — ride lane 0 on the existing strict-FIFO control send path and are never reordered; only bulk channel data frames ride the lower lanes (focused-channel frames first, then background channels round-robin under a per-pass byte budget with a bufferedAmount soft cap). Focused-channel priority derives from the single focus signal (ReportClientTaskFocus.focusedChannelIds, published only bysrc/app/backend-focus-reporter.tsfrom the renderer registry insrc/app/terminal-focused-channels.ts). The lane scheduler is per channel manager, not per client: it consumes the merged union of every connected client's focused channel ids, so a channel focused by one client also drains with focused priority (and focused byte budget) toward other clients. That is a deliberate fairness tradeoff, not an ownership change — single-focus authority stays with each renderer — and any move to per-client lanes must keep lane 0 strict-FIFO per client - steady-state continuity still stays delta-first: attach / backpressure / hibernate recovery now drains queued local output before asking the backend for recovery state so the renderer and backend agree on the current tail, while reconnect replacement restores deliberately preserve the queued tail until the replacement restore wins so reconnect churn cannot reorder or duplicate live output
- global startup visibility stays separate from backend-owned task attention; local
attach/restore progress belongs to the renderer-side startup owner, not to
src/app/task-presentation-status.ts - local terminal fit stays local, but committed PTY resize authority follows backend task-command control; peer-controlled geometry is deferred locally and server-side resize requests are accepted or rejected by the current task owner. Reattaching an existing session must not implicitly resize the shared PTY; explicit resize commands own that mutation, and recovery responses carry both backend rows and columns so the renderer aligns to backend PTY geometry before replay
- an absent browser compatibility shell without task control returns the typed
task-control-unavailableblocker before process admission. Passive attachment never acquires control. An explicit Restore terminal action uses the existing task-command lease and peer takeover workflow, then retries the same canonical identity without inventing creation intent. Managed restart evidence and exact existing-session attachment remain unchanged. Attach failures are announced by the accessible terminal overlay, not written into xterm as if they were process output - peer-control notices share a compact single-row presentation across terminal and prompt surfaces. Dismissal retains the owner and explicit takeover action without changing row height; long names truncate visually while the complete message remains available to assistive technology and hover. Presentation never acquires task control or changes the peer approval workflow
- desired viewport geometry stays with the existing input-pipeline resize owner while task control is unavailable. Legitimate control acquisition commits the latest requested size, not the old xterm grid. The fit manager forwards valid resize observations even when they equal the current local grid, so restoring a window can cancel an older deferred fullscreen size. Resize observation and maximize remain non-acquiring; peer-control and backend acknowledgement still gate mutation. Auxiliary xterm content stays fully opaque across focus changes; focus styling belongs to the surrounding pane rather than an opacity animation on the terminal canvas
- dense-overload and surface-role reductions remain explicitly experimental and presentation/runtime-only; they may reduce browser work under load, but backend recovery truth and switch ownership stay unchanged
Parallel Code now has a task-scoped preview model, not a generic "proxy any localhost port" model.
The core distinction is:
ObservedPort- backend heuristic
- derived from PTY output
- useful as a suggestion
- not enough to expose a preview on its own
ExposedPort- explicit allowlist entry owned by a task
- safe enough to route through the product
- replayed to reconnecting clients like other backend-owned state
Relevant files:
electron/ipc/port-detection.tselectron/ipc/task-ports.tsserver/browser-preview.tssrc/app/task-ports.tssrc/components/PreviewPanel.tsx
This follows the same ownership rule as other server-owned state:
- backend detects or computes task port state
- backend pushes and replays task port snapshots
- clients project those snapshots into UI
- browser mode proxies only explicitly exposed ports
Parallel Code runs tasks on the host, not in a strict sandbox. Detection is advisory, while
exposure is explicit and task-scoped.
Preview target revalidation also reports backend runtime diagnostics for cache reuse, probe
success, connection failure, timeout failure, target, and duration. Those diagnostics are
generation-guarded so cache entries and probes that began before
reset_backend_runtime_diagnostics cannot pollute the next measured scenario.
The task attention path is a product-facing reliability path.
electron/ipc/pty.tsemits spawn, output, pause, and exit signalselectron/ipc/agent-supervision.tsconverts those signals into canonical supervision snapshots- Electron emits
agent_supervision_changedthrough IPC - browser/server mode replays the latest supervision snapshots through
server/browser-control-plane.ts
src/app/desktop-session.tshydrates and buffers startup supervision events in Electron mode- browser mode receives the same supervision events through the browser control plane
src/app/task-attention.tsprojects agent-level supervision into task-level attention entriessrc/app/task-presentation-status.tscombines supervision, lifecycle, and git readiness into a canonical task presentation model- coordinator-run operator states (
stale-after-restore, pending workflow approvals, budget-exhausted workflows) join the same pipeline as renderer-sidecoordinator-stale/coordinator-approval/coordinator-budgetattention reasons; they are pure renderer projections of backend run snapshots (getCoordinatorTaskAttentionSummaryinsrc/app/coordinator-attention.ts), never new wire state, and the domainTaskAttentionReasonvalidator is untouched src/components/SidebarTaskRow.tsxrenders the compact sidebar attention and review signals inline with each task row
This stays separate from raw task/agent dot status derivation. Attention and review are richer task supervision concepts, but they now surface through the same compact task-list UI instead of separate top-level queue panels.
All renderer-initiated coordinator operator actions (resume_run, pause_run, unpause_run,
approvals, lane retries) route through one app-layer workflow,
src/app/coordinator-operator-actions.ts, so the coordinator rail, the task title bar Resume
affordance, and any future surface share one request shape, run lookup, and rejection mapping.
src/components/task-panel/TaskCoordinatorSectionEntry.tsx is the stable task-panel boundary for
the coordinator rail. It preserves the fixed PanelChild contract while lazy-loading the full
TaskCoordinatorSection.tsx inspector only when a coordinator task is rendered. Compact
coordinator attention remains eager through src/app/coordinator-attention.ts; inspector-specific
workflow projection and controls stay out of the default task-panel startup bundle. The inspector
owns one request generation keyed by task, run, popover kind, and target across tool, operator,
spawn, and clipboard actions; switching target/run, starting a newer action, or unmounting
invalidates the older completion so it cannot overwrite the current popover or clear newer busy
state.
The startup and action-feedback layer keeps real latency windows truthful with renderer-local presentation state. These owners are presentation-only by design and must never become restore or canonical-state truth:
src/app/app-startup-status.tsowns the coarseisAppStartupPresentationPending()window: begun at desktop session startup entry, completed at startup completion, failure, and dispose.TilingLayout,Sidebar, andSidebarProjectsSectionsuppress first-run onboarding while it is pending.src/app/workspace-shape-cache.tscaches the last-known workspace shape (task names, project count) in localStorage, keyed by the serving origin so one browser profile pointing at two servers cannot render the wrong ghost shape. It is a presentation cache only: nothing may hydrate canonical state from it. Returning users get correctly-shaped ghost columns (src/components/WorkspaceStartupSkeleton.tsx) while bootstrap is pending; users with no cached shape still get first-run onboarding. Two write guardrails keep the cache a truthful returning-user signal: the persistence subscription never schedules a write whileisAppStartupPresentationPending()(the subscription registers before the awaited cold bootstrap, so an unguarded debounce would clobber the cache with the still-empty store), and an empty shape (no projects, no tasks) is never cached — persisting an emptied workspace removes the entry and a previously cached empty shape reads as no cache.- a failed startup must degrade honestly: when bootstrap or the desktop session startup fails over a live connection, clearing the startup status drops the skeleton, so the failure also routes through the persistent error-class notification. A returning user never gets a silent false first-run empty state.
src/app/task-creation-optimism.tsowns provisionalPendingTaskCreationrecords rendered assrc/components/PendingTaskColumn.tsx. Pending ids never enterstore.tasks/taskOrder, are never persisted or synced, and there is deliberately no provisional-id to real-id swap: the real task lands through the unchangedcreateTaskstore insert and the ghost is removed in the same resolve continuation. Pending panels mount astransientResizablePanelchildren, which are excluded from panel-size persistence so provisional ids can never leak intostore.panelSizes.- optimistic coordinator action state must drive the click intent it renders: while the pause/unpause flip is active, the rail derives the label, busy id, and dispatched tool from the flip target (never from the stale run view), and an accepted resume keeps the rail/title-bar Resume controls disabled until a newer run snapshot lands so duplicate requests cannot surface spurious rejection alerts.
src/components/terminal-view/terminal-pending-session-input.tsbuffers keystrokes typed between focusing a terminal and the session object existing, keyed byterminalStartupKeywith a byte cap and a drain TTL.acceptStartedTerminalSessiondrains it intosession.handleTerminalDatabefore any later input; the task-state cleanup authority clears it on task removal.- the terminal loading overlay renders a dimmed static last-known screen
(
getTaskTerminalPlaceholderTailinsrc/store/task-terminal-slate.ts, falling back to the supervision preview on cold start) under the masked surface. Placeholder removal and the queued-input indicator key off thedata-terminal-live-render-readytransition per TERMINAL-CONTRACT, never off loading copy. - error-class notifications (
src/store/notification.ts) persist until dismissed and carry the failed action name; info notifications keep the auto-dismiss window.
Agent runtimes that the product claims to bundle need a runtime-asset resolution path that works across:
- local development
- Electron packaged layout
- standalone browser/server builds
Hydra now resolves through electron/ipc/runtime-assets.ts instead of assuming one compiled
directory layout. The rule is:
- bundled tools should either work everywhere the product claims they work
- or fail with a concrete reason that the UI can surface
electron/ipc/hydra-adapter.tsremains the protocol owner for its intentionally long-lived daemon and operator children: bounded health requests validate the launched process and checkout, while the shared bounded subprocess owner runs with no automatic child deadline and owns process-group/tree termination, escalation, exit waiting, and stream cleanup. Cleanup never sends shutdown commands to a possibly replaced TCP listener. Adapter failure and signal paths settle daemon and operator cleanup together; a requested termination is successful only after tree release is confirmed, and any independent operation/cleanup failures remain composed for the nonzero adapter exit.
Electron package dependencies follow the same ownership boundary. Packages imported only by
src/** are Vite build inputs and belong in devDependencies; their code is already emitted into
dist and dist-remote, so copying their source trees into the Electron Node runtime only adds
size and duplicate authority. dependencies is reserved for Electron/backend runtime imports and
the vendored Hydra runtime. Declaration bucket is not a security-exposure boundary:
scripts/lib/dependency-exposure.mjs owns the renderer-bundled direct-root set and classifies every
concrete lockfile node through npm's installed nearest-ancestor graph as backend-runtime,
renderer-shipped, or tooling, retaining every membership and one deterministic shortest path.
The package verifier and dependency-audit command consume that owner rather than maintaining
parallel lists. The frontend/remote runtime-import architecture guard derives the actual Vite
closure, rejects unresolved or computed runtime loads, and requires its renderer-only roots to
match the owner exactly. The pinned npm version also enforces strict-allow-scripts; only exact,
reviewed dependency versions in package.json#allowScripts may execute native/build install
lifecycles, so a lock refresh cannot silently skip or introduce executable setup code.
scripts/verify-electron-package.mjs rejects renderer-bundled package
trees, requires the unique locked name/version identities represented by all non-development,
non-optional package-lock entries plus every declared direct runtime dependency in each produced
archive (so legitimate package-manager hoisting is layout-independent), recursively checks every
packaged build/runtime input for freshness, verifies every archive from multi-target release
builds, and rejects known test, benchmark, demo, coverage, snapshot, and runner-config artifacts
from dependencies. The standalone server uses server/tsconfig.build.json as its production-only
emit boundary;
server/build-server.mjs removes the previous output before compilation, scans successful output
for development artifacts and Vitest imports, and removes failed partial emits, while
server/tsconfig.json remains the broader typecheck boundary that includes tests. Session-stress
and server integration harnesses invoke this production build owner too; raw non-watch TypeScript
emits must not repopulate the shared dist-server tree with test modules after it was validated.
Browser static delivery is precompressed and cache-aware:
scripts/compress-dist-assets.mjs(wired intoprepare:browser-artifacts) writes.gzand.brsiblings for compressibledist/dist-remoteassets;--checkis the CI byte-budget gate (gzipped main bundle < 250KB plus the terminal-session modulepreload link), enforced becauseprepare:browser-artifactsruns it after the compress step and CI'snpm testprepares browser artifactsserver/browser-static.tsserves the precompressed sibling withContent-EncodingandVary: Accept-Encodingwhen the client accepts it (identity fallback for dev watch dirs), addsCache-Control: public, max-age=31536000, immutablefor hashed/assets/files, and keeps HTMLno-store- the Vite build injects modulepreload links for the lazily imported terminal-session chunk and its
static import closure, plus prefetch hints only for the explicitly startup-relevant
addon-web-linksandaddon-webglchunks. The terminal-search addon is intentionally excluded so it has zero network cost until the first nonempty query. The graph is computed inelectron/vite.config.electron.ts; the remote bundle is a single chunk and needs no equivalent injection
Defined in src/store/types.ts and managed mainly in src/store/projects.ts.
A project is the persistent repo-level configuration:
- path
- display name
- project mode, defaulting to git when omitted
- branch prefix
- default task Git isolation (
worktreeor project root) - delete-branch defaults for managed worktrees
- bookmarks
Projects matter to both task creation and git status lookup.
Project-root tasks are shared-checkout memberships, not exclusive worktree owners. Their backend registry retains every task ID for a canonical root; exact task identity still selects command leases, runtime cleanup, and content authority. Creation observes the current checkout without switching branches. The Git-isolation contract and default-branch rules live in GIT-ISOLATION-MODEL-SPEC.md.
Defined in src/store/types.ts and managed mainly in src/store/tasks.ts.
A task is the main desktop-level unit. It carries:
- explicit execution mode (
agentorterminal) - human name
- project association
- branch name
- worktree path
- Git isolation, base branch, and worktree ownership
- agent IDs for agent-mode tasks
- selected agent ID, which is a projection hint for prompt and terminal targeting
- task-scoped shell IDs
- notes
- prompt state
- plan content
- optional
stepsTrackingconfig for backend-owned.claude/steps.jsontracking - agent-mode runtime and permissions config
The UI is task-centric. Agents are mostly viewed through the task that owns them.
One ownership split matters here:
Task.stepsTrackingis durable task config- task step history and next-action summary are not persisted inside
Task - the backend owns
.claude/steps.json, projects replayable snapshots, and repairs malformed or unstamped entries before renderer code sees them
Defined in src/store/types.ts, backed by PTY sessions in electron/ipc/pty.ts.
An agent is the long-lived execution session. It carries:
- task ownership
- chosen agent definition
- explicit resume strategy
- current status
- exit information
- last output tail
- generation/restart identity
- optional backend runner identity for non-host execution
Status is partly authoritative from the backend and partly interpreted on the frontend.
One ownership rule matters here now:
- agent definitions declare
resume_strategy - the trusted built-in catalog alone may declare
resume_failure_classifierandresume_failure_fallback; persistence strips those reserved capabilities and restores them only from the separately retained trusted catalog - CLI-style agents resume through launch arguments
- Hydra resumes through backend-owned startup recovery in the vendored operator runtime
- renderer code may request a resumed spawn, but it must not recreate Hydra's
:resumeworkflow or special-case Hydra boot timing locally
That split exists because persisted workspace state can outlive backend PTY sessions. After a server restart, the first attach still owns the real recovery decision. For Hydra, that recovery is now worktree-scoped and serialized in the backend/vendored runtime instead of being approximated in the renderer.
Hydra's durable coordination files remain checkout-scoped. The task-agent spawn owner reserves the canonical checkout across pending and live Hydra launches and rejects a second Hydra writer there; ordinary CLI agents remain eligible to share it. The adapter also refuses to shut down an existing same-checkout daemon while choosing a port. Distinct worktrees keep independent Hydra sessions.
Runner identity follows the same ownership rule: a project may configure the preferred runner, but the active runner instance is backend PTY/supervision metadata. Browser and mobile clients consume that projection; they do not decide whether a Docker container is live by reading project settings.
The managed agent-session owner is active in the production task-experience composition:
src/lib/agent-resume.tsowns bounded, exact output classification; it retains at most the final 16 KiB / 50 logical lines and currently recognizes only Claude's exact missing-conversation linesrc/domain/agent-session-operation.tsowns transport-neutral request, phase, replay, action, and removal-hook contractselectron/ipc/agent-session-operation-journal.tsowns the versioned operation/identity journal, queue-exclusive exact-prior/exact-proposal durability classification, bounded rich replay, and exact-task record deletionelectron/ipc/agent-session-workflow.tsowns generation/lease admission, phase-before-effect ordering, spawn acknowledgement, replay, and the versioned removal drain/finalizer hookselectron/ipc/agent-session-recovery.tsis the backend-only finalized-exit adapter; the production runtime owns its single exit subscription, short system lease, and deterministic fallback requestsrc/app/agent-session-action.tsowns the small renderer restart/resume/switch intent contract;src/app/agent-session-workflows.tsis its sole effect adapter. It retains one bounded operation identity across lost responses and applies only correlated backend projections. The terminal tile loads the effect adapter only after an explicit user action, keeping the reliability facade outside the default renderer startup path
Renderer reconnect is therefore identity recovery, not process policy. EnsureAgentSessionsBatch
and managed AttachTerminalSession requests never accept command, argument, environment, working
directory, resume, runner, or replacement material. The D11 owner classifies canonical agent
identity and the D13 task-shell owner classifies canonical primary-shell identity; only an explicit
compatibility-shell or Arena owner may reach the bounded compatibility spawn workflow. Exact attach
validates (taskId, sessionId, isShell, generation) without mutating PTY metadata and binds the
channel only after that validation succeeds.
electron/ipc/task-experience-runtime-composition.ts starts the journal owners dark, activates the
generic task-removal cutover and exact agent-session-owner-hooks-v1 epoch, disables legacy process
writers, then enables initial/manual/fallback admission. Missing journal health, gate, epoch, hook,
or trusted built-in launch semantics returns unavailable before a generation or process effect.
Lifecycle events carry the backend generation, operation identity, launch reason, and resumed bit;
the renderer uses those fields only to hydrate authoritative status and explain a successful fresh
fallback.
Activation and close use the same explicit ownership boundary. A failed activation attempts every owner acquired so far; simultaneous activation and rollback failures remain paired, and a failed rollback retains an exact retry callback instead of allowing replacement acquisition over uncertain state. Normal close is single-flight while pending and retryable after failure. Electron window cleanup and standalone browser cleanup both close task-experience admission, then settle task experience and workspace-storage owners with stable labels. Every owner is attempted, failures stay observable to the desktop/browser shutdown aggregate, and desktop shutdown exits nonzero after the aggregate settles.
Clean shutdown uses one ordered process boundary shared by desktop and browser hosts: D11 closes
agent-session admission and snapshots exact managed-agent identities while D13 closes primary-shell
admission and issues in-memory candidates; the global runner owner then stops and drains every PTY,
pending spawn, and prepared runner while keeping admission permanently closed; only after that stop
is proven may D11 atomically persist its marker batch and D13 persist every candidate as a prepared
one-shot permit; journals and remaining task-experience owners close last. A preparation or stop
failure persists no new permit, aborts unpersisted shell candidates, remains labeled in the host
shutdown aggregate, and keeps process admission closed. Crash/SIGKILL has no clean permit. On the
next process, available -> restoring -> restored (D11) and pending -> spawning -> terminal
(D13) make restoration single-flight and fail closed across another crash; generations advance
monotonically and a consumed permit is never recreated by renderer reconnect.
Initial prompts use the same composition boundary. electron/ipc/task-initial-prompt-runtime.ts
owns the durable draft/delivery/manual-send state and removal hooks. Managed task creation tracks the
canonical prompt after task commit and before initial process launch. A failed launch retains its
waiting-session record; unavailable tracking cannot be bypassed by spawning first. Delivery then
binds to the admitted agent generation, and InitialPromptDeliveryControl is a lazy
renderer projection/editor rather than a second delivery owner. Automatic/manual writes and draft
clearing occur only through the backend owner after the matching removal cutover epoch is active.
The task panel retains that editor by delivery identity across selected-agent changes, preserving
unsaved text, selection, and save ownership. If the canonical delivery clears or changes during a
local edit, the old editor remains read-only recovery until explicit confirmed discard. Copy and
late acknowledgements do not discard it. Once no unsafe local edit remains, the ordinary composer
resumes its selected-agent lifetime. The compact prompt panel remains manually resizable.
Explicit Review/View disclosure requests space through the existing task-panel size owner, and Hide
restores its previous height. Status updates never request a resize; constrained recovery details
remain scrollable, and the one-shot request does not override subsequent manual resizing.
ResizablePanel initializes from ordered geometry and layout-mode changes, not short-lived
descriptor object identity. Identical descriptors retain current expansion and editor/terminal
identity; real constraints, orientation, ordering, and persistence-key changes still reinitialize
through the existing size owner. Geometry initialization and explicit size requests run in one
ordered update, so opening a previously fixed toolbar section cannot reset its requested expansion
and strand a terminal with no measurable content height.
Supervision events wake this owner but do not solely drive it: a bounded safety observation polls
pending readiness, runtime discovery, and post-write evidence through their existing deadlines, so
presentation-event coalescing cannot strand an otherwise observable delivery. A delivery without a
running agent remains in waiting-agent-session without consuming its readiness window. The durable
45-second readiness deadline begins only when the first running generation is admitted and permits
one persisted pre-write extension for a generation change. The backend service, rather than the
runtime timer, owns expiry; it can settle missing-runtime writing, retry-wait, readiness, and
five-second post-acceptance verification states, release the command lease, and retry a failed lease
release. Runtime observation serializes those core turns, uses the same injected clock, and rearms
nonterminal work after typed unavailability or thrown persistence/projection work.
Activation also repairs exact canonical legacy prompt identities that lack a journal record,
including workspaces whose protection cutover is already active. The persistence owner commits
manual-only canonical mode and a sealed recovery record atomically, before runtime admission.
Unknown prior delivery is an explicit durable fact, not a fabricated write attempt. Valid records
are never replaced; mismatched identities fail closed. Recovery does not authorize automatic replay,
and manual admission still requires the existing inspection/confirmation protocol.
A legacy delivery may still name an original agent that the user legitimately replaced before
recovery. The projection query returns a typed, read-only recovery-unavailable issue for this
identity mismatch instead of throwing an internal error or retargeting the delivery. It includes
the exact canonical task/delivery identity, current server instance, and bounded saved text for
inspection or copying, but no fabricated delivery snapshot, editable draft acknowledgement, or send
authority. Task removal and owner availability are rechecked after inspection; unknown deliveries,
closed tasks, and dark owners retain the existing null result. Unexpected storage failures remain
errors rather than being relabeled as recoverable history.
The renderer reliability client revokes its active capability before forwarding a matching restart
invalidation to mounted consumers. Prompt controls fence older refresh completions against newer live
events and invalidate write actions when status cannot be verified, without discarding local text.
Recovery UI uses the shared compact action style and an always-visible summary; draft details are
disclosed on demand, while unsaved edits and conflicts keep their editor visible. Read-only legacy
recovery exposes saved text for inspection/copy, never as an acknowledged sendable draft.
Projection fanout has one runtime-owned pending obligation per delivery. A thrown or temporarily
null projection keeps that obligation dirty, and a later safety turn retries it even when durable
status did not change. With no listeners the obligation is complete because subscribers bootstrap
from an explicit current-projection read rather than historical events.
Manual takeover persists the automation seal before releasing the automatic command lease. Both
automatic and manual leases remain retained until release succeeds; same-delivery replays retry
that release inside the delivery serializer before they may resume or return a durable result, so a
concurrent replay cannot revoke control from an in-flight manual write.
A durable pending manual-send confirmation survives backend restart unchanged: it is not a failed
write attempt and cannot become a confirmation-free retry.
The production persistence repository owns the atomic cross-record mutations: revising a draft
also advances its journal edit high-water, while clearing an accepted draft also seals and compacts
that high-water. The service never repeats those writes from a stale pre-mutation record; it reloads
the committed record before any following transition. Decoder validation checks both directions of
the delivery/manual-operation indexes and requires each phase to agree with its terminal receipt, so
a forward-only index or half-settled operation cannot reopen admission after restart.
An accepted manual write whose compare-clear acknowledgement is transiently unavailable remains a
durable write-accepted operation. The runtime schedules bounded same-session finalization that
first proves that exact operation is still accepted and then replays only its idempotent clear/seal
transition. A competing operation, temporary gate rejection, or unavailable projection reschedules
proof; it cannot cancel the accepted operation or admit another PTY write.
Its draft controller retains at most one exact failed/in-flight edit request and one latest trailing
text snapshot: an unknown response replays the same operation ID and request before the trailing edit
receives a new ID against the acknowledged result.
PTY byte admission is a separate server-lifecycle owner shared by every task-prompt producer.
createIpcHandlers creates one TaskPromptInputAdmissionService, stores it on the host's
HandlerContext, and injects that exact instance into both the ordinary prompt IPC handler and the
initial-prompt runtime. Its per-agent tail therefore serializes ordinary, automatic-initial, and
manual-initial frames through one generation/version/lease/question/closing recheck boundary in
both Electron and standalone browser mode. Producer modules cannot construct fallback admission
services of their own. The service starts fail-closed; the production task-experience runtime binds
it once to TaskStructureMutationService.isTaskMutationAdmissionClosed and releases that binding on
shutdown or failed activation. That O(1) structural projection includes the private removal
mutation-drain/fence window as well as durable closing and absence, so byte admission does not lag
the catalog lifecycle event. Initial delivery additionally requires live matching PTY metadata;
ordinary exited-generation sends retain their explicit metadata-free compatibility behavior.
Extra standalone terminal panels stored in src/store/types.ts and src/store/terminals.ts.
These are not task agents. They are UI panels backed by shell agents, but conceptually they are side terminals rather than the primary task execution lane.
Browser scratch-panel layout lives in client session storage, not canonical workspace task state. Shared scoped-state reconciliation treats both canonical tasks and live local terminals as owners: workspace refreshes preserve scratch focus, layout, input, and retained control. Explicit terminal close and full application-state replacement retire that panel's scoped runtime state. Removal revokes all old local lease holders; delayed command cleanup releases only its captured hold and cannot consume a newly created same-ID lease. On an explicitly admitted standalone creation, the backend records the transport-normalized creating client on that live PTY as initial-admission provenance, not an exclusive read permission. Authenticated full-browser clients may reattach or observe the exact live task/session/generation after canonical removal checks and live channel binding; attach never resizes the PTY or transfers task-command control. This is existing-session authority, never a replacement-spawn permit. The provenance disappears with the process and is not restored after host restart. Missing provenance, missing processes, or mismatched identities cannot use browser layout alone to authorize attachment or a new process. Shared task auxiliary shells continue to use canonical task membership instead of this scratch-panel admission rule.
Browser mode uses channel IDs to route PTY output over websocket.
Relevant files:
server/channel-frames.tsserver/browser-channels.tsserver/browser-server.tssrc/lib/ipc.tssrc/components/TerminalView.tsx
Each browser terminal binds to a channel. The server routes PTY output to that channel. Rebind and scrollback restore are channel-level recovery mechanisms.
Control-plane websocket events include things like:
agent-lifecycleagent-controllerremote-statusgit-status-changed- some
statusupdates
These now use replayable sequencing in electron/remote/ws-transport.ts.
Main files:
electron/main.ts(default-dark entry shim)electron/application.ts(side-effect-free, one-shot composition root)electron/ipc/register.tssrc/App.tsxsrc/lib/ipc.ts
Shape:
- frontend runs inside Electron
- frontend calls backend via
window.electron.ipcRenderer - backend work is handled through
createIpcHandlers(...) - no browser websocket transport is required for the main desktop UI
Electron mode is the most direct path: renderer -> Electron IPC -> backend services.
Main files:
server/main.tsserver/browser-server.tssrc/App.tsxsrc/lib/ipc.tssrc/runtime/browser-session.ts
Shape:
- a standalone Node/Express server serves the desktop UI
- the frontend still imports the same store/UI code
- command-style calls go over HTTP IPC
- terminal and event streams go over websocket
Browser mode is the most complex runtime because it combines:
- HTTP command/query plane for request/response backend commands
- websocket control plane for sequenced control events
- websocket channel plane for PTY output
- authenticated preview proxy routes for explicitly exposed task ports
Those three planes are now explicit in code:
src/lib/browser-http-ipc.tssrc/lib/browser-control-client.tssrc/lib/browser-channel-client.ts
src/lib/ipc.ts remains the façade that makes browser mode feel close to Electron mode to the rest of the UI.
Preview routing is handled separately in:
server/browser-preview.ts
Preview is not part of the websocket transport. It is an authenticated HTTP/WebSocket reverse-proxy concern layered on top of task-scoped exposure state.
Main files:
electron/remote/server.tselectron/remote/http-handler.tselectron/remote/ws-server.tssrc/remote/App.tsxsrc/remote/ws.ts
Shape:
- a separate mobile SPA is served from the remote server
- the mobile SPA has two explicit host compositions: Electron's dedicated remote server uses the
default
/auth/bootstrap+/api/auth/*session routes, while standalone browser mode namespaces the scoped principal under/remote/auth/bootstrap+/api/remote/auth/*and its socket under/remote-ws - standalone browser mode keeps the full browser administrator and scoped remote principals
separate: distinct long-lived tokens, distinct cookies/routes, generic
/api/ipcauthority only for the full browser principal, and/remote/*static authorization only for the scoped remote principal. Remote status/QR projection carries the scoped HTTPS bootstrap URL targeting/remote/; startup output prints the full-browser HTTPS bootstrap separately - coordinator agents that self-call a standalone HTTPS host receive the configured public certificate in their mode-0600 per-run credential, so the loopback tool URL verifies that exact trust anchor without globally disabling TLS verification
- remote UI talks directly to the shared websocket transport
- remote mode does not reuse the full desktop store
- legacy remote/mobile compatibility operates against the smaller agent-focused stream:
agentsstatusoutputscrollback
- scoped remote/mobile composes a separately versioned task catalog and task-first list/detail
route. A task detail opens the exact catalog
sessionId; shell sessions therefore remain usable without being copied into the broad agent projection, while an optional catalogagentIdsupplies presentation metadata only. - secure-session bootstrap validates and freezes an exact command/admission capability snapshot. Malformed or stale bootstrap responses retain no client authority; Notes, terminal control, kill, and task creation UI derive independently from that snapshot and still rely on backend admission.
- Task Notes content stays out of catalog/bootstrap and loads through the lazy Notes route. The common editor/controller owns draft and conflict behavior, while recovery-only controls are a nested lazy boundary and the terminal DOM/session remains mounted across Terminal/Notes switches.
- remote/mobile also now participates in the shared collaboration/control stream:
peer-presencesstate-bootstraptask-command-takeover-requesttask-command-takeover-resultipc-eventcontroller updates- both
src/remote/ws.tsandsrc/remote/remote-collaboration.tsnow classify handled versus intentionally ignored live messages explicitly so remote scope drift is visible in code review - the remote live
ipc-eventchannel set is shared insrc/domain/remote-live-ipc-events.tsso server emitters and remote consumers stay aligned - remote/mobile triage surfaces now project actionable row state from the pushed backend control
streams instead of recency-only renderer heuristics:
agent-supervisionowns waiting / ready / busy / protected / failed statetask-reviewowns changed-file and conflict summarytask-portsowns preview-port availability- task-command controller snapshots own blocked-owner / read-only truth
- peer presence remains a softer activity cue until controller snapshots confirm ownership
- replayed peer-presence entries are validated before sorting or projection so malformed transport data cannot crash the remote shell or become a false ownership cue
- remote task-state projections use the shared server-state version tracker for supervision, review, and task-port replay so stale live snapshots or removals cannot erase newer bootstrap truth
This runtime is still simpler than browser desktop, but it is no longer just a read-mostly shell. It shares session naming, presence, ownership, and takeover behavior with desktop while keeping its own agent-centric UI model.
Flow:
src/index.tsxrenderssrc/App.tsxsrc/App.tsxdelegates desktop startup/session coordination tosrc/app/desktop-session.tssrc/app/desktop-session.tssets up:- window session hooks
- shortcuts
- autosave
- startup reconciliation
- app-level listeners
loadAgents()andloadState()populate the client storeelectron/ipc/register.tshas already registeredcreateIpcHandlers(...)withipcMain.handle(...)- subsequent UI actions call store functions or app workflows, which invoke backend operations through
src/lib/ipc.ts
Important property:
- Electron mode uses the same frontend store and UI surface as browser mode, but a different transport path.
Flow:
server/main.tsbootstrapsserver/browser-server.ts; boot is snapshot-first and demand-driven:server/server-listener-policy.tsvalidates the credential and bind address before acquiring runtime owners. Ordinary startup defaults to127.0.0.1; explicit scoped HTTPS configuration defaults to0.0.0.0, andPARALLEL_CODE_SERVER_HOSTmay select either address. Missing/emptyAUTH_TOKENgenerates a fresh private token in the entrypoint; the retired public development token is rejected even on localhost. The selected bind policy flows to server-info projection, which advertises WiFi/Tailscale URLs only for a network-accessible listenerstartBrowserServeractivates the one production task-experience runtime before opening any HTTP or websocket surface.BrowserServerController.whenReady()resolves only after that owner is active and the listener is accepting requests; activation, transport composition, and listen failures reject it.server/main.tsawaits this contract, so failed startup is an observable nonzero process outcome rather than a process that stays alive without a port- persisted state is parsed once into a
SavedStateDocument(electron/ipc/saved-state-document.ts) shared by the registry sync, restore, and save/load/reconnect consumers instead of re-parsing the JSON per consumer electron/ipc/saved-state-restore.tsrestores watchers and hydrates persisted derived snapshots fromderived-state.json; no per-task git refresh is scheduled at boot- the coordinator runtime is not imported before listen;
server/coordinator-runtime-loader.tsdynamic-imports and initializes it on the serverlisteningevent, and every coordinator entry point (the/api/coordinator/tool-callroute and the lazy coordinator IPC group bound throughelectron/ipc/lazy-handler-group.ts) awaits the single load promise, so an early coordinator request is answered after init completes instead of being rejected. Owner acquisition is transactional: if a later initialization step fails, every earlier persistence, mutation-producer, and event-consumer owner is asked to release in shutdown order before readiness rejects. A successful rollback releases the serialized ownership turn so a later loader may retry; a failed rollback preserves both errors, leaves the turn unreleased, and rejects replacement admission - clients whose WS auth or cold bootstrap landed inside the load window received an empty
coordinator category; the loader repairs them after hydration by re-emitting the restored
runs as ordinary
run-upsertedevents (emitCoordinatorRunRepairEvents), and shutdown awaits the loader's asynccleanup()before the exit-on-close path. Cleanup first closes and drains coordinator mutation producers (active prompt chains, the workflow scheduler execution, admitted tool/renderer calls including their persisted result-ledger writes, and nested spawn rollbacks), then unsubscribes outbound event consumers, and flushes persistence last; every owner is attempted and failures are aggregated, so one rejection cannot skip a later cleanup or make a rollback mutation non-durable. Loader instances serialize owner acquisition behind the prior loader's full teardown, so synchronous browser-server cleanup can be followed immediately by an in-process replacement without overlapping persistence owners. Failed owner cleanup retains the failed admission barrier and rejects both public cleanup and any replacement loader, which makes signal-driven browser shutdown exit nonzero rather than acquiring owners over an uncertain predecessor; cleanup before thelisteningcallback settles the never-started loader explicitly - the Electron shell does not use the loader:
electron/ipc/register.tshydrates persisted coordinator state eagerly (ensureCoordinatorServiceLoaded) before binding IPC handlers so the renderer's firstGetServerStateBootstrapalready carries restored runs releaseBackendBackgroundWork()runs inside the listen callback as the single post-listen background scheduler
- the frontend loads
src/App.tsx src/App.tsxinitializes the same store and root UI shellsrc/app/desktop-session.tscoordinates the shared desktop startup pathsrc/runtime/browser-session.tsregisters browser-only runtime listenerssrc/lib/ipc.tscomposes:src/lib/browser-http-ipc.tssrc/lib/browser-control-client.tssrc/lib/browser-channel-client.ts
- state is loaded via the HTTP command/query plane
- ongoing control updates arrive over the websocket control plane
- terminal output arrives over the websocket channel plane
Important property:
- browser mode is shaped to feel like Electron mode to the UI, but the actual transport is explicitly split under the surface.
Flow:
electron/ipc/register.tscan startelectron/remote/server.tselectron/remote/server.tscomposeselectron/remote/http-handler.tsandelectron/remote/ws-server.ts- the remote server serves the mobile SPA
src/remote/App.tsxruns a much smaller app shellsrc/remote/ws.tsconnects through the shared websocket client coresrc/remote/remote-presence.tspublishes remote/mobile display name, focus, visibility, and current control statesrc/remote/remote-collaboration.tsapplies:- peer presence
- controller snapshots
- takeover requests/results
src/remote/remote-task-command.tsuses:- HTTP IPC lease requests for acquire / renew / release / resize / write
- websocket control messages for takeover request / response and shared lease result parsing
- the remote UI receives both terminal data and collaboration state, then projects them into:
- agent cards and previews
- ownership chips and read-only states
- takeover dialogs and result notices
- an identity-safe post-restore paint step after accepted recovery bytes and buffered live bytes have both reached xterm; stale/disposed callbacks and non-write completion remain no-ops
Important property:
- remote/mobile is not "the desktop UI in a smaller layout"
- it is a separate agent-view application that shares backend services, transport rules, and task-command control semantics
Desktop flow:
- user triggers a task action in the UI
- component calls a store or app-workflow action
- frontend workflow modules like
src/app/task-workflows.tsdecide the higher-level behavior - backend IPC is invoked through
src/lib/ipc.ts - Electron mode routes that through
window.electron.ipcRenderer.invoke(...) - browser mode routes it through the HTTP IPC endpoint registered by
server/browser-ipc.ts electron/ipc/handlers.tsvalidates input and delegates to backend workflows or low-level services- backend workflow modules like
electron/ipc/task-workflows.tsorchestrate:- task creation/deletion
- watcher setup or teardown
- PTY spawn coordination
- follow-up refresh or cleanup
- low-level services like
electron/ipc/tasks.ts,electron/ipc/pty.ts, andelectron/ipc/git.tsperform the underlying work - the frontend store updates based on:
- direct request success
- PTY lifecycle events
- websocket control messages
Important property:
- there is now a workflow layer on both the frontend and backend
- the remaining architectural question is how far to keep moving orchestration out of store slices and large handlers
Flow:
electron/ipc/pty.tsstreams task outputelectron/ipc/port-detection.tsextracts likely localhost ports from outputelectron/ipc/task-ports.tsupdates the runtime task-port registry- renderer clients receive pushed
task-ports-changedevents through:- Electron IPC in desktop mode
- browser control-plane replay/push in browser mode
src/app/task-ports.tsprojects those snapshots into preview state and URLssrc/components/PreviewPanel.tsxis the canonical preview and port-management surface:- it shows exposed preview ports
- it merges live scan candidates with advisory output-detected ports
- it makes the “detected from output” fallback explicit when no current listener scan succeeds
- it lets the user expose, retry, or unexpose ports without switching into a separate modal flow
- it also renders backend-owned task container inspect state, actions, logs, and declared app
previews through
TaskContainersPanel, while the preview controller owns inspect/log/action sequencing, stale-request suppression, and explicit error-state management
- browser mode opens exposed ports through
/_preview/:taskId/:port/*
Important properties:
- detection is advisory
- exposure is explicit
- preview proxy responses rewrite target cookies onto the preview path and strip upstream cookie domains so app sessions stay scoped to the Parallel Code preview origin
- task container previews are distinct from observed/exposed task ports:
- container previews come from backend-owned inspect truth
- task-port previews come from port detection, explicit exposure, and authenticated preview routes
- task-container preview workflow is distinct from presentation:
src/components/task-panel/task-panel-preview-controller.tsowns request ordering, stale-result suppression, and error statesrc/components/TaskContainersPanel.tsxrenders that workflow-owned state
- preview state is replayable after reconnect
- task deletion clears task-scoped port, review, permission, takeover, focus, and layout state
- opening preview is snapshot-first; the controller renders current task-port truth immediately and expensive candidate scans stay behind explicit rescan policy
Flow:
- task creation stores
stepsTrackingon the durable task config - backend task workflows register tracked tasks with
electron/ipc/task-steps.ts electron/ipc/task-steps.tswatches.claude/steps.json, normalizes rows, preserves explicit tracking-disabled tasks, repairs or stamps timestamps with the host clock, and emits replayable summary snapshots- renderer clients receive
task-stepsupdates through:- Electron IPC in desktop mode
- browser control-plane replay/push in browser mode
src/app/server-state-bootstrap.tsapplies compact summary snapshots during browser cold bootstrap and reconnect replay without shipping full history by defaultsrc/store/task-steps.tsstores:- compact per-task summaries for sidebar/attention/task-panel projections
- lazily fetched full snapshots when the steps surface is active
src/app/task-presentation-status.tsmaps ready step summaries into existingready-for-next-stepattention semantics instead of inventing a second readiness channelsrc/components/task-panel/task-panel-steps-controller.tsxlazy-loads the full snapshot only when the task is active or the steps panel is focused, thenTaskStepsSection.tsxrenders the history, next action, and jump affordancessrc/app/task-steps.tskeeps prompt seeding, next-action prefill, and jump routing aligned with current task focus owners
Important properties:
.claude/steps.jsonis the durable shared artifact; renderer code must not poll or parse it directly- browser cold bootstrap carries only compact summary snapshots by default
- full step history is fetched lazily per task
- backend timestamps are authoritative; model-provided timestamps are normalized, repaired, or overwritten before projection
- local UI state such as expanded cards or per-tab prefill remains local and is not shared
Electron mode:
electron/ipc/pty.tsreceives PTY bytes- output is batched and forwarded through the Electron channel bridge
src/components/TerminalView.tsxwrites output into xtermsrc/store/taskStatus.tsfronts the output-activity owners:src/store/agent-output-activity.tssrc/store/agent-ready-callbacks.tssrc/store/agent-question-state.ts
- those owners observe recent output tails and update question/prompt state
Browser mode:
electron/ipc/pty.tsemits output to a browser-mode channel callbackserver/browser-channels.tspackages output viaserver/channel-frames.ts- per-client/per-channel fanout and backpressure rules are applied
- channel frames are sent over websocket
src/lib/ipc.tsroutes channel payloads to terminal listenerssrc/app/terminal-output-scheduler.tschooses which terminals get render budget firstsrc/components/terminal-view/terminal-session.tskeeps the transport-aware session lifecycle, whilesrc/components/terminal-view/terminal-output-pipeline.tswrites output into xterm under that scheduler andsrc/components/TerminalView.tsxprojects focus/visibility priority into it- status/prompt detection runs in the frontend with slower background cadence
Important property:
- terminal output is the most performance-sensitive path
- it cuts across PTY, server shell, transport, and UI
- this area still resists aggressive abstraction
- noisy background terminals should not be able to keep themselves hot purely by repaint volume
- focused user-driven terminal transitions may use short app-owned preemption so task switches and typing stay ahead of background drain pressure
- focused terminals may still fast-path small plain output, but redraw-heavy control bursts should be paced so the UI does not expose every intermediate repaint frame
- that pacing works on raw bytes and must not invent terminal semantics: transport chunks are not ANSI boundaries, and the renderer still writes the original bytes to xterm unchanged
- queued browser control/channel diagnostics are generation-guarded so delayed channel sends and micro-batched control broadcasts from before a diagnostics reset cannot contaminate the next measured scenario
- more aggressive hidden-terminal suspension remains experimental until wake and restore costs are proven at the same browser-validation bar
Browser mode only:
- terminals bind a channel over websocket
- if the socket drops, the server may retain channel backlog briefly
- if backlog is too old or too large, the server marks the channel
RecoveryRequired src/components/terminal-view/terminal-session.tsdelegates batched terminal recovery tosrc/components/terminal-view/terminal-recovery-runtime.ts, which requests recovery throughsrc/lib/scrollbackRestore.ts- browser IPC uses
get_terminal_recovery_batchover HTTP IPC to fetch a backend-owned recovery result:- request state includes both the last applied
outputCursorand the retained rendered tail - backend prefers cursor-based delta when the requested cursor is still within the retained window
- rendered-tail overlap is the fallback delta path when cursor continuity is unavailable
noopdeltasnapshot
- request state includes both the last applied
- the terminal applies the lightest valid recovery and resumes live output
Visible non-shell startup attach uses get_terminal_startup_recovery_batch instead. That path can
return terminal-state, a backend-owned serialized xterm state produced from the PTY mirror, with
compact snapshot as the fallback if the mirror is unavailable.
Important property:
- browser recovery is now explicit catch-up, not implicit live replay of historical output
deltaandnooprecovery should stay non-blocking in the renderer; only full-state recovery (terminal-stateor snapshot fallback) should surface a blocking restore state- selected handoff protection may temporarily prioritize restore and replay for the selected terminal, but that ordering aid remains presentation/runtime policy rather than backend truth
- experimental live-surface caps remain presentation/runtime policy only and do not change backend recovery truth
- destructive reset is reserved for full-state recovery, not ordinary delta/noop continuity
- large-history terminals should stay stable under reconnect, backpressure recovery, and rebind
For the practical testing and debugging workflow around this area, including which browser-lab helpers to use and which lifecycle signals to trust, see TERMINAL-DEVELOPMENT-GUIDE.md.
Files:
electron/ipc/pty.tselectron/remote/protocol.tselectron/remote/ws-transport.tssrc/runtime/server-sync.tssrc/store/taskStatus.ts
Flow:
- PTY pause state is tracked by pause reasons in
electron/ipc/pty.ts - reasons include:
manualflow-controlrestore
- protocol helpers translate pause reasons into user-facing agent status
- transport broadcasts lifecycle/control events
- frontend runtime sync converts those messages into store updates
- UI components render those states through badges and status dots
Important property:
- this is one of the best examples of a concept crossing many layers
- the protocol and lifecycle derivation are now shared
- task-dot and attention semantics are canonicalized through
src/app/task-presentation-status.ts - local prompt detection still exists in the renderer, but only for terminal-local UX and one-shot prompt affordances
Files:
electron/remote/ws-transport.tsserver/browser-server.tsserver/browser-websocket.tselectron/remote/server.ts
Flow:
- an interactive command arrives from a websocket client
- the shell checks whether that action requires control ownership
- the shared transport layer enforces a short controller lease per agent
- on success, the backend command executes
- controller changes are broadcast as replayable control events
Important property:
- controller ownership now lives in one place
- command execution still lives in the shell that is handling the websocket message
Files:
electron/ipc/bounded-process.tselectron/ipc/git-exec.tselectron/ipc/git.tselectron/ipc/git-watcher.tselectron/ipc/git-status-workflows.tsserver/browser-control-plane.tssrc/store/task-git-status.tssrc/runtime/server-sync.ts
Current shape:
- browser mode prefers server-owned push and replay for git state and convergence state
- Electron mode still has some targeted on-demand refresh paths, but review and convergence ownership are no longer client-derived
Flow:
- backend watchers or git mutations invalidate or refresh git state
electron/ipc/git.tscomputes and caches worktree statuselectron/ipc/git-status-workflows.tsbuilds the normalized git payload and emits it to the relevant runtime- in browser mode:
server/browser-control-plane.tskeeps the latest worktree snapshot- authenticated clients receive replay of current snapshots
- live clients receive
git-status-changedpushes - the browser runtime updates local state from pushed payloads instead of polling for server-owned git state
- in Electron mode:
- pushed git state updates remain primary
- some advanced UI surfaces still use targeted on-demand refresh
src/store/task-git-status.tsandsrc/runtime/server-sync.tsmap pushed browser events into store updates
Important property:
- browser mode now has a clear canonical path: backend owns git state, server pushes and replays it
- asynchronous Git commands enter through
git-exec.ts, whilebounded-process.tsowns their one buffered-or-streamed process lifecycle, deadline, process-group/tree termination, and cleanup - the batched
git cat-filereader may fall back to individual safe reads after an ordinary Git failure, but not after bounded cleanup exhausts its deadline without confirming process-tree release - Electron mode is much closer to the same ownership model for git and convergence state
- the main remaining asymmetry is startup/restore contract alignment and a few advanced on-demand UI reads
- task-bound destructive dialogs consume
src/store/task-git-status.tsthrough shared selectors and refresh helpers; they do not fetch worktree status directly from dialog-local transport code
Files:
electron/ipc/git-diff-ops.tselectron/ipc/task-git-handlers.tssrc/app/review-diffs.tssrc/app/review-files.tssrc/components/review-panel/review-panel-controller.tssrc/components/ReviewPanel.tsxsrc/components/ScrollingDiffView.tsx
Current shape:
- backend owns changed-file enumeration and per-file diff semantics for both review and non-review surfaces
src/app/review-diffs.tsis only a routing seam between review surfaces and the typed backend IPC channels- review surfaces pass the actual
ChangedFilemetadata into that seam, so the backend can take status-aware fast paths without re-deriving file intent in the renderer src/components/ChangedFilesList.tsxhas explicittaskandworktreemodes:- task-bound surfaces read pushed task review snapshots
- generic worktree surfaces own branch-fallback and worktree-revalidation policy without regrowing backend truth in dialogs or leaf components
Flow:
electron/ipc/git-diff-ops.tscomputes changed files from 3 backend-owned sources:- committed branch delta via
git diff --raw --numstat <mergeBase> <head> - tracked worktree delta via
git diff --raw --numstat HEAD - untracked files via
git ls-files --others --exclude-standard
- committed branch delta via
- the backend supplements that split with
git ls-files -uso merge-conflict paths keepUstatus instead of collapsing toM - review and non-review file lists consume the same changed-file metadata, while presentation-only
helpers like
src/lib/changed-file-display.tsstay renderer-local - per-file diff requests flow through
src/app/review-diffs.ts, which routes committed files to branch diff IPC and worktree files to worktree diff IPC electron/ipc/git-diff-ops.tsuses status-aware fast paths:- untracked or added files synthesize text diffs without unnecessary history probes
- modified files load
git diff HEAD -- <file>,git show HEAD:<file>, and disk content in parallel - deleted files load only the worktree diff plus the
HEADblob
- repeat selections are served through a narrow backend diff cache keyed by repo path, revision, file path, status, and disk fingerprint
Important property:
- review mode does not own its own diff heuristics
- non-review and review surfaces share the same backend truth for
diff,oldContent, andnewContent - the main performance lever is backend subprocess fan-out, not renderer-side reinterpretation
- sibling task surfaces should stay on one canonical changed-file path; if a surface looks task-bound, it should consume task-bound review or git-status projections instead of choosing an ad hoc local fetch path
Files:
src/store/persistence.tssrc/store/persistence-save.tssrc/store/persistence-load.tssrc/store/persistence-codecs.tssrc/store/persistence-session.tssrc/domain/workspace-edit-intents.tselectron/ipc/storage.tselectron/ipc/workspace-state-storage.tselectron/ipc/workspace-state-mutations.tselectron/ipc/task-structure-mutations.tselectron/ipc/merge-progress.tssrc/domain/task-creation-provenance.tssrc/domain/task-merge.tssrc/app/merge-progress.tssrc/runtime/server-sync.tssrc/runtime/window-session.ts
Flow:
- the frontend periodically submits a full shared-state proposal with its last acknowledged workspace revision
workspace-state-mutations.tsserializes the load/CAS/pure-mutation/commit/publication turn by canonical host-storage identity- before structural admission,
task-structure-mutations.tsatomically stamps legacy tasks withpre-managed-v1, records the active backend writer epoch, and activates task-set, identity/location, and writer-provenance protection - semantic create/delete handlers add or remove membership through that owner; renderer full saves can edit fields and order canonical IDs but cannot manufacture, omit, or relocate a task
workspace-state-storage.tscommits one generation/digest-guarded host record: standalone usesworkspace-state.json; Electron keeps adapter-local fields and revisioned shared truth together in its existingstate.json- a changed shared write increments the shared revision once; every shared, private, or local write increments the canonical unsigned-decimal uint64 storage generation once
- projection preparation happens before the write; cache invalidation, complete projection
publication, and
WorkspaceStateChangedhappen only after an exact durability-healthy commit - on startup, the adapter holds its instance lock, fsyncs the containing directory, re-enumerates fixed candidates, and rereads the primary before enabling mutation
- saved state is loaded back into the store and runtime reconciliation checks live backend state against it; missing agents are marked exited and notifications may be shown
Non-obvious current rule:
system-handlers.tscreates exactly one workspace-mutation runtime perHandlerContext: the mutation host, reconnect saved-state cache, storage kind, projection callbacks, and removal lifecycle callback share that owner. Recomposition must retain the same storage kind and callback identities; a pre-supplied host is rejected instead of creating an untracked second authority. Lazy storage and structural-cutover failures remain retryable while admission is open. Shutdown closes admission first, settles any in-flight structural initialization, releases any subscription installed by that attempt, and then closes only a service that was actually created.- full-state loads and workspace-state loads now reuse the same canonical project and task hydration
helpers instead of maintaining parallel ad hoc parsing paths in
src/store/persistence.ts - only the canonical primary is a commit witness; temporary and backup files are validated recovery evidence and are never automatically promoted over a missing or invalid generation-aware primary
- runtime write failure is classified while the mutation queue is still exclusive: exact prior is proven not committed, exact proposal after acknowledged parent-directory fsync is committed, exact proposal without that acknowledgement is durability-pending, and anything else enters typed host recovery
- workspace and sharded-journal instance locks use retryable two-phase ownership. Acquisition records the exact process/token identity before its parent-directory fsync, so an ambiguous fsync failure remains closeable. Release retains that identity through exact-token reread, unlink, and parent-directory fsync; after unlink it records a pending-fsync phase and retries from there. Same-process replacement remains blocked until the exact release settles, and a missing parent directory during fsync is a visible durability failure rather than inferred success
- the protected-workspace policy registry and server-merge/rejection machinery initialize every
policy at version
0; the structural admission cutover stamps every extant task and atomically activates onlytask-structure,task-identity-location, andcreation-writer-epoch. Other semantic owners activate their named policies only with their writer cutovers; no policy is inferred from task shape - task creation, generic removal, and merge are active backend owners in one production composition for Electron and standalone browser mode. Activation establishes the generic removal epoch first, then the managed creation writer, then the merge owner; merge activation refuses to start if either prerequisite is absent. A source guard keeps both hosts on that shared ordering
- local and remote creation resolve projects, locations, agent definitions, runtime identities, and
coordinator metadata backend-side. The renderer applies the returned canonical workspace revision
and never constructs the committed
TaskorAgent. Remote managed creation needstask:create; project-root, imported-worktree, and permission-bypass requests additionally require their exact independent grants at both capability projection and final gateway admission - desktop optimistic creation allocates one
adapterOperationIdwhen a submit is admitted, outside the retry thunk. Managed local creation derives its private operation ID/capability from that adapter identity, so a lost renderer response and Retry replay one backend operation instead of creating another task. A distinct admitted form submission always gets a new adapter identity - task-creation Create admission is journal-first. Authorization, the lookup-lane limiter, journal readiness, pure semantic normalization/fingerprinting, and the known-operation lookup all precede live project, agent, branch, or worktree resolution. An exact committed operation replays without its expired ticket or vanished selections; changed semantics conflict. Only an absent operation consumes first-admission capacity and resolves current canonical selections, and a rejected selection creates no record or filesystem, workspace, session, or shell effect
- the trusted-local adapter reconstructs deterministic opaque branch/worktree references without reading canonical state. It checks for the derived known operation before issuing another local ticket, then relies on the shared workflow for the same fingerprint comparison and fresh-only selection validation as remote creation
- task-creation operation events are a bounded, capability-bound workflow projection. A client may
subscribe before the first record without learning whether the operation exists. Successful
durable record writes schedule snapshots only for an exact principal/capability match; unavailable
current state emits nothing.
refreshOperationrepublishes that same persisted record after catalog/current changes and cannot cross either authority boundary - fresh task-creation admission and journal replacement share one serialized conflict-index commit. Every managed worktree is deterministically planned from operation ID plus normalized intent before effects, reserves its exact worktree-path and branch keys, and rechecks that same plan before preparation. Active or retained overlap rejects the fresh record; reconciliation may later release only the exact predeclared keys that its owner proves resolved
- production composition owns one task-creation reconciliation service. It projects records through
the active creation workflow and delegates exact canonical/worktree/quarantine proof to the
preparation owner instead of rebuilding either policy. Two closure-bound commands inject
electron-mainorowning-user-cliauthority; request data cannot name an actor. The current desktop adapter registers bounded List and Execute directly with Electron main only—never in the shared handler map, browser HTTP, remote gateway, or WebSocket protocols—and the renderer facade fails before transport outside Electron - reconciliation List pages are tied to a fingerprint of the ordered operation-ID/record-version
candidate set. A mutation between pages returns explicit
stale, requiring a fresh first page, rather than splicing two journal states. Mapping adoption clears the scoped barrier intocreated-needs-attentionwith mode-appropriate launch/projection recovery; it never strands a committed task in the creation workflow's non-resumablestartingphase - failed-creation quarantine deliberately preserves its detached branch, so production records mark
branch cleanup
not-applicable. Exact quarantine absence can therefore terminalize the record and release its worktree key. Restore, unlock, and branch mutation remain proof-insufficient until their physical owners can supply the exact witnesses required by those transitions - remote creation stores only the operation access pair in session storage. It deliberately does not persist the raw ticket, semantic request, or prompt, so reload recovery is tracking-only. On receipt and again after every reload it starts a full ticket-TTL wait on a monotonic clock; browser wall-clock skew cannot shorten that wait. Only a fresh exact-absence lookup after the conservative wait releases the credential and permits a new request
- top-level coordinator tasks use the same managed creation owner. Coordinator
spawn_subtaskremains an explicit compound-runtime exception inelectron/coordinator/tool-gateway.ts: it owns a transient hidden worktree task plus a separately configured coordinator agent/credential, so it does not call the top-level trusted-local facade or claimcoordinatorMode. Its gateway tests own dedupe, late-admission, launch, and cleanup behavior - merge uses server-issued operation access and typed Issue/Start/Status channels. The renderer
retains only the access pair and semantic options for lost-response recovery; it sends no project,
worktree, branch, cleanup-plan, or Git evidence authority. The backend performs Git, delegates
cleanup to the generic removal owner, advances the full progress snapshot in the removal commit,
and disables the legacy merge writer before activating
merge-progressprotection - if canonical task absence arrives while every Start response is lost, startup/workspace
reconciliation status-joins the retained access pair. Status may terminalize
merged-awaiting-removalonly from the generic removal owner's durable commit evidence; pending or manual outcomes remain retained. A terminal join applies the fresh progress/removal projection and clears the credential, so repeated lost replies cannot strand the renderer's 32-entry recovery cap. Exact-operation durable credential release is single-consumer: whichever of Start completion or Status recovery wins may surface finalizer repair, preventing duplicate notices across their race - canonical task absence is projected after the renderer's command lease releases. This preserves the lease protocol while ensuring no renderer cleanup, local completion delta, best-effort save, or second removal owner can race the backend result
src/app/merge-progress.tsaccepts only newer complete snapshots; duplicate Status/workspace delivery is inert.SidebarFooter.tsxrenders that projection through one atomic polite status region, giving each accepted version one visual update and one accessible announcementTask.closeStateand terminalclosingStatusremain presentation-only. Persistence codecs echo closing, removing, and error rows; only the backend structural owner may change canonical task membership- renderer rename, active/collapsed reorder, project-setting, task-setting, and shared workspace edits use a bounded typed intent queue. Save dispatch marks represented values as submitted before invoking IPC, including pagehide writes. Only the unsubmitted same-field tail coalesces; submitted values remain ordered until canonical acknowledgement, so editing or undoing during a save cannot erase the final local choice. A newer canonical match to a later result acknowledges the satisfied same-field prefix, including skipped save responses. Otherwise replay requires the target to equal the edit's base; a genuine peer same-field change or removed target keeps canonical truth, retires the conflicting intent, and raises a persistent warning. Safe unrelated edits stay queued.
- auxiliary-shell membership uses per-shell intents in that same queue, preserving unrelated sibling
changes and deriving
shellCountfrom exact IDs. A matching newer revision acknowledges membership; an equal-revision snapshot cannot retire a close while an earlier add can still commit. The queue never reconstructs a removed task or removes a managed primary shell. Close reserves bounded edit capacity before terminating the process, publishes removal only after successful termination, and releases its reservation on every exit path. Existing autosave remains the retry owner; membership intent grants neither process-creation nor task-control authority. Task addition/removal, prompt drafts, and note drafts are not generic replayable JSON edits - desktop and browser autosave acknowledge the snapshot submitted by that save, not whatever is visible when its response arrives. Edits made during an in-flight save remain pending for the existing serialized follow-up save
Important property:
- persisted state is not considered fully authoritative
- runtime reconciliation is a second pass that repairs persisted assumptions using live backend data
src/store/persistence.tsis now a thin facade; save, load/reconcile, codec, and sync-session changes should stay in their dedicated owners instead of re-accumulating in one fileSaveWorkspaceStatekeeps its existing{ revision }response andWorkspaceStateChangednotification shape while delegating transaction ownership; ElectronSaveAppStatekeeps itsundefinedresponse and performs the legacy unversioned-to-generation-1 migration in place on its first acknowledged save
The backend additionally persists its own derived external-state snapshots:
electron/ipc/derived-state-persistence.tsowns<stateDir>/derived-state.json(formatVersiongated, debounced atomic writes, tolerant per-entry load)- on boot, both shells hydrate those snapshots through
electron/ipc/saved-state-restore.tsbehind exact identity filters against the registered task metadata; a missing or corrupt file simply boots with empty snapshot maps - hydrated snapshots are demand-repaired (selected-task refresh on focus, watcher-driven refresh on real changes, background reconciliation sweep for never-focused tasks) instead of being eagerly recomputed for every task at startup
Files:
src/app/remote-access.tssrc/runtime/browser-session.tsserver/browser-control-plane.tselectron/ipc/remote-access-workflows.tselectron/remote/server.ts
Flow:
- Electron can start or stop the remote/mobile server through backend remote-access workflows
- backend workflows map server state into a discriminated enabled/disabled remote-status contract
- browser mode replays remote-access status through the browser control plane
- the frontend store keeps remote-access status separate from browser peer-presence snapshots
- UI components render availability and any connected-client counts without making remote-access the owner of collaboration state
Important property:
- the status shape is now cleaner and more explicit than before
- start, stop, and refresh share one frontend generation guard so stale async completions cannot re-enable or disable remote access after a newer user action
- peer-count-only updates do not mutate disabled remote-access status, so stale browser-side count messages cannot violate the disabled-state invariant
- remote-access connected-client counts stay separate from collaboration peer presence; browser and remote/mobile presence now share one payload/runtime contract, while each shell keeps its own UI projection
These areas are in reasonably good shape:
- shared websocket client behavior in
src/lib/websocket-client.ts - shared websocket server behavior in
electron/remote/ws-transport.ts - protocol vocabulary in
electron/remote/protocol.ts - explicit browser transport planes in:
src/lib/browser-http-ipc.tssrc/lib/browser-control-client.tssrc/lib/browser-channel-client.ts
- runtime extraction from
src/App.tsxintosrc/runtime/*andsrc/app/* - browser control-plane composition in:
server/browser-control-plane.tsserver/browser-control-delayed-sends.tsserver/browser-peer-presence.tsserver/browser-task-command-takeovers.ts
- backend workflow modules in:
electron/ipc/task-workflows.tselectron/ipc/git-status-workflows.tselectron/ipc/remote-access-workflows.ts
- terminal-view lifecycle composition in:
src/components/terminal-view/terminal-session.tssrc/components/terminal-view/terminal-input-pipeline.tssrc/components/terminal-view/terminal-output-pipeline.tssrc/components/terminal-view/terminal-recovery-runtime.ts
- browser-only channel framing extracted into
server/channel-frames.ts
These areas have a clear reason to exist and a clear boundary.
The store now has more help from src/app/* workflow modules, but it still owns a mix of:
- pure client state
- UI mutations
- status derivation
- persistence behavior
- some workflow-style orchestration
Why this matters:
- components still reach into store APIs that may mutate local state, talk to the backend, or both
- the boundary between state projection and application behavior is better than before, but still not fully crisp
Browser mode is now correctly expressed as three planes:
- HTTP command/query plane
- websocket control plane
- websocket channel plane
That model is correct, but it is still the most complex runtime.
Why this matters:
src/lib/ipc.tsis still a high-value façade and lifecycle hotspot- reconnect behavior spans queue replay, control replay, and channel restore
- new features still need discipline to stay inside the right plane
Recent work moved major browser responsibilities into:
server/browser-ipc.tsserver/browser-websocket.tsserver/browser-channels.tsserver/browser-control-plane.ts
That said, server/browser-server.ts still owns:
- top-level browser server composition
- shutdown wiring
- backend composition for browser mode
- server-info and presence coordination
Why this matters:
- the shell is now understandable, but still one of the easiest places for browser-only drift to reappear
The backend now has a workflow layer. The remaining problem is not the lack of workflows; it is that the low-level service and handler surface is still large and uneven.
Hotspots:
electron/ipc/handlers.tselectron/ipc/git.tselectron/ipc/tasks.ts
Why this matters:
- large capability modules still make it easy to smuggle use-case behavior back into services
- the next quality gains are likely to come from shrinking and clarifying these modules, not from inventing more new layers
The desktop UI and authenticated scoped remote/mobile UI are task-centric. Scoped remote consumes a bounded task catalog and opens task detail by exact session identity; it does not load the desktop store. The legacy remote compatibility stream remains agent-centric. Browser mode additionally adds channel framing for terminal output.
Those distinct projections are intentional. The remaining challenge is keeping shared concepts consistent without expanding the legacy agent stream or copying desktop state into scoped remote.
Why this matters:
- new features must decide whether they belong to the shared concept, the desktop projection, the scoped task-centric remote projection, the legacy agent projection, or only one transport plane
Recent work improved several concepts:
- backend canonical agent status now exists
- browser git state now prefers server-owned push/replay
- review and convergence state are now backend-owned, pushed, and replayed
- task-dot, attention, and focus semantics now come from one canonical presentation mapper
- remote-access status now uses a clearer enabled/disabled contract
- remote presence now has one shared payload/runtime contract across browser desktop and remote/mobile, with runtime-specific projections kept at the UI edge
- browser replay and Electron startup hydration now register bootstrap-owned state categories through the shared bootstrap registry and session bootstrap gate
Still mixed:
- Electron git delivery still includes some targeted on-demand refresh in advanced UI surfaces
- remote presence projections are still runtime-specific because desktop and mobile show different shells, but they consume the same backend peer-presence truth
- browser viewport/focus geometry still needs targeted browser proof when it changes, even though committed PTY resize authority is tied to backend task ownership
- full-screen and alt-screen TUI restore still falls back to heavier redraw paths than ideal
Why this matters:
- these are the remaining high-value sources of semantic drift
Examples:
- module-scope websocket clients
- module-scope store
- module-scope pending request queues
- module-scope PTY session registries
Why this matters:
- it keeps call sites simple
- it still hides some ownership, startup order, and teardown rules
Recent work added stronger typing and stricter compiler passes for lifecycle-heavy modules.
Still weaker:
- some IPC event payloads are still narrowed from generic runtime shapes
- some protocol/store/shared-domain concepts still have parallel type surfaces
- strict optional/indexed-access guarantees are not yet universal across the whole repo
Why this matters:
- the highest-risk paths are in better shape
- the long-term quality goal is to make the safer type discipline boring and normal everywhere
These are the principles that best fit the current codebase and the direction of the recent simplification work:
Best example:
- browser-mode git state now prefers backend watcher/mutation updates, server-side snapshot replay, and pushed
git-status-changedevents
Why this matters:
- it reduces client polling drift
- it makes reconnect behavior easier to define
Good examples:
- websocket transport rules
- control-event sequencing
- backend canonical agent status
- backend convergence snapshots
- peer presence payload and runtime publication
- task presentation mapping
- bootstrap-owned state-category replay through the shared session bootstrap registry
Good examples:
src/app/task-workflows.tselectron/ipc/task-workflows.tselectron/ipc/git-status-workflows.tselectron/ipc/remote-access-workflows.ts
Why this matters:
- it keeps handlers thin
- it keeps low-level services honest
- it gives tests a stable unit for end-to-end behavior
Good examples:
src/runtime/window-session.tssrc/runtime/drag-drop.ts- browser transport split across explicit browser planes
Still mixed:
- browser reconciliation and some runtime sync still blend translation with policy
Good:
- shared websocket transport extraction
- browser shell decomposition into IPC, control-plane, websocket, and channel modules
Still mixed:
server/browser-server.tsremains the heaviest composition root
Good:
- more orchestration now lives in
src/app/*
Still mixed:
- store slices still carry some transport-aware or workflow-style behavior
Good:
- connection banners
- replay cursors
- control replay
- reset-and-restore signaling
- typed browser runtime lifecycle transitions
- reconnect restore now stays in
reconnectinguntil authenticated control traffic confirms the restore can actually begin - reconnect restore cancellation is explicit when transport churn, auth expiry, cleanup, or restore failure invalidates an in-flight restore
Why this matters:
- recovery is one of the hardest places for quality to drift silently
The terminal/output path crosses PTY services, transport, browser channel fanout, and UI rendering. The goal is clear contracts, explicit backpressure rules, and explicit recovery semantics, not fake uniformity.
That now includes an explicit latency policy for browser typing:
- control keys such as Enter and Ctrl+C flush immediately
- isolated single-key interactive input prefers an idle fast path instead of sitting behind the old 4-8ms browser batch delay
- short interactive bursts still use a tiny batch window instead of sending every key blindly
- large paste or bulk input stays on the bounded batching path
- the renderer does not own lease truth; it only asks the task-command lease session whether a retained lease is still hot
- browser task-command lease acquire / renew / release requests use authenticated transport identity
instead of trusting JSON
clientId, and pagehide release uses keepalive HTTP with the browser client-id header - the PTY service mirrors the same split:
- interactive input drains on
setImmediate - bulk input keeps the short timed batch
- interactive input drains on
- browser restore pauses are keyed by a renderer-generated restore lease id and renewed while a slow restore is active; stale resume ids cannot clear a newer scoped restore pause
- browser output pacing is also explicit:
- small plain focused output may still use the immediate path
- redraw-heavy control bursts are coalesced briefly instead of surfacing each intermediate frame
- that policy is pacing only; it does not strip control bytes or move terminal truth out of the backend
IPC.WriteToAgentis the one invoke channel with a targeted clone fast path because it is the terminal hot path and its payload is already a narrow string shape
Important property:
- latency tuning stays explicit at the workflow, transport, and PTY boundaries
- no speculative local echo or renderer-owned terminal truth was added
- paste, replay, and heavy-output behavior remain different policies from interactive typing
Good:
- stricter lifecycle typecheck pass
- typestate-style lifecycle handling in key transport/runtime code
Still mixed:
- some runtime boundaries still narrow generic payloads late
Good:
- node-side contract and reliability tests cover replay, control lease, reconnect, transport, latency, and browser-server behavior
- Solid screen tests now cover high-churn user-facing flows such as task actions, terminal lifecycle, sidebar behavior, pushed git updates, and remote-access UI behavior
- Playwright browser-lab coverage now exercises authenticated browser startup, fixture-driven terminal rendering, reload/restore, and representative multi-client takeover flows in a real browser
Why this matters:
- architectural cleanup is only durable if tests pin the contracts that should survive refactors
- usability regressions usually appear first in high-churn screens, not in low-level helpers
Testing matters here only where seam choice follows architecture.
- backend and transport contracts should be proven below the browser first
- churn-heavy UI behavior should be proven at the renderer seam that owns it
- startup, persistence, replay, and reconciliation need explicit coverage because ownership crosses backend, workflow, and projection boundaries
Validation policy, quality gates, and command guidance live in TESTING.md. Terminal/browser-lab workflows live in TERMINAL-DEVELOPMENT-GUIDE.md.
The current system does not need a new architecture.
The delta is narrower:
- keep product-behavior coverage expanding with the highest-churn UI surfaces
- keep startup, persistence, and reconciliation coverage strong as those flows evolve
- keep tightening canonical derivation and shared type contracts where new features touch them
- keep server-owned state push/replay semantics boring and consistent across runtimes
The next useful architectural work is targeted cleanup around ownership, derivation, and type boundaries, not another transport rewrite.
The current architectural approach is:
- keep the shared transport core stable
- make browser mode explicit instead of pretending it is a single transport
- prefer workflow modules for multi-step behavior
- prefer server-pushed state for server-owned concepts
- keep composition roots thin and keep business logic out of runtime adapters
- use stronger typing to catch lifecycle drift before runtime
Recent phases have already been moving in this direction.
Some rules are now treated as architectural guardrails rather than informal conventions:
- replayable server-owned state categories must register through the shared bootstrap registry
desktop-session.tsmust not add ad hoc startup listeners for server-owned statebrowser-control-plane.tsmust not become a second bootstrap registry or a UI policy layer; delayed sends, peer presence, and takeover workflow stay behind their focused backend owners- remote bootstrap and remote live-event paths must classify categories explicitly as handled now or intentionally ignored now; do not hide drift behind open-ended default branches
- once a module is split into facade plus focused owners, the facade should stay thin and architecture tests should target the real owner file rather than the pass-through shell
src/store/core.tsis the internal primitive store implementation; app, runtime, and presentation code should usesrc/store/state.ts,src/store/store.ts, or a narrower authority module insteadsrc/app/*andsrc/runtime/*should not importsrc/store/store.ts; that barrel stays component-facing, while app/runtime owners should depend onsrc/store/state.tsor the narrow store authority they actually need- controller consumers should read through controller selectors rather than reaching into the raw controller map
- focused-panel consumers should read through focus selectors instead of reinterpreting the raw
focusedPanelmap locally - task close lifecycle must stay on the discriminated
Task.closeStatemodel rather than reintroducing looseclosingStatusorclosingErrorfields - task removal and workspace reconciliation must share the same task-scoped cleanup authority, including any related module-local runtime caches
- controller ordering truth must stay separate from the live controller record so a newer clear snapshot still blocks older later arrivals
- review surfaces must keep file-list freshness behind shared review-state adapters and use the shared review-surface bootstrap instead of rebuilding review-session/sidebar wiring per surface
- task-row, attention, and dot presentation must stay behind the canonical task-presentation model rather than reading raw supervision or git state inline
- browser session naming and similar app-owned dialogs stay in the app/workflow owner; leaf chrome should reopen them through the shared action registry instead of creating a second owner
- queued takeover state should stay modeled and rendered as a queue when the owner keeps a queue; do not silently collapse it to the first request in a leaf component
- terminal startup visibility belongs to the shared terminal-startup owner; leaf chrome should not reconstruct aggregate startup progress by scanning mounted terminals or raw scheduler internals
App.tsxkeeps shell-level session/bootstrap and dialog policy;src/components/app-shell/*should stay presentational and reopen workflow behavior through explicit callbacksTaskPanel.tsxshould stay a section-composition shell; focus runtime, preview workflow, and dialog, and permission-flow orchestration belong behind the named task-panel owners instead of regrowing inline or in leaf panelssrc/components/review-panel/review-panel-controller.tsshould own review loading, request-token, and selection orchestration;ReviewPanel.tsxshould stay focused on rendering and light local derivation while shared review-session behavior remains insrc/components/review-surface-session.tssrc/components/terminal-view/terminal-session.tsstays the public terminal lifecycle facade; input dispatch, output/write flow control, and recovery/rebind behavior belong behind the named terminal-view owners instead of regrowing inline or drifting intoTerminalView.tsx- sidebar render order, sidebar keyboard order, and sidebar drag-reorder semantics must share the
same
src/store/sidebar-order.tsprojection family instead of recomputing grouping separately inSidebarTaskList.tsx,focus.ts, andSidebar.tsx - task-status notification policy stays behind the shared
src/app/task-notification-runtime.tsowner; provider-specific delivery lives behind the Electron IPC seam and the browser notification sink, whileSettingsDialog.tsxonly owns the capability-aware preference UI and permission prompt entry point. The persisted notification preference is provider-neutral and default-on; browser permission state must be modeled separately from the shared preference instead of disabling the setting when permission is stilldefault - review annotation mutation stays behind the shared
src/app/review-session.tsowner;ReviewCommentCard.tsxandReviewSidebar.tsxmay own local draft/editing state, but they should update existing annotations only throughupdateAnnotation(...)instead of inventing parallel mutation paths
Architecture tests back these rules so future feature work fails early when it starts to drift.
One current example is review UI: ReviewPanel.tsx, DiffViewerDialog.tsx, and
PlanViewerDialog.tsx now share src/components/review-surface-session.ts for review-session,
copy/export, and sidebar bootstrap instead of each rebuilding that wiring locally. The review
panel's remaining loading and selection state belongs in
src/components/review-panel/review-panel-controller.ts.
The architecture is in better shape than the earlier refactor phases assumed. The remaining gaps are narrower and more product-facing.
Recent hardening work made bootstrap/replay state ownership, review freshness, supervision presentation, and preview trust more explicit.
What still matters:
- proving reconnect churn, restore overlap, and multi-client browser behavior through scenario tests
- stress-testing long-lived browser sessions and the heavy terminal/replay paths with repeatable diagnostics
- keeping deploy-readiness smoke tests and canary checks as a first-class part of the release bar
Recent work added direct screen coverage for the highest-churn UI surfaces.
What still matters:
- keep adding screen tests when task creation, focus management, terminal UX, or review flows evolve
- add app-level scenario coverage when reconnect, restore, and pushed state behavior become more sophisticated
desktop-session.ts and persistence now have direct integration tests, but startup remains one of the easiest places for subtle regressions.
Why this matters:
- startup order bugs often look nondeterministic
- stale persisted state can silently corrupt UI assumptions if the tests drift
This is better than before, but still worth watching when future features land:
- remote presence projections across desktop and mobile shells
- git refresh behavior in advanced or future UI surfaces
That is acceptable, but it means terminal-related feature work should continue to treat:
- PTY behavior
- websocket/channel behavior
- UI recovery behavior
as one reliability-sensitive path, not as isolated modules.
The next quality phases should build on the current direction.
For deeper follow-up design ideas around terminal transport, multi-client control lifecycle, restore strategy, and invariant testing, see TERMINAL-INFRA-FOLLOW-UPS.md.
Goal:
- prove the existing design under real browser, reconnect, preview, review, and multi-client conditions
Targets:
- add browser-mode scenario coverage for auth/bootstrap, reconnect, restore overlap, and preview replay
- keep the task-deletion browser canary covering review/preview-open cleanup current as those surfaces change
- add standalone deploy smoke tests
- keep flake-management and diagnostics around the heavy latency/replay suites explicit instead of ad hoc
Goal:
- make the system measurable under load as well as architecturally sound
Targets:
- add stronger diagnostics for bootstrap timing, restore cancellation, preview probe failures, and control/backpressure behavior
- stress long-lived browser sessions, multi-client browser mode, and preview/review/supervision interactions
- keep
server/terminal-latency.test.tsunder an explicit flake policy instead of treating failures as anecdotal
Goal:
- finish the supervision/review/preview loop without reopening broad architectural churn
Targets:
- approval / permission center
- post-merge guidance and sibling-refresh/staleness flows
- preview and supervision polish driven by real usage and diagnostics
Why this order:
- the biggest remaining gap is proof and observability, not core architecture shape
- after that, the product can grow on the current foundation without another broad refactor campaign
When evaluating a new change, ask:
- Which runtime shells does this feature touch?
- Is this concept task-centric, agent-centric, transport-centric, or workflow-centric?
- Is the server or the client actually responsible for this state?
- Is there already a canonical place where this state should be derived?
- Is this logic transport behavior, workflow behavior, backend service behavior, or UI behavior?
- If browser mode needs special handling, does it belong in the HTTP plane, control plane, or channel plane?
- Are we adding a new message shape when an existing protocol concept should be extended instead?
If those answers are not obvious, that is usually a sign the change is crossing the wrong layer.