Saphan StudioDocs
Troubleshooting

Dispatch and capacity

Why work went to one place and not another, or nowhere — and the refusal classes about starting a process, a server or an interactive session.

21 classes. 4 explained, 17 named only.

⚠ Read this first, because it is the part of the section most likely to mislead you. Dispatch refusals — the ones that decide where a piece of work runs, or that it will not run — are the refusals an operator meets most often, and they are not identified by a class from this list. They are identified by the rule the refusal names, and that rule is the whole diagnosis. The classes on this page are a different, narrower set: they are about getting a process, a server or an interactive session up and reachable, not about choosing where work lands.

So there are two different things to look up, and knowing which one you are holding saves the search.

If your refusal carries a rule number

Every dispatch refusal names its rule and says what it saw. A refusal costs nothing — no budget is spent, no work is lost, nothing is left half-written. When in doubt, attempt it and read the answer; that is cheaper and more reliable than reasoning about whether it would have worked.

The ones an operator meets most:

you seeit meanswhat to do
the seat is busythat exact seat is running somethingwait, or name another seat
the lane is exhaustedthe vendor's budget ran out during the runthe run is lost; wait for the window to reset
the seat is spent and an alternative is namedthis seat's budget is nearly gone and a healthier one existsdispatch onto the named seat
the seat is spent and it was admitted with a warningbudget nearly gone, and no healthier seat of that backend existsproceed knowingly, or wait for the reset
usage is unknownno reading is available for this seatnot a fault — the check abstains rather than guessing
trust is not stampedthis host has not been granted trust for this workspacea one-time setup act, per host and per workspace. This is a human act.
the repository is not freshthe host's copy of the code is behindupdate the host
an owner blocka human deliberately blocked thisask the human

⚠ Absence of a measurement is not a refusal. A seat with no reading is admitted with a note. And a row that reads unmeasured is not a row that reads refused — the first means nobody has looked, the second means someone looked and said no.

Four things that surprise operators

  • "It refused, but the seat looks free." Busy is measured at the moment of dispatch, and several things can hold a seat besides a visible run: a lock from a sibling attempt, a host ceiling reached by other seats, a role already out.
  • "It picked a tired seat over a fresh one." The redirect only applies when the fresh seat shares the tired one's backend. A fresh seat of a different vendor is not an alternative.
  • "The same work refused on three different seats in a row." Then the objection is probably not about seats at all. Trust and workspace conditions are bound to the host and workspace, so changing seats does not clear them.
  • "Two identical requests behaved differently an hour apart." Readings age. A seat measured as healthy at nine may be spent by ten, and the check reads the latest reading, not the one you saw.

⛔ Read the refusal before you change anything. Every refusal in this system is written to name what it saw; changing a setting to make a sentence go away is how a correct refusal becomes an incorrect configuration.

Starting the server

The listen address leaves the host implicit

Class: listen-ambiguous.

What it means. A listen address such as :7654 names a port and no host, and that form binds every interface. Widening beyond loopback must be a deliberate act, so the server refuses to start — whether the value came from --listen, SAPHAN_SERVER_LISTEN or the server.listen key.

What to do. Name the host. The refusal prints both choices with your port filled in: 127.0.0.1:<port> for loopback, or 0.0.0.0:<port> to bind all interfaces explicitly, and it names which setting the value came from.

--detach together with --stdio

Class: detach-stdio-conflict.

What it means. --stdio hands the MCP wire to the terminal that started the server, for the life of the process; a detached process has no such terminal. Nothing is launched.

What to do. Use --detach alone for a background server with an HTTP listener, or --stdio alone in the foreground.

--tunnel together with --stdio

Class: tunnel-needs-listener.

What it means. A tunnel fronts the HTTP listener, and --stdio starts none. Nothing is launched.

What to do. Drop --stdio, or drop --tunnel. See Transports for what a tunnel is and is not for.

--detach with a port the system would choose

Class: detach-kernel-port.

What it means. A listen address with port 0 lets the operating system pick the port when the server binds — inside the detached process, after the command that started it can no longer see which port it got, confirm it came up, or print an address anyone could reach. It is refused before anything is launched.

What to do. Give an explicit port, or start in the foreground, which reports the port it actually received.

Named, not yet explained

These classes are real, deliberate and named. This section does not yet carry their resolution.

classwhat its name indicates
routing-refusalWork was not routed anywhere. ⚠ If your refusal also names a rule, that rule is the diagnosis — see the table above.
toolchain-unavailablesaphan verify-stream found go, gofmt or git missing from the path, and ran nothing. ⚠ This is a property of the host, not of your request.
result-too-largeA response from the MCP server exceeded its byte cap and was refused rather than truncated. The message names how to narrow the call; see the response budget.
serverd-already-runningThe background service is already running: a live process holds its pidfile. A stale pidfile is taken over without a refusal.
gateway-mtls-listen-not-servedThe gateway was started with mtls_listen set in its [gateway] section, and this build serves no such listener. It refuses to start rather than run without the mutually authenticated listener you configured.

Interactive sessions — saphan cli and saphan run --interactive

classwhat its name indicates
cli-seat-unknownThe seat named is not seated anywhere in this workspace's registry.
cli-seat-not-localThe seat's machine is reached over SSH; this build opens no remote interactive session.
cli-seat-retiredThe seat's machine is retired.
cli-role-mismatchThe seat is registered for a different role than the one asked for.
cli-binary-unregisteredThe seat's machine registers no binary for the seat's backend. It is never looked up on your path instead.
cli-binary-unexecutableThe registered binary is missing, a directory, or not executable on this host.
cli-profile-home-unresolvedThe seat's profile directory does not resolve to an absolute directory on this host.
cli-interactive-flag-conflict--interactive was given together with a flag that only applies to an unattended run, or with a role it does not start.
cli-shell-not-installedsaphan cli found no saphan-shell beside this binary. It is never looked up on your path.
cli-backend-not-interactiveThe seat's backend has no vendor command-line tool to enter — for example a seat bound to an API lane — or is one this build does not know.
cli-registry-unreadableThe workspace's machine registry could not be loaded, so no seat can be resolved.
cli-session-record-unreadableResuming a session failed because its record could not be read — not because the seat is unknown.