diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 57656c3a271..1a4d705573e 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -634,6 +634,9 @@ sessionId}` and session key context. inline runs seed those entries into the per-agent runtime scratch namespace, and disk-backed tools overlay that SQLite scratch for attachment paths. The old subagent-run attachment-dir registry columns and cleanup hooks are gone. +- CLI image hydration no longer maintains stable `openclaw-cli-images` cache + files. External CLI backends still receive file paths, but those paths are + per-run temp materializations with cleanup. - Cache-trace diagnostics, Anthropic payload diagnostics, raw model stream diagnostics, and diagnostics timeline events now write SQLite diagnostic rows instead of `logs/*.jsonl` files. diff --git a/src/agents/cli-runner.helpers.test.ts b/src/agents/cli-runner.helpers.test.ts index 187868c95ba..6de28eb2796 100644 --- a/src/agents/cli-runner.helpers.test.ts +++ b/src/agents/cli-runner.helpers.test.ts @@ -202,7 +202,7 @@ describe("buildCliArgs", () => { }); describe("writeCliImages", () => { - it("uses stable hashed file paths so repeated image hydration reuses the same path", async () => { + it("materializes images into per-run temp paths and cleans them up", async () => { const workspaceDir = await fs.mkdtemp( path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-write-images-"), ); @@ -225,12 +225,16 @@ describe("writeCliImages", () => { try { expect(first.paths).toHaveLength(1); - expect(second.paths).toEqual(first.paths); - expect(first.paths[0]).toContain(`${resolvePreferredOpenClawTmpDir()}/openclaw-cli-images/`); + expect(second.paths).toHaveLength(1); + expect(second.paths).not.toEqual(first.paths); + expect(first.paths[0]).toContain(`${resolvePreferredOpenClawTmpDir()}/openclaw-cli-images-`); expect(first.paths[0]).toMatch(/\.png$/); await expect(fs.readFile(first.paths[0])).resolves.toEqual(Buffer.from(image.data, "base64")); + await first.cleanup(); + await expect(fs.access(first.paths[0])).rejects.toMatchObject({ code: "ENOENT" }); } finally { - await fs.rm(first.paths[0], { force: true }); + await first.cleanup(); + await second.cleanup(); await fs.rm(workspaceDir, { recursive: true, force: true }); } }); @@ -254,7 +258,7 @@ describe("writeCliImages", () => { try { expect(written.paths[0]).toMatch(/\.heic$/); } finally { - await fs.rm(written.paths[0], { force: true }); + await written.cleanup(); await fs.rm(workspaceDir, { recursive: true, force: true }); } }); diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index 5172f430e18..c117c891ec3 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -8,7 +8,6 @@ import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options import type { ThinkLevel } from "../../auto-reply/thinking.js"; import type { CliBackendConfig } from "../../config/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { privateFileStore } from "../../infra/private-file-store.js"; import { tempWorkspace } from "../../infra/private-temp-workspace.js"; import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js"; import { MAX_IMAGE_BYTES } from "../../media/constants.js"; @@ -206,7 +205,7 @@ export function resolvePromptInput(params: { backend: CliBackendConfig; prompt: return { argsPrompt: params.prompt }; } -function resolveCliImagePath(image: ImageContent): string { +function resolveCliImageFileName(image: ImageContent): string { const ext = extensionForMime(image.mimeType) ?? ".bin"; const digest = crypto .createHash("sha256") @@ -214,14 +213,19 @@ function resolveCliImagePath(image: ImageContent): string { .update("\0") .update(image.data) .digest("hex"); - return path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-images", `${digest}${ext}`); + return `${digest}${ext}`; } -function resolveCliImageRoot(params: { backend: CliBackendConfig; workspaceDir: string }): string { +async function createCliImageRoot(params: { + backend: CliBackendConfig; + workspaceDir: string; +}): Promise { if (params.backend.imagePathScope === "workspace") { - return path.join(params.workspaceDir, ".openclaw-cli-images"); + const root = path.join(params.workspaceDir, ".openclaw-cli-images", crypto.randomUUID()); + await fs.mkdir(root, { recursive: true, mode: 0o700 }); + return root; } - return path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-images"); + return await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-images-")); } function appendImagePathsToPrompt(prompt: string, paths: string[], prefix = ""): string { @@ -275,23 +279,22 @@ export async function writeCliImages(params: { workspaceDir: string; images: ImageContent[]; }): Promise<{ paths: string[]; cleanup: () => Promise }> { - const imageRoot = resolveCliImageRoot({ + const imageRoot = await createCliImageRoot({ backend: params.backend, workspaceDir: params.workspaceDir, }); - await fs.mkdir(imageRoot, { recursive: true, mode: 0o700 }); - const store = privateFileStore(imageRoot); const paths: string[] = []; for (let i = 0; i < params.images.length; i += 1) { const image = params.images[i]; - const fileName = path.basename(resolveCliImagePath(image)); + const fileName = resolveCliImageFileName(image); + const filePath = path.join(imageRoot, fileName); const buffer = Buffer.from(image.data, "base64"); - await store.writeText(fileName, buffer); - paths.push(store.path(fileName)); + await fs.writeFile(filePath, buffer, { mode: 0o600 }); + paths.push(filePath); } - // Keep content-addressed image paths stable across Claude CLI runs so prompt - // text and argv don't churn on every turn with fresh temp-dir suffixes. - const cleanup = async () => {}; + const cleanup = async () => { + await fs.rm(imageRoot, { recursive: true, force: true }); + }; return { paths, cleanup }; }