Skip to content

Commit b17bb63

Browse files
committed
fix: repair stale session route state in doctor
1 parent e2e0908 commit b17bb63

10 files changed

Lines changed: 894 additions & 1 deletion

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Docs: https://docs.openclaw.ai
7979
- Mattermost: clarify that the model picker only changes the session model and that runtime switches require `/oc_model <provider/model> --runtime <runtime>`. Thanks @vincentkoc.
8080
- Doctor/config: keep active `auth.profiles` metadata intact when `doctor --fix` strips stale secret fields from configs, repairing legacy `<provider>:default` API-key profile metadata when model fallbacks or explicit `model@profile` refs still depend on it. Fixes #77400.
8181
- Doctor/plugins: include `plugins.allow`-only official plugin ids in the release configured-plugin repair set, so `doctor --fix` installs official external plugins that are configured but not yet loaded instead of removing them as stale allow entries. Fixes #77155. Thanks @hclsys.
82+
- Doctor/sessions: clear auto-created stale session routing state from the sessions store when `doctor --fix` sees plugin-owned model/runtime/auth/session bindings outside the current configured route, while leaving explicit user model choices for manual review. Refs #68615.
8283
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
8384
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
8485
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.

docs/gateway/doctor.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,12 @@ That stages grounded durable candidates into the short-term dreaming store while
271271

272272
If the warning appears, choose the route you intended and edit config manually. Keep the warning as-is when PI Codex OAuth is intentional.
273273

274+
</Accordion>
275+
<Accordion title="2g. Session route cleanup">
276+
Doctor also scans the active sessions store for stale auto-created route state after you move the configured default/fallback model or runtime away from a plugin-owned route such as Codex.
277+
278+
`openclaw doctor --fix` can clear auto-created stale state such as `modelOverrideSource: "auto"` model pins, runtime model metadata, pinned harness ids, CLI session bindings, and auto auth-profile overrides when their owning route is no longer configured. Explicit user or legacy session model choices are reported for manual review and left untouched; switch them with `/model ...`, `/new`, or reset the session when that route is no longer intended.
279+
274280
</Accordion>
275281
<Accordion title="3. Legacy state migrations (disk layout)">
276282
Doctor can migrate older on-disk layouts into the current structure:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { DoctorSessionRouteStateOwner } from "openclaw/plugin-sdk/runtime-doctor";
2+
3+
export const sessionRouteStateOwners: DoctorSessionRouteStateOwner[] = [
4+
{
5+
id: "codex",
6+
label: "Codex",
7+
providerIds: ["codex", "codex-cli", "openai-codex"],
8+
runtimeIds: ["codex", "codex-cli"],
9+
cliSessionKeys: ["codex-cli"],
10+
authProfilePrefixes: ["codex:", "codex-cli:", "openai-codex:"],
11+
},
12+
];
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
applySessionRouteStateRepair,
4+
resolveConfiguredDoctorSessionStateRoute,
5+
scanSessionRouteStateOwners,
6+
} from "./doctor-session-state-providers.js";
7+
8+
const codexOwner = {
9+
id: "codex",
10+
label: "Codex",
11+
providerIds: ["codex", "codex-cli", "openai-codex"],
12+
runtimeIds: ["codex", "codex-cli"],
13+
cliSessionKeys: ["codex-cli"],
14+
authProfilePrefixes: ["codex:", "codex-cli:", "openai-codex:"],
15+
};
16+
17+
describe("doctor session state provider routes", () => {
18+
it("preserves raw configured CLI runtimes before harness policy normalization", () => {
19+
expect(
20+
resolveConfiguredDoctorSessionStateRoute({
21+
cfg: {
22+
agents: {
23+
defaults: {
24+
model: { primary: "openai/gpt-5.5" },
25+
agentRuntime: { id: "codex-cli" },
26+
},
27+
},
28+
},
29+
sessionKey: "agent:main:telegram:direct:1",
30+
env: {},
31+
}),
32+
).toMatchObject({
33+
defaultProvider: "openai",
34+
configuredModelRefs: ["openai/gpt-5.5"],
35+
runtime: "codex-cli",
36+
});
37+
});
38+
39+
it("lets environment CLI runtime overrides reach plugin-owned scanners", () => {
40+
expect(
41+
resolveConfiguredDoctorSessionStateRoute({
42+
cfg: {
43+
agents: {
44+
defaults: {
45+
model: { primary: "openai/gpt-5.5" },
46+
agentRuntime: { id: "pi" },
47+
},
48+
},
49+
},
50+
sessionKey: "agent:main:telegram:direct:1",
51+
env: { OPENCLAW_AGENT_RUNTIME: "codex-cli" },
52+
}),
53+
).toMatchObject({
54+
runtime: "codex-cli",
55+
});
56+
});
57+
58+
it("clears auto-created route state when current route no longer uses the owner", () => {
59+
const sessionKey = "agent:main:telegram:direct:1";
60+
const entry: Record<string, unknown> = {
61+
sessionId: "sess-stale-codex",
62+
updatedAt: 1,
63+
providerOverride: "openai-codex",
64+
modelOverride: "gpt-5.4",
65+
modelOverrideSource: "auto",
66+
modelProvider: "openai-codex",
67+
model: "gpt-5.4",
68+
contextTokens: 1_050_000,
69+
systemPromptReport: { source: "run" },
70+
fallbackNoticeSelectedModel: "github-copilot/gpt-5-mini",
71+
fallbackNoticeActiveModel: "openai-codex/gpt-5.4",
72+
fallbackNoticeReason: "rate-limit",
73+
agentHarnessId: "codex",
74+
authProfileOverride: "openai-codex:default",
75+
authProfileOverrideSource: "auto",
76+
authProfileOverrideCompactionCount: 2,
77+
cliSessionBindings: {
78+
"codex-cli": { sessionId: "codex-session-1" },
79+
"claude-cli": { sessionId: "claude-session-1" },
80+
},
81+
cliSessionIds: {
82+
"codex-cli": "codex-session-1",
83+
"claude-cli": "claude-session-1",
84+
},
85+
};
86+
87+
const scan = scanSessionRouteStateOwners({
88+
owners: [codexOwner],
89+
store: { [sessionKey]: entry },
90+
routes: {
91+
[sessionKey]: {
92+
defaultProvider: "github-copilot",
93+
configuredModelRefs: ["github-copilot/gpt-5-mini"],
94+
runtime: "pi",
95+
},
96+
},
97+
});
98+
99+
expect(scan.manualReview).toEqual([]);
100+
expect(scan.repairs).toEqual([
101+
{
102+
key: sessionKey,
103+
ownerId: "codex",
104+
ownerLabel: "Codex",
105+
cliSessionKeys: ["codex-cli"],
106+
reasons: [
107+
"auto model override",
108+
"runtime model state",
109+
"pinned runtime",
110+
"CLI session binding",
111+
"auto auth profile override",
112+
],
113+
},
114+
]);
115+
116+
expect(applySessionRouteStateRepair({ entry, repair: scan.repairs[0], now: 123 })).toBe(true);
117+
expect(entry).toMatchObject({
118+
sessionId: "sess-stale-codex",
119+
updatedAt: 123,
120+
cliSessionBindings: {
121+
"claude-cli": { sessionId: "claude-session-1" },
122+
},
123+
cliSessionIds: {
124+
"claude-cli": "claude-session-1",
125+
},
126+
});
127+
expect(entry.providerOverride).toBeUndefined();
128+
expect(entry.modelOverride).toBeUndefined();
129+
expect(entry.modelOverrideSource).toBeUndefined();
130+
expect(entry.modelProvider).toBeUndefined();
131+
expect(entry.model).toBeUndefined();
132+
expect(entry.contextTokens).toBeUndefined();
133+
expect(entry.systemPromptReport).toBeUndefined();
134+
expect(entry.agentHarnessId).toBeUndefined();
135+
expect(entry.authProfileOverride).toBeUndefined();
136+
expect(entry.authProfileOverrideSource).toBeUndefined();
137+
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
138+
expect(entry.fallbackNoticeActiveModel).toBeUndefined();
139+
});
140+
141+
it("leaves explicit user owner model choices for manual review", () => {
142+
const sessionKey = "agent:main:telegram:direct:2";
143+
const entry: Record<string, unknown> = {
144+
sessionId: "sess-user-codex",
145+
updatedAt: 1,
146+
providerOverride: "openai-codex",
147+
modelOverride: "gpt-5.4",
148+
modelOverrideSource: "user",
149+
modelProvider: "openai-codex",
150+
model: "gpt-5.4",
151+
agentHarnessId: "codex",
152+
cliSessionBindings: {
153+
"codex-cli": { sessionId: "codex-session-2" },
154+
},
155+
};
156+
157+
const scan = scanSessionRouteStateOwners({
158+
owners: [codexOwner],
159+
store: { [sessionKey]: entry },
160+
routes: {
161+
[sessionKey]: {
162+
defaultProvider: "github-copilot",
163+
configuredModelRefs: ["github-copilot/gpt-5-mini"],
164+
runtime: "pi",
165+
},
166+
},
167+
});
168+
169+
expect(scan.repairs).toEqual([]);
170+
expect(scan.manualReview).toEqual([
171+
{
172+
key: sessionKey,
173+
ownerLabel: "Codex",
174+
message: `${sessionKey} (openai-codex/gpt-5.4, user)`,
175+
},
176+
]);
177+
});
178+
179+
it("keeps owner state when owner remains in the configured route", () => {
180+
const sessionKey = "agent:main:telegram:direct:3";
181+
const entry: Record<string, unknown> = {
182+
sessionId: "sess-configured-codex",
183+
updatedAt: 1,
184+
providerOverride: "openai-codex",
185+
modelOverride: "gpt-5.4",
186+
modelOverrideSource: "auto",
187+
modelProvider: "openai-codex",
188+
model: "gpt-5.4",
189+
agentHarnessId: "codex",
190+
cliSessionBindings: {
191+
"codex-cli": { sessionId: "codex-session-3" },
192+
},
193+
};
194+
195+
const scan = scanSessionRouteStateOwners({
196+
owners: [codexOwner],
197+
store: { [sessionKey]: entry },
198+
routes: {
199+
[sessionKey]: {
200+
defaultProvider: "github-copilot",
201+
configuredModelRefs: ["github-copilot/gpt-5-mini", "openai-codex/gpt-5.4"],
202+
runtime: "pi",
203+
},
204+
},
205+
});
206+
207+
expect(scan).toEqual({ repairs: [], manualReview: [] });
208+
});
209+
210+
it("keeps owner CLI state when owner runtime is still configured", () => {
211+
const sessionKey = "agent:main:telegram:direct:4";
212+
const entry: Record<string, unknown> = {
213+
sessionId: "sess-codex-cli",
214+
updatedAt: 1,
215+
modelProvider: "codex-cli",
216+
model: "gpt-5.5",
217+
cliSessionBindings: {
218+
"codex-cli": { sessionId: "codex-cli-session" },
219+
},
220+
};
221+
222+
const scan = scanSessionRouteStateOwners({
223+
owners: [codexOwner],
224+
store: { [sessionKey]: entry },
225+
routes: {
226+
[sessionKey]: {
227+
defaultProvider: "openai",
228+
configuredModelRefs: ["openai/gpt-5.5"],
229+
runtime: "codex-cli",
230+
},
231+
},
232+
});
233+
234+
expect(scan).toEqual({ repairs: [], manualReview: [] });
235+
});
236+
});

0 commit comments

Comments
 (0)