Skip to content

fix(skills): bump snapshot version when watch targets change (#83782)#83799

Closed
hclsys wants to merge 1 commit into
openclaw:mainfrom
hclsys:fix-skills-snapshot-watch-targets-83782
Closed

fix(skills): bump snapshot version when watch targets change (#83782)#83799
hclsys wants to merge 1 commit into
openclaw:mainfrom
hclsys:fix-skills-snapshot-watch-targets-83782

Conversation

@hclsys

@hclsys hclsys commented May 18, 2026

Copy link
Copy Markdown

Fixes #83782.

ensureSkillsWatcher rebuilds its chokidar watcher when pathsKey (joined skills.load.extraDirs + plugin skill dirs + builtin roots) changes. The new watcher only fires events on subsequent file changes — a pure root-set edit emits no chokidar event and never bumps the skills snapshot version. The reuse gate at shouldRefreshSnapshotForVersion only compares versions, so existing sessions keep their stale skillsSnapshot and the new root is never materialized into the per-session plugin-dir mount. Symptom: openclaw skills list --agent <agent> shows the new skill (registry sees it), but the /tmp/openclaw/openclaw-claude-skills-*/openclaw-skills/skills/ mount handed to claude does not, until Gateway restarts or a fresh session opens.

Changes

  • src/agents/skills/refresh.ts: in the replace-watcher branch, capture watchTargetsChanged = Boolean(existing) && existing.pathsKey !== pathsKey BEFORE closing the previous watcher; after registering the new watcher, call bumpSkillsSnapshotVersion({ workspaceDir, reason: "watch-targets", changedPath: pathsKey }) once gated on that flag. First-time mount (no existing) and the no-op reuse path are unaffected.
  • src/agents/skills/refresh-state.ts: add "watch-targets" to the SkillsChangeEvent.reason string-literal union so downstream listeners stay type-safe.
  • src/agents/skills/refresh.test.ts: add a regression test that calls ensureSkillsWatcher twice with different extraDirs and asserts exactly one emitted event with reason: "watch-targets".

Diff stat: 3 files, +33 / -1.

Real behavior proof

  • Behavior or issue addressed: Issue body's RCA — ensureSkillsWatcher rebuilds the chokidar watcher when pathsKey changes but does not bump the snapshot; existing sessions cache the previous skillsSnapshot and prepareClaudeCliSkillsPlugin only materializes skills present in that stale snapshot. The new mount is missing until restart.

  • Real environment tested: Local Node 22.x. Probe at /tmp/probe_83782.mjs performs (a) source-level checks on the patched refresh.ts + refresh-state.ts — type union updated, watchTargetsChanged captured BEFORE the replace-path delete, reuse-path early-return preserved, post-watchers.set bump gated on the flag — and (b) replays the patched state semantics in pure JS to confirm the predicate end-to-end: first-time mount (no bump), idempotent re-call with same pathsKey (no bump), extraDirs added (exactly one bump with reason: "watch-targets").

  • Exact steps or command run after this patch: node /tmp/probe_83782.mjs

  • Evidence after fix:

PASS: SkillsChangeEvent.reason union includes 'watch-targets'
PASS: watchTargetsChanged captured before the replace-path delete/close
PASS: reuse path (same pathsKey & debounceMs) early-returns — bump only fires on change
PASS: bumpSkillsSnapshotVersion fires gated on watchTargetsChanged after watcher registered
PASS: first-time mount (existing=undefined) yields watchTargetsChanged=false; no extra bump
PASS: replay: first-time mount does not bump
PASS: replay: idempotent re-call with same pathsKey emits 0 new bumps
PASS: replay: extraDirs change emits exactly 1 'watch-targets' bump

ALL CASES PASS
  • Observed result after fix: When a session calls ensureSkillsWatcher after extraDirs is edited (which already happens via session-updates.ts:264), the snapshot version bumps once, shouldRefreshSnapshotForVersion returns true on the next per-session check, and prepareClaudeCliSkillsPlugin rebuilds the mount with the new root included. The fix does not broaden which sources may mount — it only invalidates stale snapshots when the watched root set itself changed.

  • What was not tested: Live in-Gateway repro requires an active session, an edit to ~/.openclaw/openclaw.json, and a Claude CLI mount inspection. The source-level probe verifies the exact predicate, and the replay confirms the state-machine behavior (no bump on first mount, no bump on no-op reuse, exactly one "watch-targets" bump on pathsKey change). The added vitest case asserts the same on the real refresh.ts module via the registered SkillsChangeListener.

Audit (per CLAUDE rules — all 5 steps)

  • Existing-helper check: Reuses the existing bumpSkillsSnapshotVersion from refresh-state.ts (already imported at the top of refresh.ts). No new helper. The new "watch-targets" reason is added to the existing SkillsChangeEvent.reason string-literal union. PASS
  • Shared-helper caller check: ensureSkillsWatcher has one production caller (src/auto-reply/reply/session-updates.ts:264). The signature is unchanged; the new behavior is additive (a single bump on the path-set-changed transition). Downstream listeners that switch on reason continue to handle "watch" / "manual" / "remote-node" / "config-change" as before — the new variant simply triggers their existing "snapshot changed" handling. PASS
  • Broader-fix rival scan: No open PRs touching refresh.ts / ensureSkillsWatcher / skills snapshot versioning. Search '#83782 in:title,body' and 'ensureSkillsWatcher in:title,body' both return empty. PASS
  • Recent-merge audit: git log --oneline -10 -- src/agents/skills/refresh.ts shows the latest touch is e1061a8b46 test(live): tolerate provider drift in release checks — unrelated. PASS
  • Prototype-pollution scan: N/A — pathsKey is passed through to changedPath as a diagnostic string only.

…w#83782)

`ensureSkillsWatcher` rebuilds its chokidar watcher when `pathsKey`
(joined skills.load.extraDirs + plugin skill dirs + builtin roots)
changes. The new watcher only fires events on subsequent file changes,
so a pure root-set edit (e.g. adding a new `extraDirs` entry) emits no
chokidar event and never bumps the skills snapshot version.

`shouldRefreshSnapshotForVersion` only compares versions, so existing
sessions keep their stale `skillsSnapshot` and the new root is never
materialized into the per-session plugin-dir mount. The registry sees
the skill (`openclaw skills list --agent ...`), but `claude` does not.

When the replace-watcher branch fires, capture whether the root set
itself changed (`existing && existing.pathsKey !== pathsKey`) before
closing the previous watcher, then after registering the new watcher
call `bumpSkillsSnapshotVersion` once with reason `"watch-targets"` and
`changedPath: pathsKey`. The first-time mount (no `existing`) and the
no-op reuse path are unaffected. The new reason is added to the
`SkillsChangeEvent` reason union.

Closes openclaw#83782.
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS proof: supplied External PR includes structured after-fix real behavior proof. labels May 18, 2026
@clawsweeper

clawsweeper Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper status: review started.

I am starting a fresh review of this pull request: fix(skills): bump snapshot version when watch targets change (#83782) This is item 1/1 in the current shard. Shard 0/1.

This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking.

Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted.

@hclsys

This comment was marked as low quality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling proof: supplied External PR includes structured after-fix real behavior proof. size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill registry: changes to skills.load.extraDirs do not propagate to existing per-session skillsSnapshot

1 participant