Uh oh!
There was an error while loading. Please reload this page.
Client-scoped materialize keeps a dest another client still contributes - #893
Client-scoped materialize keeps a dest another client still contributes#893philcunliffe wants to merge 3 commits into
Conversation
…es (#884) `reconcileClientAssetLedger` built its prune keep-set from `planned`, the plan for the clients this run was scoped to. Every attach path is scoped: `hyp attach` and the daemon reconciler pass one client name, the wizard finale passes the picked ones. So the keep-set never contained another client's contributions, and the invariant the doc block states - not contributed, for any client - only held under `--client all`. `claude` and `claude-desktop` both declare `.claude/skills`, so one physical dest can be recorded under one and contributed by the other. A skill whose `clients` narrows from both to `claude-desktop` alone was therefore deleted by the very next `claude`-scoped attach, and nothing re-installs it: the reconciler never attaches probe-less `claude-desktop`. The keep-set is now re-planned with `clients: 'all'` (quietly, since the run's own plan already wrote any warnings), so it asks what every client contributes rather than what this run was asked to install. A dest no client contributes any more is still retired and still pruned, which the second new test pins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…says why Review of #893 found the new `@ref` asserting the opposite of the section it cited. LLP 0219 #prune-on-materialize is Accepted and settles the prune's second condition as "the whole run's plan", and the carry as "asked of the whole run's plan too". #893 widens both to every client's contributions, which changes what 0219 decided rather than restating it, so it is recorded as its own decision extending 0219 (the shape LLP 0226 already used for the two narrowings from #745's ship review), with an `Extended-by:` forward-ref on 0219 and the code's `@ref` repointed at the new anchor. The `stderr`-less widened plan also carried a false justification: it claimed every warning `planClientAssets` writes had already been written by the run's own plan. It had not. `planClientAssets` filters by `wanted` before the unknown-client check, so a contribution naming a client outside a scoped run's filter warns in neither pass. Suppressing is still right (an explicit client list never warns about what it excluded, and `--client all` still surfaces them), and the comment now says that instead, so a later reader does not "fix" it by re-attaching `stderr`. LLP 0266 #open-stale-carried-digest records the residual the review also found: a carried record's digest is never refreshed, so a dest another client rewrites can later be withheld from pruning with a message blaming the user for HypAware's own rewrite. Pre-existing, reachable by one more route after this widening, and not closed here because both candidate fixes change what the ledger is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe
commented
Aug 19, 2026
Neutral review round: head |
Review of #893 found that widening the keep-set stops the deletion but nothing rewrites the surviving copy: a `claude`-scoped run's own plan no longer contains a dest whose contribution narrowed to `claude-desktop`, and the widened pass only answers "still contributed?", it never copies. The bytes sit at whatever the last run that planned them wrote, and the reconciler attaches no probe-less client to close the gap. That is a real residual, not a regression: before the widening the same run deleted the copy outright and nothing re-installed it. #consequences claimed the asset "survives the next scoped attach" without saying that surviving is all it gets, so the record was the thing that was wrong. Recorded as #open-kept-not-refreshed alongside #open-stale-carried-digest, with why neither obvious close is taken here: copying every kept dest makes `hyp attach claude` install what nobody asked it to, and copying only what lands in this client's own directories changes what a scoped run is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe
commented
Aug 19, 2026
Neutral review round: |
philcunliffe
commented
Aug 19, 2026
Neutral triage at the review-round cap: head |
The bug
reconcileClientAssetLedgerbuilt its prune keep-set from this run's plan:planClientAssetsfilters byoptions.clients, and every attach path is client-scoped:materializeAttachAssetspassesclients: [name], the daemon reconciler passesclients: [client], the wizard finale passesclients: clientsPicked. So the keep-set never contained another client's contributions, and the invariant the doc block states (not contributed, for any client) only held under--client all.claudeandclaude-desktopboth declare.claude/skills, so one physical dest can be recorded under one client and contributed by the other. A skill whoseclientsnarrows from['claude','claude-desktop']to['claude-desktop']was deleted by the very nextclaude-scoped attach, and nothing re-installs it: the reconciler never attaches probe-lessclaude-desktop.Reproduction
New test
a client-scoped run never prunes a destination another client still contributesintest/core/client-assets-prune.test.js. Onmasterit fails with exactly the reported line:The existing shared-dest test could not catch this: its
installWithhelper always ranhyp skills installwith the default--client all. The helper now takes an optionalclient, which is what drives the scoped run.The fix
The keep-set is re-planned with
clients: 'all'(without anstderr, since the run's own plan already wrote any warnings), so "is this path still contributed?" is asked of every client's contributions rather than of the clients this run was scoped to. When the run is already'all'the existing plan is reused.Pruning is not disabled on scoped runs: the second new test,
a client-scoped run still prunes a destination no client contributes any more, pins that a name no manifest declares any more is still retired and still removed under--client claude. It passes before and after.The carry-forward loop and condition 2 of the doc block are reworded to match, and the widening carries an
@ref LLP 0219#prune-on-materialize.Checks
npm test: 4485 pass, 0 fail, 1 skippednpm run typecheck: cleanFixes#884