fix(sandbox): a dev server killed while running crashed, it did not fail to start - #7026
Merged
Merged
Conversation
…ail to start `OnTaskExit` mapped every non-zero, unintentional dev exit to `start-failed`, whatever phase it happened in. `restartablePhase` excludes `start-failed`, so that latch permanently disarms devwatch — and nothing else calls `RestartDev`, so the sandbox stays dark for the life of the pod while still reporting ready. This fires constantly in practice. An agent has no endpoint to restart the dev server (only devwatch calls RestartDev), so it uses `pkill` — which it must, to pick up discovery.config.js or FASTSTORE_THEME. yarn reports the signal as exit code 1, and the daemon reads a deliberate restart of a healthy server as a failure to start. Observed on tenant-electrolux-prod-jg6qk: [probe] server responded on port 3000 (status 200) error Command failed with signal "SIGTERM" task exit task=dev status=exited exit_code=1 lifecycle transition from=running to=start-failed That pod, and 3 of the other 7 electrolux pods, had nothing listening on 3000. Two runs then spent 25 of 55 and ~15 of 82 tool calls rebuilding by hand what the watchdog was sitting right next to. A server that reached `running` started fine, so route it to `crashed`, which is restartable: devwatch respawns it after its grace window, and RestartDev clears the `error` status that would otherwise skip the start step. `start-failed` stays correct from `starting` — a server that never came up. Repeated failures stay bounded by devwatch's own MaxRestarts/ActionGiveUp, which is why `crashed` re-entering itself must not latch either.
pedrofrxncx
enabled auto-merge (squash)
September 4, 2026 20:38
decocms Bot
pushed a commit
that referenced
this pull request
Sep 4, 2026
PR: #7026 fix(sandbox): a dev server killed while running crashed, it did not fail to start Bump type: patch - @decocms/sandbox (packages/sandbox/package.json): 1.61.4 -> 1.61.5 - deploy/helm/sandbox-env (chart 0.16.46) (deploy/helm/sandbox-env/values.yaml deploy/helm/sandbox-env/Chart.yaml): image.tag/appVersion -> 1.61.5 Deploy-Scope: both
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
orchestrator.goOnTaskExitmapped every non-zero, unintentional dev-script exit toPhaseStartFailed— regardless of the phase it happened in.restartablePhase(main.go:406) allows onlyrunning | starting | crashed. Sostart-failedpermanently disarmsdevwatch, and nothing else callsRestartDev.OnTaskExitalso setsstatus.State = "error", andstepStartskips starting unless status isrunning— so a config re-apply won't revive it either. The sandbox stays dark for the life of the pod while still reporting ready.Why it fires constantly
An agent has no endpoint to restart the dev server —
RestartDevis called only bydevwatch. So it usespkill, which it legitimately must to pick updiscovery.config.jsorFASTSTORE_THEME(both read once at boot). yarn reports the signal as exit code 1, and the daemon reads a deliberate restart of a perfectly healthy server as a failure to start.Observed live on
tenant-electrolux-prod-jg6qk:At the time of writing that pod — and 3 of the other 7
tenant-electrolux-prod-*pods — had nothing listening on 3000. Two prod runs (ELEC-244,ELEC-245) then spent 25 of 55 and ~15 of 82 tool calls killing, rebuilding and re-polling the dev server by hand, next to a watchdog that had been switched off.Change
DevExitPhase(current)— extracted so the decision is unit-testable:runningcrashedcrashedcrashedstartingstart-failedstart-failedRecovery chain verified end to end in code:
devwatch→ActionRestart→RestartDev→clearCrashError(error→running) →stopDevTask→enqueue(StepStart), whose status guard now passes.crashedmust re-enter itself rather than latch, or one retry would disarm the watchdog and defeatMaxRestarts— repeated failures stay bounded by devwatch's ownActionGiveUp, which transitions tostart-faileddeliberately.Testing
internal/setup/devexit_test.gopins the table above.go build ./...,go vet ./...,gofmt -lclean;setup,devwatch,lifecyclesuites pass.Relation to #7023
#7023 makes the probe distinguish "answering 5xx" from "healthy". It is not the fix for this — the watchdog is disarmed before the probe's opinion is ever consulted. This PR is the one that matters; #7023 is a complementary net and can be merged, deferred, or closed independently.
Summary by cubic
Fixes the sandbox dev server lifecycle so a dev process killed while running is treated as a crash (restartable) instead of a startup failure, which permanently disarmed the watchdog and left the sandbox dark.
runningorcrashednow transition tocrashed; exits fromstartingstill becomestart-failed.start-failedis not restartable, so misclassifying a running server's death as a startup failure disabled devwatch for the pod's lifetime.MaxRestartsandActionGiveUpbecausecrashedre-enters itself.Written for commit b36713f. Summary will update on new commits.