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.