Join wait converges on the applied org config, not client attaches - #770
Conversation
The wizard's "Applying your org's configuration" wait polled the daemon's client attach markers, so an org config that attaches no clients on this machine (and the no-org-config 404 state) always burned the full 60s budget. Poll the applied config slot instead: that is the on-disk fact the locked-row computation reads, and it lands within one pull-and-apply cycle. LLP 0223 records the decision.
…d-ref LLP 0135
Review fixes on top of the applied-config convergence change.
- `waitForCentralConverge`'s budget fallback quotes `ATTACH_WAIT_DEFAULT_MS`
instead of repeating 30000. The old delegation inherited the number, so it
could not drift; a literal can.
- The swallowed probe error now logs once per wait. A durable EACCES on
`config-control/` was indistinguishable from the no-org-config steady state
at every level: same `{ ok: false }`, same "didn't hear back" narration, no
signal anywhere. Once per wait, not per poll, so a stuck fs does not log for
the whole 60s budget.
- LLP 0135 (Active) still prescribed reusing the login lane's reconcile-wait
"rather than adding a second poll loop", which is exactly what this branch
adds. LLP 0129 got its forward-ref; 0135 now gets the matching inline
`Extended-by: LLP 0223` note.
- Cover the never-joined host (no `config-control/` at all) in the default
probe test: a clean not-converged, not a swallowed probe error.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Verdict: findings (3 minor, all fixed on the branch). The core change is correct and well grounded; nothing blocking. Reviewed at What I checked and what holdsThe central claim of this PR is that the wait should converge on the fact its own consumer reads. I verified that end to end and it holds:
Findings1. Minor - LLP 0135 (Status: Active) was contradicted without a forward-ref. 2. Minor - the default budget was a literal that can now drift. 3. Minor - a stuck probe was invisible at every level (log-driven development). Also added: the default-probe test now covers a host with no Not findings, but worth recording
Verification
Nothing here needs a human decision. Nice change: grounding the wait in the fact the next line of code actually reads is the right instinct, and the reset-on-enroll interaction that makes it safe is the non-obvious part that made it work. |
…owing it
The probe-error warn added in review round 1 was dead code in production.
`hasAppliedCentralConfig` went through `readActiveSlot`, which catches every
`readlinkSync` error and answers `null`, so a `config-control/` the CLI cannot
read (EACCES, EIO, an `active` that is not a symlink) came back as a plain
`false`. The wait then polled it silently to the timeout and narrated the
no-org-config steady state, which is exactly the confusion the warn was meant
to remove. Verified before the fix: with `config-control/` at mode 000 the
probe returned `false` and never threw.
`hasAppliedCentralConfig` now re-reads the pointer on the not-converged answer
only and rethrows anything that is not ENOENT/ENOTDIR, the same
"no directory is the answer, an unreadable one is not" discrimination
`centralLayerResolutionFailure` already makes a few lines below. The happy
path is still a single readlink, the wait's verdict is unchanged in every
case (`ok: false`, unlocked picker, never an error), and the branch it feeds
is now reachable.
Covered by a test that walks all three states: an empty control directory is
the silent steady state, an `active` that is not a symlink throws, and the
wait still returns `{ ok: false }` over it.
Co-Authored-By: Claude <noreply@anthropic.com>
|
Verdict: findings (1 minor, fixed). Nothing blocking. The change itself is correct; the one finding is that round 1's own fix did not actually do what it claimed. Reviewed at Round 1's three fixes, re-checkedFix 2 (quote Fix 1 (LLP 0135 forward-ref) is correctly placed. Fix 3 (the latched probe warn) is where the finding is. The latch itself is right: Finding1. Minor (observability) - the probe-error branch was unreachable in production, so the diagnostic round 1 added never fired for the error class it was written for.
That is exactly the scenario the round-1 fix names. A Verified before the fix, against Fixed in Verified after the fix: same harness now reports Test added: Nothing left unfixed. No blockers, no preferences parked for triage. Re-examined this round, and holding
Worth recording, no action asked for
Verification
Good change, and the underlying instinct is the right one: ground the wait in the fact its own next line reads. The round-1 fixes were the right calls too; fix 3 just needed its probe to cooperate. |
|
Triage verdict: safe to ship at The review fix-loop hit its 2-round cap because round 2's own fix moved the head, not because anything is outstanding. I re-verified every finding from both rounds against the committed tree at
I also re-confirmed the LLP wiring: LLP 0223 exists as an Accepted Decision, LLP 0129's The two round-2 notes are not deferrals from this PR, so no follow-up issue was opened: the apply-then-reset race predates the PR (the attach markers the wait used to poll had the identical window, and the #139 reset already targets it), and the no-org-config 404 steady state burning the full budget is unchanged behavior that LLP 0223 explicitly scopes out. All 9 CI checks are green at this head and the PR is mergeable. Ready for the maintainer. |
This branch merged master mid-flight and inherited two documents both claiming LLP 0223: the converge-on-applied-config decision (PR #770, landed first) and the prune-direct-children-and-unreadable-assets decision (PR #749, landed second). CI's duplicate-numbers check and test/core/llp-ref-hygiene.test.js both fail on the collision. Per LLP 0156#renumber, the later claimant moves. 0226 is already spoken for by fix/issue-774, a sibling branch fixing the same collision on master directly, so this renumbers to 0227, the next free number above the highest claimed across origin/master and every remote branch. Mechanical rename only: no content, status, date, or reasoning changed. The inbound sweep retargets the Extended-by header and four body links in LLP 0219, two @ref [implements] annotations in src/core/runtime/client_assets.js, one in src/core/runtime/client_asset_ledger.js, and five @ref [tests] annotations in test/core/client-assets-prune.test.js. References to LLP 0223 that mean the converge decision (src/core/config/apply.js, src/core/cli/wizard/join.js, src/core/cli/remote_commands.js, test/core/remote-login-command.test.js, llp/0129, llp/0135) are untouched. Co-Authored-By: Claude <noreply@anthropic.com>
`master` carried two documents claiming LLP 0223. Per LLP 0156#renumber the later claimant moves: the converge decision reached master first (348b0ae, PR #770, 2026-08-13T23:16), the prune decision second (f581e4c, PR #749, 2026-08-14T10:36). The prune decision moves to 0226, the next free number above the highest claimed on origin/master and every remote branch (0225 is the highest; 0221 is unclaimed but numbers are minted as max + 1). Mechanical rename only, which CLAUDE.md and LLP 0156 allow on an Accepted document: no content, status, date, or reasoning changed. The inbound sweep retargets 13 references in 4 files - the `Extended-by:` header and four body links in LLP 0219, five `@ref ... [tests]` annotations in test/core/client-assets-prune.test.js, two `@ref ... [implements]` in src/core/runtime/client_assets.js, and one in src/core/runtime/client_asset_ledger.js. The eight remaining `LLP 0223` references all mean the converge decision, which keeps the number. Before: `git ls-tree -r origin/master --name-only llp/ | ... | uniq -d` prints 0223, and `no LLP number is claimed by two documents` fails. After: the duplicate check prints nothing and llp-ref-hygiene.test.js is 11/11 green, including `every @ref resolves to a live LLP document and one of its anchors`. Co-authored-by: test <test@test.com> Co-authored-by: Claude <noreply@anthropic.com>
…773) * Wizard prompts take their printed default at EOF instead of hanging (#772) `rl.question()` leaves its promise permanently unsettled when the input stream ends without a line, so the three legacy readline prompts in `src/core/cli/walkthrough.js` hung forever on a spent stdin: the overwrite confirm (`hyp init < /dev/null` never returns), the defaults gate, and the backfill consent. The same file already solves this for the numbered picker with `queuedLineAsker`, which resolves a pending ask as `null` on `close` and seeds `closed` from `readableEnded` so an interface built over an already-ended stream does not wait on an `end` it will never see. All three prompts now read through that helper and coalesce `null` to the empty line, so EOF takes exactly the default the question printed (`[y/N]` -> no, `select [2]` -> option 2, `[Y/n]` -> yes) and the branch cannot drift from the advertised default. Output is byte-identical: `queuedLineAsker` writes the prompt itself, the way `rl.question` did. test/core/walkthrough-prompt-eof.test.js races each prompt against a timer, because the pre-fix failure is a hang rather than a wrong value. 4 of its 7 cases fail on master and all 7 pass here. Co-Authored-By: Claude <noreply@anthropic.com> * Renumber the colliding prune decision from 0223 to 0227 This branch merged master mid-flight and inherited two documents both claiming LLP 0223: the converge-on-applied-config decision (PR #770, landed first) and the prune-direct-children-and-unreadable-assets decision (PR #749, landed second). CI's duplicate-numbers check and test/core/llp-ref-hygiene.test.js both fail on the collision. Per LLP 0156#renumber, the later claimant moves. 0226 is already spoken for by fix/issue-774, a sibling branch fixing the same collision on master directly, so this renumbers to 0227, the next free number above the highest claimed across origin/master and every remote branch. Mechanical rename only: no content, status, date, or reasoning changed. The inbound sweep retargets the Extended-by header and four body links in LLP 0219, two @ref [implements] annotations in src/core/runtime/client_assets.js, one in src/core/runtime/client_asset_ledger.js, and five @ref [tests] annotations in test/core/client-assets-prune.test.js. References to LLP 0223 that mean the converge decision (src/core/config/apply.js, src/core/cli/wizard/join.js, src/core/cli/remote_commands.js, test/core/remote-login-command.test.js, llp/0129, llp/0135) are untouched. Co-Authored-By: Claude <noreply@anthropic.com> * Revert "Renumber the colliding prune decision from 0223 to 0227" This reverts commit 5ce4283. * Review: the EOF examples name a run these prompts never reach `hyp init < /dev/null` was cited in both the overwrite confirm's JSDoc and the test header as the run this change unhangs. It is not: the wizard's first screen is `runWizardFork`, whose `legacyMenuPrompt` (src/core/cli/wizard/fork.js) still reads through `rl.question` and so still hangs a fully unanswered `hyp init` one screen before any of the three prompts fixed here. What these three do fix is real and reachable: `hyp clients enable` reaches the backfill consent directly through `maybeBackfillAfterEnable` with no TTY gate, and a partially scripted wizard run (fork answered, stdin then dry) reaches the express gate, the defaults gate and the commit-point confirm. The examples now name those instead, and the test header records the fork prompt as the remaining member of the class. Comment-only: no behaviour, no output bytes, all 7 cases still pass. Co-Authored-By: Claude <noreply@anthropic.com> * Review round 2: the replacement EOF example names a run that is also unreachable Round 1 replaced `hyp init < /dev/null` with `hyp clients enable < /dev/null` in the test header. That run is wrong twice over: there is no `clients` command (`hyp attach <client>` is the one that enables), and the backfill consent is not reachable on a piped stdin at all - `maybeBackfillAfterEnable` runs only when `activatedViaPrompt` is set, and both sites that set it go through `maybeInteractiveEnableAttach`, which returns early on `!isTty(ctx.stdin)`. Verified: `hyp attach claude < /dev/null` exits 1 on the not_enabled refusal without asking anything. The header now names the run actually probed on this branch - answer the fork, let stdin dry, and the express gate, the commit-point confirm and the backfill consent all settle on their printed defaults over the spent stream - and states the narrower terminal-drop shape that reaches the attach caller. LLP 0190 #sync-gate said these three prompts "still call `rl.question` directly and still hang at EOF", which the code this PR annotates with that very anchor contradicts. 0190 is Draft, so the sentence is corrected in place and now records the fork screen as the one prompt left outside the file. Comment and doc only: no behaviour, no output bytes. npm test 4091/0 fail, npm run typecheck clean. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: test <test@example.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: test <test@test.com>
The wizard's "Applying your org's configuration..." phase polled the daemon's client attach markers, so an org config that attaches no clients on this machine (and the no-org-config 404 steady state) always burned the full 60-second budget - on top of the login lane's own 30-second wait over the same markers.
The wait now polls for the applied config slot under
config-control/instead. That is the on-disk fact the locked-row computation reads right after the wait, and it lands within one pull-and-apply cycle, so the common cases converge in seconds. The join seed deliberately does not count as convergence, and the 404 steady state still runs to the timeout (nothing lands on disk to observe) and falls through to the unlocked picker as before.LLP 0223 records the decision and extends LLP 0129.
npm testgreen (4030 pass),npm run typecheckcleancore_boot_noopandclient_attach_idempotentsmokes greenwalkthrough_picker_to_first_querysmoke fails, but identically on unmodified master (pre-existing)