Repository navigation
Gateway restart leaves every sandbox in Error (StartFailed); --restart-policy always not honored #4910
Description
User Story
As an operator restarting the local gateway (e.g. brew services restart openshell after a config change or upgrade), I want my sandboxes to come back — automatically per their restart policy, or at least be directly restartable — so that routine gateway maintenance doesn't take down every running workload.
Problem Statement
After a gateway restart, every sandbox transitions to Error:
Ready: False (StartFailed) - Failed to start sandbox during gateway startup:
Docker supervisor exited before becoming ready
- No sandbox recovers on its own, including ones created with
--restart-policy always(Restart count stays 0). openshell sandbox startrefuses from this state:
Error: × sandbox must be Stopped, Completed, or a failed main-process Error to start
(current phase: Error); stop the sandbox before retrying start
- The only recovery is a per-sandbox
stop→start.
Impact / Why This Matters
Any gateway restart or upgrade is a fully destructive operation for all sandboxes, with per-sandbox manual recovery. --restart-policy always silently does not apply to this path, which is surprising given its name.
Acceptance Criteria
- After a gateway restart, sandboxes with
restart-policy: alwaysreturn to Ready automatically, or at minimumsandbox startworks directly from the post-restart state, or - the documentation clearly states that a gateway restart requires per-sandbox
stop/start.
Reproduction Steps
OpenShell v0.1.5, docker compute driver:
openshell sandbox create --name repro-b1 --from <any image> # Ready
openshell sandbox create --name repro-b2 --from <any image> --restart-policy always # Ready
brew services restart openshell # gateway listener back in ~2s
openshell sandbox list # both sandboxes: Error
openshell sandbox get repro-b2
# Phase: Error
# Restart count: 0 <- policy=always never engagedRecovery (works, ~6.5 s per sandbox; workspace and policy revisions fully preserved):
openshell sandbox stop repro-b2 && openshell sandbox start repro-b2 # back to ReadyMechanism (from logs)
- During gateway startup, all sandboxes are restored concurrently. Workload containers exit with code 1, logging:
Error: × sandbox boundary received SIGTERM before supervisor confirmation
- The new supervisor's log tail ends with:
boundary terminated: boundary process leaf: sandbox session is terminal
- Interpretation: only the same supervisor process can resume a frozen workload (by design), so after the gateway restart the old workload cannot be resumed; it is SIGTERMed, the new supervisor sees a terminal session and gives up with StartFailed. The restart-policy machinery never runs on this path.
Suggested UX (if applicable)
On gateway startup, either re-create the workload for sandboxes whose restart policy allows it, or leave them in a phase from which sandbox start works directly.
Environment
- OpenShell v0.1.5 (Homebrew, macOS 26.6.2 Apple Silicon)
- Docker compute driver; Docker provider: OrbStack 2.2.4 (Docker Engine 29.8.2). OrbStack is not in the support matrix, so we cannot fully rule out a provider-specific component in "supervisor exited before becoming ready" — but the SIGTERM-before-confirmation behavior looks driver-agnostic. Happy to retest on Docker Desktop if useful.
- Reproduced across 4 independent gateway restarts with 2–4 sandboxes; restart policies tested:
never(default) andalways.
Logs
$ openshell sandbox get repro-b2
Phase: Error
Restart count: 0
Conditions:
- Ready: False (StartFailed) - Failed to start sandbox during gateway startup: Docker supervisor exited before becoming ready; log tail: … openshell_supervisor: Starting sandbox supervision … boundary terminated: boundary process leaf: sandbox session is terminal
$ docker logs <workload container>
Error: × sandbox boundary received SIGTERM before supervisor confirmation
Activity
addozhang commentedon Oct 25, 2026
After filing, we looked into the v0.1.5 source, and part of this behavior is documented design rather than a defect:
boundary_startup_termination_markerexplicitly recognizes "sandbox boundary received SIGTERM before supervisor confirmation" (crates/openshell-driver-podman/src/watcher.rs, mirrored in the docker driver), and the readiness wait has a dedicated branch for "Docker supervisor exited before becoming ready".We're not contesting the non-resumability itself. The parts we'd still like to see addressed — or documented — are:
--restart-policy alwaysis not consulted on this path. The flag is documented as "Restart behavior after the canonical main process exits". A gateway restart does terminate the main process (SIGTERM before supervisor confirmation), yet Restart count stays 0: the gateway-startup restore reports StartFailed without evaluating the restart policy. If that's intentional, the flag/docs should state that the policy does not cover gateway restarts; if not, the restore path could fall back to the policy before parking the sandbox in Error.sandbox startis blocked from the post-restart Error phase ("sandbox must be Stopped, Completed, or a failed main-process Error to start … stop the sandbox before retrying start"). Either allowingstartdirectly from this state, or documenting the requiredstop→startrecovery, would remove the surprise for operators.Happy to reframe this issue to just those two points (or split them) if that helps triage.
O96a commentedon Oct 25, 2026
Likely two separate failures stacked here, and the policy one is the easier to explain: restart policy is only ever evaluated by the supervisor loop that owns the sandbox, so if that loop dies with the gateway and nothing re-reads the persisted policy on startup,
--restart-policyis dead config no matter what you pass. Worth confirming the policy field actually round-trips into the state store — if it isn't persisted, that alone explains "always not honored".The StartFailed is a different bug, and you need to know which of two shapes you have before touching policy code:
Run both right after the restart. If the sandbox record persists but the container is gone, the start path is trying to start something that isn't there — the reconciler needs a "runtime object missing → re-create from spec" branch. If the container is still alive and only the gateway process died, it's an adopt bug: the gateway is re-adopting a handle it no longer owns and failing on the stale handle.
I haven't run this on macOS with brew services, so treat the second half as what I'd check first rather than a diagnosis.
addozhang commentedon Oct 26, 2026
Ran both checks on v0.1.5 (macOS, brew services, docker driver).
Persistence: the policy field does round-trip. Same sandbox (
--restart-policy always), immediately before and afterbrew services restart:So "dead config" doesn't explain this one: the restored record carries the policy; the restore path just never consults it.
Shape:
docker ps -aright after the restart:So it's neither shape exactly: the sandbox record persists, the workload container is present but freshly
Exited (1)(the "boundary received SIGTERM before supervisor confirmation" exit), and the supervisor container is gone after failing readiness.sandbox startthen refuses purely on the phase gate:—the runtime objects a direct
startwould need (fresh supervisor + restarted workload) are exactly what the forcedstop→startcycle creates anyway.That makes it closest to your adopt/state-machine shape: restore attempts adoption → the new supervisor can't resume the frozen workload (by design) → everything unwinds to Error → and the restart policy is never evaluated even though the field survived the restart in the record.
feat(e2e): add a chaos experiment harness
feat(e2e): add a chaos experiment harness
feat(e2e): add a chaos experiment harness
mpereira commentedon Oct 29, 2026
Hi @addozhang,
I reproduced this on the current main branch (9f1cdb4, Docker driver) with an automated fault-injection experiment based on the chaos harness I am proposing in mpereira#1. The sandboxes survive the gateway outage intact. What kills them is the restarted gateway's recovery path, and on the Docker driver that path cannot succeed by construction. The graceful SIGTERM path on main works (stop, then relaunch). So, likely a bug in the Docker driver's startup recovery, not the fence working as intended.
How to reproduce
About 2 minutes after the first build, local Docker only:
git clone -b feat/chaos-experiment-harness/mpereira https://github.com/mpereira/OpenShell cd OpenShell mise run e2e:chaos e2e/chaos/experiments/gateway-kill-keeps-state.yamlThe branch is main 9f1cdb4 plus the harness (nothing under
crates/changes). The command builds the gateway, runs the experiment and prints the run directory, which holdsreport.html, the gateway and sandbox logs,docker eventsand the journals. The experiment lists this issue underexpectations:, so the verdict reads XFAIL (an expected failure) today and XPASS (an unexpected pass) once the behaviour changes. The graceful counterpart ise2e/chaos/experiments/gateway-graceful-restart.yaml.Result
Three sandboxes, each appending one line per second to a journal. The gateway gets SIGKILL 20 s in, stays down 15 s, and is started again. Times are seconds from run start for counter-2, the first sandbox the startup sweep reached; counter-1 and counter-0 go through the same steps 12 s and 24 s later, because the sweep is serial.
Error/StartFailedThe journals hold ticks 1 to 38 at one-second spacing: the 18 s without a gateway cost nothing, and the ticks stop the second the restarted gateway removes the supervisor containers. A sandbox created after the sweep ran to exit 0.
gantt title gateway-kill-keeps-state · 20261029T120842Z-3b2c · XFAIL dateFormat mm:ss axisFormat %M:%S todayMarker off section flow counter :00:01, 01:18 wait counter started :00:01, 00:03 delay 20s :00:03, 00:23 fault gateway-down (outage) :crit, 00:23, 00:41 fresh-sandbox :01:18, 01:19 section counter-0 Created :milestone, 00:03, 00:03 Running :active, 00:03, 00:24 Unknown :crit, 00:24, 00:42 Running :active, 00:42, 01:07 Pending :01:07, 01:18 Failed :milestone, crit, 01:18, 01:18 section counter-1 Created :milestone, 00:03, 00:03 Running :active, 00:03, 00:24 Unknown :crit, 00:24, 00:42 Running :active, 00:42, 00:55 Pending :00:55, 01:05 Failed :milestone, crit, 01:05, 01:05 section counter-2 Created :milestone, 00:03, 00:03 Running :active, 00:03, 00:24 Unknown :crit, 00:24, 00:42 Pending :00:42, 00:53 Failed :milestone, crit, 00:53, 00:53 section fresh-sandbox-0 Created :milestone, 01:19, 01:19 Succeeded :milestone, done, 01:19, 01:19(
Unknownis the harness getting no answer while the gateway is down; theRunningafter it is the store still sayingReadyuntil the sweep reaches that sandbox.)Trimmed log lines for counter-2, from the gateway, the sandbox and
docker eventsin that order:Mechanism
reconcile_runtime_resources_at_startup, force-removes every managed supervisor, staging and identity container (L2135-L2166). That freezes the workload.start_persisted_sandboxes_with_authentication, callsStartSandboxwith credentials frommint_persisted_authentication: same auth epoch, no takeover grant. Only the Kubernetes shared supervisor pool mints grants (supervisor_pool.rs#L2784).start_sandbox_with_lifecycle_fencesees a running container plus credentials and adopts (L2562-L2572), launching a new supervisor container, which is a new supervisor instance.plan_attachadmits only the same instance or a grant with a higher epoch, so it answersWrongSupervisorInstance. That is the documented fence (docs/about/architecture.mdx: a new supervisor can't take over a running sandbox, even with valid credentials).StartFailed(mod.rs#L4313-L4320).restart_policyis only read inshould_restart_main_process. The sweep is serial, about 11 s per sandbox, with the listener already bound, sosandbox listsaysReadyfor frozen sandboxes until the sweep reaches them.So the adopt path removes the only supervisor the sandbox would accept, offers one it will refuse, and has no grant to offer instead. I found nothing saying it is meant to work this way, and the two paths that do work both stop and relaunch:
stop_persisted_sandboxes_on_shutdownstops every running-intent sandbox on SIGTERM and the sweep relaunches it (pinned by the e2e testdocker_gateway_restart_preserves_running_and_stopped_intent). Mygateway-graceful-restartexperiment passes on the same build: both sandboxesReady6 s after SIGTERM, each script started a second time, exit 0.driver.rsrestarts the VM on a non-empty bundle during startup recovery, "rather than leaving the old supervisor connected with invalid credentials."The fix looks scoped: a running container plus credentials at Docker driver startup should mean stop and start fresh, not adopt. A crash, OOM kill or
kill -9of the gateway would then end like a graceful restart.docs/how-it-works/sandboxes/overview.mdx("an existing sandbox can return toProvisioningtemporarily while its supervisor reconnects") should then say the sandbox is restarted.Your v0.1.5 report fits the same picture, with one difference. "sandbox session is terminal" is what a v0.1.5 sandbox sets 30 s after losing its supervisor (
AUTHENTICATED_RECONNECT_TIMEOUT), so your containers ran without one for over 30 s, which means the shutdown sweep did not finish. launchd's default SIGKILL 20 s after SIGTERM, against a 10 sdocker stopper sandbox, would explain it; I have not verified that. The "Stopped sandbox during gateway shutdown" lines in that gateway log would settle it. TheExited (1)containers are the driver's post-failure stop.Not checked
I only ran this against the Docker driver, on macOS. I did not run v0.1.5, Podman, the VM driver or Kubernetes, and I did not verify the launchd timeout on the Homebrew service. The graceful run's scripts exit 0 on their second attempt, so it says nothing about
restart_policyafter the relaunch.Two questions for OpenShell maintainers:
thenashef commentedon Oct 29, 2026
We see the same end state on Linux after a gateway crash (graceful restarts are fine for us), OpenShell v0.1.5, Docker driver, gateway as a systemd service on Ubuntu 24.04.
systemctl kill -s KILL openshell-gateway(systemd restarts it).ExecSandboxfails with "sandbox is owned by gateway replica … which advertises no peer endpoint; set OPENSHELL_PEER_ENDPOINT…".Error/StartFailed; we polled for 6 minutes with no recovery, and workload traffic stops. A per-sandboxStopSandbox+StartSandboxrecovers each one, as described above.A graceful
systemctl restartbehaves differently for us: the shutdown sweep stops every sandbox and the new gateway starts them again.