Releases: edgehero/pi-dispatch
Release list
pi-dispatch v1.10.3
A worker now stops watching when it stops working.
startWorker arms three directory watches so an operator can edit triggers.json, pause-windows.json
and scoped-limits.json without a restart. Each was created and then thrown away: unref keeps a handle
from holding the event loop open, but it does not disarm it, so every worker that shut down inside a
process that kept running left three armed watches behind. Each still held that worker's log closure, so
a reload fired later wrote a line stamped with the host of a worker that had stopped.
Each watch now returns a stop handle, registered in the same list that already closes the queues and the
host registry. It closes the watcher, cancels the debounce, and silences a reload it can no longer recall.
Two smaller fixes ride along, both found while closing this one. The shutdown's closer loop now delivers
the per item isolation its own comment always claimed: a closer that threw before returning a promise, or
an absent one, used to strand every closer after it, including the registry row deletion that keeps a
stopped host from lingering as a ghost peer. And the worker test harness now tears down on a boot that
refuses after the worker was already built, where it used to skip teardown entirely.
Upgrade with npm install. Nothing in the configuration surface moved.
pi-dispatch v1.10.2
Configuration you can find without reading the source. pi-dispatch reads 106 environment variables across the worker, the receiver, the panel and the in-container runner. Twenty-six of them appeared in no .env.example, no README and no docs page, so the only way to learn one existed was to hit the refusal that names it.
Nine are now keys in .env.example, which pi-dispatch init copies verbatim into every new deployment:
GITHUB_PAT_VAR, which variable actually holds the PAT. The name you give is read verbatim and checked against nothing, so pointing it at a variable holding something else would send that value to GitHub as a token.GITLAB_AUTH_SOURCE,FORGEJO_AUTH_SOURCE,AZURE_AUTH_SOURCE, one value each,pat, which is also the default. They exist to refuse a wrong assumption of symmetry withGITHUB_AUTH_SOURCErather than to offer a choice, and the refusal needs that forge's token to be set.POLL_REPOSandPOLL_INTERVAL_SECONDS, plusPI_CODING_AGENT_DIR,PI_GRAPH_DIRandPI_DISPATCH_DEPLOYMENT_FILE.
The other seventeen carry a comment at their own read site saying why they cannot be keys, and .env.example closes with the accounting an operator can read: every variable that is not a key, and the reason.
New page: docs/polling.md. The webhook-free ingest path had one paragraph in the README and no reference anywhere, so an operator who chose it had nothing to read next. Both repository discovery modes, both boot refusals, the interval floor and what GitHub's own hint does to it, which endpoint serves which trigger, the closer-authority gate and its two log lines, and five limits worth knowing before relying on it.
New rule, with a check. REQ-DEPLOYMENT-BOOTSTRAP now states that every variable a loader reads is either a key in .env.example or is marked internal beside the read, and worker/test/env-docs.test.mjs enforces it. The gap took a long time to accumulate because nothing was watching; now something is.
Also here: the four run-mirror tests that turned red on main when their fixed 2026-08-30 fixtures aged past a seven day retention window. They pin an instant now rather than a distance from today. Repository health, no behaviour change.
Upgrading: npm install. No service reinstall. init never overwrites an existing .env, so if you want the new commentary in your own file, diff it against the .env.example that ships in the package.
Worth knowing if you run Google models: doctor accepts GOOGLE_API_KEY for PI_PROVIDER=google, and accepts gemini as a provider name. pi reads neither, so both configurations pass the preflight and then refuse every job before spending anything. Tracked as issue #286, not fixed here.
@edgehero/pi-dispatch moves to 1.10.2 and @edgehero/pi-dispatch-admin to 1.10.1, the latter because the setup wizard pins the worker version it installs. @edgehero/pi-dispatch-receiver stays at 1.5.0; nothing in it changed.
@edgehero/pi-dispatch v1.10.3
The three live edit directory watches now return a stop handle that the worker's shutdown closes.
Before this version, startWorker created each watch and discarded it. unref keeps a handle from
holding the event loop open, but it does not disarm it, so a worker that shut down inside a process that
kept running left three armed watches behind, each still holding that worker's log closure. A reload
fired afterwards wrote a line stamped with the host of a worker that had stopped.
The handle closes the watcher, cancels the debounce the callback armed, and silences a reload that had
already started, which an async reconcile makes possible. It never throws and it is idempotent.
Also in this version: the shutdown's closer loop no longer lets one bad closer strand the ones after it,
which had been costing the host registry its row deletion on that path.
Upgrade with npm install. No operator action beyond that.
@edgehero/pi-dispatch v1.10.2
The .env.example this package ships now names the configuration it reads. That file is not documentation on the side: pi-dispatch init copies it verbatim into a new deployment, so it is the whole of what an operator can learn this system accepts, and a variable missing from it is one nobody can find.
Twenty-six variables were read by the code and named in no .env.example, no README and no docs page. Nine are now keys here:
GITHUB_PAT_VAR, which variable actually holds the PAT. The name is read verbatim and checked against nothing, so pointing it at a variable holding something else would send that value to GitHub as a token.GITLAB_AUTH_SOURCE,FORGEJO_AUTH_SOURCE,AZURE_AUTH_SOURCE, one value each,pat, which is also the default. They exist to refuse a wrong assumption of symmetry withGITHUB_AUTH_SOURCE, and the refusal needs that forge's own token to be set.POLL_REPOSandPOLL_INTERVAL_SECONDSfor the poller, plusPI_CODING_AGENT_DIR,PI_GRAPH_DIRandPI_DISPATCH_DEPLOYMENT_FILE.
The other seventeen carry a comment at their own read site saying why they cannot be keys, and the file closes with the accounting: every variable that is not a key, with its reason. REQ-DEPLOYMENT-BOOTSTRAP states the rule and a test enforces it, so the gap cannot reopen quietly.
Upgrading: npm install. No service reinstall. init never overwrites an existing .env, so to pick up the new commentary in your own file, diff it against the .env.example in this package.
Worth knowing if you run Google models: doctor accepts GOOGLE_API_KEY for PI_PROVIDER=google and accepts gemini as a provider name. pi reads neither, so both pass the preflight and then refuse every job before spending anything. Issue #286, not fixed in this release.
@edgehero/pi-dispatch-admin v1.10.2
A version bump with no behaviour change of its own.
The setup wizard pins the worker version it installs, and that pin is bolted to the worker package by an
anti drift test. Worker 1.10.3 moves the pin, which changes what this bundle installs, so this package is
republished to carry it.
Installing the wizard from this version gets worker 1.10.3, which stops its live edit file watches when
the worker stops. Shipping a worker bump without republishing here is what once left the admin package
installing a worker one patch behind.
@edgehero/pi-dispatch-admin v1.10.1
A version correction. The setup wizard pins the worker version it installs, and that pin is bolted to the worker's own package.json by an anti-drift test, so it moves whenever the worker does.
1.10.1 of the worker shipped without a matching admin release, which left @edgehero/pi-dispatch-admin@1.10.0 on npm installing a worker one patch behind. This closes that: the wizard now installs @edgehero/pi-dispatch@1.10.2, the version it ships beside.
Nothing else in the console changed. The panel, the tools and the read model are byte-identical to 1.10.0 apart from that pin and a handful of comments naming which environment variables the panel reads and why they are not deployment settings.
pi-dispatch v1.10.1
A patch so the resume fix reaches an installed deployment. 1.10.0 shipped with REQ-RESUMABLE-SESSION inert, and the fix landed after that release was cut.
makeProcessor's prepareWorkspace wrapper replaced runJob's third argument instead of extending it, so piVersion never arrived and readCanonical's first gate fired on every job:
if (piVersion === null) return COLD("pi-version-changed");Every run.resume cold-started while reporting success. The failure had no visible symptom: the version stamp written alongside each transcript was correct the whole time, so the sidecar on disk always held the right value and the run record showed a completed job. Only the comparison never happened.
The defect predates 1.10.0. If you use run.resume, upgrade; if you do not, nothing here affects you.
It survived because a test pinned it, asserting the replaced argument shape as correct. That assertion now checks each key it cares about, and a new test drives the wrapper directly and fails on the old one.
@edgehero/pi-dispatch-admin stays at 1.10.0 and the receiver at 1.5.0. Neither changed.
pi-dispatch v1.10.0
Where a job's container gets built is now a named backend, and what each one guarantees is declared rather than assumed (#227).
Nothing changes for an existing deployment. There is still exactly one backend, local, the Docker daemon on the worker's own host, and a deployment that sets neither new variable runs exactly as before: same argv, same env, same mounts, byte for byte.
What is new
PI_BACKENDS names which backends this deployment blesses. PI_BACKEND_FLOOR is the minimum every one of them must declare, as property=word pairs (egress=enforced,nonRoot=asserted). Both are env-only, deliberately: a bound that can be widened from the surface it bounds is not a bound.
A trigger can name its venue with run.backend, refused at load if the name is unknown and refused before it spends if your deployment does not bless it. The panel's dispatch_trigger_add and _edit offer it as a picker bounded by the same list.
pi-dispatch doctor prints the declaration. Every backend declares thirteen properties in one of three words:
enforced: this worker builds it, in its own code, and a test reads it backasserted: something outside the worker provides it, and doctor names whoabsent: not provided, and a deployment that needs it is refused rather than downgraded
The three print differently on purpose. A control you believe in but do not have is worse than one you know is missing, so asserted renders as a warning naming its source, and absent as a failure.
Two of local's thirteen are asserted, and both are worth reading: nonRoot, because USER pi is the image's and not the worker's argv, and credentialTransit, because every spawn inherits the worker's environment and DOCKER_HOST would redirect it to another machine with your provider key and per-job token along for the ride.
docs/backends.md is the contract for adding a venue: the five functions and what each must return, the thirteen properties, the transfer rules, and the three conflicts no vendor resolves.
Worth knowing
A floor naming a switched-off control refuses to boot. Asking for egress=enforced while PI_EGRESS=0 is a bound you would believe in and not have, so the worker says so instead of starting.
pi-dispatch sandbox is local-only and says so, rather than failing on a missing directory.
The conformance suite verifies three of the thirteen properties plus a bundle's shape. The other ten need a live container on the target runtime, and the harness names each one and what it would take. A green run is not a conformant backend.
Upgrading
Nothing to do. PI_BACKENDS and PI_BACKEND_FLOOR are documented in .env.example and both are optional.
@edgehero/pi-dispatch v1.10.1
A patch so the resume fix reaches an installed deployment. 1.10.0 shipped with REQ-RESUMABLE-SESSION inert, and the fix landed after that release was cut.
makeProcessor's prepareWorkspace wrapper replaced runJob's third argument instead of extending it, so piVersion never arrived and readCanonical's first gate fired on every job:
if (piVersion === null) return COLD("pi-version-changed");Every run.resume cold-started while reporting success. The failure had no visible symptom: the version stamp written alongside each transcript was correct the whole time, so the sidecar on disk always held the right value and the run record showed a completed job. Only the comparison never happened.
The defect predates 1.10.0. If you use run.resume, upgrade; if you do not, nothing here affects you.
It survived because a test pinned it, asserting the replaced argument shape as correct. That assertion now checks each key it cares about, and a new test drives the wrapper directly and fails on the old one.
@edgehero/pi-dispatch-admin stays at 1.10.0 and the receiver at 1.5.0. Neither changed.
@edgehero/pi-dispatch v1.10.0
The container backend becomes a named, declared seam (#227). An existing deployment is unaffected: local is still the only backend, and a worker that sets neither new variable produces a byte-identical docker run argv.
Configuration
| Variable | What it does |
|---|---|
PI_BACKENDS |
which backends this deployment blesses (default: local) |
PI_BACKEND_FLOOR |
the minimum every one must declare, as property=word pairs |
Both env-only. Every malformed part refuses rather than being skipped: an unknown backend name, a pair with no =, an unknown property name, an unknown word, a property named twice. A floor the parser cannot read must never become one it reads as asking for nothing.
Triggers
run.backend names the venue. Refused at load for an unknown name or a bad charset, and refused pre-spend for a name this deployment does not bless, ahead of the image inspect, the mint, the clone and the reservation. A near-miss spelling is refused too, including the plural run.backends: a dropped venue runs the job somewhere else while the file reads as though it chose.
A remote venue on a cron or local trigger is refused permanently. The operator's own folder has to be bind-mounted and edited in place.
Internals worth knowing if you extend it
stopContainer and reap are backend functions now, not literals in the worker. makeBackendRegistry resolves every per-job function through one lookup, so the abort stops the container in the venue that built it. reap's tri-state is unchanged and still gates the scope-claim sweep: reaped: true means this host has established it holds no job containers, and one venue that could not enumerate makes the whole answer unproven.
Docker's 125/126/127 are backend-declared rather than assumed, because they collide with the runner's own exit channel.
After an abort, the wait is bounded. If the stop does not take, the job's slot, lease and budget reservation are released rather than held forever, and stop_did_not_take says so.
For adapter authors
docs/backends.md is the contract. @edgehero/pi-dispatch/backend-conformance exports a suite you run against your own backend; @edgehero/pi-dispatch/container-spec gives you the mounts as transfers, with the read-only downgrade a copying runtime takes stated rather than hidden.