diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c303d26c96..292984d5f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -165,6 +165,7 @@ Docs: https://docs.openclaw.ai - Plugin runtime/system: expose `runtime.system.requestHeartbeatNow(...)` so extensions can wake targeted sessions immediately after enqueueing system events. (#19464) Thanks @AustinEral. - Plugin runtime/events: expose `runtime.events.onAgentEvent` and `runtime.events.onSessionTranscriptUpdate` for extension-side subscriptions, and isolate transcript-listener failures so one faulty listener cannot break the entire update fanout. (#16044) Thanks @scifantastic. - CLI/Banner taglines: add `cli.banner.taglineMode` (`random` | `default` | `off`) to control funny tagline behavior in startup output, with docs + FAQ guidance and regression tests for config override behavior. +- Agents/compaction safeguard quality-audit rollout: keep summary quality audits disabled by default unless `agents.defaults.compaction.qualityGuard` is explicitly enabled, and add config plumbing for bounded retry control. (#25556) thanks @rodrigouroz. ### Breaking diff --git a/src/agents/pi-embedded-runner/extensions.test.ts b/src/agents/pi-embedded-runner/extensions.test.ts index 235664d6a9d..ff95a0b2dee 100644 --- a/src/agents/pi-embedded-runner/extensions.test.ts +++ b/src/agents/pi-embedded-runner/extensions.test.ts @@ -7,7 +7,7 @@ import compactionSafeguardExtension from "../pi-extensions/compaction-safeguard. import { buildEmbeddedExtensionFactories } from "./extensions.js"; describe("buildEmbeddedExtensionFactories", () => { - it("wires safeguard quality-guard runtime flags", () => { + it("does not opt safeguard mode into quality-guard retries", () => { const sessionManager = {} as SessionManager; const model = { id: "claude-sonnet-4-20250514", @@ -31,10 +31,44 @@ describe("buildEmbeddedExtensionFactories", () => { model, }); + expect(factories).toContain(compactionSafeguardExtension); + expect(getCompactionSafeguardRuntime(sessionManager)).toMatchObject({ + qualityGuardEnabled: false, + }); + }); + + it("wires explicit safeguard quality-guard runtime flags", () => { + const sessionManager = {} as SessionManager; + const model = { + id: "claude-sonnet-4-20250514", + contextWindow: 200_000, + } as Model; + const cfg = { + agents: { + defaults: { + compaction: { + mode: "safeguard", + qualityGuard: { + enabled: true, + maxRetries: 2, + }, + }, + }, + }, + } as OpenClawConfig; + + const factories = buildEmbeddedExtensionFactories({ + cfg, + sessionManager, + provider: "anthropic", + modelId: "claude-sonnet-4-20250514", + model, + }); + expect(factories).toContain(compactionSafeguardExtension); expect(getCompactionSafeguardRuntime(sessionManager)).toMatchObject({ qualityGuardEnabled: true, - qualityGuardMaxRetries: 1, + qualityGuardMaxRetries: 2, }); }); }); diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index 2eb2e6e9375..8833e175461 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -71,6 +71,7 @@ export function buildEmbeddedExtensionFactories(params: { const factories: ExtensionFactory[] = []; if (resolveCompactionMode(params.cfg) === "safeguard") { const compactionCfg = params.cfg?.agents?.defaults?.compaction; + const qualityGuardCfg = compactionCfg?.qualityGuard; const contextWindowInfo = resolveContextWindowInfo({ cfg: params.cfg, provider: params.provider, @@ -83,8 +84,8 @@ export function buildEmbeddedExtensionFactories(params: { contextWindowTokens: contextWindowInfo.tokens, identifierPolicy: compactionCfg?.identifierPolicy, identifierInstructions: compactionCfg?.identifierInstructions, - qualityGuardEnabled: true, - qualityGuardMaxRetries: 1, + qualityGuardEnabled: qualityGuardCfg?.enabled ?? false, + qualityGuardMaxRetries: qualityGuardCfg?.maxRetries, model: params.model, }); factories.push(compactionSafeguardExtension); diff --git a/src/config/config.compaction-settings.test.ts b/src/config/config.compaction-settings.test.ts index 21f6e611ac1..04674a7a7ac 100644 --- a/src/config/config.compaction-settings.test.ts +++ b/src/config/config.compaction-settings.test.ts @@ -13,6 +13,10 @@ describe("config compaction settings", () => { reserveTokensFloor: 12_345, identifierPolicy: "custom", identifierInstructions: "Keep ticket IDs unchanged.", + qualityGuard: { + enabled: true, + maxRetries: 2, + }, memoryFlush: { enabled: false, softThresholdTokens: 1234, @@ -34,6 +38,8 @@ describe("config compaction settings", () => { expect(cfg.agents?.defaults?.compaction?.identifierInstructions).toBe( "Keep ticket IDs unchanged.", ); + expect(cfg.agents?.defaults?.compaction?.qualityGuard?.enabled).toBe(true); + expect(cfg.agents?.defaults?.compaction?.qualityGuard?.maxRetries).toBe(2); 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 a05d1f6417f..9e12a0729de 100644 --- a/src/config/schema.help.quality.test.ts +++ b/src/config/schema.help.quality.test.ts @@ -370,6 +370,9 @@ const TARGET_KEYS = [ "agents.defaults.compaction.maxHistoryShare", "agents.defaults.compaction.identifierPolicy", "agents.defaults.compaction.identifierInstructions", + "agents.defaults.compaction.qualityGuard", + "agents.defaults.compaction.qualityGuard.enabled", + "agents.defaults.compaction.qualityGuard.maxRetries", "agents.defaults.compaction.memoryFlush", "agents.defaults.compaction.memoryFlush.enabled", "agents.defaults.compaction.memoryFlush.softThresholdTokens", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 5b9fda17424..2bcc14f3d4a 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -967,6 +967,12 @@ export const FIELD_HELP: Record = { '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.qualityGuard": + "Optional quality-audit retry settings for safeguard compaction summaries. Leave this disabled unless you explicitly want summary audits and one-shot regeneration on failed checks.", + "agents.defaults.compaction.qualityGuard.enabled": + "Enables summary quality audits and regeneration retries for safeguard compaction. Default: false, so safeguard mode alone does not turn on retry behavior.", + "agents.defaults.compaction.qualityGuard.maxRetries": + "Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.", "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 797b7f8ba67..adbe5431e90 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -434,6 +434,9 @@ export const FIELD_LABELS: Record = { "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.qualityGuard": "Compaction Quality Guard", + "agents.defaults.compaction.qualityGuard.enabled": "Compaction Quality Guard Enabled", + "agents.defaults.compaction.qualityGuard.maxRetries": "Compaction Quality Guard Max Retries", "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 1f20579d0bf..6ceba822362 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -288,6 +288,12 @@ export type AgentDefaultsConfig = { export type AgentCompactionMode = "default" | "safeguard"; export type AgentCompactionIdentifierPolicy = "strict" | "off" | "custom"; +export type AgentCompactionQualityGuardConfig = { + /** Enable compaction summary quality audits and regeneration retries. Default: false. */ + enabled?: boolean; + /** Maximum regeneration retries after a failed quality audit. Default: 1 when enabled. */ + maxRetries?: number; +}; export type AgentCompactionConfig = { /** Compaction summarization mode. */ @@ -304,6 +310,8 @@ export type AgentCompactionConfig = { identifierPolicy?: AgentCompactionIdentifierPolicy; /** Custom identifier-preservation instructions used when identifierPolicy is "custom". */ identifierInstructions?: string; + /** Optional quality-audit retries for safeguard compaction summaries. */ + qualityGuard?: AgentCompactionQualityGuardConfig; /** 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 aad541d6d1d..276f97f586d 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -95,6 +95,13 @@ export const AgentDefaultsSchema = z .union([z.literal("strict"), z.literal("off"), z.literal("custom")]) .optional(), identifierInstructions: z.string().optional(), + qualityGuard: z + .object({ + enabled: z.boolean().optional(), + maxRetries: z.number().int().nonnegative().optional(), + }) + .strict() + .optional(), memoryFlush: z .object({ enabled: z.boolean().optional(),