From 277d8fece2e3d0f1cd2f99e6f6276e189c270fc0 Mon Sep 17 00:00:00 2001 From: Galin Iliev Date: Mon, 25 May 2026 07:42:57 -0700 Subject: [PATCH] fix: quiet missing daily memory reads Closes #82928 --- CHANGELOG.md | 1 + src/agents/pi-tools.read.ts | 39 +++++++++++++++++++ .../pi-tools.workspace-only-false.test.ts | 34 ++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd57cdec5a..32f6fab262c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates. +- Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run `ENOENT` failures. Fixes #82928. Thanks @galiniliev. - Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session. - Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw. - Agents/OpenAI-compatible: stop ModelStudio-compatible chat requests before sending system/tool-only payloads that have no usable user or assistant turn. (#86177) Thanks @TurboTheTurtle. diff --git a/src/agents/pi-tools.read.ts b/src/agents/pi-tools.read.ts index 73a7d9101db..0071b704316 100644 --- a/src/agents/pi-tools.read.ts +++ b/src/agents/pi-tools.read.ts @@ -60,6 +60,7 @@ type ReadTruncationDetails = { const OFFSET_BEYOND_EOF_RE = /^Offset \d+ is beyond end of file \(\d+ lines total\)$/; const READ_CONTINUATION_NOTICE_RE = /\n\n\[(?:Showing lines [^\]]*?Use offset=\d+ to continue\.|\d+ more lines in file\. Use offset=\d+ to continue\.)\]\s*$/; +const DAILY_MEMORY_PATH_RE = /^memory\/\d{4}-\d{2}-\d{2}\.md$/; function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); @@ -218,6 +219,40 @@ function emptyReadResult(): AgentToolResult { return { content: [textBlock], details: undefined }; } +function missingDailyMemoryReadResult(relativePath: string): AgentToolResult { + return { + content: [ + { + type: "text", + text: `No daily memory file exists yet at ${relativePath}.`, + }, + ], + details: { + status: "not_found", + path: relativePath, + optional: true, + }, + }; +} + +function normalizeDailyMemoryReadPath(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const normalized = value.trim().replace(/\\/g, "/").replace(/^\.\/+/, ""); + return DAILY_MEMORY_PATH_RE.test(normalized) ? normalized : undefined; +} + +function isNotFoundError(error: unknown): boolean { + if (typeof (error as NodeJS.ErrnoException | undefined)?.code === "string") { + return (error as NodeJS.ErrnoException).code === "ENOENT"; + } + if (!(error instanceof Error)) { + return false; + } + return /\bENOENT\b|no such file or directory|file not found/i.test(error.message); +} + async function executeReadPage(params: { base: AnyAgentTool; toolCallId: string; @@ -230,6 +265,10 @@ async function executeReadPage(params: { if (isOffsetBeyondEof(error, params.args)) { return emptyReadResult(); } + const missingDailyMemoryPath = normalizeDailyMemoryReadPath(params.args.path); + if (missingDailyMemoryPath && isNotFoundError(error)) { + return missingDailyMemoryReadResult(missingDailyMemoryPath); + } throw error; } } diff --git a/src/agents/pi-tools.workspace-only-false.test.ts b/src/agents/pi-tools.workspace-only-false.test.ts index 59d532b42b9..65320e4bd2a 100644 --- a/src/agents/pi-tools.workspace-only-false.test.ts +++ b/src/agents/pi-tools.workspace-only-false.test.ts @@ -165,6 +165,40 @@ describe("FS tools with workspaceOnly=false", () => { expect(JSON.stringify(result.content)).toContain("test read content"); }); + it("returns optional not-found context for missing date-only daily memory reads", async () => { + const result = await runFsTool( + "read", + "test-call-missing-daily-memory", + { + path: "memory/2026-05-15.md", + }, + undefined, + ); + expect(result).toStrictEqual({ + content: [ + { + type: "text", + text: "No daily memory file exists yet at memory/2026-05-15.md.", + }, + ], + details: { + status: "not_found", + path: "memory/2026-05-15.md", + optional: true, + }, + }); + }); + + it("still throws for ordinary missing read paths", async () => { + const readTool = requireTool(toolsFor(undefined), "read"); + + await expect( + readTool.execute("test-call-missing-ordinary-file", { + path: "notes/missing.md", + }), + ).rejects.toThrow(/ENOENT|no such file|not found/i); + }); + it("should allow write outside workspace when workspaceOnly is unset", async () => { const outsideUnsetFile = path.join(tmpDir, "outside-unset-write.txt"); await runFsTool(