Skip to content

Fix #1691: surface tower owner-guard refusal in the CLI, not a 30s timeout - #1692

Merged
amrmelsayed merged 10 commits into
mainfrom
builder/bugfix-1691
Sep 17, 2026
Merged

amrmelsayed merged 10 commits into
mainfrom
builder/bugfix-1691

Conversation

@amrmelsayed

Copy link
Copy Markdown
Collaborator

Summary

afx tower start daemonizes the tower-server and waited only on the port for readiness, so when the #1629 owner-lock guard refuses (the daemon logs its teaching error and exits within ~1s) the launcher burned the full 30s budget and printed a generic "failed to respond within 30000ms" — indistinguishable from a genuine hang (#1685 class), with the guard's teaching error reaching only tower.log. The readiness wait now watches the spawned daemon's liveness, distinguishes three outcomes, and on a fast-exit surfaces the daemon's refusal verbatim.

Fixes #1691
Refs #1690 (item 1 of the owner-lock robustness follow-up; items 2+3 remain there)

Root Cause

waitForServer(port) in packages/codev/src/agent-farm/commands/tower.ts polled /api/status for up to STARTUP_TIMEOUT_MS (30s) with no awareness of the spawned daemon's liveness. When bootSequence (tower-server.ts) fails the owner-lock claim it calls log('ERROR', ownershipConflictMessage(...)) then process.exit(1); the port never comes up, so the loop ran the full timeout and then emitted a generic message. The teaching error was only ever written to tower.log.

Fix

packages/codev/src/agent-farm/commands/tower.ts (+89/-11):

  • New exported TowerStartupOutcome = 'started' | 'exited' | 'timeout' and waitForServerOutcome(isReady, isDaemonAlive, opts) replacing the boolean waitForServer. It short-circuits to exited the instant the daemon is seen dead (with a final readiness re-probe to close the benign same-tick race), so a refusal no longer burns the 30s budget.
  • towerStart registers serverProcess.on('exit') and captures the tower.log byte offset right after its own pre-spawn writes. On exited it reads everything the daemon appended for this run (readLogSince) and prints it verbatim on stderr (the owner-guard teaching error, or any early-boot failure), then exits non-zero. The three outcomes are now distinguishable in output: started / exited (refused-with-reason) / timeout (still running, status unknown).

No skeleton twin: the Tower launcher is product code, not a shipped template.

Test Plan

  • Regression test added (bugfix-1691-tower-start-surface-refusal.test.ts, 6 tests): the outcome logic incl. the "no 30s burn" timing guarantee and the benign same-tick race; and two towerStart tests (mocked spawn/http/shell) proving the teaching error is surfaced verbatim on stderr with a non-zero exit within seconds, plus the empty-log fallback.
  • Build passes (pnpm build)
  • All tests pass (full codev suite via porch check; tsc --noEmit clean)

…meout

`afx tower start` spawns the tower-server daemon detached and polled only its
port for readiness. When the #1629 owner-lock guard refuses, the daemon logs its
teaching error and exits within ~1s, but the port-only wait burned the full 30s
budget and then printed a generic "failed to respond within 30000ms" — the whole
value of the guard's teaching error was lost to tower.log, and the outcome was
indistinguishable from a genuine hang.

The readiness wait now also watches the spawned daemon's liveness and returns one
of three outcomes: started / exited / timeout. On a fast-exit it reads back what
the daemon appended to tower.log for this run and prints it verbatim on stderr
(the owner-guard's refusal, or any early-boot failure), then exits non-zero — the
instant the exit is observed, not after 30s.

Refs #1690 (item 1 of the owner-lock robustness follow-up; items 2+3 remain there).
- Add a post-loop liveness check in waitForServerOutcome so a daemon that exits
  as the readiness budget expires is reported as `exited`, not a misleading
  `timeout` (codex).
- Convert the started/exited ternary to if/else per the project's if/else
  preference (claude).
- Assert the fast-exit reaction time explicitly (< 5s) in the towerStart test,
  and add a unit test pinning the post-loop liveness check.

Refs #1690.
…message

The owner-lock refusal (ownershipConflictMessage) was only a tower.log line until
this lane surfaced it at the CLI, so its `(Issue #1629)` / `(#1515)` references
now show to end users. Remove them from the message text, keeping the actionable
`CODEV_AGENT_FARM_DIR (NOT AGENT_FARM_DIR)` guidance; the incident context moves
into the function's JSDoc. Comments carry issue numbers, user-visible messages do
not.
@amrmelsayed
amrmelsayed merged commit 4543313 into main Sep 17, 2026
7 checks passed
amrmelsayed added a commit that referenced this pull request Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

afx tower start: surface the owner-guard refusal in the CLI instead of the generic 30s timeout (split from #1690 item 1)

1 participant