diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cfd0caa599..cb1bce5ab24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946. - Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc. - Gateway/install: keep `.env`-managed values in the macOS LaunchAgent env file while still tracking `OPENCLAW_SERVICE_MANAGED_ENV_KEYS`, so regenerated services do not boot without managed auth/provider keys. Fixes #75374. - Gateway/restart: verify listener PIDs by argv when `lsof` reports only the Node process name, so stale gateway cleanup can find macOS `cnode` listeners. Fixes #70664. diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index d7280dd0e60..9b9769cdcfc 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -33,13 +33,13 @@ Inside `agents.defaults.workspace`, OpenClaw expects these user-editable files: - `IDENTITY.md` — agent name/vibe/emoji - `USER.md` — user profile + preferred address -On the first turn of a new session, OpenClaw injects the contents of these files directly into the agent context. +On the first turn of a new session, OpenClaw injects the contents of these files into the system prompt's Project Context. Blank files are skipped. Large files are trimmed and truncated with a marker so prompts stay lean (read the file for full content). If a file is missing, OpenClaw injects a single “missing file” marker line (and `openclaw setup` will create a safe default template). -`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts. +`BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). While it is pending, OpenClaw keeps it in Project Context and adds system-prompt bootstrap guidance for the initial ritual instead of copying it into the user message. If you delete it after completing the ritual, it should not be recreated on later restarts. To disable bootstrap file creation entirely (for pre-seeded workspaces), set: diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index 725eddf4edf..2dc5d7a395d 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -176,9 +176,10 @@ Large files are truncated with a marker. The max per-file size is controlled by `agents.defaults.bootstrapMaxChars` (default: 12000). Total injected bootstrap content across files is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 60000). Missing files inject a short missing-file marker. When truncation -occurs, OpenClaw can inject a warning block in Project Context; control this with +occurs, OpenClaw can inject a concise system-prompt warning notice; control this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; -default: `once`). +default: `once`). Detailed raw/injected counts stay in diagnostics such as +`/context`, `/status`, doctor, and logs. 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/config-agents.md b/docs/gateway/config-agents.md index 2de97c530dc..a59d2c9c4fb 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -116,12 +116,16 @@ Max total characters injected across all workspace bootstrap files. Default: `60 ### `agents.defaults.bootstrapPromptTruncationWarning` -Controls agent-visible warning text when bootstrap context is truncated. +Controls the agent-visible system-prompt notice when bootstrap context is truncated. Default: `"once"`. -- `"off"`: never inject warning text into the system prompt. -- `"once"`: inject warning once per unique truncation signature (recommended). -- `"always"`: inject warning on every run when truncation exists. +- `"off"`: never inject truncation notice text into the system prompt. +- `"once"`: inject a concise notice once per unique truncation signature (recommended). +- `"always"`: inject a concise notice on every run when truncation exists. + +Detailed raw/injected counts and config tuning fields stay in diagnostics such +as context/status reports and logs; routine WebChat user/runtime context only +gets the concise recovery notice. ```json5 { diff --git a/docs/web/webchat.md b/docs/web/webchat.md index 1df49c775f8..b5dca2a8bc7 100644 --- a/docs/web/webchat.md +++ b/docs/web/webchat.md @@ -28,6 +28,7 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket. - Compaction entries render as an explicit compacted-history divider. The divider explains that earlier turns are preserved in a checkpoint and links to the Sessions checkpoint controls, where operators can branch or restore the pre-compaction view when their permissions allow it. - Control UI remembers the backing Gateway `sessionId` returned by `chat.history` and includes it on follow-up `chat.send` calls, so reconnects and page refreshes continue the same stored conversation unless the user starts or resets a session. - Control UI coalesces duplicate in-flight submits for the same session, message, and attachments before generating a new `chat.send` run id; the Gateway still dedupes repeated requests that reuse the same idempotency key. +- Workspace startup files and pending `BOOTSTRAP.md` instructions are supplied through the agent system prompt's Project Context, not copied into the WebChat user message. Bootstrap truncation only adds a concise system-prompt recovery notice; detailed counts and config knobs stay on diagnostic surfaces. - `chat.history` is also display-normalized: runtime-only OpenClaw context, inbound envelope wrappers, inline delivery directive tags such as `[[reply_to_*]]` and `[[audio_as_voice]]`, plain-text tool-call XML diff --git a/src/agents/bootstrap-budget.test.ts b/src/agents/bootstrap-budget.test.ts index 17d693f2128..2be788b69ca 100644 --- a/src/agents/bootstrap-budget.test.ts +++ b/src/agents/bootstrap-budget.test.ts @@ -4,6 +4,7 @@ import { analyzeBootstrapBudget, buildBootstrapInjectionStats, buildBootstrapPromptWarning, + buildBootstrapPromptWarningNotice, buildBootstrapTruncationReportMeta, buildBootstrapTruncationSignature, formatBootstrapTruncationWarningLines, @@ -136,6 +137,18 @@ describe("bootstrap prompt warnings", () => { ).toBe(heartbeatPrompt); }); + it("builds a concise agent notice without raw truncation diagnostics", () => { + const notice = buildBootstrapPromptWarningNotice([ + "AGENTS.md: 200 raw -> 0 injected", + "If unintentional, raise agents.defaults.bootstrapMaxChars.", + ]); + + expect(notice).toContain("[Bootstrap truncation warning]"); + expect(notice).toContain("Treat Project Context as partial"); + expect(notice).not.toContain("raw ->"); + expect(notice).not.toContain("bootstrapMaxChars"); + }); + it("resolves seen signatures from report history or legacy single signature", () => { expect( resolveBootstrapWarningSignaturesSeen({ diff --git a/src/agents/bootstrap-budget.ts b/src/agents/bootstrap-budget.ts index 8e2b91e4558..e54f6c7cd8d 100644 --- a/src/agents/bootstrap-budget.ts +++ b/src/agents/bootstrap-budget.ts @@ -354,6 +354,18 @@ export function appendBootstrapPromptWarning( return prompt ? `${prompt}\n\n${warningBlock}` : warningBlock; } +export function buildBootstrapPromptWarningNotice(warningLines?: string[]): string | undefined { + const hasWarning = (warningLines ?? []).some((line) => line.trim().length > 0); + if (!hasWarning) { + return undefined; + } + return [ + "[Bootstrap truncation warning]", + "Some workspace bootstrap files were truncated before Project Context injection.", + "Treat Project Context as partial and read the relevant files directly if details seem missing.", + ].join("\n"); +} + export function buildBootstrapTruncationReportMeta(params: { analysis: BootstrapBudgetAnalysis; warningMode: BootstrapPromptWarningMode; diff --git a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts index 141b2ec9d13..48541fa41ce 100644 --- a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts +++ b/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts @@ -1,7 +1,5 @@ import type { BootstrapMode } from "../../bootstrap-mode.js"; import { resolveBootstrapMode } from "../../bootstrap-mode.js"; -import { buildAgentUserPromptPrefix } from "../../system-prompt.js"; -import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js"; export type AttemptBootstrapRoutingInput = { workspaceBootstrapPending: boolean; @@ -18,12 +16,6 @@ export type AttemptBootstrapRoutingInput = { export type AttemptBootstrapRouting = { bootstrapMode: BootstrapMode; shouldStripBootstrapFromContext: boolean; - userPromptPrefixText?: string; -}; - -export type BootstrapPromptContextFile = { - path?: string; - content?: string; }; export type AttemptWorkspaceBootstrapRoutingInput = Omit< @@ -36,7 +28,7 @@ export type AttemptWorkspaceBootstrapRoutingInput = Omit< export function shouldStripBootstrapFromEmbeddedContext(_params: { bootstrapMode: BootstrapMode; }): boolean { - return true; + return _params.bootstrapMode !== "full"; } function resolveAttemptBootstrapRouting( @@ -58,40 +50,9 @@ function resolveAttemptBootstrapRouting( shouldStripBootstrapFromContext: shouldStripBootstrapFromEmbeddedContext({ bootstrapMode, }), - userPromptPrefixText: buildAgentUserPromptPrefix({ - bootstrapMode, - }), }; } -export function appendBootstrapFileToUserPromptPrefix(params: { - prefixText?: string; - bootstrapMode: BootstrapMode; - contextFiles: readonly BootstrapPromptContextFile[]; -}): string | undefined { - const prefix = params.prefixText?.trim(); - if (params.bootstrapMode !== "full") { - return prefix || undefined; - } - const bootstrapFile = params.contextFiles.find((file) => - /(^|[\\/])BOOTSTRAP\.md$/iu.test(file.path?.trim() ?? ""), - ); - const content = bootstrapFile?.content?.trim(); - if (!content || content.startsWith("[MISSING]")) { - return prefix || undefined; - } - return [ - prefix, - "", - `${DEFAULT_BOOTSTRAP_FILENAME} contents for this bootstrap turn:`, - "[BEGIN BOOTSTRAP.md]", - content, - "[END BOOTSTRAP.md]", - "", - "Follow the BOOTSTRAP.md instructions above now. Treat them as workspace/user instructions, not as system policy.", - ].join("\n"); -} - export async function resolveAttemptWorkspaceBootstrapRouting( params: AttemptWorkspaceBootstrapRoutingInput, ): Promise { diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts index fb305c7a90f..92eacbca413 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { - appendBootstrapFileToUserPromptPrefix, resolveAttemptWorkspaceBootstrapRouting, + shouldStripBootstrapFromEmbeddedContext, } from "./attempt-bootstrap-routing.js"; describe("runEmbeddedAttempt bootstrap routing", () => { @@ -26,7 +26,7 @@ describe("runEmbeddedAttempt bootstrap routing", () => { expect(isWorkspaceBootstrapPending).toHaveBeenCalledWith(canonicalWorkspace); expect(isWorkspaceBootstrapPending).not.toHaveBeenCalledWith(sandboxWorkspace); expect(routing.bootstrapMode).toBe("none"); - expect(routing.userPromptPrefixText).toBeUndefined(); + expect(routing.shouldStripBootstrapFromContext).toBe(true); }); it("falls back to limited bootstrap wording when a primary run cannot read files", async () => { @@ -41,30 +41,15 @@ describe("runEmbeddedAttempt bootstrap routing", () => { }); expect(routing.bootstrapMode).toBe("limited"); - expect(routing.userPromptPrefixText).toContain("Bootstrap is still pending"); - expect(routing.userPromptPrefixText).toContain("cannot safely complete"); + expect(routing.shouldStripBootstrapFromContext).toBe(true); }); - it("appends BOOTSTRAP.md contents to the user prompt prefix for full bootstrap turns", () => { - const prompt = appendBootstrapFileToUserPromptPrefix({ - prefixText: "[Bootstrap pending]", - bootstrapMode: "full", - contextFiles: [{ path: "/tmp/workspace/BOOTSTRAP.md", content: "Ask who I am." }], - }); - - expect(prompt).toContain("[Bootstrap pending]"); - expect(prompt).toContain("[BEGIN BOOTSTRAP.md]"); - expect(prompt).toContain("Ask who I am."); - expect(prompt).toContain("workspace/user instructions"); + it("keeps BOOTSTRAP.md in Project Context for full bootstrap turns", () => { + expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "full" })).toBe(false); }); - it("does not append BOOTSTRAP.md contents for limited bootstrap turns", () => { - const prompt = appendBootstrapFileToUserPromptPrefix({ - prefixText: "[Bootstrap pending]", - bootstrapMode: "limited", - contextFiles: [{ path: "/tmp/workspace/BOOTSTRAP.md", content: "Ask who I am." }], - }); - - expect(prompt).toBe("[Bootstrap pending]"); + it("strips BOOTSTRAP.md from Project Context outside full bootstrap turns", () => { + expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "limited" })).toBe(true); + expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "none" })).toBe(true); }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts index 6fe2a82144c..e83a5163b7a 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; import { analyzeBootstrapBudget, + buildBootstrapPromptWarningNotice, buildBootstrapInjectionStats, buildBootstrapPromptWarning, - appendBootstrapPromptWarning, } from "../../bootstrap-budget.js"; import { composeSystemPromptWithHookContext } from "./attempt.thread-helpers.js"; describe("runEmbeddedAttempt bootstrap warning prompt assembly", () => { - it("keeps bootstrap warnings in the sent prompt after hook prepend context", () => { + it("keeps bootstrap warnings in system context without raw diagnostics", () => { const analysis = analyzeBootstrapBudget({ files: buildBootstrapInjectionStats({ bootstrapFiles: [ @@ -28,15 +28,17 @@ describe("runEmbeddedAttempt bootstrap warning prompt assembly", () => { analysis, mode: "once", }); - const promptWithWarning = appendBootstrapPromptWarning("hello", warning.lines); + const notice = buildBootstrapPromptWarningNotice(warning.lines); const systemPrompt = composeSystemPromptWithHookContext({ - baseSystemPrompt: promptWithWarning, + baseSystemPrompt: "base system prompt", prependSystemContext: "hook context", + appendSystemContext: notice, }); expect(systemPrompt).toContain("hook context"); expect(systemPrompt).toContain("[Bootstrap truncation warning]"); - expect(systemPrompt).toContain("- AGENTS.md: 200 raw -> 20 injected"); - expect(systemPrompt).toContain("hello"); + expect(systemPrompt).toContain("Treat Project Context as partial"); + expect(systemPrompt).not.toContain("- AGENTS.md: 200 raw -> 20 injected"); + expect(systemPrompt).toContain("base system prompt"); }); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index 6c39cc726ec..3ce99a1e8bb 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -207,6 +207,115 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { } }); + it("keeps bootstrap truncation warnings out of WebChat runtime context", async () => { + const seen: { prompt?: string; messages?: unknown[] } = {}; + hoisted.resolveBootstrapContextForRunMock.mockResolvedValueOnce({ + bootstrapFiles: [ + { + name: "AGENTS.md", + path: "/tmp/openclaw-warning-workspace/AGENTS.md", + content: "A".repeat(200), + missing: false, + }, + ], + contextFiles: [ + { path: "/tmp/openclaw-warning-workspace/AGENTS.md", content: "A".repeat(20) }, + ], + }); + + await createContextEngineAttemptRunner({ + contextEngine: createContextEngineBootstrapAndAssemble(), + sessionKey, + tempPaths, + attemptOverrides: { + config: { + agents: { + defaults: { + bootstrapMaxChars: 50, + bootstrapTotalMaxChars: 50, + }, + }, + } as OpenClawConfig, + prompt: "visible ask", + transcriptPrompt: "visible ask", + }, + sessionPrompt: async (session, prompt) => { + seen.prompt = prompt; + seen.messages = [...session.messages]; + session.messages = [ + ...session.messages, + { role: "assistant", content: "done", timestamp: 2 }, + ]; + }, + }); + + expect(seen.prompt).toBe("visible ask"); + expect(JSON.stringify(seen.messages)).not.toContain("[Bootstrap truncation warning]"); + expect(JSON.stringify(seen.messages)).not.toContain("bootstrapMaxChars"); + }); + + it("preserves bootstrap system context when system prompt override is configured", async () => { + const seen: { prompt?: string; messages?: unknown[] } = {}; + hoisted.isWorkspaceBootstrapPendingMock.mockResolvedValueOnce(true); + hoisted.createOpenClawCodingToolsMock.mockImplementationOnce(() => [ + { name: "read", execute: async () => "" }, + ]); + hoisted.resolveBootstrapContextForRunMock.mockResolvedValueOnce({ + bootstrapFiles: [ + { + name: "BOOTSTRAP.md", + path: "/tmp/openclaw-override-workspace/BOOTSTRAP.md", + content: "Ask who I am.", + missing: false, + }, + ], + contextFiles: [ + { + path: "/tmp/openclaw-override-workspace/BOOTSTRAP.md", + content: "Ask who I am.", + }, + ], + }); + + await createContextEngineAttemptRunner({ + contextEngine: createContextEngineBootstrapAndAssemble(), + sessionKey, + tempPaths, + attemptOverrides: { + config: { + agents: { + defaults: { + systemPromptOverride: "Custom override prompt.", + }, + }, + } as OpenClawConfig, + prompt: "visible ask", + transcriptPrompt: "visible ask", + trigger: "user", + }, + sessionPrompt: async (session, prompt) => { + seen.prompt = prompt; + seen.messages = [...session.messages]; + session.messages = [ + ...session.messages, + { role: "assistant", content: "done", timestamp: 2 }, + ]; + }, + }); + + expect(seen.prompt).toBe("visible ask"); + expect(JSON.stringify(seen.messages)).not.toContain("Ask who I am."); + const systemPrompt = + hoisted.systemPromptOverrideTexts.find((text) => text.includes("Custom override prompt.")) ?? + ""; + + expect(systemPrompt).toContain("Custom override prompt."); + expect(systemPrompt).toContain("## Bootstrap Pending"); + expect(systemPrompt).toContain("BOOTSTRAP.md is included below in Project Context"); + expect(systemPrompt).toContain("## /tmp/openclaw-override-workspace/BOOTSTRAP.md"); + expect(systemPrompt).toContain("Ask who I am."); + }); + it("adds explicit reply context to the current model input without exposing generic runtime context", async () => { let seenPrompt: string | undefined; diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index 4851498943d..8b05abcecf0 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -83,6 +83,7 @@ type AttemptSpawnWorkspaceHoisted = { >; limitHistoryTurnsMock: Mock<(messages: T, limit: number | undefined) => T>; preemptiveCompactionCalls: Parameters[0][]; + systemPromptOverrideTexts: string[]; sessionManager: SessionManagerMocks; }; @@ -162,6 +163,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { (messages) => messages, ); const preemptiveCompactionCalls: Parameters[0][] = []; + const systemPromptOverrideTexts: string[] = []; const sessionManager = { getLeafEntry: vi.fn(() => null), branch: vi.fn(), @@ -198,6 +200,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { getHistoryLimitFromSessionKeyMock, limitHistoryTurnsMock, preemptiveCompactionCalls, + systemPromptOverrideTexts, sessionManager, }; }); @@ -255,7 +258,8 @@ vi.mock("../../../plugins/provider-runtime.js", () => ({ resolveProviderReasoningOutputModeWithPlugin: () => undefined, resolveProviderSystemPromptContribution: () => undefined, resolveProviderTextTransforms: () => undefined, - transformProviderSystemPrompt: ({ systemPrompt }: { systemPrompt: string }) => systemPrompt, + transformProviderSystemPrompt: ({ context }: { context: { systemPrompt?: string } }) => + context.systemPrompt, })); vi.mock("../../../infra/machine-name.js", () => ({ @@ -411,11 +415,20 @@ vi.mock("../../system-prompt-report.js", () => ({ buildSystemPromptReport: () => undefined, })); -vi.mock("../system-prompt.js", () => ({ - applySystemPromptOverrideToSession: () => {}, - buildEmbeddedSystemPrompt: () => "system prompt", - createSystemPromptOverride: (prompt: string) => () => prompt, -})); +vi.mock("../system-prompt.js", async () => { + const actual = await vi.importActual("../system-prompt.js"); + return { + ...actual, + applySystemPromptOverrideToSession: (session: MutableSession, systemPrompt: string) => { + session.agent.state.systemPrompt = systemPrompt; + }, + buildEmbeddedSystemPrompt: () => "system prompt", + createSystemPromptOverride: (prompt: string) => { + hoisted.systemPromptOverrideTexts.push(prompt); + return () => prompt; + }, + }; +}); vi.mock("../extra-params.js", async () => { const actual = await vi.importActual("../extra-params.js"); @@ -817,6 +830,7 @@ export function resetEmbeddedAttemptHarness( hoisted.getHistoryLimitFromSessionKeyMock.mockReset().mockReturnValue(undefined); hoisted.limitHistoryTurnsMock.mockReset().mockImplementation((messages) => messages); hoisted.preemptiveCompactionCalls.length = 0; + hoisted.systemPromptOverrideTexts.length = 0; hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null); hoisted.sessionManager.branch.mockReset(); hoisted.sessionManager.resetLeaf.mockReset(); diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 04ff01856e8..b571b96e1a7 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1,7 +1,6 @@ import { streamSimple } from "@mariozechner/pi-ai"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; -import { appendBootstrapPromptWarning } from "../../bootstrap-budget.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../system-prompt-cache-boundary.js"; import { buildAgentSystemPrompt } from "../../system-prompt.js"; import { @@ -368,40 +367,23 @@ describe("composeSystemPromptWithHookContext", () => { ).toBe("append only"); }); - it("keeps hook-composed system prompt stable when bootstrap warnings only change the user prompt", () => { + it("keeps bootstrap truncation notices in the system prompt instead of the user prompt", () => { const baseSystemPrompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", contextFiles: [{ path: "AGENTS.md", content: "Follow AGENTS guidance." }], toolNames: ["read"], + bootstrapTruncationNotice: + "[Bootstrap truncation warning]\nSome workspace bootstrap files were truncated before Project Context injection.\nTreat Project Context as partial and read the relevant files directly if details seem missing.", }); const composedSystemPrompt = composeSystemPromptWithHookContext({ baseSystemPrompt, appendSystemContext: "hook system context", }); - const turns = [ - { - systemPrompt: composedSystemPrompt, - prompt: appendBootstrapPromptWarning("hello", ["AGENTS.md: 200 raw -> 0 injected"]), - }, - { - systemPrompt: composedSystemPrompt, - prompt: appendBootstrapPromptWarning("hello again", []), - }, - { - systemPrompt: composedSystemPrompt, - prompt: appendBootstrapPromptWarning("hello once more", [ - "AGENTS.md: 200 raw -> 0 injected", - ]), - }, - ]; - expect(turns[0]?.systemPrompt).toBe(turns[1]?.systemPrompt); - expect(turns[1]?.systemPrompt).toBe(turns[2]?.systemPrompt); - expect(turns[0]?.prompt.startsWith("hello")).toBe(true); - expect(turns[1]?.prompt).toBe("hello again"); - expect(turns[2]?.prompt.startsWith("hello once more")).toBe(true); - expect(turns[0]?.prompt).toContain("[Bootstrap truncation warning]"); - expect(turns[2]?.prompt).toContain("[Bootstrap truncation warning]"); + expect(composedSystemPrompt).toContain("[Bootstrap truncation warning]"); + expect(composedSystemPrompt).toContain("Treat Project Context as partial"); + expect(composedSystemPrompt).toContain("hook system context"); + expect("hello").not.toContain("[Bootstrap truncation warning]"); }); }); @@ -423,8 +405,8 @@ describe("resolvePromptModeForSession", () => { }); describe("shouldStripBootstrapFromEmbeddedContext", () => { - it("never injects raw BOOTSTRAP.md into embedded system context", () => { - expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "full" })).toBe(true); + it("keeps BOOTSTRAP.md in system Project Context only for full bootstrap turns", () => { + expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "full" })).toBe(false); expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "limited" })).toBe(true); expect(shouldStripBootstrapFromEmbeddedContext({ bootstrapMode: "none" })).toBe(true); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index b594e10876a..f005c582746 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -56,9 +56,9 @@ import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js"; import { analyzeBootstrapBudget, buildBootstrapPromptWarning, + buildBootstrapPromptWarningNotice, buildBootstrapTruncationReportMeta, buildBootstrapInjectionStats, - appendBootstrapPromptWarning, } from "../../bootstrap-budget.js"; import { FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE, @@ -155,6 +155,7 @@ import { import { resolveSystemPromptOverride } from "../../system-prompt-override.js"; import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; +import { appendAgentBootstrapSystemPromptSupplement } from "../../system-prompt.js"; import { resolveAgentTimeoutMs } from "../../timeout.js"; import { buildEmptyExplicitToolAllowlistError, @@ -239,7 +240,6 @@ import { abortable as abortableWithSignal } from "./abortable.js"; import { createEmbeddedAgentSessionWithResourceLoader } from "./attempt-session.js"; export { buildContextEnginePromptCacheInfo } from "./attempt.context-engine-helpers.js"; import { - appendBootstrapFileToUserPromptPrefix, resolveAttemptWorkspaceBootstrapRouting, shouldStripBootstrapFromEmbeddedContext, } from "./attempt-bootstrap-routing.js"; @@ -1284,48 +1284,59 @@ export async function runEmbeddedAttempt( context: promptContributionContext, }); - const builtAppendPrompt = - resolveSystemPromptOverride({ - config: params.config, - agentId: sessionAgentId, - }) ?? - buildEmbeddedSystemPrompt({ - workspaceDir: effectiveWorkspace, - defaultThinkLevel: params.thinkLevel, - reasoningLevel: params.reasoningLevel ?? "off", - extraSystemPrompt: params.extraSystemPrompt, - ownerNumbers: params.ownerNumbers, - ownerDisplay: ownerDisplay.ownerDisplay, - ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, - reasoningTagHint, - heartbeatPrompt, - skillsPrompt: effectiveSkillsPrompt, - docsPath: openClawReferences.docsPath ?? undefined, - sourcePath: openClawReferences.sourcePath ?? undefined, - ttsHint, - workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined, - reactionGuidance, - promptMode: effectivePromptMode, - sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, - silentReplyPromptMode: params.silentReplyPromptMode, - acpEnabled: isAcpRuntimeSpawnAvailable({ - config: params.config, - sandboxed: sandboxInfo?.enabled === true, - }), - nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), - runtimeInfo, - messageToolHints, - sandboxInfo, - tools: effectiveTools, - modelAliasLines: buildModelAliasLines(params.config), - userTimezone, - userTime, - userTimeFormat, - contextFiles, - includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy", - memoryCitationsMode: params.config?.memory?.citations, - promptContribution, - }); + const bootstrapTruncationNotice = buildBootstrapPromptWarningNotice( + bootstrapPromptWarning.lines, + ); + const systemPromptOverrideText = resolveSystemPromptOverride({ + config: params.config, + agentId: sessionAgentId, + }); + const builtAppendPrompt = systemPromptOverrideText + ? appendAgentBootstrapSystemPromptSupplement({ + systemPrompt: systemPromptOverrideText, + bootstrapMode, + bootstrapTruncationNotice, + contextFiles, + }) + : buildEmbeddedSystemPrompt({ + workspaceDir: effectiveWorkspace, + defaultThinkLevel: params.thinkLevel, + reasoningLevel: params.reasoningLevel ?? "off", + extraSystemPrompt: params.extraSystemPrompt, + ownerNumbers: params.ownerNumbers, + ownerDisplay: ownerDisplay.ownerDisplay, + ownerDisplaySecret: ownerDisplay.ownerDisplaySecret, + reasoningTagHint, + heartbeatPrompt, + skillsPrompt: effectiveSkillsPrompt, + docsPath: openClawReferences.docsPath ?? undefined, + sourcePath: openClawReferences.sourcePath ?? undefined, + ttsHint, + workspaceNotes: workspaceNotes?.length ? workspaceNotes : undefined, + reactionGuidance, + promptMode: effectivePromptMode, + sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, + silentReplyPromptMode: params.silentReplyPromptMode, + acpEnabled: isAcpRuntimeSpawnAvailable({ + config: params.config, + sandboxed: sandboxInfo?.enabled === true, + }), + nativeCommandGuidanceLines: listRegisteredPluginAgentPromptGuidance(), + runtimeInfo, + messageToolHints, + sandboxInfo, + tools: effectiveTools, + modelAliasLines: buildModelAliasLines(params.config), + userTimezone, + userTime, + userTimeFormat, + contextFiles, + bootstrapMode, + bootstrapTruncationNotice, + includeMemorySection: !activeContextEngine || activeContextEngine.info.id === "legacy", + memoryCitationsMode: params.config?.memory?.citations, + promptContribution, + }); const appendPrompt = isRawModelRun ? "" : transformProviderSystemPrompt({ @@ -1375,11 +1386,6 @@ export async function runEmbeddedAttempt( }); const systemPromptOverride = createSystemPromptOverride(appendPrompt); let systemPromptText = systemPromptOverride(); - const userPromptPrefixText = appendBootstrapFileToUserPromptPrefix({ - prefixText: bootstrapRouting.userPromptPrefixText, - bootstrapMode, - contextFiles: remappedContextFiles, - }); prepStages.mark("system-prompt"); // Keep the session lock scoped to transcript/session mutations. Cold plugin @@ -1831,7 +1837,6 @@ export async function runEmbeddedAttempt( toolsAllow: params.toolsAllow, skillsSnapshot: params.skillsSnapshot, systemPromptReport, - userPromptPrefixText, }), ); @@ -2593,16 +2598,7 @@ export async function runEmbeddedAttempt( // Run before_prompt_build hooks to allow plugins to inject prompt context. // Legacy compatibility: before_agent_start is also checked for context fields. - let effectivePrompt = appendBootstrapPromptWarning( - params.prompt, - bootstrapPromptWarning.lines, - { - preserveExactPrompt: heartbeatPrompt, - }, - ); - if (userPromptPrefixText) { - effectivePrompt = `${userPromptPrefixText}\n\n${effectivePrompt}`; - } + let effectivePrompt = params.prompt; const hookCtx = { runId: params.runId, trace: freezeDiagnosticTraceContext(diagnosticTrace), diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index bf991390f19..0cc2e993a5b 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -2,6 +2,7 @@ import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentSession } from "@mariozechner/pi-coding-agent"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { MemoryCitationsMode } from "../../config/types.memory.js"; +import type { BootstrapMode } from "../bootstrap-mode.js"; import type { ResolvedTimeFormat } from "../date-time.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import type { ProviderSystemPromptContribution } from "../system-prompt-contribution.js"; @@ -62,6 +63,8 @@ export function buildEmbeddedSystemPrompt(params: { userTime?: string; userTimeFormat?: ResolvedTimeFormat; contextFiles?: EmbeddedContextFile[]; + bootstrapMode?: BootstrapMode; + bootstrapTruncationNotice?: string; includeMemorySection?: boolean; memoryCitationsMode?: MemoryCitationsMode; promptContribution?: ProviderSystemPromptContribution; @@ -97,6 +100,8 @@ export function buildEmbeddedSystemPrompt(params: { userTime: params.userTime, userTimeFormat: params.userTimeFormat, contextFiles: params.contextFiles, + bootstrapMode: params.bootstrapMode, + bootstrapTruncationNotice: params.bootstrapTruncationNotice, includeMemorySection: params.includeMemorySection, memoryCitationsMode: params.memoryCitationsMode, promptContribution: params.promptContribution, diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 9359811d0bb..12510992595 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -4,8 +4,10 @@ import { typedCases } from "../test-utils/typed-cases.js"; import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js"; import { + appendAgentBootstrapSystemPromptSupplement, + buildAgentBootstrapSystemContext, + buildAgentBootstrapSystemPromptSupplement, buildAgentSystemPrompt, - buildAgentUserPromptPrefix, buildRuntimeLine, } from "./system-prompt.js"; @@ -502,29 +504,32 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("Reminder: commit your changes in this workspace after edits."); }); - it("keeps bootstrap instructions out of the privileged system prompt", () => { + it("includes bootstrap instructions in system prompt when bootstrap is pending", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", - workspaceNotes: ["Reminder: commit your changes in this workspace after edits."], + bootstrapMode: "full", + contextFiles: [{ path: "/tmp/openclaw/BOOTSTRAP.md", content: "Ask who I am." }], }); - expect(prompt).not.toContain("## Bootstrap"); - expect(prompt).not.toContain("Bootstrap is pending for this workspace."); - expect(prompt).not.toContain("BOOTSTRAP.md is present in Project Context"); + expect(prompt).toContain("## Bootstrap Pending"); + expect(prompt).toContain("BOOTSTRAP.md is included below in Project Context"); + expect(prompt).toContain("must follow BOOTSTRAP.md, not a generic greeting"); + expect(prompt).toContain("## /tmp/openclaw/BOOTSTRAP.md"); + expect(prompt).toContain("Ask who I am."); }); - it("adds bootstrap-specific prelude text to the user prompt prefix when bootstrap is pending", () => { - const promptPrefix = buildAgentUserPromptPrefix({ bootstrapMode: "full" }); + it("includes bootstrap truncation notice in system prompt without raw diagnostics", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/openclaw", + bootstrapTruncationNotice: + "[Bootstrap truncation warning]\nSome workspace bootstrap files were truncated before Project Context injection.\nTreat Project Context as partial and read the relevant files directly if details seem missing.", + }); - expect(promptPrefix).toContain("[Bootstrap pending]"); - expect(promptPrefix).toContain("Please read BOOTSTRAP.md from the workspace"); - expect(promptPrefix).toContain("If this run can complete the BOOTSTRAP.md workflow, do so."); - expect(promptPrefix).toContain("explain the blocker briefly"); - expect(promptPrefix).toContain("offer the simplest next step"); - expect(promptPrefix).toContain("Do not use a generic first greeting or reply normally"); - expect(promptPrefix).toContain( - "Your first user-visible reply for a bootstrap-pending workspace must follow BOOTSTRAP.md", - ); + expect(prompt).toContain("## Bootstrap Context Notice"); + expect(prompt).toContain("[Bootstrap truncation warning]"); + expect(prompt).toContain("Treat Project Context as partial"); + expect(prompt).not.toContain("raw ->"); + expect(prompt).not.toContain("bootstrapMaxChars"); }); it("shows timezone section for 12h, 24h, and timezone-only modes", () => { @@ -1073,12 +1078,15 @@ describe("buildAgentSystemPrompt", () => { }); }); -describe("buildAgentUserPromptPrefix", () => { +describe("buildAgentBootstrapSystemContext", () => { it("uses friendly full bootstrap wording that is truthful about completion blockers", () => { - const prompt = buildAgentUserPromptPrefix({ bootstrapMode: "full" }); + const prompt = buildAgentBootstrapSystemContext({ + bootstrapMode: "full", + hasBootstrapFileInProjectContext: true, + }).join("\n"); - expect(prompt).toContain("[Bootstrap pending]"); - expect(prompt).toContain("Please read BOOTSTRAP.md"); + expect(prompt).toContain("## Bootstrap Pending"); + expect(prompt).toContain("BOOTSTRAP.md is included below in Project Context"); expect(prompt).toContain("If this run can complete the BOOTSTRAP.md workflow, do so."); expect(prompt).toContain("explain the blocker briefly"); expect(prompt).toContain("offer the simplest next step"); @@ -1087,9 +1095,9 @@ describe("buildAgentUserPromptPrefix", () => { }); it("uses limited bootstrap wording for constrained user-facing runs", () => { - const prompt = buildAgentUserPromptPrefix({ bootstrapMode: "limited" }); + const prompt = buildAgentBootstrapSystemContext({ bootstrapMode: "limited" }).join("\n"); - expect(prompt).toContain("[Bootstrap pending]"); + expect(prompt).toContain("## Bootstrap Pending"); expect(prompt).toContain("cannot safely complete the full BOOTSTRAP.md workflow here"); expect(prompt).toContain("Do not claim bootstrap is complete"); expect(prompt).toContain("do not use a generic first greeting"); @@ -1097,8 +1105,38 @@ describe("buildAgentUserPromptPrefix", () => { }); it("returns nothing when bootstrap is not pending", () => { - expect(buildAgentUserPromptPrefix({ bootstrapMode: "none" })).toBeUndefined(); - expect(buildAgentUserPromptPrefix({})).toBeUndefined(); + expect(buildAgentBootstrapSystemContext({ bootstrapMode: "none" })).toEqual([]); + expect(buildAgentBootstrapSystemContext({})).toEqual([]); + }); +}); + +describe("buildAgentBootstrapSystemPromptSupplement", () => { + it("adds pending bootstrap guidance and BOOTSTRAP.md contents for override prompts", () => { + const supplement = buildAgentBootstrapSystemPromptSupplement({ + bootstrapMode: "full", + contextFiles: [{ path: "/tmp/openclaw/BOOTSTRAP.md", content: "Ask who I am." }], + }); + + expect(supplement).toContain("## Bootstrap Pending"); + expect(supplement).toContain("BOOTSTRAP.md is included below in Project Context"); + expect(supplement).toContain("## /tmp/openclaw/BOOTSTRAP.md"); + expect(supplement).toContain("Ask who I am."); + }); + + it("appends bootstrap supplement to configured system prompt overrides", () => { + const prompt = appendAgentBootstrapSystemPromptSupplement({ + systemPrompt: "Custom override prompt.", + bootstrapMode: "full", + bootstrapTruncationNotice: + "[Bootstrap truncation warning]\nSome workspace bootstrap files were truncated before Project Context injection.\nTreat Project Context as partial and read the relevant files directly if details seem missing.", + contextFiles: [{ path: "/tmp/openclaw/BOOTSTRAP.md", content: "Ask who I am." }], + }); + + expect(prompt).toContain("Custom override prompt."); + expect(prompt).toContain("## Bootstrap Pending"); + expect(prompt).toContain("Ask who I am."); + expect(prompt).toContain("## Bootstrap Context Notice"); + expect(prompt).toContain("[Bootstrap truncation warning]"); }); }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 0a628d123a5..82893f83e04 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -104,6 +104,10 @@ function isDynamicContextFile(pathValue: string): boolean { return DYNAMIC_CONTEXT_FILE_BASENAMES.has(getContextFileBasename(pathValue)); } +function isBootstrapContextFile(pathValue: string): boolean { + return /(^|[\\/])BOOTSTRAP\.md$/iu.test(pathValue.trim()); +} + function sanitizeContextFileContentForPrompt(content: string): string { // Claude Code subscription mode rejects this exact prompt-policy quote when it // appears in system context. The live heartbeat user turn still carries the @@ -223,32 +227,83 @@ function buildMemorySection(params: { }); } -export function buildAgentUserPromptPrefix(params: { +export function buildAgentBootstrapSystemContext(params: { bootstrapMode?: BootstrapMode; -}): string | undefined { + hasBootstrapFileInProjectContext?: boolean; +}): string[] { if (!params.bootstrapMode || params.bootstrapMode === "none") { - return undefined; + return []; } if (params.bootstrapMode === "limited") { return [ - "[Bootstrap pending]", + "## Bootstrap Pending", ...buildLimitedBootstrapPromptLines({ introLine: "Bootstrap is still pending for this workspace, but this run cannot safely complete the full BOOTSTRAP.md workflow here.", nextStepLine: "Typical next steps include switching to a primary interactive run with normal workspace access or having the user complete the canonical BOOTSTRAP.md deletion afterward.", }), - ].join("\n"); + "", + ]; } return [ - "[Bootstrap pending]", + "## Bootstrap Pending", ...buildFullBootstrapPromptLines({ - readLine: - "Please read BOOTSTRAP.md from the workspace and follow it before replying normally.", + readLine: params.hasBootstrapFileInProjectContext + ? "BOOTSTRAP.md is included below in Project Context; follow it before replying normally." + : "Please read BOOTSTRAP.md from the workspace and follow it before replying normally.", firstReplyLine: "Your first user-visible reply for a bootstrap-pending workspace must follow BOOTSTRAP.md, not a generic greeting.", }), - ].join("\n"); + "", + ]; +} + +export function buildAgentBootstrapSystemPromptSupplement(params: { + bootstrapMode?: BootstrapMode; + bootstrapTruncationNotice?: string; + contextFiles?: EmbeddedContextFile[]; +}): string | undefined { + const bootstrapFiles = + params.bootstrapMode === "full" + ? sortContextFilesForPrompt(params.contextFiles ?? []).filter((file) => + isBootstrapContextFile(file.path), + ) + : []; + const lines = [ + ...buildAgentBootstrapSystemContext({ + bootstrapMode: params.bootstrapMode, + hasBootstrapFileInProjectContext: bootstrapFiles.length > 0, + }), + ]; + const bootstrapTruncationNotice = params.bootstrapTruncationNotice?.trim(); + if (bootstrapTruncationNotice) { + lines.push("## Bootstrap Context Notice", bootstrapTruncationNotice, ""); + } + if (bootstrapFiles.length > 0) { + lines.push( + ...buildProjectContextSection({ + files: bootstrapFiles, + heading: "# Project Context", + dynamic: false, + }), + ); + } + const supplement = lines.join("\n").trim(); + return supplement.length > 0 ? supplement : undefined; +} + +export function appendAgentBootstrapSystemPromptSupplement(params: { + systemPrompt: string; + bootstrapMode?: BootstrapMode; + bootstrapTruncationNotice?: string; + contextFiles?: EmbeddedContextFile[]; +}): string { + const supplement = buildAgentBootstrapSystemPromptSupplement(params); + if (!supplement) { + return params.systemPrompt; + } + return `${params.systemPrompt.trimEnd()}\n\n${supplement}`; } function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: boolean) { @@ -503,6 +558,8 @@ export function buildAgentSystemPrompt(params: { userTime?: string; userTimeFormat?: ResolvedTimeFormat; contextFiles?: EmbeddedContextFile[]; + bootstrapMode?: BootstrapMode; + bootstrapTruncationNotice?: string; skillsPrompt?: string; heartbeatPrompt?: string; docsPath?: string; @@ -762,6 +819,14 @@ export function buildAgentSystemPrompt(params: { const orderedContextFiles = sortContextFilesForPrompt(validContextFiles); const stableContextFiles = orderedContextFiles.filter((file) => !isDynamicContextFile(file.path)); const dynamicContextFiles = orderedContextFiles.filter((file) => isDynamicContextFile(file.path)); + const hasBootstrapFileInProjectContext = orderedContextFiles.some((file) => + isBootstrapContextFile(file.path), + ); + const bootstrapSystemContext = buildAgentBootstrapSystemContext({ + bootstrapMode: params.bootstrapMode, + hasBootstrapFileInProjectContext, + }); + const bootstrapTruncationNotice = params.bootstrapTruncationNotice?.trim(); const stablePrefixCacheKey = hashStablePromptInput({ workspaceDir: params.workspaceDir, promptMode, @@ -787,6 +852,9 @@ export function buildAgentSystemPrompt(params: { displayWorkspaceDir, workspaceGuidance, workspaceNotes, + bootstrapMode: params.bootstrapMode, + bootstrapSystemContext, + bootstrapTruncationNotice, docsPath: params.docsPath, sourcePath: params.sourcePath, skillsPrompt, @@ -995,6 +1063,10 @@ export function buildAgentSystemPrompt(params: { ...buildTimeSection({ userTimezone, }), + ...bootstrapSystemContext, + bootstrapTruncationNotice ? "## Bootstrap Context Notice" : "", + bootstrapTruncationNotice ?? "", + bootstrapTruncationNotice ? "" : "", "## Workspace Files (injected)", "These user-editable files are loaded by OpenClaw and included below in Project Context.", "",