Worker / Agent Split#
Problem#
Today the server and executor are fused into a single process. The
server receives a webhook, enqueues a QueuedRun, and an
in-process tokio task pulls it from an mpsc channel and executes
it — all within the same pod, the same cluster.
This is a single-site, single-process assumption. It breaks as soon as any of these are true:
A second deployment context exists (a different cluster, a host with Nix, a build machine with GPU access).
Multiple users or teams share one UI but run on different infrastructure.
The server runs in Kubernetes but a build needs tools that are impractical to containerize (Nix with a warm store, hardware access).
A central web UI requires all execution contexts to report back to one server. Without a network boundary between server and executor, each deployment is an island with its own UI, its own run history, its own event stream. That contradicts the multi-user product vision.
Current State#
The run queue architecture from sprint 16 already models the concepts:
QueuedRunis a work item carrying everything needed to execute: run ID, repo URL, ref, sha, default branch.A worker task consumes from the queue, resolves workflows, and runs them via the
Executortrait.The
EventSender(mpsc channel) streams events from executor to persistence and broadcast.
The coupling is in the transport: the worker is an in-process task
pulling from a tokio mpsc channel. The split replaces that
in-process channel with a network protocol.
Proposed Model#
Server responsibilities#
The server owns the control plane:
Identity and authorization — OAuth login, session management.
Run lifecycle — receives triggers (webhook, API, UI), creates
QueuedRun, transitions state (queued → claimed → running → finished).Event persistence — receives
RunEventfrom workers, writes JSONL files, updatesRunStore.API and UI — serves the web interface, SSE endpoints, REST API.
Run routing — matches queued runs to workers based on labels.
The server never executes builds.
Worker responsibilities#
The worker owns the execution plane:
Claims work — polls or subscribes for queued runs that match its capabilities.
Executes — clones the repo, discovers workflows, runs them via whichever
Executoris configured (host, k8s, future nix-native).Streams events — sends
RunEventback to the server as they occur.Stateless — if the worker crashes, the server detects it (via heartbeat timeout) and can re-queue or mark the run as failed. No local state needs recovery.
The worker is a new binary: myci worker --server <url> --executor
<type>.
runs-on as Routing Key#
The runs-on field in workflow YAML is currently ignored. With
the worker split it becomes the routing mechanism.
Workers register with labels describing their capabilities:
myci worker --server https://myci.example \
--executor host \
--label nix=true \
--label arch=x86_64
A workflow declares what it needs:
jobs:
build:
runs-on: [nix]
steps:
- run: nix build .#my-image
The server matches the runs-on labels against registered workers
and routes the run accordingly. If no worker matches, the run stays
queued.
What Changes#
Work claim API:
POST /api/worker/claim— worker polls for queued runs matching its labels. Alternative: WebSocket subscription for push-based delivery.Event push: worker sends
RunEventback to server. Could reuse the existing event types over HTTP (batched POST) or WebSocket.Worker binary:
myci worker— connects to server, claims work, executes, streams events.Run routing: server matches
runs-onlabels to worker capabilities.Heartbeat / liveness: server detects dead workers and re-queues or fails their runs.
What Stays the Same#
Executortrait,StepRunner,RunContext— unchanged. The worker instantiates the executor locally, same as today.RunEvent/EventKind— same types, different transport (network instead of in-process channel).myci run(local, no server) — unchanged. Still a self-contained path for local development.Workflow format, expression evaluation, action resolution — all library code, used by both server and worker.
Connection to Federated Vision#
The worker split is the concrete first step toward a federated platform vision where MyCI instances act as sovereign peers in a network — receiving events via open protocols, executing locally, and reporting back.
The worker model makes this possible: each worker is a peer that connects to a server (or eventually to a federated event bus). The server is one coordination point today; it could become one of many in a federated setup. The architecture does not assume a single server — it assumes a protocol between coordinators and executors.
Longer-term directions (Matrix as event bus, CRDT-based state sync, overlay networking) build on this foundation but are not prerequisites for the worker split itself.
Migration Path#
The split does not need to happen all at once:
Extract the worker loop — move the in-process worker task into a function that can be called either in-process or from a standalone binary. Same code, two entry points.
Add the claim/event APIs — server exposes work items, accepts events. The in-process path remains as a fallback.
Worker binary —
myci workeras a thin wrapper around the extracted loop plus HTTP client for claim/event.Deprecate in-process execution — once the worker protocol is stable, the server drops the built-in executor.