Skip to content

Commit 1e5ba45

Browse files
committed
fix(task): isolate task configuration from focused provider state
1 parent 38d7e23 commit 1e5ba45

2 files changed

Lines changed: 97 additions & 21 deletions

File tree

src/core/task/Task.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,7 +1591,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15911591
// Get condensing configuration
15921592
const state = await this.providerRef.deref()?.getState()
15931593
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
1594-
const { mode, apiConfiguration } = state ?? {}
1594+
const mode = await this.getTaskMode()
1595+
const apiConfiguration = this.apiConfiguration
15951596

15961597
const { contextTokens: prevContextTokens } = this.getTokenUsage()
15971598

@@ -3782,16 +3783,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
37823783

37833784
const state = await this.providerRef.deref()?.getState()
37843785

3785-
const {
3786-
mode,
3787-
customModes,
3788-
customModePrompts,
3789-
customInstructions,
3790-
experiments,
3791-
language,
3792-
apiConfiguration,
3793-
enableSubfolderRules,
3794-
} = state ?? {}
3786+
const { customModes, customModePrompts, customInstructions, experiments, language, enableSubfolderRules } =
3787+
state ?? {}
3788+
const mode = await this.getTaskMode()
3789+
const apiConfiguration = this.apiConfiguration
37953790

37963791
return await (async () => {
37973792
const provider = this.providerRef.deref()
@@ -3857,7 +3852,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38573852

38583853
private async handleContextWindowExceededError(): Promise<void> {
38593854
const state = await this.providerRef.deref()?.getState()
3860-
const { profileThresholds = {}, mode, apiConfiguration } = state ?? {}
3855+
const { profileThresholds = {} } = state ?? {}
3856+
const mode = await this.getTaskMode()
3857+
const apiConfiguration = this.apiConfiguration
38613858

38623859
const { contextTokens } = this.getTokenUsage()
38633860
await this.safeEnsureModelFetched()
@@ -3997,9 +3994,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39973994
* the `api_req_rate_limit_wait` say type (not an error).
39983995
*/
39993996
private async maybeWaitForProviderRateLimit(retryAttempt: number): Promise<void> {
4000-
const state = await this.providerRef.deref()?.getState()
4001-
const rateLimitSeconds =
4002-
state?.apiConfiguration?.rateLimitSeconds ?? this.apiConfiguration?.rateLimitSeconds ?? 0
3997+
const rateLimitSeconds = this.apiConfiguration?.rateLimitSeconds ?? 0
40033998

40043999
const lastRequestTime = this.rateLimitClock.getLastRequestTime()
40054000
if (rateLimitSeconds <= 0 || !lastRequestTime) {
@@ -4032,14 +4027,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
40324027
const state = await this.providerRef.deref()?.getState()
40334028

40344029
const {
4035-
apiConfiguration,
40364030
autoApprovalEnabled,
40374031
requestDelaySeconds,
4038-
mode,
40394032
autoCondenseContext = true,
40404033
autoCondenseContextPercent = 100,
40414034
profileThresholds = {},
40424035
} = state ?? {}
4036+
const mode = await this.getTaskMode()
4037+
const apiConfiguration = this.apiConfiguration
40434038

40444039
// Get condensing configuration for automatic triggers.
40454040
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
@@ -4452,7 +4447,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
44524447

44534448
// Respect provider rate limit window
44544449
let rateLimitDelay = 0
4455-
const rateLimit = (state?.apiConfiguration ?? this.apiConfiguration)?.rateLimitSeconds || 0
4450+
const rateLimit = this.apiConfiguration?.rateLimitSeconds || 0
44564451
const lastRequestTime = this.rateLimitClock.getLastRequestTime()
44574452
if (lastRequestTime && rateLimit > 0) {
44584453
const elapsed = performance.now() - lastRequestTime

src/core/task/__tests__/Task.spec.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { TelemetryService } from "@roo-code/telemetry"
1717

1818
import { Task } from "../Task"
19+
import { SYSTEM_PROMPT } from "../../prompts/system"
1920
import { createRateLimitClock } from "../RateLimitClock"
2021
import { summarizeConversation } from "../../condense"
2122
import { ClineProvider } from "../../webview/ClineProvider"
@@ -223,6 +224,15 @@ vi.mock("../../condense", async (importOriginal) => {
223224
}),
224225
}
225226
})
227+
228+
vi.mock("../../prompts/system", async (importOriginal) => {
229+
const actual = await importOriginal<typeof import("../../prompts/system")>()
230+
return {
231+
...actual,
232+
SYSTEM_PROMPT: vi.fn(actual.SYSTEM_PROMPT),
233+
}
234+
})
235+
226236
// Mock storagePathManager to prevent dynamic import issues.
227237
vi.mock("../../../utils/storage", () => ({
228238
getTaskDirectoryPath: vi
@@ -451,6 +461,73 @@ describe("Cline", () => {
451461
})
452462
})
453463

464+
describe("task-local configuration isolation", () => {
465+
it("uses the task mode and API configuration when focused provider state differs", async () => {
466+
const taskApiConfiguration: ProviderSettings = {
467+
...mockApiConfig,
468+
todoListEnabled: true,
469+
}
470+
vi.spyOn(mockProvider, "getState").mockResolvedValue({ mode: "architect", mcpEnabled: false })
471+
472+
const task = new Task({
473+
provider: mockProvider,
474+
apiConfiguration: taskApiConfiguration,
475+
task: "test task",
476+
startTask: false,
477+
})
478+
await task.getTaskMode()
479+
480+
vi.spyOn(mockProvider, "getState").mockResolvedValue({
481+
mode: "code",
482+
mcpEnabled: false,
483+
apiConfiguration: { ...mockApiConfig, todoListEnabled: false },
484+
})
485+
vi.mocked(SYSTEM_PROMPT).mockResolvedValueOnce("mock system prompt")
486+
487+
await getTaskTestAccess(task).getSystemPrompt()
488+
489+
const systemPromptCall = requireDefined(vi.mocked(SYSTEM_PROMPT).mock.calls.at(-1))
490+
expect(systemPromptCall[5]).toBe("architect")
491+
expect(systemPromptCall[12]).toMatchObject({ todoListEnabled: true })
492+
})
493+
494+
it("uses the task mode in request metadata when focused provider state differs", async () => {
495+
vi.spyOn(mockProvider, "getState").mockResolvedValue({
496+
mode: "ask",
497+
mcpEnabled: false,
498+
autoApprovalEnabled: true,
499+
requestDelaySeconds: 0,
500+
})
501+
const task = new Task({
502+
provider: mockProvider,
503+
apiConfiguration: mockApiConfig,
504+
task: "test task",
505+
startTask: false,
506+
})
507+
await task.getTaskMode()
508+
vi.spyOn(getTaskTestAccess(task), "getSystemPrompt").mockResolvedValue("mock system prompt")
509+
510+
vi.spyOn(mockProvider, "getState").mockResolvedValue({
511+
mode: "code",
512+
mcpEnabled: false,
513+
autoApprovalEnabled: true,
514+
requestDelaySeconds: 0,
515+
})
516+
const stream = (async function* () {
517+
yield { type: "text", text: "response" } as ApiStreamChunk
518+
})()
519+
const createMessage = vi.spyOn(task.api, "createMessage").mockReturnValue(stream)
520+
task.apiConversationHistory = [
521+
{ role: "user", content: [{ type: "text", text: "test message" }], ts: Date.now() },
522+
]
523+
524+
await task.attemptApiRequest().next()
525+
526+
const metadata = requireDefined(createMessage.mock.calls[0])[2]
527+
expect(metadata?.mode).toBe("ask")
528+
})
529+
})
530+
454531
describe("sayAndCreateMissingParamError", () => {
455532
it("surfaces a localized error notice and returns the missing-parameter tool error for both relPath branches", async () => {
456533
const cline = new Task({
@@ -750,7 +827,7 @@ describe("Cline", () => {
750827
expect(mockDelay).toHaveBeenCalledWith(1000)
751828
})
752829

753-
it("should respect rate limit window in retry backoff", async () => {
830+
it("uses the task rate limit in retry backoff when focused provider state differs", async () => {
754831
const clock = createRateLimitClock()
755832
const rateLimitConfig = {
756833
...mockApiConfig,
@@ -815,15 +892,19 @@ describe("Cline", () => {
815892
const providerState = await mockProvider.getState()
816893
vi.spyOn(mockProvider, "getState").mockResolvedValue({
817894
...providerState,
818-
apiConfiguration: rateLimitConfig,
895+
apiConfiguration: {
896+
...mockApiConfig,
897+
rateLimitSeconds: 1,
898+
},
819899
autoApprovalEnabled: true,
820900
requestDelaySeconds: 3,
821901
})
822902

823903
const iterator = cline.attemptApiRequest(0)
824904
await iterator.next()
825905

826-
// rateLimitSeconds=10 > exponentialDelay=ceil(3*2^0)=3, so
906+
// The task rateLimitSeconds=10 (rather than the focused provider's 1)
907+
// exceeds exponentialDelay=ceil(3*2^0)=3, so
827908
// finalDelay=10 and the countdown loop fires delay(1000) ten times.
828909
expect(mockDelay).toHaveBeenCalledWith(1000)
829910
expect(mockDelay).toHaveBeenCalledTimes(10)

0 commit comments

Comments
 (0)