feat(config): add compaction.thinkingLevel to override thinking level during compaction (#98074)

* feat(config): add compaction.thinkingLevel to override thinking during compaction

* fix: make compaction.thinkingLevel override inherited session thinking

* fix: warn on compaction.thinkingLevel ignored by Codex native runtime

* feat(config): add compaction thinking override

Co-authored-by: wu.duozhen@xydigit.com <wu.duozhen@xydigit.com>

* test(agents): remove duplicate compaction harness

Co-authored-by: wu.duozhen@xydigit.com <wu.duozhen@xydigit.com>

* test(plugin-sdk): account for thinking level export

Co-authored-by: wu.duozhen@xydigit.com <wu.duozhen@xydigit.com>

* chore: keep release note in PR metadata

* chore: reconcile plugin SDK surface budgets

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Kris Wu
2026-07-18 17:18:28 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 9fe92cf5ce
commit e5b2ca3c6b
17 changed files with 187 additions and 43 deletions
@@ -94,8 +94,8 @@ d340686cf814326b5a554a32cc5ca324a9e1254a933c4d9d8d1ada5ac7109a10 module/command
a821f9cc4e6f9339399d99e73f58c3b00baf139aa9d85c22d73c89d8be5702d2 module/config-contracts
20f3f8042de53e4eee61b64de9102c8c202b9299e6a29235647a4729f70145f2 module/config-mutation
316949815affe623ac63951a5db580527f02663576dffa084f02768f612c0c1c module/config-runtime
e0c0e99a4d797ba583a7d99e258530230ea61c66875bd6cf45843eb9211f4ff0 module/config-schema
2efdb1e995106ed5843461b7706100838db80453afa7e27c2c14f9c10a5a1e62 module/config-types
493fe6fcb0e4a50b19c1b9574a30e56f5a561212a63202eda82c1c6a2ec3c17a module/config-schema
23fc4e2588ed7d182a80eccc1b364baebb4c5816819397f38d4e8a299c655622 module/config-types
42d15153981cfe3adc1d5f91621434c56f07a9bd48c15ce34742f72dd040c142 module/context-visibility-runtime
03636897fb99cb73e4d8620c8a0e0d72b4d52fc32bf94f525af2aa88c489c6c2 module/conversation-binding-runtime
c1ea9510dfda047609a99d5d2cd1f1560f5d469a36e6b695766213d695c25b0f module/conversation-runtime
@@ -225,7 +225,7 @@ c543747f32aebde42508e31606db325739f0c54fcc7cf683f68ac941c835f737 module/provide
096f53f25cde2c3428b6ff042f1687c5a7c1b161a375ba97aefcdf2f1c38887f module/provider-setup
413af0d4597c1bab45e2f1a260b520445a25b9d11abd46a98fdfb74c60ddd601 module/provider-stream
ea8bd63655e983dacfd306fe77d0793097166228a2dabd9b7fc8e33252eb359f module/provider-stream-family
cdcc81cbd0aff7e341cd7edeec77b02b55d0d27af03278d13ef07487aa8eeae7 module/provider-stream-shared
dc720974ed8d5cfd98fb3bb2958630736883f38317ee4744ce0b4042fa4485d8 module/provider-stream-shared
5a907292055c941ec7420163ea50e8bc90ead816b3c5867d94a23024d3b579e8 module/provider-tools
8236935f56423a26aeb7219173c0c8c8c7aacb4a13d0d851f05bdf2d35790cd4 module/provider-transport-runtime
009a594b19aa9f0d0c0c28f54968f807cad4b3ee40ec13c685367116d63aafdc module/provider-usage
+2
View File
@@ -618,6 +618,7 @@ Periodic heartbeat runs.
compaction: {
mode: "safeguard", // default | safeguard
provider: "my-provider", // id of a registered compaction provider plugin (optional)
thinkingLevel: "low", // optional compaction-only thinking override
timeoutSeconds: 180,
reserveTokensFloor: 24000,
keepRecentTokens: 50000,
@@ -649,6 +650,7 @@ Periodic heartbeat runs.
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
- `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction).
- `thinkingLevel`: optional thinking level used only for embedded OpenClaw compaction summaries (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `adaptive`, `max`, or `ultra`). It overrides the session's current thinking level and is clamped to the selected compaction model/runtime. Leave unset to inherit the session level. Native Codex app-server compaction ignores this setting because the native compact request has no per-operation thinking override; OpenClaw logs a warning when configured.
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `180`.
- `reserveTokens`: token headroom kept available for model output and future tool results after compaction. When the model context window is known, OpenClaw caps the effective reserve so it cannot consume the prompt budget.
- `reserveTokensFloor`: minimum reserve enforced by the embedded runtime. Set `0` to disable the floor. The floor remains subject to the active context-window cap.
@@ -203,6 +203,8 @@ More on limits: [/reference/token-use](/reference/token-use).
Compaction summarizes older conversation into a persisted `compaction` entry in the transcript and keeps recent messages intact. After compaction, future turns see the compaction summary plus messages after `firstKeptEntryId`. Compaction is **persistent**, unlike session pruning - see [/concepts/session-pruning](/concepts/session-pruning).
Embedded OpenClaw compaction inherits the session thinking level by default. Set `agents.defaults.compaction.thinkingLevel` to use a separate level for summary calls; the runtime clamps it to each concrete compaction model or fallback. Native Codex app-server compaction owns its compact request and cannot accept a per-compaction thinking override, so OpenClaw warns and leaves that setting to Codex.
AGENTS.md section reinjection after compaction is opt-in via `agents.defaults.compaction.postCompactionSections`; when unset or `[]`, OpenClaw does not append AGENTS.md excerpts on top of the compaction summary.
### Chunk boundaries and tool pairing
@@ -1554,6 +1554,7 @@ describe("maybeCompactCodexAppServerSession", () => {
compaction: {
model: "openai/gpt-5.4",
provider: "custom-summary",
thinkingLevel: "ultra",
},
},
},
@@ -1566,7 +1567,11 @@ describe("maybeCompactCodexAppServerSession", () => {
{
sessionId: "session-1",
sessionKey: "agent:main:session-1",
ignoredConfig: ["agents.defaults.compaction.model", "agents.defaults.compaction.provider"],
ignoredConfig: [
"agents.defaults.compaction.model",
"agents.defaults.compaction.thinkingLevel",
"agents.defaults.compaction.provider",
],
},
);
warn.mockRestore();
@@ -409,6 +409,9 @@ function readIgnoredCompactionOverridePaths(params: CompactEmbeddedAgentSessionP
if (typeof entry.record.model === "string" && entry.record.model.trim()) {
ignored.add(`${entry.path}.compaction.model`);
}
if (typeof entry.record.thinkingLevel === "string" && entry.record.thinkingLevel.trim()) {
ignored.add(`${entry.path}.compaction.thinkingLevel`);
}
if (providerPath) {
ignored.add(providerPath);
}
+6 -4
View File
@@ -114,8 +114,8 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
"approval-runtime": 1,
"config-runtime": 123,
"config-contracts": 1,
// +1: unified implicit-mention config type.
"config-types": 426,
// +1 each: unified implicit-mention config and AgentThinkingLevel types.
"config-types": 427,
"config-schema": 3,
"reply-dedupe": 1,
"inbound-reply-dispatch": 26,
@@ -277,9 +277,10 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
// +3: bounded visible transcript cursor request, result, and reader.
// +1: explicit AgentModelPolicyConfig shared with provider setup surfaces.
// +1: AgentHarnessSessionSupersededError lets harness plugins stop stale-owner fallback.
// +1: AgentThinkingLevel shared by default-turn and compaction config.
// +9: shared ingress monitor factory and lifecycle/result contracts across
// channel-outbound and its two deprecated compatibility barrels.
8176,
8177,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
@@ -345,7 +346,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
// Harvest: retired dual-field plan payload builder -1; lower-only drift -8.
// +1: AgentModelPolicyConfig mirrored by deprecated config-types.
// +6: ingress monitor lifecycle/result contracts through deprecated channel barrels.
3012,
// +1: AgentThinkingLevel mirrored by deprecated config-types.
3013,
env,
),
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
+10 -12
View File
@@ -159,7 +159,6 @@ import {
} from "../session-write-lock.js";
import { createAgentSession, estimateTokens, SessionManager } from "../sessions/index.js";
import { detectRuntimeShell } from "../shell-utils.js";
import { resolveCandidateThinkingLevel } from "../thinking-runtime.js";
import {
filterProviderNormalizableTools,
filterRuntimeCompatibleTools,
@@ -187,6 +186,7 @@ import {
} from "./compaction-hooks.js";
import {
resolveCompactionHarnessRuntime,
resolveEmbeddedCompactionThinkingLevel,
resolveEmbeddedCompactionTarget,
} from "./compaction-runtime-context.js";
import {
@@ -508,22 +508,12 @@ export async function compactEmbeddedAgentSessionDirect(
provider === primaryProvider ||
provider === requestedPrimaryProvider;
const authProfileId = preservesPrimaryAuth ? params.authProfileId : undefined;
const candidateThinkLevel = resolveCandidateThinkingLevel({
cfg: params.config,
provider,
modelId: model,
level: params.thinkLevel,
agentId: fallbackAgentId,
sessionKey: fallbackSessionKey,
agentRuntime: params.agentHarnessId,
});
return await compactEmbeddedAgentSessionDirectOnce({
...params,
provider,
model,
authProfileId,
authProfileIdSource: preservesPrimaryAuth ? params.authProfileIdSource : undefined,
thinkLevel: candidateThinkLevel,
// The primary attempt retains its already prepared atomic plan. An
// actual fallback may change route/auth class and must rebuild it.
runtimeAuthPlan: isPrimaryCandidate ? params.runtimeAuthPlan : undefined,
@@ -645,7 +635,15 @@ async function compactEmbeddedAgentSessionDirectOnce(
agentHarnessRuntimeOverride: selectedHarnessRuntimeOverride,
workspaceDir: resolvedWorkspace,
});
let thinkLevel: ThinkLevel = params.thinkLevel ?? "off";
let thinkLevel = resolveEmbeddedCompactionThinkingLevel({
config: params.config,
provider,
modelId,
inheritedLevel: params.thinkLevel,
agentId: runtimePolicyAgentId,
sessionKey: runtimePolicySessionKey,
agentRuntime: selectedHarnessRuntime,
});
const attemptedThinking = new Set<ThinkLevel>();
const fail = (reason: string, err?: unknown): EmbeddedAgentCompactResult => {
const failureReason = classifyCompactionReason(reason);
@@ -7,9 +7,53 @@ import { resetProcessRegistryForTests } from "../bash-process-registry.test-supp
import {
buildEmbeddedCompactionRuntimeContext,
resolveCompactionHarnessRuntime,
resolveEmbeddedCompactionThinkingLevel,
resolveEmbeddedCompactionTarget,
} from "./compaction-runtime-context.js";
describe("resolveEmbeddedCompactionThinkingLevel", () => {
it("lets the compaction override replace the inherited session level", () => {
expect(
resolveEmbeddedCompactionThinkingLevel({
config: {
agents: { defaults: { compaction: { thinkingLevel: "low" } } },
},
provider: "demo",
modelId: "demo-model",
inheritedLevel: "high",
}),
).toBe("low");
});
it("revalidates an unsupported configured level for the actual candidate", () => {
expect(
resolveEmbeddedCompactionThinkingLevel({
config: {
agents: { defaults: { compaction: { thinkingLevel: "ultra" } } },
},
provider: "demo",
modelId: "demo-model",
}),
).toBe("high");
});
it("inherits the session level and otherwise defaults to off", () => {
expect(
resolveEmbeddedCompactionThinkingLevel({
provider: "demo",
modelId: "demo-model",
inheritedLevel: "medium",
}),
).toBe("medium");
expect(
resolveEmbeddedCompactionThinkingLevel({
provider: "demo",
modelId: "demo-model",
}),
).toBe("off");
});
});
describe("buildEmbeddedCompactionRuntimeContext", () => {
afterEach(() => {
resetProcessRegistryForTests();
@@ -21,6 +21,7 @@ import {
import { resolveSelectedOpenAIRuntimeProvider } from "../openai-routing.js";
import { agentRuntimeAuthPlanMatchesTarget } from "../runtime-plan/prepare-auth.js";
import type { AgentRuntimeAuthPlan, AgentRuntimePlan } from "../runtime-plan/types.js";
import { resolveCandidateThinkingLevel } from "../thinking-runtime.js";
type EmbeddedCompactionRuntimeContext = {
sessionKey?: string;
@@ -57,6 +58,37 @@ type EmbeddedCompactionRuntimeContext = {
activeProcessSessions?: ActiveProcessSessionReference[];
};
/** Resolve the configured compaction override against the actual model/runtime candidate. */
export function resolveEmbeddedCompactionThinkingLevel(params: {
config?: OpenClawConfig;
provider: string;
modelId: string;
inheritedLevel?: ThinkLevel;
agentId?: string;
sessionKey?: string;
agentRuntime?: string | null;
}): ThinkLevel {
const requestedLevel =
params.config?.agents?.defaults?.compaction?.thinkingLevel ?? params.inheritedLevel;
if (!requestedLevel) {
return "off";
}
// A compaction model override or fallback can change the supported level set.
// Revalidate the immutable request for every concrete candidate instead of
// carrying a level clamped for an earlier model into a later attempt.
return (
resolveCandidateThinkingLevel({
cfg: params.config,
provider: params.provider,
modelId: params.modelId,
level: requestedLevel,
agentId: params.agentId,
sessionKey: params.sessionKey,
agentRuntime: params.agentRuntime,
}) ?? "off"
);
}
/**
* Resolve the effective compaction target from config, falling back to the
* caller-supplied provider/model and optionally applying runtime defaults.
@@ -94,4 +94,11 @@ describe("config compaction settings", () => {
expect(compaction?.qualityGuard?.maxRetries).toBe(99);
});
it.each(["off", "low", "adaptive", "max", "ultra"] as const)(
"preserves compaction thinkingLevel=%s during materialization",
(thinkingLevel) => {
expect(materializeCompactionConfig({ thinkingLevel })?.thinkingLevel).toBe(thinkingLevel);
},
);
});
+2
View File
@@ -128,6 +128,8 @@ export const AGENT_FIELD_HELP: Record<string, string> = {
'Compaction strategy mode: "default" uses baseline behavior, while "safeguard" applies stricter guardrails to preserve recent context. Keep "default" unless you observe aggressive history loss near limit boundaries.',
"agents.defaults.compaction.provider":
"Id of a registered compaction provider plugin used for summarization. When set and the provider is registered, its summarize() method is called instead of the built-in summarizeInStages pipeline. Falls back to built-in on provider failure. Leave unset to use the default built-in summarization.",
"agents.defaults.compaction.thinkingLevel":
'Optional thinking level used only for embedded OpenClaw compaction summaries: "off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max", or "ultra". It overrides the session level and is clamped to the actual compaction model/runtime; leave unset to inherit the session level. Native Codex app-server compaction ignores this setting because its compact request has no per-operation thinking override, and OpenClaw logs a warning.',
"agents.defaults.compaction.reserveTokens":
"Token headroom reserved for reply generation and tool output after compaction runs. Use higher reserves for verbose/tool-heavy sessions, and lower reserves when maximizing retained history matters more. When the active model context window is known, the effective reserve is capped to preserve usable prompt space.",
"agents.defaults.compaction.keepRecentTokens":
@@ -407,6 +407,7 @@ export const TARGET_KEYS = [
"agents.defaults.compaction",
"agents.defaults.compaction.mode",
"agents.defaults.compaction.provider",
"agents.defaults.compaction.thinkingLevel",
"agents.defaults.compaction.reserveTokens",
"agents.defaults.compaction.keepRecentTokens",
"agents.defaults.compaction.reserveTokensFloor",
@@ -474,6 +475,17 @@ export const ENUM_EXPECTATIONS: Record<string, string[]> = {
"cli.banner.taglineMode": ['"random"', '"default"', '"off"'],
"update.channel": ['"stable"', '"extended-stable"', '"beta"', '"dev"'],
"agents.defaults.compaction.mode": ['"default"', '"safeguard"'],
"agents.defaults.compaction.thinkingLevel": [
'"off"',
'"minimal"',
'"low"',
'"medium"',
'"high"',
'"xhigh"',
'"adaptive"',
'"max"',
'"ultra"',
],
"agents.defaults.compaction.identifierPolicy": ['"strict"', '"off"', '"custom"'],
};
+7
View File
@@ -440,6 +440,13 @@ describe("config help copy quality", () => {
expect(mode.includes('"default"')).toBe(true);
expect(mode.includes('"safeguard"')).toBe(true);
const thinkingLevel = expectDefined(
FIELD_HELP["agents.defaults.compaction.thinkingLevel"],
'FIELD_HELP["agents.defaults.compaction.thinkingLevel"] test invariant',
);
expect(/session level|inherit/i.test(thinkingLevel)).toBe(true);
expect(/Codex app-server|no per-operation thinking override/i.test(thinkingLevel)).toBe(true);
const historyShare = expectDefined(
FIELD_HELP["agents.defaults.compaction.maxHistoryShare"],
'FIELD_HELP["agents.defaults.compaction.maxHistoryShare"] test invariant',
+1
View File
@@ -730,6 +730,7 @@ export const FIELD_LABELS: Record<string, string> = {
"agents.defaults.compaction": "Compaction",
"agents.defaults.compaction.mode": "Compaction Mode",
"agents.defaults.compaction.provider": "Compaction Provider",
"agents.defaults.compaction.thinkingLevel": "Compaction Thinking Level",
"agents.defaults.compaction.reserveTokens": "Compaction Reserve Tokens",
"agents.defaults.compaction.keepRecentTokens": "Compaction Keep Recent Tokens",
"agents.defaults.compaction.reserveTokensFloor": "Compaction Reserve Token Floor",
+14 -10
View File
@@ -24,6 +24,17 @@ export type EmbeddedAgentExecutionContract = "default" | "strict-agentic";
export type SubagentDelegationMode = "suggest" | "prefer";
/** Image compression/detail preference used before sending image inputs to models. */
export type AgentImageQualityPreference = "auto" | "efficient" | "balanced" | "high";
/** Canonical thinking levels accepted by agent defaults and compaction overrides. */
export type AgentThinkingLevel =
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "adaptive"
| "max"
| "ultra";
export type Gpt5PromptOverlayConfig = {
/** Friendly interaction-style layer for GPT-5-family models (default: friendly). */
@@ -361,16 +372,7 @@ export type AgentDefaultsConfig = {
/** Vector memory search configuration (per-agent overrides supported). */
memorySearch?: MemorySearchConfig;
/** Default thinking level when no /think directive is present. */
thinkingDefault?:
| "off"
| "minimal"
| "low"
| "medium"
| "high"
| "xhigh"
| "adaptive"
| "max"
| "ultra";
thinkingDefault?: AgentThinkingLevel;
/** Default verbose level when no /verbose directive is present. */
verboseDefault?: "off" | "on" | "full";
/**
@@ -528,6 +530,8 @@ export type AgentCompactionMidTurnPrecheckConfig = {
export type AgentCompactionConfig = {
/** Compaction summarization mode. */
mode?: AgentCompactionMode;
/** Override the session thinking level for embedded OpenClaw compaction summaries. */
thinkingLevel?: AgentThinkingLevel;
/** Embedded OpenClaw reserve target before floor and context-window caps. */
reserveTokens?: number;
/** Embedded OpenClaw keepRecentTokens budget used for cut-point selection. */
@@ -344,6 +344,28 @@ describe("agent defaults schema", () => {
expect(result.compaction?.maxActiveTranscriptBytes).toBe("20mb");
});
it.each([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"adaptive",
"max",
"ultra",
] as const)("accepts compaction.thinkingLevel=%s", (thinkingLevel) => {
const result = AgentDefaultsSchema.parse({ compaction: { thinkingLevel } })!;
expect(result.compaction?.thinkingLevel).toBe(thinkingLevel);
});
it("rejects an unknown compaction thinking level", () => {
expectSchemaFailurePath(
AgentDefaultsSchema.safeParse({ compaction: { thinkingLevel: "extreme" } }),
"compaction.thinkingLevel",
);
});
it("rejects unsafe byte-size strings in compaction defaults", () => {
const unsafe = String(Number.MAX_SAFE_INTEGER + 1);
expect(
+14 -13
View File
@@ -34,6 +34,18 @@ const OptionalBootstrapFileNameSchema = z.enum([
"IDENTITY.md",
]);
const AgentThinkingLevelSchema = z.enum([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"adaptive",
"max",
"ultra",
]);
const EmbeddedAgentConfigSchema = z
.object({
projectSettingsPolicy: z
@@ -162,6 +174,7 @@ export const AgentDefaultsSchema = z
.object({
mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(),
provider: z.string().optional(),
thinkingLevel: AgentThinkingLevelSchema.optional(),
reserveTokens: z.number().int().nonnegative().optional(),
keepRecentTokens: z.number().int().positive().optional(),
reserveTokensFloor: z.number().int().nonnegative().optional(),
@@ -208,19 +221,7 @@ export const AgentDefaultsSchema = z
.optional(),
runRetries: AgentRunRetriesConfigSchema.optional(),
embeddedAgent: EmbeddedAgentConfigSchema.optional(),
thinkingDefault: z
.union([
z.literal("off"),
z.literal("minimal"),
z.literal("low"),
z.literal("medium"),
z.literal("high"),
z.literal("xhigh"),
z.literal("adaptive"),
z.literal("max"),
z.literal("ultra"),
])
.optional(),
thinkingDefault: AgentThinkingLevelSchema.optional(),
verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(),
toolProgressDetail: z.union([z.literal("explain"), z.literal("raw")]).optional(),
reasoningDefault: z.union([z.literal("off"), z.literal("on"), z.literal("stream")]).optional(),