The sandbox path is LIVE and it works — verified end to end against
api.hanzo.ai as a real user: create 201, `test -w .` exit 0, write, read
back, 404 for a missing file, 409 → the running sandbox, DELETE 204 with
the volume re-attached and the file still on it. The root-owned-mount
defect the comment here described is fixed in prod (/work is root:sandbox,
setgid, uid 1000 in the group), so `durable:true` is the real answer now
and the in-memory fallback is genuinely the exception. Four comments that
asserted otherwise said so as present tense and are corrected.
WHAT WAS STILL WRONG.
Nothing left the server until the pod was running. `resolveFs` ran before
the route returned its response, and a streamed body sends no head until
its first chunk, so a browser's `fetch()` stayed pending for the whole
create — 14.6s measured on a cold project, indistinguishable from a hung
request, and it is the first thing a person sees of the feature. The slow
work moves onto the stream and an SSE comment flushes the head: 14.59s →
0.59s to first byte, same run, still durable.
Every refusal was the same refusal. `openSandbox` returned `Info | null`,
and `null` was out-of-quota (429), pod-would-not-start (503), not-entitled
(403), unreachable, and a create we abandoned ourselves — all reported to
the person as "the sandbox service did not give one out", which is true of
all five and actionable for none. It returns a sandbox or the service's own
sentence, and the caller cannot read the second without handling the first.
We hung up before cloud answered. The create deadline was 30s; a cold image
pull is ~27s and cloud waits ~2min before it answers 503 itself. Inside that
window our abort was indistinguishable from a refusal, so the run fell back
to memory while the pod came up moments later with nobody holding its id —
and then held the project's volume, so the NEXT run got a 409 for a sandbox
this one had given up on. 150s, past cloud's own deadline: the answer a
person gets should be the service's, never the sound of us hanging up.
A 502 reached the model as a broken sandbox. It is what cloud returns when a
command runs past its deadline (a deadline is not an exit code), and
`SandboxError` dropped the body, so `returned 502` sent the model debugging
the cluster instead of its own command. Cloud's sentence now survives.
A gateway refusal reached the person as raw JSON. This loop was the one AI
path deciding for itself what a status means; it now uses `refusal()`, the
same one definition the rest of the app uses — so an agent run that stops
because the credit ran out says that.
/v1/agents/runs was in the cheapest rate-limit tier. Simply unlisted, so it
fell to a generic 60/min while single-shot /v1/generate got 30 — the call
that can cost 24 model turns plus arbitrary commands in a pod was allowed
twice the calls of the one that cannot. Listed exactly, not as /v1/agents:
the registry under that prefix is polled to watch a run.
And `changedFiles` read the run's files one at a time, after the model had
stopped talking — twenty round trips of dead air with nothing to stream over
them. Concurrent.
tsc clean, 984 tests green, lint ratchet at the ceiling, build green.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>