Skip to content

Commit 0c8c176

Browse files
giodl73-repoCopilot
andcommitted
fix(doctor): scan raw snapshot paths
Expand home-relative cached snapshot paths before stale bundled-skill classification and scan raw session-store JSON so persisted resolvedSkills are inspected before normal session-store normalization strips them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fa62bd1 commit 0c8c176

2 files changed

Lines changed: 98 additions & 6 deletions

File tree

src/commands/doctor-session-snapshots.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ describe("doctor session snapshot stale runtime metadata", () => {
113113
]);
114114
});
115115

116+
it("expands home-relative cached bundled skill locations before classifying them", () => {
117+
const homeDir = path.join(root, "home");
118+
const stalePath = "~/old-runtime/node_modules/openclaw/skills/doctor/SKILL.md";
119+
120+
const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
121+
bundledSkillsDir,
122+
env: { HOME: homeDir },
123+
store: {
124+
"agent:home": sessionEntry({
125+
skillsSnapshot: {
126+
prompt: skillPrompt(stalePath),
127+
skills: [{ name: "doctor" }],
128+
},
129+
}),
130+
},
131+
});
132+
133+
expect(findings).toEqual([
134+
{
135+
sessionKey: "agent:home",
136+
field: "skillsSnapshot.prompt",
137+
cachedPath: stalePath,
138+
expectedPath: path.join(bundledSkillsDir, "doctor", "SKILL.md"),
139+
},
140+
]);
141+
});
142+
116143
it("ignores current bundled locations and unrelated workspace skill locations", () => {
117144
const currentPath = path.join(bundledSkillsDir, "doctor", "SKILL.md");
118145
const workspacePath = path.join(root, "workspace", "skills", "doctor", "SKILL.md");
@@ -223,6 +250,52 @@ describe("doctor session snapshot stale runtime metadata", () => {
223250
expect(message).toContain(path.join(bundledSkillsDir, "doctor", "SKILL.md"));
224251
});
225252

253+
it("scans resolvedSkills before session store normalization strips them", async () => {
254+
const stalePath = path.join(
255+
root,
256+
"old-runtime",
257+
"node_modules",
258+
"openclaw",
259+
"skills",
260+
"doctor",
261+
"SKILL.md",
262+
);
263+
const storePath = path.join(root, "state", "agents", "main", "sessions", "sessions.json");
264+
await writeSessionStore(storePath, {
265+
"agent:main": sessionEntry({
266+
skillsSnapshot: {
267+
prompt: "",
268+
skills: [{ name: "doctor" }],
269+
resolvedSkills: [
270+
{
271+
name: "doctor",
272+
description: "Doctor skill",
273+
source: "bundled",
274+
filePath: stalePath,
275+
baseDir: path.dirname(stalePath),
276+
sourceInfo: {
277+
path: stalePath,
278+
source: "bundled",
279+
scope: "user",
280+
origin: "top-level",
281+
baseDir: path.dirname(stalePath),
282+
},
283+
disableModelInvocation: false,
284+
},
285+
],
286+
},
287+
}),
288+
});
289+
290+
await noteSessionSnapshotHealth({ storePaths: [storePath], bundledSkillsDir });
291+
292+
expect(note).toHaveBeenCalledTimes(1);
293+
const [message] = note.mock.calls[0] as [string, string];
294+
expect(message).toContain("agent:main");
295+
expect(message).toContain("skillsSnapshot.resolvedSkills");
296+
expect(message).toContain(stalePath);
297+
});
298+
226299
it("reports stale cached metadata from configured session stores", async () => {
227300
const stalePath = path.join(
228301
root,

src/commands/doctor-session-snapshots.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import fs from "node:fs";
22
import path from "node:path";
33
import { resolveBundledSkillsDir } from "../agents/skills/bundled-dir.js";
44
import { resolveStateDir } from "../config/paths.js";
5-
import { loadSessionStore } from "../config/sessions/store-load.js";
65
import { resolveAllAgentSessionStoreTargetsSync } from "../config/sessions/targets.js";
76
import type { SessionEntry } from "../config/sessions/types.js";
87
import type { OpenClawConfig } from "../config/types.openclaw.js";
8+
import { expandHomePrefix } from "../infra/home-dir.js";
99
import { note } from "../terminal/note.js";
1010
import { shortenHomePath } from "../utils.js";
1111

@@ -168,14 +168,20 @@ function resolveExpectedBundledSkillPath(params: {
168168
cachedPath: string;
169169
bundledSkillsDir: string;
170170
pathExists: (filePath: string) => boolean;
171+
homeDir?: string;
172+
env?: NodeJS.ProcessEnv;
171173
}): string | undefined {
172-
if (!isAbsolutePathLike(params.cachedPath)) {
174+
const expandedCachedPath = expandHomePrefix(params.cachedPath, {
175+
home: params.homeDir,
176+
env: params.env,
177+
});
178+
if (!isAbsolutePathLike(expandedCachedPath)) {
173179
return undefined;
174180
}
175-
if (isInsidePath(params.bundledSkillsDir, params.cachedPath)) {
181+
if (isInsidePath(params.bundledSkillsDir, expandedCachedPath)) {
176182
return undefined;
177183
}
178-
const relativeSegments = extractBundledSkillRelativeSegments(params.cachedPath);
184+
const relativeSegments = extractBundledSkillRelativeSegments(expandedCachedPath);
179185
if (!relativeSegments) {
180186
return undefined;
181187
}
@@ -187,6 +193,8 @@ export function scanSessionStoreForStaleRuntimeSnapshotPaths(params: {
187193
store: Record<string, SessionEntry>;
188194
bundledSkillsDir: string | undefined;
189195
pathExists?: (filePath: string) => boolean;
196+
homeDir?: string;
197+
env?: NodeJS.ProcessEnv;
190198
}): StaleSessionSnapshotPathFinding[] {
191199
const bundledSkillsDir = params.bundledSkillsDir?.trim();
192200
if (!bundledSkillsDir) {
@@ -204,6 +212,8 @@ export function scanSessionStoreForStaleRuntimeSnapshotPaths(params: {
204212
cachedPath: cached.path,
205213
bundledSkillsDir,
206214
pathExists,
215+
homeDir: params.homeDir,
216+
env: params.env,
207217
});
208218
if (!expectedPath) {
209219
continue;
@@ -252,6 +262,11 @@ function resolveSessionStorePaths(params: {
252262
.toSorted((a, b) => a.localeCompare(b));
253263
}
254264

265+
function loadSessionStoreForSnapshotScan(storePath: string): Record<string, SessionEntry> {
266+
const parsed = JSON.parse(fs.readFileSync(storePath, "utf-8")) as unknown;
267+
return isRecord(parsed) ? (parsed as Record<string, SessionEntry>) : {};
268+
}
269+
255270
export async function noteSessionSnapshotHealth(params?: {
256271
storePaths?: string[];
257272
bundledSkillsDir?: string;
@@ -270,15 +285,19 @@ export async function noteSessionSnapshotHealth(params?: {
270285
for (const storePath of storePaths) {
271286
let store: Record<string, SessionEntry>;
272287
try {
273-
store = loadSessionStore(storePath);
288+
store = loadSessionStoreForSnapshotScan(storePath);
274289
} catch (err) {
275290
note(
276291
`- Failed to inspect session snapshot metadata in ${shortenHomePath(storePath)}: ${String(err)}`,
277292
"Session snapshots",
278293
);
279294
continue;
280295
}
281-
const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({ store, bundledSkillsDir });
296+
const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
297+
store,
298+
bundledSkillsDir,
299+
env: params?.env,
300+
});
282301
if (findings.length > 0) {
283302
findingsByStore.set(storePath, findings);
284303
}

0 commit comments

Comments
 (0)