Uh oh!
There was an error while loading. Please reload this page.
detach: report every externally-overridden managed key, not just the last (issue #440 finding 2) - #441
Conversation
…last `DetachFromDiskResult.warning` was a single string reassigned inside the per-key loop, so when the undo left two or more managed values in place because the user had re-pointed them, only the last key's notice survived. The protection itself was never affected (the user values were always kept); the operator was simply never told which keys stayed behind. Both loops that produce the notice now accumulate one message per key and fold them through a shared `joinWarnings` helper. The field stays a single human-readable string - its one consumer logs it as a `detail` attribute - so the published `DetachFromDiskResult` contract is unchanged; it now just carries every key instead of one. Issue #440 finding 2. LLP 0045 Part 3 gains the never-clobber section the new `@ref`s point at. Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-up on the multi-key `warning` accumulation. Every notice already reads "<key> was overridden externally; leaving in place", so joining them on `; ` produced four `; `-delimited clauses for two keys and left a reader unable to tell where one notice ended: ANTHROPIC_BASE_URL was overridden externally; leaving in place; ENABLE_TOOL_SEARCH was overridden externally; leaving in place `|` cannot occur in an env key name or a dotted JSON path, so ` | ` keeps the notice boundary unambiguous. The single-key string is untouched. No test pinned the separator (a `', '` mutant survived the suite), so both multi-key tests now assert the whole joined string, not just per-key matches. Also corrects the "one consumer" claim in LLP 0045 and the `joinWarnings` JSDoc: `warning` has two consumers, `action_attach.js` (span `detail`) and `hyp detach` (stdout plus the `--json` payload, src/core/commands/clients.js). Neither parses it, which is what actually justifies keeping a single string, and the doc now says so. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe
commented
Jul 29, 2026
Neutral review round - PR #441 @ |
Round 2 review of #441. The PR documented ` | ` as a safe notice boundary for `DetachFromDiskResult.warning` ("not safely splittable on anything but the ` | ` boundary"; "`|` cannot occur in an env key name or a dotted JSON path, so the boundary stays unambiguous"). That is true for the two branches that join, but false for the field. The field is shared with `detachTomlManagedBlock`, whose single unjoined notice interpolates the user's live `model_provider` straight from their config: a `~/.codex/config.toml` reading `model_provider = "acme | prod"` yields `model_provider was changed externally; leaving acme | prod in place`, which splits on ` | ` into two bogus notices. Nothing in-tree parses `warning` (action_attach.js logs it as a span detail; clients.js prints it and echoes it into `hyp detach --json`), so this is a doc-contract correction, not a behavior change. Retitles ` | ` as the readability choice it is and states plainly that the field must not be split. Adds the codex regression test that branch never had, pinning the exact warning text and demonstrating the one-notice-splits-into-two case.
philcunliffe
commented
Jul 29, 2026
Neutral review — round 2 of 2 (final)Reviewed Verdict: the core change is correct and I would approve the behavior. One documentation contract the PR itself introduced was demonstrably false and is now corrected. One item remains deliberately deferred, and the PR body carries a stale claim that only triage can fix. Round 1's fixes — re-tested, all holdSeparator is genuinely pinned (round 1 LOW, confirmed fixed). Mutated The Zero-warning case yields an absent field. Single-warning string is byte-identical to pre-PR. The template literals are unchanged from
Both consumers confirmed (round 1 MEDIUM-docs). Re-swept: NEW — LOW: the |
| plugin | format | reaches the set loop? |
|---|---|---|
claude | json | no — different function |
codex | toml | no — different function |
openclaw | json_path + marker_record | yes |
claude-desktop | json_path, no marker_record | no — see below |
- OpenClaw is the only real producer, and it emits exactly one entry, always:
const set = [{ path: PRIMARY_PATH, value: managedPrimary, prev: primary }](hypaware-core/plugins-workspace/openclaw/src/settings.js:175). I also checked the re-attach path round 1 did not mention —reattach()passes the originalmanagedblock through verbatim toattachRecord(...)and never growsset, so no number of re-attaches can produce a second entry. - claude-desktop declares
format: json_path(hypaware-core/plugins-workspace/claude-desktop/hypaware.plugin.json:22) but nomarker_record, sorecordPath === undefined→managed === undefined→detachJsonPathMarkerthrowsMALFORMED_MARKERat:438, before the loop.
Verdict: round 1's claim holds — genuinely unreachable via any shipped producer. I'd sharpen the framing for the follow-up, though: it is unreached, not unreachable by construction. detachJsonPathMarker is documented as a format-generic record replayer (@ref LLP 0109#probe-and-detach-core-owned: "the record replay is format-generic, core knows json_path semantics, never 'OpenClaw'"), PluginAttachProbeManifest is in the published root kernel contract (hypaware-plugin-kernel-types.d.ts:163-181), and the repo has a real plugin-install path (src/core/plugin_install/, LLP 0007). Any installed plugin declaring json_path + marker_record with two set entries hits the defect on day one. It is one third-party plugin away, not impossible.
Even so, I agree it should not be fixed in this PR. Unlike warning, there is no obviously-correct answer: first-wins (matching removed), last-wins (status quo), or plural all encode different operator semantics, and silently picking one here would mint a contract nobody designed, in a PR scoped to messages. It wants its own issue.
Unresolved — for triage
- The PR body still carries the false "its one consumer" claim. Body: "its one consumer,
action_attach.js, logs it as adetailattribute". There are two:src/core/config/action_attach.js:346andsrc/core/commands/clients.js:588, and the second is the more consequential one — it echoeswarninginto the machine-readablehyp detach --jsonpayload, which is precisely the surface where the parseability question above bites. Round 1 flagged this; neither round is permitted to edit the PR body. Triage should correct it before merge, as it did for Attach declares the gateway first-party so Claude keeps its real context window #438. The in-repo docs are already right (LLP 0045 andtypes.d.tsboth name both consumers) — the body is the only stale copy. - Finding 4 needs a follow-up issue:
restoredValuelast-wins atclient_detach_disk.js:468, inconsistent withremoved's first-wins guard at:471. Not reachable today; blocked on a deliberate first/last/plural decision. - Not this PR, noticed in passing:
hyp detach --client claude-desktoplooks like it always throwsMALFORMED_MARKER— the manifest declaresjson_pathwith nomarker_record(claude-desktop/hypaware.plugin.json:22), anddetachJsonPathMarker:437refuses any record-less marker. Pre-existing onmasterand unrelated to this change; worth its own look.
Checks
npm testat9613eb3: 2796 pass / 8 fail — the 8 known pre-existingtest/core/leave-command.test.jsfailures, identical to the baseline ata891403e. No new failures.npm run typecheck: clean.- Detach suites:
40 pass / 0 fail. - Pushed
9613eb38954cf38f3b517fb8927340f0c0fc966ftofix/issue-440.
Round cap (2) reached; anything above under "Unresolved" goes to triage.
philcunliffe
commented
Jul 29, 2026
Triage - PR #441 @ |
Uh oh!
There was an error while loading. Please reload this page.
Follow-up on the findings issue #438's triage deferred. One of the three findings is fixed here; the other two are deliberately deferred, with reasons below.
Fixed: finding 2 -
warningoverwritten when several managed keys were externally overriddenDetachFromDiskResult.warningwas a singlestring | undefinedreassigned inside the per-key loop indetachJsonMarker, so when the undo left two or more managed values in place because the user had re-pointed them, only the last key's notice survived. The on-disk protection was never affected (the user values were always kept); the operator was simply never told which keys stayed behind after a detach that otherwise reports success.The identical pattern exists in
detachJsonPathMarker'sset-entry loop, so both now accumulate one notice per key and fold them through a sharedjoinWarningshelper.The contract shape is unchanged.
warningstays a single human-readable string (it has two consumers, not one:action_attach.js:346logs it as adetailattribute onclient_action.attach_reverse_warning, andclients.js:588echoes it into thehyp detach --jsonpayload); it now carries every key instead of one. That is deliberately not the array change the issue floated as a reason to defer - no caller has to change, and there is no published-type break.Regression tests (fail before, pass after)
test/core/client-detach-disk.test.js:claude undo names EVERY externally-overridden managed key in the warning, not just the last- attach, override bothANTHROPIC_BASE_URLandENABLE_TOOL_SEARCH, detach; asserts both key names appear inwarningand that both user values survive on disk.test/core/client-detach-json-path.test.js:json_path undo names EVERY externally-overridden set entry in the warning- a hand-written record with twosetentries, both overridden; asserts both paths appear.claude undo reports a single overridden key without the join separator, pinning the one-key string to its exact existing wording so the accumulation did not change the common case.Verified: the two multi-key tests fail on
masterbefore the source change and pass after.npm testis green apart from the 8 known pre-existingtest/core/leave-command.test.jsfailures, which are unrelated.npm run typecheckis clean.LLP 0045 Part 3 gains a
Never clobber a user edit: report every override, not just the lastsection, landed in the same commit, which the two new@refs point at.Deferred: finding 1 - ownership gate silently adopts a non-string user value
Deferred on purpose, and it should be redone once #438 merges.
The correct fix is to gate on presence (
key in env) rather thantypeof env[key] === 'string'. Onmasterthat predicate lives on a single line insideattach():PR #438deletes that exact line and replaces the whole block with the
MANAGED_ENV_ADDITIONStable plusmanageEnvAdditions(). Editing a line the held PR removes is a guaranteed merge conflict, and the fix would have to be rewritten against the new helper anyway - the very outcome that made triage defer it. #438's diff also rewrites 77 lines oftest/plugins/claude-settings-attach.test.js, so the regression test would collide too.Fixing it after #438 lands is strictly better: one edit to
manageEnvAdditions's singleif (!weOwnIt && typeof env[key] === 'string') continueguard covers both managed keys at once, rather than fixingENABLE_TOOL_SEARCHhere and_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URLagain later. The data-loss risk is unchanged by waiting: it is pre-existing onmasterand #438 does not make an individual key any more exposed.Deferred: finding 3 - verified-version discrepancy (2.1.220 vs 2.1.215)
Deferred because there is nothing to reconcile on
master. Both version stamps were introduced by PR #438 and exist only onfix/issue-437:hypaware-core/plugins-workspace/claude/src/settings.js:61(last verified 2.1.220) - added by Attach declares the gateway first-party so Claude keeps its real context window #438llp/0045-client-attach.design.md:291(verified against 2.1.215) - added by Attach declares the gateway first-party so Claude keeps its real context window #438 round 2git greponorigin/masterfinds neither string in either file. A "fix" here would have to invent the text it is correcting. It is a one-word edit to make after #438 merges (the triage worktree independently confirmed the installed bundle is 2.1.215, so2.1.220is the stamp to correct).Finding 4 in the issue was already corrected in #438's body during triage and needs nothing here.
Interaction with PR #438
This branch is based on
origin/masterand touches no file #438 modifies excepttest/core/client-detach-disk.test.js, where #438 inserts around line 136 and this PR appends at the end of the file.src/core/config/client_detach_disk.js,src/core/config/types.d.tsandtest/core/client-detach-json-path.test.jsare untouched by #438. The LLP 0045 edit inserts a new section before#### ENABLE_TOOL_SEARCH, while #438 edits text inside and after that section. #438's review is not affected and needs no new commits.Fixes#440