diff --git a/CHANGELOG.md b/CHANGELOG.md index d659cad3668..d7fc5f0c283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - Telegram/native commands: clean up metadata-driven progress placeholders when replies fall back, edits fail, or local exec approval prompts are suppressed. (#59300) Thanks @jalehman. - Matrix: allow secret-storage recreation during automatic repair bootstrap so clients that lose their recovery key can recover and persist new cross-signing keys. (#59846) Thanks @al3mart. - Matrix/crypto persistence: capture and write the IndexedDB snapshot while holding the snapshot file lock so concurrent gateway and CLI persists cannot overwrite newer crypto state. (#59851) Thanks @al3mart. +- Telegram/media: keep inbound image attachments readable on upgraded installs where legacy state roots still differ from the managed config-dir media cache. (#59971) Thanks @neeravmakwana. ## 2026.4.2 diff --git a/src/media/local-roots.test.ts b/src/media/local-roots.test.ts index 46d4f4281c8..91467864698 100644 --- a/src/media/local-roots.test.ts +++ b/src/media/local-roots.test.ts @@ -1,7 +1,9 @@ +import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + buildMediaLocalRoots, getAgentScopedMediaLocalRoots, getAgentScopedMediaLocalRootsForSources, getDefaultMediaLocalRoots, @@ -177,4 +179,19 @@ describe("local media roots", () => { expectPicturesRootAbsent(roots, moviesDir); expect(roots.map(normalizeHostPath)).not.toContain(normalizeHostPath("/")); }); + + it("includes the config media root when legacy state and config dirs diverge", () => { + const homeRoot = path.join(os.tmpdir(), "openclaw-legacy-home-test"); + const roots = buildMediaLocalRoots( + path.join(homeRoot, ".clawdbot"), + path.join(homeRoot, ".openclaw"), + ); + + expectNormalizedRootsContain(roots, [ + path.join(homeRoot, ".clawdbot", "media"), + path.join(homeRoot, ".clawdbot", "workspace"), + path.join(homeRoot, ".clawdbot", "sandboxes"), + path.join(homeRoot, ".openclaw", "media"), + ]); + }); }); diff --git a/src/media/local-roots.ts b/src/media/local-roots.ts index 1b5e1bbe993..3d7c8f77388 100644 --- a/src/media/local-roots.ts +++ b/src/media/local-roots.ts @@ -3,6 +3,7 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +import { resolveConfigDir } from "../utils.js"; type BuildMediaLocalRootsOptions = { preferredTmpDir?: string; @@ -17,29 +18,38 @@ function resolveCachedPreferredTmpDir(): string { return cachedPreferredTmpDir; } -function buildMediaLocalRoots( +export function buildMediaLocalRoots( stateDir: string, + configDir: string, options: BuildMediaLocalRootsOptions = {}, ): string[] { const resolvedStateDir = path.resolve(stateDir); + const resolvedConfigDir = path.resolve(configDir); const preferredTmpDir = options.preferredTmpDir ?? resolveCachedPreferredTmpDir(); - return [ - preferredTmpDir, - path.join(resolvedStateDir, "media"), - path.join(resolvedStateDir, "workspace"), - path.join(resolvedStateDir, "sandboxes"), - ]; + return Array.from( + new Set([ + preferredTmpDir, + path.join(resolvedStateDir, "media"), + path.join(resolvedStateDir, "workspace"), + path.join(resolvedStateDir, "sandboxes"), + // Upgraded installs can still resolve the active state dir to the legacy + // ~/.clawdbot tree while new media writes already go under ~/.openclaw/media. + // Keep inbound media readable across that split without widening roots beyond + // the managed media cache. + path.join(resolvedConfigDir, "media"), + ]), + ); } export function getDefaultMediaLocalRoots(): readonly string[] { - return buildMediaLocalRoots(resolveStateDir()); + return buildMediaLocalRoots(resolveStateDir(), resolveConfigDir()); } export function getAgentScopedMediaLocalRoots( cfg: OpenClawConfig, agentId?: string, ): readonly string[] { - const roots = buildMediaLocalRoots(resolveStateDir()); + const roots = buildMediaLocalRoots(resolveStateDir(), resolveConfigDir()); if (!agentId?.trim()) { return roots; }