All documentation

Processes and messaging

The processes OpenOrc runs, how the interface talks to the core, and what happens at startup and shutdown.

Checked against the source on September 27, 2026.

Every process

OpenOrc runs on Electron 44. Electron gives it three kinds of process of its own: a main process, a renderer for each window and each Preview view, and a utility process that OpenOrc calls the core. Everything else is a child of one of those.

ProcessStarted byWhat it does
Electron mainYour OSCreates windows and menus, starts the core, runs terminal shells, hosts the Preview browser, encrypts secrets, and checks for updates in packaged builds.
RendererMain, one per windowThe React interface. It runs sandboxed with context isolation and has no Node.js access.
Preview pagesMainA renderer for each Preview browser view, in a separate browser session. Up to six are kept alive.
CoreMainA full Node.js process named openorc-core, started with Electron's utilityProcess.fork. It owns the SQLite database, starts agents, runs Git, serves OpenOrc's tools to agents, and runs schedules and Slack.
Embedding workerCore, on first useA worker thread that computes memory embeddings, so model loading never blocks the core.
Agent CLIsCoreclaude, codex app-server, or opencode acp: one long-lived process per active run.
Short-lived CLIsCoreSign-in checks, model lists, usage readings, agent update checks and updates, and background text jobs such as memory extraction.
git and ghCoreWorktrees, diffs, snapshots, commits, pushes, and pull requests.
Login shellCoreRuns at startup, on Rescan, when Settings opens, when a team checks its members, and before agent update checks, to read your PATH and find the agent binaries. Not used on Windows.
Setup scriptsCoreA project's setup script, run with /bin/bash -lc when a new worktree is created.
Terminal shellsMainYour shell, one per terminal panel, through node-pty.
Google ChromeMainOnly when you choose Open in Chrome Incognito on a link: --incognito --new-window <url>.

The core also listens on 127.0.0.1 for OpenOrc's MCP server, and main listens on another loopback port to serve interactive tool results (MCP Apps) in sandboxed frames. The OS chooses both ports at startup. When you host a Slack team relay, the core also listens on 127.0.0.1 at the port you set, 47831 by default.

Source: apps/desktop/src/main/index.ts, apps/desktop/src/core/index.ts

Startup

Main process

  1. Chooses the profile folder: OPENORC_USER_DATA, or OpenOrc inside the OS app data folder. It sets the app name before Electron is ready, because on macOS and Linux that name selects the keychain entry used for encryption.
  2. Takes the single-instance lock for that profile. A second copy of the app focuses the first window and exits.
  3. Registers the openorc-asset: scheme, which serves attachments and images from disk to the interface.
  4. Starts the core with OPENORC_DATA_DIR set to the profile folder, then creates the first window.

Core

  1. Opens openorc.sqlite and applies any pending migrations.
  2. Compacts the database once, if it has a lot of free space and has never been compacted. The window shows "Tidying up saved history. This happens once and can take a minute." while this runs.
  3. Creates the Workspace home folder and builds its services.
  4. Recovers from the last session: runs that were still open are marked as errors, and interrupted comment replies and plans are marked as such. Team turns that were running are marked as needing attention, and team turns that had not started are queued to run; see Restarts and recovery.
  5. Probes your login shell for PATH and the agent binaries.
  6. Starts the MCP server, then tells every window it is ready.
  7. Sends messages still waiting in a conversation's queue, which can start agent runs right after launch. A message that was being sent when OpenOrc stopped is not sent again; it is marked as interrupted so you can check the conversation and retry it.
  8. Starts its timers. Every 60 seconds it wakes snoozed conversations and checks schedules; every fifth minute it refreshes the state of open pull requests with gh pr view. Database upkeep runs after 60 seconds, then every 6 hours. Agent update checks run after 30 seconds, then every 6 hours. It also embeds any memories that are missing vectors.

The interface can connect before the core is ready. Requests wait until startup finishes, and a window that connects late receives the latest startup or ready message.

Source: apps/desktop/src/main/app-identity.ts, packages/core/src/openorc.ts (OpenOrc.create)

Interface to core

Everything the interface shows from OpenOrc's records comes from the core. It asks main only for what needs Electron: terminals, the Preview browser, frames for interactive tool results (MCP Apps), the folder picker, opening links in your browser and showing files in their folder, new windows and window controls, and the setting for app update checks. When a window loads, it asks main for a connection to the core. Main creates a MessageChannelMain, gives one end to the core and the other to the window's preload script, and takes no further part. The preload script passes the port on to the page with window.postMessage, because message ports cannot cross Electron's context bridge.

Requests

Every request is a JSON-compatible object with a numeric ID chosen by the window:

{ "type": "rpc", "id": 42, "method": "runs.start", "params": { … } }

Methods are grouped by name: threads.*, runs.*, tasks.*, review.*, git.*, memory.*, orchestration.* (teams), slack.*, schedules.*, projects.*, agents.*, events.*, approvals.resolve, and a few smaller groups. Each method's parameters are a zod schema in packages/protocol/src/rpc.ts.

The core checks each message in three steps:

  1. The envelope must match the request schema. A malformed message is logged and dropped without a reply.
  2. The method must exist, and its parameters must pass that method's schema. Otherwise the reply is an error.
  3. Team-owned conversations and tasks reject methods that the team preview does not support yet.

The handler's result goes back as rpc.result, or rpc.error with a message. Replies are posted to every connected window, and each window keeps the ones whose ID matches one of its pending requests; see Known issues. The TypeScript compiler requires exactly one handler per method. Results are typed but not validated at runtime; the core is trusted. Requests have no timeout.

Setting OPENORC_RPC_LOG=1 logs every call that succeeds, with its duration and the start of its parameters. Parameters of Slack methods and memory settings are left out because they can carry tokens.

Source: packages/protocol/src/rpc.ts, packages/core/src/openorc.ts (handle), apps/desktop/src/renderer/src/lib/rpc.ts

Live updates

Besides replies, the core pushes these messages to every connected window:

MessageWhat it carries
frameNew agent events for one run. The core buffers events per run and sends at most one frame per run every 16 ms. The window applies frames in the order the port delivers them.
invalidateTags such as threads or thread:<id>. The window refetches only the queries marked with those tags.
notifyA desktop notification, such as a finished turn or a waiting approval. Sent only when notifications are on.
startup, readyStartup progress, then the core's process ID and MCP port.
logCore log lines. Only development and test builds have the Diagnostics screen that shows them.

Frames never touch the query cache. The window folds them into a separate transcript store. Before reading stored events, the core flushes pending frames and database writes, so a transcript loaded from the database never misses events that were still in flight.

There is no backpressure. Message ports have no flow control, so a very chatty run is limited only by the 16 ms batching.

Source: packages/core/src/frames.ts, packages/protocol/src/rpc.ts (CorePush)

Core to main

The core cannot open windows or use Electron's encryption, so it asks main over the utility process's own port. These are the only messages on that channel:

  • Port handoff: main passes a new message port each time a window connects.
  • Secrets: the core asks main to load or save the encrypted Slack configuration and the optional memory API key. Main encrypts with Electron safeStorage.
  • Browser commands: when an agent uses the browser tool, the core forwards the command to main, which drives the Preview.
  • Shell environment: the core sends the PATH it found, so terminals use the same one.
  • Shutdown and updates: main asks the core to shut down, or to confirm it is idle before an update is installed.

Inside the interface

The interface is React 19, built with electron-vite and styled with Tailwind 4. It has no router library: a small zustand store holds the current view, up to three side-by-side conversation panes, and back and forward history.

Data the window asks for lives in TanStack Query. Each query key is the method name and its parameters, and each query carries tags. When an invalidate push arrives, the window waits 50 ms to collect more, then refetches the tagged queries that are on screen.

Live transcripts live in a separate store. Each frame is folded into the affected run only, and the screen updates at most once per animation frame. Conversation views subscribe to single runs, so a busy background run does not re-render the conversation you are reading.

Opening a long conversation

  1. The window shows the newest run first and asks the core for its last 4 turns with events.page.
  2. Tool results whose stored event, input included, is longer than 4,096 characters arrive without their output. The output loads when you hover over or focus that row.
  3. Scrolling up loads older turns, 4 at a time, then older runs.
  4. Runs that are not on screen are dropped from memory after 3 minutes, unless they are still running or waiting for you.

Showing agent output

Agent replies are Markdown, rendered with Streamdown. Raw HTML in a reply is parsed and then sanitized to a GitHub-style allowlist, which removes scripts, styles, and event handlers. Code is highlighted with Shiki, math with KaTeX, and Mermaid diagrams render in Mermaid's strict mode. Remote images are not loaded; only attachments, images tools returned, inline image data, and local image files are shown.

The window's content security policy allows scripts only from the app itself, and images only from the app, data:, blob:, and openorc-asset: URLs.

Diffs render with @pierre/diffs, highlighted in two web workers. Terminals use xterm.js. The animated agent indicator is a WebGPU shader and falls back to a static icon when WebGPU is unavailable or reduced motion is on.

Drafts

Composer text is saved to the window's local storage on every change. A conversation's draft is also saved to the database 600 ms after you stop typing. Images pasted into a task document are staged in the window's IndexedDB until they are saved as attachments.

Source: apps/desktop/src/renderer/src/lib/query.ts, lib/transcript.ts, components/Conversation.tsx, components/ThreadImages.tsx

Finding your tools

Apps started from the Dock do not inherit your terminal's PATH, so on macOS the core asks your login shell. It runs your shell with -ilc and a small script that prints $PATH and the result of command -v for codex, claude, and opencode. The probe gets a minimal environment and 8 seconds to finish.

Only PATH and the binary locations are kept. Other variables your shell profile sets are not imported. The result is frozen as a numbered snapshot; each run uses the snapshot that was current when it started, and Rescan takes a new one.

On Windows there is no probe, and OpenOrc does not search PATH. It finds an agent only through OPENORC_CODEX_BIN, OPENORC_CLAUDE_BIN, or OPENORC_OPENCODE_BIN.

Each agent's readiness is checked with its own commands, with an 8-second timeout and a 60-second cache: --version and, at the same time, claude auth status, codex login status, or opencode auth list. The readiness check reads only whether you are signed in; the usage screen also shows your plan and, for Codex, your account email. OpenOrc never reads the credentials these tools store.

Source: packages/core/src/services/shell-environment.ts, packages/core/src/services/system.ts

Agent updates

The core checks the agent CLIs for newer versions 30 seconds after launch and then every 6 hours. This is on by default; the switch is Check for agent updates automatically in Settings, Connections, where Check for updates runs a check at any time.

A check probes your login shell again and runs the readiness check above. It then works out how each agent was installed from where its binary lives: a Homebrew cask, npm, Claude Code's own installer, or OpenCode's. For npm, it runs npm root --global to confirm that npm's global folder holds the binary. For Claude Code's installer, it reads the release channel from Claude's settings.json. Last, it asks the package registry for the latest version; see Network. A check never installs anything.

Updating runs only when you press Update Claude, Update Codex, Update OpenCode, or Update all. It refuses while an agent is working, an approval is waiting, a team execution is open, or background work such as memory extraction is running. Idle agent processes are closed first and start again with your next message. OpenOrc then runs one command per agent from your home folder, with a 3-minute limit:

  • Homebrew cask: brew upgrade --cask <name>
  • npm: npm install --global <package>@latest
  • Claude Code's installer: claude update
  • OpenCode's installer: opencode upgrade --method curl

Agents installed any other way, such as a Homebrew formula, pnpm, or bun, and every agent on Windows, are updated by hand.

Source: packages/core/src/services/agent-updates.ts, agent-update-installation.ts

Terminals

Terminal panels run in main, not in the core, so shell output never passes through the event ledger or the core's frame buffer. Main starts your shell ($SHELL, or /bin/zsh; on Windows, %COMSPEC%, usually cmd.exe, or PowerShell if that is unset) through node-pty in the conversation's folder. The shell gets main's environment, the PATH from the login-shell probe, and TERM=xterm-256color, COLORTERM=truecolor, and TERM_PROGRAM=OpenOrc.

Output is kept in a 256 KiB buffer per shell and sent to the window in 16 ms batches while a panel is attached. A shell survives closing its panel and reloading the window. It ends when you press Stop or Restart, or when you quit. OpenOrc refuses to install an update while a shell is running.

When a command in a terminal finishes, the Changes panel refreshes, so edits you make by hand show up.

Source: apps/desktop/src/main/pty-host.ts

Preview browser

The Preview panel is a Chromium view that main places over the window, sized to the panel. Each window has one view per conversation. OpenOrc keeps up to six views alive; beyond that, it closes the least recently shown view that is idle. Views run in their own browser session, separate from the app's pages, with no preload script and no Node.js access.

  • Every permission request (camera, location, notifications, and so on) is denied.
  • Downloads are cancelled and pop-ups are blocked.
  • Only http and https URLs without an embedded username or password load. Every navigation, redirect, and frame is checked.

Links in agent replies open here when the conversation has a Preview, and in your default browser otherwise.

When an agent uses it

Agents drive the Preview with OpenOrc's browser tool, which has seven actions: open, snapshot, screenshot, click, fill, press, and scroll.

  1. The core checks that the calling run is active and works out which conversation it belongs to. The agent cannot choose another conversation's view.
  2. The core sends the command to main, which runs one action at a time per conversation, with a 30-second limit. If the agent opens a page and no view exists, main creates one at 1000 × 720, hidden unless that conversation is on screen. Other actions need an existing view.
  3. Snapshots and form filling run a fixed script in an isolated JavaScript world, so page scripts cannot see or change it. A snapshot returns up to 16,000 characters of text and 200 visible controls, with password values left out.
  4. Clicks and key presses are sent as input events through the Chrome DevTools Protocol, attached to that view only. No remote debugging port is opened.

Source: apps/desktop/src/main/browser-pane.ts, browser-page.ts, packages/protocol/src/browser.ts

Shutdown

  1. Main closes every terminal shell and Preview view.
  2. Main asks the core to shut down, and waits for it to exit. After 12 seconds it sends the core a termination signal.
  3. The core stops its timers and integrations and denies pending approvals. Then it closes every agent process: an idle Claude process has its input closed so it can exit on its own, and any process still running gets SIGTERM to its process group, then SIGKILL after 2 seconds.
  4. It flushes the event ledger and the provider log, closes the MCP server, and closes the database.

Installing an update adds a check first. The core refuses while it is handling a request, an agent is working or waiting for approval, memory extraction is running, a team execution is open, or a schedule is starting. Main installs the update only after the core confirms and exits cleanly.

If you quit while an agent is working, its turn stops and the run is recorded as failed. Sending a message later resumes the provider session. After a crash, the next start marks every run that was still open as failed. See recovery.

Source: apps/desktop/src/main/index.ts, packages/core/src/openorc.ts (close), packages/agents/src/process-lifetime.ts