Skip to content

fix: make hyp init migrate joined upgrades into proxy mode - #846

Open
philcunliffe wants to merge 1 commit into
masterfrom
fix/issue-842
Open

fix: make hyp init migrate joined upgrades into proxy mode#846
philcunliffe wants to merge 1 commit into
masterfrom
fix/issue-842

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

What was wrong

On a fleet-enrolled machine upgrading from a base-URL version to a proxy-capable
one, the attended hyp init team pathway reported success while leaving Claude
attached with _hypaware.mode: "base_url". Two independent gaps stacked:

  1. The attach probe could not tell the two attaches apart.
    probeClientAttachFromDescriptor returned the marker's version and port
    but not its mode, so runWizardFinale's attached: true filter read a
    stale base-URL attach as work enrollment had already done and put the client
    into skipAttachClients.

  2. Proxy CA readiness came after the last chance to attach.
    runPickerFinale gated its CA wait on !skipInstall. With the install
    skipped, the one restart that puts proxy mode (and therefore the CA) on the
    wire sat at the very end of the lane, after the attach loop. Even an
    unattached client would have entered that loop with no CA on disk and been
    attached by base URL, exactly as the issue describes.

What changed

  • probeClientAttachFromDescriptor reports the marker's mode, and
    hyp status carries it on the client row (ClientAttachReport.mode). This
    is also the state LLP 0244's consequences list wanted status to be able to
    name.
  • The joined finale's skip is mode-aware: a client whose picker row declares
    gateway_proxy_mode and whose marker records anything but proxy, on an
    install whose governing gateway runs proxy mode, is no longer skipped.
  • runPickerFinale moves its single daemon restart ahead of attach (instead of
    adding a second one) when the install was skipped, a restart is coming, and
    governingGatewayProxyMode says the gateway will run in proxy mode. The tail
    restart then stands down. Base-URL installs and --no-daemon keep today's
    ordering byte for byte.
  • governingGatewayProxyMode is the central-layer-aware predicate extracted out
    of waitForProxyCaBeforeAttach, so the wizard and the finale answer "will this
    gateway run proxy mode" the same way (LLP 0244 #central-managed).
  • restartDaemonFn joins installDaemonFn as an injectable seam, for the same
    LLP 0181 reason: the real service-manager call refuses to spawn launchd or
    systemd under the test runner, so the ordering would otherwise be untestable.

No new LLP

This closes a gap between the code and LLP 0243/0244 rather than changing what
they settled. LLP 0243 #composed-default already requires a proxy-mode install
to attach in proxy mode rather than lose the race to the CA mint; LLP 0244's
consent gate covers writing proxy_mode into a config that lacks it, which is
not what this path does (the key is already governing, from the central layer or
from the fold the user's own picks drove). The CA trust grant keeps its own
dialog (LLP 0237) untouched.

Regression test

test/core/cli/wizard/joined-upgrade-proxy-attach.test.js

On master four of its five cases fail; with this change all five pass, and the
rest of the suite is unchanged (the 22 remaining failures in this sandbox are
pre-existing parquet/cache/report-render failures from optional deps that are
not installed here, identical before and after).

  • a joined upgrade does not skip attach for a client still marked base_url -
    drives runInitWizard down the team pathway against a home whose
    ~/.claude/settings.json carries a mode: "base_url" marker and asserts the
    finale is not handed claude in skipAttachClients.
  • a joined upgrade does not skip attach for a marker that records no mode at all
  • a joined run still skips attach for a client already attached by proxy -
    pins that the skip still does its original job.
  • a skipped install restarts the daemon before attach so the proxy CA exists -
    asserts the event order is restart, ca-wait, attach, exactly one restart.
  • a skipped install with no proxy mode leaves the restart at the end

npm test and npm run typecheck were run locally; typecheck reports no errors
in any changed file.

Fixes #842

The joined finale read any `_hypaware` marker as "attach already done" and
left its one daemon restart at the end of the lane. On an enrolled machine
upgrading from a base-URL version, that meant: skip the install, skip the
attach, restart last, mint the proxy CA after the only chance to attach had
passed - and report success with Claude still on `base_url`.

Two gaps, both between the code and LLP 0243/0244 rather than in what they
settled:

- The attach probe returned the marker's version and port but not its
  `mode`, so `base_url` and `proxy` were indistinguishable to every caller.
  It now reports the mode, and `hyp status` carries it on the client row.
- The finale's CA wait was gated on having installed a daemon. When the
  install is skipped but a restart is still coming and the governing gateway
  runs in proxy mode, the restart now moves ahead of attach instead of a
  second one being added, so the CA exists when adapters preflight on it.

The wizard's joined skip is now mode-aware: a client whose picker row
attaches through the proxy and whose marker records anything else is not
skipped.

Fixes #842

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 18, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral was resolving this branch's conflict against newly-merged master (#818, otel-attach-0245, and #785). The text merge is easy and goes green. The reason I am stopping is that #818 invalidated the premise this PR is built on, and picking the replacement premise is a design call, not a merge call.

The textual conflict (resolvable, not the blocker)

  • src/core/daemon/types.d.ts - 1 hunk. Both sides add ClientAttachReport.mode. Master's is a strict superset (base_url / proxy / otel, plus attachedAt and telemetryPort).
  • src/core/daemon/status.js - 3 hunks, all the same story: probeClientAttachFromDescriptor reporting the marker's mode. Master added it too, alongside attached_at and markerTelemetryPort().
  • Plus one silent auto-merge artifact in test/core/daemon.test.js: this branch asserts mode: undefined on a mode-less marker, master spreads the key away entirely.

Item 1 of this PR's "What changed" list is already on master, done more completely. Taking master for all four spots and dropping the mode: undefined expectation gives a tree where npm test fails only the known inherited repo-scratch-hygiene case (x/npm-test.log, x/typecheck.log), and npm run typecheck is clean. All five cases of joined-upgrade-proxy-attach.test.js pass.

The real conflict is semantic, and it is in the files that did not conflict

src/core/cli/wizard/index.js and src/core/cli/walkthrough.js merged without a marker, and that is the trap. After #818 (LLP 0262), hyp attach claude is OTEL-only: hypaware-core/plugins-workspace/claude/src/index.js hardcodes mode: MODE_OTEL and the comment there is explicit that a machine carrying a proxy attach is migrated by that write, "never re-attached by proxy". claude is also the only client that declares compose.gateway_proxy_mode: true; codex attaches by TOML model_provider, and nothing else proxy-attaches at all.

So both halves of this PR now point at a mode nothing attaches in:

Half 1, the mode-aware skip. attachModeIsStale() hardcodes the desired mode as 'proxy'. Against merged master that inverts in the case that matters most:

  • A claude correctly attached in otel mode is judged stale and re-attached on every joined hyp init. I ran this against the merged tree: otel marker -> skipAttachClients does not contain claude. Harmless (attach is idempotent) but it retires the LLP 0134 login-lane skip for the only client it applies to.
  • A claude marked proxy is skipped - and post-OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) #818 that is exactly the machine that must be re-attached, because proxy capture is the thing being migrated away from. This PR's test a joined run still skips attach for a client already attached by proxy would pin that as intended behaviour.

Half 2, the restart-before-attach reordering. Its entire justification is getting the proxy CA minted before the attach loop, because "adapters pick their mode by whether the CA file exists". An otel attach needs no CA, no keychain, no launchd env (LLP 0262 R5). With claude off the proxy, there is no client left that the reordering serves - so governingGatewayProxyMode, the early restartFinaleDaemon, and the restartDaemonFn seam are machinery with no live subject. The two ordering tests would be asserting a lane no client walks.

What is not fixed by #818, and still needs this PR's idea

The bug in #842 is real and open on master. runWizardFinale there is still a bare (report?.clients ?? []).filter((c) => c.attached). An enrolled machine upgrading to an OTEL-capable version, with _hypaware.mode: "base_url" or "proxy" in ~/.claude/settings.json, still gets skipped by the joined finale and still ends hyp init unmigrated with the wizard reporting success. There is no hyp status diagnostic for a stale attach mode either (client_attach_stale only compares ports). So the shape of this PR's gap-1 fix is still wanted; only its target mode is wrong.

The call a human needs to make

  1. Retarget the predicate at the mode this install actually attaches in. The honest form of attachModeIsStale is "the marker's mode is not the mode this install attaches this client in" - but core cannot answer that today. The attach mode lives inside the plugin (MODE_OTEL in claude's index.js); the wizard only sees compose.gateway_proxy_mode, which after OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) #818 means "the gateway runs proxy mode", not "this client attaches by proxy". Cheapest honest fix is probably a client-descriptor field (client.attach_mode: "otel" in hypaware.plugin.json) that both the picker and hyp attach read. That is a new contract, so this PR's "No new LLP" section lapses and a decision doc is owed - and possibly a client_attach_mode_stale status diagnostic alongside it, since hyp init is not the only way to arrive here.
  2. Drop half 2 entirely and shrink this PR to the skip fix, since no client proxy-attaches any more. If you would rather keep governingGatewayProxyMode as an extraction on its own merits, it should land without the reordering it was extracted for.
  3. Or close fix: make hyp init migrate joined upgrades into proxy mode #846 and re-cut hyp init leaves joined upgrades in base-URL mode because attach skips precede proxy CA readiness #842 against the OTEL world, if you would rather not carry this branch's proxy-shaped tests and prose forward at all.
  4. Landing the merge as-is is possible and green, but it ships the two inverted behaviours above with tests pinning them. I do not recommend it and did not push it.

One adjacent thing you may want in the same call: maybeOfferProxyModeMigration in src/core/commands/clients.js still gates on compose.gateway_proxy_mode === true and still prints "this install attaches claude by base URL". That reads as #818 residue on the same seam.

Nothing has been pushed; the branch is untouched at 7c35350.

Reply with a comment on this PR (or push to the branch); neutral monitors this thread and will re-engage with your guidance on its next tick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hyp init leaves joined upgrades in base-URL mode because attach skips precede proxy CA readiness

1 participant