From 94fdee2eac0703882cf7e6554dd44660cbe8e29b Mon Sep 17 00:00:00 2001 From: zerone0x Date: Fri, 6 Mar 2026 10:15:13 +0800 Subject: [PATCH] fix(memory-flush): ban timestamped variant files in default flush prompt (#34951) Merged via squash. Prepared head SHA: efadda4988b460e6da07be72994d4951d64239d0 Co-authored-by: zerone0x <39543393+zerone0x@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman --- CHANGELOG.md | 1 + src/auto-reply/reply/memory-flush.test.ts | 15 ++++++++++++++- src/auto-reply/reply/memory-flush.ts | 3 ++- src/cli/daemon-cli/install.test.ts | 10 +++++++++- src/infra/process-respawn.test.ts | 1 + 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa27b86e3..902e6acb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,7 @@ Docs: https://docs.openclaw.ai - TUI/webchat command-owner scope alignment: treat internal-channel gateway sessions with `operator.admin` as owner-authorized in command auth, restoring cron/gateway/connector tool access for affected TUI/webchat sessions while keeping external channels on identity-based owner checks. (from #35666, #35673, #35704) Thanks @Naylenv, @Octane0411, and @Sid-Qin. - Discord/inbound timeout isolation: separate inbound worker timeout tracking from listener timeout budgets so queued Discord replies are no longer dropped when listener watchdog windows expire mid-run. (#36602) Thanks @dutifulbob. - Memory/doctor SecretRef handling: treat SecretRef-backed memory-search API keys as configured, and fail embedding setup with explicit unresolved-secret errors instead of crashing. (#36835) Thanks @joshavant. +- Memory/flush default prompt: ban timestamped variant filenames during default memory flush runs so durable notes stay in the canonical daily `memory/YYYY-MM-DD.md` file. (#34951) thanks @zerone0x. ## 2026.3.2 diff --git a/src/auto-reply/reply/memory-flush.test.ts b/src/auto-reply/reply/memory-flush.test.ts index e444b9e7a..0e04e7e0e 100644 --- a/src/auto-reply/reply/memory-flush.test.ts +++ b/src/auto-reply/reply/memory-flush.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -import { resolveMemoryFlushPromptForRun } from "./memory-flush.js"; +import { DEFAULT_MEMORY_FLUSH_PROMPT, resolveMemoryFlushPromptForRun } from "./memory-flush.js"; describe("resolveMemoryFlushPromptForRun", () => { const cfg = { @@ -36,3 +36,16 @@ describe("resolveMemoryFlushPromptForRun", () => { expect((prompt.match(/Current time:/g) ?? []).length).toBe(1); }); }); + +describe("DEFAULT_MEMORY_FLUSH_PROMPT", () => { + it("includes append-only instruction to prevent overwrites (#6877)", () => { + expect(DEFAULT_MEMORY_FLUSH_PROMPT).toMatch(/APPEND/i); + expect(DEFAULT_MEMORY_FLUSH_PROMPT).toContain("do not overwrite"); + }); + + it("includes anti-fragmentation instruction to prevent timestamped variant files (#34919)", () => { + // Agents must not create YYYY-MM-DD-HHMM.md variants alongside the canonical file + expect(DEFAULT_MEMORY_FLUSH_PROMPT).toContain("timestamped variant"); + expect(DEFAULT_MEMORY_FLUSH_PROMPT).toContain("YYYY-MM-DD.md"); + }); +}); diff --git a/src/auto-reply/reply/memory-flush.ts b/src/auto-reply/reply/memory-flush.ts index e23703c7b..c02fad5ec 100644 --- a/src/auto-reply/reply/memory-flush.ts +++ b/src/auto-reply/reply/memory-flush.ts @@ -13,7 +13,8 @@ export const DEFAULT_MEMORY_FLUSH_FORCE_TRANSCRIPT_BYTES = 2 * 1024 * 1024; export const DEFAULT_MEMORY_FLUSH_PROMPT = [ "Pre-compaction memory flush.", "Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed).", - "IMPORTANT: If the file already exists, APPEND new content only and do not overwrite existing entries.", + "IMPORTANT: If the file already exists, APPEND new content only — do not overwrite existing entries.", + "Do NOT create timestamped variant files (e.g., YYYY-MM-DD-HHMM.md); always use the canonical YYYY-MM-DD.md filename.", `If nothing to store, reply with ${SILENT_REPLY_TOKEN}.`, ].join(" "); diff --git a/src/cli/daemon-cli/install.test.ts b/src/cli/daemon-cli/install.test.ts index bc488c3ac..cd03bddbe 100644 --- a/src/cli/daemon-cli/install.test.ts +++ b/src/cli/daemon-cli/install.test.ts @@ -1,4 +1,5 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { captureFullEnv } from "../../test-utils/env.js"; import type { DaemonActionResponse } from "./response.js"; const loadConfigMock = vi.hoisted(() => vi.fn()); @@ -118,6 +119,7 @@ vi.mock("../../runtime.js", () => ({ })); const { runDaemonInstall } = await import("./install.js"); +const envSnapshot = captureFullEnv(); describe("runDaemonInstall", () => { beforeEach(() => { @@ -162,6 +164,12 @@ describe("runDaemonInstall", () => { isGatewayDaemonRuntimeMock.mockReturnValue(true); installDaemonServiceAndEmitMock.mockResolvedValue(undefined); service.isLoaded.mockResolvedValue(false); + delete process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.CLAWDBOT_GATEWAY_TOKEN; + }); + + afterEach(() => { + envSnapshot.restore(); }); it("fails install when token auth requires an unresolved token SecretRef", async () => { diff --git a/src/infra/process-respawn.test.ts b/src/infra/process-respawn.test.ts index 188b942eb..06591711c 100644 --- a/src/infra/process-respawn.test.ts +++ b/src/infra/process-respawn.test.ts @@ -68,6 +68,7 @@ describe("restartGatewayProcessWithFreshPid", () => { }); it("returns supervised when launchd/systemd hints are present", () => { + clearSupervisorHints(); process.env.LAUNCH_JOB_LABEL = "ai.openclaw.gateway"; const result = restartGatewayProcessWithFreshPid(); expect(result.mode).toBe("supervised");