Problem
Tower opens its HTTP port and accepts requests before its internal dependency wiring is complete. Requests that need _deps (set by initInstances()) during that window fail closed. Concretely: DELETE /api/terminals/:id returns 404 for a terminal that exists — while GET on the same id returns 200, because the GET path doesn't touch _deps.
Root cause
The entire boot sequence lives inside the server.listen() callback (tower-server.ts:375): stale-socket cleanup → session reconcile → orphan kill → husk sweep (#1227) → session-log retention scan (#1238/#1243) → and only then initInstances() (tower-server.ts:575). killTerminalWithShellper() (tower-instances.ts:799) starts with if (!_deps) return false, which the DELETE route (tower-routes.ts:842) translates into 404 Session not found.
The race predates 3.2.4, but the husk sweep and retention scan added in the 3.2.4 window run between port-open and deps-wiring, so the window now scales with disk state: with ~1,400 session-log files (2.2 GB) it exceeds 100ms, enough that afx tower start && <immediate afx command> can hit it. Discovered when tower-terminals.e2e.test.ts > DELETE > kills and removes a terminal began failing deterministically on a log-heavy dev machine while CI (fresh runners, no logs) stayed green. Instrumentation confirmed _deps was null at DELETE time; a 400ms delay before DELETE makes the test pass.
Suggested fix
- Routes that need
_deps should return 503 with Retry-After ("Tower is starting up") instead of 404/false — generalizing the pattern stopInstance already uses ("Tower is still starting up. Try again shortly.").
- Structurally: defer
server.listen() until after initInstances() (or hold requests until init completes). Note the ordering comments in the boot sequence — some steps must run before initInstances() reads architect state — so this needs care.
(1) is small and safe; (2) closes the window for good if it proves compatible with the ordering constraints.
Testing notes
The existing DELETE e2e test covers the regression once the window is closed (it currently only fails on machines with large session-log volume — CI won't catch it until the fix makes readiness deterministic).
Problem
Tower opens its HTTP port and accepts requests before its internal dependency wiring is complete. Requests that need
_deps(set byinitInstances()) during that window fail closed. Concretely:DELETE /api/terminals/:idreturns 404 for a terminal that exists — whileGETon the same id returns 200, because the GET path doesn't touch_deps.Root cause
The entire boot sequence lives inside the
server.listen()callback (tower-server.ts:375): stale-socket cleanup → session reconcile → orphan kill → husk sweep (#1227) → session-log retention scan (#1238/#1243) → and only theninitInstances()(tower-server.ts:575).killTerminalWithShellper()(tower-instances.ts:799) starts withif (!_deps) return false, which the DELETE route (tower-routes.ts:842) translates into 404Session not found.The race predates 3.2.4, but the husk sweep and retention scan added in the 3.2.4 window run between port-open and deps-wiring, so the window now scales with disk state: with ~1,400 session-log files (2.2 GB) it exceeds 100ms, enough that
afx tower start && <immediate afx command>can hit it. Discovered whentower-terminals.e2e.test.ts > DELETE > kills and removes a terminalbegan failing deterministically on a log-heavy dev machine while CI (fresh runners, no logs) stayed green. Instrumentation confirmed_depswas null at DELETE time; a 400ms delay before DELETE makes the test pass.Suggested fix
_depsshould return 503 with Retry-After ("Tower is starting up") instead of 404/false — generalizing the patternstopInstancealready uses ("Tower is still starting up. Try again shortly.").server.listen()until afterinitInstances()(or hold requests until init completes). Note the ordering comments in the boot sequence — some steps must run beforeinitInstances()reads architect state — so this needs care.(1) is small and safe; (2) closes the window for good if it proves compatible with the ordering constraints.
Testing notes
The existing DELETE e2e test covers the regression once the window is closed (it currently only fails on machines with large session-log volume — CI won't catch it until the fix makes readiness deterministic).