Skip to content

Commit f1a55cb

Browse files
clawsweeper[bot]hclsysTakhoffman
authored
fix(skills): refresh snapshots when watch roots change (#83823)
Summary: - The replacement PR adds a `watch-targets` skills snapshot invalidation when `ensureSkillsWatcher` rebuilds f ... root set, reads the snapshot version after watcher setup, adds regression tests, and updates the changelog. - Reproducibility: yes. Source inspection shows current main rebuilds the skills watcher on changed root targe ... the version before watcher setup; I did not run a live Gateway mount reproduction in this read-only review. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(skills): refresh snapshots when watch roots change Validation: - ClawSweeper review passed for head 2677dcc. - Required merge gates passed before the squash merge. Prepared head SHA: 2677dcc Review: #83823 (comment) Co-authored-by: hclsys <hclsys@openclaw.ai> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 9b517b5 commit f1a55cb

6 files changed

Lines changed: 66 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai
5353
- CLI/update: bypass npm freshness filters consistently during managed package and plugin installs so freshly published release plugins remain installable. Thanks @jalehman.
5454
- CLI/update: guide root-owned npm install EACCES recovery by stopping the managed Gateway before manual package replacement, then reinstalling and restarting the service. Fixes #83747. (#83757) Thanks @brokemac79.
5555
- Agents/subagents: keep collect-mode announce queues batching unresolved-origin items with compatible same-route messages and resume collection after a true cross-channel drain when a later compatible batch remains. Fixes #83577.
56+
- Skills: refresh existing session skill snapshots when watched skill roots change, so changed extra skill directories take effect without starting a new session. Fixes #83782. (#83800) Thanks @hclsys.
5657
- Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.
5758
- Control UI: render live tool progress from session-scoped `session.tool` Gateway events so externally started runs show their tool cards in the active session. (#83734) Thanks @TurboTheTurtle.
5859
- Outbound: resolve send-capable channel plugins from the active runtime registry when the pinned startup registry only has setup metadata. (#83733) Thanks @TurboTheTurtle.

src/agents/skills/refresh-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type SkillsChangeEvent = {
22
workspaceDir?: string;
3-
reason: "watch" | "manual" | "remote-node" | "config-change";
3+
reason: "watch" | "watch-targets" | "manual" | "remote-node" | "config-change";
44
changedPath?: string;
55
};
66

src/agents/skills/refresh.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,30 @@ describe("ensureSkillsWatcher", () => {
151151
]);
152152
},
153153
);
154+
155+
it("refreshes skills snapshots when watched skill roots change", () => {
156+
const seen: SkillsChangeEvent[] = [];
157+
refreshModule.registerSkillsChangeListener((change) => {
158+
seen.push(change);
159+
});
160+
refreshModule.ensureSkillsWatcher({
161+
workspaceDir: "/tmp/workspace",
162+
config: { skills: { load: { extraDirs: ["/tmp/shared-a"] } } },
163+
});
164+
165+
refreshModule.ensureSkillsWatcher({
166+
workspaceDir: "/tmp/workspace",
167+
config: { skills: { load: { extraDirs: ["/tmp/shared-b"] } } },
168+
});
169+
170+
expect(watchMock).toHaveBeenCalledTimes(2);
171+
expect(createdWatchers[0]?.close).toHaveBeenCalledTimes(1);
172+
expect(seen).toEqual([
173+
{
174+
workspaceDir: "/tmp/workspace",
175+
reason: "watch-targets",
176+
changedPath: expect.stringContaining("/tmp/shared-b"),
177+
},
178+
]);
179+
});
154180
});

src/agents/skills/refresh.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Ope
127127
if (existing && existing.pathsKey === pathsKey && existing.debounceMs === debounceMs) {
128128
return;
129129
}
130+
const watchTargetsChanged = existing ? existing.pathsKey !== pathsKey : false;
130131
if (existing) {
131132
watchers.delete(workspaceDir);
132133
if (existing.timer) {
@@ -174,6 +175,13 @@ export function ensureSkillsWatcher(params: { workspaceDir: string; config?: Ope
174175
});
175176

176177
watchers.set(workspaceDir, state);
178+
if (watchTargetsChanged) {
179+
bumpSkillsSnapshotVersion({
180+
workspaceDir,
181+
reason: "watch-targets",
182+
changedPath: pathsKey,
183+
});
184+
}
177185
}
178186

179187
export async function resetSkillsRefreshForTest(): Promise<void> {

src/auto-reply/reply/session-updates.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const {
3737
})),
3838
ensureSkillsWatcherMock: vi.fn(),
3939
getSkillsSnapshotVersionMock: vi.fn(() => 0),
40-
shouldRefreshSnapshotForVersionMock: vi.fn(() => false),
40+
shouldRefreshSnapshotForVersionMock: vi.fn((_cached?: number, _next?: number) => false),
4141
getRemoteSkillEligibilityMock: vi.fn(() => ({
4242
platforms: [],
4343
hasBin: () => false,
@@ -59,6 +59,9 @@ vi.mock("../../agents/skills.js", () => ({
5959

6060
vi.mock("../../agents/skills/refresh.js", () => ({
6161
ensureSkillsWatcher: ensureSkillsWatcherMock,
62+
}));
63+
64+
vi.mock("../../agents/skills/refresh-state.js", () => ({
6265
getSkillsSnapshotVersion: getSkillsSnapshotVersionMock,
6366
shouldRefreshSnapshotForVersion: shouldRefreshSnapshotForVersionMock,
6467
}));
@@ -197,6 +200,31 @@ describe("ensureSkillSnapshot", () => {
197200
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledTimes(2);
198201
});
199202

203+
it("reads the skills snapshot version after watcher-side invalidation", async () => {
204+
vi.stubEnv("OPENCLAW_TEST_FAST", "0");
205+
getSkillsSnapshotVersionMock.mockReturnValue(0);
206+
ensureSkillsWatcherMock.mockImplementation(() => {
207+
getSkillsSnapshotVersionMock.mockReturnValue(5);
208+
});
209+
shouldRefreshSnapshotForVersionMock.mockImplementation((cached = 0, next = 0) => cached < next);
210+
211+
await ensureSkillSnapshot({
212+
sessionEntry: testSessionEntry("sess-1", strippedSnapshot()),
213+
sessionStore: {},
214+
sessionKey: "main",
215+
isFirstTurnInSession: false,
216+
workspaceDir: TEST_WORKSPACE_DIR,
217+
cfg: { skills: { load: { extraDirs: ["/tmp/shared-skills"] } } },
218+
});
219+
220+
expect(shouldRefreshSnapshotForVersionMock).toHaveBeenCalledWith(0, 5);
221+
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledTimes(1);
222+
const [[, snapshotParams]] = buildWorkspaceSkillSnapshotMock.mock.calls as unknown as Array<
223+
[string, { snapshotVersion?: number }]
224+
>;
225+
expect(snapshotParams.snapshotVersion).toBe(5);
226+
});
227+
200228
it("invalidates cache when non-skills config gates change", async () => {
201229
vi.stubEnv("OPENCLAW_TEST_FAST", "0");
202230

src/auto-reply/reply/session-updates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,9 @@ export async function ensureSkillSnapshot(params: {
259259
agentId: sessionAgentId,
260260
}),
261261
});
262-
const snapshotVersion = getSkillsSnapshotVersion(workspaceDir);
263262
const existingSnapshot = nextEntry?.skillsSnapshot;
264263
ensureSkillsWatcher({ workspaceDir, config: cfg });
264+
const snapshotVersion = getSkillsSnapshotVersion(workspaceDir);
265265
const shouldRefreshSnapshot =
266266
shouldRefreshSnapshotForVersion(existingSnapshot?.version, snapshotVersion) ||
267267
!matchesSkillFilter(existingSnapshot?.skillFilter, skillFilter);

0 commit comments

Comments
 (0)