From 96911d7790add35799dd8bbc5fffc496b4d384dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 27 Dec 2025 01:17:03 +0000 Subject: [PATCH] fix: enqueue system event on model switch --- CHANGELOG.md | 1 + src/auto-reply/reply.directive.test.ts | 31 ++++++++++++++++++++++++++ src/auto-reply/reply.ts | 18 +++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d92a54fc0..a22efbb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - LM Studio/Ollama replies now require tags; streaming ignores content until begins. - LM Studio responses API: tools payloads no longer include `strict: null`, and LM Studio no longer gets forced `/` tags. - Identity emoji no longer auto-prefixes replies (set `messages.responsePrefix` explicitly if desired). +- Model switches now enqueue a system event so the next run knows the active model. - `process log` pagination is now line-based (omit `offset` to grab the last N lines). - macOS WebChat: assistant bubbles now update correctly when toggling light/dark mode. - macOS: avoid spawning a duplicate gateway process when an external listener already exists. diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index ab4921190..e7f364929 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -28,6 +28,7 @@ import { extractVerboseDirective, getReplyFromConfig, } from "./reply.js"; +import { drainSystemEvents } from "../infra/system-events.js"; async function withTempHome(fn: (home: string) => Promise): Promise { const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-reply-")); @@ -425,6 +426,36 @@ describe("directive parsing", () => { }); }); + it("queues a system event when switching models", async () => { + await withTempHome(async (home) => { + drainSystemEvents(); + vi.mocked(runEmbeddedPiAgent).mockReset(); + const storePath = path.join(home, "sessions.json"); + + await getReplyFromConfig( + { Body: "/model Opus", From: "+1222", To: "+1222" }, + {}, + { + agent: { + model: "openai/gpt-4.1-mini", + workspace: path.join(home, "clawd"), + allowedModels: ["openai/gpt-4.1-mini", "anthropic/claude-opus-4-5"], + modelAliases: { + Opus: "anthropic/claude-opus-4-5", + }, + }, + session: { store: storePath }, + }, + ); + + const events = drainSystemEvents(); + expect(events).toContain( + "Model switched to Opus (anthropic/claude-opus-4-5).", + ); + expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + }); + }); + it("uses model override for inline /model", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 04fe831cd..d8d40a373 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -39,6 +39,7 @@ import { import { logVerbose } from "../globals.js"; import { buildProviderSummary } from "../infra/provider-summary.js"; import { triggerClawdisRestart } from "../infra/restart.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; import { drainSystemEvents } from "../infra/system-events.js"; import { clearCommandLane, getQueueSize } from "../process/command-queue.js"; import { defaultRuntime } from "../runtime.js"; @@ -546,6 +547,10 @@ export async function getReplyFromConfig( lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS; + const initialModelLabel = `${provider}/${model}`; + const formatModelSwitchEvent = (label: string, alias?: string) => + alias ? `Model switched to ${alias} (${label}).` : `Model switched to ${label}.`; + const directiveOnly = (() => { if ( !hasThinkDirective && @@ -639,6 +644,12 @@ export async function getReplyFromConfig( isDefault, alias: resolved.alias, }; + const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; + if (nextLabel !== initialModelLabel) { + enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), { + contextKey: `model:${nextLabel}`, + }); + } } if (sessionEntry && sessionStore && sessionKey) { @@ -745,6 +756,13 @@ export async function getReplyFromConfig( } provider = resolved.ref.provider; model = resolved.ref.model; + const nextLabel = `${provider}/${model}`; + if (nextLabel !== initialModelLabel) { + enqueueSystemEvent( + formatModelSwitchEvent(nextLabel, resolved.alias), + { contextKey: `model:${nextLabel}` }, + ); + } contextTokens = agentCfg?.contextTokens ?? lookupContextTokens(model) ??