Conversation
…orkspace `create` proved a container ready before handing it back; `wake` only started it. But the bootstrap deletes its readiness marker on every start and then brings up dockerd, fixes the socket permissions and launches the preview gateways — so `docker start` returning says nothing about whether the workspace can be used. Both callers of `wake` act on the handle immediately. `exec` runs the agent's command, which is where `docker`, `docker compose` and a half-chowned `/workspace` fail intermittently after a resume or a host restart; `preview.open` reads the published ports, which are bound before the gateway inside is listening. The Vercel runtime already re-initializes on `wake`, so the two providers did not honour the same contract. Prove readiness on `wake` as well. Readiness only moves forward within one container start, so each start is proven once and remembered by container name and `StartedAt` — a resumed or replaced container gets a new start and is proven again, and `exec` does not pay for a probe per command.
691cb3c to
c5f1ee5
Compare
adrian-lorenzo
left a comment
There was a problem hiding this comment.
Thanks for addressing this.
A real restart still fails because /var/run/docker.pid survives the stop and blocks dockerd from starting. Please handle stale PID cleanup and add a real suspend/wake regression test so the resume fix works end to end.
…pace wakes Waiting for the bootstrap was necessary but not sufficient: on a real suspend and wake the bootstrap never becomes ready at all. A stopped container keeps its writable layer, so dockerd finds the pidfile its own previous start wrote and refuses to boot. The readiness loop then spends its 120 attempts, `set -eu` aborts, and the container exits — which the readiness check now reports honestly instead of handing back a workspace, but the wake still fails. The bootstrap removes the daemon's stale runtime state before starting it: the pidfile, the socket, and the exec root holding containerd's own pidfile and socket. Nothing is running at that point, because the script is the container's entrypoint, so all of it is stale by construction. Verified on a real container rather than reasoned about: a value written to /var/run/docker.pid before `docker stop` is still there after `docker start`, and the new line removes it. Two tests, because one of them cannot run everywhere: - a unit case asserts the generated bootstrap clears that state *before* the dockerd line, since doing it afterwards would be useless; - an end-to-end case suspends and wakes the same container, asserts the compute reference is unchanged so it is a resume rather than a replacement, and asserts the nested daemon answers again. It runs the cycle twice: the first wake writes a pidfile of its own, so a fix that only cleaned up after the original create would pass once. The existing end-to-end case never met this. It exercises `replaceCompute`, which discards the writable layer; only stopping and starting the same container reaches the state that breaks.
|
Addressed at 14ef273. You were right, and I confirmed the mechanism on a real container rather than assuming it: a value written to |
The gap
DockerWorkspaceRuntime.createproved a container ready before handing it back.wakeonly started it:But the bootstrap deletes its own readiness marker on every start, and only then brings up
dockerd,chowns/workspace, fixes the socket permissions and launches the preview gateways:So
docker startreturning says nothing about whether the workspace can be used, and both callers ofwakeact on the handle immediately:exec→wake→ run the agent's command. This is wheredocker,docker composeand a half-chowned/workspacefail intermittently after a resume, a host restart, or an OOM restart — the socket has just been deleted and not yet recreated.preview.open→wake→ read the published ports. Docker binds the host port at container start, so the endpoint URL is valid while the gateway process behind it is not yet listening.VercelWorkspaceRuntime.wakealready re-runsinitializeAndHandle, so the two providers were not honouring the same contract — and Docker is the default for self-hosting anddocker-compose.This is the level-triggered version of the same bug shape as #319/#320:
wakewas reacting to the edge ("start returned") instead of observing the state ("the marker is there and dockerd answers").The change
wakeproves readiness too, through a sharedensureReady.Readiness only ever moves forward within one container start, so each start is proven once and remembered by container name →
${Id}:${StartedAt}:StartedAt(or a newId) and is proven again;exec— which callswakeper command — does not pay for a probe per command, only theinspectit already needed;preview.openand a turn'sexecgenuinely race here.The map is keyed by the workspace's stable container name and cleared in
destroy, so it is bounded by live workspaces.ensureReadyalso fails loudly when the bootstrap exited instead of becoming ready, rather than returning a handle to a dead container.Test
New
services/api/test/workspace-docker.test.ts, using an injected fakedockerode— the same shape as the existingworkspace-vercel.test.ts— so it runs in the default suite with no Docker daemon:start;wakecalls against the same container start probe once;workspace_initialize_failedinstead of returning a handle.All four fail on unmodified
main(git stash-ed the source file to check) and pass with the change.Verification
Run on Windows 11, Node 26, against the compose Postgres.
pnpm --filter @facility/api typecheck— cleanpnpm lint— cleanpnpm guards— 2 guards, 0 failedpnpm check:unused— cleanvitest run test/workspace-docker.test.ts test/workspace-runtime.test.ts test/workspace-preview.integration.test.ts test/story-workspaces.integration.test.ts— 21 passedservices/apisuite: 258 passed, 14 failed — every failure also fails on unmodifiedmainin this environment (the POSIX-shell fakes inworkspace-vercel-bootstrap,agent-engines,facility-012.e2e,project-environment,turn-dispatcher, i.e. the#227/#241Windows family), plus one load-dependent flake inworkspace-runtime.test.tsthat passes 3/3 in isolation and only exercisesFakeWorkspaceRuntime.What I could not verify here: the real Docker path.
FACILITY_E2E_DOCKER=1 pnpm test:e2e-workspaceneeds a Linux daemon that can run the privileged runner image with nesteddockerd, and this machine has rootless Podman. The probe itself is the onecreatehas always used, so the risk is inensureReady's call sites rather than in the probe, but a realsuspend→wake→execon CI is the check that would actually close it.