@volter/supercode-orchestrator
v0.5.7
Published
The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)
Downloads
22,848
Readme
@volter/supercode-orchestrator
This is currently a private workspace package. Examples assume the checkout's workspace dependencies and a built Volter Harness CLI; the package name is not a claim that an installable npm release exists.
The orchestrator's runtime over the ontology: one typed operational model
(OrchestratorState = an orchestration value plus a runtime half) whose folder is its serialization.
docs/ORCHESTRATOR-IR.md is normative. The orchestration value and every codec are the ontology's, in
Rust (crates/interchange/src/orchestration/); this package reads and writes its home through the
harness's orchestration doors (harness.v1.orchestration.*), never a codec of its own.
import { load, save, compile, importFrom, exportTo, closeWorld } from '@volter/supercode-orchestrator';
import { homedir } from 'node:os';
import { join } from 'node:path';
const root = process.env.SUPERCODE_ORCHESTRATOR_HOME || join(homedir(), '.supercode/orchestrator');
const { state, vault } = await load(root); // strict; secrets in `vault`, never `state`
await save(state, root); // canonical, byte-stable when unchanged
const { state: hermesState } = await compile('hermes', process.env.HERMES_HOME); // a home as a state value
await closeWorld(); // close the shared door when doneThe doors are orchestration.mjs: load/save/compile/importFrom/exportTo/closeWorld over one
SupercodeHarnessClient. A secret VALUE never crosses the wire — load and compile answer with
the .env KEY NAMES, and load reads the values out of the home's own .env. The door is held
open while a loop runs (retainWorld/releaseWorld, the loop's start/stop) and closes itself
when idle, so a one-shot verb (a test, a migration) exits on its own.
Rules the codec keeps (enforced in Rust, observed here):
- Unknown keys in an orchestrator-owned block (
worker) fail the load, naming the file and key. Hermes-shaped records keep unmodeled fields inresidue, verbatim. - A file whose decoded record is unchanged is written back byte for byte; a changed record is emitted canonically in Hermes's own field order.
- Secret values live in the vault (
.envin our folder; inline in a Hermes export because that is where Hermes reads them). A value pasted inline into our folder is moved into.envon first load and the file rewritten with the ref.JSON.stringify(state)never contains one. - Exporting to Hermes refuses to write Hermes's session store when bindings or obligations changed since import (UNI-22); it copies the store byte for byte when they did not.
- Files the IR does not model are never touched by
saveand are carried by path on a migration.
OpenClaw, and the two migration verbs (ORC-12)
import { compile, importFrom, exportTo } from '@volter/supercode-orchestrator';
const { state } = await compile('openclaw', process.env.OPENCLAW_STATE_DIR); // a home as a state valuenode bin/orchestrator.mjs import --from hermes|openclaw [--home <path>] [--into <root>]
node bin/orchestrator.mjs export --to hermes|openclaw [--from <root>] [--into <dest>]import (orchestration.import) compiles that harness's home and saves ours, credentials along; export
(orchestration.export) loads our folder and decompiles it back, printing the tier of every file it wrote.
Because a migration moves credentials, both are composed inside the harness process, not from the
value-level doors — a client composing them would have to send the secret over the wire. A refused
write exits 2 with its reason — a partial migration is never reported as a whole one.
What OpenClaw's state directory compiles to, at the pinned v2026.7.1-2:
agents.*→ profiles. The DEFAULT agent (the entry flaggeddefault, else the first declared, elsemain) is the IR'sdefaultprofile; every other agent keeps its id. Each is rooted at its ownagents/<id>/.channels.<name>[.accounts.<id>]→ oneChannelConfigper rowchannels listshows, keyed by that row's name (slack/T0FIXTURE); the transport isextra.kindand the source blocks ride verbatim inextra.channel_block/extra.account_block.bindings[]→Route. The IR's match carries the three fields it has a noun for (channel→platform,guildId→guild_id,peer.id→chat_id— the projectionroutes listmakes);accountId,teamId,roles,peer.kindand the binding'styperide in residue.hooks.mappings[]→WebhookSubscriptions (the inbound-trigger nountriggers listreports), with the block's sharedtokenas each subscription's secret.cron_jobs→Job.sessionTargethas no IR field: it stays in residue verbatim AND surfaces asattach_to_sessionwhen it names an existing conversation (main→agent:<id>:main,current→ the row's own session key,session:<id>→<id>;isolatedopens a fresh one).cron_run_logs→Fire.ok | error | skippedmap tosucceeded | failed | unknownand the native word stays in residue. OpenClaw's run log is written once, at finish, and records no claim, so the fire's start stands in forclaimed_atandresidue.__no_claimsays so.delivery_status/delivery_error/deliveredride in residue, and the queue entry on the same conversation key becomes the fire'sobligation_id.delivery_queue_entries→Obligation, partitioned by the agent segment of its session key.- each agent's
sessions/*.jsonlheader key → aBinding.agent:<id>:mainis OpenClaw's DM COLLAPSE: it becomes{platform: 'main', kind: 'dm', chat_id: 'main'}and the collapse is recorded in residue, because the IR cannot recover which transport a collapsed DM arrived on.
Tiers on the way out:
- byte —
state/openclaw.sqlitewhen every record decoded from it is unchanged (the file is copied); any row whose own record is unchanged when the store must be rewritten (its columns go back verbatim, andreport.rowscounts them); every unmodeled file under the state directory; andopenclaw.jsonwhen its decoded record is unchanged. - semantic — a CHANGED
openclaw.json. It is re-emitted as JSON, which openclaw's JSON5 parser reads, but the source's comments, trailing commas and key order are lost;report.notessays so every time that path is taken. - refused — bindings. An OpenClaw conversation's surface lives in its session KEY inside the transcript header, so writing them back is behind the UNI-22 stability gate, exactly as the Hermes export refuses Hermes's session store. A credential ref the vault cannot resolve is refused too, rather than written as its own reference.
Secrets never enter the model: any string under a credential-shaped key — openclaw's own rule
(channels.rs::is_credential_key: the lower-cased key ENDS WITH token|key|secret|password|
credential), minus the names it spells that way for non-secrets (sessionKey, store_key, …) —
moves to the vault as a {dotenv} ref and is re-inlined on export where openclaw reads it.
Two limits worth naming. save writes our folder in the IR's own serialization, so a home
imported from another harness loses that harness's file COMMENTS and, for fires, any run-log
column our Hermes-shaped cron/executions.db has no room for (OpenClaw's delivery_status,
session_key, summary, model, token counts): a direct compile-then-decompile of an OpenClaw
home is byte tier, the same trip through our folder is not. And import carries the source home's
unmodeled files by path, because save only ever writes the files the IR owns.
The loop (ORC-4)
import { Orchestrator, LoopbackAdapter, events, load } from '@volter/supercode-orchestrator';
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
const { state, vault } = await load(root);
const orch = new Orchestrator({ state, vault, client: new SupercodeHarnessClient(), adapters: { loopback: new LoopbackAdapter() } });
await orch.start(); // adapters connect; inbound messages become events
const now = new Date().toISOString();
await orch.dispatch(events.tick(now)); // the clock is an event toostep(state, event) is the pure reducer (§4): it mutates the one state value and returns effects
(§5); the Orchestrator performs them over the SDK (runtimes.start|send_input|respond) and the
adapters (deliver). A worker runs with the profile folder as cwd and the profile's harness home
inside it (activationEnv), so its AGENTS.md is in context by the ordinary instruction-file walk.
Cron and webhooks (ORC-5)
await orch.dispatch(events.tick(now)); // §4.3: expiry, due jobs, obligation retry, breakers
await orch.tick(); // the same, on the orchestrator's own clock
// POST /webhooks/<name> on orch.webhookPort (config `gateway.webhook_port`, else any free port)- A due job is claimed before it runs: the fire row (
cron_<job_id>_<YYYYMMDD_HHMMSS>, which is both the fire's id and its session name) is saved before theruntime_starteffect, and a second tick that sees aclaimed/runningfire for that job does nothing. - The fire runs on a synthetic key
{platform: 'cron', kind: 'dm', chat_id: job_id}with a binding carryingrecurrence. Itsfinaldelivers to the job's resolved target —origin→job.origin,home→profile.home,explicit→ as given,local→cron/output/<fire_id>.mdunder the profile — never to the synthetic key. An error or timeout goes tofailure_deliver, else todeliverwith aCron job <id> failed:prefix. next_run_atis recomputed after every fire:onceis spent (unless a numericrepeatremains),intervalcounts from the run,cronevaluates five fields inschedule.tz.context_fromprepends a note naming the session; the transcript is never loaded.- The daemon does not die on a failed event: a timer tick, an adapter's inbound, the webhook listener
or a queued follow-up whose effect fails (a save on a full disk) is logged, where before its
unhandled rejection ended the process. History is bounded as Hermes bounds it: run files per job
(
cron.output_retention, 50), completed one-shots (cron.completed_retention_days, 7), 1000 finished fires, and obligations as its delivery ledger (7 days, 500 rows). - Slack, Telegram and Discord blocks from a Hermes home connect through the Chat SDK sidecar with
Hermes's own
.envvariables and ingress (Socket Mode, polling, the gateway); install the platform package (@chat-adapter/slack, …) beside the orchestrator. - The agent-setup applier (
orchestrator apply plan|apply|adopt|provision --home-id <id> --hermes <home> | --orchestrator <root> --spec <setup.json>): a package's declared jobs andInferencereach one home through its own functions (Hermes's cron and config functions under its own interpreter; the orchestrator's operator verbs), owned by harness id against a base in the applier's own state. A home with no state is plan-only untilprovisionoradopt;deleteruns only with--reviewed. - Limits as Hermes sets them:
cron.max_parallel_jobs/HERMES_CRON_MAX_PARALLELcaps the fires in flight across the daemon; anagent.max_turnsa worker cannot take at launch is reported as not enforced. - A webhook subscription with
deliver_only: true(Hermes's direct-delivery mode) runs no agent: the rendered prompt is delivered as the message. - A job's gate runs before any model, as in Hermes:
script(its stdout leads the prompt, or skips the model when empty or{"wakeAgent": false}),no_agent(the script IS the job), andmonitor_script/monitor_url(unchanged output suppresses the run; a change puts a diff in the prompt). Scripts live under the profile'sscripts/. - A fire whose explicit delivery target is on a platform no adapter serves is blocked before its
worker runs (Hermes's delivery preflight), alerting once until a healthy run. Each run resets the
job's
last_delivery_error; a delivery that finally fails writes it. - A fire whose worker has been silent for
HERMES_CRON_TIMEOUTseconds (600 by default, 0 for no limit, as in Hermes) is closed as a timeout, its failure delivered, and its runtime closed; before, a hung fire kept its job from ever running again. - A job's
model(and a profile's, and/model's) reaches the worker's launch (claude --model,codex app-server -c model=…,gemini -m). For a harness with no such flag it is refused where it is set (/model,jobs.create|update,cronjob_create) and again at the start, rather than run on the harness's default. A job'sskillsare invoked as Hermes invokes them: each is found through the worker harness's own skill loader, from the fire's own working directory, and itsSKILL.md(with${HERMES_SKILL_DIR}resolved) goes ahead of the prompt, which follows Hermes's instruction marker; a skill that is not there is named in a notice. - A schedule nothing can evaluate — a cron expression that does not parse, a
tzthat is not an IANA zone (America/New_Yrok) — is refused by name at every door a job enters by (jobs.create|update,jobs.resume,cronjob_create). One that reached the folder by hand is caught fail-closed: the tick that finds it due, or the fire that cannot re-arm it, disables the job (enabled: false,next_run_at: null, the reason in Hermes'slast_error) and sends one notice tofailure_deliver, elsedeliver, else home. It is never left due. - A pending obligation is retried on the tick with a 1 / 2 / 4-minute backoff up to three attempts.
Three consecutive send failures on one adapter open its breaker for five minutes
(
state.runtime.adapters[platform].breaker); while it is open, obligations staypendingand theirattemptsare untouched. An adapter anadapterevent has already declareddown(state.runtime.adapters[platform].state) is treated the same way before the breaker has counted to three, so a single delivery cannot spend its three attempts inside a known outage and diefailedwhile the transport is still gone. POST /webhooks/<name>verifies an HMAC-SHA256X-Signature-256header against the subscription's secret from the vault and is fail-closed: unverified is dropped and logged. A verified call rendersprompt_templatewith{field}substitution from the JSON payload, opens a turn on{platform: 'webhook', kind: 'dm', chat_id: name}in the subscription's profile, and delivers the answer persubscription.deliver.- A subscription's
eventsfilter on the event the SOURCE names:X-GitHub-Event,X-GitLab-Event,X-Event-Key(Bitbucket),X-Event-Type, else the body's ownevent/type/action. An entry with a dot names the action too (pull_request.openedmatches<event>.<payload.action>); a call naming no event is not a subscribed one. A delivery id (X-GitHub-Delivery,X-GitLab-Event-UUID,Webhook-Id,X-Request-Id) already handled for that subscription — the last 200 are remembered — is dropped with an info log, so a redelivery runs once. The memory is runtime state, as in Hermes's own adapter: a daemon restart forgets it. - A permission prompt raised inside a fire or a webhook turn has nobody to ask, so it is answered
at once — never left to
worker.permission.timeout_seconds— withworker.permission.unattended(deny, the default, orapprove; Hermes'sapprovals.cron_mode), and one line saying what was decided goes to the job'sfailure_deliver, elsedeliver, else home (a webhook's: itsdeliver). A chat's prompt is still relayed to the chat. - A Codex worker asks only when started with an approval policy: where the profile keeps Hermes's
approvals (
approvals.modeunset or notoff) it starts withapproval_policy: untrusted, so it asks before every command and Hermes's rule answers; with approvals off it asks nothing.
worker:
harness: codex
permission:
unattended: approve # deny | approve; what a cron fire or webhook turn's prompts getKnown gap: Fire.session_id and Fire.obligation_id are live-record fields with no column in
Hermes's executions table, whose schema is frozen. They round-trip by construction instead — the
fire's id is its session name (fireSessionId), its obligation is obl-<fire_id>, and the
binding carries residue.fire_id — so load() still joins a fire to its session and its delivery.
The model-facing door (ORC-6)
One stdio MCP server per worker session, spawned by the worker itself and bound to the conversation
that spawned it (§7). Orchestrator.start() opens a Unix socket at <root>/orchestrator.sock
(mcp/bridge.mjs); startRuntime mounts mcp/server.mjs through whatever door the worker's harness
publishes (mcp/mount.mjs), with the binding in the server's environment — the three variables
activationEnv already sets, plus SUPERCODE_ORCHESTRATOR_SOCK.
| Harness | Door | What is written |
|---|---|---|
| claude-code | --mcp-config <file> in the launch arguments | <profile>/claude-code/mcp-orchestrator-<surface>.json, .mcp.json-shaped |
| codex | [mcp_servers.orchestrator] | appended (idempotently) to <profile>/codex/config.toml |
| opencode | mcp.orchestrator | <profile>/opencode/opencode.json |
| pi | none | pi ships no MCP client by design; the mount refuses and says so |
| hermes, openclaw, any other ACP agent | session/new's mcpServers | harness.v1.runtimes.start's mcp_servers |
Only the flag and ACP doors are named per launch, so only they carry the per-session
SUPERCODE_ORCHESTRATOR_SURFACE; under codex and opencode the server inherits it from the worker
process the loop already activated for this session.
Tools, one per verb, names and argument shapes transcribed from Hermes's tools/cronjob_tools.py
(cronjob_manage) and tools/send_message_tool.py (send_message): cronjob_create,
cronjob_list, cronjob_delete, send_message, session_search, session_handoff, memory_show,
skills_list. Every result names what it did in ran.
cronjob_createcapturesoriginfrom the binding, never from an argument, so a job created in a conversation delivers back to it. Hermes'snamehas no IR field and rides the residue channel.scheduletakes the IR's typedScheduleor the four Hermes string forms that map onto it exactly (30m/every 2h,in 30m, five-field cron, an ISO timestamp); natural day/time phrasing is refused by name rather than re-implemented.send_messagedefaults to this session's own surface; a target that routes to another profile is refused, naming that profile.memory_showandskills_listproxyharness.v1.memory.show/skills.listfor the worker harness against this profile's own config home.
supercode mcp serve (the coding tools) is a different server for a different purpose and is
untouched.
Acceptance (ORC-8)
The bar of docs/ORCHESTRATOR-IR.md §8 — one scripted week of realistic use replayed against
Hermes and against this package, compile-diffed, only zero defects done — closed on 2026-09-04 at
defect 0 (61 scenes, 21/21 expectations, 28 seconds, both subjects on the scripted clock). §8
carries the record, the defects fixed on the way and the Hermes evidence boundary; the instrument
was deleted after its run (owner rule: the roadmap's one test run, then gone).
The two borrowed adapters (ORC-9)
Every adapter is the same three methods — connect(emit) / send(target, content) / close() —
and every one of them reaches the reducer through inbound and nothing else (§3: "An adapter that
cannot express something as an inbound event does not get a private path into the loop"). A
platforms: block picks its adapter by extra.adapter when it names one and by the block's own
name otherwise:
platforms:
slack: # one block = one (platform, account)
enabled: true
extra:
adapter: chat-sdk # -> adapters/chat-sdk.mjs
botToken: ${SLACK_BOT_TOKEN} # a reference, as Hermes writes one; the value lives in .env
api_server: # -> adapters/api-server.mjs, by the block's name
enabled: true
extra:
host: 127.0.0.1
port: 8090
key: ${API_SERVER_KEY}RH2 Room chat is an installed app (ADR 0017).
Register the Room-chat app in the RH2 organization (organization.read, room.read; a Room's
manager seats its principal where it should answer), have an admin install it, and exchange the
code the install returns:
supercode orchestrator apps install room-chat --origin https://pilot.runhuman.com \
--client-id app_… --client-secret-file ./room-chat.secret --code … --profile default
supercode orchestrator apps list
supercode orchestrator apps remove room-chatThe record (with the installation's token) is kept owner-only under <home>/apps/; the next
daemon start attaches the rh2 channel for that profile. Uninstalling in RH2 revokes the token and
the channel stops. A platforms: rh2 block grants no integration authority and the runtime reports
it as unsupported. Native Hermes channel configuration remains the worker's configuration.
The Chat SDK sidecar mounts [email protected] (npm package chat, repo vercel/chat; a
devDependency of this package, installed alongside @chat-adapter/[email protected]). The
platform packages (@chat-adapter/slack, …) are not dependencies: the one the block names is
imported lazily at connect, and the credential key names in the block are that factory's own
option names — Volter Harness renames nothing and resolves the refs out of the vault at connect, never
earlier. Surface identity is read off the handler's Thread (adapter.name, channelId, id,
isDM, channelVisibility) and content off the Message (author.userId/userName/isMe/isBot,
isMention, id, metadata.dateSent, attachments with fetchMetadata as the IR's
Attachment.ref) — ORC-1 F4's table, transcribed. Two decisions the IR left open are made in
adapters/chat-sdk.mjs and nowhere else:
kind(the surface key's chat type) (F4 finding 4, which named this as a rule the IR "would have to invent"):isDM→dm; elsechannelVisibility: private→group; else (workspace/external/unknown) →channel.threadis never produced — a thread is the key's ownthread_id.- Queueing (F4 finding 11): the SDK's per-thread concurrency is set to
concurrent, so §4.1 step 5's queue is the only queue on a surface.extra.concurrencyoverrides it.
is_me is now on the event and the reducer drops such an inbound at step 0 (§4.1): a platform that
replays what we posted is not a turn. send returns the platform's handle and deliver records it
as Obligation.posted.message_id (§2.6) — the thing §4.2's edit-in-place would edit.
The api_server surface serves Hermes's own client API on its own node:http listener bound to
platforms.api_server.extra.host|port (exactly Hermes's keys, api_server.py:1515-1519). It is
deliberately not mounted on the webhook listener: that one binds gateway.webhook_port, a
different key with a different meaning. Auth is Authorization: Bearer <extra.key> —
Hermes's _check_auth (:1916-1968) — and every route quotes the handler it was matched against.
Routes: GET /v1/health (the one unauthenticated one), GET /v1/capabilities,
POST /api/sessions, POST /api/sessions/{id}/chat, GET /api/sessions/{id}/messages,
POST /v1/runs, GET /v1/runs/{id}, GET /v1/runs/{id}/events, and
POST /v1/runs/{id}/approval|steer|stop.
Everything it makes the loop do is an inbound: a chat is one; a run is one on a fresh binding
{platform: 'api_server', kind: 'dm', chat_id: <run id>}; a steer is one queued behind the
running turn (§4.1 step 5); a stop is /reset (§2.7); and an approval is resolved to the worker's
own option and answered as the number §4.2's permission prompt asked for. The run event stream
is derived from §2.8 runtime state — the turn's text buffer, the pending permission requests, and
the obligation the turn's final produces — because the loop publishes no event bus and ORC-9 does
not add one.
Where Hermes's shape has a field the IR has no source for, it is returned empty, never guessed:
| route | empty field(s) | why |
|---|---|---|
| POST /api/sessions/{id}/chat | usage, runtime | the IR records no token accounting and no provider/model routing |
| GET /api/sessions/{id}/messages | token_count, tool_calls, tool_call_id, tool_name, finish_reason, reasoning | §2.5 keeps no per-message tool or token record; the transcript stays native and is read through sessions.load |
| POST /api/sessions (session object) | user_id, model, tool_call_count, input_tokens, output_tokens, parent_session_id | same |
| GET /v1/runs/{id} | model | the run does not choose a provider model; profile.worker does |
Four honest divergences, stated rather than hidden:
platformreadssupercode-orchestrator, nothermes-agent, on/v1/healthand/v1/capabilities: it names the server that answered.X-Hermes-Session-Keyis not an auth credential in Hermes either —_parse_session_key_header(:2403-2440) reads it as the caller-declared conversation scope, and it is read with that meaning here. Bearer is the trust boundary./v1/runs/{id}/approvalaccepts Hermes'sonce|session|always|deny(withapprove|approved|allowaliasing toonce), but the IR has no standing-grant store:sessionandalwaysresolve the pending request exactly asoncedoes and no grant outlives it./v1/chat/completions,/v1/responses,/api/sessions/{id}/chat/stream,/api/jobs/*, idempotency (Idempotency-Key) and the/p/<profile>/route mirrors are not served;/v1/capabilitiesreports each asfalserather than copying Hermes'strue.- A run is
runningfrom the moment it is accepted (Hermes passes throughqueuedfirst), and/stopleaves itcancelledonce the/resetlands rather than instopping.
Known gaps, named:
Obligation.postedlives on the in-memory record only. Hermes'sdelivery_obligationstable has no column for it and the store is byte-compatible with that schema, so a save/reload loses the handle. Adding it is a schema change to §2.6's codec, not an adapter change.deliverprojects the obligation to{ text, at }before callingsend, soOutboundContent.reply_toandattachmentsnever reach an adapter today even though §2.1 declares both and the Chat SDK sidecar honours both (thread.reply) when a caller supplies them.- The production path that builds a real platform adapter from a block (
@chat-adapter/*→createSlackAdapter(...)) is not exercised by the suite: the tests drive a hand-writtenAdapter(the SDK'spackages/chat/src/mock-adapter.tsshape, transcribed — that file is not re-exported from the package entry, is outside its publishedfileslist, and importsvitest). Evidence for the sidecar stops at the SDK boundary; the platform packages are extrapolation.
Access, the daemon's lease, and coming back (ORC-10)
node bin/orchestrator.mjs access pair-list --root <home> [--profile <name>]
node bin/orchestrator.mjs access pair-approve <code> --root <home>
node bin/orchestrator.mjs access allow|revoke <platform> <user_id> --root <home>
node bin/orchestrator.mjs access admin-add|admin-remove <platform> <user_id> --root <home>supercode orchestrator pair approve|list and access allow|revoke|admin add|remove spawn exactly
these: the folder has one encoder (save()), and it is not in the Rust binary. Each verb loads the
home, applies the reducer's own operator event and saves; a verb the reducer refuses exits
non-zero with the reducer's own words and writes nothing it did not ask for. They are the COLD path
— for a running daemon the same verbs go through its socket (ORC-13).
- Access lives in Hermes's own stores, and only there. A platform is open when the profile's
own
.envsets<PLATFORM>_ALLOW_ALL_USERS=true(orGATEWAY_ALLOW_ALL_USERS=truefor every platform); its allowlist is<PLATFORM>_ALLOWED_USERSplus the pairings Hermes approved (platforms/pairing/<platform>-approved.json). A platform with neither is allowlist-only. An unknown sender is answered with a pairing code from Hermes's ownPairingStore(its expiry, rate limit and lockout) and nothing else: no worker starts. - Admins are each platform's
allow_admin_from/group_allow_admin_frominconfig.yaml: the tier whose numeric reply resolves a relayed permission prompt (§4.2). An allowlisted non-admin's number is an ordinary message. - No other file grants access.
approve,allow,revokeandadmingo through Hermes's store and writers, live or cold.
The lease <home>/orchestrator.lock is written by this daemon, not by supercode orchestrator
start: one writer means a launchd/systemd-managed daemon reports the same lease a foreground one
does. The lock names the process and its machine (hostname, and Linux's boot id). A home whose
lock names a live process on this machine, since this boot, is refused; a lock naming a process
that is gone, or one another machine or an earlier boot wrote (a home on a volume the
sandbox before this one mounted), is the mark of a daemon that died, and the daemon dispatches
restart (§4.7) before it accepts anything. Nothing is signalled on the word of a lock from
elsewhere. leaseIsLive and readLease are the package's reading of it. A clean stop removes its own lock, so an ordinary start replays nothing.
On restart: every pending obligation goes out again, and every binding that held a lease is
re-opened. The lease table is never persisted (§2.8), so a lease start records runtime_id on the
binding's residue; the loop tries runtimes.attach_existing where the harness's registry row says
attach_existing_process, and otherwise marks the binding interrupted and tells its surface once
(Session interrupted by a restart; send your message again.). Cron and webhook surfaces get the
mark without the notice — nobody is reading them.
The repo's Hermes fixture home (crates/harness/fixtures/hermes_home) is a ready home to drive it
against by hand; that needs target/debug/supercode built.
The operator door (ORC-13)
The orchestrator's WRITE door — what supercode jobs|profiles|sessions <verb> --harness orchestrator
drives, the way Hermes's write door is hermes cron … (§4.6). Volter Harness writes no file of this
folder: save() here stays the only writer, so the IR's byte-stability and residue rules hold by
construction.
Two doors, one implementation (applyOperator in operator.mjs); only how the event is
dispatched differs.
Live — the same socket the model-facing door uses, a second line family beside it, told apart by
the op key:
in {"op":"jobs.create","args":{"prompt":"…","schedule":"every 30m"},"profile":"coder"}
out {"ok":true,"result":{"ran":"created cron job job_… …","job_id":"…","job":{…}}}
{"ok":false,"error":"jobs_delete: no job job_x"}op is one of jobs.create|update|pause|resume|run|delete, profiles.create|delete,
sessions.new|reset, access.pair_approve. The verb dispatches its events.operator(...) on the
loop's own queue, so an operator write is ordered against inbound messages and ticks like
anything else and the running loop's state and the folder can never disagree. result re-reads the
affected record out of state after the reducer ran.
Cold — the same verbs when no daemon is serving the home:
node bin/orchestrator.mjs <op> --root <home> [--profile <p>] [--at <rfc3339>] --json '<args>'It loads, applies through applyOperator, performs the effects the loop would (save,
remove_profile_dir), prints the same {ok, result} line and exits 0, or 1 on a refusal.
crates/harness/src/orchestrator_door.rs picks between the two: the socket when
<root>/orchestrator.lock names a live pid and the socket answers, the CLI otherwise. A refusal on
the live door is final — only a broken socket falls through, so a verb the daemon already answered is
never run twice.
- A refusal names its reason. The reducer refuses by emitting a
warnlog effect and nosave; that sentence becomes{ok:false, error}verbatim, and nothing is written. sessions.new|resetname a BINDING by its SURFACE (platform|kind|chat_id|thread_id|participant_id), not by a session id — a binding has none of its own. A surface with no live conversation refuses.profiles.deletenever unlinks. It removes the record and emitsremove_profile_dir, which renames the home toprofiles/.trash/<name>-<stamp>/. A dot-prefixed folder is not a profile to either loader, so a trashed home neither loads nor lists.
Hosted scheduled tasks
A host can retain its existing conversation and deterministic action runtime while this package owns schedules, durable fire claims, run output and the operator socket:
supercode-orchestrator --root /instance/var/orchestrator \
--executor-url http://127.0.0.1:5200/os/scheduled-task \
--executor-token-file /instance/var/executor.tokenThe executor URL must be loopback HTTP. Its private token file contains at least
32 characters. Each authenticated POST carries {root, profile, job, fire}; the
host returns {accepted: true, profile, fire_id, output, delivery?} only after
accepting that exact fire. A delivery receipt is {platform, chat_id, id} and
confirms the submitted task, not a model's eventual answer. The output must say
which milestone it confirms. jobs.create accepts opaque JSON metadata, stored
in job.residue.host, and enabled: false for initially paused tasks.
Hosted execution starts no worker and uses no channel adapter. The host must
persist an idempotency record before its side effect, return the saved receipt
on duplicates, and refuse to repeat an uncertain dispatch. The daemon records
failed/timeout fires without replaying them; a restart advances a stale claim
once. callOperator addresses the live socket, with no cold-write fallback.
Orchestrator({executeJob}) is the corresponding in-process API for embedders.
The daemon acquires its exclusive lease before loading state. A crash during the
short acquisition critical section leaves orchestrator.acquire; subsequent
starts fail closed until an operator inspects and removes that guard. Normal
stops remove only their own lease. Native jobs.resume computes the first fire
for a job that was created paused with no next-run timestamp.
Hosted daemon lifecycle
stopHostedDaemon({ root, executorUrl?, timeoutMs? }) validates the hosted lease
and signals this package's daemon through its native shutdown handler. A foreign
root/executor is refused; the lease is retained until shutdown completes. The
helper does not clear a live lease or escalate to SIGKILL. Embedders can stop their
owned clock before disabling the integration and start it again on the next boot.
Hosted HTTP refusal messages preserve the executor's bounded JSON error reason.
