diff --git a/CHANGELOG.md b/CHANGELOG.md index ace912cf3f3..930d2294d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov. - Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss `chokidar` or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03. +- Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409. - Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg. - CLI/message: resolve targeted `openclaw message` channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl. - CLI/models: keep route-first `models status --json` stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar. diff --git a/src/agents/compaction.tool-result-details.test.ts b/src/agents/compaction.tool-result-details.test.ts index d2477f2bde5..391b6bf588b 100644 --- a/src/agents/compaction.tool-result-details.test.ts +++ b/src/agents/compaction.tool-result-details.test.ts @@ -82,6 +82,38 @@ describe("compaction toolResult details stripping", () => { expect(serialized).not.toContain('"details"'); }); + it("does not pass runtime-context custom messages into generateSummary", async () => { + const messages = [ + { role: "user", content: "visible ask", timestamp: 1 }, + { + role: "custom", + customType: "openclaw.runtime-context", + content: "secret runtime context", + display: false, + timestamp: 2, + }, + { role: "assistant", content: "visible answer", timestamp: 3 }, + ] as unknown as AgentMessage[]; + + await summarizeWithFallback({ + messages, + model: { id: "mock", name: "mock", contextWindow: 10000, maxTokens: 1000 } as never, + apiKey: "test", // pragma: allowlist secret + signal: new AbortController().signal, + reserveTokens: 100, + maxChunkTokens: 5000, + contextWindow: 10000, + }); + + const chunk = ( + piCodingAgentMocks.generateSummary.mock.calls as unknown as Array<[unknown]> + )[0]?.[0]; + const serialized = JSON.stringify(chunk); + expect(serialized).toContain("visible ask"); + expect(serialized).not.toContain("openclaw.runtime-context"); + expect(serialized).not.toContain("secret runtime context"); + }); + it("ignores toolResult.details when evaluating oversized messages", () => { piCodingAgentMocks.estimateTokens.mockImplementation((message: unknown) => { const record = message as { details?: unknown }; diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index 00b97d8fbac..aa56199dadf 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -11,6 +11,7 @@ import { isAbortError } from "../infra/unhandled-rejections.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; import { isTimeoutError } from "./failover-error.js"; +import { stripRuntimeContextCustomMessages } from "./internal-runtime-context.js"; import { repairToolUseResultPairing, stripToolResultDetails } from "./session-transcript-repair.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js"; @@ -101,8 +102,8 @@ export function buildCompactionSummarizationInstructions( } export function estimateMessagesTokens(messages: AgentMessage[]): number { - // SECURITY: toolResult.details can contain untrusted/verbose payloads; never include in LLM-facing compaction. - const safe = stripToolResultDetails(messages); + // SECURITY: toolResult.details and runtime-context transcript entries must never enter LLM-facing compaction. + const safe = stripToolResultDetails(stripRuntimeContextCustomMessages(messages)); return safe.reduce((sum, message) => sum + estimateTokens(message), 0); } @@ -305,8 +306,8 @@ async function summarizeChunks(params: { return params.previousSummary ?? DEFAULT_SUMMARY_FALLBACK; } - // SECURITY: never feed toolResult.details into summarization prompts. - const safeMessages = stripToolResultDetails(params.messages); + // SECURITY: never feed toolResult.details or runtime-context transcript entries into summarization prompts. + const safeMessages = stripToolResultDetails(stripRuntimeContextCustomMessages(params.messages)); const chunks = chunkMessagesByMaxTokens(safeMessages, params.maxChunkTokens); let summary = params.previousSummary; const effectiveInstructions = buildCompactionSummarizationInstructions( diff --git a/src/agents/internal-runtime-context.ts b/src/agents/internal-runtime-context.ts index fb17f275494..52d5e73242c 100644 --- a/src/agents/internal-runtime-context.ts +++ b/src/agents/internal-runtime-context.ts @@ -4,12 +4,15 @@ export const INTERNAL_RUNTIME_CONTEXT_END = "<<>> const ESCAPED_INTERNAL_RUNTIME_CONTEXT_BEGIN = "[[OPENCLAW_INTERNAL_CONTEXT_BEGIN]]"; const ESCAPED_INTERNAL_RUNTIME_CONTEXT_END = "[[OPENCLAW_INTERNAL_CONTEXT_END]]"; +export const OPENCLAW_RUNTIME_CONTEXT_NOTICE = + "This context is runtime-generated, not user-authored. Keep internal details private."; +export const OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER = + "OpenClaw runtime context for the immediately preceding user message."; +export const OPENCLAW_RUNTIME_EVENT_HEADER = "OpenClaw runtime event."; +export const OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE = "openclaw.runtime-context"; + const LEGACY_INTERNAL_CONTEXT_HEADER = - [ - "OpenClaw runtime context (internal):", - "This context is runtime-generated, not user-authored. Keep internal details private.", - "", - ].join("\n") + "\n"; + ["OpenClaw runtime context (internal):", OPENCLAW_RUNTIME_CONTEXT_NOTICE, ""].join("\n") + "\n"; const LEGACY_INTERNAL_EVENT_MARKER = "[Internal task completion event]"; const LEGACY_INTERNAL_EVENT_SEPARATOR = "\n\n---\n\n"; @@ -154,6 +157,42 @@ function stripLegacyInternalRuntimeContext(text: string): string { } } +function isRuntimeContextPromptHeader(line: string): boolean { + return ( + line === OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER || line === OPENCLAW_RUNTIME_EVENT_HEADER + ); +} + +function stripRuntimeContextPromptPreface(text: string): string { + const lines = text.split(/\r?\n/); + let changed = false; + const output: string[] = []; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] ?? ""; + const nextLine = lines[index + 1] ?? ""; + if ( + isRuntimeContextPromptHeader(line.trim()) && + nextLine.trim() === OPENCLAW_RUNTIME_CONTEXT_NOTICE + ) { + changed = true; + index += 1; + while (index + 1 < lines.length && (lines[index + 1] ?? "").trim() === "") { + index += 1; + } + continue; + } + output.push(line); + } + + return changed + ? output + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim() + : text; +} + export function stripInternalRuntimeContext(text: string): string { if (!text) { return text; @@ -163,7 +202,9 @@ export function stripInternalRuntimeContext(text: string): string { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END, ); - return stripLegacyInternalRuntimeContext(withoutDelimitedBlocks); + return stripRuntimeContextPromptPreface( + stripLegacyInternalRuntimeContext(withoutDelimitedBlocks), + ); } export function hasInternalRuntimeContext(text: string): boolean { @@ -172,6 +213,27 @@ export function hasInternalRuntimeContext(text: string): boolean { } return ( findDelimitedTokenIndex(text, INTERNAL_RUNTIME_CONTEXT_BEGIN, 0) !== -1 || - text.includes(LEGACY_INTERNAL_CONTEXT_HEADER) + text.includes(LEGACY_INTERNAL_CONTEXT_HEADER) || + text.includes( + `${OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER}\n${OPENCLAW_RUNTIME_CONTEXT_NOTICE}`, + ) || + text.includes(`${OPENCLAW_RUNTIME_EVENT_HEADER}\n${OPENCLAW_RUNTIME_CONTEXT_NOTICE}`) ); } + +export function isOpenClawRuntimeContextCustomMessage(message: unknown): boolean { + if (!message || typeof message !== "object") { + return false; + } + const candidate = message as { role?: unknown; customType?: unknown }; + return ( + candidate.role === "custom" && candidate.customType === OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE + ); +} + +export function stripRuntimeContextCustomMessages(messages: T[]): T[] { + if (!messages.some(isOpenClawRuntimeContextCustomMessage)) { + return messages; + } + return messages.filter((message) => !isOpenClawRuntimeContextCustomMessage(message)); +} diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index 9d2cafca1f3..1f6be125886 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -325,6 +325,30 @@ describe("sanitizeUserFacingText", () => { expect(sanitizeUserFacingText(input)).toBe("Visible intro.\n\nVisible outro."); }); + it("strips copied next-turn runtime context prefaces from user-facing text", () => { + const input = [ + "OpenClaw runtime context for the immediately preceding user message.", + "This context is runtime-generated, not user-authored. Keep internal details private.", + "", + "<<>>", + "secret runtime context", + "<<>>", + "", + "Visible reply.", + ].join("\n"); + + expect(sanitizeUserFacingText(input)).toBe("Visible reply."); + }); + + it("strips copied runtime event prefaces when no visible text remains", () => { + const input = [ + "OpenClaw runtime event.", + "This context is runtime-generated, not user-authored. Keep internal details private.", + ].join("\n"); + + expect(sanitizeUserFacingText(input)).toBe(""); + }); + it("does not strip ordinary text that merely mentions internal marker strings", () => { const input = [ "The literal header `OpenClaw runtime context (internal):` appears in this note.", diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index 57ad1690e41..1b45703e51b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -164,10 +164,15 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { role: "custom", customType: "openclaw.runtime-context", display: false, - content: expect.stringContaining("secret runtime context"), + content: + "<<>>\nsecret runtime context\n<<>>", }), ]), ); + expect(JSON.stringify(seen.messages)).not.toContain( + "OpenClaw runtime context for the immediately preceding user message.", + ); + expect(JSON.stringify(seen.messages)).not.toContain("not user-authored"); const trajectoryEvents = ( await fs.readFile(path.join(tempPaths[0] ?? "", "session.trajectory.jsonl"), "utf8") ) diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index d37fb381227..6a7e3463d9e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -96,6 +96,42 @@ describe("normalizeMessagesForLlmBoundary", () => { expect(output[0]?.content).toEqual([{ type: "text", text: "visible output" }]); expect(input[0]).toHaveProperty("details"); }); + + it("keeps runtime-context transcript entries out of the LLM boundary", () => { + const input = [ + { + role: "user", + content: [{ type: "text", text: "visible ask" }], + timestamp: 1, + }, + { + role: "custom", + customType: "openclaw.runtime-context", + content: "secret runtime context", + display: false, + timestamp: 2, + }, + { + role: "custom", + customType: "other-extension-context", + content: "normal custom context", + display: false, + timestamp: 3, + }, + ]; + + const output = normalizeMessagesForLlmBoundary( + input as Parameters[0], + ) as Array>; + + expect(output).toHaveLength(2); + expect(output).not.toEqual( + expect.arrayContaining([expect.objectContaining({ customType: "openclaw.runtime-context" })]), + ); + expect(output).toEqual( + expect.arrayContaining([expect.objectContaining({ customType: "other-extension-context" })]), + ); + }); }); describe("shouldCreateBundleMcpRuntimeForAttempt", () => { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 157d3b24fb8..61a3a7ce682 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -78,6 +78,7 @@ import { resolveOpenClawReferencePaths } from "../../docs-path.js"; import { isTimeoutError } from "../../failover-error.js"; import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js"; import { resolveImageSanitizationLimits } from "../../image-sanitization.js"; +import { stripRuntimeContextCustomMessages } from "../../internal-runtime-context.js"; import { buildModelAliasLines } from "../../model-alias-lines.js"; import { resolveModelAuthMode } from "../../model-auth.js"; import { resolveDefaultModelForAgent } from "../../model-selection.js"; @@ -314,6 +315,7 @@ import { shouldPreemptivelyCompactBeforePrompt, } from "./preemptive-compaction.js"; import { + buildRuntimeContextSystemContext, queueRuntimeContextForNextTurn, resolveRuntimeContextPromptParts, } from "./runtime-context-prompt.js"; @@ -480,7 +482,8 @@ export function applyEmbeddedAttemptToolsAllow( } export function normalizeMessagesForLlmBoundary(messages: AgentMessage[]): AgentMessage[] { - return stripToolResultDetails(normalizeAssistantReplayContent(messages)); + const normalized = stripToolResultDetails(normalizeAssistantReplayContent(messages)); + return stripRuntimeContextCustomMessages(normalized); } export function shouldCreateBundleMcpRuntimeForAttempt(params: { @@ -2657,19 +2660,35 @@ export async function runEmbeddedAttempt( if (promptSubmission.runtimeOnly) { await abortable(activeSession.prompt(promptSubmission.prompt)); } else { - await queueRuntimeContextForNextTurn({ - session: activeSession, - runtimeContext: promptSubmission.runtimeContext, - }); + const runtimeContext = promptSubmission.runtimeContext?.trim(); + const runtimeSystemPrompt = runtimeContext + ? composeSystemPromptWithHookContext({ + baseSystemPrompt: systemPromptText, + appendSystemContext: buildRuntimeContextSystemContext(runtimeContext), + }) + : undefined; + if (runtimeSystemPrompt) { + applySystemPromptOverrideToSession(activeSession, runtimeSystemPrompt); + } + try { + await queueRuntimeContextForNextTurn({ + session: activeSession, + runtimeContext, + }); - // Only pass images option if there are actually images to pass - // This avoids potential issues with models that don't expect the images parameter - if (imageResult.images.length > 0) { - await abortable( - activeSession.prompt(promptSubmission.prompt, { images: imageResult.images }), - ); - } else { - await abortable(activeSession.prompt(promptSubmission.prompt)); + // Only pass images option if there are actually images to pass + // This avoids potential issues with models that don't expect the images parameter + if (imageResult.images.length > 0) { + await abortable( + activeSession.prompt(promptSubmission.prompt, { images: imageResult.images }), + ); + } else { + await abortable(activeSession.prompt(promptSubmission.prompt)); + } + } finally { + if (runtimeSystemPrompt) { + applySystemPromptOverrideToSession(activeSession, systemPromptText); + } } } } diff --git a/src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts b/src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts index ab044e8c816..736169527b8 100644 --- a/src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts +++ b/src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { + buildRuntimeContextSystemContext, queueRuntimeContextForNextTurn, resolveRuntimeContextPromptParts, } from "./runtime-context-prompt.js"; @@ -62,7 +63,10 @@ describe("runtime context prompt submission", () => { }); it("queues runtime context as a hidden next-turn custom message", async () => { - const sendCustomMessage = vi.fn(async () => {}); + const sentMessages: Array<{ content: string }> = []; + const sendCustomMessage = vi.fn(async (message: { content: string }) => { + sentMessages.push(message); + }); await queueRuntimeContextForNextTurn({ session: { sendCustomMessage }, @@ -72,11 +76,25 @@ describe("runtime context prompt submission", () => { expect(sendCustomMessage).toHaveBeenCalledWith( expect.objectContaining({ customType: "openclaw.runtime-context", - content: expect.stringContaining("secret runtime context"), + content: "secret runtime context", display: false, }), { deliverAs: "nextTurn" }, ); + expect(sentMessages[0]?.content).not.toContain( + "OpenClaw runtime context for the immediately preceding user message.", + ); + expect(sentMessages[0]?.content).not.toContain("not user-authored"); + }); + + it("labels next-turn runtime context only when used as prompt-local system context", () => { + const systemContext = buildRuntimeContextSystemContext("secret runtime context"); + + expect(systemContext).toContain( + "OpenClaw runtime context for the immediately preceding user message.", + ); + expect(systemContext).toContain("not user-authored"); + expect(systemContext).toContain("secret runtime context"); }); it("labels runtime-only events as system context", async () => { diff --git a/src/agents/pi-embedded-runner/run/runtime-context-prompt.ts b/src/agents/pi-embedded-runner/run/runtime-context-prompt.ts index dd219df4ac6..218c777a304 100644 --- a/src/agents/pi-embedded-runner/run/runtime-context-prompt.ts +++ b/src/agents/pi-embedded-runner/run/runtime-context-prompt.ts @@ -1,4 +1,10 @@ -const OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE = "openclaw.runtime-context"; +import { + OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER, + OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, + OPENCLAW_RUNTIME_CONTEXT_NOTICE, + OPENCLAW_RUNTIME_EVENT_HEADER, +} from "../../internal-runtime-context.js"; +export { OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE }; type RuntimeContextSession = { sendCustomMessage: ( @@ -65,14 +71,18 @@ function buildRuntimeContextMessageContent(params: { }): string { return [ params.kind === "runtime-event" - ? "OpenClaw runtime event." - : "OpenClaw runtime context for the immediately preceding user message.", - "This context is runtime-generated, not user-authored. Keep internal details private.", + ? OPENCLAW_RUNTIME_EVENT_HEADER + : OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER, + OPENCLAW_RUNTIME_CONTEXT_NOTICE, "", params.runtimeContext, ].join("\n"); } +export function buildRuntimeContextSystemContext(runtimeContext: string): string { + return buildRuntimeContextMessageContent({ runtimeContext, kind: "next-turn" }); +} + export function buildRuntimeEventSystemContext(runtimeContext: string): string { return buildRuntimeContextMessageContent({ runtimeContext, kind: "runtime-event" }); } @@ -88,7 +98,7 @@ export async function queueRuntimeContextForNextTurn(params: { await params.session.sendCustomMessage( { customType: OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE, - content: buildRuntimeContextMessageContent({ runtimeContext, kind: "next-turn" }), + content: runtimeContext, display: false, details: { source: "openclaw-runtime-context" }, }, diff --git a/src/agents/pi-hooks/compaction-safeguard.test.ts b/src/agents/pi-hooks/compaction-safeguard.test.ts index 089f915d1e5..85b9495ffe1 100644 --- a/src/agents/pi-hooks/compaction-safeguard.test.ts +++ b/src/agents/pi-hooks/compaction-safeguard.test.ts @@ -1433,6 +1433,13 @@ describe("compaction-safeguard recent-turn preservation", () => { preparation: { messagesToSummarize: [ { role: "user", content: "older context", timestamp: 1 }, + { + role: "custom", + customType: "openclaw.runtime-context", + content: "secret runtime context", + display: false, + timestamp: 1.5, + } as unknown as AgentMessage, { role: "assistant", content: "older reply", timestamp: 2 } as unknown as AgentMessage, { role: "user", content: "latest ask status", timestamp: 3 }, { @@ -1831,6 +1838,9 @@ describe("compaction-safeguard recent-turn preservation", () => { }, }), ); + const providerMessages = providerSummarize.mock.calls[0]?.[0]?.messages ?? []; + expect(JSON.stringify(providerMessages)).not.toContain("openclaw.runtime-context"); + expect(JSON.stringify(providerMessages)).not.toContain("secret runtime context"); expect(compaction.summary).toContain("provider summary body"); expect(compaction.summary).toContain("**Turn Context (split turn):**"); expect(compaction.summary).toContain("prefix request that was split out"); diff --git a/src/agents/pi-hooks/compaction-safeguard.ts b/src/agents/pi-hooks/compaction-safeguard.ts index 996dc9a0d37..7bf6222eaef 100644 --- a/src/agents/pi-hooks/compaction-safeguard.ts +++ b/src/agents/pi-hooks/compaction-safeguard.ts @@ -30,6 +30,7 @@ import { import { collectTextContentBlocks } from "../content-blocks.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "../copilot-dynamic-headers.js"; import { isTimeoutError } from "../failover-error.js"; +import { stripRuntimeContextCustomMessages } from "../internal-runtime-context.js"; import { repairToolUseResultPairing } from "../session-transcript-repair.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js"; import { @@ -776,10 +777,15 @@ async function readWorkspaceContextForSummary(): Promise { export default function compactionSafeguardExtension(api: ExtensionAPI): void { api.on("session_before_compact", async (event, ctx) => { const { preparation, customInstructions: eventInstructions, signal } = event; - const hasRealSummarizable = preparation.messagesToSummarize.some((message, index, messages) => + const rawTurnPrefixMessages = preparation.turnPrefixMessages ?? []; + const baseMessagesToSummarize = stripRuntimeContextCustomMessages( + preparation.messagesToSummarize, + ); + const baseTurnPrefixMessages = stripRuntimeContextCustomMessages(rawTurnPrefixMessages); + const hasRealSummarizable = baseMessagesToSummarize.some((message, index, messages) => isRealConversationMessage(message, messages, index), ); - const hasRealTurnPrefix = preparation.turnPrefixMessages.some((message, index, messages) => + const hasRealTurnPrefix = baseTurnPrefixMessages.some((message, index, messages) => isRealConversationMessage(message, messages, index), ); setCompactionSafeguardCancelReason(ctx.sessionManager, undefined); @@ -811,8 +817,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { const { readFiles, modifiedFiles } = computeFileLists(preparation.fileOps); const fileOpsSummary = formatFileOperations(readFiles, modifiedFiles); const toolFailures = collectToolFailures([ - ...preparation.messagesToSummarize, - ...preparation.turnPrefixMessages, + ...baseMessagesToSummarize, + ...baseTurnPrefixMessages, ]); const toolFailureSection = formatToolFailuresSection(toolFailures); @@ -829,10 +835,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { }; const identifierPolicy = runtime?.identifierPolicy ?? "strict"; const providerId = runtime?.provider; - const turnPrefixMessages = preparation.turnPrefixMessages ?? []; + const turnPrefixMessages = baseTurnPrefixMessages; const recentTurnsPreserve = resolveRecentTurnsPreserve(runtime?.recentTurnsPreserve); const { preservedMessages: providerPreservedMessages } = splitPreservedRecentTurns({ - messages: preparation.messagesToSummarize, + messages: baseMessagesToSummarize, recentTurnsPreserve, }); const preservedTurnsSection = formatPreservedTurnsSection(providerPreservedMessages); @@ -854,10 +860,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { try { // Give the provider ALL messages — no pruning, no chunking, no split-turn splitting. // The provider handles its own context management. - const allMessages = [ - ...preparation.messagesToSummarize, - ...(preparation.turnPrefixMessages ?? []), - ]; + const allMessages = [...baseMessagesToSummarize, ...turnPrefixMessages]; const providerResult = await tryProviderSummarize(compactionProvider, { messages: allMessages, signal, @@ -937,7 +940,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { try { const modelContextWindow = resolveContextWindowTokens(model); const contextWindowTokens = runtime?.contextWindowTokens ?? modelContextWindow; - let messagesToSummarize = preparation.messagesToSummarize; + let messagesToSummarize = baseMessagesToSummarize; const headers = buildCompactionSummaryHeaders({ model, messages: messagesToSummarize,