Skip to content

Fix local-only picker still skipping on a genuinely fresh enroll (issue #281 follow-up)#286

Merged
philcunliffe merged 1 commit into
masterfrom
fix/picker-fresh-enroll-registry
Jul 8, 2026
Merged

Fix local-only picker still skipping on a genuinely fresh enroll (issue #281 follow-up)#286
philcunliffe merged 1 commit into
masterfrom
fix/picker-fresh-enroll-registry

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

After #283 the local-only picker runs at the right time on a fresh enroll, but it still silently skips there (prints only the durable hint). Diagnosed on a real fresh box (full ~/.hyp wipe + v1.13.0 enroll to prod).

Root cause chain:

  1. hyp remote login boots its kernel at dispatch time, before enrollment. On a fresh box the local config names no plugins; @hypaware/ai-gateway (which registers the ai_gateway_messages dataset) only arrives via the org config-control pull the daemon does after enrollCentralSink installs it. The login process's ctx.query boot snapshot never learns about it.
  2. listCapturedDirectories hits "SQL query references unknown dataset" -> best-effort null.
  3. waitForCapturedDirectories reads null as "enumeration cannot run, stop immediately" (no 30s poll).
  4. The picker takes enumeration_failed -> durable hint only.

The #283 tests stubbed waitForCaptured/listCandidates, so this was never exercised; a re-login on a populated box works (plugins load from the applied central layer at CLI boot), which is why manual verification passed.

Fix

freshenCaptureEnumeration (src/core/cli/remote_commands.js): on the fresh-enroll interactive fork, after waitForClientAttach returns a client, re-boot one fresh kernel (config profile, the same layered resolution any subsequent hyp command runs) and hand its registry's enumeration to the capture wait.

  • One re-boot is sufficient by construction: attach markers are written by the reconciler that runs only after a confirmed config apply, so attach means the pulled central layer is on disk - and the attach handler requires the gateway capability, so that layer enables the plugin. Per-poll re-resolution would re-pay plugin discovery every tick for nothing.
  • No cost when not needed: if the boot snapshot already has the dataset (re-login on a populated box), no second boot is paid.
  • Best-effort (LLP 0072): a failed re-boot or a merged config that still lacks the plugin falls back to today's behavior; the refinement never breaks the login it refines.
  • No leaked handles: the fresh kernel is disposed (sources.stopAll, mirroring dispatch's boot-source cleanup) after the picker, so the login process still exits.

local_only.js exports CAPTURE_DATASET so the registry check and the enumeration SQL cannot drift. LLP 0069/0080 carry forward-ref notes per the immutable-record convention.

Tests

  • Seam-level: the fresh enumeration is what the capture wait polls; the handle is disposed only after the picker used the candidates; a null refresh falls back to the snapshot and the login still succeeds.
  • Unit: freshenCaptureEnumeration snapshot no-op (no boot paid), success, dataset-still-missing (sources stopped, null), boot-throw (null).
  • The Fix local-only picker silently skipping on first-time enroll (issue #281) #283 lesson - one deliberately unstubbed test runs the REAL bootKernel against an on-disk central layer naming @hypaware/ai-gateway and asserts enumeration RUNS (empty list, pollable) instead of failing to null.

npm test: 2129 pass. npm run build:types: clean. core_boot_noop smoke: ok. (walkthrough_picker_to_first_query fails identically on clean origin/master - pre-existing, unrelated.)

Note: filing a follow-up GitHub issue for this residual was blocked in this autonomous session; linkage is preserved here and in the LLP forward-notes as "issue #281 follow-up".

🤖 Generated with Claude Code

#281 follow-up)

After the #283 reordering the picker ran at the right time but queried
through the wrong registry: hyp remote login boots its kernel at dispatch
time, before enrollment, and on a first-run box the local config names no
plugins. The login process's query-registry snapshot therefore never
registers ai_gateway_messages - that registration arrives with
@hypaware/ai-gateway, which the org config-control pull enables only after
enrollCentralSink installs the daemon. listCapturedDirectories then failed
with "unknown dataset" (best-effort null), waitForCapturedDirectories read
null as "enumeration cannot run, stop now" (no 30s poll), and the picker
took its enumeration_failed durable-hint path on exactly the fresh enroll
it exists for. The #283 tests stubbed waitForCaptured/listCandidates, so
the gap was never exercised; a re-login on a populated box worked, which is
why manual verification passed.

Fix: freshenCaptureEnumeration (src/core/cli/remote_commands.js). On the
fresh-enroll interactive fork, after waitForClientAttach returns a client,
re-boot one fresh kernel (config profile, the same layered resolution any
subsequent hyp command runs) and hand its registry's enumeration to the
capture wait. One re-boot is sufficient by construction: attach markers are
written by the reconciler that runs only after a confirmed config apply, so
attach means the pulled central layer is on disk - and the attach handler
requires the gateway capability, so that layer enables the plugin. The
snapshot-already-has-it case (re-login on a populated box) skips the boot
entirely; a failed re-boot or a still-missing dataset falls back to the
prior behavior. Best-effort throughout: the refinement never breaks the
login it refines (LLP 0072). The handle is disposed (sources.stopAll,
mirroring dispatch) after the picker so no boot-started source keeps the
login process alive.

local_only.js exports CAPTURE_DATASET so the registry check and the
enumeration SQL cannot drift. LLP 0069/0080 carry forward-ref notes per
the immutable-record convention.

Tests: seam-level coverage of the refresh wiring (fresh enumeration is
polled, disposed after the picker; null refresh falls back), unit coverage
of freshenCaptureEnumeration (snapshot no-op, success, still-missing,
boot-throw), and - the #283 lesson - one deliberately unstubbed test that
runs the REAL bootKernel against an on-disk central layer naming
@hypaware/ai-gateway and asserts enumeration RUNS (empty list, pollable)
instead of failing to null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@philcunliffe philcunliffe added the neutral:adopt Foreign PR adopted into neutral's reconcile scope label Jul 8, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral review — adopted PR (full-heal), head a479420

Verdict: clean. No blockers or majors; one theoretical minor noted below (no action required). Mergeable ∧ green (typecheck + test both SUCCESS) ∧ reviewed.

What it does: On a genuinely fresh enroll, hyp remote login boots its kernel before enrollment; the local config names no plugins, so the boot snapshot never registers ai_gateway_messages (it ships with @hypaware/ai-gateway, enabled only after the daemon's org config-control pull). Enumeration failed to null, waitForCapturedDirectories read that as "cannot run", and the picker silently skipped. The fix adds freshenCaptureEnumeration (src/core/cli/remote_commands.js:216): after a client attaches (guaranteeing the pulled central layer is on disk), it re-boots one fresh kernel with bootProfile: 'config', confirms the dataset is now registered, and hands that registry's enumeration to the capture-wait poll.

Correctness:

  • Root-cause fix, not a symptom mask — the re-boot resolves the same layered config any later hyp command would, registering the dataset.
  • Bounded/idempotent: freshen is called exactly once (not in the poll loop) — no boot loop. Guard at :218 short-circuits to null when the snapshot already knows the dataset, so a populated-box re-login pays no second boot.
  • No double-registration / port conflict: the ai-gateway source listener binds only on sources.start, not at activation, so the second in-process kernel does not re-bind the gateway port the daemon owns.
  • Resource lifecycle correct: dispose()runtime.sources.stopAll(); caller's finally disposes after the picker runs. Tests assert the fresh kernel stays alive while its enumeration closure is used, then is disposed.
  • All error/edge paths degrade to the durable-hint fallback (null), never an error; runRemoteLogin still returns 0.

Test quality: Genuinely reproduces the bug and is not vacuous. Five DI unit tests cover every branch; the real-bootKernel test stages the on-disk fresh-enroll moment (empty local config + a central seed naming @hypaware/ai-gateway), boots the real kernel, and asserts enumeration RUNS (returns []) rather than failing to null — directly exercising the registry gap #283's stubbed tests masked. 61/61 tests in the file pass.

@ref: New @ref LLP 0069#enumerate [constrained-by] resolves and is honest; LLP 0069/0080 "Revisited-by #281 follow-up" notes are accurate. No stale refs.

Minor advisory (no action): src/core/cli/remote_commands.js:253 outer catch — if bootKernel ever threw after starting a listener during activation, the catch returns null without a runtime handle to stopAll(), so a partially-started source could leak. Theoretical for this path (ai-gateway doesn't start at activation) and mirrors dispatch's existing non-cleanup of a throwing boot. Noted for completeness only.

@philcunliffe philcunliffe added the neutral:approved neutral reviewed this adopted PR and approves; held for maintainer merge (LLP 0025) label Jul 8, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral verdict: approved ✅ — mergeable ∧ green ∧ reviewed at head a479420. Held for you to merge (neutral does not ready/merge adopted PRs).

@philcunliffe philcunliffe merged commit 642d1de into master Jul 8, 2026
4 checks passed
@philcunliffe philcunliffe deleted the fix/picker-fresh-enroll-registry branch July 8, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:adopt Foreign PR adopted into neutral's reconcile scope neutral:approved neutral reviewed this adopted PR and approves; held for maintainer merge (LLP 0025)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant