diff --git a/config/env-var-count-budget.txt b/config/env-var-count-budget.txt index 178ed3f867e6..4b7dc6a240d1 100644 --- a/config/env-var-count-budget.txt +++ b/config/env-var-count-budget.txt @@ -3,4 +3,4 @@ # Worker bundle installation no longer injects OPENCLAW_STATE_DIR into a remote npm process. # The internal Node-version check no longer contributes a false OPENCLAW_* name. # The next-turn runtime-context preface constant no longer exists as an OPENCLAW_* name. -493 +492 diff --git a/src/agents/embedded-agent-runner/run/attempt-llm-boundary.ts b/src/agents/embedded-agent-runner/run/attempt-llm-boundary.ts index 0be4e19ff784..3ab136163da8 100644 --- a/src/agents/embedded-agent-runner/run/attempt-llm-boundary.ts +++ b/src/agents/embedded-agent-runner/run/attempt-llm-boundary.ts @@ -68,7 +68,7 @@ export function usesEscapedRuntimeContext(sessionVersion?: number): boolean { } /** The model boundary renders producer facts; transcript content remains untouched. */ -export function projectRuntimeContextFragments(fragments: RuntimeContextFragment[]): string { +function projectRuntimeContextFragments(fragments: RuntimeContextFragment[]): string { return fragments .map(({ kind, text }) => { const escaped = escapeInternalRuntimeContextDelimiters(text); @@ -86,10 +86,9 @@ function projectRuntimeContextMessages(messages: AgentMessage[]): AgentMessage[] if (details.success) { return { ...message, - content: buildRuntimeContextMessageContent({ - runtimeContext: projectRuntimeContextFragments(details.data.fragments), - kind: "next-turn", - }), + content: buildRuntimeContextMessageContent( + projectRuntimeContextFragments(details.data.fragments), + ), }; } } diff --git a/src/agents/embedded-agent-runner/run/attempt-prompt-build.ts b/src/agents/embedded-agent-runner/run/attempt-prompt-build.ts index e274a4361289..70399c79159e 100644 --- a/src/agents/embedded-agent-runner/run/attempt-prompt-build.ts +++ b/src/agents/embedded-agent-runner/run/attempt-prompt-build.ts @@ -54,7 +54,6 @@ import { } from "../tool-result-truncation.js"; import { normalizeCurrentPromptTextForLlmBoundary, - projectRuntimeContextFragments, usesEscapedRuntimeContext, normalizeMessagesForCurrentPromptBoundary, } from "./attempt-llm-boundary.js"; @@ -70,7 +69,6 @@ import { pruneProcessedHistoryImages } from "./history-image-prune.js"; import { buildCurrentInboundPrompt, buildRuntimeContextCustomMessage, - buildRuntimeContextMessageContent, resolveRuntimeContextPromptParts, type RuntimeContextCustomMessage, } from "./runtime-context-prompt.js"; @@ -405,7 +403,6 @@ export function prepareEmbeddedAttemptPromptContext(input: { prompt: PromptAssemblyContext; replaceSessionMessages: (messages: AgentMessage[]) => void; sessionAgentId: string; - setActiveSessionSystemPrompt: (systemPrompt: string) => void; systemPromptReport?: SessionSystemPromptReport; systemPromptText: string; toolResultPromptProjectionState: ToolResultPromptProjectionState; @@ -511,25 +508,7 @@ export function prepareEmbeddedAttemptPromptContext(input: { ...(promptForModel !== promptForSession ? { alternateText: promptForModel } : {}), } : undefined; - const runtimeSystemContext = promptSubmission.runtimeOnly - ? buildRuntimeContextMessageContent({ - runtimeContext: escapedProjection - ? projectRuntimeContextFragments(eventFragments) - : (promptSubmission.runtimeContext ?? ""), - kind: "runtime-event", - }) - : undefined; - let systemPromptForHook = input.systemPromptText; - if (promptSubmission.runtimeOnly && runtimeSystemContext) { - const runtimeSystemPrompt = composeSystemPromptWithHookContext({ - baseSystemPrompt: input.systemPromptText, - appendSystemContext: runtimeSystemContext, - }); - if (runtimeSystemPrompt) { - systemPromptForHook = runtimeSystemPrompt; - input.setActiveSessionSystemPrompt(runtimeSystemPrompt); - } - } + const systemPromptForHook = input.systemPromptText; const runtimeFacts = input.isRawModelRun || attempt.operation === "settled-tool-finalization" ? [] @@ -541,7 +520,7 @@ export function prepareEmbeddedAttemptPromptContext(input: { agentId: input.sessionAgentId, }); const contextFragments = promptSubmission.runtimeOnly - ? runtimeFacts + ? [...eventFragments, ...runtimeFacts] : [...fragments, ...runtimeFacts]; const runtimeContextForHook = contextFragments @@ -573,9 +552,7 @@ export function prepareEmbeddedAttemptPromptContext(input: { input.systemPromptReport.currentTurn = { ...(attempt.currentInboundEventKind ? { kind: attempt.currentInboundEventKind } : {}), promptChars: promptForModel.length, - runtimeContextChars: - (runtimeContextForHook?.length ?? 0) + - (promptSubmission.runtimeOnly ? (runtimeSystemContext?.length ?? 0) : 0), + runtimeContextChars: runtimeContextForHook?.length ?? 0, // Hook context reaches only the model, so count the delta beyond the // transcript prompt or downstream context accounting undercounts it. modelOnlyPromptChars: Math.max(0, promptForModel.length - promptForSession.length), diff --git a/src/agents/embedded-agent-runner/run/attempt-prompt-context.test.ts b/src/agents/embedded-agent-runner/run/attempt-prompt-context.test.ts index 2c2c6083145f..133d67c77910 100644 --- a/src/agents/embedded-agent-runner/run/attempt-prompt-context.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt-prompt-context.test.ts @@ -96,7 +96,6 @@ function createInput(options?: { report?: SessionSystemPromptReport; }) { const replaceSessionMessages = vi.fn(); - const setActiveSessionSystemPrompt = vi.fn(); const report = options?.report ?? ({} as SessionSystemPromptReport); return { input: { @@ -111,14 +110,12 @@ function createInput(options?: { prompt: options?.prompt ?? createPrompt(), replaceSessionMessages, sessionAgentId: "agent-1", - setActiveSessionSystemPrompt, systemPromptReport: report, systemPromptText: "Base system prompt", toolResultPromptProjectionState: projectionState, }, replaceSessionMessages, report, - setActiveSessionSystemPrompt, }; } @@ -167,7 +164,6 @@ describe("prepareEmbeddedAttemptPromptContext", () => { ); expect(after.systemPromptForHook).toBe(before.systemPromptForHook); expect(after.promptForSession).toBe(before.promptForSession); - expect(fixture.setActiveSessionSystemPrompt).not.toHaveBeenCalled(); })); it("carries execution-owned processes in id order without elapsed time or output", () => { const fixture = createInput(); @@ -191,7 +187,6 @@ describe("prepareEmbeddedAttemptPromptContext", () => { text: expect.stringContaining("Active exec sessions:"), }); expect(active.promptForSession).toBe("Visible request"); - expect(fixture.setActiveSessionSystemPrompt).not.toHaveBeenCalled(); }); it("carries changed subagent status without rewriting the system prompt", () => { @@ -222,7 +217,6 @@ describe("prepareEmbeddedAttemptPromptContext", () => { expect(completed.runtimeContextMessageForCurrentTurn?.content).toContain( "## Active Subagents\nnone", ); - expect(fixture.setActiveSessionSystemPrompt).not.toHaveBeenCalled(); }); it("carries changed media progress without rewriting the system prompt", () => { @@ -248,7 +242,6 @@ describe("prepareEmbeddedAttemptPromptContext", () => { 'progress_json="Encoding"', ); expect(encoding.runtimeContextMessageForCurrentTurn?.content).not.toContain("Rendering"); - expect(fixture.setActiveSessionSystemPrompt).not.toHaveBeenCalled(); }); it("supersedes retained active facts with explicit empty snapshots", () => { @@ -353,7 +346,6 @@ describe("prepareEmbeddedAttemptPromptContext", () => { modelOnlyPromptChars: 0, }); expect(fixture.replaceSessionMessages).not.toHaveBeenCalled(); - expect(fixture.setActiveSessionSystemPrompt).not.toHaveBeenCalled(); expect(hoisted.reconcileToolResultPromptProjectionState).toHaveBeenCalledWith( messages, projectionState, @@ -443,7 +435,7 @@ describe("prepareEmbeddedAttemptPromptContext", () => { }); it.each([3, 4])( - "keeps version %s runtime-only events in system context with current facts", + "carries version %s runtime-only events in the message tail carrier without rewriting system prompt", (sessionVersion) => { const fixture = createInput({ attempt: createAttempt({ @@ -462,7 +454,9 @@ describe("prepareEmbeddedAttemptPromptContext", () => { fixture.input.capabilityToolNames.add("process"); const result = prepareEmbeddedAttemptPromptContext({ ...fixture.input, sessionVersion }); - expect(result.systemPromptForHook).toContain("OpenClaw runtime event."); + expect(result.systemPromptForHook).toBe("Base system prompt"); + expect(result.systemPromptForHook).not.toContain("OpenClaw runtime event."); + expect(result.systemPromptForHook).not.toContain("Runtime room event"); expect(result.promptSubmission.runtimeOnly).toBe(true); expect(result.promptForSession).toBe( "Room conversation data\n\nContinue the OpenClaw runtime event.", @@ -472,18 +466,40 @@ describe("prepareEmbeddedAttemptPromptContext", () => { expect(result.runtimeContextMessageForCurrentTurn?.content).toContain( "Active exec sessions:\nnone", ); - expect(result.runtimeContextMessageForCurrentTurn?.content).not.toContain( - "Runtime room event", - ); - expect(result.systemPromptForHook).toContain("Runtime room event"); - expect(fixture.setActiveSessionSystemPrompt).toHaveBeenCalledWith( - expect.stringContaining("Runtime room event"), - ); + expect(result.runtimeContextMessageForCurrentTurn?.content).toContain("Runtime room event"); expect(fixture.report.currentTurn?.kind).toBe("room_event"); expect(fixture.report.currentTurn?.runtimeContextChars).toBeGreaterThan(0); }, ); + it("preserves identical system prompt bytes across normal turns and runtime-only event turns", () => { + const fixture = createInput(); + const normalTurn = prepareEmbeddedAttemptPromptContext(fixture.input); + + const runtimeEventFixture = createInput({ + attempt: createAttempt({ + runtimeContextFragments: [ + { kind: "runtime-instruction", text: "Subagent completed task 42" }, + ], + currentInboundEventKind: "room_event", + }), + prompt: createPrompt({ + effectivePrompt: "", + effectiveTranscriptPrompt: "", + }), + }); + const runtimeTurn = prepareEmbeddedAttemptPromptContext(runtimeEventFixture.input); + + const normalTurnAfter = prepareEmbeddedAttemptPromptContext(fixture.input); + + expect(normalTurn.systemPromptForHook).toBe("Base system prompt"); + expect(runtimeTurn.systemPromptForHook).toBe(normalTurn.systemPromptForHook); + expect(normalTurnAfter.systemPromptForHook).toBe(normalTurn.systemPromptForHook); + expect(runtimeTurn.runtimeContextMessageForCurrentTurn?.content).toContain( + "Subagent completed task 42", + ); + }); + it("keeps a pure heartbeat task active while persisting only the poll marker", () => { const taskPrompt = "Check the deployment and report any failures."; const transcriptPrompt = "[OpenClaw heartbeat poll]"; diff --git a/src/agents/embedded-agent-runner/run/attempt-prompt-phase.ts b/src/agents/embedded-agent-runner/run/attempt-prompt-phase.ts index 8b5992354edf..27bdc1ceb29b 100644 --- a/src/agents/embedded-agent-runner/run/attempt-prompt-phase.ts +++ b/src/agents/embedded-agent-runner/run/attempt-prompt-phase.ts @@ -193,7 +193,6 @@ export async function runEmbeddedAttemptPromptPhase( isRawModelRun, ...(preparedUserTurnMessage ? { preparedUserTurnMessage } : {}), sessionAgentId, - setActiveSessionSystemPrompt, ...(systemPromptReport ? { systemPromptReport } : {}), systemPromptText, toolResultPromptProjectionState, diff --git a/src/agents/embedded-agent-runner/run/attempt.media-hint-cache-boundary.test.ts b/src/agents/embedded-agent-runner/run/attempt.media-hint-cache-boundary.test.ts index 1871c62593cf..c35e19ce6613 100644 --- a/src/agents/embedded-agent-runner/run/attempt.media-hint-cache-boundary.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.media-hint-cache-boundary.test.ts @@ -128,7 +128,6 @@ async function createTurnFixture(systemPromptOverride?: string) { includeBoundaryTimestamp: false, isRawModelRun: false, sessionAgentId: "main", - setActiveSessionSystemPrompt, systemPromptText, toolResultPromptProjectionState: { replacements: new Map(), diff --git a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts index 1f526da971c8..e89cae0d3d52 100644 --- a/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -1905,7 +1905,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { ); }); - it("submits runtime-only context through system prompt without visible prompt", async () => { + it("submits runtime-only context through the tail carrier without visible prompt", async () => { hoisted.sessionManager.getHeader.mockReturnValue({ version: 4 }); let seenPrompt: string | undefined; let seenModelMessages: unknown[] | undefined; @@ -1963,7 +1963,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { expect(contextCompiled?.data?.prompt).toContain("dynamic hook context"); expect(contextCompiled?.data?.prompt).toContain("internal heartbeat event"); expect(contextCompiled?.data?.prompt).toContain("dynamic hook tail"); - expect(contextCompiled?.data?.systemPrompt).toContain("internal heartbeat event"); + expect(contextCompiled?.data?.systemPrompt).not.toContain("internal heartbeat event"); expect(contextCompiled?.data?.systemPrompt).not.toContain("dynamic hook context"); expect(contextCompiled?.data?.systemPrompt).not.toContain("dynamic hook tail"); }); @@ -2061,7 +2061,8 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { const trajectoryEvents = await readTrajectoryEvents(tempPaths); const contextCompiled = trajectoryEvents.find((event) => event.type === "context.compiled"); expect(contextCompiled?.data?.prompt).toContain("Hello from the replied message"); - expect(contextCompiled?.data?.systemPrompt).toContain("runtime bare mention event"); + expect(contextCompiled?.data?.prompt).toContain("runtime bare mention event"); + expect(contextCompiled?.data?.systemPrompt).not.toContain("runtime bare mention event"); expect(contextCompiled?.data?.systemPrompt).not.toContain("Hello from the replied message"); expect(contextCompiled?.data?.systemPrompt).not.toContain( "Reply target of current user message:", diff --git a/src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts b/src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts index d0ef4cd0dfa6..a3002d45cab1 100644 --- a/src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts +++ b/src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts @@ -48,7 +48,7 @@ describe("runtime context prompt submission", () => { ).toEqual({ prompt: "[OpenClaw heartbeat poll]", modelPrompt: "Check the deployment." }); }); - it("requires producer context for runtime-only system context", () => { + it("requires producer context for the runtime-only continuation prompt", () => { const fragments = [ { kind: "runtime-instruction" as const, text: "Continue the background task." }, ]; diff --git a/src/agents/embedded-agent-runner/run/runtime-context-prompt.ts b/src/agents/embedded-agent-runner/run/runtime-context-prompt.ts index f9896851ccac..da5be131e7b2 100644 --- a/src/agents/embedded-agent-runner/run/runtime-context-prompt.ts +++ b/src/agents/embedded-agent-runner/run/runtime-context-prompt.ts @@ -6,8 +6,6 @@ import { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END, OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, - OPENCLAW_RUNTIME_CONTEXT_NOTICE, - OPENCLAW_RUNTIME_EVENT_HEADER, type RuntimeContextFragment, } from "../../internal-runtime-context.js"; import type { CurrentInboundPromptContext } from "./params.js"; @@ -89,22 +87,9 @@ export function resolveRuntimeContextPromptParts(params: { }; } -export function buildRuntimeContextMessageContent(params: { - runtimeContext: string; - kind: "next-turn" | "runtime-event"; -}): string { - // Next-turn carriers carry only the delimited body: the stable system prompt - // explains the markers once, and the delimiters are what hasInternalRuntimeContext - // and the leak strippers key on. Runtime events keep their preface because the - // model receives no user message alongside them. - return [ - ...(params.kind === "runtime-event" - ? [OPENCLAW_RUNTIME_EVENT_HEADER, OPENCLAW_RUNTIME_CONTEXT_NOTICE, ""] - : []), - INTERNAL_RUNTIME_CONTEXT_BEGIN, - params.runtimeContext, - INTERNAL_RUNTIME_CONTEXT_END, - ].join("\n"); +export function buildRuntimeContextMessageContent(runtimeContext: string): string { + // The stable system prompt explains the markers once; leak strippers use the delimiters. + return [INTERNAL_RUNTIME_CONTEXT_BEGIN, runtimeContext, INTERNAL_RUNTIME_CONTEXT_END].join("\n"); } /** Creates a non-displayed custom transcript message for runtime context, if any exists. */ @@ -119,10 +104,7 @@ export function buildRuntimeContextCustomMessage( return { role: "custom", customType: OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, - content: buildRuntimeContextMessageContent({ - runtimeContext: trimmedRuntimeContext, - kind: "next-turn", - }), + content: buildRuntimeContextMessageContent(trimmedRuntimeContext), display: false, details: { source: "openclaw-runtime-context", @@ -148,10 +130,7 @@ export function prependRuntimeContextForModel( const prepend = (text: string) => text.startsWith(`${INTERNAL_RUNTIME_CONTEXT_BEGIN}\n`) ? `${INTERNAL_RUNTIME_CONTEXT_BEGIN}\n${runtimeContext}\n\n${text.slice(INTERNAL_RUNTIME_CONTEXT_BEGIN.length + 1)}` - : buildRuntimeContextMessageContent({ - runtimeContext: [runtimeContext, text].filter(Boolean).join("\n\n"), - kind: "next-turn", - }); + : buildRuntimeContextMessageContent([runtimeContext, text].filter(Boolean).join("\n\n")); if (carrier?.role !== "user") { return [ ...messages, diff --git a/src/agents/embedded-agent-runner/tool-result-truncation.test.ts b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts index ded86b7b42a0..ae00c651183f 100644 --- a/src/agents/embedded-agent-runner/tool-result-truncation.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts @@ -131,7 +131,6 @@ function preparePromptProjectionStateForTest(params: { }, replaceSessionMessages: () => {}, sessionAgentId: "main", - setActiveSessionSystemPrompt: () => {}, systemPromptText: params.raw ? "" : "system", toolResultPromptProjectionState: params.state, }); diff --git a/src/agents/harness/selection.test.ts b/src/agents/harness/selection.test.ts index 398ed33a8f08..e130ba6f3baf 100644 --- a/src/agents/harness/selection.test.ts +++ b/src/agents/harness/selection.test.ts @@ -57,7 +57,6 @@ import { publishCurrentModelGeneration, resetModelGenerationFixtureState, } from "../embedded-agent-runner/model.generation-scope.test-support.js"; -import { projectRuntimeContextFragments } from "../embedded-agent-runner/run/attempt-llm-boundary.js"; import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult, @@ -641,7 +640,7 @@ describe("runAgentHarnessAttempt", () => { ...currentInboundContext.fragments, { kind: "heartbeat-outcome", text: expect.stringContaining("ISOLATED_OUTCOME_731") }, ]); - expect(projectRuntimeContextFragments(fragments ?? [])).toContain("ISOLATED_OUTCOME_731"); + expect(JSON.stringify(fragments)).toContain("ISOLATED_OUTCOME_731"); expect(received?.prompt).toBe("hello"); expect(params.currentInboundContext).toEqual(currentInboundContext); expect(currentInboundContext.text).toBe("Current quoted reply"); diff --git a/src/agents/internal-runtime-context.test.ts b/src/agents/internal-runtime-context.test.ts index d472d29411ee..c903c32839c5 100644 --- a/src/agents/internal-runtime-context.test.ts +++ b/src/agents/internal-runtime-context.test.ts @@ -12,7 +12,6 @@ import { INTERNAL_RUNTIME_CONTEXT_END, OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, OPENCLAW_RUNTIME_CONTEXT_NOTICE, - OPENCLAW_RUNTIME_EVENT_HEADER, relocateCurrentRuntimeContextCarrierToTail, stripInternalRuntimeContext, } from "./internal-runtime-context.js"; @@ -126,7 +125,7 @@ describe("internal runtime context codec", () => { "previous current turn", "OpenClaw runtime context for the immediately preceding user message.", ], - ["runtime event", OPENCLAW_RUNTIME_EVENT_HEADER], + ["runtime event", "OpenClaw runtime event."], ])("detects and strips the %s prompt preface", (_name, header) => { const preface = [header, OPENCLAW_RUNTIME_CONTEXT_NOTICE].join("\n"); const input = [ diff --git a/src/agents/internal-runtime-context.ts b/src/agents/internal-runtime-context.ts index 031048eb9309..761077ffe89f 100644 --- a/src/agents/internal-runtime-context.ts +++ b/src/agents/internal-runtime-context.ts @@ -16,8 +16,6 @@ const ESCAPED_INTERNAL_RUNTIME_CONTEXT_END = "[[OPENCLAW_INTERNAL_CONTEXT_END]]" /** Notice inserted into runtime-generated context blocks. */ export const OPENCLAW_RUNTIME_CONTEXT_NOTICE = "This context is runtime-generated, not user-authored. Keep internal details private."; -/** Header for runtime events passed as prompt context. */ -export const OPENCLAW_RUNTIME_EVENT_HEADER = "OpenClaw runtime event."; /** Custom message type used for structured runtime-context messages. */ export const OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE = "openclaw.runtime-context"; @@ -215,7 +213,7 @@ function stripLegacyInternalRuntimeContext(text: string): string { const RUNTIME_CONTEXT_PROMPT_HEADERS: readonly string[] = [ "OpenClaw runtime context for the active user request in this turn. Do not reply to or describe this context. Use it to continue answering the active user request now. Do not wait for another message.", "OpenClaw runtime context for the immediately preceding user message.", - OPENCLAW_RUNTIME_EVENT_HEADER, + "OpenClaw runtime event.", ]; const RUNTIME_CONTEXT_NOTICE_PATTERN = new RegExp(