# Developing This document covers day-to-day development workflows for the web relay, its session workers, and the Copilot CLI extension, plus the relay internals that used to live in the README. The [README](README.md) is written for people who install and use OAR. ## Naming: OAR vs "copilot" The product is **OAR — Open Agent Relay** (`@oar-sh/oar`, binary `oar`). Write it `OAR` in prose and `oar` in code. Many internals deliberately still say "copilot": the `COPILOT_WEB_RELAY_*` env vars, `.copilot/`-derived host paths, DB tables and cookies either address the real GitHub Copilot CLI's contracts or are invisible identifiers whose rename would only buy a migration. Do not "fix" them in passing — the deferred rename list lives in `docs/plans/oar-rebrand-and-release.md` §11. The database file did get the product's name in 0.9.20: `oar.db` for a new database, `copilot.db` still read on installs from before (see [The database file and its backups](#the-database-file-and-its-backups)). ## Rules for coding agents These rules apply to any agent working in this repository (they were in the README until 0.9.3): - Do not restart the web relay unless the user has explicitly given permission. If a manual restart is requested, use `POST /api/relay/shutdown` only — never restart the relay by killing processes (see [Manual relay control](#manual-relay-control)). - Do not run tests that spawn Copilot CLI clients unless the user explicitly permits it. Unit tests (`npm test`) and e2e tests (`npm run test:e2e`) are isolated from a live relay and may run beside it; see [Tests](#tests). - Do not run extension-managed relay transport together with standalone relay runtime transport (see [Single runtime owner rule](#single-runtime-owner-rule)). - Work on a topic branch, never on `main`. Push it to the private remote `work` (a bare `git push` does that), never to `origin`. Land finished work with `npm run land`. Never pass `--no-verify` to `git push`, and never set the `OAR_ALLOW_PUBLIC_*` variables unless the user asked for exactly that (see [Branches and landing](#branches-and-landing)). - Invent every name that goes into a fixture, a comment, a doc or a commit message. Do not copy what a live relay, a log or this machine shows you: session titles, repository slugs, branch names, ticket ids, accounts, hosts. - Do not add a dependency the repository does not use yet (a package, a binary, a tool the build or the tests call) without a check of the exact version first: its public source and whether the published artefact matches it, the licence, the publisher and release history, what the artefact contains (binaries, WebAssembly, encoded blobs, install scripts, network calls), its own dependencies, known advisories, and one or two alternatives. Record the result (adopt, adopt with conditions, reject) with its evidence in the plan or the pull request; only a clear adopt goes in. A package from the maintainers of something the repository already depends on, or a long-established and widely used one, needs no such check; when in doubt, it does. `.github/copilot-instructions.md` carries the same restart policy for Copilot sessions. ## Repository layout ```text oar/ ├── .github/extensions/web-relay/ # Copilot CLI extension for the Extension engine (worker link, ask_user bridge, model snapshots) ├── bin/ # The `oar` command (oar.js) and a Windows cmd shim for checkouts (bat/oar.bat) ├── docs/ # User guides behind the README, preview-server guide, Copilot BYOK notes, SDK feature tracker ├── scripts/ # Proof, live-check and maintenance scripts (agent-sdk-lock.mjs writes the pins lockfiles) ├── server/ │ ├── agent-sdks/ # Pins of the agent SDKs (and koffi) the relay installs on demand: package.json, lockfile, entries │ ├── claude-api-worker/ # Claude API session worker (the Claude worker on a Claude Console API key) │ ├── claude-cloud-worker/ # Claude Cloud session worker (follows a cloud session's event stream; runs no agent itself) │ ├── claude-worker/ # Claude Agent SDK session worker (turn runner, ask-user bridge, attachments) │ ├── copilot-worker/ # Copilot SDK engine session worker (steering, background tasks, questions) │ ├── cursor-worker/ # Cursor Agent SDK session worker (turn runner, mode nudges, auth retry) │ ├── deepseek-worker/ # DeepSeek session worker (the Claude worker against DeepSeek's Anthropic-compatible API) │ ├── grok-worker/ # Grok CLI session worker over ACP │ ├── migrations/ # Database migrations │ ├── public/ # Browser app (index.html, app/ modules, PWA shell) │ ├── repositories/ # SQLite data access │ ├── routes/ # Express route registration │ ├── services/ # Relay services (workers, usage, images, tunnels, previews, updates) │ ├── tools/ # Diagnostic tools (OpenAI BYOK capture proxy) │ ├── server.js # Entry point: supervisor plus relay runtime │ └── server-runtime.mjs # Express + Socket.IO relay server ├── shared/ # Code shared by the server, the extension, and the workers │ ├── claude-cloud/ # Claude Cloud API client, credentials reader, repository and branch rules │ └── worker-runtime/ # Common worker plumbing (relay API client, heartbeat, worker link) ├── tests/ # Playwright end-to-end suite and its isolated relay harness ├── CHANGELOG.md ├── DEVELOPING.md # Development workflows, tests, relay internals └── README.md ``` ## Relay internals Operator and developer detail moved here from the README. ### The pieces 1. **Web relay server** (`server/`): queueing, persistence, auth, browser UI, file browser, uploads, the OpenAI Image path, and the supervision of per-conversation session workers. 2. **Session workers** (`server/copilot-worker/`, `server/claude-worker/`, `server/cursor-worker/`, `server/grok-worker/`): one Node process per conversation that runs its turns through the Copilot SDK engine, the Claude Agent SDK, the Cursor Agent SDK, or the Grok CLI over ACP, and speaks the same relay contracts as the Copilot workers. The DeepSeek worker (`server/deepseek-worker/`) is the Claude worker with its CLI pointed at DeepSeek, and the Claude API worker (`server/claude-api-worker/`) is the Claude worker billed to an API key the relay holds. The Claude Cloud worker (`server/claude-cloud-worker/`) speaks them too but runs no agent: it sends messages to a session in Anthropic's cloud and follows that session's event stream. 3. **Copilot CLI extension** (`.github/extensions/web-relay/`): the Extension engine. It polls the relay, executes turns, streams activity, and bridges `ask_user` questions into web question cards. ### The single server entry point `server/server.js` is the only entry point, in every mode — manual runs, the CLI extension, `oar`, the systemd unit, Windows autostart, and the e2e runner all start it (`npm start` is an alias for exactly `node server/server.js`). Its role is chosen by argv, never by the environment: - `node server/server.js` — the process stays attached to your terminal as a supervisor and runs the real server (`server-runtime.mjs`) in a worker child it marks with `--relay-runtime`. Exit code 75 relaunches the worker; any other non-zero exit is retried up to 3 times before the supervisor gives up. Under a systemd user service whose main process the supervisor is (`systemctl --user show -p MainPID`), exit 75 is handed to systemd instead (`reset-failed` + `restart --no-block`), so the next supervisor and runtime start from the code on disk; no SIGTERM within 30 s, or a failing systemctl, relaunches the worker as above. - `node server/server.js --supervised` — runs the server in this process and exits 75 on restart, leaving restarts to whoever spawned it. The CLI extension passes this so its own bounded-backoff supervision is the only one in play. Role flags travel on argv because the server's environment is inherited by tmux worker sessions and by the Copilot CLI it launches — an env-based flag would be read by unrelated servers started further down that tree. ### Runtime ownership With session worker routing on (the default), the server launches each conversation's worker itself, so a plain `node server/server.js` — `npm start`, the systemd unit, Windows autostart — serves every runtime without a Copilot CLI session attached. ### Single runtime owner rule Run only one relay owner at a time: 1. **Extension-managed mode**: the Copilot CLI extension (loaded into `gh copilot` by `oar copilot`, `npm run copilot:relay`, or the global wrapper) starts and supervises `server.js --supervised`, and owns the relay worker WebSocket and fallback dequeue loop. `github`/`openai` turns run in Copilot sessions the server launches per conversation. 2. **Standalone mode** (manual escape hatch): start the server, then run `node server/relay.mjs` by hand. It spawns its own Copilot CLI process and polls `GET /api/pending` over HTTP. Use it only when the extension transport is unavailable — there is no npm script for it. On Windows, its visible launcher targets a stable per-workspace Windows Terminal window name, so later foreground launches reuse the same window instead of opening new desktop windows; use the hidden/stdio fallback only when you explicitly need it. Do not run extension-managed relay transport together with standalone relay runtime transport. `npm run copilot:relay` is the convenience launcher for extension-managed mode in this repository: it runs `gh copilot -- --allow-all` from the repo root, so the project-local extension loads and starts the relay. With the global wrapper installed (see [Global extension install](#global-extension-install-optional)), plain `gh copilot` from any repository does the same. In extension-managed mode, the worker WebSocket begins after the CLI session becomes active (typically after the first prompt), with HTTP dequeue kept only as fallback when the socket is unavailable. The extension supervises managed `server.js` restarts (bounded backoff) while the CLI session is alive, and stops restart attempts on session shutdown. When the CLI extension connects, it also prints the relay info window (local/network/remote/auth URLs) directly in the Copilot CLI client. On Windows, **Settings → Autostart (Windows)** can add a per-user Startup entry (*At sign-in*) that opens a visible terminal at sign-in and runs the installed `node server\server.js` path, or a boot-triggered scheduled task (*At system startup*). Either starts only the web relay server, with the config, data and log folders `oar start` uses (`resolveRelayStateDirs` in `server/services/oar-state-layout.mjs`). Before Node starts, both wait up to 5 minutes in all for `%TEMP%` and the drive of the data folder, and log a wait that runs out (`autostartWaitLines`). Once it runs, the relay brings an entry or launcher an earlier version generated for the install up to date: one without the data and log folders, without the wait, or with the Node line that dropped Node's exit code (`buildEarlierWindowsAutostartScripts`, `buildEarlierBootLauncherScripts`); one edited by hand is left alone. Every generated launcher jumps over a line of blanks that reaches past the end of the file it replaced (at least 4 KB, for a cmd of an earlier entry still running when autostart is switched off and on), and its Node line (`& exit`, which hands on Node's exit code) ends the script, so it can be rewritten while a cmd.exe still runs the earlier one (`buildLauncherFile` in `server/services/windows-autostart-service.mjs`). With session worker routing on (the default), the relay then launches each conversation's worker itself; only with routing off must a Copilot CLI session using the extension attach before queued turns can be processed. Turning the setting off removes the OAR Startup entry. ### Manual relay control Do not restart the relay by killing processes; use `POST /api/relay/shutdown` instead. - `POST /api/relay/shutdown` without `restart` queues a normal relay shutdown. - `POST /api/relay/shutdown` with `restart: true` queues an intentional self-restart. - Requests require relay auth (the token); they are not limited to localhost, the browser app sends them from anywhere it is signed in. - The relay waits until the queue is idle before acting; this endpoint does not interrupt an in-flight turn. - `/api/status` exposes the queued relay exit state as `relayShutdown` so the UI/logs can distinguish idle, queued, and shutting-down restart/shutdown flows. - Restart ownership follows the argv role, so exactly one supervisor acts on exit code 75: - `node server/server.js` — the attached supervisor respawns its worker child in the same terminal session - `node server/server.js --supervised` — the server exits 75 and the CLI extension relaunches it (bounded backoff, and it stops trying once the CLI session shuts down) The web UI's **🌄 Restart web relay**, `oar stop`, `oar restart`, `oar update` and `oar setup` (after a changed token, port or access) use the same endpoint. The runtime a supervisor starts after a restart reads the config file again, so a new port or token applies; `oar` asks on the port and with the token the relay runs with, then waits for an answer on the new ones. A relay that `oar copilot` started carries its port and token on its command line and keeps them. ### Session mismatch recovery Session mismatch recovery is restart-driven: the relay restart orchestrator parks queue work, restarts/rebinds the CLI runtime, and resumes dequeueing after rebind confirmation. The extension no longer attempts in-process session switch APIs from the dequeue/send path. ### Global npm command from a checkout You can install a checkout locally and get a global `oar` command without publishing: ```powershell npm link # or npm install -g . ``` `oar copilot`, run from any folder, starts the web relay server for that folder's workspace root, then immediately hands the shell to `gh copilot` without a bootstrap prompt. If a relay already answers on the port, the command reuses it and still opens Copilot in the same shell. The other commands (`oar help`) are described in the README; in a checkout `oar setup`, `oar stop`, `oar restart`, `oar status` and `oar url` work on `server/config.json`. To run a checkout as a systemd user service (Linux), run `node bin/oar.js service install` from a login shell of your own in the checkout (its `PATH` goes into the unit). The unit runs `node /server/server.js` with the checkout as its working directory and no `COPILOT_WEB_RELAY_*` paths, so the relay finds `server/config.json`, its data and `server/logs` as with `npm start`; `node bin/oar.js start|stop|restart|status` then go through it, and without the unit `node bin/oar.js start` refuses. A relay running in a terminal is asked to stop first. Under the unit a restart is a deploy for the supervisor too: `git pull`, then a restart from the web UI or `node bin/oar.js restart` (it waits for running turns) starts the new code, `server.js` included. A worktree is a checkout of its own: its install finds the main checkout's unit as another install's and leaves it, unless `--replace`. A hand-made unit (no OAR marker) is the checkout's service only when its `ExecStart` runs `/server/server.js` by its full path; one that only has the checkout as its `WorkingDirectory` (`npm start`, `node server/server.js`) is kept as it is and not used by `start|stop|restart`: `node bin/oar.js service install --replace` puts OAR's unit in its place, keeping a copy. Relay server output is written to a logfile under `%LOCALAPPDATA%\copilot-remote\logs` (`~/.config/copilot-remote/logs` elsewhere) for git checkouts, or `~/.oar/logs` (`%APPDATA%\oar\logs`) for global installs, unless `COPILOT_WEB_RELAY_LOG_DIR` is set, so it stays out of the CLI terminal. If you want custom token/tunnel settings from a specific `server/config.json`, point `COPILOT_WEB_RELAY_CONFIG` at that file before launching. A plain `npm install -g .` does not bundle the repo-local gitignored config file. Manual relay shutdowns are queued via `POST /api/relay/shutdown` and only take effect after the current turn goes idle, so they are not a way to interrupt a turn in progress. ### Global extension install (optional) Install a user-global extension entrypoint for use across repositories: ```text %USERPROFILE%\.copilot\extensions\web-relay\ (Windows) ~/.copilot/extensions/web-relay/ (Linux/macOS) ``` Recommended command: ```bash oar --install-extension ``` This writes/updates `extension.mjs` in the user-global extension directory as a wrapper that imports the repository extension entrypoint directly. `oar copilot` does the same on every run unless you pass `--no-install-extension`. The wrapper also avoids double-loading when you start Copilot from this repository itself, so the project-local extension remains the single runtime owner in repo-root sessions. Useful environment variables: - `COPILOT_WEB_RELAY_SERVER_DIR` (recommended) - `COPILOT_WEB_RELAY_ROOT` - `COPILOT_WEB_RELAY_CONFIG` - `COPILOT_WEB_RELAY_TOOLS` - `COPILOT_WEB_RELAY_LOG_DIR` - `COPILOT_WEB_RELAY_NODE` Project-local extension files still take precedence when both exist. If the same extension is available both project-local (`.github/extensions/web-relay/`) and user-global (`~/.copilot/extensions/web-relay/`), Copilot may show duplicates in extension management. Keep only one active copy to avoid double-loading. ### Roadmap for later launcher modes 1. **Option 2**: launch/attach a Copilot CLI session directly. 2. **Option 3**: support `oar -- [gh copilot args]` pass-through. *Shipped:* `oar copilot -- ` (or `oar -- `) runs `gh copilot -- `. 3. **Session resume**: add `--session-id=<...>` handoff once the session orchestration contract is defined. ### The database file and its backups **The name.** A database made from nothing is `oar.db` (with `oar.db-wal` and `oar.db-shm`). An install from before 0.9.20 runs on `copilot.db` until its owner renames it by hand, with the relay stopped; the relay never renames a database it runs on. `server/services/database-file.mjs` is the only code that spells either name: `resolveDatabaseFile` picks `oar.db`, else `copilot.db`, else a new `oar.db` (read only, it never opens or creates a file), and doctor and About show the full path of the file in use. `server/services/database-file-scan.test.mjs` fails on a `copilot.db` literal anywhere else in `server/`, `bin/`, `shared/` or `scripts/` code: a path built by hand would create an empty database under the old name. **Both names in one folder** (after a downgrade that skipped the rename back, or a stray file) are settled at start, under the lock and before the earlier-database adoption (`settleDatabaseFile`). Both are read through copies. A `copilot.db` that is not newer (the `newerOf` stamps), is empty, or holds only what a start writes (`STARTUP_SETTING_KEYS` and `STARTUP_TABLES`: an earlier version started on the folder and nothing was done there) is checkpointed and renamed to `copilot.db.older-