diff --git a/CHANGELOG.md b/CHANGELOG.md index ec23b6d2370..bd9683bd727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -291,6 +291,7 @@ Docs: https://docs.openclaw.ai - Exec approvals: treat bare allowlist `*` as a true wildcard for parsed executables, including unresolved PATH lookups, so global opt-in allowlists work as configured. (#25250) Thanks @widingmarcus-cyber. - Gateway/Auth: allow trusted-proxy authenticated Control UI websocket sessions to skip device pairing when device identity is absent, preventing false `pairing required` failures behind trusted reverse proxies. (#25428) Thanks @SidQin-cyber. - Agents/Tool dispatch: await block-reply flush before tool execution starts so buffered block replies preserve message ordering around tool calls. (#25427) Thanks @SidQin-cyber. +- Agents/Compaction: harden summarization prompts to preserve opaque identifiers verbatim (UUIDs, IDs, tokens, host/IP/port, URLs), reducing post-compaction identifier drift and hallucinated identifier reconstruction. - iOS/Signing: improve `scripts/ios-team-id.sh` for Xcode 16+ by falling back to Xcode-managed provisioning profiles, add actionable guidance when an Apple account exists but no Team ID can be resolved, and ignore Xcode `xcodebuild` output directories (`apps/ios/build`, `apps/shared/OpenClawKit/build`, `Swabble/build`). (#22773) Thanks @brianleach. - macOS/Menu bar: stop reusing the injector delegate for the "Usage cost (30 days)" submenu to prevent recursive submenu injection loops when opening cost history. (#25341) Thanks @yingchunbai. - Control UI/Chat images: route image-click opens through a shared safe-open helper (allowing only safe URL schemes) and open new tabs with opener isolation to block tabnabbing. (#18685, #25444, #25847) Thanks @Mariana-Codebase and @shakkernerd. diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index cc6effb7e64..d83f4190032 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -22,6 +22,7 @@ Compaction **persists** in the session’s JSONL history. ## Configuration Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.). +Compaction summarization preserves opaque identifiers by default (`identifierPolicy: "strict"`). You can override this with `identifierPolicy: "off"` or provide custom text with `identifierPolicy: "custom"` and `identifierInstructions`. ## Auto-compaction (default on) diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 9bbf0328fc9..c816705e8ae 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -939,6 +939,8 @@ Periodic heartbeat runs. compaction: { mode: "safeguard", // default | safeguard reserveTokensFloor: 24000, + identifierPolicy: "strict", // strict | off | custom + identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom memoryFlush: { enabled: true, softThresholdTokens: 6000, @@ -952,6 +954,8 @@ Periodic heartbeat runs. ``` - `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction). +- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization. +- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`. - `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only. ### `agents.defaults.contextPruning` diff --git a/src/agents/compaction.identifier-policy.test.ts b/src/agents/compaction.identifier-policy.test.ts new file mode 100644 index 00000000000..ddc6f5ecb8e --- /dev/null +++ b/src/agents/compaction.identifier-policy.test.ts @@ -0,0 +1,117 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; +import * as piCodingAgent from "@mariozechner/pi-coding-agent"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildCompactionSummarizationInstructions, summarizeInStages } from "./compaction.js"; + +vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateSummary: vi.fn(), + }; +}); + +const mockGenerateSummary = vi.mocked(piCodingAgent.generateSummary); + +function makeMessage(index: number, size = 1200): AgentMessage { + return { + role: "user", + content: `m${index}-${"x".repeat(size)}`, + timestamp: index, + }; +} + +describe("compaction identifier policy", () => { + const testModel = { + provider: "anthropic", + model: "claude-3-opus", + contextWindow: 200_000, + } as unknown as NonNullable; + + beforeEach(() => { + mockGenerateSummary.mockReset(); + mockGenerateSummary.mockResolvedValue("summary"); + }); + + it("defaults to strict identifier preservation", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 8000, + contextWindow: 200_000, + }); + + const firstCall = mockGenerateSummary.mock.calls[0]; + expect(firstCall?.[5]).toContain("Preserve all opaque identifiers exactly as written"); + expect(firstCall?.[5]).toContain("UUIDs"); + }); + + it("can disable identifier preservation with off policy", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 8000, + contextWindow: 200_000, + summarizationInstructions: { identifierPolicy: "off" }, + }); + + const firstCall = mockGenerateSummary.mock.calls[0]; + expect(firstCall?.[5]).toBeUndefined(); + }); + + it("supports custom identifier instructions", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 8000, + contextWindow: 200_000, + summarizationInstructions: { + identifierPolicy: "custom", + identifierInstructions: "Keep ticket IDs unchanged.", + }, + }); + + const firstCall = mockGenerateSummary.mock.calls[0]; + expect(firstCall?.[5]).toContain("Keep ticket IDs unchanged."); + expect(firstCall?.[5]).not.toContain("Preserve all opaque identifiers exactly as written"); + }); + + it("falls back to strict text when custom policy is missing instructions", () => { + const built = buildCompactionSummarizationInstructions(undefined, { + identifierPolicy: "custom", + identifierInstructions: " ", + }); + expect(built).toContain("Preserve all opaque identifiers exactly as written"); + }); + + it("avoids duplicate additional-focus headers in split+merge path", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2), makeMessage(3), makeMessage(4)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 1000, + contextWindow: 200_000, + parts: 2, + minMessagesForSplit: 4, + customInstructions: "Prioritize customer-visible regressions.", + }); + + const mergedCall = mockGenerateSummary.mock.calls.at(-1); + const instructions = mergedCall?.[5] ?? ""; + expect(instructions).toContain("Merge these partial summaries into a single cohesive summary."); + expect(instructions).toContain("Prioritize customer-visible regressions."); + expect((instructions.match(/Additional focus:/g) ?? []).length).toBe(1); + }); +}); diff --git a/src/agents/compaction.identifier-preservation.test.ts b/src/agents/compaction.identifier-preservation.test.ts new file mode 100644 index 00000000000..810b6307d3f --- /dev/null +++ b/src/agents/compaction.identifier-preservation.test.ts @@ -0,0 +1,128 @@ +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; +import * as piCodingAgent from "@mariozechner/pi-coding-agent"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { buildCompactionSummarizationInstructions, summarizeInStages } from "./compaction.js"; + +vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + generateSummary: vi.fn(), + }; +}); + +const mockGenerateSummary = vi.mocked(piCodingAgent.generateSummary); + +function makeMessage(index: number, size = 1200): AgentMessage { + return { + role: "user", + content: `m${index}-${"x".repeat(size)}`, + timestamp: index, + }; +} + +describe("compaction identifier-preservation instructions", () => { + const testModel = { + provider: "anthropic", + model: "claude-3-opus", + contextWindow: 200_000, + } as unknown as NonNullable; + + beforeEach(() => { + mockGenerateSummary.mockReset(); + mockGenerateSummary.mockResolvedValue("summary"); + }); + + it("injects identifier-preservation guidance even without custom instructions", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 8000, + contextWindow: 200_000, + }); + + expect(mockGenerateSummary).toHaveBeenCalled(); + const firstCall = mockGenerateSummary.mock.calls[0]; + expect(firstCall?.[5]).toContain("Preserve all opaque identifiers exactly as written"); + expect(firstCall?.[5]).toContain("UUIDs"); + expect(firstCall?.[5]).toContain("IPs"); + expect(firstCall?.[5]).toContain("ports"); + }); + + it("keeps identifier-preservation guidance when custom instructions are provided", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 8000, + contextWindow: 200_000, + customInstructions: "Focus on release-impacting bugs.", + }); + + const firstCall = mockGenerateSummary.mock.calls[0]; + expect(firstCall?.[5]).toContain("Preserve all opaque identifiers exactly as written"); + expect(firstCall?.[5]).toContain("Additional focus:"); + expect(firstCall?.[5]).toContain("Focus on release-impacting bugs."); + }); + + it("applies identifier-preservation guidance on staged split + merge summarization", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2), makeMessage(3), makeMessage(4)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 1000, + contextWindow: 200_000, + parts: 2, + minMessagesForSplit: 4, + }); + + expect(mockGenerateSummary.mock.calls.length).toBeGreaterThan(1); + for (const call of mockGenerateSummary.mock.calls) { + expect(call[5]).toContain("Preserve all opaque identifiers exactly as written"); + } + }); + + it("avoids duplicate additional-focus headers in split+merge path", async () => { + await summarizeInStages({ + messages: [makeMessage(1), makeMessage(2), makeMessage(3), makeMessage(4)], + model: testModel, + apiKey: "test-key", + signal: new AbortController().signal, + reserveTokens: 4000, + maxChunkTokens: 1000, + contextWindow: 200_000, + parts: 2, + minMessagesForSplit: 4, + customInstructions: "Prioritize customer-visible regressions.", + }); + + const mergedCall = mockGenerateSummary.mock.calls.at(-1); + const instructions = mergedCall?.[5] ?? ""; + expect(instructions).toContain("Merge these partial summaries into a single cohesive summary."); + expect(instructions).toContain("Prioritize customer-visible regressions."); + expect((instructions.match(/Additional focus:/g) ?? []).length).toBe(1); + }); +}); + +describe("buildCompactionSummarizationInstructions", () => { + it("returns base instructions when no custom text is provided", () => { + const result = buildCompactionSummarizationInstructions(); + expect(result).toContain("Preserve all opaque identifiers exactly as written"); + expect(result).not.toContain("Additional focus:"); + }); + + it("appends custom instructions in a stable format", () => { + const result = buildCompactionSummarizationInstructions("Keep deployment details."); + expect(result).toContain("Preserve all opaque identifiers exactly as written"); + expect(result).toContain("Additional focus:"); + expect(result).toContain("Keep deployment details."); + }); +}); diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index 25163471839..45f32cccda1 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -1,6 +1,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; import { estimateTokens, generateSummary } from "@mariozechner/pi-coding-agent"; +import type { AgentCompactionIdentifierPolicy } from "../config/types.agent-defaults.js"; import { retryAsync } from "../infra/retry.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { DEFAULT_CONTEXT_TOKENS } from "./defaults.js"; @@ -16,6 +17,46 @@ const DEFAULT_PARTS = 2; const MERGE_SUMMARIES_INSTRUCTIONS = "Merge these partial summaries into a single cohesive summary. Preserve decisions," + " TODOs, open questions, and any constraints."; +const IDENTIFIER_PRESERVATION_INSTRUCTIONS = + "Preserve all opaque identifiers exactly as written (no shortening or reconstruction), " + + "including UUIDs, hashes, IDs, tokens, API keys, hostnames, IPs, ports, URLs, and file names."; + +export type CompactionSummarizationInstructions = { + identifierPolicy?: AgentCompactionIdentifierPolicy; + identifierInstructions?: string; +}; + +function resolveIdentifierPreservationInstructions( + instructions?: CompactionSummarizationInstructions, +): string | undefined { + const policy = instructions?.identifierPolicy ?? "strict"; + if (policy === "off") { + return undefined; + } + if (policy === "custom") { + const custom = instructions?.identifierInstructions?.trim(); + return custom && custom.length > 0 ? custom : IDENTIFIER_PRESERVATION_INSTRUCTIONS; + } + return IDENTIFIER_PRESERVATION_INSTRUCTIONS; +} + +export function buildCompactionSummarizationInstructions( + customInstructions?: string, + instructions?: CompactionSummarizationInstructions, +): string | undefined { + const custom = customInstructions?.trim(); + const identifierPreservation = resolveIdentifierPreservationInstructions(instructions); + if (!identifierPreservation && !custom) { + return undefined; + } + if (!custom) { + return identifierPreservation; + } + if (!identifierPreservation) { + return `Additional focus:\n${custom}`; + } + return `${identifierPreservation}\n\nAdditional focus:\n${custom}`; +} export function estimateMessagesTokens(messages: AgentMessage[]): number { // SECURITY: toolResult.details can contain untrusted/verbose payloads; never include in LLM-facing compaction. @@ -164,6 +205,7 @@ async function summarizeChunks(params: { reserveTokens: number; maxChunkTokens: number; customInstructions?: string; + summarizationInstructions?: CompactionSummarizationInstructions; previousSummary?: string; }): Promise { if (params.messages.length === 0) { @@ -174,7 +216,10 @@ async function summarizeChunks(params: { const safeMessages = stripToolResultDetails(params.messages); const chunks = chunkMessagesByMaxTokens(safeMessages, params.maxChunkTokens); let summary = params.previousSummary; - + const effectiveInstructions = buildCompactionSummarizationInstructions( + params.customInstructions, + params.summarizationInstructions, + ); for (const chunk of chunks) { summary = await retryAsync( () => @@ -184,7 +229,7 @@ async function summarizeChunks(params: { params.reserveTokens, params.apiKey, params.signal, - params.customInstructions, + effectiveInstructions, summary, ), { @@ -214,6 +259,7 @@ export async function summarizeWithFallback(params: { maxChunkTokens: number; contextWindow: number; customInstructions?: string; + summarizationInstructions?: CompactionSummarizationInstructions; previousSummary?: string; }): Promise { const { messages, contextWindow } = params; @@ -282,6 +328,7 @@ export async function summarizeInStages(params: { maxChunkTokens: number; contextWindow: number; customInstructions?: string; + summarizationInstructions?: CompactionSummarizationInstructions; previousSummary?: string; parts?: number; minMessagesForSplit?: number; @@ -325,8 +372,9 @@ export async function summarizeInStages(params: { timestamp: Date.now(), })); - const mergeInstructions = params.customInstructions - ? `${MERGE_SUMMARIES_INSTRUCTIONS}\n\nAdditional focus:\n${params.customInstructions}` + const custom = params.customInstructions?.trim(); + const mergeInstructions = custom + ? `${MERGE_SUMMARIES_INSTRUCTIONS}\n\n${custom}` : MERGE_SUMMARIES_INSTRUCTIONS; return summarizeWithFallback({ diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index fc0e76acdc9..5ecf2c9bb06 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -81,6 +81,8 @@ export function buildEmbeddedExtensionFactories(params: { setCompactionSafeguardRuntime(params.sessionManager, { maxHistoryShare: compactionCfg?.maxHistoryShare, contextWindowTokens: contextWindowInfo.tokens, + identifierPolicy: compactionCfg?.identifierPolicy, + identifierInstructions: compactionCfg?.identifierInstructions, model: params.model, }); factories.push(compactionSafeguardExtension); diff --git a/src/agents/pi-extensions/compaction-safeguard-runtime.ts b/src/agents/pi-extensions/compaction-safeguard-runtime.ts index 7391e3c1cba..74dc10cfa63 100644 --- a/src/agents/pi-extensions/compaction-safeguard-runtime.ts +++ b/src/agents/pi-extensions/compaction-safeguard-runtime.ts @@ -1,9 +1,12 @@ import type { Api, Model } from "@mariozechner/pi-ai"; +import type { AgentCompactionIdentifierPolicy } from "../../config/types.agent-defaults.js"; import { createSessionManagerRuntimeRegistry } from "./session-manager-runtime-registry.js"; export type CompactionSafeguardRuntimeValue = { maxHistoryShare?: number; contextWindowTokens?: number; + identifierPolicy?: AgentCompactionIdentifierPolicy; + identifierInstructions?: string; /** * Model to use for compaction summarization. * Passed through runtime because `ctx.model` is undefined in the compact.ts workflow diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index fbcf82b2003..19a9366fcb6 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -212,6 +212,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { // Model resolution: ctx.model is undefined in compact.ts workflow (extensionRunner.initialize() is never called). // Fall back to runtime.model which is explicitly passed when building extension paths. const runtime = getCompactionSafeguardRuntime(ctx.sessionManager); + const summarizationInstructions = { + identifierPolicy: runtime?.identifierPolicy, + identifierInstructions: runtime?.identifierInstructions, + }; const model = ctx.model ?? runtime?.model; if (!model) { // Log warning once per session when both models are missing (diagnostic for future issues). @@ -295,6 +299,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { maxChunkTokens: droppedMaxChunkTokens, contextWindow: contextWindowTokens, customInstructions, + summarizationInstructions, previousSummary: preparation.previousSummary, }); } catch (droppedError) { @@ -333,6 +338,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { maxChunkTokens, contextWindow: contextWindowTokens, customInstructions, + summarizationInstructions, previousSummary: effectivePreviousSummary, }); @@ -347,6 +353,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { maxChunkTokens, contextWindow: contextWindowTokens, customInstructions: TURN_PREFIX_INSTRUCTIONS, + summarizationInstructions, previousSummary: undefined, }); summary = `${historySummary}\n\n---\n\n**Turn Context (split turn):**\n\n${prefixSummary}`; diff --git a/src/config/config.compaction-settings.test.ts b/src/config/config.compaction-settings.test.ts index 2503b4dbef5..21f6e611ac1 100644 --- a/src/config/config.compaction-settings.test.ts +++ b/src/config/config.compaction-settings.test.ts @@ -11,6 +11,8 @@ describe("config compaction settings", () => { compaction: { mode: "safeguard", reserveTokensFloor: 12_345, + identifierPolicy: "custom", + identifierInstructions: "Keep ticket IDs unchanged.", memoryFlush: { enabled: false, softThresholdTokens: 1234, @@ -28,6 +30,10 @@ describe("config compaction settings", () => { expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard"); expect(cfg.agents?.defaults?.compaction?.reserveTokens).toBeUndefined(); expect(cfg.agents?.defaults?.compaction?.keepRecentTokens).toBeUndefined(); + expect(cfg.agents?.defaults?.compaction?.identifierPolicy).toBe("custom"); + expect(cfg.agents?.defaults?.compaction?.identifierInstructions).toBe( + "Keep ticket IDs unchanged.", + ); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(false); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens).toBe(1234); expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe("Write notes."); diff --git a/src/config/schema.help.quality.test.ts b/src/config/schema.help.quality.test.ts index 33c583578d6..603be7ed73f 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -361,6 +361,8 @@ const TARGET_KEYS = [ "agents.defaults.compaction.keepRecentTokens", "agents.defaults.compaction.reserveTokensFloor", "agents.defaults.compaction.maxHistoryShare", + "agents.defaults.compaction.identifierPolicy", + "agents.defaults.compaction.identifierInstructions", "agents.defaults.compaction.memoryFlush", "agents.defaults.compaction.memoryFlush.enabled", "agents.defaults.compaction.memoryFlush.softThresholdTokens", @@ -415,6 +417,7 @@ const ENUM_EXPECTATIONS: Record = { "logging.redactSensitive": ['"off"', '"tools"'], "update.channel": ['"stable"', '"beta"', '"dev"'], "agents.defaults.compaction.mode": ['"default"', '"safeguard"'], + "agents.defaults.compaction.identifierPolicy": ['"strict"', '"off"', '"custom"'], }; const TOOLS_HOOKS_TARGET_KEYS = [ @@ -777,6 +780,11 @@ describe("config help copy quality", () => { const historyShare = FIELD_HELP["agents.defaults.compaction.maxHistoryShare"]; expect(/0\\.1-0\\.9|fraction|share/i.test(historyShare)).toBe(true); + const identifierPolicy = FIELD_HELP["agents.defaults.compaction.identifierPolicy"]; + expect(identifierPolicy.includes('"strict"')).toBe(true); + expect(identifierPolicy.includes('"off"')).toBe(true); + expect(identifierPolicy.includes('"custom"')).toBe(true); + const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"]; expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true); }); diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index e99b6ed0ce8..e5ec1ad413b 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -920,6 +920,10 @@ export const FIELD_HELP: Record = { "Minimum floor enforced for reserveTokens in Pi compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.", "agents.defaults.compaction.maxHistoryShare": "Maximum fraction of total context budget allowed for retained history after compaction (range 0.1-0.9). Use lower shares for more generation headroom or higher shares for deeper historical continuity.", + "agents.defaults.compaction.identifierPolicy": + 'Identifier-preservation policy for compaction summaries: "strict" prepends built-in opaque-identifier retention guidance (default), "off" disables this prefix, and "custom" uses identifierInstructions. Keep "strict" unless you have a specific compatibility need.', + "agents.defaults.compaction.identifierInstructions": + 'Custom identifier-preservation instruction text used when identifierPolicy="custom". Keep this explicit and safety-focused so compaction summaries do not rewrite opaque IDs, URLs, hosts, or ports.', "agents.defaults.compaction.memoryFlush": "Pre-compaction memory flush settings that run an agentic memory write before heavy compaction. Keep enabled for long sessions so salient context is persisted before aggressive trimming.", "agents.defaults.compaction.memoryFlush.enabled": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 4c466f09992..4ded77e83a7 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -408,6 +408,8 @@ export const FIELD_LABELS: Record = { "agents.defaults.compaction.keepRecentTokens": "Compaction Keep Recent Tokens", "agents.defaults.compaction.reserveTokensFloor": "Compaction Reserve Token Floor", "agents.defaults.compaction.maxHistoryShare": "Compaction Max History Share", + "agents.defaults.compaction.identifierPolicy": "Compaction Identifier Policy", + "agents.defaults.compaction.identifierInstructions": "Compaction Identifier Instructions", "agents.defaults.compaction.memoryFlush": "Compaction Memory Flush", "agents.defaults.compaction.memoryFlush.enabled": "Compaction Memory Flush Enabled", "agents.defaults.compaction.memoryFlush.softThresholdTokens": diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 38cbea44588..303d3b953e7 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -269,6 +269,7 @@ export type AgentDefaultsConfig = { }; export type AgentCompactionMode = "default" | "safeguard"; +export type AgentCompactionIdentifierPolicy = "strict" | "off" | "custom"; export type AgentCompactionConfig = { /** Compaction summarization mode. */ @@ -281,6 +282,10 @@ export type AgentCompactionConfig = { reserveTokensFloor?: number; /** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */ maxHistoryShare?: number; + /** Identifier-preservation instruction policy for compaction summaries. */ + identifierPolicy?: AgentCompactionIdentifierPolicy; + /** Custom identifier-preservation instructions used when identifierPolicy is "custom". */ + identifierInstructions?: string; /** Pre-compaction memory flush (agentic turn). Default: enabled. */ memoryFlush?: AgentCompactionMemoryFlushConfig; }; diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 3e304361396..afbe226b0fd 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -84,6 +84,10 @@ export const AgentDefaultsSchema = z keepRecentTokens: z.number().int().positive().optional(), reserveTokensFloor: z.number().int().nonnegative().optional(), maxHistoryShare: z.number().min(0.1).max(0.9).optional(), + identifierPolicy: z + .union([z.literal("strict"), z.literal("off"), z.literal("custom")]) + .optional(), + identifierInstructions: z.string().optional(), memoryFlush: z .object({ enabled: z.boolean().optional(),