Uh oh!
There was an error while loading. Please reload this page.
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>
philcunliffe
commented
Jul 7, 2026
🔍 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 |
philcunliffe
commented
Jul 7, 2026
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 |
philcunliffe
commented
Jul 7, 2026
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. |
philcunliffe
commented
Jul 7, 2026
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. |
Uh oh!
There was an error while loading. Please reload this page.
…655) (#656) * Two long-stale attach smokes assert the behaviour that replaced them (#655) `claude_attach_detach` and `client_attach_on_join` have both failed on master since long before v1.19.0. Neither is a code defect: each asserts a contract that a later, documented decision deliberately replaced, and neither smoke was updated with it. Both are in neither the release battery nor CI, so nothing caught the drift. claude_attach_detach broke at 41ea01c (LLP 0106, #316). Attach used to install exactly one managed hook group per event; LLP 0106 added `classify-cwd` beside `session-context` on the two events where a fresh working directory appears (SessionStart, CwdChanged), so `hooks.SessionStart` now carries two groups and the smoke's `v.length === 1` fails. The unit test for the marker's managed hook list was updated at the time; the on-disk golden compare in the smoke was not. Assert both sides of the split instead: SessionStart and CwdChanged carry the pair, UserPromptSubmit and PostToolUse carry `session-context` alone, so dropping either kind or leaking `classify-cwd` onto the per-prompt and per-tool events fails here. client_attach_on_join broke at 1ce40da (LLP 0086, #277/#278). Its step 3 asserted the pre-0086 model, attach once and done forever: a relaunch on an unchanged revision must not re-attach. LLP 0086 D1 made a `done` marker at a moved endpoint a forward gap precisely so an ephemeral gateway port does not strand `ANTHROPIC_BASE_URL` on a port nothing bound, and rev-1 binds `127.0.0.1:0`, so the relaunch drifts by construction and the marker is refreshed. Cover both branches of that check rather than one: the relaunch on rev-1 now asserts the re-attach and that the base URL follows the new port, then rev-1b pins the gateway's `listen` so the endpoint stops moving and a further relaunch proves the short-circuit the step originally meant to prove. Co-Authored-By: Claude <noreply@anthropic.com> * Review fixes: race-free port pin, honest LLP 0106 gloss (#655) client_attach_on_join reserved rev-1b's pinned port by binding port 0 and releasing it, then let the daemon bind it seconds later. Anything on the host could win that window, including the smoke's own stub central server, which made this the only flow in the suite that could collide with a co-resident process. Read the port back from the drifted daemon's own status.json instead (resolveLiveGatewayEndpointFromStatus, LLP 0086 D2) and pin rev-1b to that. The daemon then reclaims a port it already holds across the staged restart, so no reservation window opens at all. Pinning a port the daemon already bound means the relaunch short-circuits rather than re-attaching, so the separate pinned_attach boot is folded into the no_reattach one. The drift step still proves the re-attach branch and still asserts env.ANTHROPIC_BASE_URL === drifted.endpoint, and the short-circuit comparison still runs against a genuinely fresh marker timestamp: the drift step asserted it moved off the first attach's. The guard boot now also proves the gateway really reclaimed the pinned port, matched against this boot's status.json rather than the outgoing daemon's leftover snapshot (a pinned port makes both report the same endpoint). claude_attach_detach's LLP 0106 gloss claimed the doc scopes the classification hook to the fresh-cwd events. It does not: 0106 settles only that the hook is installed alongside session-context, and the event scoping is decided by MANAGED_HOOK_SPECS in the claude plugin's settings.js. Narrow the gloss to what 0106 says and point the scoping at the code that owns it. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: test <test@test.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: test <test@example.com>
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