Skip to content

Commit 569e75f

Browse files
committed
fix(agents): sweep stale CLI image files
Fixes #98946
1 parent d362073 commit 569e75f

2 files changed

Lines changed: 87 additions & 0 deletions

File tree

src/agents/cli-runner.helpers.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,42 @@ describe("writeCliImages", () => {
287287
}
288288
});
289289

290+
it("sweeps stale workspace-scoped CLI image files", async () => {
291+
const workspaceDir = await fs.mkdtemp(
292+
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-write-sweep-"),
293+
);
294+
const imageRoot = path.join(workspaceDir, ".openclaw-cli-images");
295+
const stalePath = path.join(imageRoot, "stale.png");
296+
const freshPath = path.join(imageRoot, "fresh.png");
297+
const image: ImageContent = {
298+
type: "image",
299+
data: "bmV3LWltYWdl",
300+
mimeType: "image/png",
301+
};
302+
303+
await fs.mkdir(imageRoot, { recursive: true });
304+
await fs.writeFile(stalePath, "stale");
305+
await fs.writeFile(freshPath, "fresh");
306+
const staleTime = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000);
307+
await fs.utimes(stalePath, staleTime, staleTime);
308+
309+
const written = await writeCliImages({
310+
backend: { command: "gemini", imagePathScope: "workspace" },
311+
workspaceDir,
312+
images: [image],
313+
});
314+
315+
try {
316+
await expect(fs.access(stalePath)).rejects.toMatchObject({ code: "ENOENT" });
317+
await expect(fs.readFile(freshPath, "utf-8")).resolves.toBe("fresh");
318+
await expect(fs.readFile(written.paths[0])).resolves.toEqual(
319+
Buffer.from(image.data, "base64"),
320+
);
321+
} finally {
322+
await fs.rm(workspaceDir, { recursive: true, force: true });
323+
}
324+
});
325+
290326
it("hydrates prompt media refs into codex image args through the helper seams", async () => {
291327
const tempDir = await fs.mkdtemp(
292328
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-prompt-image-"),

src/agents/cli-runner/helpers.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { buildConfiguredAgentSystemPrompt } from "../system-prompt-config.js";
3636
import { buildSystemPromptParams } from "../system-prompt-params.js";
3737
import type { SilentReplyPromptMode } from "../system-prompt.types.js";
3838
import { sanitizeImageBlocks } from "../tool-images.js";
39+
import { cliBackendLog } from "./log.js";
3940
import { formatTomlConfigOverride } from "./toml-inline.js";
4041
/** Re-export CLI reliability helpers used by older runner call sites. */
4142
export {
@@ -45,6 +46,8 @@ export {
4546
} from "./reliability.js";
4647

4748
const CLI_RUN_QUEUE = new KeyedAsyncQueue();
49+
const CLI_IMAGE_SWEEP_TTL_MS = 7 * 24 * 60 * 60 * 1_000;
50+
const sweptCliImageRoots = new Set<string>();
4851

4952
function isClaudeCliProvider(providerId: string): boolean {
5053
return normalizeOptionalLowercaseString(providerId) === "claude-cli";
@@ -295,6 +298,53 @@ function resolveCliImageRoot(params: { backend: CliBackendConfig; workspaceDir:
295298
return path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-images");
296299
}
297300

301+
function isFileNotFoundError(error: unknown): boolean {
302+
return Boolean(
303+
error &&
304+
typeof error === "object" &&
305+
"code" in error &&
306+
(error as { code?: unknown }).code === "ENOENT",
307+
);
308+
}
309+
310+
async function sweepCliImageRoot(imageRoot: string): Promise<void> {
311+
if (sweptCliImageRoots.has(imageRoot)) {
312+
return;
313+
}
314+
sweptCliImageRoots.add(imageRoot);
315+
try {
316+
const cutoffMs = Date.now() - CLI_IMAGE_SWEEP_TTL_MS;
317+
const entries = await fs.readdir(imageRoot, { withFileTypes: true });
318+
for (const entry of entries) {
319+
if (!entry.isFile()) {
320+
continue;
321+
}
322+
const entryPath = path.join(imageRoot, entry.name);
323+
const stat = await fs.stat(entryPath).catch((error: unknown) => {
324+
if (isFileNotFoundError(error)) {
325+
return undefined;
326+
}
327+
throw error;
328+
});
329+
if (!stat) {
330+
continue;
331+
}
332+
if (stat.mtimeMs >= cutoffMs) {
333+
continue;
334+
}
335+
try {
336+
await fs.rm(entryPath, { force: true });
337+
} catch (error) {
338+
if (!isFileNotFoundError(error)) {
339+
throw error;
340+
}
341+
}
342+
}
343+
} catch (error) {
344+
cliBackendLog.debug(`cli image cache sweep failed: ${String(error)}`);
345+
}
346+
}
347+
298348
function appendImagePathsToPrompt(prompt: string, paths: string[], prefix = ""): string {
299349
if (!paths.length) {
300350
return prompt;
@@ -353,6 +403,7 @@ export async function writeCliImages(params: {
353403
workspaceDir: params.workspaceDir,
354404
});
355405
await fs.mkdir(imageRoot, { recursive: true, mode: 0o700 });
406+
await sweepCliImageRoot(imageRoot);
356407
const store = privateFileStore(imageRoot);
357408
const paths: string[] = [];
358409
for (const image of params.images) {

0 commit comments

Comments
 (0)