hyp status: a probe-less client is unattachable, not unattached - #553
hyp status: a probe-less client is unattachable, not unattached#553philcunliffe wants to merge 2 commits into
Conversation
`action_attach.desired()` skips a client descriptor with no `attach_probe`
(attach must be reversible, and only the probe can reverse it), so
`perform()` never runs and no attach marker is ever written for openclaw or
claude-desktop. Three `hyp status` surfaces derived against the attach
contract without that gate, and each turned that permanent silence into a
permanent negative on a joined host:
clients:
- openclaw [configured, not attached] [local]
client actions:
- attach openclaw [pending]
diagnostics:
[WARN ] client_attach_missing: '@hypaware/openclaw' is enabled but
openclaw settings show no HypAware marker - run 'hyp attach --client
openclaw'
None of the three can ever resolve, and the printed repair resolves the
adapter's deliberate LLP 0143 no-op, so running it clears nothing.
Gate all three on `descriptor.attachProbe`, the same rule `desired()` uses:
- the client-actions row derives `n/a` (via a new `inert` flag on the
declared-attach entry) rather than a permanent `pending`;
- `ClientAttachReport` gains a required `attachable`, and the clients row
prints `attach n/a` instead of `not attached`; `--json` carries
`attachable` beside the unchanged `attached` boolean;
- `client_attach_missing` no longer fires for a probe-less client.
Clients that do declare a probe are untouched: claude with no marker still
reports `pending`, `not attached`, and the warning.
LLP 0143 grows #status-derives-by-the-same-gate for the rule; LLP 0044's
status-surface vocabulary and LLP 0139 #repair-must-be-runnable are amended
to match (Desktop's attach-missing warning fired identically before consent,
after a decline, and after a successful install, so withdrawing it loses no
signal; `hyp claude-desktop verify` is the check that can answer it).
Co-Authored-By: Claude <noreply@anthropic.com>
The probe-less gate removed `client_attach_missing` for openclaw, but docs/ACCEPTANCE.md step 1 of the OpenClaw flow still told the release tester to expect that warning, so its (correct) absence would read as a regression. Restate the expectation as the pass condition. The `@ref LLP 0139#repair-must-be-runnable` gloss claimed the repair names an adapterless client's `configure_command`; after the gate the only adapterless client (claude-desktop) never reaches that code, and no shipped picker row takes the branch. Re-glossed to what the code does, with LLP 0139's amendment box recording the same. README's diagnostics table said `client_attach_missing` fires for any enabled client plugin with no marker; it is now probe-gated. Co-Authored-By: Claude <noreply@anthropic.com>
Neutral review round - PR #553 @
|
| File | Before | After |
|---|---|---|
docs/ACCEPTANCE.md:302 |
Expect `hyp status` to also carry a `client_attach_missing` warning for |
Expect `hyp status` to carry **no** `client_attach_missing` warning for |
src/core/daemon/status.js |
:626 gloss an adapterless client's attach-missing repair names its configure_command |
:629 gloss the repair is the client's own picker configure_command when it declares one |
README.md:381 |
a client plugin is enabled but its settings file shows no HypAware marker |
a client plugin with an attach probe is enabled but shows no HypAware marker |
llp/0139-...decision.md:220 |
(absent) | no plugin shipping today takes its first branch |
Each row was read back with git show <sha>:<path>, not inferred from a green suite (LLP 0002).
Neutral review round 2 - PR #553 @
|
| home | claude / codex (probed) |
openclaw / claude-desktop (probe-less) |
|---|---|---|
| A no settings files | attachable:true, [configured, not attached], attach [pending], client_attach_missing fired |
attachable:false, [configured, attach n/a], attach [n/a], no warning |
B settings file present, no marker ({"theme":"dark"}, [model_providers.other]) |
identical to A - all three negatives survive | same |
C probe errors (corrupt JSON; EISDIR on the TOML) |
attachable:true with error carried to text and --json; not attached, pending, client_attach_missing all still fired |
same |
| D marker present | attachable:true, attached:true, [configured, attached], no warning |
same |
So a probed client that is genuinely unattached still reports the full trio, including the two cases the brief singled out (probe reads back no marker; probe throws). An unresolvable/erroring probe is not collapsed into the probe-less bucket - attachable keys strictly on !!descriptor.attachProbe (status.js:600), never on the probe result.
I also enumerated contributes.client across every bundled manifest, so the comment's parenthetical is exhaustive rather than illustrative: probe-less = {claude-desktop, openclaw}, probed = {claude, codex}.
Mutation testing (each mutation applied to status.js, targeted tests run, then reverted):
| mutation | result |
|---|---|
drop attachable from the construction site |
npm run typecheck fails: TS2345 ... Property 'attachable' is missing ... but required in type 'ClientAttachReport' at status.js(604,18) |
const attachable = true (revert the gate) |
2 failures in status-probeless-client.test.js |
const attachable = false (maximal over-suppression) |
2 failures - the over-suppression guards are load-bearing, not decorative |
const inert = false |
1 failure (attach openclaw back to permanent pending) |
const inert = true |
1 failure (attach claude loses its real pending) |
Both directions are pinned. client_attach_stale (status.js:638-644) sits on the else if and requires probe.attached, so it is unreachable for a probe-less client and untouched for a probed one.
3. The required attachable field
grep -rn ClientAttachReport finds one production construction site (src/core/daemon/status.js:604) and one test-side one (test/core/status-client-error.test.js:67), both of which set it; the typecheck mutation above proves a missed site cannot land silently.
--json consumers are unbroken: renderStatusJson (src/core/commands/status.js:180-190) still emits attached as a boolean, in place, for every row - confirmed in all four runtime scenarios above, including the probe-less rows where it stays false exactly as before. I checked every reader of report.clients: wizard/index.js:294 (filter(c => c.attached)), wizard/fork.js:269 and commands/status.js:58 (both filter on configured), and hypaware-core/smoke/flows/status_diagnostics.js:298 (pins claude, a probed client). None changes answer.
On round 1's open nit (c.attachable !== false at commands/status.js:186, === false at :363): I checked whether a cross-version report could actually reach the renderers, since that would make the defensive form correct rather than redundant. It cannot - report.clients is built in-process at status.js:586-613; only daemon/sources come from status.json. So the defensive read protects nothing today, but it is consistent in both directions (a missing field degrades to the pre-PR reading in both renderers) and breaks nothing. Concurring with round 1: noted, not actionable, not worth the churn.
4. LLP 0139 amendment, and 0115 / 0143 consistency
The amendment box (llp/0139-desktop-picker-consent.decision.md:212-224) still reads correctly: the rule stands, the lookup stays, the warning stops firing for probe-less clients, and the "fired identically before consent / after decline / after success" reasoning is intact and is what carries the claim that no signal is lost. Its new sentence ("Claude Desktop was the only picker row declaring a configure_command, no plugin shipping today takes its first branch") is the claim I verified independently in section 1 - it holds, and it is correctly scoped to today's shipped set rather than stated as a permanent property.
No contradiction with the neighbours. LLP 0143's Consequences (:120-127) tell the same story from the other side and name hyp claude-desktop verify as the real check; the amendment does not restate anything 0143 denies. LLP 0115 #no-attach-on-join (:98-102) asserts only that Desktop registers no attach_probe and exposes explicit commands instead - it makes no claim about hyp status attach state at all (grepping 0115 for hyp status / attach_missing / pending / attached returns nothing), so there is nothing for the amendment to contradict. llp/0139:46-52 still describes the old repair, but it is Context, in the past tense, narrating what was observed at the time - correct as history.
5. The two other doc fixes, re-checked against a running binary
docs/ACCEPTANCE.md:302-313: every renderable claim matches what I actually printed - the clients row is - openclaw [configured, attach n/a], the action row is attach openclaw [n/a], backfill @hypaware/openclaw [pending] is present beside it, and no client_attach_missing mentions openclaw. client actions: is the real section header (src/core/commands/status.js:471), hyp attach openclaw is a real positional form (core_commands.js:276), and the LLP 0143 anchor link resolves. The other client_attach_missing mention in that file (:152) is in the Codex Desktop flow, a probed client, so it is still correct and was rightly left alone.
README.md:381: accurate now, and the table's column padding is intact ([39][84][74], matching every other body row).
6. House style
No U+2014 anywhere in the diff or in any touched file. The status.js delta is comments only, so the no-semicolon rule is untouched; no @typedef, no inline import('...'), and the new test's @import specifier is root-anchored. test/core/llp-ref-hygiene.test.js: 10/10 pass, so both #status-derives-by-the-same-gate and #repair-must-be-runnable resolve after the re-gloss.
Checks
npm test@03f6d47: 3273 pass / 0 fail / 1 skipped.npm run typecheck@03f6d47: clean.- Smokes:
status_diagnosticsok,client_attach_idempotentok. client_attach_on_join,claude_attach_detachandcli_bundled_plugins_activatedFAIL here - but they fail identically onorigin/masterin a second clean worktree (client_attach_on_joinreproduced 3/3 on master with the same message, "the attach.claude marker timestamp is unchanged"). Pre-existing, unrelated to this PR, and not counted against it. Flagging only so the failures are not mistaken for a regression by whoever runs them next.
Findings
None actionable. Nothing pushed; head remains 03f6d4757dab872a1c73fce19dbf32807cb21d6d. No findings are left open.
Previously adjudicated and deliberately not reopened: the backfill @hypaware/openclaw [pending] row, LLP 0143's open question about a registry-derived attach signal, and the residual that nothing in hyp status cues claude-desktop verify after a declined install. The accepted LLP 0167/0169 RFC that will give OpenClaw a real attach surface reverses this PR's premise for that one client in a future change set; it is not a defect here.
Root cause
The attach reconciler and
hyp statusderived "is this client an attach target?" by two different rules.action_attach.desired()skips any client descriptor with noattach_probe(src/core/config/action_attach.js:114) because attach-eligibility requires reverse-capability and only the probe can reverse it. So for a probe-less client (openclaw per LLP 0143, claude-desktop per LLP 0115 / #444 / #445)perform()never runs and no marker is ever written.Three status surfaces derived against the attach contract without that gate, and each read that permanent silence as a permanent negative:
buildClientActionsReportbuiltdeclaredAttachfrom every enabled client descriptor on a joined host, so no-marker + declared +hasCentralresolved topending, forever.{ attached: false }, renderingnot attachedwhere nothing is attachable.client_attach_missingfired onconfigured && !probe.attached, ungated, printinghyp attach --client openclaw- which resolves the adapter's deliberate LLP 0143 no-op and writes no marker either, so running it clears nothing.The doc comment above
buildClientActionsReportalready stated the intended invariant ("pending/n/aare derived for declared targets the reconciler would act on but has not yet"), so this was a missed gate, not a design choice.The fix
Gate all three on
descriptor.attachProbe, the same ruledesired()uses - the wayreadAttachPolicy/readBackfillPolicyalready keep the two sides from disagreeing abouton_join:inert: trueand derivesn/a, joiningon_join: falseand non-joined as the third "the reconciler is a no-op for this target" case. Not dropped from the report - a vanished row is its own wrong answer.ClientAttachReportgains a requiredattachable: boolean. The text surface printsattach n/ainstead ofnot attached;--jsoncarriesattachablebeside the unchangedattachedboolean, so a consumer pinningattacheddoes not break and one that wants to distinguish "no marker" from "no such thing as a marker" reads the new key.client_attach_missingno longer fires for a probe-less client.A probe-less client is unattachable, not unattached. This is deliberately a gate, not a new signal: nothing here reports whether OpenClaw is in fact routing, which stays LLP 0143's open question (a registry-derived attach signal, worth its own LLP).
Docs landed in the same commit
#status-derives-by-the-same-gate(the rule, and theattachablefield), plus consequences covering Claude Desktop and the JSON shape.n/anow also covers "a clientdesired()would never name", with the reasonpendingmust be derived bydesired()'s own rule.#repair-must-be-runnableis amended, not silently contradicted: the rule stands and theconfigure_commandlookup stays, but the warning now has to be one the repair can clear. Desktop loses itsclient_attach_missing, and no signal goes with it - with no probe to read back it fired identically before the consent prompt, after a decline, and after a fully successful install.hyp claude-desktop verifyis the check that can answer that question.Test evidence
New:
test/core/status-probeless-client.test.js(3 tests). A joined host whose central layer enables both@hypaware/openclaw(probe-less) and@hypaware/claude(probed) - claude is the over-suppression guard in every one of them.Before (on the unfixed code) - all 3 fail, for the right reasons
The failure in test 2 is the issue's reported string verbatim.
After - all 3 pass
Rendered status on that same joined host, after
The probed client keeps all three of its states.
backfill @hypaware/openclaw [pending]is correct and untouched: the OpenClaw adapter really does register a backfill provider (LLP 0161 #backfill-provider), so that target resolves.Suite
npm test: 3273 pass, 0 fail, 1 skipped. The skip is the pre-existing zstd-availability guard (resolveEncodeSettings falls back to SNAPPY...), unrelated. No pre-existing failures to report -masteris green here too.npm run typecheck: clean.test/core/llp-ref-hygiene.test.jspasses, so the three new@ref LLP 0143#status-derives-by-the-same-gateannotations resolve to a live anchor.Refs: LLP 0044, LLP 0115, LLP 0139, LLP 0143, #444, #445, PR #510.
Fixes #544