synrgcoflow
v1.3.0
Published
The CoFlow subscription-runner daemon that executes agent inference jobs through the operator's local Claude CLI subscription
Maintainers
Readme
FlowForge Subscription Runner Daemon
The subscription runner daemon (runner/) is a self-hosted Node process that runs on the operator's own machine. It polls the FlowForge server for inference jobs, executes them through the operator's logged-in claude CLI (using the operator's Anthropic subscription), and posts results back — so the compute cost is subscription-billed rather than API-billed.
Architecture
FlowForge server (Railway)
└─ agent_runner_jobs table
↑ POST /api/agent-runner/jobs/:id/result
└─ GET /api/agent-runner/jobs (long-poll, ≤30s)
↓
runner/index.js ← this daemon
↓
claude CLI (on the operator's box, logged-in subscription)Concurrency: one job per daemon process. Run multiple daemon processes to handle parallel jobs (each independently claims via FOR UPDATE SKIP LOCKED on the server).
Tool execution: v1 performs pure inference. The payload.tools field is accepted and passed through but tools are not executed locally in this version.
Requirements
- Node ≥ 20 (uses global
fetchand modern built-ins). The repo pins Node 24 — match that version for consistency. claudeCLI installed and authenticated. Install via:
Then authenticate using the interactive login (subscription, NOT an API key):npm install -g @anthropic-ai/claude-code
Verify authentication:claude # Follow the /login prompt — use "Claude.ai login" to authenticate with your subscription.
The daemon explicitly removesclaude --print "hello" --output-format jsonANTHROPIC_API_KEYfrom the child process environment so the CLI always routes through your subscription even if the host environment has that key set.- Zero additional npm dependencies. The daemon uses only Node built-ins and the global
fetchAPI.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| FF_RUNNER_BASE_URL | Yes | — | FlowForge server base URL, e.g. https://app.flowforge.ai |
| FF_RUNNER_TOKEN | Yes | — | 64-hex raw Bearer token issued at runner registration (one-time display) |
| FF_RUNNER_POLL_IDLE_MS | No | 1000 | Milliseconds to wait after a 204 (idle) before re-polling |
| FF_RUNNER_CLAUDE_BIN | No | claude | Path or binary name of the claude CLI |
| FF_RUNNER_POLL_ERROR_BACKOFF_MS | No | 1000 | Base backoff (ms) when a poll/claim attempt throws; doubles on each consecutive error |
| FF_RUNNER_POLL_ERROR_BACKOFF_MAX_MS | No | 30000 | Maximum backoff (ms) between error retries; prevents thundering-herd during server outages |
Resilience Guarantees
Idempotent result ack: postResult retries on transient network errors and 5xx responses (up to 5 attempts, capped exponential backoff: 1s → 2s → 4s → 8s → 16s). If the server committed the result but the HTTP response was lost, the retry hits the server's idempotent-ack path (200 { ok: true, idempotent: true }) rather than a 403 — so the job is confirmed landed without triggering a second inference run.
Poll error backoff: when claimJob throws (server down, network error), the daemon waits FF_RUNNER_POLL_ERROR_BACKOFF_MS × 2^(n-1) ms before the next attempt (capped at FF_RUNNER_POLL_ERROR_BACKOFF_MAX_MS). The backoff resets to base on the next successful poll (200 job claimed or 204 idle).
End-to-End Live Round-Trip
Step 1 — Register the runner in FlowForge
In the FlowForge app, navigate to Settings → Runners (or use the API):
curl -X POST https://app.flowforge.ai/api/companies/<companyId>/runners \
-H "Cookie: <session>" \
-H "Content-Type: application/json" \
-d '{"name": "my-macbook"}'The response includes a token field — copy it now, it is shown only once:
{ "id": "...", "name": "my-macbook", "status": "active", "token": "<rawToken>" }Step 2 — Start the daemon on the runner box
No clone or install step needed — npx fetches the published package on demand.
# Authenticate the claude CLI first (if not already logged in)
claude
# Start the daemon
FF_RUNNER_BASE_URL=https://app.flowforge.ai \
FF_RUNNER_TOKEN=<rawToken> \
npx -y synrgcoflowYou will see structured JSON log lines on stdout:
{"ts":"...","event":"runner.daemon.started","baseUrl":"https://app.flowforge.ai","concurrency":1}
{"ts":"...","event":"runner.poll.idle"}
{"ts":"...","event":"runner.poll.claimed","jobId":"...","agentId":"..."}
{"ts":"...","event":"runner.infer.started","jobId":"...","model":"claude-opus-4-5"}
{"ts":"...","event":"runner.infer.completed","jobId":"...","durationMs":4200,"inputTokens":450,"outputTokens":212,"costCents":0}
{"ts":"...","event":"runner.result.posted","jobId":"...","outcome":"ok"}Step 3 — Assign an agent to use the subscription runner
In FlowForge, open Agent Detail for any agent and set:
- Adapter type:
subscription_runner - Model ID: (optional) e.g.
claude-opus-4-5
Step 4 — Trigger a run
Start a chat or trigger a chimera run using that agent. Watch:
SELECT id, status, claimed_at, completed_at
FROM agent_runner_jobs
ORDER BY created_at DESC
LIMIT 5;The job transitions pending → claimed → completed and the output appears in the FlowForge chat/run view.
Persistent Auto-Start (install / uninstall / status)
By default the daemon runs only while its shell process is alive. To make it auto-start on login AND auto-restart on crash — surviving reboot, logout, and sleep, with no re-run required — install it as an OS-native user service:
FF_RUNNER_BASE_URL=https://app.flowforge.ai \
FF_RUNNER_TOKEN=<rawToken> \
npx synrgcoflow installIf either env var is absent, install prompts for it interactively. Nothing
about the token is ever printed back — it is written only into the generated
service-unit file, which is chmod'd 0600 (owner read/write only) immediately
after creation.
macOS: writes a LaunchAgent (~/Library/LaunchAgents/com.synrgcoflow.runner.plist)
with RunAtLoad + KeepAlive.SuccessfulExit=false, then loads it via
launchctl bootstrap. Logs go to ~/Library/Logs/synrgcoflow/runner.{out,err}.log.
Linux: writes a systemd --user unit
(~/.config/systemd/user/synrgcoflow-runner.service) with Restart=always,
enables + starts it via systemctl --user enable --now, and runs
loginctl enable-linger <user> so the service keeps running after a full logout
(systemd --user instances otherwise stop when the last session ends).
Other platforms: install/uninstall/status print a "not yet supported"
message and exit — the daemon still runs fine via the manual npx -y synrgcoflow
path documented above.
npx synrgcoflow status # print the current service state
npx synrgcoflow uninstall # stop + remove the service unitThe install/uninstall/status subcommands run without requiring
FF_RUNNER_BASE_URL/FF_RUNNER_TOKEN to already be set in the environment —
only the default (no-argument) daemon path requires them.
Fencing Token (lock_token) Echo
When the server includes a lock_token field alongside a claimed job — on
either the WS runner_job push frame or the HTTP GET /api/agent-runner/jobs
claim response — the daemon captures it and echoes it back verbatim as
lock_token in the POST /api/agent-runner/jobs/:id/result body (both the
success and cancelled-result paths). This lets the server's result-write verify
the runner still holds the claim lock before committing. If a job carries no
lock_token (older server), the field is omitted from the POST entirely — the
daemon never invents a token client-side.
Graceful Shutdown
Send SIGINT (Ctrl+C) or SIGTERM to the daemon process. It finishes the in-flight job (if any), then exits 0. In-flight jobs that were interrupted before result posting are reset by the server's 5-minute reaper so they can be retried.
Logs
The daemon writes one JSON line per event to stdout. Pipe to any log aggregator:
FF_RUNNER_BASE_URL=... FF_RUNNER_TOKEN=... npx -y synrgcoflow | tee -a runner.log