fix: prevent auto-mode model switches from persisting as global default (#30) and harden resume resilience (#16)

Patch SDK setModel() to accept { persist: false } so per-unit model
switching in auto-mode no longer overwrites the user's global default.
Add state rebuild + doctor on resume, guard logging for silent dispatch
failures, and active-state check before prompt injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-11 17:39:46 -06:00
parent f4c46516a6
commit 1872e8db78
2 changed files with 77 additions and 3 deletions

View file

@ -1,3 +1,63 @@
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
index 90622c2..cff094b 100644
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
+++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js
@@ -1007,7 +1007,7 @@ export class AgentSession {
* Validates API key, saves to session and settings.
* @throws Error if no API key available for the model
*/
- async setModel(model) {
+ async setModel(model, options) {
const apiKey = await this._modelRegistry.getApiKey(model);
if (!apiKey) {
throw new Error(`No API key for ${model.provider}/${model.id}`);
@@ -1016,7 +1016,9 @@ export class AgentSession {
const thinkingLevel = this._getThinkingLevelForModelSwitch();
this.agent.setModel(model);
this.sessionManager.appendModelChange(model.provider, model.id);
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
+ if (options?.persist !== false) {
+ this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
+ }
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(thinkingLevel);
await this._emitModelSelect(model, previousModel, "set");
@@ -1067,7 +1069,9 @@ export class AgentSession {
// Apply model
this.agent.setModel(next.model);
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
- this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
+ if (options?.persist !== false) {
+ this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
+ }
// Apply thinking level.
// - Explicit scoped model thinking level overrides current session level
// - Undefined scoped model thinking level inherits the current session preference
@@ -1094,7 +1098,9 @@ export class AgentSession {
const thinkingLevel = this._getThinkingLevelForModelSwitch();
this.agent.setModel(nextModel);
this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
- this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
+ if (options?.persist !== false) {
+ this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
+ }
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(thinkingLevel);
await this._emitModelSelect(nextModel, currentModel, "cycle");
@@ -1659,11 +1665,11 @@ export class AgentSession {
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
refreshTools: () => this._refreshToolRegistry(),
getCommands,
- setModel: async (model) => {
+ setModel: async (model, options) => {
const key = await this.modelRegistry.getApiKey(model);
if (!key)
return false;
- await this.setModel(model);
+ await this.setModel(model, options);
return true;
},
getThinkingLevel: () => this.thinkingLevel,
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js
index 27fe820..68f277f 100644
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/tools/bash.js

View file

@ -247,6 +247,14 @@ export async function startAuto(
if (!getLedger()) initMetrics(base);
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
// Rebuild disk state before resuming — user interaction during pause may have changed files
try { await rebuildState(base); } catch { /* non-fatal */ }
try {
const report = await runGSDDoctor(base, { fix: true });
if (report.fixesApplied.length > 0) {
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
}
} catch { /* non-fatal */ }
await dispatchNextUnit(ctx, pi);
return;
}
@ -758,7 +766,12 @@ async function dispatchNextUnit(
ctx: ExtensionContext,
pi: ExtensionAPI,
): Promise<void> {
if (!active || !cmdCtx) return;
if (!active || !cmdCtx) {
if (active && !cmdCtx) {
ctx.ui.notify("Auto-mode dispatch failed: no command context. Run /gsd auto to restart.", "error");
}
return;
}
let state = await deriveState(basePath);
let mid = state.activeMilestone?.id;
@ -1086,7 +1099,7 @@ async function dispatchNextUnit(
const allModels = ctx.modelRegistry.getAll();
const model = allModels.find(m => m.id === preferredModelId);
if (model) {
const ok = await pi.setModel(model);
const ok = await pi.setModel(model, { persist: false });
if (ok) {
ctx.ui.notify(`Model: ${preferredModelId}`, "info");
}
@ -1186,7 +1199,8 @@ async function dispatchNextUnit(
await pauseAuto(ctx, pi);
}, hardTimeoutMs);
// Inject prompt
// Inject prompt — verify auto-mode still active (guards against race with timeout/pause)
if (!active) return;
pi.sendMessage(
{ customType: "gsd-auto", content: finalPrompt, display: verbose },
{ triggerTurn: true },