Relationships
#1750 Dispatched steps over 15 minutes cannot write results: dispatch credentials expire and never refresh
Opened by skunk-ape · 8/20/2026· Shipped 8/24/2026
Summary
A dispatched model method that runs longer than 15 minutes cannot write its
resource. The data-plane call fails with 401 Missing or invalid session credential and the dispatch is recorded as failed — after the work has already
succeeded on the worker.
This is the worst shape a failure can take: the side effects happened, the host was changed, and swamp records the step as failed with nothing persisted.
Cause
src/domain/remote/session_credential.ts:
export const DEFAULT_SESSION_TTL_MS = 15 * 60 * 1000;refresh() in the same file explicitly refuses to renew a dispatch credential:
/**
* Slide the window forward: re-issue against a currently valid
* control-channel credential. Does not affect dispatch credentials.
*/
refresh(credential: string): SessionCredentialRecord | null {
const result = this.verify(credential);
if (result === null) return null;
if (result.dispatchId) return null; // <-- dispatch credentials never renew
...So a dispatch credential is minted once, lives 15 minutes, and cannot be slid
forward. src/serve/data_plane.ts:160 then rejects the write:
const auth = this.#authenticate(req);
if (auth === null) {
return errorResponse(401, "Missing or invalid session credential");
}I could not find a swamp serve flag that changes the TTL.
Reproduction
swamp serveon the orchestrator; enroll a worker with a label.- Author a workflow whose step is placed on that worker by label, calling a
model method that takes more than 15 minutes and then calls
ctx.writeResource(...)at the end. - Run it through the orchestrator.
Observed twice, with a model method that installs a large upstream project (Node, Python toolchain, and a Chromium download):
Dispatch d7dcb302-6d53-46c6-8a2f-6361baa02f0d on worker "hermes-vm" "failed" in 1519199ms
✗ Dispatch d7dcb302: @skunk-ape/hermes.install failed in 1519194ms
— Data plane POST /data/resource failed (401): {"error":"Missing or invalid session credential"}and again at 27m40s on a second run. The install itself succeeded both times — the binary, the pinned checkout, and the downloaded browser were all correct on the host afterwards. Only the bookkeeping failed.
Why this matters beyond one extension
The remote-execution design record makes the worker the right place to run host-shaped work: installs, builds, image pulls, migrations. Those are exactly the operations that routinely exceed 15 minutes. The current behaviour puts a hard ceiling on how long any dispatched step can be and still record anything, and the ceiling is invisible until you cross it — there is no warning at 14 minutes, and the error names a credential rather than a timeout, so it reads as an auth misconfiguration rather than a duration limit.
It also interacts badly with retries. A no-write step re-dispatches automatically, but a step that failed at its write is treated as having written, so it does not re-dispatch — and there is nothing to resume from.
Suggested directions
Any one of these would resolve it:
- Renew while the dispatch is live. The orchestrator already tracks whether
a dispatch is active (
#dispatchesWithWrites,releaseDispatch). If the dispatch is still open, sliding its credential forward does not widen the trust window in any meaningful way — the credential dies with the dispatch either way. - Mint the credential against the step's own timeout, so a step allowed to run for an hour gets a credential that lasts an hour.
- Make the TTL configurable on
swamp serve, as a stopgap.
Option 1 seems strictly best: it needs no new configuration and it ties the credential's lifetime to the thing it actually authorizes.
Smaller, separate observation
While diagnosing this I also hit a related sharp edge that may be worth a
mention in the remote-execution docs: a dispatched method that shells out can
have its wall-clock inflated by orphaned grandchildren holding its stdout pipe
open. timeout(1) signals only its direct child, so a killed step can leave
descendants alive, and anything that reads the child's output to EOF then blocks
long after the command finished. In my case that added thirteen minutes and was
what pushed the step past the fifteen-minute ceiling in the first place. That
part is my bug to fix, not swamp's, but the combination is what makes the 401
easy to misdiagnose.
Environment
- swamp
20260820.011410.0-sha.23de2d5a - Orchestrator: macOS 15 (arm64),
swamp serve --host 127.0.0.1 --port 9099,--auth-mode none - Worker: Ubuntu on Lima
vz, linux/aarch64, enrolled viaswamp worker connectunder systemd
Shipped
Click a lifecycle step above to view its details.
stack72 commented 8/24/2026, 8:36:57 PM
Thanks @skunk-ape for reporting this! The fix has been merged and a release is on its way. We appreciate your contribution to swamp.
Sign in to post a ripple.