26 KiB
Orca Runtime Layer Design
Goal
Define the shared runtime/orchestration layer that makes the Orca CLI's live terminal contract implementable.
This layer is required because the current codebase splits ownership across:
- Electron main process:
- PTY process lifecycle
- PTY IDs
- PTY data and exit events
- Renderer:
- tabs
- split-pane layout
- active pane
- terminal titles
- buffered offscreen writes
- unread/activity side effects
- Persistence:
- repo config
- worktree metadata
- saved terminal layout snapshots
- saved tab state
That split is fine for the editor UI, but it is not enough for a CLI that needs:
- a stable
runtimeId - live terminal handles
- safe stale-handle rejection
- compact live summaries like
worktree ps - terminal reads and writes that do not depend on renderer-local pane IDs
- a real external transport path from the
orcaCLI into the running app
Problem Statement
Today there is no single shared service that can answer:
- what live terminal targets currently exist
- which worktree/tab/leaf each target belongs to
- which PTY each target is connected to
- what a safe public handle for that live target should be
- whether a handle is still valid
Relevant current ownership:
- PTY ownership: ../src/main/ipc/pty.ts
- Renderer tab state: ../src/renderer/src/store/slices/terminals.ts
- Pane lifecycle and PTY connection: ../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
- Pane -> PTY wiring: ../src/renderer/src/components/terminal-pane/pty-connection.ts
- Leaf ID serialization: ../src/renderer/src/components/terminal-pane/layout-serialization.ts
Non-Goals
This runtime layer does not try to:
- replace the renderer store
- replace the PTY implementation
- make pane IDs durable across reloads
- implement every future terminal automation feature in one step
The first purpose is to provide a shared control plane for the current app and CLI.
Current Constraints To Preserve
The design should stay honest about three existing realities:
-
Orca is effectively single-window today. The current PTY IPC wiring is attached to one
mainWindowand forwards PTY data back through that window'swebContents. -
Leaf-level terminal state is not yet first-class renderer state. Today Orca persists tab-level state and layout snapshots, but it does not persist a canonical renderer-side record for each leaf's title, preview, or screen snapshot.
-
Hidden terminals already accumulate deferred output in the renderer. The runtime layer cannot assume every hidden leaf has a continuously updated visible-screen model without adding new explicit publication behavior.
This means the first runtime layer should optimize for correctness in the current single-window app before trying to generalize further.
Core Design Principle
The runtime layer should be a main-process service that maintains a live registry built from:
- main-process PTY events
- renderer lifecycle registrations
- persisted repo/worktree metadata when useful
It should be the only place that:
- issues live terminal handles
- validates or rejects handles
- answers live summary queries
- exposes terminal read/write operations to the CLI
This avoids editor/CLI drift.
Source Of Truth Boundaries
The runtime layer must not replace existing durable sources of truth.
Durable truth remains:
- Git for worktree existence and branch state
Storepersistence for repo config, worktree metadata, and saved session snapshots
Live truth becomes:
- runtime layer for terminal handles, live leaf/PTy mappings, and live summaries
This means:
- the runtime layer may cache and index persisted state
- but it should not become the canonical persistence owner for repo or worktree metadata
- renderer/UI code should stop inventing separate live-terminal contracts once the runtime layer exists
Why Main Process Ownership
The runtime layer should live in the main process, not the renderer.
Reasons:
- the CLI will need to call into it even when no renderer component currently has focus
- PTY ownership already lives in the main process
- handle validation and stale-handle rejection are security and correctness boundaries
- renderer reloads should not destroy the authoritative registry object itself, even if they invalidate live handles
The renderer should publish registrations and updates into the runtime layer, not own the runtime layer.
Scope note:
- v1 runtime-layer design assumes one active Orca window
- multi-window support should be treated as a later extension, not an implicit requirement of the first implementation
CLI Transport Boundary
The runtime layer also needs a transport boundary for the external CLI.
Recommendation:
- expose a local-only RPC endpoint from the main process
- use:
- Unix domain socket on macOS/Linux
- named pipe on Windows
- persist connection metadata in Orca user data:
runtimeId- endpoint path
- auth token
- pid
Suggested flow:
- Orca main process starts the runtime service.
- Orca opens the local RPC endpoint.
- Orca writes connection metadata.
- CLI reads connection metadata.
- CLI connects locally and authenticates.
- Runtime service handles CLI requests against the live registry.
Security properties:
- local machine only
- random auth token required
- stale pid/socket detection on startup
Why this matters:
- Electron renderer IPC is not the CLI transport
- the main process runtime service is the right authority for requests coming from the external CLI
Runtime Identity
The runtime layer must generate a runtimeId when Orca launches.
Rules:
runtimeIdis unique per Orca process lifetime- any full app restart creates a new
runtimeId - renderer reloads do not necessarily require a new
runtimeId, but they may invalidate all live handles
Recommendation:
- keep
runtimeIdstable for the lifetime of the main Electron process - separately track a renderer graph epoch that increments only when the renderer graph is explicitly reset or replaced in a way that breaks existing leaf mappings
Why:
runtimeIdis the coarse session identity exposed in CLI responses- the renderer graph epoch is the finer invalidation boundary for ephemeral handles
CLI-facing simplification:
- handles are treated as ephemeral by default
- if the live graph is rebuilt in a way that invalidates mappings, all prior handles become stale
Public Responsibilities
The runtime layer must support:
status- live terminal discovery
- canonical selector resolution for CLI-facing repo/worktree lookups
- handle issuance
- handle validation
- handle-based terminal reads
- handle-based terminal writes
- compact worktree live summaries
Internal Data Model
The runtime layer should maintain the following registry objects.
RuntimeState
type RuntimeState = {
runtimeId: string
rendererGraphEpoch: number
graphStatus: 'ready' | 'reloading' | 'unavailable'
authoritativeWindowId: number | null
}
RegisteredTab
type RegisteredTab = {
tabId: string
worktreeId: string
title: string | null
activeLeafId: string | null
layout: TerminalPaneLayoutNode | null
lastSeenAt: number
}
RegisteredLeaf
type RegisteredLeaf = {
tabId: string
worktreeId: string
leafId: string
paneRuntimeId: number
ptyId: string | null
ptyGeneration: number
lastOutputAt: number | null
lastExitCode: number | null
preview: string
tailBuffer: string[]
connected: boolean
writable: boolean
lastSeenAt: number
}
TerminalHandleRecord
type TerminalHandleRecord = {
handle: string
runtimeId: string
rendererGraphEpoch: number
worktreeId: string
tabId: string
leafId: string
ptyId: string | null
ptyGeneration: number
createdAt: number
}
Why these fields matter:
leafIdgives stable layout identity within the current renderer graphptyIdis needed for actual write routingptyGenerationprevents a restarted PTY in the same leaf from inheriting an old handletailBufferpowersterminal readpreviewpowers cheap discovery andworktree pswritableprevents CLI writes from racing against renderer-driven close or detach flows
Handle Semantics
Handles are synthetic public identifiers issued by the runtime layer.
Rules:
- handles are opaque
- handles bind to:
runtimeIdrendererGraphEpochworktreeIdtabIdleafId- current
ptyId - current
ptyGeneration
- handles are invalid if:
runtimeIdno longer matchesrendererGraphEpochhas advanced past the handle's epoch- the leaf registration no longer exists
- the leaf now points at a different
ptyIdorptyGeneration - the handle's current target cannot be resolved
This is intentionally strict.
Why:
- the CLI must never silently retarget input to a different live terminal
- handle invalidation should happen only for real remapping events, not every routine reconciliation pass
Stale-handle ergonomics:
- stale-handle errors should include the current
runtimeId - if the target leaf still exists but the specific handle is stale, the runtime layer may include a rediscovery hint scoped to that worktree or leaf
- the runtime layer should not implement a magical handle refresh that silently retargets the caller
Event Sources
The runtime layer needs two classes of inputs.
A. Main-process PTY events
Current source:
Add runtime-layer integration points for:
- PTY spawned
- PTY data
- PTY exit
- PTY kill
What the runtime layer should record:
ptyId- PTY generation changes for a leaf
- data arrival timestamps
- exit code
- a bounded text tail buffer
B. Renderer graph publication
The renderer already knows:
- when a tab exists
- what the saved and current layout is
- which leaf is active
- which pane has which current PTY
- titles derived from OSC updates
The runtime layer needs renderer-published graph state like:
- which tabs currently exist
- which leaves currently exist
- which worktree each tab belongs to
- which PTY each leaf is currently attached to
- which leaf is active within each tab
- what the current layout tree is for each tab
These are not current public APIs. They should be introduced as an explicit internal IPC channel.
Important source-of-truth rule:
- leaf records in the runtime registry are authoritative only when published by the renderer's full-graph sync
- persisted session state and renderer store state remain advisory inputs for tabs and worktrees, not a substitute for live leaf publication
Suggested Internal IPC Contract
These are not CLI commands. They are editor-runtime plumbing.
Renderer -> Main
runtime:syncWindowGraph
Recommendation:
- start with one idempotent full-graph message as the source of truth for renderer-owned tab and leaf structure
- allow the renderer to resend the full graph whenever tab, layout, active-leaf, or PTY attachment state changes
- add narrower incremental messages later only if performance proves it necessary
Suggested payloads:
type RuntimeSyncWindowGraph = {
windowId: number
tabs: Array<{
tabId: string
worktreeId: string
title: string | null
activeLeafId: string | null
layout: TerminalPaneLayoutNode | null
}>
leaves: Array<{
tabId: string
worktreeId: string
leafId: string
paneRuntimeId: number
ptyId: string | null
}>
}
Why payloads matter:
- this is where ownership boundaries become real
- if these messages stay vague, implementation will drift back into ad hoc IPC
- treat full-graph sync as both the normal publication path and the repair path if an earlier renderer event was missed
Single-window v1 rule:
- Orca should accept exactly one authoritative publishing window in v1
- if a second window starts publishing, the runtime layer should reject it or mark the graph unavailable until the conflict is resolved
windowIdexists to make that restriction explicit now and extensible later
Main -> Renderer
Only if needed for editor features:
runtime:handleInvalidatedruntime:statusChanged
The initial version can keep the runtime layer mostly main-owned and query-driven.
How The Renderer Should Integrate
The renderer should publish runtime graph snapshots from the same places that already own lifecycle.
Recommendation:
- start with event-driven full snapshot publication
- do not add granular register/update/remove messages unless profiling shows the full graph is too expensive
- build the sync payload in one renderer-side collector/helper, and let lifecycle sites only schedule that helper rather than hand-assembling payload fragments
Why:
- renderer lifecycle is complex
- split/close/reload sequences are easy places to lose one incremental event
- a full snapshot lets the main process repair drift instead of accumulating ghost leaves or stale mappings
- the initial implementation needs correctness more than minimal event chatter
- a single collector reduces the risk that
runtime:syncWindowGraphlogic gets duplicated across store and pane lifecycle code
Tab lifecycle
Source:
Integration:
- when a tab is created, changed, or closed, republish the full graph
- when layout snapshot changes, republish the full graph
Leaf/pane lifecycle
Source:
- ../src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
- ../src/renderer/src/components/terminal-pane/pty-connection.ts
Integration:
- on pane created or closed, republish the full graph
- on active pane change, republish the full graph
- on PTY spawn or detach, republish the full graph
- on PTY respawn for an existing leaf, republish the full graph and let the runtime layer advance
ptyGeneration
Why the code needs comments
When this runtime graph publication is added, it needs comments explaining why Orca duplicates renderer lifecycle into a main-process registry:
- the CLI needs a shared live control plane
- pane IDs are renderer-local and not safe as a public contract
- handle validation must not depend on renderer-local assumptions
Those are design-driven constraints and should be documented in code comments per AGENTS.md.
How The Main PTY Layer Should Integrate
Current PTY code:
Required additions:
- publish PTY spawn/exit/data events to the runtime service
- maintain a lightweight PTY registry accessible to the runtime service
Suggested PTY event shape:
type RuntimePtySpawned = {
ptyId: string
loadGeneration: number
}
type RuntimePtyData = {
ptyId: string
data: string
at: number
}
type RuntimePtyExit = {
ptyId: string
exitCode: number
at: number
}
The runtime service should not parse terminal DOM state. It should build read models from:
- PTY output bytes
- renderer registrations
Selector Resolution Service
The runtime layer should own canonical selector resolution for repo and worktree selectors rather than leaving it to the CLI frontend.
Why:
- selector semantics are part of the public contract, not presentation glue
- if the CLI resolves selectors differently from editor-driven integrations, Orca will drift
This service should:
- accept tagged selectors like
id:,path:,branch:, andissue: - reject ambiguous bare values with structured ambiguity errors
- return stable repo or worktree identities that downstream runtime operations can use
The terminal layer should remain handle-first once discovery is complete, but selector resolution must still be runtime-owned for consistent discovery semantics.
Single-Window Assumption In V1
The current app is effectively single-window, and the first runtime layer should embrace that instead of pretending multi-window support already exists.
Recommendation:
- one runtime service per app process
- one CLI target runtime per app process
- v1 should permit only one authoritative publishing window
- if multiple windows appear later, they may register into the same runtime service only after Orca has an explicit multi-window routing model
The runtime layer should not issue window-scoped handles.
Terminal Read Model
terminal show and terminal read need cheap buffers.
The runtime layer should maintain:
previewtailBuffer
Preview
Purpose:
- cheap discovery
- worktree summary
Strategy:
- derived from most recent meaningful lines
- capped to a few hundred characters
- should be main-owned once PTY data reaches the runtime service
Tail buffer
Purpose:
- powers
terminal read
Strategy:
- bounded ring buffer by line count and char count
- updated from PTY output
Visible screen snapshots
Visible screen snapshots should be treated as a later enhancement, not a required v1 runtime primitive.
Why:
- hidden panes currently accumulate deferred output in
pendingWritesRef, so a renderer-owned "current screen" is not uniformly trustworthy across visible and hidden leaves - the CLI needs an honest contract more than a more ambitious but misleading one
So:
terminal showshould rely on runtime-owned metadata plus preview- initial
terminal readshould rely on runtime-owned PTY tail data only - if Orca later adds explicit visible-screen publication, that can be layered on as an optional richer read mode rather than a v1 requirement
Drift Recovery
The runtime layer should assume registrations can drift.
Examples:
- renderer reload before all leaf removals are delivered
- pane closes while PTY exit is also firing
- a restored tab graph replaces leaf IDs
Recovery strategy:
- event-driven full graph sync for correctness
- explicit epoch bump only when the renderer graph is reset or replaced incompatibly
- reject stale handles instead of trying to preserve them through remaps
This is another reason handles should be treated as ephemeral by default.
Reload And Unavailable States
The runtime layer needs an explicit graph-availability state rather than assuming the renderer graph is always present when PTYs exist.
Recommendation:
- enter
graphStatus: 'reloading'when the authoritative renderer is tearing down or the window is reloading - enter
graphStatus: 'unavailable'if no authoritative renderer graph is available - return to
graphStatus: 'ready'only after a fresh successfulruntime:syncWindowGraph
Why:
- the current PTY layer can briefly keep PTYs alive while the renderer graph is gone or rebuilding
- CLI calls should fail closed during that window instead of acting on stale registry state
Behavior:
terminal list,terminal show,terminal read, andterminal sendshould reject with a distinct runtime-unavailable error whilegraphStatus != 'ready'statusshould still work and report why the live terminal graph is unavailable
worktree ps Summary Model
worktree ps should be powered by the runtime layer, not persistence alone.
For each worktree, it should summarize:
- repo
- branch
- linked issue
- unread metadata
- live terminal count
- whether any terminal is attached to a live PTY
- last output time if known
- recent preview if useful
Recommendation:
- compute this in the runtime service from:
- persisted worktree metadata
- live tab/leaf registrations
- PTY connectivity
Batch-read note:
worktree psis the preferred cheap batched live summary for many worktrees in v1- Orca should avoid a second overlapping batch-preview primitive until real usage shows
worktree psis insufficient
The runtime layer should expose a single summary builder used by both:
- CLI
worktree ps - any future editor surfaces that want the same live summary semantics
Why:
- the CLI needs a cheap orchestration summary across many worktrees
Wait Semantics
terminal wait needs to be split by what is actually observable.
Safe first support
exit
This can be grounded in PTY exit events.
Later support requiring instrumentation or heuristics
outputidleinput
Why:
- current code does not expose a first-class “waiting for input” state
- title heuristics exist in the renderer, but they are not sufficient as a strong CLI contract
Recommendation:
- runtime layer v1 supports only
wait --for exit - later phases may add:
- output wait from PTY data arrival
- idle wait from time-based quiescence
- input wait from agent-specific instrumentation, not generic shell guessing
Failure Modes And Safety Rules
1. Stale handles
Must fail explicitly.
Never silently redirect to:
- another leaf with the same title
- the current active leaf
- another PTY in the same tab
This includes PTY restarts inside the same leaf. A restarted process must not inherit an old handle.
2. Renderer reload
The current code already kills prior-generation PTYs on page reload in ../src/main/ipc/pty.ts.
The runtime layer should treat renderer reload as a graph invalidation event:
- bump
rendererGraphEpoch - invalidate all old handles
- require fresh discovery
During the reload window:
- set
graphStatustoreloading - reject live terminal operations until a fresh graph sync completes
3. Missing renderer registrations
If the runtime layer has PTYs but no renderer graph for a target:
statusmay report degraded runtime health- but terminal discovery and live terminal operations must not surface orphan PTYs as valid targets
This should surface as capability truth, not silent omission.
4. Closing or detached targets
If a leaf is present in the graph but is no longer writable:
- mark it
writable: false - reject
terminal send - continue to allow metadata reads when useful
Why:
- current Orca shutdown and PTY replacement flows are partly renderer-driven
- the CLI should not race writes into a target that Orca is intentionally closing or detaching
V1 definition:
writableshould be computed from facts Orca can actually observe now- a target is writable only when:
graphStatus === 'ready'- the leaf exists in the current authoritative graph
ptyId != null- the leaf is still marked
connected
- if Orca later adds an explicit renderer-side closing or detaching marker, that can tighten
writablefurther
Proposed Implementation Phases
Phase 1: Runtime identity and service skeleton
Deliver:
runtimeId- main-process runtime service object
statussupport- lifecycle wiring hooks only
Phase 2: Local CLI RPC transport and runtime metadata
Deliver:
- local socket/pipe listener
- auth token bootstrap
- request/response envelope shared by the editor and CLI
- runtime metadata file in Orca user data
Why this comes early:
- the CLI contract depends on a real runtime transport boundary
- it is better to lock the transport and auth model before layering more command handlers on top
Phase 3: Renderer graph sync and PTY event ingestion
Deliver:
runtime:syncWindowGraph- tab/leaf graph registry
- PTY attach/detach mapping
- tail buffer updates from PTY events
- preview generation
Phase 4: Handle issuance and validation
Deliver:
- handle generation
- handle lookup
- stale-handle rejection
- replacement hints in stale-handle errors when safe
terminal listterminal show
Phase 5: Read and write surface
Deliver:
- main-owned tail ring buffer
terminal readterminal sendgraphStatus-aware rejection during reload and unavailable windows
Phase 6: Summary service
Deliver:
worktree ps
Phase 7: Optional richer terminal reads
Deliver:
- renderer-published visible screen snapshots if Orca proves it needs them
Phase 8: Wait support beyond exit
Deliver:
wait --for exit- explicitly defer the rest until instrumentation exists
Recommended File/Module Shape
Main process:
src/main/runtime/orca-runtime.tssrc/main/ipc/runtime.ts
Renderer integration:
src/renderer/src/runtime/sync-runtime-graph.ts- targeted calls from:
terminals.tsuse-terminal-pane-lifecycle.tspty-connection.ts
Why separate files:
- start with one runtime service and one IPC entrypoint so the design stays easy to land
- split handle, registry, and buffer helpers into separate modules later only if the implementation earns that complexity
Open Questions
- Should
runtimeIdchange only on app restart, or also on explicit renderer graph reset?
Recommendation:
- app restart only
- use
rendererGraphEpochfor graph invalidation
- Should handles encode any meaning, or be fully opaque?
Recommendation:
- fully opaque
- Should visible screen snapshots be pushed continuously or only on demand if Orca adds them later?
Recommendation:
- defer this until after the tail-buffer-based runtime contract is stable
- if added later, start with on-demand or throttled publication for visible leaves only
- Should terminal previews come from screen snapshots or tail buffers?
Recommendation:
- use tail buffer for preview generation
- reserve visible screen snapshots for an optional richer read mode later
- Should Orca support more than one publishing window in v1?
Recommendation:
- no
- keep one authoritative publishing window until PTY routing and renderer graph ownership are explicitly multi-window-safe
Recommendation
Build the runtime layer as a main-process orchestration service with:
- stable
runtimeId - renderer-published full tab/leaf graph sync
- PTY-event integration
- local CLI RPC transport
- opaque handle issuance
- strict stale-handle rejection
- bounded read models for discovery and terminal reads
That is the smallest honest architecture that can support the Orca CLI's live terminal contract without drifting from the editor.