Headless Celorga server
Keep chats and scheduled work available on a dedicated Mac
celorga server start runs Celorga's chat relay and automation scheduler without opening the desktop app. It can also host an optional read-only Streamable HTTP MCP endpoint for external corpus retrieval. This implementation requires macOS 14 or later and reuses the desktop's chat engine, Mobile Remote protocol, corpus operations, and runtime adapters. The TypeScript runtime continues to own workflow scheduling, durable run semantics, and MCP retrieval.
The host needs its own accessible corpus, configured agent runtimes, and Tailscale connection. A phone can use the server while a different Mac is asleep or disconnected. The server holds a process-scoped assertion against idle system sleep while running; it cannot override a closed laptop lid, loss of power, or logout.
Set up a host
Build from a checkout with locked dependencies:
npm ci
npm run build:server
Initialize a machine-local configuration. The corpus must already have a valid org2.json identity; use celorga corpus init --help when creating a new test corpus. Obtain this machine's address with tailscale ip -4.
node dist/cli.js server init --dir /path/to/corpus --host-ref home-server --bind 100.64.0.1
node dist/cli.js server init --dir /path/to/corpus --host-ref home-server --bind 100.64.0.1 --apply
node dist/cli.js server start
init previews before writing ~/.local/state/openorg/server.json. Its parent directory must have mode 0700; configuration and the local control socket are private to the operating-system user. Use --config /private/state/server.json for another instance, and pass it to subsequent commands. The configuration must stay outside the corpus and source checkout. It records absolute local paths, a symbolic host reference, the display name, Tailscale address, relay and MCP ports, destination configurations, and whether scheduling and MCP are enabled. The MCP listener defaults to the relay port plus one and remains disabled until a read-only access token is created.
Local Codex, Claude Code, Pi, OpenCode, and OpenClaw local-edit permissions belong to the execution host. The desktop setting applies when iOS is paired directly with that desktop; an iPhone connected to a headless server uses the server's machine-local localAgentFilesystemAccess setting. New configurations default to workspace-write. Choose a different mode during initialization, or update an existing stopped server with a preview and then restart it:
node dist/cli.js server init --dir /path/to/corpus --host-ref home-server --bind 100.64.0.1 --filesystem-access full-access
node dist/cli.js server permissions --filesystem-access full-access
node dist/cli.js server permissions --filesystem-access full-access --apply
The accepted values are read-only, workspace-write, and full-access. read-only also disables OpenClaw's local edit bridge; both writable modes enable that corpus-scoped bridge, while the selected mode remains the sandbox or native tool policy for local Codex, Claude Code, Pi, and OpenCode. Full access lets those local harnesses write anywhere the server's macOS account can and does not provide interactive approval prompts, so use it only on a trusted host for trusted conversations. Applying server permissions changes the private configuration atomically and reports whether a restart is required; it does not change a running turn. server status reports the active worker's filesystemAccess value so the effective host policy can be verified after restart.
The default destination is local Codex using its existing login. --destination claude, --destination pi, --destination opencode, or --destination openclaw selects the corresponding built-in adapter. Additional destination configurations use the same fields as the desktop, including SSH-hosted Pi and OpenCode destinations. OpenClaw's built-in destination honors its configured endpoint and agentID; empty values use the host's gateway settings and main agent. Provider credentials stay in runtime login stores, protected gateway configuration, or Keychain. The headless worker keeps its OpenClaw device identity in its mode-0600 private state directory so an unattended connection never depends on a desktop app's interactive Keychain item. It never waits for Keychain dialogs: any other inaccessible credential fails the affected operation instead of blocking the relay. Setup does not copy data or credentials from another machine.
Use an existing corpus and chat history
Chat transcripts, thread IDs, pins, and settled state are persisted under the corpus's .org2/ directory. Point server init --dir at the existing corpus on the host to make that history available immediately. If the corpus is already synchronized there, no separate transcript import is needed. Verify that synchronization includes the hidden .org2/ directory and all referenced transcript shards and attachment blobs.
Agent runtime sessions are separate from the visible transcript. Existing Codex or Claude conversations may reference session IDs stored outside the corpus; make those specific runtime sessions available on the host and configure the same destination IDs before continuing them. Reuse an existing remote gateway for OpenClaw conversations. Provider authentication remains machine-local and must not be copied into the corpus.
To change an already paired server's corpus, stop it, update corpusRoot and destinations in its private configuration, then start it again. Preserve its configuration location, hostRef, and paired-device file to preserve the phone pairing. Check for running work before switching. Keep scheduling disabled until the intended automation owner and required runtimes are ready.
Run server status from a second terminal to see the endpoint, scheduler status, active chats, and paired devices. server stop closes the relay and saves chat interruption state before shutdown. A second native server cannot acquire the same corpus while its first server is running.
Expose read-only corpus MCP
Create a separate bearer credential for each external client. Preview first:
node dist/cli.js server token create --name chatgpt
node dist/cli.js server token create --name chatgpt --apply
The applied command enables MCP, prints the token once, and stores only its SHA-256 hash in the private server configuration. Put the token in the connecting client's secret environment or credential store; never copy it into the corpus, a shell-history argument, source control, logs, or an MCP URL. Restart the server after applying token or MCP configuration changes.
The endpoint is http://TAILSCALE_IP:MCP_PORT/mcp. It implements stateless Streamable HTTP MCP with bearer authentication and exposes only read operations: bounded cited search, stable-ID fetch, context assembly, agent-profile resolution, run listing, paginated Org resources, and workflow prompts. It does not expose run creation, run transitions, thread posting, corpus mutation, shell access, raw hidden state, or paths outside the configured corpus.
List non-secret credential metadata or revoke one client independently:
node dist/cli.js server token list
node dist/cli.js server token revoke --id TOKEN_ID
node dist/cli.js server token revoke --id TOKEN_ID --apply
Revoking the final token also disables MCP. Re-enable or change the port with a preview-first configuration update:
node dist/cli.js server mcp enable --mcp-port 48923
node dist/cli.js server mcp enable --mcp-port 48923 --apply
node dist/cli.js server mcp disable --apply
Requests with an Origin header are rejected unless the exact HTTP(S) origin is configured with repeatable --allow-origin. Ordinary server-to-server and native MCP clients do not normally send one. The endpoint negotiates MCP 2025-03-26, returns JSON responses, accepts individual messages or batches of at most 100 messages, and returns 405 for the optional GET event stream. The request limit is 2 MB; tool schemas additionally cap retrieval result counts and character budgets. Resource pages contain at most 100 entries and full resource reads return a SHA-256 revision and line count.
This listener remains bound to Tailscale and does not provide public TLS. Native clients on the same tailnet can connect directly. A hosted service needs an authenticated HTTPS reverse proxy or tunnel that can reach the Tailscale listener. ChatGPT web currently consumes remote MCP through a plugin, while the ChatGPT desktop app and Codex can connect to a Streamable HTTP URL directly; see MCP and agent skills for configuration and disclosure guidance.
Pair the iPhone
node dist/cli.js server pair
This returns a pairing URL, endpoint, and one-use six-digit code, valid for ten minutes. Open the pairing link on the iPhone, or enter the endpoint and code under Settings → Host Connection. Both devices must be on the same Tailscale network. The public listener accepts only the Tailscale binding; authenticated relay requests use a device credential stored in the iPhone Keychain. The headless host persists only token hashes in its private state directory, so pairing does not require a GUI Keychain unlock. server revoke --device-id ID removes a paired device.
The updated iOS app saves multiple hosts and lets you explicitly select the Mac or the server. Each host has its own credential and serves its configured corpus's conversations. Switching is disabled while a request is in flight, and changing pairing text cannot redirect an existing host's credential. Existing single-Mac pairings migrate into this host list. The current Mobile Remote protocol remains compatible with older iOS builds, which can pair with the server as their single host.
The desktop refreshes its chat list and messages when synchronized transcript files arrive or the app becomes active. Conversations started from iOS through the server appear on the Mac after corpus synchronization, preserving the Mac's selected conversation and unfinished draft. The desktop and server import unrelated conversations even while agents are working or local saves are pending. Each locally active or edited conversation keeps its in-memory messages and send queue; merely viewing remote history does not dispatch agent work or rewrite the transcript. A stale local save preserves newly arrived threads it has not yet seen and merges newer persisted revisions of a conversation message by message instead of overwriting them.
Pair a Mac and host shared thread links
A desktop can pair with the server the same way an iPhone does. Run server pair, then open Settings → Sharing → Celorga Server in Celorga on the Mac. Paste the pairing link, or enter the endpoint and code. The Mac keeps its device credential in its Keychain. Forgetting the server revokes that credential through the relay.
A paired Mac can choose the server when it shares an AI chat thread. The Mac sends only the thread ID and its appearance settings. The server renders the read-only page from its own copy of the synced transcript and keeps it current while the Mac is asleep or offline. The page server binds only to the configured Tailscale address. It listens on its own port, which persists with its links in publications-HOST beside the server configuration, so URLs survive restarts. server status reports sharedThreads. If the thread has not synced to the server yet, the request fails with a message instead of creating an empty page.
The relay exposes GET and POST /v1/threads/ID/share and POST /v1/threads/ID/share/stop to paired devices, and /v1/status advertises supportsThreadSharing. Restart an existing server after updating it to enable these endpoints.
Use the server and a desktop together
The desktop and the server share one chat history and behave as peers. Each host owns a single commit file under .org2/openclaw-chat.store/heads/. Everything else in that store is immutable and content-addressed, so no path is ever rewritten by two machines and a file synchronizer such as Syncthing has nothing to put in conflict. When two hosts extend the same conversation at the same time, readers keep both sides' messages; a message removed on one host, such as a dequeued follow-up, is not restored by the other. Older builds read the historical migration-marker.json, which is created once and no longer rewritten; they still reconcile the newer commits, but update every desktop and server so each one writes its own head.
Each running host publishes a small presence record at .org2/openclaw-chat.store/live/, refreshed about once a second while it runs a turn and every 45 seconds otherwise. Other hosts show that turn's streaming reply, reasoning, and tool activity, and report it as running on that host. This covers scheduled automations and turns started from any client. A paired iPhone therefore sees the same progress whether it connects to the Mac or to the server. A record that stops refreshing for about two and a half minutes means the host is unavailable.
A conversation continues on the host whose runtime last ran it, because Codex, OpenCode, Claude Code, and Pi sessions are local to that machine. When you send a message to a conversation another host runs, the receiving host records it and hands it to that host, which accepts it and runs the turn. This applies whether you typed it on the desktop or sent it from an iPhone connected to either host. If the owning host is offline, does not have the destination enabled, or has not accepted the message within two minutes, the receiving host runs the turn itself and says so. New conversations run on the host that created them, unless the desktop prefers a server for new turns (see Upgrade or restart without interrupting turns). Stopping a turn works only on the host that runs it.
Messages record where they came from and where they ran: the originating client (desktop, phone, automation, or a background celorga thread post), the phone's paired name, the host that received the message, and the host that executed the turn. The desktop shows this beside messages that did not both arrive and run on that Mac. iOS shows it on every message and names the host in the chat list and progress indicator.
Hand-offs and presence travel through the synchronized corpus. When a local Syncthing instance shares the corpus, Celorga asks it to rescan the changed chat files immediately instead of waiting for Syncthing's filesystem-watcher delay (ten seconds by default). This uses the API key from that machine's own Syncthing configuration at request time, sends it only to Syncthing's loopback GUI address, and never stores it. Set the OpenOrgSyncthingScanHints default to false to turn it off. Other synchronization tools work with their own propagation delay. Agents on the server can use celorga thread post to deliver idempotent background results to the same conversation.
Choose the automation owner
A corpus defaults to the symbolic scheduler owner desktop. Assign its scheduled automations to a server with a preview:
node dist/cli.js server assign --dir /path/to/corpus --host-ref home-server
node dist/cli.js server assign --dir /path/to/corpus --host-ref home-server --apply
This writes only automationHostRef to org2.json. The server passes its host reference to workflow due and scheduled workflow run calls; the desktop passes desktop. Both discovery and dispatch check ownership. Updated desktop installations skip a server-owned corpus when the laptop reconnects. Older desktop builds do not enforce this field; stop their scheduler or update them before transferring ownership. Switch back by assigning desktop. Let active work finish or explicitly stop it before transferring ownership; changing the owner does not cancel a dispatched agent turn.
Scheduling catches up the latest missed occurrence, suppresses overlap with queued, running, blocked, or approval-waiting attempts, and retains the explicit destination reference. The server must have that destination enabled. Missing destinations fail visibly. There is no automatic rerouting to another provider or another machine.
Attempt creation is serialized per workflow on the actual filesystem, and a previously recorded scheduled occurrence cannot be dispatched again, even after its run terminates. An orphaned .org2/workflow-dispatch-locks/ID.lock fails closed; inspect its process and host before removing it. A background process exiting does not prove a whole run is complete: durable approvals, artifacts, and review boundaries still apply.
Independent copies synchronized through Git, iCloud, or another replication tool do not provide a distributed lock. Do not run the same symbolic server identity on multiple such copies. Use one authoritative host for scheduling and execution. Automatic machine failover and per-workflow host placement are future work.
Keep the process running
node dist/cli.js server service
node dist/cli.js server service --apply
The preview shows the complete launchd agent and the exact launchctl bootstrap command. Applying writes ~/Library/LaunchAgents/org.org.server.HOST.plist; run the returned command to load it. The service starts at user login and restarts after failures. This is a user service, so the Mac must remain logged in. A deliberate server stop exits successfully and stays stopped until explicitly started again. After a reboot, sign in to make runtime logins and Keychain available.
Upgrade or restart without interrupting turns
node dist/cli.js server drain # stop taking turns; wait for running ones
node dist/cli.js server restart --drain # drain, stop, and start the launchd service again
node dist/cli.js server stop --drain --drain-timeout 1800
node dist/cli.js server resume # cancel a drain
Draining makes a planned update or restart invisible to live conversations. The server publishes a draining presence record, accepts no new turns, hand-offs, or scheduled automations, and lets running turns finish (up to 900 seconds by default). Meanwhile desktops and phones send new turns to another online host that has the destination enabled, or the receiving host runs them itself; a hand-off already addressed to the draining server is taken back immediately instead of after the two-minute timeout. stop --drain and restart --drain stop only once no turn is running and report the turns that are still running otherwise. server status reports draining and runningThreads; celorga activity hosts marks the host as draining and excludes it from failover.
The desktop app behaves the same way when it quits or installs an update with turns running: the quit prompt offers Quit When Turns Finish, which stops taking new local turns and quits once the running ones end.
To make the Mac a detachable client, choose the server in Settings → AI Chat → Execution → Run new agent turns on. New conversations, and conversations this Mac would otherwise run, are handed to that server while it is online, draining-free, and has the destination enabled; the Mac shows the server's live progress and can quit, sleep, or update without interrupting the turn. When the server is offline or restarting, the Mac runs turns itself as before. Local agents chosen this way run on the server's machine with its files, logins, and filesystem policy.
Logs live beside the server configuration as server.log and server-error.log. Pairing codes are returned through the private control socket, not written to service logs. Use stable checkout and executable paths for a persistent installation; do not remove the installed build directory.
Foreground iPhone chat works immediately after pairing. Background reply notifications additionally require Apple Push Notification credentials for the iOS app:
node dist/cli.js server push-config --team-id TEAM --key-id KEY --key-file /private/path/AuthKey.p8
node dist/cli.js server push-config --team-id TEAM --key-id KEY --key-file /private/path/AuthKey.p8 --apply
The CLI reads the private key locally and sends it only through the private control socket for storage in Keychain. It does not print the key. The iPhone registers its notification token on its next connection.
Other headless uses
The shared relay also exposes canonical Agenda, approvals, workflow controls, cited file previews, and external Codex tasks to iOS. Its optional HTTP MCP listener provides read-only search, fetch, context, and resource access to separately authorized clients. Existing bounded CLI commands provide capture, ingestion, publishing, and broader automation on the host. Those commands retain their own preview, mount, credential, and review requirements. The server does not automatically enable source synchronization, meeting recording, or local document publication merely by starting. It hosts only the thread links that paired clients explicitly share.