Skip to content

[#240] Handle broker/platform/room bind failures as controlled command errors#245

Merged
realproject7 merged 1 commit into
mainfrom
fix/240-controlled-listen-errors
Jul 15, 2026
Merged

[#240] Handle broker/platform/room bind failures as controlled command errors#245
realproject7 merged 1 commit into
mainfrom
fix/240-controlled-listen-errors

Conversation

@realproject7

Copy link
Copy Markdown
Owner

Fixes #240

Implements the reviewed #240 CLI bind-failure contract (Batch 40, code batch). Baseline fresh origin/main@1a98db6.

EPIC Alignment

What changed

  • src/cli/commands/listen.ts (new, smallest shared helper):
    • listenOrError(server, port, host) — binds while guarding the one-shot 'error' event: an occupied/invalid bind resolves a failure outcome instead of hanging the listen promise or throwing an uncaught Server error. The guard is removed on success, so post-bind behavior is identical to a bare server.listen. It only observes its own listen attempt — never inspects/kills/mutates any other listener.
    • listenErrorMessage(host, port, error) — a controlled, token-free message built from the bind coordinates + OS error code only (EADDRINUSE / EACCES / EADDRNOTAVAIL / fallback).
  • broker serve, platform serve, room serve — replace the unguarded await new Promise(res => server.listen(...)) with listenOrError; on failure write listenErrorMessage to stderr and return non-zero (no hang, no uncaught error). room serve returns before mutating current-room state or emitting the --allow-remote warning. Each command takes an optional injectable listen hook (defaulting to listenOrError) so the invalid-bind path is deterministically testable.
  • launch (Launcher UX: make bare agentgather open the local dashboard #232) — its listen() now delegates to listenOrError(server, port, "127.0.0.1").then(o => o.ok), preserving the exact prior boolean behavior (localhost-only; bind failure → refuse with the existing token-free message, never a kill). Mechanical reuse, no functional change.

Self-Verification

  • Acceptance criteria 1:1:
    • Occupied-port tests for broker, platform serve, room serve exit non-zero without an uncaught errortest/cli-listen-errors.test.ts occupies a real localhost port and runs each serve on it → real EADDRINUSE → returns 1 (awaited, so no uncaught error / no hang).
    • Deterministic injected-listen-error / invalid-bind per surface, no OS hostname resolution → each surface has an injected listen hook returning { ok:false, error:{ code:"EADDRNOTAVAIL" } } → returns 1 with a controlled token-free message; no hostname lookup involved.
    • Normal bind/shutdown remains covered → existing broker serve (SIGINT), platform serve (waitForShutdown), and room serve (SIGTERM) tests stay green.
    • Error output token-free; no foreign-listener inspect/kill/mutate → tests assert stderr has no tgl_|token=|Bearer|Authorization and that the occupying server is still listening after each failure.
    • Launcher UX: make bare agentgather open the local dashboard #232 launcher behavior has focused regression + no functional change → the shared helper has direct listenOrError/listenErrorMessage unit tests; the existing cli-launch (launch refuses an occupied/incompatible port… Launcher UX: make bare agentgather open the local dashboard #232) and browser-launch suites pass unchanged.
    • Existing CLI tests remain green → full suite 439/439.
  • Kill-list scan: no token/URL in messages (only host:port + code); no new runtime dependencies (Node node:http types + internal module only); no port or remote-exposure policy change; loopback-default/--allow-remote and signal lifecycle untouched; no launch/#232 functional change; no #241–#243 files touched.
  • Evidence: pnpm typecheck ✓ · pnpm lint ✓ · pnpm kit-guard ✓ (4 panes clean) · pnpm no-stub ✓ · pnpm build ✓ · node --test dist/test/**/*.js439 pass / 0 fail.

Deviations

  • Extracted a shared helper (listen.ts) rather than inlining per command — the ticket explicitly allows "reuse or extract the smallest shared listen helper," and this is the smallest surface that lets launch/#232 delegate with zero behavior change while the three serve commands share one proven pattern.
  • Injectable listen hook added to each serve command (defaulting to the real helper) solely so the invalid-bind path is exercised deterministically without OS hostname resolution — matching the amendment's requirement. broker/room gain a hooks param (default {}); the CLI dispatcher passes none, so runtime behavior is unchanged.
  • No UI surface → no Design Fidelity table.

…d errors

From the #238 audit: broker serve, platform serve, and room serve awaited
`server.listen(port, host, resolve)` with no 'error' listener, so an
occupied or invalid bind either hung the listen promise or crashed the
process with an uncaught Server error.

- Extract the smallest shared listen helper (`src/cli/commands/listen.ts`):
  `listenOrError` binds while guarding the one-shot 'error' event —
  resolving a failure outcome instead of hanging/throwing, and removing
  the guard on success so post-bind behavior is unchanged. It only
  observes its own listen attempt (never inspects/kills/mutates a foreign
  listener). `listenErrorMessage` renders a controlled, token-free message
  from the bind coordinates + OS error code only.
- broker/platform/room serve now use it and, on a bind failure, write the
  token-free message to stderr and return non-zero (no hang, no uncaught
  error). room serve returns before mutating current-room state or warning.
- launch (#232) reuses the same helper: its `listen()` delegates to
  `listenOrError(..., "127.0.0.1")` mapped to the prior boolean — mechanical
  reuse, behavior unchanged.

Loopback-default/`--allow-remote` policy, SIGINT/SIGTERM lifecycle, ports,
and remote-exposure policy are unchanged; no new dependencies; #241-#243
untouched.

Tests (`test/cli-listen-errors.test.ts`): per surface (broker, platform
serve, room serve) an occupied-port case (real EADDRINUSE) and a
deterministic injected invalid-bind case (no OS hostname resolution) each
assert a controlled non-zero, token-free error and that the foreign
listener is left untouched; plus `listenOrError`/`listenErrorMessage` unit
coverage. Existing #232 launcher and serve tests stay green. Full suite
439/439.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: BLOCK

Epic Alignment: PASS

The diff stays within #240: controlled bind failures for broker/platform/room plus mechanical #232 helper reuse; #241-#243 are untouched.

Checked (evidence)

  • Structural gate: live REST PR body contains filled EPIC Alignment, Self-Verification, and Deviations sections.
  • Bind handling: src/cli/commands/listen.ts:13-27 resolves own-listen errors; broker/index.ts:34-40, platform/index.ts:47-53, and room/index.ts:932-938 return controlled non-zero errors.
  • Launcher: launch/index.ts:184-187 maps the shared outcome to the existing boolean behavior.
  • Tests: test/cli-listen-errors.test.ts covers occupied-port and injected invalid-bind paths for all three surfaces, token-free stderr, and untouched foreign listeners.
  • Riskiest part: helper extraction; it removes its one-shot error listener on success and preserves launcher failure-as-false semantics.
  • Kill-list: scanned all items — clean.
  • CI: gh pr checks 245 → Release gates pending (live; checked twice).

Findings

  • [blocking] Required CI has not completed.
    • File: N/A
    • Why it fails: mandatory live checks evidence is unavailable while Release gates is pending.
    • Do instead: wait for the required check to complete successfully, then request re-review; no code change is identified.

Decision

The reviewed diff aligns with #240, but approval is blocked until live CI completes successfully.

@realproject7

Copy link
Copy Markdown
Owner Author

RE2 — APPROVE (code/tests) — authoritative verdict in project chat. One CI caveat below.

The #240 contract + Batch-38 amendments are fully and correctly met:

CI caveat (merge gate, not the code): Release gates failed 1/439 — the sole failure is device-local archive/delete for joined rooms (#210), a browser-platform test outside this PR's surface (this diff touches only cli/commands/{broker,launch,listen,platform,room} + the new test). A CLI listen-error change cannot affect a browser archive/delete assertion; it's the same #210 test that flaked during #244. Please re-trigger CI — a re-run, not a code fix. Merge gate needs green CI.

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Epic Alignment: PASS

The diff stays within #240’s controlled bind-error scope; loopback/allow-remote policy, signal lifecycle, and #232 behavior are preserved.

Checked (evidence)

  • Re-review range: no code delta; head remains 3c2c213 from the initial review.
  • Bind handling: src/cli/commands/listen.ts:13-43 converts own-listen errors into bounded outcomes; broker/index.ts:34-40, platform/index.ts:47-53, and room/index.ts:932-938 emit controlled non-zero errors.
  • Launcher: launch/index.ts:184-187 maps the helper outcome to the prior boolean semantics.
  • Tests: test/cli-listen-errors.test.ts covers real occupied ports and deterministic injected invalid-bind errors for broker/platform/room, token-free stderr, and untouched foreign listeners.
  • Riskiest part: shared helper extraction; its one-shot listener is removed on both paths and preserves launcher failure-as-false behavior.
  • Kill-list: scanned all items — clean.
  • CI: gh pr checks 245 → Release gates pass (live, run 29386362460/job 87261224684).

Findings

  • None.

Decision

APPROVE. The #240 contract is implemented with deterministic per-surface coverage and verified green live CI.

@realproject7
realproject7 merged commit 99a0d22 into main Jul 15, 2026
1 of 3 checks passed
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.

[P2][CLI] Handle broker, platform, and room bind failures as command errors

2 participants