Skip to content

hyp status: a probe-less client is unattachable, not unattached - #553

Open
philcunliffe wants to merge 2 commits into
masterfrom
fix/issue-544
Open

hyp status: a probe-less client is unattachable, not unattached#553
philcunliffe wants to merge 2 commits into
masterfrom
fix/issue-544

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

Root cause

The attach reconciler and hyp status derived "is this client an attach target?" by two different rules.

action_attach.desired() skips any client descriptor with no attach_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:

  1. buildClientActionsReport built declaredAttach from every enabled client descriptor on a joined host, so no-marker + declared + hasCentral resolved to pending, forever.
  2. The clients row mapped a probe-less descriptor to { attached: false }, rendering not attached where nothing is attachable.
  3. client_attach_missing fired on configured && !probe.attached, ungated, printing hyp 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 buildClientActionsReport already stated the intended invariant ("pending / n/a are 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 rule desired() uses - the way readAttachPolicy / readBackfillPolicy already keep the two sides from disagreeing about on_join:

  • client actions: a probe-less declared target carries inert: true and derives n/a, joining on_join: false and 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.
  • clients row: ClientAttachReport gains a required attachable: boolean. The text surface prints attach n/a instead of not attached; --json carries attachable beside the unchanged attached boolean, so a consumer pinning attached does not break and one that wants to distinguish "no marker" from "no such thing as a marker" reads the new key.
  • diagnostic: client_attach_missing no 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

  • LLP 0143 grows #status-derives-by-the-same-gate (the rule, and the attachable field), plus consequences covering Claude Desktop and the JSON shape.
  • LLP 0044 §Status surface: n/a now also covers "a client desired() would never name", with the reason pending must be derived by desired()'s own rule.
  • LLP 0139 #repair-must-be-runnable is amended, not silently contradicted: the rule stands and the configure_command lookup stays, but the warning now has to be one the repair can clear. Desktop loses its client_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 verify is 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

not ok 1 - a probe-less client on a joined host renders attach n/a, never a permanent pending (#544)
  error: |-
    Expected values to be strictly equal:

    'pending' !== 'n/a'

not ok 2 - a probe-less client raises no client_attach_missing, the probed one still does (#544)
  error: "expected no client_attach_missing for openclaw, got: '@hypaware/claude' is enabled but claude settings show no HypAware marker - run 'hyp attach --client claude' | '@hypaware/openclaw' is enabled but openclaw settings show no HypAware marker - run 'hyp attach --client openclaw'"

not ok 3 - a probe-less client row reads attach n/a, not "not attached" (#544)
  error: |-
    Expected values to be strictly equal:
    + actual - expected

    + undefined
    - false

1..3
# pass 0
# fail 3

The failure in test 2 is the issue's reported string verbatim.

After - all 3 pass

ok 1 - a probe-less client on a joined host renders attach n/a, never a permanent pending (#544)
ok 2 - a probe-less client raises no client_attach_missing, the probed one still does (#544)
ok 3 - a probe-less client row reads attach n/a, not "not attached" (#544)
1..3
# pass 3
# fail 0

Rendered status on that same joined host, after

  clients:
    - claude  [configured, not attached]  [central · locked]
    - openclaw  [configured, attach n/a]  [central · locked]
    - claude-desktop  [not in config, attach n/a]  [local]
  client actions:
    - attach claude  [pending]
    - attach openclaw  [n/a]
    - backfill @hypaware/claude  [pending]
    - backfill @hypaware/openclaw  [pending]
  diagnostics:
    [WARN ] client_attach_missing: '@hypaware/claude' is enabled but claude settings show no HypAware marker - run 'hyp attach --client claude'

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 - master is green here too.
  • npm run typecheck: clean.
  • test/core/llp-ref-hygiene.test.js passes, so the three new @ref LLP 0143#status-derives-by-the-same-gate annotations resolve to a live anchor.

Refs: LLP 0044, LLP 0115, LLP 0139, LLP 0143, #444, #445, PR #510.

Fixes #544

test and others added 2 commits July 31, 2026 22:00
`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>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review round - PR #553 @ daca753

Verdict: findings (3 actionable, all fixed and pushed). The core change is correct: the gate keys strictly on descriptor.attachProbe, matches desired()'s own rule, and cannot swallow a real negative. What it missed is the paper trail: a release-gate doc and a user-facing doc still describe the behaviour this PR deliberately removes, and one @ref gloss now describes a code path no shipped plugin can reach.

New head after fixes: 03f6d4757dab872a1c73fce19dbf32807cb21d6d.


What I verified against the code (not the prose)

Over-suppression - clean. src/core/daemon/status.js:600 sets attachable = !!descriptor.attachProbe, and src/core/plugin_catalog.js:77 only ever assigns descriptor.attachProbe from a truthy client.attach_probe, so the flag is exactly the negation of action_attach.js:114's if (!descriptor.attachProbe) continue. Nothing about the result of a probe feeds the gate:

  • probed client, no settings file → attachable: true, probe.attached: falsepending / not attached / client_attach_missing all survive (test/core/status-probeless-client.test.js asserts all three for claude in every one of its three cases - the over-suppression guard is real, not decorative).
  • probed client whose probe resolves and finds no marker → identical path; probeClientAttachFromDescriptor returns { attached: false } and the gate never sees it.
  • probed client whose probe errors{ attached: false, error }, still configured && attachable && !attached → warns. test/core/status-client-error.test.js:67 pins attachable: true for exactly that row, with a comment saying why an unresolvable probe is not the same as no probe. Good distinction to have written down.
  • client_attach_stale (status.js:637) is on the else if and requires probe.attached, so it is untouched in both directions.

attachable as a required field - clean. There is exactly one construction site (src/core/daemon/status.js:604-613); grep -rn ClientAttachReport finds no other producer. npm run typecheck is clean, so a missed site would have failed. --json is genuinely non-breaking: renderStatusJson (src/core/commands/status.js:187) still emits attached as a boolean for every row, in place, with attachable added beside it.

Nit, not fixed: the renderers read the required field defensively (c.attachable !== false at status.js:186, c.attachable === false at :367). With a single in-process producer and a required type there is no cross-version report to defend against, and the defensive form would silently degrade a future missing field to the old wrong not attached instead of failing typecheck. Harmless today; flagging so it is a choice rather than an accident.

LLP 0139 / Claude Desktop - the judgement call holds. I checked the claim rather than taking it: hypaware-core/plugins-workspace/claude-desktop/hypaware.plugin.json declares contributes.client with no attach_probe, so probe was hard-coded { attached: false } and the warning fired on configured alone. Per LLP 0139's own Consequences the plugins stay in config after a decline, so configured is true before consent, after a decline, and after a successful install alike. The warning genuinely never varied, so removing it removes no information. And hyp claude-desktop verify is real and substantive, not a hand-wave: hypaware-core/plugins-workspace/claude-desktop/src/verify.js:28 checkInstallState reads plist-present, plist-up-to-date and residue-cleared and drives the exit code. Claim accepted.

Residual gap worth a follow-up issue (not blocking, not fixed here): nothing in hyp status now cues a user to run that verify, so a declined Desktop install is silent on the surface people actually look at. checkInstallState's own doc comment already advertises itself as shareable with "any future caller (e.g. a status surface)", so the hook exists. Wiring a plugin-specific check into core status is exactly the kind of special-casing LLP 0143 leaves open as a registry-derived signal, so it belongs in that follow-up, not in this PR.

backfill @hypaware/openclaw [pending] - correct and untouched. inert is only ever set on declaredAttach (status.js:913); the backfill declared map (status.js:866) never carries the key, so decl.inert === true is strictly false for every backfill row. And the target is real: hypaware-core/plugins-workspace/openclaw/src/index.js:152 calls ctx.backfills.register(...) under an @ref LLP 0161#backfill-provider. The PR's claim checks out.

Conventions. No U+2014 anywhere in the diff; no semicolons outside prose; the new test's @import specifier (../../src/core/daemon/types.js) is correctly repo-root-anchored; test/core/llp-ref-hygiene.test.js passes, so #status-derives-by-the-same-gate and #decision both resolve.


Findings

1. docs/ACCEPTANCE.md:302 - release gate still asserts the removed warning. Severity: medium. FIXED.

Step 1 of the OpenClaw acceptance flow read:

Expect hyp status to also carry a client_attach_missing warning for openclaw, telling you to run hyp attach openclaw.

That warning is precisely what this PR deletes. A human running the acceptance gate would find it absent and read the fix as a regression - or, worse, note-and-move-on and leave a stale expectation in a manual release gate that nothing else checks. CLAUDE.md's "living docs" rule wants this in the same commit as the code. Rewritten so the absence is the pass condition, with the clients row ([configured, attach n/a]), the joined-host attach openclaw [n/a] action, the unrelated backfill @hypaware/openclaw [pending] row, and a link to LLP 0143 #status-derives-by-the-same-gate.

2. src/core/daemon/status.js:626 - the @ref LLP 0139 gloss is now false. Severity: low. FIXED.

The gloss read "an adapterless client's attach-missing repair names its configure_command". After this PR the adapterless client can never reach that line. I enumerated every shipped manifest: claude-desktop is the only picker row declaring a configure_command, and it is probe-less, so the configureCommand ? ... : ... truthy branch is unreachable for the entire shipped plugin set. (openclaw is the inverse case - probe-less but does registerClient, so "adapterless" and "probe-less" are not synonyms here.) That is the "keep refs honest" rule biting: the annotation asserts the opposite of what the amendment box in LLP 0139 says. Re-glossed to what the code does, and the surrounding comment now states plainly that no shipping plugin takes the branch and why the lookup is kept anyway. LLP 0139's amendment box records the same, so doc and code agree.

3. README.md:381 - diagnostics table overstates when client_attach_missing fires. Severity: low. FIXED.

Read "a client plugin is enabled but its settings file shows no HypAware marker", which is now false for openclaw and claude-desktop (enabled, no marker, no warning). Now "a client plugin with an attach probe is enabled but shows no HypAware marker". Column padding preserved.

Noted, not actionable

  • desired() has a second skip status cannot mirror: if (!clients.getClient(descriptor.name)) continue. A descriptor whose plugin registers no runtime client would still derive a permanent pending. Pre-existing, unreachable in the shipped set (the only adapterless client is also probe-less and now gated), and hyp status deliberately does not activate plugins - so it cannot be closed the same way. Mentioning it so the "status derives by the same gate" rule is not read as fully closed.
  • A pre-LLP-0109 done attach marker for openclaw would still render attach openclaw [done] while its clients row says attach n/a. Marker-derived, out of this gate's path, and arguably the honest reading of both facts.

Checks

  • npm test on daca753: 3273 pass / 0 fail / 1 skipped (the pre-existing zstd guard) - matches the PR's claim exactly.
  • npm test after the fixes: 3273 pass / 0 fail / 1 skipped.
  • npm run typecheck before and after: clean.
  • test/core/llp-ref-hygiene.test.js: 10/10 pass after the re-gloss.

Fixes verified landed (committed tree, daca75303f6d47)

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).

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review round 2 - PR #553 @ 03f6d47

Verdict: clean. Nothing actionable; nothing pushed; head is unchanged at 03f6d47.

This round reviews the round-1 fixes themselves (daca753..03f6d47: README.md, docs/ACCEPTANCE.md, llp/0139-..., src/core/daemon/status.js). I re-derived the high-risk properties from the code and from a running binary rather than from round 1's write-up.


1. The status.js line in the delta is provably comment-only

Round 1 called it a re-gloss; the diff shows 21 changed lines, so I checked rather than assumed. Every +/- line in git diff daca753 03f6d47 -- src/core/daemon/status.js starts with //, and stripping line + block comments from both blobs and comparing gives byte-identical source (27965 bytes each). No behaviour changed between the two heads. Reviewed as a comment change.

The re-gloss is honest against the code it sits on. @ref LLP 0139#repair-must-be-runnable at src/core/daemon/status.js:629 now reads "the repair is the client's own picker configure_command when it declares one, never a generic attach that would answer unknown client", which is exactly :630-631. I re-verified the unreachability claim independently instead of trusting it: picker rows come only from plugin manifests (src/core/plugin_catalog.js:90-106 reads entry.manifest.contributes.picker; there is no core-side picker table), and grep -rn configure_command --include=*.json over the whole tree returns exactly one hit, hypaware-core/plugins-workspace/claude-desktop/hypaware.plugin.json:30, on a probe-less plugin. The configureCommand truthy branch is therefore dead for the shipped set, and the comment now says so.

2. Over-suppression, re-verified empirically at this head

I drove collectHypAwareStatus + both renderers against a joined host (central seed enabling @hypaware/central, ai-gateway, openclaw, claude, codex) across four homes, with the real bundled catalog and no stubbing:

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_diagnostics ok, client_attach_idempotent ok.
  • client_attach_on_join, claude_attach_detach and cli_bundled_plugins_activated FAIL here - but they fail identically on origin/master in a second clean worktree (client_attach_on_join reproduced 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.

@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hyp status shows a permanent 'attach openclaw [pending]', 'not attached', and an inert client_attach_missing repair for probe-less clients

1 participant