mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
TUI: sync /model status immediately
This commit is contained in:
@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Onboarding/Custom providers: raise default custom-provider model context window to the runtime hard minimum (16k) and auto-heal existing custom model entries below that threshold during reconfiguration, preventing immediate `Model context window too small (4096 tokens)` failures. (#21653) Thanks @r4jiv007.
|
||||
- Web UI/Assistant text: strip internal `<relevant-memories>...</relevant-memories>` scaffolding from rendered assistant messages (while preserving code-fence literals), preventing memory-context leakage in chat output for models that echo internal blocks. (#29851) Thanks @Valkster70.
|
||||
- Dashboard/Sessions: allow authenticated Control UI clients to delete and patch sessions while still blocking regular webchat clients from session mutation RPCs, fixing Dashboard session delete failures. (#21264) Thanks @jskoiz.
|
||||
- TUI/Session model status: clear stale runtime model identity when model overrides change so `/model` updates are reflected immediately in `sessions.patch` responses and `sessions.list` status surfaces. (#28619) Thanks @lejean2000.
|
||||
- Podman/Quadlet setup: fix `sed` escaping and UID mismatch in Podman Quadlet setup. (#26414) Thanks @KnHack and @vincentkoc.
|
||||
- Browser/Navigate: resolve the correct `targetId` in navigate responses after renderer swaps. (#25326) Thanks @stone-jin and @vincentkoc.
|
||||
- Agents/Ollama discovery: skip Ollama discovery when explicit models are configured. (#28827) Thanks @Kansodata and @vincentkoc.
|
||||
|
||||
@@ -466,6 +466,9 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
const next = await updateSessionStore(storePath, (store) => {
|
||||
const { primaryKey } = migrateAndPruneSessionStoreKey({ cfg, key, store });
|
||||
const entry = store[primaryKey];
|
||||
const parsed = parseAgentSessionKey(primaryKey);
|
||||
const sessionAgentId = normalizeAgentId(parsed?.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId);
|
||||
oldSessionId = entry?.sessionId;
|
||||
oldSessionFile = entry?.sessionFile;
|
||||
const now = Date.now();
|
||||
@@ -478,8 +481,8 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
responseUsage: entry?.responseUsage,
|
||||
model: entry?.model,
|
||||
modelProvider: entry?.modelProvider,
|
||||
model: resolvedModel.model,
|
||||
modelProvider: resolvedModel.provider,
|
||||
contextTokens: entry?.contextTokens,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
label: entry?.label,
|
||||
|
||||
@@ -447,7 +447,13 @@ describe("gateway server sessions", () => {
|
||||
piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }];
|
||||
const modelPatched = await rpcReq<{
|
||||
ok: true;
|
||||
entry: { modelOverride?: string; providerOverride?: string };
|
||||
entry: {
|
||||
modelOverride?: string;
|
||||
providerOverride?: string;
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
};
|
||||
resolved?: { model?: string; modelProvider?: string };
|
||||
}>(ws, "sessions.patch", {
|
||||
key: "agent:main:main",
|
||||
model: "openai/gpt-test-a",
|
||||
@@ -455,6 +461,20 @@ describe("gateway server sessions", () => {
|
||||
expect(modelPatched.ok).toBe(true);
|
||||
expect(modelPatched.payload?.entry.modelOverride).toBe("gpt-test-a");
|
||||
expect(modelPatched.payload?.entry.providerOverride).toBe("openai");
|
||||
expect(modelPatched.payload?.entry.model).toBeUndefined();
|
||||
expect(modelPatched.payload?.entry.modelProvider).toBeUndefined();
|
||||
expect(modelPatched.payload?.resolved?.modelProvider).toBe("openai");
|
||||
expect(modelPatched.payload?.resolved?.model).toBe("gpt-test-a");
|
||||
|
||||
const listAfterModelPatch = await rpcReq<{
|
||||
sessions: Array<{ key: string; modelProvider?: string; model?: string }>;
|
||||
}>(ws, "sessions.list", {});
|
||||
expect(listAfterModelPatch.ok).toBe(true);
|
||||
const mainAfterModelPatch = listAfterModelPatch.payload?.sessions.find(
|
||||
(session) => session.key === "agent:main:main",
|
||||
);
|
||||
expect(mainAfterModelPatch?.modelProvider).toBe("openai");
|
||||
expect(mainAfterModelPatch?.model).toBe("gpt-test-a");
|
||||
|
||||
const compacted = await rpcReq<{ ok: true; compacted: boolean }>(ws, "sessions.compact", {
|
||||
key: "agent:main:main",
|
||||
@@ -492,8 +512,8 @@ describe("gateway server sessions", () => {
|
||||
expect(reset.ok).toBe(true);
|
||||
expect(reset.payload?.key).toBe("agent:main:main");
|
||||
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
||||
expect(reset.payload?.entry.modelProvider).toBe("anthropic");
|
||||
expect(reset.payload?.entry.model).toBe("claude-sonnet-4-6");
|
||||
expect(reset.payload?.entry.modelProvider).toBe("openai");
|
||||
expect(reset.payload?.entry.model).toBe("gpt-test-a");
|
||||
const filesAfterReset = await fs.readdir(dir);
|
||||
expect(filesAfterReset.some((f) => f.startsWith("sess-main.jsonl.reset."))).toBe(true);
|
||||
|
||||
|
||||
90
src/sessions/model-overrides.test.ts
Normal file
90
src/sessions/model-overrides.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { applyModelOverrideToSessionEntry } from "./model-overrides.js";
|
||||
|
||||
describe("applyModelOverrideToSessionEntry", () => {
|
||||
it("clears stale runtime model fields when switching overrides", () => {
|
||||
const before = Date.now() - 5_000;
|
||||
const entry: SessionEntry = {
|
||||
sessionId: "sess-1",
|
||||
updatedAt: before,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-sonnet-4-6",
|
||||
fallbackNoticeSelectedModel: "anthropic/claude-sonnet-4-6",
|
||||
fallbackNoticeActiveModel: "anthropic/claude-sonnet-4-6",
|
||||
fallbackNoticeReason: "provider temporary failure",
|
||||
};
|
||||
|
||||
const result = applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: "openai",
|
||||
model: "gpt-5.2",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.updated).toBe(true);
|
||||
expect(entry.providerOverride).toBe("openai");
|
||||
expect(entry.modelOverride).toBe("gpt-5.2");
|
||||
expect(entry.modelProvider).toBeUndefined();
|
||||
expect(entry.model).toBeUndefined();
|
||||
expect(entry.fallbackNoticeSelectedModel).toBeUndefined();
|
||||
expect(entry.fallbackNoticeActiveModel).toBeUndefined();
|
||||
expect(entry.fallbackNoticeReason).toBeUndefined();
|
||||
expect((entry.updatedAt ?? 0) > before).toBe(true);
|
||||
});
|
||||
|
||||
it("clears stale runtime model fields even when override selection is unchanged", () => {
|
||||
const before = Date.now() - 5_000;
|
||||
const entry: SessionEntry = {
|
||||
sessionId: "sess-2",
|
||||
updatedAt: before,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5.2",
|
||||
};
|
||||
|
||||
const result = applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: "openai",
|
||||
model: "gpt-5.2",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.updated).toBe(true);
|
||||
expect(entry.providerOverride).toBe("openai");
|
||||
expect(entry.modelOverride).toBe("gpt-5.2");
|
||||
expect(entry.modelProvider).toBeUndefined();
|
||||
expect(entry.model).toBeUndefined();
|
||||
expect((entry.updatedAt ?? 0) > before).toBe(true);
|
||||
});
|
||||
|
||||
it("retains aligned runtime model fields when selection and runtime already match", () => {
|
||||
const before = Date.now() - 5_000;
|
||||
const entry: SessionEntry = {
|
||||
sessionId: "sess-3",
|
||||
updatedAt: before,
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.2",
|
||||
providerOverride: "openai",
|
||||
modelOverride: "gpt-5.2",
|
||||
};
|
||||
|
||||
const result = applyModelOverrideToSessionEntry({
|
||||
entry,
|
||||
selection: {
|
||||
provider: "openai",
|
||||
model: "gpt-5.2",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.updated).toBe(false);
|
||||
expect(entry.modelProvider).toBe("openai");
|
||||
expect(entry.model).toBe("gpt-5.2");
|
||||
expect(entry.updatedAt).toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -15,24 +15,49 @@ export function applyModelOverrideToSessionEntry(params: {
|
||||
const { entry, selection, profileOverride } = params;
|
||||
const profileOverrideSource = params.profileOverrideSource ?? "user";
|
||||
let updated = false;
|
||||
let selectionUpdated = false;
|
||||
|
||||
if (selection.isDefault) {
|
||||
if (entry.providerOverride) {
|
||||
delete entry.providerOverride;
|
||||
updated = true;
|
||||
selectionUpdated = true;
|
||||
}
|
||||
if (entry.modelOverride) {
|
||||
delete entry.modelOverride;
|
||||
updated = true;
|
||||
selectionUpdated = true;
|
||||
}
|
||||
} else {
|
||||
if (entry.providerOverride !== selection.provider) {
|
||||
entry.providerOverride = selection.provider;
|
||||
updated = true;
|
||||
selectionUpdated = true;
|
||||
}
|
||||
if (entry.modelOverride !== selection.model) {
|
||||
entry.modelOverride = selection.model;
|
||||
updated = true;
|
||||
selectionUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Model overrides supersede previously recorded runtime model identity.
|
||||
// If runtime fields are stale (or the override changed), clear them so status
|
||||
// surfaces reflect the selected model immediately.
|
||||
const runtimeModel = typeof entry.model === "string" ? entry.model.trim() : "";
|
||||
const runtimeProvider = typeof entry.modelProvider === "string" ? entry.modelProvider.trim() : "";
|
||||
const runtimePresent = runtimeModel.length > 0 || runtimeProvider.length > 0;
|
||||
const runtimeAligned =
|
||||
runtimeModel === selection.model &&
|
||||
(runtimeProvider.length === 0 || runtimeProvider === selection.provider);
|
||||
if (runtimePresent && (selectionUpdated || !runtimeAligned)) {
|
||||
if (entry.model !== undefined) {
|
||||
delete entry.model;
|
||||
updated = true;
|
||||
}
|
||||
if (entry.modelProvider !== undefined) {
|
||||
delete entry.modelProvider;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user