Agents: raise bootstrap total cap and warn on /context truncation (#18229)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f6620526df
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-02-16 12:04:53 -05:00
committed by GitHub
parent 5b185da366
commit 8a67016646
16 changed files with 188 additions and 14 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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: whats injected vs loaded on-demand

View File

@@ -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).

View File

@@ -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 } },
}
```

View File

@@ -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)

View File

@@ -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...]");
});

View File

@@ -83,7 +83,7 @@ export function stripThoughtSignatures<T>(
}
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;

View File

@@ -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,

View File

@@ -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);
});
});

View File

@@ -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<string, string>();
@@ -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,

View File

@@ -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");
});
});

View File

@@ -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<R
typeof report.bootstrapMaxChars === "number"
? `${formatInt(report.bootstrapMaxChars)} chars`
: "? chars";
const bootstrapTotalLabel =
typeof report.bootstrapTotalMaxChars === "number"
? `${formatInt(report.bootstrapTotalMaxChars)} chars`
: "? chars";
const bootstrapMaxChars = report.bootstrapMaxChars;
const bootstrapTotalMaxChars = report.bootstrapTotalMaxChars;
const nonMissingBootstrapFiles = report.injectedWorkspaceFiles.filter((f) => !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<R
"🧠 Context breakdown (detailed)",
`Workspace: ${workspaceLabel}`,
`Bootstrap max/file: ${bootstrapMaxLabel}`,
`Bootstrap max/total: ${bootstrapTotalLabel}`,
sandboxLine,
systemPromptLine,
...(bootstrapWarningLines.length ? ["", ...bootstrapWarningLines] : []),
"",
"Injected workspace files:",
...fileLines,
@@ -317,8 +355,10 @@ export async function buildContextReply(params: HandleCommandsParams): Promise<R
"🧠 Context breakdown",
`Workspace: ${workspaceLabel}`,
`Bootstrap max/file: ${bootstrapMaxLabel}`,
`Bootstrap max/total: ${bootstrapTotalLabel}`,
sandboxLine,
systemPromptLine,
...(bootstrapWarningLines.length ? ["", ...bootstrapWarningLines] : []),
"",
"Injected workspace files:",
...fileLines,

View File

@@ -145,7 +145,7 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@@ -159,6 +159,7 @@ export type SessionSystemPromptReport = {
model?: string;
workspaceDir?: string;
bootstrapMaxChars?: number;
bootstrapTotalMaxChars?: number;
sandbox?: {
mode?: string;
sandboxed?: boolean;

View File

@@ -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;