You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The daemon's gateway binds an ephemeral port by default (DEFAULT_LISTEN = '127.0.0.1:0', ai-gateway/src/config.js), so the port changes on every daemon restart. That choice is fine. The defect is that the attach lifecycle was never built to track a moving endpoint: it assumes "attach once, done forever". Two gaps, one root cause.
Gap 1: standalone hyp attach <client> cannot resolve the daemon's live port
runClientLifecycle in src/core/commands/clients.js resolves the endpoint as:
live in-process gateway.localEndpoint(), which always throws in a CLI boot (the gateway source only starts inside the daemon), then
configuredGatewayEndpoint(ctx.config), which returns undefined on a default install because endpointFromListen rejects port 0 (src/core/config/gateway_endpoint.js), then
a disk probe that either reports "already attached; the daemon manages attach for this install, nothing to do" or errors with "cannot resolve the gateway endpoint".
It never reads ~/.hyp/hypaware/run/status.json, even though the daemon's actual bound port is persisted there: the gateway source's status() returns details: { host, port, ... } (ai-gateway/src/source.js), captured by startConfiguredSources into the snapshots writeStatusFile writes. The discovery mechanism exists on disk; attach just does not use it. So on a default (ephemeral-port, daemon-managed) install, hyp attach has no path to success at all.
Pre-#271 builds (e.g. the published 1.11.0) additionally leak the raw internal error:
$ hyp attach claude
error: attach client 'claude' failed: ai-gateway: localEndpoint() called before the gateway started
PR #271 replaced that with the probe + guided message, but it fixed the diagnostics, not the capability.
Gap 2: nothing re-attaches when the daemon rebinds
A reconcile pass does run on every boot of a joined host (runtime.js, reason boot-already-confirmed), and the client seam resolves a fresh proven-bound localEndpoint() each boot. But the pass is level-triggered on marker existence: action_reconciler.js short-circuits any desired action whose marker is done, and the attach marker is keyed by client name only. Its detail records settings_path / prev_value but not the endpoint it attached at (action_attach.js), so "attached, but at a stale endpoint" is a state the reconciler cannot even represent.
Result: every daemon restart binds a new port, env.ANTHROPIC_BASE_URL in the client settings keeps pointing at the old one (claude/src/settings.js), no error is surfaced anywhere, and capture silently stops.
Compounding it, the drift is detectable today and nothing detects it:
the settings marker records the port it attached at, and probeClientAttachFromDescriptor reads it back (src/core/daemon/status.js), but hyp status only warns on client_attach_missing when the marker is absent, never on port mismatch;
LLP 0045's rule is sound: auto-attach must never record a URL for a port nothing bound, and the client seam is deliberately resolved once per boot (resolveClientActionSeam, runtime.js). The design closed the "write an unproven URL" failure mode but never closed the complement: rewriting a proven URL when the proven value changes.
Proposed fix shape
Endpoint-aware attach markers: record the endpoint in the done marker and treat an endpoint mismatch as a forward gap, so the boot-time reconcile pass re-performs attach after a rebind instead of short-circuiting on done.
Manual attach reads the live port: teach hyp attach to fall back to status.json's sources[].details.port (guarded by a daemon-liveness check via the pid file) before giving up, and make the "already attached" branch validate the recorded port against the live one instead of trusting marker existence.
Optionally, surface port drift in hyp status as a diagnostic (client_attach_stale), since all the data for the comparison is already on disk.
Related: #126 (config-driven auto-attach on join), PR #271 (message-level fix for gap 1's error leak). Design context: LLP 0044 / LLP 0045.
Summary
The daemon's gateway binds an ephemeral port by default (
DEFAULT_LISTEN = '127.0.0.1:0',ai-gateway/src/config.js), so the port changes on every daemon restart. That choice is fine. The defect is that the attach lifecycle was never built to track a moving endpoint: it assumes "attach once, done forever". Two gaps, one root cause.Gap 1: standalone
hyp attach <client>cannot resolve the daemon's live portrunClientLifecycleinsrc/core/commands/clients.jsresolves the endpoint as:gateway.localEndpoint(), which always throws in a CLI boot (the gateway source only starts inside the daemon), thenconfiguredGatewayEndpoint(ctx.config), which returnsundefinedon a default install becauseendpointFromListenrejects port 0 (src/core/config/gateway_endpoint.js), thenIt never reads
~/.hyp/hypaware/run/status.json, even though the daemon's actual bound port is persisted there: the gateway source'sstatus()returnsdetails: { host, port, ... }(ai-gateway/src/source.js), captured bystartConfiguredSourcesinto the snapshotswriteStatusFilewrites. The discovery mechanism exists on disk; attach just does not use it. So on a default (ephemeral-port, daemon-managed) install,hyp attachhas no path to success at all.Pre-#271 builds (e.g. the published 1.11.0) additionally leak the raw internal error:
PR #271 replaced that with the probe + guided message, but it fixed the diagnostics, not the capability.
Gap 2: nothing re-attaches when the daemon rebinds
A reconcile pass does run on every boot of a joined host (
runtime.js, reasonboot-already-confirmed), and the client seam resolves a fresh proven-boundlocalEndpoint()each boot. But the pass is level-triggered on marker existence:action_reconciler.jsshort-circuits any desired action whose marker isdone, and the attach marker is keyed by client name only. Its detail recordssettings_path/prev_valuebut not the endpoint it attached at (action_attach.js), so "attached, but at a stale endpoint" is a state the reconciler cannot even represent.Result: every daemon restart binds a new port,
env.ANTHROPIC_BASE_URLin the client settings keeps pointing at the old one (claude/src/settings.js), no error is surfaced anywhere, and capture silently stops.Compounding it, the drift is detectable today and nothing detects it:
probeClientAttachFromDescriptorreads it back (src/core/daemon/status.js), buthyp statusonly warns onclient_attach_missingwhen the marker is absent, never on port mismatch;hyp attachsees the stale marker and prints "already attached ... nothing to do", so the one command a user reaches for to repair a stale attach reports everything is fine.Why this was designed in
LLP 0045's rule is sound: auto-attach must never record a URL for a port nothing bound, and the client seam is deliberately resolved once per boot (
resolveClientActionSeam,runtime.js). The design closed the "write an unproven URL" failure mode but never closed the complement: rewriting a proven URL when the proven value changes.Proposed fix shape
donemarker and treat an endpoint mismatch as a forward gap, so the boot-time reconcile pass re-performs attach after a rebind instead of short-circuiting ondone.hyp attachto fall back tostatus.json'ssources[].details.port(guarded by a daemon-liveness check via the pid file) before giving up, and make the "already attached" branch validate the recorded port against the live one instead of trusting marker existence.hyp statusas a diagnostic (client_attach_stale), since all the data for the comparison is already on disk.Related: #126 (config-driven auto-attach on join), PR #271 (message-level fix for gap 1's error leak). Design context: LLP 0044 / LLP 0045.