Skip to content

Commit 8284c03

Browse files
authored
fix(doctor): clear stale runtime override pins (#84221)
* fix(doctor): clear stale runtime override pins * fix(doctor): register CLI runtime session owners
1 parent ae80adb commit 8284c03

5 files changed

Lines changed: 305 additions & 8 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
1+
import type { DoctorSessionRouteStateOwner } from "openclaw/plugin-sdk/runtime-doctor";
2+
13
export const legacyConfigRules = [];
4+
5+
export const sessionRouteStateOwners: DoctorSessionRouteStateOwner[] = [
6+
{
7+
id: "anthropic",
8+
label: "Anthropic",
9+
providerIds: ["anthropic", "claude-cli"],
10+
runtimeIds: ["claude-cli"],
11+
cliSessionKeys: ["claude-cli"],
12+
authProfilePrefixes: ["anthropic:", "claude-cli:"],
13+
},
14+
];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { DoctorSessionRouteStateOwner } from "openclaw/plugin-sdk/runtime-doctor";
2+
3+
export const sessionRouteStateOwners: DoctorSessionRouteStateOwner[] = [
4+
{
5+
id: "google",
6+
label: "Google",
7+
providerIds: ["google", "google-antigravity", "google-gemini-cli", "google-vertex"],
8+
runtimeIds: ["google-gemini-cli"],
9+
cliSessionKeys: ["google-gemini-cli", "gemini-cli"],
10+
authProfilePrefixes: [
11+
"google:",
12+
"google-antigravity:",
13+
"google-gemini-cli:",
14+
"google-vertex:",
15+
"gemini-cli:",
16+
],
17+
},
18+
];

src/commands/doctor-session-state-providers.test.ts

Lines changed: 199 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ describe("doctor session state provider routes", () => {
4343
},
4444
}),
4545
).toBe(true);
46+
47+
expect(
48+
storeMayContainPluginSessionRouteState({
49+
"agent:main:telegram:direct:2": {
50+
sessionId: "session-claude-cli",
51+
updatedAt: 1,
52+
agentRuntimeOverride: "claude-cli",
53+
},
54+
}),
55+
).toBe(true);
4656
});
4757

4858
it("preserves configured provider CLI runtimes before harness policy normalization", () => {
@@ -135,10 +145,11 @@ describe("doctor session state provider routes", () => {
135145
ownerId: "codex",
136146
ownerLabel: "Codex",
137147
cliSessionKeys: ["codex-cli"],
148+
pinnedRuntimeKeys: ["agentHarnessId"],
138149
reasons: [
139150
"auto model override",
140-
"runtime model state",
141151
"pinned runtime",
152+
"runtime model state",
142153
"CLI session binding",
143154
"auto auth profile override",
144155
],
@@ -206,7 +217,7 @@ describe("doctor session state provider routes", () => {
206217
]);
207218
});
208219

209-
it("keeps owner state when owner remains in the configured route", () => {
220+
it("clears stale runtime pins while preserving configured owner model state", () => {
210221
const sessionKey = "agent:main:telegram:direct:3";
211222
const entry: Record<string, unknown> = {
212223
sessionId: "sess-configured-codex",
@@ -234,7 +245,28 @@ describe("doctor session state provider routes", () => {
234245
},
235246
});
236247

237-
expect(scan).toEqual({ repairs: [], manualReview: [] });
248+
expect(scan.manualReview).toStrictEqual([]);
249+
expect(scan.repairs).toEqual([
250+
{
251+
key: sessionKey,
252+
ownerId: "codex",
253+
ownerLabel: "Codex",
254+
cliSessionKeys: ["codex-cli"],
255+
pinnedRuntimeKeys: ["agentHarnessId"],
256+
reasons: ["pinned runtime"],
257+
},
258+
]);
259+
260+
expect(applySessionRouteStateRepair({ entry, repair: scan.repairs[0], now: 123 })).toBe(true);
261+
expect(entry.updatedAt).toBe(123);
262+
expect(entry.providerOverride).toBe("openai-codex");
263+
expect(entry.modelOverride).toBe("gpt-5.4");
264+
expect(entry.modelProvider).toBe("openai-codex");
265+
expect(entry.model).toBe("gpt-5.4");
266+
expect(entry.agentHarnessId).toBeUndefined();
267+
expect(entry.cliSessionBindings).toStrictEqual({
268+
"codex-cli": { sessionId: "codex-session-3" },
269+
});
238270
});
239271

240272
it("keeps owner CLI state when owner runtime is still configured", () => {
@@ -263,4 +295,168 @@ describe("doctor session state provider routes", () => {
263295

264296
expect(scan).toEqual({ repairs: [], manualReview: [] });
265297
});
298+
299+
it("clears stale agentRuntimeOverride-only pins when current route no longer uses the owner", () => {
300+
const sessionKey = "agent:main:telegram:direct:5";
301+
const entry: Record<string, unknown> = {
302+
sessionId: "sess-stale-claude-cli",
303+
updatedAt: 1,
304+
agentRuntimeOverride: "claude-cli",
305+
};
306+
307+
const scan = scanSessionRouteStateOwners({
308+
owners: [
309+
{
310+
id: "anthropic",
311+
label: "Anthropic",
312+
providerIds: ["anthropic"],
313+
runtimeIds: ["claude-cli"],
314+
cliSessionKeys: ["claude-cli"],
315+
authProfilePrefixes: ["anthropic:", "claude-cli:"],
316+
},
317+
],
318+
store: { [sessionKey]: entry },
319+
routes: {
320+
[sessionKey]: {
321+
defaultProvider: "openai",
322+
configuredModelRefs: ["openai/gpt-5.5"],
323+
runtime: "pi",
324+
},
325+
},
326+
});
327+
328+
expect(scan.manualReview).toStrictEqual([]);
329+
expect(scan.repairs).toEqual([
330+
{
331+
key: sessionKey,
332+
ownerId: "anthropic",
333+
ownerLabel: "Anthropic",
334+
cliSessionKeys: ["claude-cli"],
335+
pinnedRuntimeKeys: ["agentRuntimeOverride"],
336+
reasons: ["pinned runtime"],
337+
},
338+
]);
339+
340+
expect(applySessionRouteStateRepair({ entry, repair: scan.repairs[0], now: 123 })).toBe(true);
341+
expect(entry.sessionId).toBe("sess-stale-claude-cli");
342+
expect(entry.updatedAt).toBe(123);
343+
expect(entry.agentRuntimeOverride).toBeUndefined();
344+
});
345+
346+
it("keeps agentRuntimeOverride pins when owner runtime remains configured", () => {
347+
const sessionKey = "agent:main:telegram:direct:6";
348+
const entry: Record<string, unknown> = {
349+
sessionId: "sess-active-claude-cli",
350+
updatedAt: 1,
351+
agentRuntimeOverride: "claude-cli",
352+
};
353+
354+
const scan = scanSessionRouteStateOwners({
355+
owners: [
356+
{
357+
id: "anthropic",
358+
label: "Anthropic",
359+
providerIds: ["anthropic"],
360+
runtimeIds: ["claude-cli"],
361+
cliSessionKeys: ["claude-cli"],
362+
authProfilePrefixes: ["anthropic:", "claude-cli:"],
363+
},
364+
],
365+
store: { [sessionKey]: entry },
366+
routes: {
367+
[sessionKey]: {
368+
defaultProvider: "anthropic",
369+
configuredModelRefs: ["anthropic/claude-opus-4.7"],
370+
runtime: "claude-cli",
371+
},
372+
},
373+
});
374+
375+
expect(scan).toEqual({ repairs: [], manualReview: [] });
376+
});
377+
378+
it("clears stale owner runtime pins when owner provider remains configured", () => {
379+
const sessionKey = "agent:main:telegram:direct:7";
380+
const entry: Record<string, unknown> = {
381+
sessionId: "sess-provider-active-runtime-stale",
382+
updatedAt: 1,
383+
agentRuntimeOverride: "claude-cli",
384+
};
385+
386+
const scan = scanSessionRouteStateOwners({
387+
owners: [
388+
{
389+
id: "anthropic",
390+
label: "Anthropic",
391+
providerIds: ["anthropic"],
392+
runtimeIds: ["claude-cli"],
393+
cliSessionKeys: ["claude-cli"],
394+
authProfilePrefixes: ["anthropic:", "claude-cli:"],
395+
},
396+
],
397+
store: { [sessionKey]: entry },
398+
routes: {
399+
[sessionKey]: {
400+
defaultProvider: "anthropic",
401+
configuredModelRefs: ["anthropic/claude-opus-4.7"],
402+
runtime: "pi",
403+
},
404+
},
405+
});
406+
407+
expect(scan.manualReview).toStrictEqual([]);
408+
expect(scan.repairs).toEqual([
409+
{
410+
key: sessionKey,
411+
ownerId: "anthropic",
412+
ownerLabel: "Anthropic",
413+
cliSessionKeys: ["claude-cli"],
414+
pinnedRuntimeKeys: ["agentRuntimeOverride"],
415+
reasons: ["pinned runtime"],
416+
},
417+
]);
418+
419+
expect(applySessionRouteStateRepair({ entry, repair: scan.repairs[0], now: 123 })).toBe(true);
420+
expect(entry.updatedAt).toBe(123);
421+
expect(entry.agentRuntimeOverride).toBeUndefined();
422+
});
423+
424+
it("preserves non-owner runtime overrides when clearing owner harness pins", () => {
425+
const sessionKey = "agent:main:telegram:direct:8";
426+
const entry: Record<string, unknown> = {
427+
sessionId: "sess-mixed-runtime-pins",
428+
updatedAt: 1,
429+
agentHarnessId: "codex-cli",
430+
agentRuntimeOverride: "claude-cli",
431+
};
432+
433+
const scan = scanSessionRouteStateOwners({
434+
owners: [codexOwner],
435+
store: { [sessionKey]: entry },
436+
routes: {
437+
[sessionKey]: {
438+
defaultProvider: "openai",
439+
configuredModelRefs: ["openai/gpt-5.5"],
440+
runtime: "pi",
441+
},
442+
},
443+
});
444+
445+
expect(scan.manualReview).toStrictEqual([]);
446+
expect(scan.repairs).toEqual([
447+
{
448+
key: sessionKey,
449+
ownerId: "codex",
450+
ownerLabel: "Codex",
451+
cliSessionKeys: ["codex-cli"],
452+
pinnedRuntimeKeys: ["agentHarnessId"],
453+
reasons: ["pinned runtime"],
454+
},
455+
]);
456+
457+
expect(applySessionRouteStateRepair({ entry, repair: scan.repairs[0], now: 123 })).toBe(true);
458+
expect(entry.updatedAt).toBe(123);
459+
expect(entry.agentHarnessId).toBeUndefined();
460+
expect(entry.agentRuntimeOverride).toBe("claude-cli");
461+
});
266462
});

src/commands/doctor-session-state-providers.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ function entryMayContainPluginSessionRouteState(entry: SessionEntry): boolean {
114114
normalizeString(record.modelProvider) !== undefined ||
115115
normalizeString(record.model) !== undefined ||
116116
normalizeString(record.agentHarnessId) !== undefined ||
117+
normalizeString(record.agentRuntimeOverride) !== undefined ||
117118
record.cliSessionBindings !== undefined ||
118119
record.cliSessionIds !== undefined ||
119120
normalizeString(record.authProfileOverride) !== undefined ||
@@ -138,6 +139,7 @@ export type DoctorSessionRouteStateRepair = {
138139
ownerId: string;
139140
ownerLabel: string;
140141
reasons: string[];
142+
pinnedRuntimeKeys: string[];
141143
cliSessionKeys: string[];
142144
};
143145

@@ -232,7 +234,11 @@ function scanEntryForOwner(params: {
232234
const cliSessionKeys = [...normalizeIdSet(params.owner.cliSessionKeys)];
233235
const authProfilePrefixes = normalizePrefixList(params.owner.authProfilePrefixes);
234236
const routeAllowsOwner = routeAllowsOwnerState({ owner: params.owner, route: params.route });
237+
const routeRuntime = normalizeString(params.route?.runtime);
238+
const routeAllowsOwnerRuntime =
239+
routeRuntime !== undefined && runtimeIds.has(normalizeProviderId(routeRuntime));
235240
const reasons: string[] = [];
241+
const pinnedRuntimeKeys: string[] = [];
236242
const directOverride = resolvePersistedOverrideModelRef({
237243
defaultProvider: params.route?.defaultProvider ?? "",
238244
overrideProvider: params.entry.providerOverride,
@@ -274,6 +280,18 @@ function scanEntryForOwner(params: {
274280

275281
const explicitOwnedOverride =
276282
directOverrideIsOwned && directOverrideSource !== undefined && directOverrideSource !== "auto";
283+
if (!routeAllowsOwnerRuntime && !explicitOwnedOverride) {
284+
const harnessId = normalizeString(params.entry.agentHarnessId);
285+
if (harnessId && runtimeIds.has(normalizeProviderId(harnessId))) {
286+
addReason(reasons, "pinned runtime");
287+
pinnedRuntimeKeys.push("agentHarnessId");
288+
}
289+
const runtimeOverride = normalizeString(params.entry.agentRuntimeOverride);
290+
if (runtimeOverride && runtimeIds.has(normalizeProviderId(runtimeOverride))) {
291+
addReason(reasons, "pinned runtime");
292+
pinnedRuntimeKeys.push("agentRuntimeOverride");
293+
}
294+
}
277295
if (!routeAllowsOwner && !explicitOwnedOverride) {
278296
const runtimeModel = normalizeString(params.entry.model);
279297
const runtimeRef = runtimeModel
@@ -284,10 +302,6 @@ function scanEntryForOwner(params: {
284302
if (runtimeRef && providerIds.has(normalizeProviderId(runtimeRef.provider))) {
285303
addReason(reasons, "runtime model state");
286304
}
287-
const harnessId = normalizeString(params.entry.agentHarnessId);
288-
if (harnessId && runtimeIds.has(normalizeProviderId(harnessId))) {
289-
addReason(reasons, "pinned runtime");
290-
}
291305
if (hasOwnedCliSession({ entry: params.entry, cliSessionKeys })) {
292306
addReason(reasons, "CLI session binding");
293307
}
@@ -308,6 +322,7 @@ function scanEntryForOwner(params: {
308322
ownerId: params.owner.id,
309323
ownerLabel: params.owner.label,
310324
reasons,
325+
pinnedRuntimeKeys,
311326
cliSessionKeys,
312327
},
313328
};
@@ -396,7 +411,9 @@ export function applySessionRouteStateRepair(params: {
396411
clear("fallbackNoticeReason");
397412
}
398413
if (params.repair.reasons.includes("pinned runtime")) {
399-
clear("agentHarnessId");
414+
for (const key of params.repair.pinnedRuntimeKeys) {
415+
clear(key);
416+
}
400417
}
401418
if (params.repair.reasons.includes("CLI session binding")) {
402419
changed =

0 commit comments

Comments
 (0)