Attach lifecycle: track the gateway's ephemeral port (endpoint-aware markers + manual attach reads status.json) - #278
Conversation
The daemon's gateway binds an ephemeral port by default, so the port changes on every restart, but attach was built as "attach once, done forever" and never modeled a moving endpoint. Two gaps, one root cause. Gap 1 - standalone `hyp attach <client>` could not resolve the daemon's live port on a default install (in-process localEndpoint() throws in a CLI boot; configuredGatewayEndpoint is undefined because endpointFromListen rejects port 0). It now falls back to the port the daemon persists to status.json (sources[].details.port), guarded by a daemon-liveness check via the pid file, before giving up. The "already attached" branch now validates the recorded port against the live one instead of trusting marker existence: a stale-port marker re-attaches; a dead-daemon snapshot is never used. Gap 2 - nothing re-attached when the daemon rebound. The attach marker is now endpoint-aware: perform() records the endpoint it attached at, and the generic reconciler consults a new optional ActionHandler.isCurrent() hook, so a done marker at a stale endpoint is a forward gap that re-attaches rather than a permanent short-circuit. The reconciler stays generic; the attach handler owns the endpoint comparison. Backward compatible: a pre-fix marker with no recorded endpoint is treated as stale (re-attach once) and self-heals. Preserves LLP 0045's invariant - auto-attach never records a URL for a port nothing bound (no live endpoint this pass leaves the existing attach untouched). Also adds a non-degrading `client_attach_stale` diagnostic to `hyp status` when a client's recorded port no longer matches the live gateway port. Decision recorded in LLP 0086 (Extended-by forward-refs on 0044/0045/0041). Reproduce tests: test/core/attach-live-port.test.js (Gap 1) and test/core/attach-endpoint-drift.test.js (Gap 2), both fail before / pass after. Full suite green (2098 pass), typecheck + build:types clean. Fixes #277 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔍 neutral review (Codex + 2 Claude lenses): request_changesRound 1, head Major: a failed re-attach can orphan a client attachWhen a stale-but-applied Must-fix (house style): em dashes and the
|
Neutral review round - PR #278 (attach tracks the gateway's ephemeral port)Head reviewed: Reviewers: Claude (5 parallel focus passes + maintainer synthesis). Codex was launched but failed on an environment error (its own local proxy dropped the connection: What the change doesExtends the LLP 0044/0045 "attach once, done forever" model to an endpoint-aware one (LLP 0086): the attach marker records the endpoint it attached at; the reconciler gains an optional Verified correct (no defect)
FindingsF1 - Minor (style, not fixed): new lines introduce U+2014 em dashes, violating the repo rule. F2 - Minor (behavior change, not a defect): TOML-probe clients (codex) lose the "already attached, nothing to do" fast path against a live daemon. DispositionSound, well-tested, faithful to LLP 0086 and preserves the LLP 0045 Part 1 proven-bound-endpoint invariant. No actionable (blocker/major) finding; no fix pushed; head SHA unchanged at |
neutral PR-health review, round 2Head reviewed: Codex: failed again on the same local-proxy environment error as round 1 ( Independent Claude review: full read of the diff plus two parallel review subagents (bug/logic scan and test/contract scan), each reading the actual files (producers and comparers), and the 31 affected tests run green. The core logic is sound and all endpoint/port comparisons are internally consistent:
FindingsF1 (style, non-actionable) - em dashes on new lines. 12 added source/test lines carry U+2014 em dashes ( F2 (behavior, non-actionable) - TOML-probe clients (codex) re-attach every manual F3 (test gap, non-blocking - NEW this round) - the Non-issue noted for completeness: auto re-attach self-heals a daemon restart-rebind (the boot reconcile pass observes the drift) but not an in-process SIGHUP/central-config reload-rebind (the client seam endpoint is frozen for the daemon lifetime and reload schedules no reconcile pass). This is pre-existing, not regressed by this PR, honestly scoped out in LLP 0086 (D1 / Consequences say "restart"), and is now at least surfaced by the new DispositionHold for the human merge gate. No blocker or major correctness / security / performance defect; F1 and F2 confirm round 1 and stay non-actionable; F3 is a genuine test-coverage gap on correct code, recommended as author follow-up rather than a merge blocker. |
Review round 3 (confirmation) — approveIndependent, focused re-read of head CodexAttempted once ( What I traced independently
Residual findings (all non-blocking; concur with rounds 1 and 2, would ship as-is)
Verdictclean — no blocker/major correctness/security/regression defect. Nothing fixed this round (no truly-actionable finding; F1/F2/F3 remain non-actionable). Concur with rounds 1 and 2: approve. |
Problem
The daemon's gateway binds an ephemeral port by default (
DEFAULT_LISTEN = '127.0.0.1:0'), so the port changes on every restart. The attach lifecycle (LLP 0044 / 0045) was built as attach once, done forever and never modeled a moving endpoint. Two gaps, one root cause.Gap 1 - manual
hyp attach <client>cannot resolve the live port. On a default installlocalEndpoint()throws (no in-process gateway in a CLI boot) andconfiguredGatewayEndpointis undefined (endpointFromListenrejects port 0), so attach never reached a success path - even though the daemon persists its bound port tostatus.json.Gap 2 - nothing re-attaches when the daemon rebinds. The boot reconcile pass is level-triggered on marker existence: a
doneattach marker short-circuits forever, and the marker recordedsettings_path/prev_valuebut not the endpoint, so "attached, but at a stale endpoint" was unrepresentable. Every restart bound a new port,env.ANTHROPIC_BASE_URLkept pointing at the old one, and capture silently stopped.Fix
action_attach.jsperform()records the endpoint it attached at; the generic reconciler gains one optional, provider-agnostic hookActionHandler.isCurrent(), so adonemarker at a stale endpoint is a forward gap that re-perform()s instead of short-circuiting. The reconciler stays generic; the attach handler owns themarker.endpoint === ctx.endpointcomparison. Backward compatible: a pre-fix marker (no recorded endpoint) is treated as stale and self-heals on the first pass. Preserves LLP 0045's invariant - no live endpoint this pass leaves the existing attach untouched (auto-attach never records a URL for a port nothing bound).hyp attachfalls back tostatus.json'ssources[].details.portviaresolveLiveGatewayEndpointFromStatus, guarded by a daemon-liveness check (pid file +processIsAlive) - a dead-daemon snapshot is never used. The "already attached" branch now validates the recorded port against the live one: a stale-port marker re-attaches; a matching one is a genuine no-op.hyp statusdrift diagnostic (optional, included). A non-degradingclient_attach_stalewarning when a client's recorded port no longer matches the live gateway port (both already on disk).Design
New decision LLP 0086 records the endpoint-aware-marker + re-attach-on-drift choice; additive
Extended-byforward-refs added to LLP 0044 / 0045 / 0041 (settled text untouched). Code annotated with@ref LLP 0086#....Verification
Reproduce tests (fail before, pass after):
test/core/attach-live-port.test.js- Gap 1 (live-port discovery, liveness gate, stale-port re-attach, port-match no-op)test/core/attach-endpoint-drift.test.js- Gap 2 (rebind re-attaches, legacy endpoint-less marker self-heals, unresolved endpoint leaves attach untouched)npm test2098 pass / 0 fail (1 pre-existing skip);npm run typecheckandnpm run build:typesclean. Existingaction-attachexpectations updated for the newly-recordedendpointdetail.Fixes #277