Skip to content

Commit 792b356

Browse files
committed
fix: harden sticky provider profile persistence
Avoid mutating mode defaults when restoring a task profile; persist apiConfigName safely; await async apiConfig init; update tests.
1 parent 1f467ac commit 792b356

File tree

4 files changed

+58
-31
lines changed

4 files changed

+58
-31
lines changed

src/core/task-persistence/taskMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export async function taskMetadata({
110110
size: taskDirSize,
111111
workspace,
112112
mode,
113-
apiConfigName,
113+
...(typeof apiConfigName === "string" && apiConfigName.length > 0 ? { apiConfigName } : {}),
114114
...(initialStatus && { status: initialStatus }),
115115
}
116116

src/core/task/Task.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
10911091
globalStoragePath: this.globalStoragePath,
10921092
})
10931093

1094+
if (this._taskApiConfigName === undefined) {
1095+
await this.taskApiConfigReady
1096+
}
1097+
10941098
const { historyItem, tokenUsage } = await taskMetadata({
10951099
taskId: this.taskId,
10961100
rootTaskId: this.rootTaskId,

src/core/webview/ClineProvider.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,10 @@ export class ClineProvider
914914

915915
if (profile?.name) {
916916
try {
917-
await this.activateProviderProfile({ name: profile.name })
917+
await this.activateProviderProfile(
918+
{ name: profile.name },
919+
{ persistModeConfig: false, persistTaskHistory: false },
920+
)
918921
} catch (error) {
919922
// Log the error but continue with task restoration.
920923
this.log(
@@ -1414,6 +1417,9 @@ export class ClineProvider
14141417
// Change the provider for the current task.
14151418
// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
14161419
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
1420+
1421+
// Keep the current task's sticky provider profile in sync with the newly-activated profile.
1422+
await this.persistStickyProviderProfileToCurrentTask(name)
14171423
} else {
14181424
await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
14191425
}
@@ -1453,9 +1459,42 @@ export class ClineProvider
14531459
await this.postStateToWebview()
14541460
}
14551461

1456-
async activateProviderProfile(args: { name: string } | { id: string }) {
1462+
private async persistStickyProviderProfileToCurrentTask(apiConfigName: string): Promise<void> {
1463+
const task = this.getCurrentTask()
1464+
if (!task) {
1465+
return
1466+
}
1467+
1468+
try {
1469+
const history = this.getGlobalState("taskHistory") ?? []
1470+
const taskHistoryItem = history.find((item) => item.id === task.taskId)
1471+
1472+
if (taskHistoryItem) {
1473+
taskHistoryItem.apiConfigName = apiConfigName
1474+
await this.updateTaskHistory(taskHistoryItem)
1475+
}
1476+
1477+
// Only update in-memory state after successful persistence.
1478+
task.setTaskApiConfigName(apiConfigName)
1479+
} catch (error) {
1480+
// If persistence fails, log the error but don't fail the profile switch.
1481+
this.log(
1482+
`Failed to persist provider profile switch for task ${task.taskId}: ${
1483+
error instanceof Error ? error.message : String(error)
1484+
}`,
1485+
)
1486+
}
1487+
}
1488+
1489+
async activateProviderProfile(
1490+
args: { name: string } | { id: string },
1491+
options?: { persistModeConfig?: boolean; persistTaskHistory?: boolean },
1492+
) {
14571493
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
14581494

1495+
const persistModeConfig = options?.persistModeConfig ?? true
1496+
const persistTaskHistory = options?.persistTaskHistory ?? true
1497+
14591498
// See `upsertProviderProfile` for a description of what this is doing.
14601499
await Promise.all([
14611500
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
@@ -1465,35 +1504,17 @@ export class ClineProvider
14651504

14661505
const { mode } = await this.getState()
14671506

1468-
if (id) {
1507+
if (id && persistModeConfig) {
14691508
await this.providerSettingsManager.setModeConfig(mode, id)
14701509
}
1510+
14711511
// Change the provider for the current task.
14721512
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
14731513

1474-
// Update the current task's sticky provider profile if there's an active task.
1475-
// This ensures the task retains its designated provider profile and switching
1476-
// profiles in one workspace doesn't alter other tasks.
1477-
const task = this.getCurrentTask()
1478-
if (task) {
1479-
try {
1480-
// Update the task history with the new provider profile first.
1481-
const history = this.getGlobalState("taskHistory") ?? []
1482-
const taskHistoryItem = history.find((item) => item.id === task.taskId)
1483-
1484-
if (taskHistoryItem) {
1485-
taskHistoryItem.apiConfigName = name
1486-
await this.updateTaskHistory(taskHistoryItem)
1487-
}
1488-
1489-
// Only update the task's provider profile after successful persistence.
1490-
task.setTaskApiConfigName(name)
1491-
} catch (error) {
1492-
// If persistence fails, log the error but don't fail the profile switch.
1493-
this.log(
1494-
`Failed to persist provider profile switch for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`,
1495-
)
1496-
}
1514+
// Update the current task's sticky provider profile, unless this activation is
1515+
// being used purely as a non-persisting restoration (e.g., reopening a task from history).
1516+
if (persistTaskHistory) {
1517+
await this.persistStickyProviderProfileToCurrentTask(name)
14971518
}
14981519

14991520
await this.postStateToWebview()

src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import * as vscode from "vscode"
44
import { TelemetryService } from "@roo-code/telemetry"
55
import { ClineProvider } from "../ClineProvider"
66
import { ContextProxy } from "../../config/ContextProxy"
7-
import { Task } from "../../task/Task"
8-
import type { HistoryItem, ProviderName } from "@roo-code/types"
7+
import type { HistoryItem } from "@roo-code/types"
98

109
vi.mock("vscode", () => ({
1110
ExtensionContext: vi.fn(),
@@ -433,8 +432,11 @@ describe("ClineProvider - Sticky Provider Profile", () => {
433432
// Initialize task with history item
434433
await provider.createTaskWithHistoryItem(historyItem)
435434

436-
// Verify provider profile was restored via activateProviderProfile
437-
expect(activateProviderProfileSpy).toHaveBeenCalledWith({ name: "saved-profile" })
435+
// Verify provider profile was restored via activateProviderProfile (restore-only: don't persist mode config)
436+
expect(activateProviderProfileSpy).toHaveBeenCalledWith(
437+
{ name: "saved-profile" },
438+
{ persistModeConfig: false, persistTaskHistory: false },
439+
)
438440
})
439441

440442
it("should use current profile if history item has no saved apiConfigName", async () => {

0 commit comments

Comments
 (0)