Compaction: preserve opaque identifiers in summaries (openclaw#25553) thanks @rodrigouroz

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: rodrigouroz <384037+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Rodrigo Uroz
2026-02-27 11:14:05 -03:00
committed by GitHub
parent 84a88b2ace
commit 0fe6cf06b2
15 changed files with 344 additions and 4 deletions

View File

@@ -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.

View File

@@ -22,6 +22,7 @@ Compaction **persists** in the sessions 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)

View File

@@ -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`

View File

@@ -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<typeof piCodingAgent>();
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<ExtensionContext["model"]>;
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);
});
});

View File

@@ -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<typeof piCodingAgent>();
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<ExtensionContext["model"]>;
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.");
});
});

View File

@@ -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<string> {
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<string> {
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({

View File

@@ -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);

View File

@@ -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

View File

@@ -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}`;

View File

@@ -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.");

View File

@@ -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<string, string[]> = {
"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);
});

View File

@@ -920,6 +920,10 @@ export const FIELD_HELP: Record<string, string> = {
"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":

View File

@@ -408,6 +408,8 @@ export const FIELD_LABELS: Record<string, string> = {
"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":

View File

@@ -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.10.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;
};

View File

@@ -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(),