feat: allow compaction model override via config (#38753)
Merged via squash. Prepared head SHA: a3d6d6c845c9ef492370c4cc12ea790ca92123f0 Co-authored-by: starbuck100 <25417736+starbuck100@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman
This commit is contained in:
@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Mattermost/model picker: add Telegram-style interactive provider/model browsing for `/oc_model` and `/oc_models`, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm.
|
- Mattermost/model picker: add Telegram-style interactive provider/model browsing for `/oc_model` and `/oc_models`, fix picker callback updates, and emit a normal confirmation reply when a model is selected. (#38767) thanks @mukhtharcm.
|
||||||
- Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add `OPENCLAW_VARIANT=slim` build arg for a bookworm-slim variant. (#38479) Thanks @sallyom.
|
- Docker/multi-stage build: restructure Dockerfile as a multi-stage build to produce a minimal runtime image without build tools, source code, or Bun; add `OPENCLAW_VARIANT=slim` build arg for a bookworm-slim variant. (#38479) Thanks @sallyom.
|
||||||
- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.
|
- Google/Gemini 3.1 Flash-Lite: add first-class `google/gemini-3.1-flash-lite-preview` support across model-id normalization, default aliases, media-understanding image lookups, Google Gemini CLI forward-compat fallback, and docs.
|
||||||
|
- Agents/compaction model override: allow `agents.defaults.compaction.model` to route compaction summarization through a different model than the main session, and document the override across config help/reference surfaces. (#38753) thanks @starbuck100.
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,36 @@ Compaction **persists** in the session’s JSONL history.
|
|||||||
Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.).
|
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`.
|
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`.
|
||||||
|
|
||||||
|
You can optionally specify a different model for compaction summarization via `agents.defaults.compaction.model`. This is useful when your primary model is a local or small model and you want compaction summaries produced by a more capable model. The override accepts any `provider/model-id` string:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"compaction": {
|
||||||
|
"model": "openrouter/anthropic/claude-sonnet-4-5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works with local models, for example a second Ollama model dedicated to summarization or a fine-tuned compaction specialist:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"compaction": {
|
||||||
|
"model": "ollama/llama3.1:8b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When unset, compaction uses the agent's primary model.
|
||||||
|
|
||||||
## Auto-compaction (default on)
|
## Auto-compaction (default on)
|
||||||
|
|
||||||
When a session nears or exceeds the model’s context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.
|
When a session nears or exceeds the model’s context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.
|
||||||
|
|||||||
@@ -1005,6 +1005,7 @@ Periodic heartbeat runs.
|
|||||||
identifierPolicy: "strict", // strict | off | custom
|
identifierPolicy: "strict", // strict | off | custom
|
||||||
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
||||||
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
|
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
|
||||||
|
model: "openrouter/anthropic/claude-sonnet-4-5", // optional compaction-only model override
|
||||||
memoryFlush: {
|
memoryFlush: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
softThresholdTokens: 6000,
|
softThresholdTokens: 6000,
|
||||||
@@ -1021,6 +1022,7 @@ Periodic heartbeat runs.
|
|||||||
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
|
- `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`.
|
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
|
||||||
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
|
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
|
||||||
|
- `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model.
|
||||||
- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.
|
- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.
|
||||||
|
|
||||||
### `agents.defaults.contextPruning`
|
### `agents.defaults.contextPruning`
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ const unitIsolatedFilesRaw = [
|
|||||||
"src/slack/monitor/slash.test.ts",
|
"src/slack/monitor/slash.test.ts",
|
||||||
// Uses process-level unhandledRejection listeners; keep it off vmForks to avoid cross-file leakage.
|
// Uses process-level unhandledRejection listeners; keep it off vmForks to avoid cross-file leakage.
|
||||||
"src/imessage/monitor.shutdown.unhandled-rejection.test.ts",
|
"src/imessage/monitor.shutdown.unhandled-rejection.test.ts",
|
||||||
|
// Mutates process.cwd() and mocks core module loaders; isolate from the shared fast lane.
|
||||||
|
"src/infra/git-commit.test.ts",
|
||||||
];
|
];
|
||||||
const unitIsolatedFiles = unitIsolatedFilesRaw.filter((file) => fs.existsSync(file));
|
const unitIsolatedFiles = unitIsolatedFilesRaw.filter((file) => fs.existsSync(file));
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ describe("models-config merge helpers", () => {
|
|||||||
it("merges explicit providers onto trimmed keys", () => {
|
it("merges explicit providers onto trimmed keys", () => {
|
||||||
const merged = mergeProviders({
|
const merged = mergeProviders({
|
||||||
explicit: {
|
explicit: {
|
||||||
" custom ": { api: "openai-responses", models: [] } as ProviderConfig,
|
" custom ": {
|
||||||
|
api: "openai-responses",
|
||||||
|
models: [] as ProviderConfig["models"],
|
||||||
|
} as ProviderConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -271,8 +271,31 @@ export async function compactEmbeddedPiSessionDirect(
|
|||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const prevCwd = process.cwd();
|
const prevCwd = process.cwd();
|
||||||
|
|
||||||
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
// Resolve compaction model: prefer config override, then fall back to caller-supplied model
|
||||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim();
|
||||||
|
let provider: string;
|
||||||
|
let modelId: string;
|
||||||
|
// When switching provider via override, drop the primary auth profile to avoid
|
||||||
|
// sending the wrong credentials (e.g. OpenAI profile token to OpenRouter).
|
||||||
|
let authProfileId: string | undefined = params.authProfileId;
|
||||||
|
if (compactionModelOverride) {
|
||||||
|
const slashIdx = compactionModelOverride.indexOf("/");
|
||||||
|
if (slashIdx > 0) {
|
||||||
|
provider = compactionModelOverride.slice(0, slashIdx).trim();
|
||||||
|
modelId = compactionModelOverride.slice(slashIdx + 1).trim() || DEFAULT_MODEL;
|
||||||
|
// Provider changed — drop primary auth profile so getApiKeyForModel
|
||||||
|
// falls back to provider-based key resolution for the override model.
|
||||||
|
if (provider !== (params.provider ?? "").trim()) {
|
||||||
|
authProfileId = undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
|
modelId = compactionModelOverride;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
|
modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||||
|
}
|
||||||
const fail = (reason: string): EmbeddedPiCompactResult => {
|
const fail = (reason: string): EmbeddedPiCompactResult => {
|
||||||
log.warn(
|
log.warn(
|
||||||
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
`[compaction-diag] end runId=${runId} sessionKey=${params.sessionKey ?? params.sessionId} ` +
|
||||||
@@ -302,7 +325,7 @@ export async function compactEmbeddedPiSessionDirect(
|
|||||||
const apiKeyInfo = await getApiKeyForModel({
|
const apiKeyInfo = await getApiKeyForModel({
|
||||||
model,
|
model,
|
||||||
cfg: params.config,
|
cfg: params.config,
|
||||||
profileId: params.authProfileId,
|
profileId: authProfileId,
|
||||||
agentDir,
|
agentDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -639,6 +639,72 @@ describe("prependSystemPromptAddition", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("buildAfterTurnLegacyCompactionParams", () => {
|
describe("buildAfterTurnLegacyCompactionParams", () => {
|
||||||
|
it("uses primary model when compaction.model is not set", () => {
|
||||||
|
const legacy = buildAfterTurnLegacyCompactionParams({
|
||||||
|
attempt: {
|
||||||
|
sessionKey: "agent:main:session:abc",
|
||||||
|
messageChannel: "slack",
|
||||||
|
messageProvider: "slack",
|
||||||
|
agentAccountId: "acct-1",
|
||||||
|
authProfileId: "openai:p1",
|
||||||
|
config: {} as OpenClawConfig,
|
||||||
|
skillsSnapshot: undefined,
|
||||||
|
senderIsOwner: true,
|
||||||
|
provider: "openai-codex",
|
||||||
|
modelId: "gpt-5.3-codex",
|
||||||
|
thinkLevel: "off",
|
||||||
|
reasoningLevel: "on",
|
||||||
|
extraSystemPrompt: "extra",
|
||||||
|
ownerNumbers: ["+15555550123"],
|
||||||
|
},
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
agentDir: "/tmp/agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(legacy).toMatchObject({
|
||||||
|
provider: "openai-codex",
|
||||||
|
model: "gpt-5.3-codex",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes primary model through even when compaction.model is set (override resolved in compactDirect)", () => {
|
||||||
|
const legacy = buildAfterTurnLegacyCompactionParams({
|
||||||
|
attempt: {
|
||||||
|
sessionKey: "agent:main:session:abc",
|
||||||
|
messageChannel: "slack",
|
||||||
|
messageProvider: "slack",
|
||||||
|
agentAccountId: "acct-1",
|
||||||
|
authProfileId: "openai:p1",
|
||||||
|
config: {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
compaction: {
|
||||||
|
model: "openrouter/anthropic/claude-sonnet-4-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as OpenClawConfig,
|
||||||
|
skillsSnapshot: undefined,
|
||||||
|
senderIsOwner: true,
|
||||||
|
provider: "openai-codex",
|
||||||
|
modelId: "gpt-5.3-codex",
|
||||||
|
thinkLevel: "off",
|
||||||
|
reasoningLevel: "on",
|
||||||
|
extraSystemPrompt: "extra",
|
||||||
|
ownerNumbers: ["+15555550123"],
|
||||||
|
},
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
agentDir: "/tmp/agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
// buildAfterTurnLegacyCompactionParams no longer resolves the override;
|
||||||
|
// compactEmbeddedPiSessionDirect does it centrally for both auto + manual paths.
|
||||||
|
expect(legacy).toMatchObject({
|
||||||
|
provider: "openai-codex",
|
||||||
|
model: "gpt-5.3-codex",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("includes resolved auth profile fields for context-engine afterTurn compaction", () => {
|
it("includes resolved auth profile fields for context-engine afterTurn compaction", () => {
|
||||||
const legacy = buildAfterTurnLegacyCompactionParams({
|
const legacy = buildAfterTurnLegacyCompactionParams({
|
||||||
attempt: {
|
attempt: {
|
||||||
|
|||||||
@@ -378,6 +378,7 @@ const TARGET_KEYS = [
|
|||||||
"agents.defaults.compaction.qualityGuard.enabled",
|
"agents.defaults.compaction.qualityGuard.enabled",
|
||||||
"agents.defaults.compaction.qualityGuard.maxRetries",
|
"agents.defaults.compaction.qualityGuard.maxRetries",
|
||||||
"agents.defaults.compaction.postCompactionSections",
|
"agents.defaults.compaction.postCompactionSections",
|
||||||
|
"agents.defaults.compaction.model",
|
||||||
"agents.defaults.compaction.memoryFlush",
|
"agents.defaults.compaction.memoryFlush",
|
||||||
"agents.defaults.compaction.memoryFlush.enabled",
|
"agents.defaults.compaction.memoryFlush.enabled",
|
||||||
"agents.defaults.compaction.memoryFlush.softThresholdTokens",
|
"agents.defaults.compaction.memoryFlush.softThresholdTokens",
|
||||||
@@ -810,6 +811,9 @@ describe("config help copy quality", () => {
|
|||||||
expect(/Every Session|Safety/i.test(postCompactionSections)).toBe(true);
|
expect(/Every Session|Safety/i.test(postCompactionSections)).toBe(true);
|
||||||
expect(/\[\]|disable/i.test(postCompactionSections)).toBe(true);
|
expect(/\[\]|disable/i.test(postCompactionSections)).toBe(true);
|
||||||
|
|
||||||
|
const compactionModel = FIELD_HELP["agents.defaults.compaction.model"];
|
||||||
|
expect(/provider\/model|different model|primary agent model/i.test(compactionModel)).toBe(true);
|
||||||
|
|
||||||
const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"];
|
const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"];
|
||||||
expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true);
|
expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1013,6 +1013,8 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.",
|
"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.postCompactionSections":
|
"agents.defaults.compaction.postCompactionSections":
|
||||||
'AGENTS.md H2/H3 section names re-injected after compaction so the agent reruns critical startup guidance. Leave unset to use "Session Startup"/"Red Lines" with legacy fallback to "Every Session"/"Safety"; set to [] to disable reinjection entirely.',
|
'AGENTS.md H2/H3 section names re-injected after compaction so the agent reruns critical startup guidance. Leave unset to use "Session Startup"/"Red Lines" with legacy fallback to "Every Session"/"Safety"; set to [] to disable reinjection entirely.',
|
||||||
|
"agents.defaults.compaction.model":
|
||||||
|
"Optional provider/model override used only for compaction summarization. Set this when you want compaction to run on a different model than the session default, and leave it unset to keep using the primary agent model.",
|
||||||
"agents.defaults.compaction.memoryFlush":
|
"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.",
|
"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":
|
"agents.defaults.compaction.memoryFlush.enabled":
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||||||
"agents.defaults.compaction.qualityGuard.enabled": "Compaction Quality Guard Enabled",
|
"agents.defaults.compaction.qualityGuard.enabled": "Compaction Quality Guard Enabled",
|
||||||
"agents.defaults.compaction.qualityGuard.maxRetries": "Compaction Quality Guard Max Retries",
|
"agents.defaults.compaction.qualityGuard.maxRetries": "Compaction Quality Guard Max Retries",
|
||||||
"agents.defaults.compaction.postCompactionSections": "Post-Compaction Context Sections",
|
"agents.defaults.compaction.postCompactionSections": "Post-Compaction Context Sections",
|
||||||
|
"agents.defaults.compaction.model": "Compaction Model Override",
|
||||||
"agents.defaults.compaction.memoryFlush": "Compaction Memory Flush",
|
"agents.defaults.compaction.memoryFlush": "Compaction Memory Flush",
|
||||||
"agents.defaults.compaction.memoryFlush.enabled": "Compaction Memory Flush Enabled",
|
"agents.defaults.compaction.memoryFlush.enabled": "Compaction Memory Flush Enabled",
|
||||||
"agents.defaults.compaction.memoryFlush.softThresholdTokens":
|
"agents.defaults.compaction.memoryFlush.softThresholdTokens":
|
||||||
|
|||||||
@@ -322,6 +322,10 @@ export type AgentCompactionConfig = {
|
|||||||
* Set to [] to disable post-compaction context injection entirely.
|
* Set to [] to disable post-compaction context injection entirely.
|
||||||
*/
|
*/
|
||||||
postCompactionSections?: string[];
|
postCompactionSections?: string[];
|
||||||
|
/** Optional model override for compaction summarization (e.g. "openrouter/anthropic/claude-sonnet-4-5").
|
||||||
|
* When set, compaction uses this model instead of the agent's primary model.
|
||||||
|
* Falls back to the primary model when unset. */
|
||||||
|
model?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AgentCompactionMemoryFlushConfig = {
|
export type AgentCompactionMemoryFlushConfig = {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export const AgentDefaultsSchema = z
|
|||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
postCompactionSections: z.array(z.string()).optional(),
|
postCompactionSections: z.array(z.string()).optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
memoryFlush: z
|
memoryFlush: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user