mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 14:51:03 +00:00
fix(doctor): preserve legacy Claude CLI runtime intent
This commit is contained in:
committed by
GitHub
parent
ae29d14abf
commit
cce00498cd
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex app-server: complete OpenClaw dynamic tool diagnostics at the request boundary so successful, failed, timed out, aborted, and blocked tool calls do not leave active tool state behind. Fixes #83474. Thanks @rozmiarD.
|
||||
- Gateway/config: keep config writes from failing on unrelated unresolved auth-profile SecretRefs while preserving live auth-profile runtime snapshots.
|
||||
- Gateway/sessions: clear stored CLI provider resume bindings on non-subagent `/reset` so the next turn starts a fresh provider-side CLI conversation instead of resuming old context. (#83448) Thanks @jasonyliu.
|
||||
- Doctor: preserve legacy whole-agent Claude CLI intent by moving matching Anthropic model selections to model-scoped runtime policy before removing stale runtime pins. Fixes #83491. Thanks @danielcrick.
|
||||
- Discord/OpenAI: keep realtime Discord voice sessions hearing follow-up turns with OpenAI realtime and prebuffer assistant playback to avoid choppy starts. (#80505) Thanks @Solvely-Colin.
|
||||
- Discord/subagents: route the initial reply from thread-bound delegated sessions into the bound Discord thread instead of the parent channel. Fixes #83170. (#83172) Thanks @100menotu001.
|
||||
- Gateway/sessions: rotate failed agent sessions when their transcript file is missing instead of wedging per-channel lanes. Fixes #83488. (#83553) Thanks @LLagoon3.
|
||||
|
||||
@@ -639,6 +639,60 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves legacy whole-agent Claude CLI intent for canonical Anthropic defaults", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-7",
|
||||
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.5"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig);
|
||||
|
||||
expect(res.config.agents?.defaults?.agentRuntime).toEqual({ id: "claude-cli" });
|
||||
expect(res.config.agents?.defaults?.models).toEqual({
|
||||
"anthropic/claude-opus-4-7": {
|
||||
alias: "Opus",
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
"anthropic/claude-sonnet-4-6": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved agents.defaults.agentRuntime.id claude-cli to matching anthropic model runtime policy.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not overwrite explicit model runtime while preserving legacy whole-agent CLI intent", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "paige",
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as OpenClawConfig);
|
||||
|
||||
expect(res.config.agents?.list?.[0]?.agentRuntime).toEqual({ id: "claude-cli" });
|
||||
expect(res.config.agents?.list?.[0]?.models).toEqual({
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } },
|
||||
});
|
||||
expect(res.changes).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("migrates legacy Codex CLI primary refs to the Codex app-server route", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
agents: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
legacyRuntimeModelAliasRequiresRuntimePolicy,
|
||||
listLegacyRuntimeModelProviderAliases,
|
||||
migrateLegacyRuntimeModelRef,
|
||||
} from "../../../agents/model-runtime-aliases.js";
|
||||
import { normalizeProviderId } from "../../../agents/provider-id.js";
|
||||
@@ -205,6 +206,32 @@ type SelectedRuntimeRef = {
|
||||
const LEGACY_CODEX_CLI_RUNTIME_ID = "codex-cli";
|
||||
const CODEX_APP_SERVER_RUNTIME_ID = "codex";
|
||||
|
||||
function resolveLegacyWholeAgentRuntimePolicy(raw: unknown):
|
||||
| {
|
||||
provider: string;
|
||||
runtime: string;
|
||||
requiresRuntimePolicy: boolean;
|
||||
}
|
||||
| undefined {
|
||||
if (!isRecord(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = normalizeOptionalLowercaseString(raw.id);
|
||||
if (!runtime || runtime === "auto" || runtime === "pi") {
|
||||
return undefined;
|
||||
}
|
||||
const alias = listLegacyRuntimeModelProviderAliases().find(
|
||||
(entry) => entry.cli && normalizeProviderId(entry.runtime) === runtime,
|
||||
);
|
||||
return alias
|
||||
? {
|
||||
provider: alias.provider,
|
||||
runtime: alias.runtime,
|
||||
requiresRuntimePolicy: alias.requiresRuntimePolicy,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function migratedRuntimeRequiresPolicy(legacyProvider: string): boolean {
|
||||
return legacyRuntimeModelAliasRequiresRuntimePolicy(legacyProvider);
|
||||
}
|
||||
@@ -417,6 +444,44 @@ function ensureSelectedModelRuntimePolicies(
|
||||
return { value: next, changed };
|
||||
}
|
||||
|
||||
function selectedCanonicalModelRefsForRuntimePolicy(
|
||||
rawModel: unknown,
|
||||
provider: string,
|
||||
runtime: string,
|
||||
requiresRuntimePolicy: boolean,
|
||||
): SelectedRuntimeRef[] {
|
||||
const refs: SelectedRuntimeRef[] = [];
|
||||
const addRef = (rawRef: unknown) => {
|
||||
if (typeof rawRef !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = rawRef.trim();
|
||||
const slash = trimmed.indexOf("/");
|
||||
if (slash <= 0 || slash >= trimmed.length - 1) {
|
||||
return;
|
||||
}
|
||||
if (normalizeProviderId(trimmed.slice(0, slash)) !== normalizeProviderId(provider)) {
|
||||
return;
|
||||
}
|
||||
refs.push({ ref: trimmed, runtime, requiresRuntimePolicy });
|
||||
};
|
||||
|
||||
if (typeof rawModel === "string") {
|
||||
addRef(rawModel);
|
||||
return refs;
|
||||
}
|
||||
if (!isRecord(rawModel)) {
|
||||
return refs;
|
||||
}
|
||||
addRef(rawModel.primary);
|
||||
if (Array.isArray(rawModel.fallbacks)) {
|
||||
for (const fallback of rawModel.fallbacks) {
|
||||
addRef(fallback);
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function normalizeLegacyCodexCliRuntimePinsInModels(
|
||||
rawModels: unknown,
|
||||
path: string,
|
||||
@@ -451,6 +516,7 @@ function normalizeLegacyRuntimeAgentContainer(
|
||||
): { value: Record<string, unknown>; changed: boolean } {
|
||||
let changed = false;
|
||||
const next: Record<string, unknown> = { ...raw };
|
||||
const legacyWholeAgentRuntime = resolveLegacyWholeAgentRuntimePolicy(raw.agentRuntime);
|
||||
|
||||
const model = normalizeLegacyRuntimeAgentModelConfig(raw.model);
|
||||
if (model.changed) {
|
||||
@@ -484,6 +550,23 @@ function normalizeLegacyRuntimeAgentContainer(
|
||||
}
|
||||
}
|
||||
|
||||
if (legacyWholeAgentRuntime) {
|
||||
const selectedRefs = selectedCanonicalModelRefsForRuntimePolicy(
|
||||
next.model ?? raw.model,
|
||||
legacyWholeAgentRuntime.provider,
|
||||
legacyWholeAgentRuntime.runtime,
|
||||
legacyWholeAgentRuntime.requiresRuntimePolicy,
|
||||
);
|
||||
const modelRuntimes = ensureSelectedModelRuntimePolicies(next.models, selectedRefs);
|
||||
if (modelRuntimes.changed) {
|
||||
next.models = modelRuntimes.value;
|
||||
changed = true;
|
||||
changes.push(
|
||||
`Moved ${path}.agentRuntime.id ${legacyWholeAgentRuntime.runtime} to matching ${legacyWholeAgentRuntime.provider} model runtime policy.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const codexCliRuntimePins = normalizeLegacyCodexCliRuntimePinsInModels(
|
||||
next.models,
|
||||
`${path}.models`,
|
||||
|
||||
@@ -602,6 +602,85 @@ describe("legacy migrate sandbox scope aliases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("moves recoverable whole-agent Claude CLI runtime policy before removing stale pins", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-7",
|
||||
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.5"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "paige",
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toStrictEqual([
|
||||
"Moved agents.defaults.agentRuntime.id claude-cli to matching anthropic model runtime policy.",
|
||||
"Removed agents.defaults.agentRuntime; runtime is now provider/model scoped.",
|
||||
"Moved agents.list.0.agentRuntime.id claude-cli to matching anthropic model runtime policy.",
|
||||
"Removed agents.list.0.agentRuntime; runtime is now provider/model scoped.",
|
||||
]);
|
||||
expect(res.config?.agents?.defaults).toEqual({
|
||||
model: {
|
||||
primary: "anthropic/claude-opus-4-7",
|
||||
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.5"],
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": {
|
||||
alias: "Opus",
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
"anthropic/claude-sonnet-4-6": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.config?.agents?.list?.[0]).toEqual({
|
||||
id: "paige",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
models: {
|
||||
"anthropic/claude-sonnet-4-6": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite explicit model runtime when removing stale whole-agent policy", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
agents: {
|
||||
defaults: {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.changes).toStrictEqual([
|
||||
"Removed agents.defaults.agentRuntime; runtime is now provider/model scoped.",
|
||||
]);
|
||||
expect(res.config?.agents?.defaults).toEqual({
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("moves agents.defaults.sandbox.perSession into scope", () => {
|
||||
const res = migrateLegacyConfigForTest({
|
||||
agents: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { listLegacyRuntimeModelProviderAliases } from "../../../agents/model-runtime-aliases.js";
|
||||
import { normalizeProviderId } from "../../../agents/provider-id.js";
|
||||
import {
|
||||
defineLegacyConfigMigration,
|
||||
ensureRecord,
|
||||
@@ -27,6 +29,11 @@ const AGENT_HEARTBEAT_KEYS = new Set([
|
||||
|
||||
const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]);
|
||||
|
||||
type LegacyAgentRuntimeIntent = {
|
||||
provider: string;
|
||||
runtime: string;
|
||||
};
|
||||
|
||||
const MEMORY_SEARCH_RULE: LegacyConfigRule = {
|
||||
path: ["memorySearch"],
|
||||
message:
|
||||
@@ -275,11 +282,116 @@ function removeLegacyAgentRuntimePolicy(
|
||||
changes.push(`Removed ${pathLabel}.embeddedHarness; runtime is now provider/model scoped.`);
|
||||
}
|
||||
if (getRecord(container.agentRuntime) !== null) {
|
||||
preserveLegacyWholeAgentRuntimePolicy(container, pathLabel, changes);
|
||||
delete container.agentRuntime;
|
||||
changes.push(`Removed ${pathLabel}.agentRuntime; runtime is now provider/model scoped.`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLegacyAgentRuntimeIntent(raw: unknown): LegacyAgentRuntimeIntent | undefined {
|
||||
const record = getRecord(raw);
|
||||
if (!record) {
|
||||
return undefined;
|
||||
}
|
||||
const runtime = typeof record.id === "string" ? record.id.trim().toLowerCase() : "";
|
||||
if (!runtime || runtime === "auto" || runtime === "pi") {
|
||||
return undefined;
|
||||
}
|
||||
const alias = listLegacyRuntimeModelProviderAliases().find(
|
||||
(entry) => entry.cli && normalizeProviderId(entry.runtime) === runtime,
|
||||
);
|
||||
return alias ? { provider: alias.provider, runtime: alias.runtime } : undefined;
|
||||
}
|
||||
|
||||
function selectedCanonicalModelRefsForRuntimePolicy(rawModel: unknown, provider: string): string[] {
|
||||
const refs: string[] = [];
|
||||
const addRef = (rawRef: unknown) => {
|
||||
if (typeof rawRef !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = rawRef.trim();
|
||||
const slash = trimmed.indexOf("/");
|
||||
if (slash <= 0 || slash >= trimmed.length - 1) {
|
||||
return;
|
||||
}
|
||||
if (normalizeProviderId(trimmed.slice(0, slash)) !== normalizeProviderId(provider)) {
|
||||
return;
|
||||
}
|
||||
refs.push(trimmed);
|
||||
};
|
||||
|
||||
if (typeof rawModel === "string") {
|
||||
addRef(rawModel);
|
||||
return refs;
|
||||
}
|
||||
const model = getRecord(rawModel);
|
||||
if (!model) {
|
||||
return refs;
|
||||
}
|
||||
addRef(model.primary);
|
||||
if (Array.isArray(model.fallbacks)) {
|
||||
for (const fallback of model.fallbacks) {
|
||||
addRef(fallback);
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
function modelEntryWithRuntimePolicy(
|
||||
entry: unknown,
|
||||
runtime: string,
|
||||
): {
|
||||
changed: boolean;
|
||||
entry: Record<string, unknown>;
|
||||
} {
|
||||
const base = getRecord(entry) ? { ...(entry as Record<string, unknown>) } : {};
|
||||
const currentRuntime = getRecord(base.agentRuntime);
|
||||
const currentRuntimeId =
|
||||
typeof currentRuntime?.id === "string" ? currentRuntime.id.trim().toLowerCase() : "";
|
||||
if (currentRuntimeId && currentRuntimeId !== "auto") {
|
||||
return { changed: false, entry: base };
|
||||
}
|
||||
base.agentRuntime = {
|
||||
...currentRuntime,
|
||||
id: runtime,
|
||||
};
|
||||
return { changed: true, entry: base };
|
||||
}
|
||||
|
||||
function preserveLegacyWholeAgentRuntimePolicy(
|
||||
container: Record<string, unknown>,
|
||||
pathLabel: string,
|
||||
changes: string[],
|
||||
): void {
|
||||
const intent = resolveLegacyAgentRuntimeIntent(container.agentRuntime);
|
||||
if (!intent) {
|
||||
return;
|
||||
}
|
||||
const selectedRefs = selectedCanonicalModelRefsForRuntimePolicy(container.model, intent.provider);
|
||||
if (selectedRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentModels = getRecord(container.models);
|
||||
const nextModels: Record<string, unknown> = currentModels ? { ...currentModels } : {};
|
||||
let changed = false;
|
||||
for (const ref of selectedRefs) {
|
||||
const updated = modelEntryWithRuntimePolicy(nextModels[ref], intent.runtime);
|
||||
if (!updated.changed) {
|
||||
continue;
|
||||
}
|
||||
nextModels[ref] = updated.entry;
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
container.models = nextModels;
|
||||
changes.push(
|
||||
`Moved ${pathLabel}.agentRuntime.id ${intent.runtime} to matching ${intent.provider} model runtime policy.`,
|
||||
);
|
||||
}
|
||||
|
||||
function removeIgnoredAgentModelTimeout(
|
||||
model: unknown,
|
||||
pathLabel: string,
|
||||
|
||||
Reference in New Issue
Block a user