From 52bf19c45e0891a17dd7031aefe3c10f4e4713e9 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:24:07 -0500 Subject: [PATCH] fix(active-memory): remove built-in fallback model (#65047) * fix(active-memory): remove built-in fallback model * fix(active-memory): tighten fallback cleanup --- docs/concepts/active-memory.md | 22 +++++------ extensions/active-memory/index.test.ts | 7 +++- extensions/active-memory/index.ts | 53 +++++++++++++------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/concepts/active-memory.md b/docs/concepts/active-memory.md index 088850190af..596ad4955af 100644 --- a/docs/concepts/active-memory.md +++ b/docs/concepts/active-memory.md @@ -111,7 +111,7 @@ What this means: - `config.agents: ["main"]` opts only the `main` agent into active memory - `config.allowedChatTypes: ["direct"]` keeps active memory on for direct-message style sessions only by default - if `config.model` is unset, active memory inherits the current session model first -- `config.modelFallbackPolicy: "default-remote"` keeps the built-in remote fallback as the default when no explicit or inherited model is available +- `config.modelFallback` optionally provides your own fallback provider/model for recall - `config.promptStyle: "balanced"` uses the default general-purpose prompt style for `recent` mode - active memory still runs only on eligible interactive persistent chat sessions @@ -335,26 +335,22 @@ If `config.model` is unset, Active Memory tries to resolve a model in this order explicit plugin model -> current session model -> agent primary model --> optional built-in remote fallback +-> optional configured fallback model ``` -`config.modelFallbackPolicy` controls the last step. +`config.modelFallback` controls the configured fallback step. -Default: +Optional custom fallback: ```json5 -modelFallbackPolicy: "default-remote" +modelFallback: "google/gemini-3-flash" ``` -Other option: +If no explicit, inherited, or configured fallback model resolves, Active Memory +skips recall for that turn. -```json5 -modelFallbackPolicy: "resolved-only" -``` - -Use `resolved-only` if you want Active Memory to skip recall instead of falling -back to the built-in remote default when no explicit or inherited model is -available. +`config.modelFallbackPolicy` is retained only as a deprecated compatibility +field for older configs. It no longer changes runtime behavior. ## Advanced escape hatches diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 13300b67951..c85ee37554a 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -771,12 +771,12 @@ describe("active-memory plugin", () => { expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); }); - it("uses config.modelFallback before the built-in default fallback", async () => { + it("uses config.modelFallback when no session or agent model resolves", async () => { api.config = {}; api.pluginConfig = { agents: ["main"], modelFallback: "google/gemini-3-flash", - modelFallbackPolicy: "resolved-only", + modelFallbackPolicy: "default-remote", }; await plugin.register(api as unknown as OpenClawPluginApi); @@ -794,6 +794,9 @@ describe("active-memory plugin", () => { provider: "google", model: "gemini-3-flash-preview", }); + expect(api.logger.warn).toHaveBeenCalledWith( + expect.stringContaining("config.modelFallbackPolicy is deprecated"), + ); }); it("does not use a built-in fallback model even when default-remote is configured", async () => { diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 9d2d62f079e..481f8671280 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -238,6 +238,11 @@ function normalizePromptConfigText(value: unknown): string | undefined { return text ? text : undefined; } +function hasDeprecatedModelFallbackPolicy(pluginConfig: unknown): boolean { + const raw = asRecord(pluginConfig); + return raw ? Object.hasOwn(raw, "modelFallbackPolicy") : false; +} + function resolveSafeTranscriptDir(baseSessionsDir: string, transcriptDir: string): string { const normalized = transcriptDir.trim(); if (!normalized || normalized.includes(":") || path.isAbsolute(normalized)) { @@ -1217,35 +1222,22 @@ function getModelRef( modelProviderId?: string; modelId?: string; }, -): { - modelRef?: { - provider: string; - model: string; - }; - source: "plugin-model" | "session-model" | "agent-primary" | "config-fallback" | "none"; -} { +): { provider: string; model: string } | undefined { const currentRunModel = ctx?.modelProviderId && ctx?.modelId ? `${ctx.modelProviderId}/${ctx.modelId}` : undefined; - const agentPrimaryModel = resolveAgentEffectiveModelPrimary(api.config, agentId); - const candidates: Array<{ - source: "plugin-model" | "session-model" | "agent-primary" | "config-fallback"; - value?: string; - }> = [ - { source: "plugin-model", value: config.model }, - { source: "session-model", value: currentRunModel }, - { source: "agent-primary", value: agentPrimaryModel }, - { source: "config-fallback", value: config.modelFallback }, + const candidates = [ + config.model, + currentRunModel, + resolveAgentEffectiveModelPrimary(api.config, agentId), + config.modelFallback, ]; for (const candidate of candidates) { - const parsed = parseModelCandidate(candidate.value); + const parsed = parseModelCandidate(candidate); if (parsed) { - return { - modelRef: parsed, - source: candidate.source, - }; + return parsed; } } - return { source: "none" }; + return undefined; } async function runRecallSubagent(params: { @@ -1263,7 +1255,7 @@ async function runRecallSubagent(params: { }): Promise<{ rawReply: string; transcriptPath?: string }> { const workspaceDir = resolveAgentWorkspaceDir(params.api.config, params.agentId); const agentDir = resolveAgentDir(params.api.config, params.agentId); - const { modelRef } = getModelRef(params.api, params.agentId, params.config, { + const modelRef = getModelRef(params.api, params.agentId, params.config, { modelProviderId: params.currentModelProviderId, modelId: params.currentModelId, }); @@ -1507,11 +1499,20 @@ export default definePluginEntry({ description: "Proactively surfaces relevant memory before eligible conversational replies.", register(api: OpenClawPluginApi) { let config = normalizePluginConfig(api.pluginConfig); + const warnDeprecatedModelFallbackPolicy = (pluginConfig: unknown) => { + if (hasDeprecatedModelFallbackPolicy(pluginConfig)) { + api.logger.warn?.( + "active-memory: config.modelFallbackPolicy is deprecated and no longer changes runtime behavior; set config.modelFallback explicitly if you want a fallback model", + ); + } + }; + warnDeprecatedModelFallbackPolicy(api.pluginConfig); const refreshLiveConfigFromRuntime = () => { - config = normalizePluginConfig( + const livePluginConfig = resolveActiveMemoryPluginConfigFromConfig(api.runtime.config.loadConfig()) ?? - api.pluginConfig, - ); + api.pluginConfig; + config = normalizePluginConfig(livePluginConfig); + warnDeprecatedModelFallbackPolicy(livePluginConfig); }; api.registerCommand({ name: "active-memory",