diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4e3ee9000..1445eed3c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -121,6 +121,7 @@ - Fixed `/rename` title arguments treating `#` prompt-action tokens as autocomplete triggers instead of literal text. - Fixed empty session `.jsonl` files accumulating in the sessions directory after a draft-then-clear exit cycle. - Fixed advisor being disabled for the entire session when resolving to a reasoning model with no controllable effort surface (e.g., `devin/glm-5-2*`). +- Fixed live advisors continuing to use a stale `modelRoles.advisor` selection after `/model` changed the advisor model. ([#4612](https://github.com/can1357/oh-my-pi/issues/4612)) - Fixed legacy extension plugin validation failures by re-exporting relocated catalog symbols (such as `calculateCost`, `modelsAreEqual`, and `getBundledProviders`) through the legacy `pi-ai` root shim. - Fixed legacy Pi extension imports of `DefaultResourceLoader` by adding a compatibility loader shim that translates `resourceLoader` into native session discovery options. - Fixed legacy Pi extension reloads on POSIX to ensure same-process re-imports pick up edits across the entire dependency graph. diff --git a/packages/coding-agent/src/config/settings.ts b/packages/coding-agent/src/config/settings.ts index 0bde19651..268b147f7 100644 --- a/packages/coding-agent/src/config/settings.ts +++ b/packages/coding-agent/src/config/settings.ts @@ -429,6 +429,9 @@ export class Settings { if (path === "statusLine.sessionAccent") { statusLineSessionAccentSignal.fire(); } + if (path === "modelRoles") { + modelRolesSignal.fire(); + } } /** @@ -1477,6 +1480,12 @@ const appendOnlyModeSignal = new SettingSignal<[value: string]>("provider.append */ export const onAppendOnlyModeChanged = (cb: (value: string) => void) => appendOnlyModeSignal.on(cb); +/** Fires when any model role changes at runtime. */ +const modelRolesSignal = new SettingSignal("modelRoles"); + +/** Subscribe to model role changes. Returns an unsubscribe function. */ +export const onModelRolesChanged: (cb: () => void) => () => void = modelRolesSignal.on.bind(modelRolesSignal); + /** Fires when `statusLine.sessionAccent` changes at runtime. */ const statusLineSessionAccentSignal = new SettingSignal("statusLine.sessionAccent"); diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 5fc4697f9..687874031 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -181,7 +181,12 @@ import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles"; import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates"; import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier"; import type { Settings, SkillsSettings } from "../config/settings"; -import { getDefault, onAppendOnlyModeChanged, validateProviderMaxInFlightRequests } from "../config/settings"; +import { + getDefault, + onAppendOnlyModeChanged, + onModelRolesChanged, + validateProviderMaxInFlightRequests, +} from "../config/settings"; import { RawSseDebugBuffer } from "../debug/raw-sse-buffer"; import { loadCapability } from "../discovery"; import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit"; @@ -1562,6 +1567,7 @@ export class AgentSession { #cancelExitRecorder?: () => void; #exitRecorded = false; #unsubscribeAppendOnly?: () => void; + #unsubscribeModelRoles?: () => void; /** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */ #lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined }; #eventListeners: AgentSessionEventListener[] = []; @@ -2270,6 +2276,11 @@ export class AgentSession { this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent); // Re-evaluate append-only context mode when the setting changes at runtime. this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model)); + this.#unsubscribeModelRoles = onModelRolesChanged(() => { + if (!this.#advisorEnabled || this.#isDisposed) return; + if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime(); + this.#buildAdvisorRuntime(true); + }); } // ------------------------------------------------------------------------- // Advisor runtime lifecycle @@ -5781,6 +5792,10 @@ export class AgentSession { this.#unsubscribeAppendOnly(); this.#unsubscribeAppendOnly = undefined; } + if (this.#unsubscribeModelRoles) { + this.#unsubscribeModelRoles(); + this.#unsubscribeModelRoles = undefined; + } this.#eventListeners = []; } diff --git a/packages/coding-agent/test/advisor-toggle.test.ts b/packages/coding-agent/test/advisor-toggle.test.ts index 0812a895b..0d911aad0 100644 --- a/packages/coding-agent/test/advisor-toggle.test.ts +++ b/packages/coding-agent/test/advisor-toggle.test.ts @@ -98,6 +98,18 @@ describe("AgentSession advisor toggle", () => { expect(session.getAdvisorAgent()?.state.model.id).toBe(replacementModel.id); }); + it("refreshes the live advisor when the advisor role setting changes", () => { + session.settings.setModelRole("advisor", `${model.provider}/${model.id}`); + expect(session.setAdvisorEnabled(true)).toBe(true); + expect(session.getAdvisorAgent()?.state.model.provider).toBe(model.provider); + expect(session.getAdvisorAgent()?.state.model.id).toBe(model.id); + + session.settings.setModelRole("advisor", `${replacementModel.provider}/${replacementModel.id}`); + + expect(session.getAdvisorAgent()?.state.model.provider).toBe(replacementModel.provider); + expect(session.getAdvisorAgent()?.state.model.id).toBe(replacementModel.id); + }); + it("keeps explicit enable idempotent when the advisor config is unchanged", () => { session.settings.setModelRole("advisor", `${model.provider}/${model.id}`); expect(session.setAdvisorEnabled(true)).toBe(true);