Ace is a collaborative coding environment that runs on the team's own machines. Old Ace ran one process per channel inside a cloud VM; most of its pain was the VM and cloud infrastructure, not the per-channel process. Ace keeps the process and drops the infrastructure.
Words in this document have the meanings in terms.
Ace is open source software that teams clone, fork, and operate on their own machines and in their own infrastructure or cloud accounts. Ace will not operate a hosted service. The tailnet is the sole authority for team membership and collaboration access; see the team.
| Path | Runs | Owns |
|---|---|---|
packages/channel |
inside a channel worker | The channel: chats, agents, lanes, and the client protocol types |
apps/host |
on every machine (Bun) | The ace CLI, channel workers, the channel catalog, local transport |
apps/desktop |
on macOS | The desktop client and packaging for Ace Helper |
services/channel |
Cloudflare Workers | Hosted channels: one Durable Object per channel |
services/directory |
Cloudflare Workers | The team's list of hosts and where each channel lives |
Clients (the CLI and desktop app) attach to channels; a channel is not a client.
The app's Issues and PRs pages read the selected project's GitHub repository through the host's GitHub CLI account. A peer can resolve the Git remote of a project already exposed by its channels; the app's own host performs authenticated GitHub reads. The app caches GitHub responses in IndexedDB, scoped to its host connection. It refreshes in the background at startup and every three hours, keeping cached content visible while requests run or fail. GitHub content stays outside channel history. Issues and PRs share the Channels sidebar and project picker, with Open, Closed, and All filters in the sidebar. GitHub issue and PR links open these pages inside Ace.
A channel owns one pi-durable Session. A local channel runs as one worker process over one SQLite file; a hosted channel runs in a team-deployed cell. A chat is one pi conversation in that Session.
- Durable state is pi's. Messages, runs, tool calls, and child chats are pi entries, tasks, and conversations. A crash resumes unfinished work from its last checkpoint; tools that are not safe to rerun report an interruption to the model instead.
- The model sees the whole room. A human message that does not invoke an agent is a pi
write: it enters the transcript without starting a run. Invoking an agent is a piinput. - Providers are optional until invocation. Creating a channel or chat, posting human messages, and using its workspace require no model or provider credentials. A chat without a model resolves its runtime's default on its first invocation: the local host's current preference, or an available model on the hosting service. Availability is checked before admitting the input, so a rejected invocation queues no work and leaves its draft editable.
- Model per run. The chat's model is set when a run is admitted. Changing it while a run is active is rejected rather than changing the active run's later turns.
- Subagents are child chats. A subagent's chat is owned by the parent's tool task, so killing the parent kills the child. Agents in different channels only exchange messages.
- Lanes are Git worktrees created by the chat's agent for each unit of work. A chat's working directory is its current lane.
- Kill is durable. Killing records pi's abort marks before the worker exits, so reopening the channel does not resume the killed work.
- Usage comes from pi's ledger. A chat reports cumulative model and tool usage from
UsageDoc, including compaction. The app shows input, output, cache tokens, and estimated USD at model catalog prices. Reopening the chat reads the same totals; Ace keeps no second ledger. - Names and summaries are durable metadata. The owner can rename a channel. The root chat's agent names a randomly named channel and keeps a short summary as work progresses, preserving deliberate names unless asked to rename. Metadata lives in a pi session document and is projected into host listings. Watch events update open clients; the details sidebar shows the full summary. Lane paths and branch prefixes keep the channel's original name.
packages/channel must stay runtime-neutral: no node:*, bun:*, or Workers imports. The host
injects storage, models, and the execution environment. This keeps hosted channels (a channel
inside a Durable Object, with tools served from a team machine) an adapter rather than a rewrite.
Process per channel gives isolation and instant kill, and lets thousands of channels exist as files: a dormant channel is a closed SQLite file with no process. It is process isolation, not a security boundary: every worker runs as the host's user and can read the others' files. Agents have full access to that user's execution environment by design. Collaborator invocation is controlled by one switch for all available tools.
apps/host keeps channels under $ACE_HOME/channels/<id>/ (default ~/.local/state/ace). Each
directory holds channel.json, channel.sqlite, and the channel's lanes. Clients start a
channel's worker on demand and talk to it over channel.sock with newline-delimited JSON. A worker
retires when it has no clients and no live work.
Channel data survives app replacement and upgrades. ace backup takes a verified SQLite snapshot
of a local channel while it runs, alongside its catalog record. Project files and lane worktrees
have their own repository backup. Stored format changes require migrations; see data.
Opening a folder adds a project to the host's projects.json, independently of channel creation
or model credentials. This is a list of local folders, not channel history. Existing catalog
channels also contribute their project paths. Only local owner connections can read or change
the opened-project list; teammates see the project information already present in channel listings.
A channel's project path is the checkout it works in: its agents' working directory and the base of its lanes. That checkout may be a Git worktree, such as a lane or another tool's worktree. The host groups every worktree under its repository's main checkout and labels it with the GitHub remote, so the picker shows one project per repository on a host without rewriting channel records. Opening a worktree adds its main checkout.
Model credentials come from ACE_<NAME>, <NAME>, then the OS keychain, for each name pi-ai asks
for, such as OPENAI_API_KEY. Agent shells inherit the process environment. Participants allowed
to invoke an agent can use all of its tools, so processes that run tools first move credential-like
variables out of the environment; only the key lookup can read them.
Workers resolve Keychain credentials on each model request, so adding, replacing, or removing a key through the app or CLI takes effect without restarting workers. Keychain access errors are distinct from missing keys. Settings exposes provider status and environment overrides, never stored key values; it validates new keys against the provider before saving them.
The default model for a chat's first agent run is a nonsecret preference in settings.json under
~/Library/Application Support/Ace on macOS, or $XDG_CONFIG_HOME/ace on Linux. The app and CLI
read the same preference; ACE_MODEL and an explicit per-channel model still take precedence.
Existing chats keep their chosen model. Adding credentials later makes an unconfigured channel's
agent available without recreating the channel or restarting its worker.
Host configuration is captured before credential cleanup. A worker receives the host's launch
environment, then seals credentials again before running tools. Source workers disable automatic
.env loading, so an unrelated working directory cannot change their credentials or configuration.
Stopping the host closes listeners and workspace links, then closes active local workers and their tool processes without writing durable abort marks. Shutdown identifies workers through their live Unix sockets; a stale PID file must never cause an unrelated process to be signaled or a dormant worker to start. A receipt of suspended worker IDs lets the next host startup resume unfinished work through pi without waiting for a client to reopen a channel.
Every host process writes JSON lines to $ACE_HOME/logs/<process>.jsonl: host (gateway, peers,
directory, workspace links), channel-<id> (one per worker), and cli. Each process owns its file,
writes synchronously so the lines before a crash survive, and rotates at 10 MB keeping four files.
ace logs merges them in time order and filters by --channel, --trace, --level, --since,
and --grep; --follow tails them.
Ids tie lines together: channel and chat on everything a channel does, trace on each
gateway request as it passes through peer hosts and into a worker, submission from a request to
its outcome, and task and call on model responses and tool calls. Channels log requests, model
responses with usage and provider errors, and tool calls with durations; message text is not
logged, but tool arguments are, clipped to 2,000 characters. debug lines (listings, watches,
directory syncs, workspace calls) are written only with ACE_DEBUG=1. Hosted services log the same
records to Workers Logs.
One Ace.app carries the desktop UI and Ace Helper, an independently running host executable.
The helper compiles with Bun 1.4 or newer and dispatches channel workers through the same executable.
The UI keeps Electrobun's bundled Bun version to match its native FFI callbacks. The helper resolves web
assets from its application bundle and writes its log under the channel data directory. Closing a
window or quitting the desktop leaves the helper running; reopening the UI verifies the host's
protocol, data directory, and a fresh proof of ownership before attaching.
The host stores a private owner token in host.token with mode 0600. The native UI supplies it only
to its local webview; ace open supplies it in a URL fragment that the app immediately removes.
The first WebSocket handshake authenticates with this token and sets an HttpOnly, SameSite cookie
for page reloads. The loopback listener checks Host and Origin, and Settings requests are forbidden
on the tailnet listener, including requests from the owner's other machines. A health challenge
binds discovery to the owner token, listening port, and process before the desktop or CLI opens
the app. As with channels, this does not isolate Ace from tools running as the same OS user.
On macOS, a bundled LaunchAgent runs Ace Helper as the logged-in user. A small native bridge calls
SMAppService to register it after first-launch consent and expose its approval status. Registration
starts the helper immediately and at subsequent logins. A disabled background item is left for the
user to enable in System Settings. The stable service identifier is dev.ace.desktop.helper; the
executable and desktop controls are named Ace Helper.
macOS groups the background permission under the parent app's name, Ace.
The desktop owns the native project picker and helper controls. Its webview uses a typed native bridge, authenticates each request with the owner token, and restricts navigation to the local app origin. Settings exposes Start, Stop, and Restart through this bridge, and its controls remain available while the host is disconnected. Stopping unregisters login hosting until the user enables it again. An authenticated command-line host can serve the UI, but the desktop does not manage that process as Ace Helper.
Git, shell, Tailscale, and directory diagnostics come from the host through the same local-only boundary as provider settings. The project picker selects a local Git checkout; project creation errors remain in the form so the person can fix the path or provider setup.
Agents inspect native windows through an injected desktop capability. The desktop embeds Peekaboo's native bridge inside Ace's UI process, which owns macOS Accessibility and Screen Recording permissions. The host invokes a bundled client over a local Unix socket. The bridge accepts only the client's exact identifier signed by Ace's team; the client verifies the host's signing team. Only application inventory, window inventory, and observation are exposed. The host bounds accessibility text and resizes screenshots before returning them. Quitting Ace stops inspection; Ace Helper can keep channels running. Hosted channels forward the capability to their workspace. Pi stores the result; channel clients project its images into the existing tool output. The collaborator-agent switch applies to desktop tools with the rest of the agent's tools. See native desktop inspection for setup and the current observation-only scope.
Development builds use dev.ace.desktop.dev, port 4141, and ~/.local/state/ace-dev, keeping them
separate from the installed app. Their preferences live under Ace-dev, and their Keychain service
is ace-dev. ACE_CONFIG_HOME and ACE_KEYCHAIN_SERVICE can target an isolated profile for
development or smoke checks. They are signed locally for SMAppService; distribution signing
and notarization are handled by the release pipeline. Sparkle checks for updates while Ace is open;
installation explicitly pauses and unregisters the helper before replacing the application. The
relaunched app restores hosting and resumes interrupted workers. See updates for
the handoff, signed feeds, hosting, and release credentials.
Teams deploy services/channel to run the same packages/channel core in a Durable Object,
one per channel. Hosted describes channel placement, not an Ace-operated service.
The object provides what the host's worker provides locally (single ownership of the store,
isolation, and routing by channel ID) and adds hibernation and placement off any one machine.
pi's portable SQLite core runs on the object's own SQL through storage.ts.
A Durable Object has no file system or shell, so a hosted channel's tools run on a workspace:
the host that created it. That host dials out to the object and serves each chat's file and shell
calls from its lanes, using the same execution environment as a local worker
(packages/channel/src/workspace.ts). The project checkout stays the source of truth. While the
workspace is disconnected, the channel stays reachable for chat, tools report the workspace as
offline, and a call in flight when it drops is reported to the model as failed.
- The workspace socket hibernates, so an idle hosted channel costs nothing while its host stays connected. Client sockets carry live pi event streams and keep the object awake.
- An alarm re-wakes the object while a run is active, so an evicted object resumes the run.
- Hosts share the team's secret with the service (
ACE_SECRETon hosts and Workers) and state each message's author. They verify their own users over the tailnet, so the service trusts hosts, not individual people. - Model keys are Worker secrets.
Shared state that must outlive any one machine lives in cells: Durable Objects deployed to the team's Cloudflare account, or celld on a team machine. The team deploys and operates these services. Each service is named for its job.
services/directory holds the team's hosts and where each channel lives. Every host publishes its
whole channel set on each change and every 30 seconds, then reads everyone's. It answers lookups
only: channel traffic goes to the channel's host over the tailnet, or to the hosted channel. So a
channel stays listed while its host is asleep. The app shows a local channel whose host is
unreachable as offline, and reaches a hosted channel directly. Hosts find the directory through
ace directory <url> and authenticate with the team's ACE_SECRET.
Lobbies (presence, notifications) are not built.
The tailnet is the team and the sole authority for team membership and collaboration access. Its identities and access rules decide which people and machines can reach each other. Ace has no separate accounts, invitations, roles, or team access controls. Every human author is a Tailscale login, so a person is the same participant on every host.
The channel owner can turn collaborator agent invocation on or off with ace share <channel> on|off.
This is one switch for all of the agent's available tools, including native desktop tools when
supported. It prevents new collaborator invocations; active work continues until stopped or
killed. There are no separate per-tool grants or approval policies within a channel.
Local owner tokens and browser-origin checks authenticate local connections. Team-deployed
services use ACE_SECRET to authenticate hosts and trust the participant identities those hosts
supply; the services do not check tailnet membership themselves. These mechanisms carry the
team's existing trust and do not define another way for a participant to join the team.
Each host's gateway serves its owner's app over loopback. Its tailnet listeners name each caller
with tailscale whois and accept peer hosts or the owner's browser from an allowed origin.
Teammates use their own host to reach shared channels. Local-only settings remain on loopback.
Hosts find online, untagged peers through tailscale status. A host shows its app every peer's
channels and proxies their requests; the peer that runs a channel stamps every author. Only a
channel's host creates it; only the host's owner, from any of their machines, archives, deletes,
or kills.
Terminals and previews, attachments, lobby cells, and external harnesses (Claude Code, Codex). Hosted channels cannot message other channels yet. Projects are still keyed by a host's main checkout, so a teammate's checkout of the same repository shows as a separate project.