refactor(agents): own system prompt assembly

This commit is contained in:
Peter Steinberger
2026-05-29 01:22:02 +01:00
parent c3ff31e770
commit e12a6d6a67
45 changed files with 434 additions and 905 deletions

View File

@@ -562,20 +562,6 @@ Optional CLI backends for text-only fallback runs (no tool calls). Useful as a b
first compaction summary exists. Auth profile or credential-epoch changes
still never raw-reseed.
### `agents.defaults.systemPromptOverride`
Replace the entire OpenClaw-assembled system prompt with a fixed string. Set at the default level (`agents.defaults.systemPromptOverride`) or per agent (`agents.list[].systemPromptOverride`). Per-agent values take precedence; an empty or whitespace-only value is ignored. Useful for controlled prompt experiments.
```json5
{
agents: {
defaults: {
systemPromptOverride: "You are a helpful assistant.",
},
},
}
```
### `agents.defaults.promptOverlays`
Provider-independent prompt overlays applied by model family on OpenClaw-assembled prompt surfaces. GPT-5-family model ids receive the shared behavior contract across OpenClaw/provider routes; `personality` controls only the friendly interaction-style layer. Native Codex app-server routes keep Codex-owned base/model instructions instead of this OpenClaw GPT-5 overlay, and OpenClaw disables Codex's built-in personality for native threads.

View File

@@ -230,7 +230,7 @@ Current runtime behaviour:
- Provider-owned raw config lives under `realtime.providers.<providerId>`.
- Voice Call exposes the shared `openclaw_agent_consult` realtime tool by default. The realtime model can call it when the caller asks for deeper reasoning, current information, or normal OpenClaw tools.
- `realtime.consultPolicy` optionally adds guidance for when the realtime model should call `openclaw_agent_consult`.
- `realtime.agentContext.enabled` is default-off. When enabled, Voice Call injects a bounded agent identity, system prompt override, and selected workspace-file capsule into the realtime provider instructions at session setup.
- `realtime.agentContext.enabled` is default-off. When enabled, Voice Call injects a bounded agent identity and selected workspace-file capsule into the realtime provider instructions at session setup.
- `realtime.fastContext.enabled` is default-off. When enabled, Voice Call first searches indexed memory/session context for the consult question and returns those snippets to the realtime model within `realtime.fastContext.timeoutMs` before falling back to the full consult agent only if `realtime.fastContext.fallbackToConsult` is true.
- If `realtime.provider` points at an unregistered provider, or no realtime voice provider is registered at all, Voice Call logs a warning and skips realtime media instead of failing the whole plugin.
- Consult session keys reuse the stored call session when available, then fall back to the configured `sessionScope` (`per-phone` by default, or `per-call` for isolated calls).
@@ -278,7 +278,6 @@ for tool work, current information, memory lookups, or workspace state.
enabled: true,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
},

View File

@@ -271,8 +271,8 @@ describe("normalizeClaudeBackendConfig", () => {
});
it("passes system prompt on every turn (issue #80374 — systemPromptWhen must be 'always')", () => {
// Before fix this was hardcoded to "first", which silently dropped
// systemPromptOverride on every resumed / compacted claude-cli session.
// Before fix this was hardcoded to "first", which silently dropped updated
// OpenClaw system prompt context on resumed / compacted claude-cli sessions.
const backend = buildAnthropicCliBackend();
expect(backend.config.systemPromptWhen).toBe("always");
});

View File

@@ -5505,7 +5505,7 @@ describe("DiscordVoiceManager", () => {
await vi.waitFor(() => expect(release).toHaveBeenCalledTimes(1));
});
it("passes per-channel system prompt overrides to voice agent runs", async () => {
it("passes per-channel system prompt context to voice agent runs", async () => {
const client = createClient();
client.fetchMember.mockResolvedValue({
nickname: "Guest Nick",

View File

@@ -125,7 +125,7 @@ const voiceCallConfigSchema = {
},
"realtime.agentContext.enabled": {
label: "Enable Agent Voice Context",
help: "Injects a compact agent identity, system prompt, and workspace context capsule into realtime voice instructions.",
help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
advanced: true,
},
"realtime.agentContext.maxChars": {
@@ -136,10 +136,6 @@ const voiceCallConfigSchema = {
label: "Include Agent Identity",
advanced: true,
},
"realtime.agentContext.includeSystemPrompt": {
label: "Include Agent System Prompt",
advanced: true,
},
"realtime.agentContext.includeWorkspaceFiles": {
label: "Include Agent Workspace Files",
advanced: true,

View File

@@ -188,7 +188,7 @@
},
"realtime.agentContext.enabled": {
"label": "Enable Agent Voice Context",
"help": "Injects a compact agent identity, system prompt, and workspace context capsule into realtime voice instructions.",
"help": "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
"advanced": true
},
"realtime.agentContext.maxChars": {
@@ -199,10 +199,6 @@
"label": "Include Agent Identity",
"advanced": true
},
"realtime.agentContext.includeSystemPrompt": {
"label": "Include Agent System Prompt",
"advanced": true
},
"realtime.agentContext.includeWorkspaceFiles": {
"label": "Include Agent Workspace Files",
"advanced": true
@@ -617,9 +613,6 @@
"includeIdentity": {
"type": "boolean"
},
"includeSystemPrompt": {
"type": "boolean"
},
"includeWorkspaceFiles": {
"type": "boolean"
},

View File

@@ -62,6 +62,35 @@ describe("voice-call config compatibility", () => {
expect(streaming?.sttModel).toBeUndefined();
});
it("removes legacy realtime agentContext system prompt toggle", () => {
const normalized = normalizeVoiceCallLegacyConfigInput({
realtime: {
agentContext: {
enabled: true,
includeSystemPrompt: false,
includeWorkspaceFiles: true,
},
},
});
const agentContext = (
normalized.realtime as
| {
agentContext?: {
enabled?: boolean;
includeSystemPrompt?: unknown;
includeWorkspaceFiles?: boolean;
};
}
| undefined
)?.agentContext;
expect(agentContext).toEqual({
enabled: true,
includeWorkspaceFiles: true,
});
});
it("does not migrate non-finite legacy streaming numbers", () => {
const migration = migrateVoiceCallLegacyConfigInput({
value: {
@@ -104,6 +133,11 @@ describe("voice-call config compatibility", () => {
sttProvider: "openai",
openaiApiKey: "sk-test", // pragma: allowlist secret
},
realtime: {
agentContext: {
includeSystemPrompt: true,
},
},
};
expect(collectVoiceCallLegacyConfigIssues(raw)).toEqual([
@@ -127,6 +161,12 @@ describe("voice-call config compatibility", () => {
replacement: "streaming.providers.openai.apiKey",
message: "Move streaming.openaiApiKey to streaming.providers.openai.apiKey.",
},
{
path: "realtime.agentContext.includeSystemPrompt",
replacement: "realtime.agentContext",
message:
"Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.",
},
]);
expect(
formatVoiceCallLegacyConfigWarnings({
@@ -140,6 +180,7 @@ describe("voice-call config compatibility", () => {
"[voice-call] plugins.entries.voice-call.config.twilio.from: Move twilio.from to fromNumber.",
"[voice-call] plugins.entries.voice-call.config.streaming.sttProvider: Move streaming.sttProvider to streaming.provider.",
"[voice-call] plugins.entries.voice-call.config.streaming.openaiApiKey: Move streaming.openaiApiKey to streaming.providers.openai.apiKey.",
"[voice-call] plugins.entries.voice-call.config.realtime.agentContext.includeSystemPrompt: Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.",
]);
});
@@ -150,6 +191,11 @@ describe("voice-call config compatibility", () => {
streaming: {
sttProvider: "openai",
},
realtime: {
agentContext: {
includeSystemPrompt: true,
},
},
},
configPathPrefix: "plugins.entries.voice-call.config",
});
@@ -157,6 +203,7 @@ describe("voice-call config compatibility", () => {
expect(migration.changes).toEqual([
'Moved plugins.entries.voice-call.config.provider "log" → "mock".',
"Moved plugins.entries.voice-call.config.streaming.sttProvider → plugins.entries.voice-call.config.streaming.provider.",
"Removed plugins.entries.voice-call.config.realtime.agentContext.includeSystemPrompt.",
]);
});
});

View File

@@ -40,6 +40,8 @@ function mergeProviderConfig(
export function collectVoiceCallLegacyConfigIssues(value: unknown): VoiceCallLegacyConfigIssue[] {
const raw = asObject(value) ?? {};
const realtime = asObject(raw.realtime);
const realtimeAgentContext = asObject(realtime?.agentContext);
const twilio = asObject(raw.twilio);
const streaming = asObject(raw.streaming);
@@ -93,6 +95,17 @@ export function collectVoiceCallLegacyConfigIssues(value: unknown): VoiceCallLeg
message: "Move streaming.vadThreshold to streaming.providers.openai.vadThreshold.",
});
}
if (
realtimeAgentContext &&
Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")
) {
issues.push({
path: "realtime.agentContext.includeSystemPrompt",
replacement: "realtime.agentContext",
message:
"Remove realtime.agentContext.includeSystemPrompt; realtime context now uses the generated agent prompt.",
});
}
return issues;
}
@@ -124,6 +137,8 @@ export function migrateVoiceCallLegacyConfigInput(params: {
issues: VoiceCallLegacyConfigIssue[];
} {
const raw = asObject(params.value) ?? {};
const realtime = asObject(raw.realtime);
const realtimeAgentContext = asObject(realtime?.agentContext);
const twilio = asObject(raw.twilio);
const streaming = asObject(raw.streaming);
const configPathPrefix = params.configPathPrefix ?? "plugins.entries.voice-call.config";
@@ -174,12 +189,29 @@ export function migrateVoiceCallLegacyConfigInput(params: {
delete normalizedTwilio.from;
}
const normalizedRealtimeAgentContext = realtimeAgentContext
? {
...realtimeAgentContext,
}
: undefined;
if (normalizedRealtimeAgentContext) {
delete normalizedRealtimeAgentContext.includeSystemPrompt;
}
const normalizedRealtime = realtime
? {
...realtime,
agentContext: normalizedRealtimeAgentContext ?? realtime.agentContext,
}
: undefined;
const config = {
...raw,
provider: raw.provider === "log" ? "mock" : raw.provider,
fromNumber: raw.fromNumber ?? (typeof twilio?.from === "string" ? twilio.from : undefined),
twilio: normalizedTwilio,
streaming: normalizedStreaming,
realtime: normalizedRealtime,
};
const changes: string[] = [];
@@ -218,6 +250,12 @@ export function migrateVoiceCallLegacyConfigInput(params: {
} else if (typeof streaming?.vadThreshold === "number") {
changes.push(`Removed invalid ${configPathPrefix}.streaming.vadThreshold.`);
}
if (
realtimeAgentContext &&
Object.prototype.hasOwnProperty.call(realtimeAgentContext, "includeSystemPrompt")
) {
changes.push(`Removed ${configPathPrefix}.realtime.agentContext.includeSystemPrompt.`);
}
return { config, changes, issues };
}

View File

@@ -427,7 +427,6 @@ describe("normalizeVoiceCallConfig", () => {
enabled: false,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
});

View File

@@ -267,8 +267,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z
maxChars: z.number().int().positive().default(6000),
/** Include configured agent identity fields. */
includeIdentity: z.boolean().default(true),
/** Include agents.defaults/list systemPromptOverride when configured. */
includeSystemPrompt: z.boolean().default(true),
/** Include selected workspace files such as SOUL.md and IDENTITY.md. */
includeWorkspaceFiles: z.boolean().default(true),
/** Workspace-relative files to include, bounded by maxChars. */
@@ -279,7 +277,6 @@ const VoiceCallRealtimeAgentContextConfigSchema = z
enabled: false,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
});
@@ -350,7 +347,6 @@ const VoiceCallRealtimeConfigSchema = z
enabled: false,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
},

View File

@@ -57,17 +57,13 @@ function createAgentRuntime(workspaceDir: string): CoreAgentDeps {
}
describe("buildRealtimeVoiceInstructions", () => {
it("injects bounded identity, system prompt, and workspace context", async () => {
it("injects bounded identity and workspace context", async () => {
const workspaceDir = await createWorkspace();
await writeFile(path.join(workspaceDir, "SOUL.md"), "Stay quick, direct, and warm.\n");
await writeFile(path.join(workspaceDir, "IDENTITY.md"), "Name: Claw Voice\nVibe: snappy\n");
await writeFile(path.join(workspaceDir, "SECRET.md"), "do not include\n");
const coreConfig = {
agents: {
list: [{ id: "voice", systemPromptOverride: "Keep spoken answers short." }],
},
} as CoreConfig;
const coreConfig = { agents: { list: [{ id: "voice" }] } } as CoreConfig;
const instructions = await buildRealtimeVoiceInstructions({
baseInstructions: "Base voice instructions.",
@@ -77,7 +73,6 @@ describe("buildRealtimeVoiceInstructions", () => {
enabled: true,
maxChars: 2000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "../SECRET.md"],
},
@@ -92,7 +87,6 @@ describe("buildRealtimeVoiceInstructions", () => {
expect(instructions).toContain("- Agent id: voice");
expect(instructions).toContain("- Name: Claw Voice");
expect(instructions).toContain("- Vibe: snappy");
expect(instructions).toContain("Keep spoken answers short.");
expect(instructions).toContain("### SOUL.md");
expect(instructions).toContain("Stay quick, direct, and warm.");
expect(instructions).toContain("### IDENTITY.md");

View File

@@ -7,7 +7,6 @@ import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
type AgentEntryLike = {
id?: unknown;
systemPromptOverride?: unknown;
};
type VoiceIdentityLike = {
@@ -27,18 +26,6 @@ function readAgentEntries(cfg: CoreConfig): AgentEntryLike[] {
: [];
}
function resolveAgentSystemPromptOverride(cfg: CoreConfig, agentId: string): string | undefined {
const entries = readAgentEntries(cfg);
const entry = entries.find((candidate) => normalizeString(candidate.id) === agentId);
return (
normalizeString(entry?.systemPromptOverride) ??
normalizeString(
(cfg as { agents?: { defaults?: { systemPromptOverride?: unknown } } }).agents?.defaults
?.systemPromptOverride,
)
);
}
function limitText(text: string, maxChars: number): string {
if (text.length <= maxChars) {
return text;
@@ -119,13 +106,6 @@ export async function buildRealtimeVoiceInstructions(params: {
}
}
if (contextConfig.includeSystemPrompt) {
const systemPrompt = resolveAgentSystemPromptOverride(params.coreConfig, agentId);
if (systemPrompt) {
capsule.push(`Configured system prompt override:\n${systemPrompt}`);
}
}
if (contextConfig.includeWorkspaceFiles) {
const workspaceDir = params.agentRuntime.resolveAgentWorkspaceDir(
params.coreConfig as OpenClawConfig,

View File

@@ -64,7 +64,6 @@ export function createVoiceCallBaseConfig(params?: {
enabled: false,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
},

View File

@@ -76,7 +76,6 @@ function makeHandler(
enabled: false,
maxChars: 6000,
includeIdentity: true,
includeSystemPrompt: true,
includeWorkspaceFiles: true,
files: ["SOUL.md", "IDENTITY.md", "USER.md"],
},

View File

@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
// Live prompt probe for Anthropic setup-token and Claude CLI prompt-path debugging.
// Usage:
// OPENCLAW_PROMPT_TRANSPORT=direct|gateway
// OPENCLAW_PROMPT_MODE=extra|override
// OPENCLAW_PROMPT_MODE=extra
// OPENCLAW_PROMPT_TEXT='...'
// OPENCLAW_PROMPT_CAPTURE=1
// pnpm probe:anthropic:prompt
@@ -27,8 +27,7 @@ import {
} from "./lib/dev-tooling-safety.ts";
const TRANSPORT = process.env.OPENCLAW_PROMPT_TRANSPORT?.trim() === "direct" ? "direct" : "gateway";
const GATEWAY_PROMPT_MODE =
process.env.OPENCLAW_PROMPT_MODE?.trim() === "override" ? "override" : "extra";
const GATEWAY_PROMPT_MODE = "extra";
const PROMPT_TEXT = process.env.OPENCLAW_PROMPT_TEXT?.trim() ?? "";
const PROMPT_LIST_JSON = process.env.OPENCLAW_PROMPT_LIST_JSON?.trim() ?? "";
const USER_PROMPT = process.env.OPENCLAW_USER_PROMPT?.trim() || "is clawd here?";
@@ -79,7 +78,7 @@ type PromptResult = {
prompt: string;
ok: boolean;
transport: "direct" | "gateway";
promptMode?: "extra" | "override";
promptMode?: "extra";
exitCode?: number | null;
signal?: NodeJS.Signals | null;
status?: string;
@@ -578,7 +577,6 @@ async function runGatewayPrompt(prompt: string): Promise<PromptResult> {
heartbeat: {
includeSystemPromptSection: false,
},
...(GATEWAY_PROMPT_MODE === "override" ? { systemPromptOverride: prompt } : {}),
},
},
},

View File

@@ -16,7 +16,6 @@ export type ResolvedAgentConfig = {
name?: string;
workspace?: string;
agentDir?: string;
systemPromptOverride?: AgentEntry["systemPromptOverride"];
model?: AgentEntry["model"];
thinkingDefault?: AgentEntry["thinkingDefault"];
verboseDefault?: AgentDefaultsConfig["verboseDefault"];
@@ -117,7 +116,6 @@ export function resolveAgentConfig(
name: readStringValue(entry.name),
workspace: readStringValue(entry.workspace),
agentDir: readStringValue(entry.agentDir),
systemPromptOverride: readStringValue(entry.systemPromptOverride),
model:
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
? entry.model

View File

@@ -115,7 +115,6 @@ async function createTestMcpLoopbackServer(port = 0) {
function createCliBackendConfig(
params: {
systemPromptOverride?: string | null;
bundleMcp?: boolean;
reseedFromRawTranscriptWhenUncompacted?: boolean;
} = {},
@@ -123,9 +122,6 @@ function createCliBackendConfig(
return {
agents: {
defaults: {
...(params.systemPromptOverride !== null
? { systemPromptOverride: params.systemPromptOverride ?? "test system prompt" }
: {}),
cliBackends: {
"test-cli": {
command: "test-cli",
@@ -560,7 +556,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-legacy-merge",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.params.prompt).toBe("prompt prepend\n\nlegacy prepend\n\nlatest ask");
@@ -595,13 +591,14 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-hook-failure",
config: createCliBackendConfig({ systemPromptOverride: "base extra system" }),
config: createCliBackendConfig(),
});
expect(context.params.prompt).toBe("latest ask");
expect(context.systemPrompt).toBe(
"base extra system\n\nCurrent model identity: test-cli/test-model. If asked what model you are, answer with this value for the current run.",
expect(context.systemPrompt).toContain(
"You are a personal assistant running inside OpenClaw.",
);
expect(context.systemPrompt).toContain("Current model identity: test-cli/test-model.");
expect(context.systemPrompt).not.toContain("hook exploded");
expect(hookRunner.runBeforePromptBuild).toHaveBeenCalledOnce();
} finally {
@@ -840,7 +837,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
sessionId: "cli-session",
cwdHash: hashCliSessionText(dir),
},
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.systemPrompt).toContain("## Inbound Context\nchannel=telegram");
@@ -865,7 +862,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-cwd-prompt",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.cwd).toBe(taskDir);
@@ -937,7 +934,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
extraSystemPromptHash: hashCliSessionText("old stable prompt"),
},
config: createCliBackendConfig({
systemPromptOverride: null,
reseedFromRawTranscriptWhenUncompacted: true,
}),
});
@@ -978,7 +974,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
cwdHash: hashCliSessionText(dir),
},
config: createCliBackendConfig({
systemPromptOverride: null,
reseedFromRawTranscriptWhenUncompacted: true,
}),
});
@@ -1141,7 +1136,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-loopback-prompt-tools",
config: createCliBackendConfig({ bundleMcp: true, systemPromptOverride: null }),
config: createCliBackendConfig({ bundleMcp: true }),
cliSessionBinding: {
sessionId: "cli-session",
promptToolNamesHash: "old-tool-surface",
@@ -1231,7 +1226,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-test-loopback-prompt-tools-fallback",
config: createCliBackendConfig({ bundleMcp: true, systemPromptOverride: null }),
config: createCliBackendConfig({ bundleMcp: true }),
});
expect(ensureMcpLoopbackServer).toHaveBeenCalledTimes(1);
@@ -1435,7 +1430,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
runId: "run-77011-missing",
cliSessionBinding: { sessionId: "stale-claude-sid" },
cliSessionId: "stale-claude-sid",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(transcriptCheck).toHaveBeenCalledWith({ sessionId: "stale-claude-sid" });
@@ -1483,7 +1478,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
runId: "run-77011-present",
cliSessionBinding: { sessionId: "live-claude-sid", cwdHash: hashCliSessionText(dir) },
cliSessionId: "live-claude-sid",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(transcriptCheck).toHaveBeenCalledWith({ sessionId: "live-claude-sid" });
@@ -1546,7 +1541,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "opus",
timeoutMs: 1_000,
runId: "run-claude-plugin-skills-prompt",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
skillsSnapshot: {
prompt: [
"<available_skills>",
@@ -1623,7 +1618,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "opus",
timeoutMs: 1_000,
runId: "run-claude-plugin-skills-prompt-fallback",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
skillsSnapshot: {
prompt: [
"<available_skills>",
@@ -1716,7 +1711,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "opus",
timeoutMs: 1_000,
runId: "run-claude-plugin-skills-prompt-materialization-fallback",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
skillsSnapshot: {
prompt: [
"<available_skills>",
@@ -1775,7 +1770,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
timeoutMs: 1_000,
runId: "run-77011-other-provider",
cliSessionBinding: { sessionId: "test-cli-sid", cwdHash: hashCliSessionText(dir) },
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(transcriptCheck).not.toHaveBeenCalled();
@@ -1826,7 +1821,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "claude-haiku-3-5",
timeoutMs: 1_000,
runId: "run-auto-claude-reseed-history-chars",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.openClawHistoryPrompt).toBeDefined();
@@ -1881,7 +1876,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "claude-opus-4-7",
timeoutMs: 1_000,
runId: "run-auto-claude-alias-reseed-history-chars",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.openClawHistoryPrompt).toBeDefined();
@@ -1915,7 +1910,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
model: "test-model",
timeoutMs: 1_000,
runId: "run-default-reseed-history-chars",
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.openClawHistoryPrompt).toBeDefined();
@@ -1990,7 +1985,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
timeoutMs: 1_000,
runId: "run-raw-reseed-cap-override",
cliSessionBinding: { sessionId: "cli-session", cwdHash: hashCliSessionText(dir) },
config: createCliBackendConfig({ systemPromptOverride: null }),
config: createCliBackendConfig(),
});
expect(context.reusableCliSession).toEqual({ sessionId: "cli-session" });

View File

@@ -55,7 +55,6 @@ import { buildCurrentInboundPrompt } from "../embedded-agent-runner/run/runtime-
import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js";
import { applyPluginTextReplacements } from "../plugin-text-transforms.js";
import { resolveSkillsPromptForRun } from "../skills.js";
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
import { buildSystemPromptReport } from "../system-prompt-report.js";
import { appendModelIdentitySystemPrompt } from "../system-prompt.js";
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
@@ -426,29 +425,24 @@ export async function prepareCliRunContext(
agentId: sessionAgentId,
});
const systemPromptSkillsPrompt = claudeSkillsPlugin.args.length > 0 ? "" : skillsPrompt;
const builtSystemPrompt =
resolveSystemPromptOverride({
config: params.config,
agentId: sessionAgentId,
}) ??
buildCliAgentSystemPrompt({
workspaceDir,
cwd,
config: params.config,
defaultThinkLevel: params.thinkLevel,
extraSystemPrompt,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
silentReplyPromptMode: params.silentReplyPromptMode,
ownerNumbers: params.ownerNumbers,
heartbeatPrompt,
docsPath: openClawReferences.docsPath ?? undefined,
sourcePath: openClawReferences.sourcePath ?? undefined,
skillsPrompt: systemPromptSkillsPrompt,
tools: promptTools,
contextFiles,
modelDisplay,
agentId: sessionAgentId,
});
const builtSystemPrompt = buildCliAgentSystemPrompt({
workspaceDir,
cwd,
config: params.config,
defaultThinkLevel: params.thinkLevel,
extraSystemPrompt,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
silentReplyPromptMode: params.silentReplyPromptMode,
ownerNumbers: params.ownerNumbers,
heartbeatPrompt,
docsPath: openClawReferences.docsPath ?? undefined,
sourcePath: openClawReferences.sourcePath ?? undefined,
skillsPrompt: systemPromptSkillsPrompt,
tools: promptTools,
contextFiles,
modelDisplay,
agentId: sessionAgentId,
});
const transformedSystemPrompt =
backendResolved.transformSystemPrompt?.({
config: params.config,

View File

@@ -1,14 +0,0 @@
import { describe, expect, it } from "vitest";
import { createSystemPromptOverride } from "./embedded-agent-runner.js";
describe("createSystemPromptOverride", () => {
it("returns the override prompt trimmed", () => {
const override = createSystemPromptOverride("OVERRIDE");
expect(override()).toBe("OVERRIDE");
});
it("returns an empty string for blank overrides", () => {
const override = createSystemPromptOverride(" \n ");
expect(override()).toBe("");
});
});

View File

@@ -16,7 +16,6 @@ export {
waitForEmbeddedAgentRunEnd,
} from "./embedded-agent-runner/runs.js";
export { buildEmbeddedSandboxInfo } from "./embedded-agent-runner/sandbox-info.js";
export { createSystemPromptOverride } from "./embedded-agent-runner/system-prompt.js";
export { splitSdkTools } from "./embedded-agent-runner/tool-split.js";
export type {
EmbeddedAgentMeta,

View File

@@ -854,9 +854,8 @@ export async function loadCompactHooksHarness(): Promise<{
}));
vi.doMock("./system-prompt.js", () => ({
applySystemPromptOverrideToSession: vi.fn(),
applySystemPromptToSession: vi.fn(),
buildEmbeddedSystemPrompt: buildEmbeddedSystemPromptMock,
createSystemPromptOverride: vi.fn(() => () => ""),
}));
vi.doMock("./utils.js", async () => {

View File

@@ -106,7 +106,6 @@ import {
applySkillEnvOverridesFromSnapshot,
resolveSkillsPromptForRun,
} from "../skills.js";
import { resolveSystemPromptOverride } from "../system-prompt-override.js";
import { filterRuntimeCompatibleTools } from "../tool-schema-projection.js";
import { logRuntimeToolSchemaQuarantine } from "../tool-schema-quarantine.js";
import {
@@ -156,11 +155,7 @@ import {
resolveEmbeddedAgentBaseStreamFn,
resolveEmbeddedAgentStreamFn,
} from "./stream-resolution.js";
import {
applySystemPromptOverrideToSession,
buildEmbeddedSystemPrompt,
createSystemPromptOverride,
} from "./system-prompt.js";
import { applySystemPromptToSession, buildEmbeddedSystemPrompt } from "./system-prompt.js";
import {
collectAllowedToolNames,
collectRegisteredToolNames,
@@ -960,67 +955,60 @@ async function compactEmbeddedAgentSessionDirectOnce(
};
const promptContribution =
runtimePlan.prompt.resolveSystemPromptContribution(promptContributionContext);
const buildSystemPromptOverride = (defaultThinkLevel: ThinkLevel) => {
const builtSystemPrompt =
resolveSystemPromptOverride({
const buildSystemPromptText = (defaultThinkLevel: ThinkLevel) => {
const builtSystemPrompt = buildEmbeddedSystemPrompt({
config: params.config,
agentId: sessionAgentId,
workspaceDir: effectiveWorkspace,
defaultThinkLevel,
reasoningLevel: params.reasoningLevel ?? "off",
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
reasoningTagHint,
heartbeatPrompt: resolveHeartbeatPromptForSystemPrompt({
config: params.config,
agentId: sessionAgentId,
}) ??
buildEmbeddedSystemPrompt({
config: params.config,
agentId: sessionAgentId,
workspaceDir: effectiveWorkspace,
defaultThinkLevel,
reasoningLevel: params.reasoningLevel ?? "off",
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
reasoningTagHint,
heartbeatPrompt: resolveHeartbeatPromptForSystemPrompt({
config: params.config,
agentId: sessionAgentId,
defaultAgentId,
}),
skillsPrompt,
docsPath: openClawReferences.docsPath ?? undefined,
sourcePath: openClawReferences.sourcePath ?? undefined,
promptMode,
promptSurface,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
acpEnabled: isAcpRuntimeSpawnAvailable({
config: params.config,
sandboxed: sandboxInfo?.enabled === true,
}),
runtimeInfo,
reactionGuidance,
messageToolHints,
sandboxInfo,
tools: effectiveTools,
userTimezone,
userTime,
userTimeFormat,
contextFiles,
promptContribution,
nativeCommandGuidanceLines,
});
return createSystemPromptOverride(
transformProviderSystemPrompt({
provider,
config: params.config,
workspaceDir: effectiveWorkspace,
context: {
config: params.config,
agentDir,
workspaceDir: effectiveWorkspace,
provider,
modelId,
promptMode,
runtimeChannel,
runtimeCapabilities,
agentId: sessionAgentId,
systemPrompt: builtSystemPrompt,
},
defaultAgentId,
}),
);
skillsPrompt,
docsPath: openClawReferences.docsPath ?? undefined,
sourcePath: openClawReferences.sourcePath ?? undefined,
promptMode,
promptSurface,
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
acpEnabled: isAcpRuntimeSpawnAvailable({
config: params.config,
sandboxed: sandboxInfo?.enabled === true,
}),
runtimeInfo,
reactionGuidance,
messageToolHints,
sandboxInfo,
tools: effectiveTools,
userTimezone,
userTime,
userTimeFormat,
contextFiles,
promptContribution,
nativeCommandGuidanceLines,
});
return transformProviderSystemPrompt({
provider,
config: params.config,
workspaceDir: effectiveWorkspace,
context: {
config: params.config,
agentDir,
workspaceDir: effectiveWorkspace,
provider,
modelId,
promptMode,
runtimeChannel,
runtimeCapabilities,
agentId: sessionAgentId,
systemPrompt: builtSystemPrompt,
},
});
};
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
@@ -1152,7 +1140,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
resourceLoader,
});
session = createdSession.session;
applySystemPromptOverrideToSession(session, buildSystemPromptOverride(thinkLevel)());
applySystemPromptToSession(session, buildSystemPromptText(thinkLevel));
session.setActiveToolsByName(sessionToolAllowlist);
// Compaction builds the same embedded system prompt, so it must flow
// through the same transport/payload shaping stack as normal turns.

View File

@@ -23,7 +23,7 @@ const transformProviderSystemPrompt: Parameters<
>[0]["transformProviderSystemPrompt"] = ({ context }) => context.systemPrompt;
describe("buildAttemptSystemPrompt", () => {
it("injects workspace identity context without a system prompt override", () => {
it("injects workspace identity context", () => {
const result = buildAttemptSystemPrompt({
isRawModelRun: false,
transformProviderSystemPrompt,
@@ -58,10 +58,9 @@ describe("buildAttemptSystemPrompt", () => {
expect(result.systemPrompt).toContain("USER_CONTEXT_MARKER");
});
it("preserves bootstrap Project Context when a system prompt override is configured", () => {
it("preserves bootstrap Project Context", () => {
const result = buildAttemptSystemPrompt({
isRawModelRun: false,
systemPromptOverrideText: "Custom override prompt.",
transformProviderSystemPrompt,
embeddedSystemPrompt: {
workspaceDir: "/tmp/openclaw",
@@ -83,31 +82,42 @@ describe("buildAttemptSystemPrompt", () => {
path: "/tmp/openclaw/BOOTSTRAP.md",
content: "Reply with BOOTSTRAP_OK.",
},
{
path: "/tmp/openclaw/SOUL.md",
content: "SOUL_CONTEXT_MARKER",
},
{
path: "/tmp/openclaw/IDENTITY.md",
content: "IDENTITY_CONTEXT_MARKER",
},
{
path: "/tmp/openclaw/USER.md",
content: "User profile should stay in normal prompt context only.",
content: "USER_CONTEXT_MARKER",
},
],
},
providerTransform: baseProviderTransform,
});
expect(result.systemPrompt).toContain("Custom override prompt.");
expect(result.systemPrompt).toContain("Current model identity: openai/gpt-5.5.");
expect(result.systemPrompt).toContain("## Bootstrap Pending");
expect(result.systemPrompt).toContain("BOOTSTRAP.md is included below in Project Context");
expect(result.systemPrompt).toContain("## Bootstrap Context Notice");
expect(result.systemPrompt).toContain("Bootstrap context was truncated.");
expect(result.systemPrompt).toContain("# Project Context");
expect(result.systemPrompt).toContain("## /tmp/openclaw/SOUL.md");
expect(result.systemPrompt).toContain("SOUL_CONTEXT_MARKER");
expect(result.systemPrompt).toContain("## /tmp/openclaw/IDENTITY.md");
expect(result.systemPrompt).toContain("IDENTITY_CONTEXT_MARKER");
expect(result.systemPrompt).toContain("## /tmp/openclaw/USER.md");
expect(result.systemPrompt).toContain("USER_CONTEXT_MARKER");
expect(result.systemPrompt).toContain("## /tmp/openclaw/BOOTSTRAP.md");
expect(result.systemPrompt).toContain("Reply with BOOTSTRAP_OK.");
expect(result.systemPrompt).not.toContain("USER.md");
});
it("preserves runtime extra system prompt context when a system prompt override is configured", () => {
it("preserves runtime extra system prompt context", () => {
const result = buildAttemptSystemPrompt({
isRawModelRun: false,
systemPromptOverrideText: "Custom override prompt.",
transformProviderSystemPrompt,
embeddedSystemPrompt: {
workspaceDir: "/tmp/openclaw",
@@ -131,7 +141,6 @@ describe("buildAttemptSystemPrompt", () => {
providerTransform: baseProviderTransform,
});
expect(result.systemPrompt).toContain("Custom override prompt.");
expect(result.systemPrompt).toContain("Current model identity: openai/gpt-5.5.");
expect(result.systemPrompt).toContain("## Subagent Context");
expect(result.systemPrompt).toContain("RUN_MODE_TASK_77950");
@@ -167,6 +176,5 @@ describe("buildAttemptSystemPrompt", () => {
expect(result.baseSystemPrompt).toContain("BOOTSTRAP.md is included below in Project Context");
expect(result.systemPrompt).toBe("");
expect(result.systemPromptOverride()).toBe("");
});
});

View File

@@ -1,10 +1,6 @@
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { ProviderTransformSystemPromptContext } from "../../../plugins/types.js";
import {
appendAgentBootstrapSystemPromptSupplement,
appendModelIdentitySystemPrompt,
} from "../../system-prompt.js";
import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js";
import { buildEmbeddedSystemPrompt } from "../system-prompt.js";
type EmbeddedSystemPromptParams = Parameters<typeof buildEmbeddedSystemPrompt>[0];
type ProviderSystemPromptTransform = (params: {
@@ -16,7 +12,6 @@ type ProviderSystemPromptTransform = (params: {
export type BuildAttemptSystemPromptParams = {
isRawModelRun: boolean;
systemPromptOverrideText?: string;
embeddedSystemPrompt: EmbeddedSystemPromptParams;
transformProviderSystemPrompt: ProviderSystemPromptTransform;
providerTransform: {
@@ -30,42 +25,12 @@ export type BuildAttemptSystemPromptParams = {
export type AttemptSystemPrompt = {
baseSystemPrompt: string;
systemPrompt: string;
systemPromptOverride: (defaultPrompt?: string) => string;
};
function appendRuntimeExtraSystemPrompt(params: {
systemPrompt: string;
extraSystemPrompt?: string;
promptMode?: EmbeddedSystemPromptParams["promptMode"];
}): string {
const extraSystemPrompt = params.extraSystemPrompt?.trim();
if (!extraSystemPrompt || params.promptMode === "none") {
return params.systemPrompt;
}
const contextHeader =
params.promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context";
return `${params.systemPrompt.trimEnd()}\n\n${contextHeader}\n${extraSystemPrompt}\n`;
}
export function buildAttemptSystemPrompt(
params: BuildAttemptSystemPromptParams,
): AttemptSystemPrompt {
const baseSystemPrompt = params.systemPromptOverrideText
? appendModelIdentitySystemPrompt({
systemPrompt: appendRuntimeExtraSystemPrompt({
systemPrompt: appendAgentBootstrapSystemPromptSupplement({
systemPrompt: params.systemPromptOverrideText,
bootstrapMode: params.embeddedSystemPrompt.bootstrapMode,
bootstrapTruncationNotice: params.embeddedSystemPrompt.bootstrapTruncationNotice,
contextFiles: params.embeddedSystemPrompt.contextFiles,
}),
extraSystemPrompt: params.embeddedSystemPrompt.extraSystemPrompt,
promptMode: params.embeddedSystemPrompt.promptMode,
}),
model: params.embeddedSystemPrompt.runtimeInfo.model,
})
: buildEmbeddedSystemPrompt(params.embeddedSystemPrompt);
const baseSystemPrompt = buildEmbeddedSystemPrompt(params.embeddedSystemPrompt);
const systemPrompt = params.isRawModelRun
? ""
: params.transformProviderSystemPrompt({
@@ -81,6 +46,5 @@ export function buildAttemptSystemPrompt(
return {
baseSystemPrompt,
systemPrompt,
systemPromptOverride: createSystemPromptOverride(systemPrompt),
};
}

View File

@@ -1027,7 +1027,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
expect(JSON.stringify(seen.messages)).not.toContain("bootstrapMaxChars");
});
it("preserves bootstrap system context when system prompt override is configured", async () => {
it("preserves bootstrap system context in the assembled system prompt", async () => {
const seen: { prompt?: string; messages?: unknown[] } = {};
hoisted.isWorkspaceBootstrapPendingMock.mockResolvedValueOnce(true);
hoisted.createOpenClawCodingToolsMock.mockImplementationOnce(() => [
@@ -1037,14 +1037,14 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
bootstrapFiles: [
{
name: "BOOTSTRAP.md",
path: "/tmp/openclaw-override-workspace/BOOTSTRAP.md",
path: "/tmp/openclaw-bootstrap-workspace/BOOTSTRAP.md",
content: "Ask who I am.",
missing: false,
},
],
contextFiles: [
{
path: "/tmp/openclaw-override-workspace/BOOTSTRAP.md",
path: "/tmp/openclaw-bootstrap-workspace/BOOTSTRAP.md",
content: "Ask who I am.",
},
],
@@ -1055,13 +1055,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
sessionKey,
tempPaths,
attemptOverrides: {
config: {
agents: {
defaults: {
systemPromptOverride: "Custom override prompt.",
},
},
} as OpenClawConfig,
disableTools: false,
prompt: "visible ask",
transcriptPrompt: "visible ask",
@@ -1079,15 +1072,18 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
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.")) ??
"";
const promptInput = hoisted.embeddedSystemPromptInputs.at(-1) as {
bootstrapMode?: string;
contextFiles?: Array<{ path: string; content: string }>;
};
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.");
expect(promptInput.bootstrapMode).toBe("full");
expect(promptInput.contextFiles).toEqual([
{
path: "/tmp/openclaw-bootstrap-workspace/BOOTSTRAP.md",
content: "Ask who I am.",
},
]);
});
it("includes hook-adjusted bootstrap files preloaded before routing", async () => {
@@ -1106,13 +1102,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
sessionKey,
tempPaths,
attemptOverrides: {
config: {
agents: {
defaults: {
systemPromptOverride: "Custom override prompt.",
},
},
} as OpenClawConfig,
prompt: "visible ask",
transcriptPrompt: "visible ask",
trigger: "user",
@@ -1128,14 +1117,18 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
expect(hoisted.resolveBootstrapFilesForRunMock).toHaveBeenCalledOnce();
expect(hoisted.resolveBootstrapContextForRunMock).not.toHaveBeenCalled();
const systemPrompt =
hoisted.systemPromptOverrideTexts.find((text) => text.includes("Custom override prompt.")) ??
"";
const promptInput = hoisted.embeddedSystemPromptInputs.at(-1) as {
bootstrapMode?: string;
contextFiles?: Array<{ path: string; content: string }>;
};
expect(systemPrompt).toContain("## Bootstrap Pending");
expect(systemPrompt).toContain("BOOTSTRAP.md is included below in Project Context");
expect(systemPrompt).toContain(`## ${workspaceDir}/BOOTSTRAP.md`);
expect(systemPrompt).toContain("Ask who I am before continuing.");
expect(promptInput.bootstrapMode).toBe("full");
expect(promptInput.contextFiles).toEqual([
{
path: `${workspaceDir}/BOOTSTRAP.md`,
content: "Ask who I am before continuing.",
},
]);
});
it("skips bootstrap preload on completed continuation-skip turns", async () => {

View File

@@ -90,7 +90,8 @@ type AttemptSpawnWorkspaceHoisted = {
>;
limitHistoryTurnsMock: Mock<<T>(messages: T, limit: number | undefined) => T>;
preemptiveCompactionCalls: Parameters<ShouldPreemptivelyCompactBeforePromptFn>[0][];
systemPromptOverrideTexts: string[];
systemPromptTexts: string[];
embeddedSystemPromptInputs: unknown[];
sessionManager: SessionManagerMocks;
};
@@ -187,7 +188,8 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
(messages) => messages,
);
const preemptiveCompactionCalls: Parameters<ShouldPreemptivelyCompactBeforePromptFn>[0][] = [];
const systemPromptOverrideTexts: string[] = [];
const systemPromptTexts: string[] = [];
const embeddedSystemPromptInputs: unknown[] = [];
const sessionManager = {
getLeafEntry: vi.fn(() => null),
branch: vi.fn(),
@@ -228,7 +230,8 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
getHistoryLimitFromSessionKeyMock,
limitHistoryTurnsMock,
preemptiveCompactionCalls,
systemPromptOverrideTexts,
systemPromptTexts,
embeddedSystemPromptInputs,
sessionManager,
};
});
@@ -519,13 +522,13 @@ vi.mock("../system-prompt.js", async () => {
const actual = await vi.importActual<typeof import("../system-prompt.js")>("../system-prompt.js");
return {
...actual,
applySystemPromptOverrideToSession: (session: MutableSession, systemPrompt: string) => {
applySystemPromptToSession: (session: MutableSession, systemPrompt: string) => {
hoisted.systemPromptTexts.push(systemPrompt);
session.agent.state.systemPrompt = systemPrompt;
},
buildEmbeddedSystemPrompt: () => "system prompt",
createSystemPromptOverride: (prompt: string) => {
hoisted.systemPromptOverrideTexts.push(prompt);
return () => prompt;
buildEmbeddedSystemPrompt: (params: unknown) => {
hoisted.embeddedSystemPromptInputs.push(params);
return "system prompt";
},
};
});
@@ -985,7 +988,8 @@ export function resetEmbeddedAttemptHarness(
hoisted.getHistoryLimitFromSessionKeyMock.mockReset().mockReturnValue(undefined);
hoisted.limitHistoryTurnsMock.mockReset().mockImplementation((messages) => messages);
hoisted.preemptiveCompactionCalls.length = 0;
hoisted.systemPromptOverrideTexts.length = 0;
hoisted.systemPromptTexts.length = 0;
hoisted.embeddedSystemPromptInputs.length = 0;
hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null);
hoisted.sessionManager.branch.mockReset();
hoisted.sessionManager.resetLeaf.mockReset();

View File

@@ -170,7 +170,6 @@ import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../../subagent-capabilities.js";
import { resolveSystemPromptOverride } from "../../system-prompt-override.js";
import { buildSystemPromptParams } from "../../system-prompt-params.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { appendModelIdentitySystemPrompt } from "../../system-prompt.js";
@@ -246,7 +245,7 @@ import {
resolveEmbeddedAgentBaseStreamFn,
resolveEmbeddedAgentStreamFn,
} from "../stream-resolution.js";
import { applySystemPromptOverrideToSession } from "../system-prompt.js";
import { applySystemPromptToSession } from "../system-prompt.js";
import {
dropReasoningFromHistory,
dropThinkingBlocks,
@@ -1674,13 +1673,8 @@ export async function runEmbeddedAttempt(
const bootstrapTruncationNotice = buildBootstrapPromptWarningNotice(
bootstrapPromptWarning.lines,
);
const systemPromptOverrideText = resolveSystemPromptOverride({
config: params.config,
agentId: sessionAgentId,
});
const attemptSystemPrompt = buildAttemptSystemPrompt({
isRawModelRun,
systemPromptOverrideText,
transformProviderSystemPrompt,
embeddedSystemPrompt: {
config: params.config,
@@ -1767,8 +1761,7 @@ export async function runEmbeddedAttempt(
skillsPrompt,
tools: effectiveTools,
});
const systemPromptOverride = attemptSystemPrompt.systemPromptOverride;
let systemPromptText = systemPromptOverride();
let systemPromptText = attemptSystemPrompt.systemPrompt;
prepStages.mark("system-prompt");
const compactionTimeoutMs = resolveCompactionTimeoutMs(params.config);
@@ -2103,7 +2096,7 @@ export async function runEmbeddedAttempt(
},
});
session = createdSession.session;
applySystemPromptOverrideToSession(session, systemPromptText);
applySystemPromptToSession(session, systemPromptText);
if (!session) {
throw new Error("Embedded agent session missing");
}
@@ -2117,10 +2110,10 @@ export async function runEmbeddedAttempt(
if (isRawModelRun) {
// Raw model probes should measure exactly the requested prompt against
// the selected provider/model. Reset clears restored transcript state
// and queues; the empty system override prevents the runtime from rebuilding the
// and queues; the empty system prompt prevents the runtime from rebuilding the
// normal OpenClaw agent/tool prompt when `session.prompt()` starts.
activeSession.agent.reset();
applySystemPromptOverrideToSession(activeSession, "");
applySystemPromptToSession(activeSession, "");
systemPromptText = "";
}
if (typeof activeSession.agent.convertToLlm === "function") {
@@ -2668,7 +2661,7 @@ export async function runEmbeddedAttempt(
try {
if (isRawModelRun) {
activeSession.agent.reset();
applySystemPromptOverrideToSession(activeSession, "");
applySystemPromptToSession(activeSession, "");
systemPromptText = "";
cacheTrace?.recordStage("session:raw-model-run", {
messages: activeSession.messages,
@@ -2750,7 +2743,7 @@ export async function runEmbeddedAttempt(
systemPrompt: systemPromptText,
systemPromptAddition: activeSubagentPromptAddition,
});
applySystemPromptOverrideToSession(activeSession, systemPromptText);
applySystemPromptToSession(activeSession, systemPromptText);
}
}
@@ -2816,7 +2809,7 @@ export async function runEmbeddedAttempt(
systemPrompt: systemPromptText,
systemPromptAddition: assembled.systemPromptAddition,
});
applySystemPromptOverrideToSession(activeSession, systemPromptText);
applySystemPromptToSession(activeSession, systemPromptText);
log.debug(
`context engine: prepended system prompt addition (${assembled.systemPromptAddition.length} chars)`,
);
@@ -3284,9 +3277,9 @@ export async function runEmbeddedAttempt(
}
const legacySystemPrompt = normalizeOptionalString(hookResult?.systemPrompt) ?? "";
if (legacySystemPrompt) {
applySystemPromptOverrideToSession(activeSession, legacySystemPrompt);
applySystemPromptToSession(activeSession, legacySystemPrompt);
systemPromptText = legacySystemPrompt;
log.debug(`hooks: applied systemPrompt override (${legacySystemPrompt.length} chars)`);
log.debug(`hooks: applied systemPrompt (${legacySystemPrompt.length} chars)`);
}
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
baseSystemPrompt: systemPromptText,
@@ -3300,7 +3293,7 @@ export async function runEmbeddedAttempt(
if (prependedOrAppendedSystemPrompt) {
const prependSystemLen = hookResult?.prependSystemContext?.trim().length ?? 0;
const appendSystemLen = hookResult?.appendSystemContext?.trim().length ?? 0;
applySystemPromptOverrideToSession(activeSession, prependedOrAppendedSystemPrompt);
applySystemPromptToSession(activeSession, prependedOrAppendedSystemPrompt);
systemPromptText = prependedOrAppendedSystemPrompt;
log.debug(
`hooks: applied prependSystemContext/appendSystemContext (${prependSystemLen}+${appendSystemLen} chars)`,
@@ -3312,7 +3305,7 @@ export async function runEmbeddedAttempt(
model: runtimeInfo.model,
});
if (modelAwareSystemPrompt !== systemPromptText) {
applySystemPromptOverrideToSession(activeSession, modelAwareSystemPrompt);
applySystemPromptToSession(activeSession, modelAwareSystemPrompt);
systemPromptText = modelAwareSystemPrompt;
}
@@ -3488,7 +3481,7 @@ export async function runEmbeddedAttempt(
appendSystemContext: runtimeSystemContext,
});
if (runtimeSystemPrompt) {
applySystemPromptOverrideToSession(activeSession, runtimeSystemPrompt);
applySystemPromptToSession(activeSession, runtimeSystemPrompt);
systemPromptText = runtimeSystemPrompt;
}
}

View File

@@ -1,11 +1,7 @@
import type { AgentSession } from "openclaw/plugin-sdk/agent-sessions";
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearMemoryPluginState, registerMemoryPromptSection } from "../../plugins/memory-state.js";
import {
applySystemPromptOverrideToSession,
buildEmbeddedSystemPrompt,
createSystemPromptOverride,
} from "./system-prompt.js";
import { applySystemPromptToSession, buildEmbeddedSystemPrompt } from "./system-prompt.js";
vi.mock("../../tts/tts.js", () => ({
buildTtsSystemPromptHint: vi.fn(() => undefined),
@@ -33,18 +29,16 @@ function createMockSession(): {
return { session };
}
function applyAndGetMutableSession(
prompt: Parameters<typeof applySystemPromptOverrideToSession>[1],
) {
function applyAndGetMutableSession(prompt: Parameters<typeof applySystemPromptToSession>[1]) {
const { session } = createMockSession();
applySystemPromptOverrideToSession(session as unknown as AgentSession, prompt);
applySystemPromptToSession(session as unknown as AgentSession, prompt);
return {
mutable: session,
};
}
describe("applySystemPromptOverrideToSession", () => {
it("applies a string override to the session system prompt", () => {
describe("applySystemPromptToSession", () => {
it("applies the string to the session system prompt", () => {
const prompt = "You are a helpful assistant with custom context.";
const { mutable } = applyAndGetMutableSession(prompt);
@@ -52,20 +46,13 @@ describe("applySystemPromptOverrideToSession", () => {
expect(mutable["_baseSystemPrompt"]).toBe(prompt);
});
it("trims whitespace from string overrides", () => {
it("trims whitespace", () => {
const { mutable } = applyAndGetMutableSession(" padded prompt ");
expect(mutable.agent.state.systemPrompt).toBe("padded prompt");
});
it("applies a function override to the session system prompt", () => {
const override = createSystemPromptOverride("function-based prompt");
const { mutable } = applyAndGetMutableSession(override);
expect(mutable.agent.state.systemPrompt).toBe("function-based prompt");
});
it("sets _rebuildSystemPrompt that returns the override", () => {
it("sets _rebuildSystemPrompt that returns the prompt", () => {
const { mutable } = applyAndGetMutableSession("rebuild test");
expect(mutable["_rebuildSystemPrompt"]?.(["tool1"])).toBe("rebuild test");
});

View File

@@ -122,18 +122,8 @@ export function buildEmbeddedSystemPrompt(params: {
});
}
export function createSystemPromptOverride(
systemPrompt: string,
): (defaultPrompt?: string) => string {
const override = systemPrompt.trim();
return (_defaultPrompt?: string) => override;
}
export function applySystemPromptOverrideToSession(
session: AgentSession,
override: string | ((defaultPrompt?: string) => string),
) {
const prompt = typeof override === "function" ? override() : override.trim();
export function applySystemPromptToSession(session: AgentSession, systemPrompt: string) {
const prompt = systemPrompt.trim();
session.agent.state.systemPrompt = prompt;
const mutableSession = session as unknown as {
_baseSystemPrompt?: string;

View File

@@ -250,7 +250,7 @@ describe("gateway tool", () => {
return {
ok: true,
path: "/tmp/openclaw.json",
config: { agents: { defaults: { systemPromptOverride: "You are a terse assistant." } } },
config: { agents: { defaults: { reasoningDefault: "medium" } } },
restart: { ok: true, config: "nested field preserved" },
};
}
@@ -260,7 +260,7 @@ describe("gateway tool", () => {
const tool = requireGatewayTool(sessionKey);
const raw =
'{\n agents: { defaults: { systemPromptOverride: "You are a terse assistant." } },\n tools: { exec: { ask: "on-miss", security: "allowlist" } }\n}\n';
'{\n agents: { defaults: { reasoningDefault: "medium" } },\n tools: { exec: { ask: "on-miss", security: "allowlist" } }\n}\n';
const result = await tool.execute("call2", {
action: "config.apply",
raw,
@@ -498,7 +498,7 @@ describe("gateway tool", () => {
await expect(
tool.execute("call-missing-protected", {
action: "config.apply",
raw: '{ agents: { defaults: { systemPromptOverride: "You are a terse assistant." } } }',
raw: '{ agents: { defaults: { reasoningDefault: "medium" } } }',
}),
).rejects.toThrow(
"gateway config.apply cannot change protected config paths: tools.exec.ask, tools.exec.security",
@@ -651,7 +651,7 @@ describe("gateway tool", () => {
const sessionKey = "agent:main:whatsapp:dm:+15555550123";
const tool = requireGatewayTool(sessionKey);
const raw = '{ agents: { defaults: { systemPromptOverride: "You are a terse assistant." } } }';
const raw = '{ agents: { defaults: { reasoningDefault: "medium" } } }';
await tool.execute("call-keep-dangerous", {
action: "config.patch",
raw,

View File

@@ -0,0 +1,33 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "./resource-loader.js";
describe("DefaultResourceLoader", () => {
it("keeps deprecated SDK prompt override aliases wired to prompt transforms", async () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-resource-loader-"));
try {
const loader = new DefaultResourceLoader({
cwd: root,
agentDir: root,
noExtensions: true,
noSkills: true,
noPromptTemplates: true,
noThemes: true,
noContextFiles: true,
systemPrompt: "base",
appendSystemPrompt: ["tail"],
systemPromptOverride: (base) => `${base ?? ""} legacy`,
appendSystemPromptOverride: (base) => [...base, "legacy"],
});
await loader.reload();
expect(loader.getSystemPrompt()).toBe("base legacy");
expect(loader.getAppendSystemPrompt()).toEqual(["tail", "legacy"]);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
});

View File

@@ -161,7 +161,11 @@ export interface DefaultResourceLoaderOptions {
agentsFilesOverride?: (base: { agentsFiles: Array<{ path: string; content: string }> }) => {
agentsFiles: Array<{ path: string; content: string }>;
};
systemPromptTransform?: (base: string | undefined) => string | undefined;
appendSystemPromptTransform?: (base: string[]) => string[];
/** @deprecated Public SDK alias. Use systemPromptTransform. */
systemPromptOverride?: (base: string | undefined) => string | undefined;
/** @deprecated Public SDK alias. Use appendSystemPromptTransform. */
appendSystemPromptOverride?: (base: string[]) => string[];
}
@@ -204,8 +208,8 @@ export class DefaultResourceLoader implements ResourceLoader {
}) => {
agentsFiles: Array<{ path: string; content: string }>;
};
private systemPromptOverride?: (base: string | undefined) => string | undefined;
private appendSystemPromptOverride?: (base: string[]) => string[];
private systemPromptTransform?: (base: string | undefined) => string | undefined;
private appendSystemPromptTransform?: (base: string[]) => string[];
private extensionsResult: LoadExtensionsResult;
private skills: Skill[];
@@ -252,8 +256,9 @@ export class DefaultResourceLoader implements ResourceLoader {
this.promptsOverride = options.promptsOverride;
this.themesOverride = options.themesOverride;
this.agentsFilesOverride = options.agentsFilesOverride;
this.systemPromptOverride = options.systemPromptOverride;
this.appendSystemPromptOverride = options.appendSystemPromptOverride;
this.systemPromptTransform = options.systemPromptTransform ?? options.systemPromptOverride;
this.appendSystemPromptTransform =
options.appendSystemPromptTransform ?? options.appendSystemPromptOverride;
this.extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() };
this.skills = [];
@@ -507,8 +512,8 @@ export class DefaultResourceLoader implements ResourceLoader {
this.systemPromptSource ?? this.discoverSystemPromptFile(),
"system prompt",
);
this.systemPrompt = this.systemPromptOverride
? this.systemPromptOverride(baseSystemPrompt)
this.systemPrompt = this.systemPromptTransform
? this.systemPromptTransform(baseSystemPrompt)
: baseSystemPrompt;
const appendSources =
@@ -517,8 +522,8 @@ export class DefaultResourceLoader implements ResourceLoader {
const baseAppend = appendSources
.map((s) => resolvePromptInput(s, "append system prompt"))
.filter((s): s is string => s !== undefined);
this.appendSystemPrompt = this.appendSystemPromptOverride
? this.appendSystemPromptOverride(baseAppend)
this.appendSystemPrompt = this.appendSystemPromptTransform
? this.appendSystemPromptTransform(baseAppend)
: baseAppend;
}

View File

@@ -1,46 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveSystemPromptOverride } from "./system-prompt-override.js";
describe("resolveSystemPromptOverride", () => {
it("uses defaults when no per-agent override exists", () => {
expect(
resolveSystemPromptOverride({
config: {
agents: {
defaults: { systemPromptOverride: " default system " },
list: [{ id: "main" }],
},
},
agentId: "main",
}),
).toBe("default system");
});
it("prefers the per-agent override", () => {
expect(
resolveSystemPromptOverride({
config: {
agents: {
defaults: { systemPromptOverride: "default system" },
list: [{ id: "main", systemPromptOverride: " agent system " }],
},
},
agentId: "main",
}),
).toBe("agent system");
});
it("ignores blank override values", () => {
expect(
resolveSystemPromptOverride({
config: {
agents: {
defaults: { systemPromptOverride: "default system" },
list: [{ id: "main", systemPromptOverride: " " }],
},
},
agentId: "main",
}),
).toBe("default system");
});
});

View File

@@ -1,27 +0,0 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentConfig } from "./agent-scope.js";
function trimNonEmpty(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
export function resolveSystemPromptOverride(params: {
config?: OpenClawConfig;
agentId?: string;
}): string | undefined {
const config = params.config;
if (!config) {
return undefined;
}
const agentOverride = trimNonEmpty(
params.agentId ? resolveAgentConfig(config, params.agentId)?.systemPromptOverride : undefined,
);
if (agentOverride) {
return agentOverride;
}
return trimNonEmpty(config.agents?.defaults?.systemPromptOverride);
}

View File

@@ -6,10 +6,8 @@ import { resolveAgentPromptSurfaceForSessionKey } from "./prompt-surface.js";
import { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js";
import {
appendAgentBootstrapSystemPromptSupplement,
buildAgentBootstrapSystemContext,
buildAgentBootstrapSystemPromptSections,
buildAgentBootstrapSystemPromptSupplement,
buildAgentSystemPrompt,
buildRuntimeLine,
} from "./system-prompt.js";
@@ -1229,13 +1227,12 @@ describe("buildAgentBootstrapSystemContext", () => {
});
});
describe("buildAgentBootstrapSystemPromptSupplement", () => {
describe("buildAgentBootstrapSystemPromptSections", () => {
it("can render bootstrap guidance without duplicating Project Context", () => {
const sections = buildAgentBootstrapSystemPromptSections({
bootstrapMode: "full",
bootstrapTruncationNotice: "Bootstrap context was truncated.",
contextFiles: [{ path: "/tmp/openclaw/BOOTSTRAP.md", content: "Ask who I am." }],
includeProjectContext: false,
}).join("\n");
expect(sections).toContain("## Bootstrap Pending");
@@ -1245,34 +1242,6 @@ describe("buildAgentBootstrapSystemPromptSupplement", () => {
expect(sections).not.toContain("## /tmp/openclaw/BOOTSTRAP.md");
expect(sections).not.toContain("Ask who I am.");
});
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]");
});
});
describe("buildSubagentSystemPrompt", () => {

View File

@@ -178,6 +178,17 @@ function sortContextFilesForPrompt(contextFiles: EmbeddedContextFile[]): Embedde
});
}
function prepareContextFilesForPrompt(contextFiles: EmbeddedContextFile[] = []) {
const ordered = sortContextFilesForPrompt(
contextFiles.filter((file) => typeof file.path === "string" && file.path.trim().length > 0),
);
return {
ordered,
stable: ordered.filter((file) => !isDynamicContextFile(file.path)),
dynamic: ordered.filter((file) => isDynamicContextFile(file.path)),
};
}
function buildProjectContextSection(params: {
files: EmbeddedContextFile[];
heading: string;
@@ -308,25 +319,10 @@ export function buildAgentBootstrapSystemContext(params: {
];
}
export function buildAgentBootstrapSystemPromptSupplement(params: {
bootstrapMode?: BootstrapMode;
bootstrapTruncationNotice?: string;
contextFiles?: EmbeddedContextFile[];
}): string | undefined {
const supplement = buildAgentBootstrapSystemPromptSections({
...params,
includeProjectContext: true,
})
.join("\n")
.trim();
return supplement.length > 0 ? supplement : undefined;
}
export function buildAgentBootstrapSystemPromptSections(params: {
bootstrapMode?: BootstrapMode;
bootstrapTruncationNotice?: string;
contextFiles?: EmbeddedContextFile[];
includeProjectContext?: boolean;
}): string[] {
const bootstrapFiles =
params.bootstrapMode === "full"
@@ -344,31 +340,9 @@ export function buildAgentBootstrapSystemPromptSections(params: {
if (bootstrapTruncationNotice) {
lines.push("## Bootstrap Context Notice", bootstrapTruncationNotice, "");
}
if (params.includeProjectContext === true && bootstrapFiles.length > 0) {
lines.push(
...buildProjectContextSection({
files: bootstrapFiles,
heading: "# Project Context",
dynamic: false,
}),
);
}
return lines;
}
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) {
if (!ownerLine || isMinimal) {
return [];
@@ -967,18 +941,11 @@ export function buildAgentSystemPrompt(params: {
.join("\n");
}
const contextFiles = params.contextFiles ?? [];
const validContextFiles = contextFiles.filter(
(file) => typeof file.path === "string" && file.path.trim().length > 0,
);
const orderedContextFiles = sortContextFilesForPrompt(validContextFiles);
const stableContextFiles = orderedContextFiles.filter((file) => !isDynamicContextFile(file.path));
const dynamicContextFiles = orderedContextFiles.filter((file) => isDynamicContextFile(file.path));
const contextFiles = prepareContextFilesForPrompt(params.contextFiles);
const bootstrapSystemPromptSections = buildAgentBootstrapSystemPromptSections({
bootstrapMode: params.bootstrapMode,
bootstrapTruncationNotice: params.bootstrapTruncationNotice,
contextFiles: orderedContextFiles,
includeProjectContext: false,
contextFiles: contextFiles.ordered,
});
const stablePrefixCacheKey = hashStablePromptInput({
workspaceDir: params.workspaceDir,
@@ -1019,7 +986,7 @@ export function buildAgentSystemPrompt(params: {
memoryCitationsMode: params.memoryCitationsMode,
memorySection,
acpEnabled,
stableContextFiles,
stableContextFiles: contextFiles.stable,
});
const stablePrefix = cacheStablePromptPrefix(stablePrefixCacheKey, () => {
const lines = [
@@ -1227,7 +1194,7 @@ export function buildAgentSystemPrompt(params: {
lines.push(
...buildProjectContextSection({
files: stableContextFiles,
files: contextFiles.stable,
heading: "# Project Context",
dynamic: false,
}),
@@ -1258,8 +1225,8 @@ export function buildAgentSystemPrompt(params: {
lines.push(
...buildProjectContextSection({
files: dynamicContextFiles,
heading: stableContextFiles.length > 0 ? "# Dynamic Project Context" : "# Project Context",
files: contextFiles.dynamic,
heading: contextFiles.stable.length > 0 ? "# Dynamic Project Context" : "# Project Context",
dynamic: true,
}),
);

View File

@@ -58,7 +58,7 @@ function expectAllowedApply(
describe("gateway config mutation guard coverage", () => {
it("keeps a narrow allowlist of agent-tunable config paths", () => {
expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toContain("agents.defaults.systemPromptOverride");
expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toContain("agents.defaults.promptOverlays");
expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toContain("agents.defaults.model");
expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toContain("agents.defaults.subagents.thinking");
expect(ALLOWED_GATEWAY_CONFIG_PATHS_FOR_TEST).toContain("agents.list[].id");
@@ -512,13 +512,13 @@ describe("gateway config mutation guard coverage", () => {
expectAllowed(
{
agents: {
defaults: { systemPromptOverride: "You are a helpful assistant." },
defaults: { reasoningDefault: "low" },
list: [{ id: "worker", model: "sonnet-4" }],
},
},
{
agents: {
defaults: { systemPromptOverride: "You are a terse assistant." },
defaults: { reasoningDefault: "medium" },
list: [{ id: "worker", model: "opus-4.6" }],
},
},
@@ -531,7 +531,7 @@ describe("gateway config mutation guard coverage", () => {
agents: {
defaults: {
sandbox: { mode: "all" },
systemPromptOverride: "You are a helpful assistant.",
reasoningDefault: "low",
},
},
},
@@ -539,7 +539,7 @@ describe("gateway config mutation guard coverage", () => {
agents: {
defaults: {
sandbox: { mode: "off" },
systemPromptOverride: "You are a terse assistant.",
reasoningDefault: "medium",
},
},
},
@@ -568,13 +568,13 @@ describe("gateway config mutation guard coverage", () => {
expectAllowedApply(
{
agents: {
defaults: { systemPromptOverride: "You are a helpful assistant." },
defaults: { reasoningDefault: "low" },
list: [{ id: "worker", model: "sonnet-4" }],
},
},
{
agents: {
defaults: { systemPromptOverride: "You are a terse assistant." },
defaults: { reasoningDefault: "medium" },
list: [{ id: "worker", model: "opus-4.6" }],
},
},

View File

@@ -36,7 +36,6 @@ const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000;
// must fail closed and allow only a narrow set of agent-tunable paths.
const ALLOWED_GATEWAY_CONFIG_PATHS = [
// Agent prompt/model tuning.
"agents.defaults.systemPromptOverride",
"agents.defaults.promptOverlays",
"agents.defaults.model",
"agents.defaults.thinkingDefault",
@@ -44,7 +43,6 @@ const ALLOWED_GATEWAY_CONFIG_PATHS = [
"agents.defaults.reasoningDefault",
"agents.defaults.fastModeDefault",
"agents.list[].id",
"agents.list[].systemPromptOverride",
"agents.list[].model",
"agents.list[].thinkingDefault",
"agents.list[].subagents.thinking",

View File

@@ -198,6 +198,45 @@ describe("legacy silent reply config migrate", () => {
});
});
describe("legacy agent system prompt override config migrate", () => {
it("removes default and per-agent system prompt overrides", () => {
const raw = {
agents: {
defaults: {
systemPromptOverride: "old default prompt",
model: {
primary: "openai/gpt-5.5",
},
},
list: [
{
id: "alpha",
systemPromptOverride: "old alpha prompt",
},
{
id: "beta",
},
],
},
};
expect(findLegacyConfigIssues(raw).map((issue) => issue.path)).toEqual([
"agents.defaults.systemPromptOverride",
"agents.list",
]);
const res = migrateLegacyConfigForTest(raw);
expect(res.config?.agents?.defaults).not.toHaveProperty("systemPromptOverride");
expect(res.config?.agents?.list?.[0]).not.toHaveProperty("systemPromptOverride");
expect(res.config?.agents?.list?.[1]).toEqual({ id: "beta" });
expect(res.changes).toEqual([
"Removed agents.defaults.systemPromptOverride.",
"Removed agents.list.0.systemPromptOverride.",
]);
});
});
describe("profile configured tool section migrate", () => {
it("does not add grants when configured sections are the only signal", () => {
const raw = {

View File

@@ -223,6 +223,20 @@ const SILENT_REPLY_LEGACY_RULES: LegacyConfigRule[] = [
},
];
const SYSTEM_PROMPT_OVERRIDE_LEGACY_RULES: LegacyConfigRule[] = [
{
path: ["agents", "defaults", "systemPromptOverride"],
message:
'agents.defaults.systemPromptOverride was removed; OpenClaw owns the generated system prompt. Run "openclaw doctor --fix" to remove it.',
},
{
path: ["agents", "list"],
message:
'agents.list[].systemPromptOverride was removed; OpenClaw owns the generated system prompt. Run "openclaw doctor --fix" to remove it.',
match: (value) => hasAgentListSystemPromptOverride(value),
},
];
function sandboxScopeFromPerSession(perSession: boolean): "session" | "shared" {
return perSession ? "session" : "shared";
}
@@ -314,6 +328,15 @@ function hasAgentListRuntimePolicy(value: unknown): boolean {
return value.some((agent) => getRecord(getRecord(agent)?.agentRuntime) !== null);
}
function hasAgentListSystemPromptOverride(value: unknown): boolean {
if (!Array.isArray(value)) {
return false;
}
return value.some((agent) =>
Object.prototype.hasOwnProperty.call(getRecord(agent) ?? {}, "systemPromptOverride"),
);
}
function hasOwnTimeoutMs(value: unknown): boolean {
const record = getRecord(value);
return Boolean(record && Object.prototype.hasOwnProperty.call(record, "timeoutMs"));
@@ -602,6 +625,30 @@ function removeLegacySilentReplyConfig(raw: Record<string, unknown>, changes: st
}
}
function removeLegacySystemPromptOverride(raw: Record<string, unknown>, changes: string[]): void {
const agents = getRecord(raw.agents);
const defaults = getRecord(agents?.defaults);
if (defaults && Object.prototype.hasOwnProperty.call(defaults, "systemPromptOverride")) {
delete defaults.systemPromptOverride;
changes.push("Removed agents.defaults.systemPromptOverride.");
}
if (!Array.isArray(agents?.list)) {
return;
}
for (const [index, agent] of agents.list.entries()) {
const agentRecord = getRecord(agent);
if (
!agentRecord ||
!Object.prototype.hasOwnProperty.call(agentRecord, "systemPromptOverride")
) {
continue;
}
delete agentRecord.systemPromptOverride;
changes.push(`Removed agents.list.${index}.systemPromptOverride.`);
}
}
const CONFIGURED_TOOL_SECTION_GRANTS = [
{ key: "exec", grants: ["exec", "process"] },
{ key: "fs", grants: ["read", "write", "edit"] },
@@ -1150,6 +1197,12 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_AGENTS: LegacyConfigMigrationSpec[
legacyRules: SILENT_REPLY_LEGACY_RULES,
apply: removeLegacySilentReplyConfig,
}),
defineLegacyConfigMigration({
id: "agents.systemPromptOverride-removed",
describe: "Remove legacy agent system prompt override config",
legacyRules: SYSTEM_PROMPT_OVERRIDE_LEGACY_RULES,
apply: removeLegacySystemPromptOverride,
}),
defineLegacyConfigMigration({
id: "agents.defaults.llm->models.providers.timeoutSeconds",
describe: "Remove legacy agents.defaults.llm timeout config",

View File

@@ -239,8 +239,6 @@ export type AgentDefaultsConfig = {
silentReply?: SilentReplyPolicyShape;
/** Optional repository root for system prompt runtime line (overrides auto-detect). */
repoRoot?: string;
/** Optional full system prompt replacement. Primarily for prompt debugging and controlled experiments. */
systemPromptOverride?: string;
/** Provider-independent prompt overlays applied by model family. */
promptOverlays?: PromptOverlaysConfig;
/** Skip bootstrap (BOOTSTRAP.md creation, etc.) for pre-configured deployments. */

View File

@@ -84,8 +84,6 @@ export type AgentConfig = {
description?: string;
workspace?: string;
agentDir?: string;
/** Optional per-agent full system prompt replacement. */
systemPromptOverride?: AgentDefaultsConfig["systemPromptOverride"];
model?: AgentModelConfig;
/**
* @deprecated Legacy raw config accepted only by doctor/migration repair.

View File

@@ -66,7 +66,6 @@ export const AgentDefaultsSchema = z
skills: z.array(z.string()).optional(),
silentReply: SilentReplyPolicyConfigSchema.optional(),
repoRoot: z.string().optional(),
systemPromptOverride: z.string().optional(),
promptOverlays: z
.object({
gpt5: z

View File

@@ -1018,7 +1018,6 @@ export const AgentEntrySchema = z
description: z.string().optional(),
workspace: z.string().optional(),
agentDir: z.string().optional(),
systemPromptOverride: z.string().optional(),
model: AgentModelSchema.optional(),
models: z.record(z.string(), AgentModelRuntimeEntrySchema).optional(),
thinkingDefault: z

View File

@@ -1,378 +0,0 @@
/**
* gateway-cli-backend.system-prompt-resume.live.test.ts
*
* End-to-end behavioral before/after proof for issue #80374. Designed to
* actually distinguish pre-fix from post-fix code at the model-response level,
* not just the argv level.
*
* Approach:
*
* Turn 1: system prompt instructs the model to append `MARKER_ALPHA`.
* Server restart (kills the live claude process).
* Config rewritten: system prompt instructs the model to append `MARKER_BRAVO`.
* Turn 2: resume the prior session. Assert reply contains `MARKER_BRAVO`.
*
* Pre-fix (systemPromptWhen="first"): on Turn 2 the new claude process is
* spawned with `--resume <id>` but WITHOUT `--append-system-prompt-file`.
* The model has no way to know `MARKER_BRAVO` exists — it only sees the
* resumed conversation where Turn 1's user message was tagged with
* `MARKER_ALPHA`. Reply contains `MARKER_ALPHA`, not `MARKER_BRAVO`.
* ASSERTION FAILS.
*
* Post-fix (systemPromptWhen="always"): Turn 2's claude process IS spawned
* with `--append-system-prompt-file <new-file>` and reads the BRAVO
* instruction. Reply contains `MARKER_BRAVO`. ASSERTION PASSES.
*
* This is paired with the argv-level unit tests in
* `src/agents/cli-runner/helpers.system-prompt-resume.test.ts`. The unit tests
* are the cheap, deterministic before/after proof. This live test is the
* expensive end-to-end proof that the argv-level fix actually changes
* observable model behavior on resumed sessions.
*
* Run (after `pnpm build`):
* OPENCLAW_LIVE_TEST=1 \
* OPENCLAW_LIVE_USE_REAL_HOME=1 \
* OPENCLAW_LIVE_CLI_BACKEND=true \
* OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-haiku-4-5 \
* pnpm vitest run --config test/vitest/vitest.live.config.ts \
* src/gateway/gateway-cli-backend.system-prompt-resume.live.test.ts
*/
import { randomBytes, randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveCliBackendConfig } from "../agents/cli-backends.js";
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
import { resolveShellFromPath } from "../agents/shell-utils.js";
import { clearRuntimeConfigSnapshot, type OpenClawConfig } from "../config/config.js";
import { isTruthyEnvValue } from "../infra/env.js";
import {
applyCliBackendLiveEnv,
connectTestGatewayClient,
ensurePairedTestGatewayClientIdentity,
getFreeGatewayPort,
parseJsonStringArray,
resolveCliBackendLiveArgs,
resolveCliBackendLiveModelSelection,
restoreCliBackendLiveEnv,
snapshotCliBackendLiveEnv,
} from "./gateway-cli-backend.live-helpers.js";
import { startGatewayServer } from "./server.js";
import { extractPayloadText } from "./test-helpers.agent-results.js";
const LIVE = isLiveTestEnabled();
const CLI_LIVE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND);
const describeLive = LIVE && CLI_LIVE ? describe : describe.skip;
// Two distinct markers, used to distinguish "model saw the new system prompt"
// from "model is just carrying forward Turn 1's instruction by conversation
// context." Random suffixes prevent the model from inferring one from the
// other or from training data.
const RAND_TAG = randomBytes(4).toString("hex").toUpperCase();
const MARKER_ALPHA = `SP-PROOF-ALPHA-${RAND_TAG}`;
const MARKER_BRAVO = `SP-PROOF-BRAVO-${RAND_TAG}`;
const DEFAULT_PROVIDER = "claude-cli";
const DEFAULT_MODEL = "claude-cli/claude-haiku-4-5";
const REQUEST_TIMEOUT_MS = 5 * 60_000;
const AGENT_TIMEOUT_SECONDS = Math.max(1, Math.ceil(REQUEST_TIMEOUT_MS / 1000) - 10);
async function isExecutableCommandAvailable(command: string): Promise<boolean> {
if (command.includes("/") || command.includes(path.sep)) {
try {
await fs.access(command, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
return Boolean(resolveShellFromPath(command));
}
describeLive("system-prompt-override on resumed cli sessions (issue #80374)", () => {
it(
"resumed session honors NEW systemPromptOverride (changing-marker proof, server restart between turns)",
async () => {
const preservedEnv = new Set(
parseJsonStringArray(
"OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV",
process.env.OPENCLAW_LIVE_CLI_BACKEND_PRESERVE_ENV,
) ?? [],
);
const rawModel = process.env.OPENCLAW_LIVE_CLI_BACKEND_MODEL ?? DEFAULT_MODEL;
const modelSelection = resolveCliBackendLiveModelSelection({
rawModel,
defaultProvider: DEFAULT_PROVIDER,
modelSwitchTarget: undefined,
});
const { providerId, configModelKey } = modelSelection;
const backendResolved = resolveCliBackendConfig(providerId);
const providerDefaults = backendResolved?.config;
const explicitCliCommand = process.env.OPENCLAW_LIVE_CLI_BACKEND_COMMAND;
const cliCommand = explicitCliCommand ?? providerDefaults?.command;
if (!cliCommand) {
throw new Error(
`OPENCLAW_LIVE_CLI_BACKEND_COMMAND is required for provider "${providerId}".`,
);
}
if (!(await isExecutableCommandAvailable(cliCommand))) {
if (explicitCliCommand) {
throw new Error(
`OPENCLAW_LIVE_CLI_BACKEND_COMMAND is not executable or not on PATH: ${cliCommand}`,
);
}
console.warn(
`[sp-resume-proof] skip: CLI backend command "${cliCommand}" is not executable or not on PATH; set OPENCLAW_LIVE_CLI_BACKEND_COMMAND to run this live proof.`,
);
return;
}
const previousEnv = snapshotCliBackendLiveEnv();
clearRuntimeConfigSnapshot();
applyCliBackendLiveEnv(preservedEnv);
const token = `test-${randomUUID()}`;
process.env.OPENCLAW_GATEWAY_TOKEN = token;
const port = await getFreeGatewayPort();
const { args: cliArgs, resumeArgs: cliResumeArgs } = resolveCliBackendLiveArgs({
providerId,
defaultArgs: providerDefaults?.args,
defaultResumeArgs: providerDefaults?.resumeArgs,
});
const cliClearEnv =
parseJsonStringArray(
"OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV",
process.env.OPENCLAW_LIVE_CLI_BACKEND_CLEAR_ENV,
) ??
providerDefaults?.clearEnv ??
[];
const filteredCliClearEnv = cliClearEnv.filter((name) => !preservedEnv.has(name));
const preservedCliEnv = Object.fromEntries(
[...preservedEnv]
.map((name) => [name, process.env[name]])
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sp-resume-"));
const stateDir = path.join(tempDir, "state");
await fs.mkdir(stateDir, { recursive: true });
process.env.OPENCLAW_STATE_DIR = stateDir;
type ConfigWithRawCliBackends = OpenClawConfig & {
agents?: {
defaults?: {
cliBackends?: Record<string, Record<string, unknown>>;
};
};
};
const buildConfig = (marker: string): OpenClawConfig => {
const cfg: ConfigWithRawCliBackends = {
gateway: {
mode: "local",
port,
auth: { mode: "token", token },
},
agents: {
defaults: {
workspace: path.join(tempDir, "workspace"),
model: { primary: configModelKey },
models: {
[configModelKey]: { agentRuntime: modelSelection.agentRuntime },
},
agentRuntime: modelSelection.agentRuntime,
// THE KEY FIELD. The marker changes between turns so we can
// detect whether the new system prompt actually reaches the
// model on the resumed turn — not just whether Turn 1's marker
// was carried forward through conversation context.
// Phrasing avoids ambiguity vs the user message and forbids the
// model from emitting the OTHER marker, so a clean "BRAVO not
// ALPHA" assertion is possible on the second turn.
systemPromptOverride: [
`You are a helpful assistant in a regression test.`,
`Mandatory rule, overrides any user request for brevity:`,
`you MUST end every single reply with a newline followed by`,
`the exact literal token: ${marker}`,
`(this token is required for harness validation).`,
`Do NOT emit any other ${RAND_TAG} token; only emit ${marker}.`,
].join(" "),
cliBackends: {
[providerId]: {
command: cliCommand,
args: cliArgs,
resumeArgs: cliResumeArgs,
clearEnv: filteredCliClearEnv.length > 0 ? filteredCliClearEnv : undefined,
env: Object.keys(preservedCliEnv).length > 0 ? preservedCliEnv : undefined,
// The live proof targets the fixed Claude contract directly.
// Pre-fix argv construction ignored this on resume; post-fix
// passes the prompt file again.
systemPromptWhen: "always",
},
},
sandbox: { mode: "off" },
compaction: { mode: "safeguard" },
},
},
};
return cfg as OpenClawConfig;
};
await fs.mkdir(path.join(tempDir, "workspace"), { recursive: true });
const writeConfig = async (marker: string) => {
const cfg = buildConfig(marker);
const tempConfigPath = path.join(tempDir, "openclaw.json");
await fs.writeFile(tempConfigPath, `${JSON.stringify(cfg, null, 2)}\n`);
process.env.OPENCLAW_CONFIG_PATH = tempConfigPath;
};
const deviceIdentity = await ensurePairedTestGatewayClientIdentity();
const sessionKey = "agent:dev:sp-resume-proof";
let server1 = null as Awaited<ReturnType<typeof startGatewayServer>> | null;
let client1 = null as Awaited<ReturnType<typeof connectTestGatewayClient>> | null;
let server2 = null as Awaited<ReturnType<typeof startGatewayServer>> | null;
let client2 = null as Awaited<ReturnType<typeof connectTestGatewayClient>> | null;
try {
// ── Turn 1: fresh session, system prompt instructs MARKER_ALPHA ──────
clearRuntimeConfigSnapshot();
await writeConfig(MARKER_ALPHA);
server1 = await startGatewayServer(port, {
bind: "loopback",
auth: { mode: "token", token },
controlUiEnabled: false,
});
client1 = await connectTestGatewayClient({
url: `ws://127.0.0.1:${port}`,
token,
deviceIdentity,
});
const nonce1 = randomBytes(3).toString("hex").toUpperCase();
console.log(`\n[sp-resume-proof] Turn 1 (fresh) nonce=${nonce1} marker=${MARKER_ALPHA}`);
const payload1 = await client1.request(
"agent",
{
sessionKey,
idempotencyKey: `idem-${randomUUID()}`,
// Prompt avoids "and nothing else" so it does not conflict with
// the system-prompt instruction to append the marker.
message: `Acknowledge with the exact token SP-T1-${nonce1}.`,
deliver: false,
timeout: AGENT_TIMEOUT_SECONDS,
},
{ expectFinal: true, timeoutMs: REQUEST_TIMEOUT_MS },
);
if (!payload1) {
return;
}
if (payload1.status !== "ok") {
throw new Error(`Turn 1 status=${String(payload1.status)}`);
}
const text1 = extractPayloadText(payload1.result);
console.log(`[sp-resume-proof] Turn 1 response: ${text1}`);
// Sanity check: Turn 1 (fresh session) must apply the ALPHA system
// prompt. If this fails, something is broken upstream of the
// resume/swap mechanism we're actually trying to test.
expect(
text1,
`Turn 1 (fresh): expected "${MARKER_ALPHA}" in response — system prompt was not applied at all`,
).toContain(MARKER_ALPHA);
// ── Restart the server AND swap the system prompt to MARKER_BRAVO ───
// This kills the live process and forces Turn 2 to spawn a NEW claude
// with `--resume <sessionId>`. The config now requests MARKER_BRAVO.
//
// Pre-fix: the new process is spawned without `--append-system-prompt-file`,
// so the model never sees the BRAVO instruction. It only sees Turn 1's
// resumed conversation history where ALPHA was instructed, so it emits
// ALPHA again. THE BRAVO ASSERTION BELOW FAILS.
// Post-fix: the new process IS spawned with `--append-system-prompt-file`
// pointing at the new file (BRAVO instruction). The model emits BRAVO.
console.log(
`[sp-resume-proof] Restarting server, swapping system prompt to ${MARKER_BRAVO}...`,
);
await client1.stopAndWait({ timeoutMs: 5_000 }).catch(() => {});
client1 = null;
await server1.close();
server1 = null;
// ── Turn 2: resumed session, NEW system prompt instructs MARKER_BRAVO ─
clearRuntimeConfigSnapshot();
await writeConfig(MARKER_BRAVO);
server2 = await startGatewayServer(port, {
bind: "loopback",
auth: { mode: "token", token },
controlUiEnabled: false,
});
client2 = await connectTestGatewayClient({
url: `ws://127.0.0.1:${port}`,
token,
deviceIdentity,
});
const nonce2 = randomBytes(3).toString("hex").toUpperCase();
console.log(
`[sp-resume-proof] Turn 2 (resume after restart) nonce=${nonce2} marker=${MARKER_BRAVO}`,
);
const payload2 = await client2.request(
"agent",
{
sessionKey,
idempotencyKey: `idem-${randomUUID()}`,
message: `Acknowledge with the exact token SP-T2-${nonce2}.`,
deliver: false,
timeout: AGENT_TIMEOUT_SECONDS,
},
{ expectFinal: true, timeoutMs: REQUEST_TIMEOUT_MS },
);
if (!payload2) {
return;
}
if (payload2.status !== "ok") {
throw new Error(`Turn 2 status=${String(payload2.status)}`);
}
const text2 = extractPayloadText(payload2.result);
console.log(`[sp-resume-proof] Turn 2 response: ${text2}`);
// THE CRITICAL BEHAVIORAL ASSERTION:
// Pre-fix: model has no way to know BRAVO exists (the new system
// prompt file is never sent on resume), so it emits ALPHA from
// Turn 1's conversation history. This assertion FAILS.
// Post-fix: the new system prompt is delivered, so the model emits
// BRAVO. This assertion PASSES.
expect(
text2,
`Turn 2 (resume): expected "${MARKER_BRAVO}" in response (the NEW system prompt's marker). ` +
`If the model emitted "${MARKER_ALPHA}" instead, the resumed session is using the OLD ` +
`system prompt from Turn 1's conversation context — the new override was never delivered ` +
`(issue #80374).`,
).toContain(MARKER_BRAVO);
console.log(
`\n[sp-resume-proof] ✓ Resumed turn honored the NEW system prompt (${MARKER_BRAVO}).`,
);
console.log(` systemPromptWhen = always`);
} finally {
await client1?.stopAndWait({ timeoutMs: 5_000 }).catch(() => {});
await client2?.stopAndWait({ timeoutMs: 5_000 }).catch(() => {});
await server1?.close().catch(() => {});
await server2?.close().catch(() => {});
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
clearRuntimeConfigSnapshot();
restoreCliBackendLiveEnv(previousEnv);
}
},
10 * 60_000,
);
});