Skip to content

Commit a134683

Browse files
authored
fix: clarify pinned session model status
1 parent c8a953a commit a134683

16 files changed

Lines changed: 406 additions & 3 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616

1717
### Fixes
1818

19+
- Status: show the configured default, session-selected model, reason, clear hint, and docs link when a session remains pinned to a model that differs from `agents.defaults.model.primary`.
1920
- Mac app: keep local packaging signed with a stable app identity for permission testing and fix Control UI production builds under current Vite/Highlight.js exports.
2021
- macOS app: update the embedded Peekaboo bridge to 3.2.1 so OpenClaw-hosted UI automation works with current Peekaboo CLI capture flows.
2122
- Cron: deliver preferred final assistant output for successful scheduled runs when trailing plain tool warnings remain in diagnostics instead of marking the run failed.

docs/cli/status.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Notes:
2626
- When the current session snapshot is sparse, `/status` can backfill token and cache counters from the most recent transcript usage log. Existing nonzero live values still win over transcript fallback values.
2727
- `/status` includes compact Gateway process uptime and host system uptime.
2828
- Transcript fallback can also recover the active runtime model label when the live session entry is missing it. If that transcript model differs from the selected model, status resolves the context window against the recovered runtime model instead of the selected one.
29+
- When a session is pinned to a model that differs from the configured primary, status prints both values, the reason (`session override`), and the clear hint (`/model <configured-default>` or `/reset`). The configured primary applies to new or unpinned sessions; existing pinned sessions keep their session selection until cleared.
2930
- For prompt-size accounting, transcript fallback prefers the larger prompt-oriented total when session metadata is missing or smaller, so custom-provider sessions do not collapse to `0` token displays.
3031
- Output includes per-agent session stores when multiple agents are configured.
3132
- Overview includes Gateway + node host service install/runtime status when available.

docs/concepts/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ The same `provider/model` can mean different things depending on where it came f
6161
- Configured defaults (`agents.defaults.model.primary` and agent-specific primaries) are the normal starting point and use `agents.defaults.model.fallbacks`.
6262
- Auto fallback selections are temporary recovery state. They are stored with `modelOverrideSource: "auto"` so later turns can keep using the fallback chain without probing a known-bad primary every time; OpenClaw periodically probes the original primary again, clears the auto selection when it recovers, and announces fallback/recovery transitions once per state change.
6363
- User session selections are exact. `/model`, the model picker, `session_status(model=...)`, and `sessions.patch` store `modelOverrideSource: "user"`; if that selected provider/model is unreachable, OpenClaw fails visibly instead of falling through to another configured model.
64+
- Changing `agents.defaults.model.primary` does not rewrite existing session selections. If status says `This session is pinned to X; config primary Y will apply to new/unpinned sessions.`, switch the current session with `/model Y` or clear stale session state with `/reset`.
6465
- Cron `--model` / payload `model` is a per-job primary. It still uses configured fallbacks unless the job supplies explicit payload `fallbacks` (use `fallbacks: []` for a strict cron run).
6566
- CLI default-model and allowlist pickers respect `models.mode: "replace"` by listing explicit `models.providers.*.models` instead of loading the full built-in catalog.
6667
- The Control UI model picker asks the Gateway for its configured model view: `agents.defaults.models` when present, including provider-wide `provider/*` entries, otherwise explicit `models.providers.*.models` plus providers with usable auth. The full built-in catalog is reserved for explicit browse views such as `models.list` with `view: "all"` or `openclaw models list --all`.

src/auto-reply/status.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,89 @@ describe("buildStatusMessage", () => {
13471347
expect(normalizeTestText(text)).toContain("Model: google-antigravity/claude-sonnet-4-6");
13481348
});
13491349

1350+
it("warns when the session-selected model differs from the configured default", () => {
1351+
const text = buildStatusMessage({
1352+
agent: {
1353+
model: "zhipu/glm-4.5-air",
1354+
},
1355+
configuredDefaultModelLabel: "zhipu/glm-4.5-air",
1356+
sessionEntry: {
1357+
sessionId: "pinned-session",
1358+
updatedAt: 0,
1359+
providerOverride: "deepseek",
1360+
modelOverride: "deepseek-v4-flash",
1361+
modelOverrideSource: "user",
1362+
},
1363+
sessionKey: "agent:main:main",
1364+
sessionScope: "per-sender",
1365+
queue: { mode: "collect", depth: 0 },
1366+
modelAuth: "api-key",
1367+
});
1368+
1369+
const normalized = normalizeTestText(text);
1370+
expect(normalized).toContain("Configured default: zhipu/glm-4.5-air");
1371+
expect(normalized).toContain("Session selected: deepseek/deepseek-v4-flash");
1372+
expect(normalized).toContain("Reason: session override");
1373+
expect(normalized).toContain(
1374+
"This session is pinned to deepseek/deepseek-v4-flash; config primary zhipu/glm-4.5-air will apply to new/unpinned sessions.",
1375+
);
1376+
expect(normalized).toContain("Clear with: /model zhipu/glm-4.5-air or /reset");
1377+
expect(normalized).toContain(
1378+
"Docs: https://docs.openclaw.ai/concepts/models#selection-source-and-fallback-behavior",
1379+
);
1380+
});
1381+
1382+
it("does not warn when only the last runtime model differs from the configured default", () => {
1383+
const text = buildStatusMessage({
1384+
agent: {
1385+
model: "zhipu/glm-4.5-air",
1386+
},
1387+
configuredDefaultModelLabel: "zhipu/glm-4.5-air",
1388+
sessionEntry: {
1389+
sessionId: "runtime-snapshot-only",
1390+
updatedAt: 0,
1391+
modelProvider: "deepseek",
1392+
model: "deepseek-v4-flash",
1393+
},
1394+
sessionKey: "agent:main:main",
1395+
sessionScope: "per-sender",
1396+
queue: { mode: "collect", depth: 0 },
1397+
modelAuth: "api-key",
1398+
});
1399+
1400+
const normalized = normalizeTestText(text);
1401+
expect(normalized).toContain("Model: zhipu/glm-4.5-air");
1402+
expect(normalized).not.toContain("Configured default:");
1403+
expect(normalized).not.toContain("Reason: session override");
1404+
});
1405+
1406+
it("does not label auto fallback model overrides as pinned selections", () => {
1407+
const text = buildStatusMessage({
1408+
agent: {
1409+
model: "zhipu/glm-4.5-air",
1410+
},
1411+
configuredDefaultModelLabel: "zhipu/glm-4.5-air",
1412+
sessionEntry: {
1413+
sessionId: "auto-fallback",
1414+
updatedAt: 0,
1415+
providerOverride: "deepseek",
1416+
modelOverride: "deepseek-v4-flash",
1417+
modelOverrideSource: "auto",
1418+
modelOverrideFallbackOriginProvider: "zhipu",
1419+
modelOverrideFallbackOriginModel: "glm-4.5-air",
1420+
},
1421+
sessionKey: "agent:main:main",
1422+
sessionScope: "per-sender",
1423+
queue: { mode: "collect", depth: 0 },
1424+
modelAuth: "api-key",
1425+
});
1426+
1427+
const normalized = normalizeTestText(text);
1428+
expect(normalized).toContain("Model: deepseek/deepseek-v4-flash");
1429+
expect(normalized).not.toContain("Configured default:");
1430+
expect(normalized).not.toContain("Reason: session override");
1431+
});
1432+
13501433
it("handles missing agent config gracefully", () => {
13511434
const text = buildStatusMessage({
13521435
agent: {},

src/commands/status.command-report-data.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ describe("buildStatusCommandReportData", () => {
3939
);
4040
expect(result.pluginCompatibilityLines).toEqual([" warn(WARN) legacy"]);
4141
expect(result.pairingRecoveryLines[0]).toBe("warn(Gateway pairing approval required.)");
42+
expect(result.modelSelectionLines).toEqual([]);
4243
expect(result.channelsRows[0]?.Channel).toBe("QuietChat");
4344
expect(result.sessionsRows[0]?.Cache).toBe("cache ok");
4445
expect(result.healthRows?.[0]).toEqual({
@@ -94,4 +95,30 @@ describe("buildStatusCommandReportData", () => {
9495
});
9596
expect(result.overviewRows[modelPricingIndex + 1]?.Item).toBe("Memory");
9697
});
98+
99+
it("adds pinned-session model selection lines", async () => {
100+
const baseParams = createStatusCommandReportDataParams();
101+
const result = await buildStatusCommandReportData(
102+
createStatusCommandReportDataParams({
103+
summary: {
104+
...baseParams.summary,
105+
sessions: {
106+
...baseParams.summary.sessions,
107+
recent: [
108+
{
109+
...baseParams.summary.sessions.recent[0],
110+
configuredModel: "zhipu/glm-4.5-air",
111+
selectedModel: "deepseek/deepseek-v4-flash",
112+
modelSelectionReason: "session override",
113+
},
114+
],
115+
},
116+
},
117+
}),
118+
);
119+
120+
expect(result.modelSelectionLines).toContain(" Configured default: zhipu/glm-4.5-air");
121+
expect(result.modelSelectionLines).toContain(" Session selected: deepseek/deepseek-v4-flash");
122+
expect(result.modelSelectionLines).toContain(" Reason: session override");
123+
});
97124
});

src/commands/status.command-report-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { AgentLocalStatus } from "./status.agent-local.js";
1515
import {
1616
buildStatusFooterLines,
1717
buildStatusHealthRows,
18+
buildStatusModelSelectionLines,
1819
buildStatusPairingRecoveryLines,
1920
buildStatusPluginCompatibilityLines,
2021
buildStatusSecurityAuditLines,
@@ -158,6 +159,12 @@ export async function buildStatusCommandReportData(
158159
muted: params.theme.muted,
159160
formatCliCommand: params.formatCliCommand,
160161
}),
162+
modelSelectionLines: buildStatusModelSelectionLines({
163+
recent: params.summary.sessions.recent,
164+
shortenText: params.shortenText,
165+
warn: params.theme.warn,
166+
muted: params.theme.muted,
167+
}),
161168
securityAuditLines,
162169
channelsColumns: statusChannelsTableColumns,
163170
channelsRows: buildStatusChannelsTableRows({

src/commands/status.command-report.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe("buildStatusCommandReportLines", () => {
1818
taskMaintenanceHint: "maintenance hint",
1919
pluginCompatibilityLines: ["warn 1"],
2020
pairingRecoveryLines: ["pairing needed"],
21+
modelSelectionLines: ["model warning"],
2122
securityAuditLines: ["audit line"],
2223
channelsColumns: [{ key: "Channel", header: "Channel" }],
2324
channelsRows: [{ Channel: "quietchat" }],
@@ -44,6 +45,9 @@ describe("buildStatusCommandReportLines", () => {
4445
"",
4546
"pairing needed",
4647
"",
48+
"# Model selection",
49+
"model warning",
50+
"",
4751
"# Security audit",
4852
"audit line",
4953
"",
@@ -79,6 +83,7 @@ describe("buildStatusCommandReportLines", () => {
7983
taskMaintenanceHint: "ignored",
8084
pluginCompatibilityLines: [],
8185
pairingRecoveryLines: [],
86+
modelSelectionLines: [],
8287
securityAuditLines: ["audit line"],
8388
channelsColumns: [{ key: "Channel", header: "Channel" }],
8489
channelsRows: [{ Channel: "quietchat" }],
@@ -105,6 +110,7 @@ describe("buildStatusCommandReportLines", () => {
105110
taskMaintenanceHint: "ignored",
106111
pluginCompatibilityLines: [],
107112
pairingRecoveryLines: [],
113+
modelSelectionLines: [],
108114
securityAuditLines: ["audit line"],
109115
channelsColumns: [{ key: "Channel", header: "Channel" }],
110116
channelsRows: [],

src/commands/status.command-report.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export async function buildStatusCommandReportLines(params: {
1919
taskMaintenanceHint: string;
2020
pluginCompatibilityLines: string[];
2121
pairingRecoveryLines: string[];
22+
modelSelectionLines: string[];
2223
securityAuditLines: string[];
2324
channelsColumns: readonly TableColumn[];
2425
channelsRows: Array<Record<string, string>>;
@@ -61,6 +62,12 @@ export async function buildStatusCommandReportLines(params: {
6162
body: params.pairingRecoveryLines.length > 0 ? ["", ...params.pairingRecoveryLines] : [],
6263
skipIfEmpty: true,
6364
},
65+
{
66+
kind: "lines",
67+
title: "Model selection",
68+
body: params.modelSelectionLines,
69+
skipIfEmpty: true,
70+
},
6471
{
6572
kind: "lines",
6673
title: "Security audit",

src/commands/status.command-sections.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { HealthSummary } from "./health.js";
33
import {
44
buildStatusFooterLines,
55
buildStatusHealthRows,
6+
buildStatusModelSelectionLines,
67
buildStatusPairingRecoveryLines,
78
buildStatusPluginCompatibilityLines,
89
buildStatusSecurityAuditLines,
@@ -69,6 +70,9 @@ describe("status.command-sections", () => {
6970
remainingTokens: null,
7071
percentUsed: null,
7172
contextTokens: null,
73+
configuredModel: "openai/gpt-5.4",
74+
selectedModel: "openai/gpt-5.4",
75+
modelSelectionReason: null,
7276
flags: [],
7377
},
7478
{
@@ -83,6 +87,9 @@ describe("status.command-sections", () => {
8387
remainingTokens: null,
8488
percentUsed: null,
8589
contextTokens: null,
90+
configuredModel: "openai/gpt-5.5",
91+
selectedModel: "openai/gpt-5.5",
92+
modelSelectionReason: null,
8693
flags: [],
8794
},
8895
],
@@ -128,6 +135,42 @@ describe("status.command-sections", () => {
128135
expect(emptyRows).toEqual([]);
129136
});
130137

138+
it("shows configured default and selected session model when they differ", () => {
139+
const lines = buildStatusModelSelectionLines({
140+
recent: [
141+
{
142+
key: "agent:main:telegram:chat-1",
143+
kind: "direct",
144+
updatedAt: 1,
145+
age: 5_000,
146+
model: "deepseek-v4-flash",
147+
configuredModel: "zhipu/glm-4.5-air",
148+
selectedModel: "deepseek/deepseek-v4-flash",
149+
modelSelectionReason: "session override",
150+
runtime: "OpenClaw Pi Default",
151+
totalTokens: null,
152+
totalTokensFresh: false,
153+
remainingTokens: null,
154+
percentUsed: null,
155+
contextTokens: null,
156+
flags: [],
157+
},
158+
],
159+
shortenText: (value) => value,
160+
warn: (value) => `warn(${value})`,
161+
muted: (value) => `muted(${value})`,
162+
});
163+
164+
expect(lines).toEqual([
165+
"warn(Session agent:main:telegram:chat-1 is pinned to deepseek/deepseek-v4-flash; config primary zhipu/glm-4.5-air will apply to new/unpinned sessions.)",
166+
" Configured default: zhipu/glm-4.5-air",
167+
" Session selected: deepseek/deepseek-v4-flash",
168+
" Reason: session override",
169+
" Clear with: /model zhipu/glm-4.5-air or /reset",
170+
" Docs: https://docs.openclaw.ai/concepts/models#selection-source-and-fallback-behavior",
171+
]);
172+
});
173+
131174
it("maps health channel detail lines into status rows", () => {
132175
const rows = buildStatusHealthRows({
133176
health: { durationMs: 42 } as HealthSummary,

src/commands/status.command-sections.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
12
import {
23
buildPairingConnectRecoveryTitle,
34
describePairingConnectRequirement,
@@ -344,6 +345,49 @@ export function buildStatusSessionsRows(params: {
344345
}));
345346
}
346347

348+
export function buildStatusModelSelectionLines(params: {
349+
recent: SessionsRecentLike[];
350+
limit?: number;
351+
shortenText: (value: string, maxLen: number) => string;
352+
warn: (value: string) => string;
353+
muted: (value: string) => string;
354+
}) {
355+
const mismatches = params.recent.filter((sess) => {
356+
if (!sess.configuredModel || !sess.selectedModel || !sess.modelSelectionReason) {
357+
return false;
358+
}
359+
return (
360+
sess.configuredModel !== sess.selectedModel &&
361+
!areRuntimeModelRefsEquivalent(sess.configuredModel, sess.selectedModel)
362+
);
363+
});
364+
if (mismatches.length === 0) {
365+
return [];
366+
}
367+
368+
const limit = params.limit ?? 3;
369+
const lines: string[] = [];
370+
for (const sess of mismatches.slice(0, limit)) {
371+
const key = params.shortenText(sess.key, 48);
372+
const configured = sess.configuredModel ?? "unknown";
373+
const selected = sess.selectedModel ?? "unknown";
374+
lines.push(
375+
params.warn(
376+
`Session ${key} is pinned to ${selected}; config primary ${configured} will apply to new/unpinned sessions.`,
377+
),
378+
` Configured default: ${configured}`,
379+
` Session selected: ${selected}`,
380+
` Reason: ${sess.modelSelectionReason ?? "session override"}`,
381+
` Clear with: /model ${configured} or /reset`,
382+
" Docs: https://docs.openclaw.ai/concepts/models#selection-source-and-fallback-behavior",
383+
);
384+
}
385+
if (mismatches.length > limit) {
386+
lines.push(params.muted(` … +${mismatches.length - limit} more pinned session(s)`));
387+
}
388+
return lines;
389+
}
390+
347391
export function buildStatusFooterLines(params: {
348392
updateHint: string | null;
349393
warn: (value: string) => string;

0 commit comments

Comments
 (0)