@danmestas/orch-executor-cf-durable-object
v0.1.0
Published
orch executor backend: spawns persistent Cloudflare Durable Object agents per orch proposal 0003 (executor-protocol contract).
Readme
orch-executor-cf-durable-object
Cloudflare Durable Object executor backend for orch.
Spawns a persistent open-agent bridge that survives across fetch requests
— the bridge keeps its NATS-over-WebSocket connection warm so the agent stays
discoverable on $SRV.INFO.agents for the full warm window of its DO.
For an ephemeral per-request bridge, see the sister repo orch-executor-cf-worker.
Status
Phase B — real implementation. Per orch proposals 0002 (typed executor contract) and 0003 (extract executor backends), this repo now ships:
- A Cloudflare Durable Object worker (
src/index.ts,src/local-sandbox.ts) that hosts the persistent open-agent bridge. - A Node binary (
bin/orch-executor-cf-durable-object, source insrc/cli.ts) implementing the orch executor-protocol — SpawnSpec on stdin, WorkerHandle on stdout, exit 0/non-zero on success/failure.
See MIGRATION.md for the per-file move manifest from orch's
in-tree executors/wasm/cf-durable-object/.
Why a separate repo
Per orch proposal 0003 (Ousterhout-review-adjusted): backends with heavyweight
dependency footprints (CF Worker / Durable Object) extract to sister repos
so orch's main repo stops shipping TypeScript + wrangler + miniflare + DO
bindings. The lightweight tmux backend stays in-tree — too small (~50 LoC
bash) to justify extraction overhead.
How orch finds this backend
orch-spawn applies the orch#142 hybrid discovery order:
- Env override —
$ORCH_EXECUTOR_CF_DURABLE_OBJECT_CMD(absolute path or PATH-resolvable command). Useful when running multiple builds side-by-side or testing a local checkout against a stable orch. - PATH lookup — first
orch-executor-cf-durable-objecton$PATH. This is the production install shape (npm install -g …puts it there). - In-tree fallback — orch's vendored copy at
orch/executors/wasm/cf-durable-object/(only present before Phase C; will be removed once installs cut over to this repo).
Cold-spawn vs re-attach: the differentiator
The headline difference between this backend and orch-executor-cf-worker:
| Aspect | cf-worker (ephemeral) | cf-durable-object (persistent) |
| ---------------------- | -------------------------------------- | ------------------------------------------- |
| Bridge lifetime | per-request (~ms to seconds) | until eviction or explicit teardown (~10m+) |
| NATS connection | re-opened per request | re-used across requests |
| $SRV.INFO.agents | flickers in/out | continuously registered |
| Dominant flow | cold spawn every time | re-attach every time after the first |
| Multi-turn convo state | none (caller carries it) | preserved in-memory across turns |
Re-attach is the differentiator. Calling orch-executor-cf-durable-object
twice with the same SpawnSpec.name is idempotent — idFromName(slug)
always resolves to the same DO instance. The CLI surfaces the distinction:
# First call (cold spawn):
$ cat spec.yaml | orch-executor-cf-durable-object
spec_version: v1
name: alpha-do
executor: cf-durable-object
status: ready
created_at: 2026-05-24T12:00:00.000Z
message: provisioned new DO instance
...
# Second call (re-attach — same WorkerHandle, no DO churn):
$ cat spec.yaml | orch-executor-cf-durable-object
spec_version: v1
name: alpha-do
executor: cf-durable-object
status: ready
created_at: 2026-05-24T12:00:00.000Z # ← same timestamp
message: re-attached to existing DO instance (provisioned at 2026-05-24T12:00:00.000Z)
...The dispatcher MAY treat both as "ready" and immediately publish prompts on the declared NATS bus subjects.
Spawn contract
$ orch-executor-cf-durable-object
stdin: SpawnSpec YAML (v1 — see orch dist/schema/spawn-spec.v1.json)
stdout: WorkerHandle YAML on success (v1 — see orch dist/schema/worker-handle.v1.json)
stderr: human-readable diagnostics
exit: 0 success; non-zero failure
64 EX_USAGE — stdin missing / arg shape wrong
65 EX_DATAERR — SpawnSpec parse / validation failed
69 EX_UNAVAILABLE — DO endpoint unreachableSupplementary commands:
| Command | Purpose |
| --------------------------------------------- | -------------------------------------- |
| orch-executor-cf-durable-object --version | Backend version for orch-version probe |
| orch-executor-cf-durable-object --validate | Pre-flight check (no spawn) |
| orch-executor-cf-durable-object --help | Usage to stderr |
Example: SpawnSpec input
spec_version: v1
name: alpha-do # DNS-label slug (also the DO routing key)
agent: claude-code
session: alpha-do
owner: dmestas
cf-durable-object:
do_namespace: AGENT_DO # wrangler binding name
do_id: alpha-do # idFromName(<this>) routes the DO
env:
ORCH_DO_ENDPOINT: https://orch-agent-do.example.workers.devExample: WorkerHandle output
spec_version: v1
name: alpha-do
agent: claude-code
session: alpha-do
created_at: 2026-05-24T12:00:00.000Z
executor: cf-durable-object
id: AGENT_DO/alpha-do
bus:
prompt: agents.prompt.open-agent.dmestas.alpha-do.prompt
status: agents.prompt.open-agent.dmestas.alpha-do.status
hb: agents.prompt.open-agent.dmestas.alpha-do.hb
signal: agents.prompt.open-agent.dmestas.alpha-do.signal
abort:
kind: do-call
target: https://orch-agent-do.example.workers.dev/agent/alpha-do/stop
status: ready
message: provisioned new DO instanceEndpoint resolution
The CLI talks to whatever wrangler-deployed worker hosts the DO. Resolution order (first match wins):
SpawnSpec.env.ORCH_DO_ENDPOINT(per-spawn override)$ORCH_DO_ENDPOINT(operator-wide default)$ORCH_CF_DO_BASE_URL(legacy alias)http://127.0.0.1:8787(the wrangler-dev default)
Install
npm install -g @danmestas/orch-executor-cf-durable-object
orch-executor-cf-durable-object --versionFor a source checkout:
git clone https://github.com/danmestas/orch-executor-cf-durable-object.git
cd orch-executor-cf-durable-object
npm install
npm run build # emits dist/cli.js
npm link # exposes the bin on PATHWhat this executor wraps
A Cloudflare Durable Object that hosts a persistent Synadia open-agent bridge.
Per-session routing is via idFromName(<session>); every fetch for the same
session lands on the same DO instance, which holds the live NATS-over-WebSocket
connection and the in-flight runBridge() return value. The agent name on the
NATS bus matches cf-worker:
agents.prompt.open-agent.<OPEN_AGENT_OWNER>.<session>Synadia metadata advertised on the bus:
| Field | Value |
| ---------- | ------------- |
| executor | wasm |
| location | edge |
| lifetime | persistent |
Keepalive: uses CF's storage alarm primitive (state.storage.setAlarm),
not setInterval, so the bridge survives DO eviction. A 5-minute cron
trigger tickles each session in OPEN_AGENT_WARM_SESSIONS so fresh deploys
re-warm without a manual client fetch. Cadence aligns with Synadia agent
heartbeats.
Local development
npm install
npm run dev # wrangler dev on http://127.0.0.1:8787
# In a second shell:
curl http://127.0.0.1:8787/health
curl -X POST http://127.0.0.1:8787/agent/demo/provisionThe DO worker accepts ORCH_DO_TEST_MODE=1 to skip the real open-agent bridge.
That mode is what the wrangler-dev tests use — it exercises the DO state
machine (session persistence, alarm, teardown) without requiring the upstream
@synadia-ai/open-agent and @nats-io/transport-websockets packages.
Tests
npm test # CLI unit tests (validation + binary shim)
npm run test:wrangler # wrangler-dev integration tests (cold + re-attach)
npm run typecheck # tsc --noEmit for DO worker + CLI
npx wrangler deploy --dry-run # validates wrangler.toml shapeThe wrangler-dev test exercises the headline acceptance criterion: two
back-to-back invocations against the same DO instance must yield handles with
the same created_at timestamp and the second one must report
re-attached to existing DO instance in its message.
Deployment
npm install
wrangler secret put NATS_WS_URL # ws://your-hub:8080
wrangler secret put OPENROUTER_API_KEY # sk-or-...
npx wrangler deploy --dry-run # validates config
wrangler deployAdjust OPEN_AGENT_OWNER in wrangler.toml to your namespace (e.g. your
GitHub username) — it scopes every NATS subject this DO publishes on.
To keep specific sessions warm via cron, set
OPEN_AGENT_WARM_SESSIONS = "alpha,beta,demo" in wrangler.toml [vars].
Releasing
Releases are tag-driven. Pushing a tag vX.Y.Z triggers
.github/workflows/release.yml:
- build —
npm ci,npm run typecheck,npm run build,npx wrangler deploy --dry-run,npm test, and a--versionprobe on the compiled bin. - publish-npm — syncs
package.jsonversion from the tag (strips leadingv) and runsnpm publish --access publicwithNODE_AUTH_TOKENsourced from theNPM_TOKENsecret.
To cut a release:
git tag vX.Y.Z
git push --tagsManual dry-run
workflow_dispatch lets you rehearse a publish without cutting a tag.
The dry_run input defaults to true, so a one-click run from the
Actions tab exercises the full pipeline (npm publish --dry-run) without
uploading anything to npm. Flip dry_run to false to publish manually
from a branch or arbitrary ref — useful for emergency republishes.
One-time operator setup
Set the NPM_TOKEN secret in the repo's GitHub settings
(Settings → Secrets and variables → Actions) with an npm automation
token that has publish access to @danmestas/orch-executor-cf-durable-object.
PRs already run npm publish --dry-run in CI
(npm-publish-dry-run job in ci.yml)
to catch packaging breakage — missing files in files:, broken bin/
references — before a tag is pushed.
License
Apache 2.0 (matches orch).
