mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(agents): preserve compatible CLI session runtime pins
Preserves provider-compatible CLI runtime session pins across reply execution, follow-up execution, dispatch visibility, preflight compaction, and memory flush. This keeps sessions pinned to compatible CLI runtimes such as `claude-cli` from leaking into embedded OpenClaw maintenance paths while still rejecting cross-provider runtime pins. Original PR by @yu-xin-c; includes maintainer follow-up for the sibling memory paths. Verification: - `node scripts/run-vitest.mjs src/auto-reply/reply/agent-runner-execution.test.ts src/auto-reply/reply/agent-runner-memory.test.ts src/agents/model-runtime-aliases.test.ts --maxWorkers=1` - autoreview clean - Crabbox AWS `cbx_44400b494e97` / `coral-prawn`, run `run_69dd43475e39`: `check:changed` passed - exact PR head CI green: `303b2f794f6c01fcf21b62b27c536b5f6eceb421`
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Tests agent runner execution setup, command args, and model fallback routing.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { OAuthRefreshFailureError } from "../../agents/auth-profiles/oauth-refresh-failure.js";
|
||||
import { testing as cliBackendsTesting } from "../../agents/cli-backends.js";
|
||||
import { FailoverError } from "../../agents/failover-error.js";
|
||||
import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js";
|
||||
import { MissingProviderAuthError } from "../../agents/model-auth.js";
|
||||
@@ -49,6 +50,10 @@ const GENERIC_RUN_FAILURE_TEXT =
|
||||
"⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session.";
|
||||
|
||||
describe("resolveSessionRuntimeOverrideForProvider", () => {
|
||||
afterEach(() => {
|
||||
cliBackendsTesting.resetDepsForTest();
|
||||
});
|
||||
|
||||
it("ignores unsupported session runtime pins", () => {
|
||||
expect(
|
||||
resolveSessionRuntimeOverrideForProvider({
|
||||
@@ -57,6 +62,48 @@ describe("resolveSessionRuntimeOverrideForProvider", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps CLI runtime pins only when the runtime serves the selected provider", () => {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolveRuntimeCliBackends: () => [],
|
||||
resolvePluginSetupCliBackend: ({ backend, config }) =>
|
||||
backend === "claude-cli" && config
|
||||
? {
|
||||
pluginId: "anthropic",
|
||||
backend: {
|
||||
id: "claude-cli",
|
||||
modelProvider: "anthropic",
|
||||
config: { command: "claude" },
|
||||
bundleMcp: false,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"claude-cli": { command: "claude" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveSessionRuntimeOverrideForProvider({
|
||||
provider: "anthropic",
|
||||
entry: { agentRuntimeOverride: "claude-cli" },
|
||||
cfg,
|
||||
}),
|
||||
).toBe("claude-cli");
|
||||
expect(
|
||||
resolveSessionRuntimeOverrideForProvider({
|
||||
provider: "openai",
|
||||
entry: { agentRuntimeOverride: "claude-cli" },
|
||||
cfg,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function makeTestModel(id: string, contextTokens: number): ModelDefinitionConfig {
|
||||
|
||||
@@ -47,7 +47,10 @@ import { ensureSelectedAgentHarnessPlugin } from "../../agents/harness/runtime-p
|
||||
import { LiveSessionModelSwitchError } from "../../agents/live-model-switch-error.js";
|
||||
import { isMissingProviderAuthError } from "../../agents/model-auth.js";
|
||||
import { runWithModelFallback, isFallbackSummaryError } from "../../agents/model-fallback.js";
|
||||
import { resolveCliRuntimeExecutionProvider } from "../../agents/model-runtime-aliases.js";
|
||||
import {
|
||||
isCliRuntimeAliasForProvider,
|
||||
resolveCliRuntimeExecutionProvider,
|
||||
} from "../../agents/model-runtime-aliases.js";
|
||||
import {
|
||||
isCliProvider,
|
||||
resolveModelRefFromString,
|
||||
@@ -1420,6 +1423,7 @@ function emitModelFallbackStepLifecycle(params: {
|
||||
export function resolveSessionRuntimeOverrideForProvider(params: {
|
||||
provider: string;
|
||||
entry?: Pick<SessionEntry, "agentRuntimeOverride">;
|
||||
cfg?: OpenClawConfig;
|
||||
}): string | undefined {
|
||||
const provider = normalizeLowercaseStringOrEmpty(params.provider);
|
||||
const runtime = normalizeLowercaseStringOrEmpty(params.entry?.agentRuntimeOverride);
|
||||
@@ -1429,6 +1433,9 @@ export function resolveSessionRuntimeOverrideForProvider(params: {
|
||||
if (provider === "openai" && runtime === "codex") {
|
||||
return "codex";
|
||||
}
|
||||
if (isCliRuntimeAliasForProvider({ provider, runtime, cfg: params.cfg })) {
|
||||
return runtime;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2020,6 +2027,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
resolveSessionRuntimeOverrideForProvider({
|
||||
provider,
|
||||
entry: params.getActiveSessionEntry(),
|
||||
cfg: runtimeConfig,
|
||||
}),
|
||||
prepareAgentHarnessRuntime: async ({ provider, model, agentHarnessRuntimeOverride }) => {
|
||||
await agentTurnTiming.measure("fallback_prepare_harness", () =>
|
||||
@@ -2104,6 +2112,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
const resolvedSessionRuntimeOverride = resolveSessionRuntimeOverrideForProvider({
|
||||
provider,
|
||||
entry: params.getActiveSessionEntry(),
|
||||
cfg: runtimeConfig,
|
||||
});
|
||||
const resolvedSelectedAuthProfile = resolveRunAuthProfile(candidateRun, provider, {
|
||||
config: runtimeConfig,
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { testing as cliBackendsTesting } from "../../agents/cli-backends.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import {
|
||||
clearMemoryPluginState,
|
||||
@@ -226,6 +227,7 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
setAgentRunnerMemoryTestDeps();
|
||||
cliBackendsTesting.resetDepsForTest();
|
||||
clearMemoryPluginState();
|
||||
await fs.rm(rootDir, { recursive: true, force: true });
|
||||
});
|
||||
@@ -912,6 +914,46 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips memory flush for compatible CLI session runtime pins", async () => {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "claude-cli",
|
||||
modelProvider: "anthropic",
|
||||
pluginId: "anthropic",
|
||||
config: { command: "claude" },
|
||||
},
|
||||
],
|
||||
});
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 80_000,
|
||||
compactionCount: 1,
|
||||
agentRuntimeOverride: "claude-cli",
|
||||
};
|
||||
|
||||
const entry = await runMemoryFlushIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
}),
|
||||
sessionCtx: { Provider: "whatsapp" } as unknown as TemplateContext,
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100_000,
|
||||
resolvedVerboseLevel: "off",
|
||||
sessionEntry,
|
||||
sessionStore: { main: sessionEntry },
|
||||
sessionKey: "main",
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses runtime policy session key when checking memory-flush sandbox writability", async () => {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
@@ -1619,6 +1661,61 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips preflight compaction for compatible CLI session runtime pins", async () => {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "claude-cli",
|
||||
modelProvider: "anthropic",
|
||||
pluginId: "anthropic",
|
||||
config: { command: "claude" },
|
||||
},
|
||||
],
|
||||
});
|
||||
registerMemoryFlushPlanResolverForTest(() => ({
|
||||
softThresholdTokens: 4_000,
|
||||
forceFlushTranscriptBytes: 1_000_000_000,
|
||||
reserveTokensFloor: 0,
|
||||
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
||||
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
||||
relativePath: "memory/2023-11-14.md",
|
||||
}));
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 347_000,
|
||||
totalTokensFresh: true,
|
||||
agentRuntimeOverride: "claude-cli",
|
||||
};
|
||||
|
||||
const entry = await runPreflightCompactionIfNeeded({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: { models: [{ id: "claude-opus-4-6", contextWindow: 350_000 }] },
|
||||
},
|
||||
},
|
||||
agents: { defaults: { compaction: { memoryFlush: {} } } },
|
||||
} as never,
|
||||
followupRun: createTestFollowupRun({
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
sessionEntry,
|
||||
sessionStore: { main: sessionEntry },
|
||||
sessionKey: "main",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the OpenAI API context window for persisted OpenClaw runtime overrides", async () => {
|
||||
registerMemoryFlushPlanResolverForTest(() => ({
|
||||
softThresholdTokens: 4_000,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { classifyCompactionReason } from "../../agents/embedded-agent-runner/com
|
||||
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
|
||||
import { ensureSelectedAgentHarnessPlugin } from "../../agents/harness/runtime-plugin.js";
|
||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||
import { isCliRuntimeAliasForProvider } from "../../agents/model-runtime-aliases.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { resolveContextConfigProviderForRuntime } from "../../agents/openai-routing.js";
|
||||
import type { AgentMessage } from "../../agents/runtime/index.js";
|
||||
@@ -235,6 +236,22 @@ function resolveMemoryFlushRuntimeOverrideForProvider(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function followupUsesCliRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
followupRun: FollowupRun;
|
||||
sessionEntry?: Pick<SessionEntry, "agentRuntimeOverride">;
|
||||
}): boolean {
|
||||
const provider = params.followupRun.run.provider;
|
||||
if (isCliProvider(provider, params.cfg)) {
|
||||
return true;
|
||||
}
|
||||
return isCliRuntimeAliasForProvider({
|
||||
provider,
|
||||
runtime: params.sessionEntry?.agentRuntimeOverride,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveFollowupContextConfigProvider(params: {
|
||||
cfg: OpenClawConfig;
|
||||
followupRun: FollowupRun;
|
||||
@@ -709,7 +726,11 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
|
||||
const isCli = isCliProvider(params.followupRun.run.provider, params.cfg);
|
||||
const isCli = followupUsesCliRuntime({
|
||||
cfg: params.cfg,
|
||||
followupRun: params.followupRun,
|
||||
sessionEntry: entry,
|
||||
});
|
||||
if (params.isHeartbeat || isCli) {
|
||||
return entry ?? params.sessionEntry;
|
||||
}
|
||||
@@ -1026,11 +1047,15 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
return sandboxCfg.workspaceAccess === "rw";
|
||||
})();
|
||||
|
||||
const isCli = isCliProvider(params.followupRun.run.provider, params.cfg);
|
||||
const canAttemptFlush = memoryFlushWritable && !params.isHeartbeat && !isCli;
|
||||
let entry =
|
||||
params.sessionEntry ??
|
||||
(params.sessionKey ? params.sessionStore?.[params.sessionKey] : undefined);
|
||||
const isCli = followupUsesCliRuntime({
|
||||
cfg: params.cfg,
|
||||
followupRun: params.followupRun,
|
||||
sessionEntry: entry,
|
||||
});
|
||||
const canAttemptFlush = memoryFlushWritable && !params.isHeartbeat && !isCli;
|
||||
const contextWindowTokens = resolveMemoryFlushContextWindowTokens({
|
||||
cfg: params.cfg,
|
||||
provider: resolveFollowupContextConfigProvider({
|
||||
|
||||
@@ -647,6 +647,7 @@ const resolveHarnessSourceVisibleRepliesDefault = (params: {
|
||||
const agentHarnessRuntimeOverride = resolveSessionRuntimeOverrideForProvider({
|
||||
provider: candidate.provider,
|
||||
entry: params.entry,
|
||||
cfg: params.cfg,
|
||||
});
|
||||
const harness = selectAgentHarness({
|
||||
provider: candidate.provider,
|
||||
|
||||
@@ -803,6 +803,7 @@ export function createFollowupRunner(params: {
|
||||
resolveSessionRuntimeOverrideForProvider({
|
||||
provider,
|
||||
entry: activeSessionEntry,
|
||||
cfg: runtimeConfig,
|
||||
}),
|
||||
prepareAgentHarnessRuntime: async ({ provider, model, agentHarnessRuntimeOverride }) => {
|
||||
await ensureSelectedAgentHarnessPlugin({
|
||||
@@ -837,6 +838,7 @@ export function createFollowupRunner(params: {
|
||||
const sessionRuntimeOverride = resolveSessionRuntimeOverrideForProvider({
|
||||
provider,
|
||||
entry: activeSessionEntry,
|
||||
cfg: runtimeConfig,
|
||||
});
|
||||
const cliExecutionProvider =
|
||||
(sessionRuntimeOverride && isCliProvider(sessionRuntimeOverride, runtimeConfig)
|
||||
|
||||
Reference in New Issue
Block a user