diff --git a/CHANGELOG.md b/CHANGELOG.md index ead6682da12..506d9cd7c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: compare post-compaction token sanity checks against full-session pre-compaction totals and skip the check when token estimation fails, so sessions with large bootstrap context keep real token counts instead of falling back to unknown. (#28347) thanks @efe-arv. - Discord/gateway startup: treat plain-text and transient `/gateway/bot` metadata fetch failures as transient startup errors so Discord gateway boot no longer crashes on unhandled rejections. (#44397) Thanks @jalehman. - Gateway/session reset: preserve `lastAccountId` and `lastThreadId` across gateway session resets so replies keep routing back to the same account and thread after `/reset`. (#44773) Thanks @Lanfei. +- Agents/memory bootstrap: load only one root memory file, preferring `MEMORY.md` and using `memory.md` as a fallback, so case-insensitive Docker mounts no longer inject duplicate memory context. (#26054) Thanks @Lanfei. ## 2026.3.12 diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 1a5edfcc6e3..a1d1b482fb2 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -59,7 +59,7 @@ Bootstrap files are trimmed and appended under **Project Context** so the model - `USER.md` - `HEARTBEAT.md` - `BOOTSTRAP.md` (only on brand-new workspaces) -- `MEMORY.md` and/or `memory.md` (when present in the workspace; either or both may be injected) +- `MEMORY.md` when present, otherwise `memory.md` as a lowercase fallback All of these files are **injected into the context window** on every turn, which means they consume tokens. Keep them concise — especially `MEMORY.md`, which can diff --git a/docs/reference/token-use.md b/docs/reference/token-use.md index 9e85c25e687..8493e99f098 100644 --- a/docs/reference/token-use.md +++ b/docs/reference/token-use.md @@ -18,7 +18,7 @@ OpenClaw assembles its own system prompt on every run. It includes: - Tool list + short descriptions - Skills list (only metadata; instructions are loaded on demand with `read`) - Self-update instructions -- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` and/or `memory.md` when present). Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 20000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 150000). `memory/*.md` files are on-demand via memory tools and are not auto-injected. +- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new, plus `MEMORY.md` when present or `memory.md` as a lowercase fallback). Large files are truncated by `agents.defaults.bootstrapMaxChars` (default: 20000), and total bootstrap injection is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 150000). `memory/*.md` files are on-demand via memory tools and are not auto-injected. - Time (UTC + user timezone) - Reply tags + heartbeat behavior - Runtime metadata (host/OS/model/thinking) diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 830b44504ad..c4f1044a8d9 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -458,41 +458,24 @@ export async function ensureAgentWorkspace(params?: { }; } -async function resolveMemoryBootstrapEntries( +async function resolveMemoryBootstrapEntry( resolvedDir: string, -): Promise> { - const candidates: WorkspaceBootstrapFileName[] = [ - DEFAULT_MEMORY_FILENAME, - DEFAULT_MEMORY_ALT_FILENAME, - ]; - const entries: Array<{ name: WorkspaceBootstrapFileName; filePath: string }> = []; - for (const name of candidates) { +): Promise<{ name: WorkspaceBootstrapFileName; filePath: string } | null> { + // Prefer MEMORY.md; fall back to memory.md only when absent. + // Checking both and deduplicating via realpath is unreliable on case-insensitive + // file systems mounted in Docker (e.g. macOS volumes), where both names pass + // fs.access() but realpath does not normalise case through the mount layer, + // causing the same content to be injected twice and wasting tokens. + for (const name of [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME] as const) { const filePath = path.join(resolvedDir, name); try { await fs.access(filePath); - entries.push({ name, filePath }); + return { name, filePath }; } catch { - // optional + // try next candidate } } - if (entries.length <= 1) { - return entries; - } - - const seen = new Set(); - const deduped: Array<{ name: WorkspaceBootstrapFileName; filePath: string }> = []; - for (const entry of entries) { - let key = entry.filePath; - try { - key = await fs.realpath(entry.filePath); - } catch {} - if (seen.has(key)) { - continue; - } - seen.add(key); - deduped.push(entry); - } - return deduped; + return null; } export async function loadWorkspaceBootstrapFiles(dir: string): Promise { @@ -532,7 +515,10 @@ export async function loadWorkspaceBootstrapFiles(dir: string): Promise