From 7e18c07e410c470cc9dd69dcae52481103e6e268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Cuevas?= Date: Fri, 17 Apr 2026 01:24:15 -0400 Subject: [PATCH] fix: dedupe repeated bootstrap truncation warnings (#67906) (thanks @rubencu) * Agents: dedupe bootstrap truncation warnings * Agents: normalize bootstrap warning cache bookkeeping * fix(agents): scope bootstrap warning dedupe by workspace * refactor(agents): simplify bootstrap warning wrapper --------- Co-authored-by: Ayaan Zaidi --- src/agents/bootstrap-files.test.ts | 65 ++++++++++++++++++++ src/agents/bootstrap-files.ts | 36 ++++++++++- src/agents/cli-runner/prepare.ts | 1 + src/agents/pi-embedded-runner/compact.ts | 1 + src/agents/pi-embedded-runner/run/attempt.ts | 6 +- 5 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/agents/bootstrap-files.test.ts b/src/agents/bootstrap-files.test.ts index 720f2ca5799..711c4771939 100644 --- a/src/agents/bootstrap-files.test.ts +++ b/src/agents/bootstrap-files.test.ts @@ -8,8 +8,10 @@ import { } from "../hooks/internal-hooks.js"; import { makeTempWorkspace } from "../test-helpers/workspace.js"; import { + _resetBootstrapWarningCacheForTest, FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE, hasCompletedBootstrapTurn, + makeBootstrapWarn, resolveBootstrapContextForRun, resolveBootstrapFilesForRun, resolveContextInjectionMode, @@ -362,6 +364,69 @@ describe("hasCompletedBootstrapTurn", () => { }); }); +describe("makeBootstrapWarn", () => { + afterEach(() => { + _resetBootstrapWarningCacheForTest(); + }); + + it("deduplicates repeated warnings for the same session and message", () => { + const warnings: string[] = []; + const warn = makeBootstrapWarn({ + sessionLabel: "agent:main:test-session", + warn: (message) => warnings.push(message), + }); + + warn?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + warn?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + + expect(warnings).toEqual([ + "workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:test-session)", + ]); + }); + + it("keeps warnings distinct across sessions", () => { + const warnings: string[] = []; + const first = makeBootstrapWarn({ + sessionLabel: "agent:main:first-session", + warn: (message) => warnings.push(message), + }); + const second = makeBootstrapWarn({ + sessionLabel: "agent:main:second-session", + warn: (message) => warnings.push(message), + }); + + first?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + second?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + + expect(warnings).toEqual([ + "workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:first-session)", + "workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:second-session)", + ]); + }); + + it("keeps warnings distinct across workspaces with the same session", () => { + const warnings: string[] = []; + const first = makeBootstrapWarn({ + sessionLabel: "agent:main:shared-session", + workspaceDir: "/tmp/workspace-a", + warn: (message) => warnings.push(message), + }); + const second = makeBootstrapWarn({ + sessionLabel: "agent:main:shared-session", + workspaceDir: "/tmp/workspace-b", + warn: (message) => warnings.push(message), + }); + + first?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + second?.("workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating"); + + expect(warnings).toEqual([ + "workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:shared-session)", + "workspace bootstrap file MEMORY.md is 36697 chars (limit 12000); truncating (sessionKey=agent:main:shared-session)", + ]); + }); +}); + describe("resolveContextInjectionMode", () => { it("defaults to always when config is missing", () => { expect(resolveContextInjectionMode(undefined)).toBe("always"); diff --git a/src/agents/bootstrap-files.ts b/src/agents/bootstrap-files.ts index aa77c2bec2b..5a3b7c8abe4 100644 --- a/src/agents/bootstrap-files.ts +++ b/src/agents/bootstrap-files.ts @@ -25,6 +25,29 @@ export type BootstrapContextRunKind = "default" | "heartbeat" | "cron"; const CONTINUATION_SCAN_MAX_TAIL_BYTES = 256 * 1024; const CONTINUATION_SCAN_MAX_RECORDS = 500; export const FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE = "openclaw:bootstrap-context:full"; +const BOOTSTRAP_WARNING_DEDUPE_LIMIT = 1024; +const seenBootstrapWarnings = new Set(); +const bootstrapWarningOrder: string[] = []; + +function rememberBootstrapWarning(key: string): boolean { + if (seenBootstrapWarnings.has(key)) { + return false; + } + if (seenBootstrapWarnings.size >= BOOTSTRAP_WARNING_DEDUPE_LIMIT) { + const oldest = bootstrapWarningOrder.shift(); + if (oldest) { + seenBootstrapWarnings.delete(oldest); + } + } + seenBootstrapWarnings.add(key); + bootstrapWarningOrder.push(key); + return true; +} + +export function _resetBootstrapWarningCacheForTest(): void { + seenBootstrapWarnings.clear(); + bootstrapWarningOrder.length = 0; +} export function resolveContextInjectionMode(config?: OpenClawConfig): AgentContextInjection { return config?.agents?.defaults?.contextInjection ?? "always"; @@ -103,12 +126,21 @@ export async function hasCompletedBootstrapTurn(sessionFile: string): Promise void; }): ((message: string) => void) | undefined { - if (!params.warn) { + const warn = params.warn; + if (!warn) { return undefined; } - return (message: string) => params.warn?.(`${message} (sessionKey=${params.sessionLabel})`); + const workspacePrefix = params.workspaceDir ?? ""; + return (message: string) => { + const key = `${workspacePrefix}\u0000${params.sessionLabel}\u0000${message}`; + if (!rememberBootstrapWarning(key)) { + return; + } + warn(`${message} (sessionKey=${params.sessionLabel})`); + }; } function sanitizeBootstrapFiles( diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index e5d83004656..98e6b01cb99 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -91,6 +91,7 @@ export async function prepareCliRunContext( sessionId: params.sessionId, warn: prepareDeps.makeBootstrapWarn({ sessionLabel, + workspaceDir, warn: (message) => cliBackendLog.warn(message), }), }); diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index 25cbaf2272b..ccf980e0d7b 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -465,6 +465,7 @@ export async function compactEmbeddedPiSessionDirect( sessionId: params.sessionId, warn: makeBootstrapWarn({ sessionLabel, + workspaceDir: effectiveWorkspace, warn: (message) => log.warn(message), }), }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 150b8a54c52..37ac21a1b08 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -464,7 +464,11 @@ export async function runEmbeddedAttempt( config: params.config, sessionKey: params.sessionKey, sessionId: params.sessionId, - warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }), + warn: makeBootstrapWarn({ + sessionLabel, + workspaceDir: effectiveWorkspace, + warn: (message) => log.warn(message), + }), contextMode: params.bootstrapContextMode, runKind: params.bootstrapContextRunKind, }),