mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-17 08:02:12 -06:00
fix(ollama): preserve thinking across reply maintenance (#116963)
Fixes #109527
This commit is contained in:
@@ -58,6 +58,17 @@ describe("resolveEmbeddedCompactionThinkingLevel", () => {
|
||||
}),
|
||||
).toBe("off");
|
||||
});
|
||||
|
||||
it("preserves thinking when the resolved Ollama model reports reasoning support", () => {
|
||||
expect(
|
||||
resolveEmbeddedCompactionThinkingLevel({
|
||||
provider: "ollama",
|
||||
modelId: "qwen3.5:4b",
|
||||
inheritedLevel: "high",
|
||||
catalog: [{ provider: "ollama", id: "qwen3.5:4b", reasoning: true }],
|
||||
}),
|
||||
).toBe("high");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Builds runtime context for context-engine backed embedded compaction.
|
||||
*/
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { ThinkLevel, ThinkingCatalogEntry } from "../../auto-reply/thinking.js";
|
||||
import type { ChatType } from "../../channels/chat-type.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
@@ -66,6 +66,7 @@ export function resolveEmbeddedCompactionThinkingLevel(params: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
inheritedLevel?: ThinkLevel;
|
||||
catalog?: ThinkingCatalogEntry[];
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
agentRuntime?: string | null;
|
||||
@@ -84,6 +85,7 @@ export function resolveEmbeddedCompactionThinkingLevel(params: {
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
level: requestedLevel,
|
||||
catalog: params.catalog,
|
||||
agentId: params.agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
agentRuntime: params.agentRuntime,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* workspace, and sandbox resolution.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import type { ThinkLevel, ThinkingCatalogEntry } from "../../auto-reply/thinking.js";
|
||||
import {
|
||||
createDiagnosticTraceContext,
|
||||
freezeDiagnosticTraceContext,
|
||||
@@ -87,7 +87,6 @@ export async function prepareDirectCompactionAttempt(
|
||||
runtimePolicySessionKey,
|
||||
runtimePolicyAgentId,
|
||||
boundHarnessRuntime,
|
||||
selectedHarnessRuntime,
|
||||
selectedHarnessRuntimeOverride,
|
||||
runtimeModelAuth: { plan: reusableRuntimeAuthPlan, authProfileId, modelAuth: initialModelAuth },
|
||||
provider,
|
||||
@@ -113,15 +112,6 @@ export async function prepareDirectCompactionAttempt(
|
||||
agentHarnessRuntimeOverride: selectedHarnessRuntimeOverride,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
const 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);
|
||||
@@ -171,7 +161,7 @@ export async function prepareDirectCompactionAttempt(
|
||||
}
|
||||
// Overrides stay unset when no bound/planned/explicit harness resolved so auth-aware
|
||||
// selection can pick the credential-owning harness (codex for ChatGPT OAuth); native
|
||||
// transcript compaction stays gated on selectedHarnessRuntime.
|
||||
// transcript compaction stays gated on the selected prepared harness.
|
||||
const {
|
||||
runtimeAuthProfileStore,
|
||||
runtimeAuthPreparation,
|
||||
@@ -297,6 +287,40 @@ export async function prepareDirectCompactionAttempt(
|
||||
const reason = formatErrorMessage(err);
|
||||
return { ok: false as const, result: fail(reason, err) };
|
||||
}
|
||||
const runtimeCompat =
|
||||
runtimeModel.compat && typeof runtimeModel.compat === "object"
|
||||
? (runtimeModel.compat as Record<string, unknown>)
|
||||
: undefined;
|
||||
const thinkingFormat =
|
||||
typeof runtimeCompat?.thinkingFormat === "string" ? runtimeCompat.thinkingFormat : undefined;
|
||||
const supportedReasoningEfforts =
|
||||
runtimeCompat?.supportedReasoningEfforts === null ||
|
||||
(Array.isArray(runtimeCompat?.supportedReasoningEfforts) &&
|
||||
runtimeCompat.supportedReasoningEfforts.every((effort) => typeof effort === "string"))
|
||||
? (runtimeCompat.supportedReasoningEfforts as readonly string[] | null)
|
||||
: undefined;
|
||||
const thinkingCompat =
|
||||
thinkingFormat !== undefined || supportedReasoningEfforts !== undefined
|
||||
? { thinkingFormat, supportedReasoningEfforts }
|
||||
: undefined;
|
||||
const thinkingCatalogEntry = {
|
||||
provider: runtimeModel.provider,
|
||||
id: runtimeModel.id,
|
||||
api: runtimeModel.api,
|
||||
reasoning: runtimeModel.reasoning,
|
||||
params: runtimeModel.params,
|
||||
...(thinkingCompat ? { compat: thinkingCompat } : {}),
|
||||
} satisfies ThinkingCatalogEntry;
|
||||
const thinkLevel = resolveEmbeddedCompactionThinkingLevel({
|
||||
config: params.config,
|
||||
provider: runtimeModel.provider,
|
||||
modelId: runtimeModel.id,
|
||||
inheritedLevel: params.thinkLevel,
|
||||
catalog: [thinkingCatalogEntry],
|
||||
agentId: runtimePolicyAgentId,
|
||||
sessionKey: runtimePolicySessionKey,
|
||||
agentRuntime: preparedHarnessRuntime,
|
||||
});
|
||||
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
const sandboxSessionKey =
|
||||
|
||||
@@ -115,6 +115,26 @@ describe("executeAgentTurn: run lifecycle and ownership", () => {
|
||||
expect(followupRun.run.thinkLevel).toBe("ultra");
|
||||
});
|
||||
|
||||
it("preserves thinking for runtime-discovered Ollama fallback models", async () => {
|
||||
const followupRun = createFollowupRun();
|
||||
followupRun.run.provider = "openai";
|
||||
followupRun.run.model = "gpt-5.6-sol";
|
||||
followupRun.run.thinkLevel = "high";
|
||||
followupRun.run.thinkingCatalog = [{ provider: "ollama", id: "qwen3.5:4b", reasoning: true }];
|
||||
state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => {
|
||||
const result = await params.run("ollama", "qwen3.5:4b");
|
||||
return { result, provider: "ollama", model: "qwen3.5:4b", attempts: [] };
|
||||
});
|
||||
state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {} });
|
||||
|
||||
const executeAgentTurn = await getExecuteAgentTurnForTest();
|
||||
await executeAgentTurn({
|
||||
...createMinimalRunAgentTurnParams({ followupRun }),
|
||||
});
|
||||
|
||||
expect(state.runEmbeddedAgentMock.mock.calls[0]?.[0]?.thinkLevel).toBe("high");
|
||||
});
|
||||
|
||||
it("freezes abort ownership only after model fallback settles", async () => {
|
||||
const { replyOperation, freezeAbortMock } = createMockReplyOperation();
|
||||
const followupRun = createFollowupRun();
|
||||
|
||||
@@ -112,6 +112,7 @@ export async function runAgentFallbackCandidates(params: AgentFallbackCycleParam
|
||||
provider,
|
||||
modelId: model,
|
||||
level: turn.followupRun.run.thinkLevel,
|
||||
catalog: turn.followupRun.run.thinkingCatalog,
|
||||
agentId: turn.followupRun.run.agentId,
|
||||
sessionKey: turn.followupRun.run.runtimePolicySessionKey ?? turn.sessionKey,
|
||||
sessionEntry: turn.getActiveSessionEntry(),
|
||||
|
||||
@@ -650,6 +650,42 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(followupRun.run.thinkLevel).toBe("ultra");
|
||||
});
|
||||
|
||||
it("preserves thinking for runtime-discovered Ollama memory-flush models", async () => {
|
||||
const storePath = path.join(rootDir, "sessions.json");
|
||||
const sessionKey = "main";
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 80_000,
|
||||
thinkingLevel: "high",
|
||||
};
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
await writeTestSessionStore(storePath, sessionKey, sessionEntry);
|
||||
const followupRun = createTestFollowupRun({
|
||||
provider: "ollama",
|
||||
model: "qwen3.5:4b",
|
||||
});
|
||||
followupRun.run.thinkLevel = "high";
|
||||
followupRun.run.thinkingCatalog = [{ provider: "ollama", id: "qwen3.5:4b", reasoning: true }];
|
||||
|
||||
await runMemoryFlushIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun,
|
||||
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
||||
defaultModel: "ollama/qwen3.5:4b",
|
||||
agentCfgContextTokens: 100_000,
|
||||
resolvedVerboseLevel: "off",
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
storePath,
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
|
||||
expect(requireEmbeddedAgentCall().thinkLevel).toBe("high");
|
||||
});
|
||||
|
||||
it("keeps catalog-adopted sessions on Codex for memory flush turns", async () => {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "catalog-adopted-session",
|
||||
|
||||
@@ -1527,6 +1527,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
provider,
|
||||
modelId: model,
|
||||
level: params.followupRun.run.thinkLevel,
|
||||
catalog: params.followupRun.run.thinkingCatalog,
|
||||
agentId: params.followupRun.run.agentId,
|
||||
sessionKey:
|
||||
params.runtimePolicySessionKey ??
|
||||
|
||||
@@ -563,6 +563,7 @@ export async function prepareReplyRunAdmission(context: PreparedReplyRunContext)
|
||||
kind: "ready",
|
||||
context,
|
||||
resolvedThinkLevel,
|
||||
thinkingCatalog,
|
||||
sessionEntry,
|
||||
skillsSnapshot,
|
||||
prefixedCommandBody,
|
||||
|
||||
@@ -39,6 +39,7 @@ export async function executePreparedReplyRun(state: PreparedReplyRunAdmission)
|
||||
const {
|
||||
context,
|
||||
resolvedThinkLevel,
|
||||
thinkingCatalog,
|
||||
skillsSnapshot,
|
||||
prefixedCommandBody,
|
||||
queuedBody,
|
||||
@@ -371,6 +372,7 @@ export async function executePreparedReplyRun(state: PreparedReplyRunAdmission)
|
||||
autoFallbackPrimaryProbe: params.autoFallbackPrimaryProbe,
|
||||
authProfileId,
|
||||
authProfileIdSource,
|
||||
thinkingCatalog,
|
||||
thinkLevel: resolvedThinkLevel,
|
||||
...(() => {
|
||||
if (useFastReplyRuntime) {
|
||||
|
||||
@@ -477,6 +477,13 @@ describe("runPreparedReply media-only handling", () => {
|
||||
expect(resolveThinkingCatalog).toHaveBeenCalledOnce();
|
||||
const call = requireRunReplyAgentCall();
|
||||
expect(call.followupRun.run.thinkLevel).toBe("off");
|
||||
expect(call.followupRun.run.thinkingCatalog).toEqual([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "chat-latest",
|
||||
reasoning: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports unsupported explicit one-turn thinking overrides", async () => {
|
||||
|
||||
@@ -142,13 +142,18 @@ describe("refreshQueuedFollowupSession", () => {
|
||||
nextProvider: "openai",
|
||||
nextModel: "gpt-5.6-luna",
|
||||
nextRouteResolution: "resolved",
|
||||
nextThinking: { level: "ultra", agentRuntime: "codex" },
|
||||
nextThinking: {
|
||||
level: "ultra",
|
||||
catalog: [{ provider: "openai", id: "gpt-5.6-luna", reasoning: true }],
|
||||
agentRuntime: "codex",
|
||||
},
|
||||
});
|
||||
|
||||
expect(queue.items[0]?.run).toMatchObject({
|
||||
provider: "openai",
|
||||
model: "gpt-5.6-luna",
|
||||
thinkLevel: "max",
|
||||
thinkingCatalog: [{ provider: "openai", id: "gpt-5.6-luna", reasoning: true }],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -256,6 +256,7 @@ export function refreshQueuedFollowupSession(params: {
|
||||
run.authProfileIdSource = run.authProfileId ? params.nextAuthProfileIdSource : undefined;
|
||||
}
|
||||
if (params.nextThinking) {
|
||||
run.thinkingCatalog = params.nextThinking.catalog;
|
||||
const explicitLevel = normalizeThinkLevel(params.nextThinking.level);
|
||||
run.thinkLevel = explicitLevel
|
||||
? resolveSupportedThinkingLevel({
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
TurnAdoptionLifecycle,
|
||||
} from "../../get-reply-options.types.js";
|
||||
import type { OriginatingChannelType } from "../../templating.js";
|
||||
import type { ThinkingCatalogEntry } from "../../thinking.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../directives.js";
|
||||
import { releaseRecentQueueMessageId } from "./recent-message-ids.js";
|
||||
|
||||
@@ -167,6 +168,8 @@ export type FollowupRun = {
|
||||
autoFallbackPrimaryProbe?: AutoFallbackPrimaryProbe;
|
||||
authProfileId?: string;
|
||||
authProfileIdSource?: "auto" | "user";
|
||||
/** Prepared model metadata reused when fallbacks revalidate the immutable thinking request. */
|
||||
thinkingCatalog?: ThinkingCatalogEntry[];
|
||||
thinkLevel?: ThinkLevel;
|
||||
fastMode?: FastMode;
|
||||
fastModeAutoOnSeconds?: number;
|
||||
|
||||
Reference in New Issue
Block a user