From 8a67016646a01912a96b1f0ea555e8fa121508db Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 16 Feb 2026 12:04:53 -0500 Subject: [PATCH] Agents: raise bootstrap total cap and warn on /context truncation (#18229) Merged via /review-pr -> /prepare-pr -> /merge-pr. Prepared head SHA: f6620526df231b571a8821edf9fc5f76c3994583 Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + docs/concepts/agent-workspace.md | 3 +- docs/concepts/context.md | 2 +- docs/concepts/system-prompt.md | 2 +- docs/gateway/configuration-reference.md | 4 +- docs/reference/token-use.md | 2 +- ...ers.buildbootstrapcontextfiles.e2e.test.ts | 15 +++- src/agents/pi-embedded-helpers/bootstrap.ts | 2 +- src/agents/pi-embedded-runner/run/attempt.ts | 2 + src/agents/system-prompt-report.test.ts | 37 +++++++++ src/agents/system-prompt-report.ts | 6 +- .../reply/commands-context-report.test.ts | 79 +++++++++++++++++++ .../reply/commands-context-report.ts | 42 +++++++++- src/config/schema.help.ts | 2 +- src/config/sessions/types.ts | 1 + src/config/types.agent-defaults.ts | 2 +- 16 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 src/auto-reply/reply/commands-context-report.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b4674769b6..537401a26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope. - CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091) - CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky. +- Agents/Context: raise default total bootstrap prompt cap from `24000` to `150000` chars (keeping `bootstrapMaxChars` at `20000`), include total-cap visibility in `/context`, and mark truncation from injected-vs-raw sizes so total-cap clipping is reflected accurately. - OpenClawKit/iOS ChatUI: accept canonical session-key completion events for local pending runs and preserve message IDs across history refreshes, preventing stuck "thinking" state and message flicker after gateway replies. (#18165) Thanks @mbelinky. - iOS/Talk: harden mobile talk config handling by ignoring redacted/env-placeholder API keys, support secure local keychain override, improve accessibility motion/contrast behavior in status UI, and tighten ATS to local-network allowance. (#18163) Thanks @mbelinky. - iOS/Gateway: stabilize connect/discovery state handling, add onboarding reset recovery in Settings, and fix iOS gateway-controller coverage for command-surface and last-connection persistence behavior. (#18164) Thanks @mbelinky. diff --git a/docs/concepts/agent-workspace.md b/docs/concepts/agent-workspace.md index 79e1647e8f..20b2fffa31 100644 --- a/docs/concepts/agent-workspace.md +++ b/docs/concepts/agent-workspace.md @@ -116,7 +116,8 @@ See [Memory](/concepts/memory) for the workflow and automatic memory flush. If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; -adjust the limit with `agents.defaults.bootstrapMaxChars` (default: 20000). +adjust limits with `agents.defaults.bootstrapMaxChars` (default: 20000) and +`agents.defaults.bootstrapTotalMaxChars` (default: 150000). `openclaw setup` can recreate missing defaults without overwriting existing files. diff --git a/docs/concepts/context.md b/docs/concepts/context.md index c06b7b7f3d..78d755f857 100644 --- a/docs/concepts/context.md +++ b/docs/concepts/context.md @@ -112,7 +112,7 @@ By default, OpenClaw injects a fixed set of workspace files (if present): - `HEARTBEAT.md` - `BOOTSTRAP.md` (first-run only) -Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `24000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened. +Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `150000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened. ## Skills: what’s injected vs loaded on-demand diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index e74cea5b56..b7ed42534b 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -73,7 +73,7 @@ compaction. Large files are truncated with a marker. The max per-file size is controlled by `agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap content across files is capped by `agents.defaults.bootstrapTotalMaxChars` -(default: 24000). Missing files inject a short missing-file marker. +(default: 150000). Missing files inject a short missing-file marker. Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files are filtered out to keep the sub-agent context small). diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index a74d3257a7..bba2a2ad77 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -589,11 +589,11 @@ Max characters per workspace bootstrap file before truncation. Default: `20000`. ### `agents.defaults.bootstrapTotalMaxChars` -Max total characters injected across all workspace bootstrap files. Default: `24000`. +Max total characters injected across all workspace bootstrap files. Default: `150000`. ```json5 { - agents: { defaults: { bootstrapTotalMaxChars: 24000 } }, + agents: { defaults: { bootstrapTotalMaxChars: 150000 } }, } ``` diff --git a/docs/reference/token-use.md b/docs/reference/token-use.md index 5b64774664..827a4b588d 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: 24000). `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` 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. - Time (UTC + user timezone) - Reply tags + heartbeat behavior - Runtime metadata (host/OS/model/thinking) diff --git a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts index e4e852e69e..805f4fa53f 100644 --- a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts +++ b/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.e2e.test.ts @@ -58,7 +58,7 @@ describe("buildBootstrapContextFiles", () => { expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]"); }); - it("caps total injected bootstrap characters across files", () => { + it("keeps total injected bootstrap characters under the new default total cap", () => { const files = [ makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }), makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }), @@ -68,6 +68,19 @@ describe("buildBootstrapContextFiles", () => { const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0); expect(totalChars).toBeLessThanOrEqual(DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS); expect(result).toHaveLength(3); + expect(result[2]?.content).toBe("c".repeat(10_000)); + }); + + it("caps total injected bootstrap characters when totalMaxChars is configured", () => { + const files = [ + makeFile({ name: "AGENTS.md", content: "a".repeat(10_000) }), + makeFile({ name: "SOUL.md", path: "/tmp/SOUL.md", content: "b".repeat(10_000) }), + makeFile({ name: "USER.md", path: "/tmp/USER.md", content: "c".repeat(10_000) }), + ]; + const result = buildBootstrapContextFiles(files, { totalMaxChars: 24_000 }); + const totalChars = result.reduce((sum, entry) => sum + entry.content.length, 0); + expect(totalChars).toBeLessThanOrEqual(24_000); + expect(result).toHaveLength(3); expect(result[2]?.content).toContain("[...truncated, read USER.md for full content...]"); }); diff --git a/src/agents/pi-embedded-helpers/bootstrap.ts b/src/agents/pi-embedded-helpers/bootstrap.ts index 9e589fc15a..749ec1afc9 100644 --- a/src/agents/pi-embedded-helpers/bootstrap.ts +++ b/src/agents/pi-embedded-helpers/bootstrap.ts @@ -83,7 +83,7 @@ export function stripThoughtSignatures( } export const DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000; -export const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 24_000; +export const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 150_000; const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64; const BOOTSTRAP_HEAD_RATIO = 0.7; const BOOTSTRAP_TAIL_RATIO = 0.2; diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 7a040c0eb6..75c2c7673a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -39,6 +39,7 @@ import { createOllamaStreamFn, OLLAMA_NATIVE_BASE_URL } from "../../ollama-strea import { isCloudCodeAssistFormatError, resolveBootstrapMaxChars, + resolveBootstrapTotalMaxChars, validateAnthropicTurns, validateGeminiTurns, } from "../../pi-embedded-helpers.js"; @@ -462,6 +463,7 @@ export async function runEmbeddedAttempt( model: params.modelId, workspaceDir: effectiveWorkspace, bootstrapMaxChars: resolveBootstrapMaxChars(params.config), + bootstrapTotalMaxChars: resolveBootstrapTotalMaxChars(params.config), sandbox: (() => { const runtime = resolveSandboxRuntimeStatus({ cfg: params.config, diff --git a/src/agents/system-prompt-report.test.ts b/src/agents/system-prompt-report.test.ts index c2737865b6..1ff31b8535 100644 --- a/src/agents/system-prompt-report.test.ts +++ b/src/agents/system-prompt-report.test.ts @@ -44,4 +44,41 @@ describe("buildSystemPromptReport", () => { expect(report.injectedWorkspaceFiles[0]?.injectedChars).toBe("trimmed".length); }); + + it("marks workspace files truncated when injected chars are smaller than raw chars", () => { + const file = makeBootstrapFile({ + path: "/tmp/workspace/policies/AGENTS.md", + content: "abcdefghijklmnopqrstuvwxyz", + }); + const report = buildSystemPromptReport({ + source: "run", + generatedAt: 0, + bootstrapMaxChars: 20_000, + systemPrompt: "system", + bootstrapFiles: [file], + injectedFiles: [{ path: "/tmp/workspace/policies/AGENTS.md", content: "trimmed" }], + skillsPrompt: "", + tools: [], + }); + + expect(report.injectedWorkspaceFiles[0]?.truncated).toBe(true); + }); + + it("includes both bootstrap caps in the report payload", () => { + const file = makeBootstrapFile({ path: "/tmp/workspace/policies/AGENTS.md" }); + const report = buildSystemPromptReport({ + source: "run", + generatedAt: 0, + bootstrapMaxChars: 11_111, + bootstrapTotalMaxChars: 22_222, + systemPrompt: "system", + bootstrapFiles: [file], + injectedFiles: [{ path: "AGENTS.md", content: "trimmed" }], + skillsPrompt: "", + tools: [], + }); + + expect(report.bootstrapMaxChars).toBe(11_111); + expect(report.bootstrapTotalMaxChars).toBe(22_222); + }); }); diff --git a/src/agents/system-prompt-report.ts b/src/agents/system-prompt-report.ts index 5783202e10..638bca55a0 100644 --- a/src/agents/system-prompt-report.ts +++ b/src/agents/system-prompt-report.ts @@ -39,7 +39,6 @@ function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChar function buildInjectedWorkspaceFiles(params: { bootstrapFiles: WorkspaceBootstrapFile[]; injectedFiles: EmbeddedContextFile[]; - bootstrapMaxChars: number; }): SessionSystemPromptReport["injectedWorkspaceFiles"] { const injectedByPath = new Map(params.injectedFiles.map((f) => [f.path, f.content])); const injectedByBaseName = new Map(); @@ -57,7 +56,7 @@ function buildInjectedWorkspaceFiles(params: { injectedByPath.get(file.name) ?? injectedByBaseName.get(file.name); const injectedChars = injected ? injected.length : 0; - const truncated = !file.missing && rawChars > params.bootstrapMaxChars; + const truncated = !file.missing && injectedChars < rawChars; return { name: file.name, path: file.path, @@ -119,6 +118,7 @@ export function buildSystemPromptReport(params: { model?: string; workspaceDir?: string; bootstrapMaxChars: number; + bootstrapTotalMaxChars?: number; sandbox?: SessionSystemPromptReport["sandbox"]; systemPrompt: string; bootstrapFiles: WorkspaceBootstrapFile[]; @@ -148,6 +148,7 @@ export function buildSystemPromptReport(params: { model: params.model, workspaceDir: params.workspaceDir, bootstrapMaxChars: params.bootstrapMaxChars, + bootstrapTotalMaxChars: params.bootstrapTotalMaxChars, sandbox: params.sandbox, systemPrompt: { chars: systemPrompt.length, @@ -157,7 +158,6 @@ export function buildSystemPromptReport(params: { injectedWorkspaceFiles: buildInjectedWorkspaceFiles({ bootstrapFiles: params.bootstrapFiles, injectedFiles: params.injectedFiles, - bootstrapMaxChars: params.bootstrapMaxChars, }), skills: { promptChars: params.skillsPrompt.length, diff --git a/src/auto-reply/reply/commands-context-report.test.ts b/src/auto-reply/reply/commands-context-report.test.ts new file mode 100644 index 0000000000..ace49f1ecf --- /dev/null +++ b/src/auto-reply/reply/commands-context-report.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import type { HandleCommandsParams } from "./commands-types.js"; +import { buildContextReply } from "./commands-context-report.js"; + +function makeParams(commandBodyNormalized: string, truncated: boolean): HandleCommandsParams { + return { + command: { + commandBodyNormalized, + channel: "telegram", + senderIsOwner: true, + }, + sessionKey: "agent:default:main", + workspaceDir: "/tmp/workspace", + contextTokens: null, + provider: "openai", + model: "gpt-5", + elevated: { allowed: false }, + resolvedThinkLevel: "off", + resolvedReasoningLevel: "off", + sessionEntry: { + totalTokens: 123, + inputTokens: 100, + outputTokens: 23, + systemPromptReport: { + source: "run", + generatedAt: Date.now(), + workspaceDir: "/tmp/workspace", + bootstrapMaxChars: 20_000, + bootstrapTotalMaxChars: 150_000, + sandbox: { mode: "off", sandboxed: false }, + systemPrompt: { + chars: 1_000, + projectContextChars: 500, + nonProjectContextChars: 500, + }, + injectedWorkspaceFiles: [ + { + name: "AGENTS.md", + path: "/tmp/workspace/AGENTS.md", + missing: false, + rawChars: truncated ? 200_000 : 10_000, + injectedChars: truncated ? 20_000 : 10_000, + truncated, + }, + ], + skills: { + promptChars: 10, + entries: [{ name: "checks", blockChars: 10 }], + }, + tools: { + listChars: 10, + schemaChars: 20, + entries: [{ name: "read", summaryChars: 10, schemaChars: 20, propertiesCount: 1 }], + }, + }, + }, + cfg: {}, + ctx: {}, + commandBody: "", + commandArgs: [], + resolvedElevatedLevel: "off", + } as unknown as HandleCommandsParams; +} + +describe("buildContextReply", () => { + it("shows bootstrap truncation warning in list output when context exceeds configured limits", async () => { + const result = await buildContextReply(makeParams("/context list", true)); + expect(result.text).toContain("Bootstrap max/total: 150,000 chars"); + expect(result.text).toContain("⚠ Bootstrap context is over configured limits"); + expect(result.text).toContain( + "Causes: 1 file(s) exceeded max/file; raw total exceeded max/total.", + ); + }); + + it("does not show bootstrap truncation warning when there is no truncation", async () => { + const result = await buildContextReply(makeParams("/context list", false)); + expect(result.text).not.toContain("Bootstrap context is over configured limits"); + }); +}); diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index 833964523d..4e3a6cf178 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -4,7 +4,10 @@ import type { HandleCommandsParams } from "./commands-types.js"; import { resolveSessionAgentIds } from "../../agents/agent-scope.js"; import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js"; import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; -import { resolveBootstrapMaxChars } from "../../agents/pi-embedded-helpers.js"; +import { + resolveBootstrapMaxChars, + resolveBootstrapTotalMaxChars, +} from "../../agents/pi-embedded-helpers.js"; import { createOpenClawCodingTools } from "../../agents/pi-tools.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js"; @@ -59,6 +62,7 @@ async function resolveContextReport( const workspaceDir = params.workspaceDir; const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg); + const bootstrapTotalMaxChars = resolveBootstrapTotalMaxChars(params.cfg); const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({ workspaceDir, config: params.cfg, @@ -169,6 +173,7 @@ async function resolveContextReport( model: params.model, workspaceDir, bootstrapMaxChars, + bootstrapTotalMaxChars, sandbox: { mode: sandboxRuntime.mode, sandboxed: sandboxRuntime.sandboxed }, systemPrompt, bootstrapFiles, @@ -250,6 +255,37 @@ export async function buildContextReply(params: HandleCommandsParams): Promise !f.missing); + const truncatedBootstrapFiles = nonMissingBootstrapFiles.filter((f) => f.truncated); + const rawBootstrapChars = nonMissingBootstrapFiles.reduce((sum, file) => sum + file.rawChars, 0); + const injectedBootstrapChars = nonMissingBootstrapFiles.reduce( + (sum, file) => sum + file.injectedChars, + 0, + ); + const perFileOverLimitCount = + typeof bootstrapMaxChars === "number" + ? nonMissingBootstrapFiles.filter((f) => f.rawChars > bootstrapMaxChars).length + : 0; + const totalOverLimit = + typeof bootstrapTotalMaxChars === "number" && rawBootstrapChars > bootstrapTotalMaxChars; + const truncationCauseParts = [ + perFileOverLimitCount > 0 ? `${perFileOverLimitCount} file(s) exceeded max/file` : null, + totalOverLimit ? "raw total exceeded max/total" : null, + ].filter(Boolean); + const bootstrapWarningLines = + truncatedBootstrapFiles.length > 0 + ? [ + `⚠ Bootstrap context is over configured limits: ${truncatedBootstrapFiles.length} file(s) truncated (${formatInt(rawBootstrapChars)} raw chars -> ${formatInt(injectedBootstrapChars)} injected chars).`, + ...(truncationCauseParts.length ? [`Causes: ${truncationCauseParts.join("; ")}.`] : []), + "Tip: increase `agents.defaults.bootstrapMaxChars` and/or `agents.defaults.bootstrapTotalMaxChars` if this truncation is not intentional.", + ] + : []; const totalsLine = session.totalTokens != null @@ -280,8 +316,10 @@ export async function buildContextReply(params: HandleCommandsParams): Promise = { "agents.defaults.bootstrapMaxChars": "Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).", "agents.defaults.bootstrapTotalMaxChars": - "Max total characters across all injected workspace bootstrap files (default: 24000).", + "Max total characters across all injected workspace bootstrap files (default: 150000).", "agents.defaults.repoRoot": "Optional repository root shown in the system prompt runtime line (overrides auto-detect).", "agents.defaults.envelopeTimezone": diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 012d59f728..809af7709c 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -159,6 +159,7 @@ export type SessionSystemPromptReport = { model?: string; workspaceDir?: string; bootstrapMaxChars?: number; + bootstrapTotalMaxChars?: number; sandbox?: { mode?: string; sandboxed?: boolean; diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 2774105fb2..164350e392 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -136,7 +136,7 @@ export type AgentDefaultsConfig = { skipBootstrap?: boolean; /** Max chars for injected bootstrap files before truncation (default: 20000). */ bootstrapMaxChars?: number; - /** Max total chars across all injected bootstrap files (default: 24000). */ + /** Max total chars across all injected bootstrap files (default: 150000). */ bootstrapTotalMaxChars?: number; /** Optional IANA timezone for the user (used in system prompt; defaults to host timezone). */ userTimezone?: string;