Skip to content

Attach lifecycle does not track the gateway's ephemeral port: manual attach cannot resolve it, and daemon rebinds silently strand ANTHROPIC_BASE_URL #277

Description

@bgmcmullen

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 port

runClientLifecycle in src/core/commands/clients.js resolves the endpoint as:

  1. live in-process gateway.localEndpoint(), which always throws in a CLI boot (the gateway source only starts inside the daemon), then
  2. configuredGatewayEndpoint(ctx.config), which returns undefined on a default install because endpointFromListen rejects port 0 (src/core/config/gateway_endpoint.js), then
  3. 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;
  • post-Manual attach without an endpoint: probe disk instead of leaking localEndpoint() error #271 hyp attach sees 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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions