Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
19788c0
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
82358bd
feat(api): per-request thinking effort override and adaptive effort e…
easonliang28 Sep 4, 2026
4075c8a
feat(task): task-local runtime thinking effort state with per-request…
easonliang28 Sep 4, 2026
710bf61
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
521e0e6
feat(settings): dynamic thinking effort experimental toggle (DTE-1)
easonliang28 Sep 4, 2026
e2f249a
Merge branch 'main' into feat/dte-v2-1-dynamic-thinking-effort
easonLiangWorldedtech Sep 4, 2026
53f22dc
Merge upstream main (0d937c05081) into feat/dte-v2-1-dynamic-thinking…
easonliang28 Sep 4, 2026
18f488f
Merge feat/dte-v2-1-dynamic-thinking-effort (53f22dc) into feat/dte-v…
easonliang28 Sep 4, 2026
39762bf
test(settings): assert false and unset dynamicThinkingEffort persiste…
easonliang28 Sep 4, 2026
f97f8d9
Merge U2 head (18f488fa5) into U3 (stack sync: U1 final CR fixes + U2…
easonliang28 Sep 4, 2026
efbd336
Merge U1 final head (39762bf81) into U2 (stack sync: U1 last CR fix —…
easonliang28 Sep 4, 2026
c5b48aa
Merge U2 head (efbd336e5) into U3 (stack sync: U1 final persistence t…
easonliang28 Sep 4, 2026
e89cced
Merge upstream main (0dbd5846f) into U1 (stack sync: main v3.82.0 - G…
easonliang28 Sep 5, 2026
069c34b
Merge U1 head (e89cceddd9a) into U2 (stack sync: U1 final head in - m…
easonliang28 Sep 5, 2026
e8c66cf
Merge U2 head (069c34b9a) into U3 (stack sync: main advances to 0dbd5…
easonliang28 Sep 5, 2026
1edd728
fix(task): clear DTE runtime-effort state on disposal and dispose abo…
easonliang28 Sep 5, 2026
1087fcb
fix(task): restore API configuration when clearing the DTE effort ove…
easonliang28 Sep 5, 2026
9bdc3c4
Merge remote-tracking branch 'upstream/main' into feat/dte-v2-2-per-r…
Oct 2, 2026
810caf1
chore: trigger label reconcile (CI green + CR approved, cron stalled)
Oct 2, 2026
2d8feeb
Merge commit 'refs/shapes/upstream-main' into feat/dte-v2-2-per-reque…
Oct 2, 2026
77ce7d2
Merge upstream main (3859e5dd8) into feat/dte-v2-2-per-request-effort
Oct 4, 2026
e776028
Retrigger CI: mutation-diff job failed on harness errors, not mutants
Oct 4, 2026
107649b
Drop the Windows related-test discovery workaround
Oct 4, 2026
331078f
merge(upstream): take main into the per-request effort unit
Oct 6, 2026
9189386
merge(upstream): take main into DTE unit 1
Oct 6, 2026
4f71c00
merge(upstream): take main into DTE unit 3
Oct 6, 2026
8fdade5
test(webview): drop two explicit-any casts the merge re-introduced
Oct 6, 2026
6aa3560
merge(upstream): redo the spec resolutions from the merge stages
Oct 6, 2026
72fd869
test(webview): close the test the conflict cut open
Oct 6, 2026
c95c510
test(webview): assert the two different outcomes for invalid delete t…
Oct 6, 2026
48ef159
Merge upstream/main into feat/dte-v2-2-per-request-effort
Oct 7, 2026
7e4c79d
Merge upstream/main into feat/dte-v2-2-per-request-effort
Oct 7, 2026
8fc8d1e
Merge upstream/main into feat/dte-v2-1-dynamic-thinking-effort
Oct 7, 2026
7c98c57
Merge upstream/main into feat/dte-v2-3-task-runtime-effort
Oct 7, 2026
d47e937
chore: re-run the e2e-mock restart scenario
Oct 7, 2026
01a111e
test(settings): restore the blanket auto-deny specs to their upstream…
Oct 8, 2026
5ebf096
Merge upstream main into feat/dte-v2-2-per-request-effort
easonliang28 Oct 13, 2026
22586f9
Merge upstream main into feat/dte-v2-3-task-runtime-effort
easonliang28 Oct 13, 2026
ea2577a
Drop the double blank line the merge left between describes
easonliang28 Oct 13, 2026
0287e7e
Make the invalid-timestamp delete assertion non-vacuous
easonliang28 Oct 13, 2026
1bbdab8
fix(settings): keep task-local thinking effort out of the profile pro…
easonliang28 Oct 14, 2026
66861d9
test(settings): regenerate experimental-settings visual baselines in …
easonliang28 Oct 14, 2026
f6e6a1e
chore(task): drop Stryker suppression comments per maintainer directive
easonliang28 Oct 14, 2026
eb6acd9
Merge Zoo-Code-Org main 4f1dee18e into dte-v2-3 (PR 1523)
easonliang28 Oct 16, 2026
bd914cf
Merge upstream/main into feat/dte-v2-1-dynamic-thinking-effort
easonliang28 Oct 17, 2026
02d56ec
Merge feat/dte-v2-1-dynamic-thinking-effort (post-main-sync head) int…
easonliang28 Oct 17, 2026
58a0534
Merge feat/dte-v2-2-per-request-effort (post-main-sync head) into fea…
easonliang28 Oct 17, 2026
5aa123e
fix(task): clear the runtime effort override before handlers are coll…
Oct 21, 2026
19e8586
Merge remote-tracking branch 'upstream/main' into feat/dte-v2-3-task-…
easonLiangWorldedtech Oct 22, 2026
8b40ef4
style: apply repo Prettier to the merge resolution
easonLiangWorldedtech Oct 22, 2026
5937e21
chore(knip): mark the test-only view-identity seams @internal
easonLiangWorldedtech Oct 22, 2026
c7f5c49
chore: re-trigger CI after the Windows webview unhandled-error flake
easonLiangWorldedtech Oct 22, 2026
5febf6a
Merge upstream/main into feat/dte-v2-3-task-runtime-effort
Nov 1, 2026
6bfd5ce
test(webview): give the experimental settings fixture the required ap…
easonLiangWorldedtech Nov 2, 2026
4b737c5
merge upstream/main (a4f774b17) into feat/dte-v2-3-task-runtime-effort
easonLiangWorldedtech Nov 2, 2026
ec840f5
test(settings): pin the hidden dynamicThinkingEffort save test to a p…
easonLiangWorldedtech Nov 3, 2026
36b9e5b
test(settings): type the hydrated settings-view state
easonLiangWorldedtech Nov 3, 2026
b1d2d58
test(webview): await the on-demand locale fetch instead of timing it
easonLiangWorldedtech Nov 4, 2026
b2b31f7
refactor(dte): drop the U5 reasoning pair and the duplicated U1 surface
easonLiangWorldedtech Nov 4, 2026
cf67a79
refactor(dte): keep only the task-runtime-effort unit in this PR
easonLiangWorldedtech Nov 4, 2026
8144b12
fix(task): make runtime-effort updates disposal-aware and prove deliv…
easonLiangWorldedtech Nov 4, 2026
60d3acd
test(condense): prove effort forwarding at the summarizer boundary
easonLiangWorldedtech Nov 5, 2026
aa9d962
Merge upstream/main (e943530c8) into feat/dte-v2-3-task-runtime-effort
easonLiangWorldedtech Nov 5, 2026
79e1c24
fix(task): address the #1523 review round on the effort override
easonLiangWorldedtech Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
retiredProviderIdentifiers,
type ProviderSettings,
type ModelInfo,
type ReasoningEffortExtended,
type ModelRecord,
} from "@roo-code/types"

Expand Down Expand Up @@ -127,6 +128,16 @@ export interface ApiHandlerCreateMessageMetadata {
* when the user clicks stop, preventing wasted API tokens/compute on the provider side.
*/
abortSignal?: AbortSignal
/**
* Per-request thinking effort override applied by rebuilding the handler.
* Providers resolve effort from the handler options (each provider reads
* `options.reasoningEffort` through its own model-params step), so passing the
* override in the configuration handed to `buildApiHandler` is what takes effect;
* it outranks the settings-derived `reasoningEffort` for the handler built from
* this object. Task-scoped and transient: it applies to the next request only
* (no mid-stream effect) and is never persisted to settings.
*/
reasoningEffort?: ReasoningEffortExtended
}

/**
Expand Down
98 changes: 98 additions & 0 deletions src/core/condense/__tests__/condense-effort-metadata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// npx vitest run src/core/condense/__tests__/condense-effort-metadata.spec.ts
//
// The summarizer boundary: Task hands the task-local thinking effort override to
// to summarizeConversation inside request metadata. This checks the other half of that hand-off:
// the real summarizer forwards the metadata to the handler's createMessage instead of dropping
// the effort. The Task-side tests cover Task-to-summarizer propagation only.

import { Anthropic } from "@anthropic-ai/sdk"
import type { ModelInfo, ReasoningEffortExtended } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"

import type { ApiHandlerCreateMessageMetadata } from "../../../api"
import { BaseProvider } from "../../../api/providers/base-provider"
import { ApiStream, ApiStreamChunk } from "../../../api/transform/stream"
import { ApiMessage } from "../../task-persistence/apiMessages"
import { summarizeConversation } from "../index"

const OVERRIDE_EFFORT: ReasoningEffortExtended = "xhigh"
const TASK_ID = "dte-condense-task"

class RecordingApiHandler extends BaseProvider {
/** The metadata of the last request this handler was asked to make. */
public requestedMetadata: ApiHandlerCreateMessageMetadata | undefined

override createMessage(
_systemPrompt: string,
_messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
this.requestedMetadata = metadata
return (async function* (): AsyncGenerator<ApiStreamChunk> {
yield { type: "text", text: "summary of the conversation" }
yield { type: "usage", inputTokens: 10, outputTokens: 5, totalCost: 0.01 }
})()
}

override getModel(): { id: string; info: ModelInfo } {
return {
id: "dte-condense-model",
info: { contextWindow: 100_000, maxTokens: 5_000, supportsPromptCache: false, supportsImages: false },
}
}
}

// Three messages with no summary in them: getMessagesSinceLastSummary returns all of them, so
// summarizeConversation gets past its "not enough messages" guard and issues the request.
function conversationHistory(): ApiMessage[] {
return [
{ role: "user", content: [{ type: "text", text: "read the failing test" }], ts: 1 },
{ role: "assistant", content: [{ type: "text", text: "the assertion drifted" }], ts: 2 },
{ role: "user", content: [{ type: "text", text: "fix it" }], ts: 3 },
]
}

describe("summarizeConversation reasoning-effort forwarding", () => {
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
})

it("forwards an active override to the handler request", async () => {
const handler = new RecordingApiHandler()

const result = await summarizeConversation({
messages: conversationHistory(),
apiHandler: handler,
systemPrompt: "system prompt",
taskId: TASK_ID,
isAutomaticTrigger: false,
metadata: { mode: "code", taskId: TASK_ID, reasoningEffort: OVERRIDE_EFFORT },
environmentDetails: "environment details",
cwd: "/workspace",
})

expect(result.error).toBeUndefined()
expect(result.summary).toBe("summary of the conversation")
expect(handler.requestedMetadata?.reasoningEffort).toBe(OVERRIDE_EFFORT)
})

it("omits the key when the caller hands no override", async () => {
const handler = new RecordingApiHandler()

const result = await summarizeConversation({
messages: conversationHistory(),
apiHandler: handler,
systemPrompt: "system prompt",
taskId: TASK_ID,
isAutomaticTrigger: false,
metadata: { mode: "code", taskId: TASK_ID },
environmentDetails: "environment details",
cwd: "/workspace",
})

expect(result.summary).toBe("summary of the conversation")
expect(handler.requestedMetadata).not.toHaveProperty("reasoningEffort")
})
})
129 changes: 121 additions & 8 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type TaskMetadata,
type TaskEvents,
type ProviderSettings,
type ReasoningEffortExtended,
type TokenUsage,
type ToolUsage,
type ToolName,
Expand Down Expand Up @@ -418,6 +419,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// API
apiConfiguration: ProviderSettings
api: ApiHandler
// Task-local thinking effort override. Transient per-task state —
// never persisted to settings; cleared on dispose (see dispose()).
private runtimeThinkingEffort?: ReasoningEffortExtended
private runtimeThinkingEffortSource?: string
/**
* Units of in-flight API work: a request from setup through its retries and
* background usage drain, or a manual condense. Work can hold any handler it
Expand Down Expand Up @@ -2489,16 +2494,39 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
* Updates the API configuration and rebuilds the API handler.
* There is no tool-protocol switching or tool parser swapping.
*
* When a task-local thinking effort override is active
* (`setRuntimeThinkingEffort`), the incoming configuration's `reasoningEffort`
* becomes the new restore value and the override is re-applied on top of the
* fresh in-memory copy — clearing the override later restores the NEW profile's
* value, not a stale one.
*
* @param newApiConfiguration - The new API configuration to use
*/
public updateApiConfiguration(newApiConfiguration: ProviderSettings): void {
// Update the configuration and rebuild the API handler. Handlers such as
// VS Code LM / GitHub Copilot own host listeners, so the old one is
// released — immediately when idle, or when the last request, retry,
// background drain, or condense that leased it settles.
// apiConfiguration stays the profile-derived projection;
// an active task-local override is applied to the rebuilt handler only, so
// a profile switch keeps the override live and clearing it later restores
// the NEW profile's value (it is the configuration itself now).
//
// Same disposal guard as setRuntimeThinkingEffort: this is the other path that rebuilds the
// handler, and once disposal has started its handler set is already collected, so a rebuild
// would strand the replacement and re-retire a disposed handler. The task is going away, so
// there is nothing left to re-project either.
if (this.disposalPromise !== undefined) {
return
}

const previousApi = this.api
this.apiConfiguration = newApiConfiguration
this.api = buildApiHandler(this.apiConfiguration)
this.api = buildApiHandler(
this.runtimeThinkingEffort !== undefined
? { ...newApiConfiguration, reasoningEffort: this.runtimeThinkingEffort }
: newApiConfiguration,
)
this.retireApi(previousApi)

// Refresh only the soft tool-repetition limit from the new profile so an
Expand All @@ -2515,6 +2543,76 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.toolRepetitionDetector.updateLimits(this.toolRepetitionSoftLimit, this.consecutiveMistakeLimit)
}

/**
* Sets — or clears with `undefined` — the task-local thinking
* effort override.
*
* Resolution order for the affected requests (strongest first): this
* task-local override → settings `reasoningEffort` → model default. The
* override applies to the NEXT API request only (no mid-stream effect): it
* is passed per request as `metadata.reasoningEffort` and applied to the
* rebuilt API handler. `apiConfiguration` itself stays the profile-derived
* projection — the override never enters it — so the webview state (which
* posts the active task's `apiConfiguration`) and any profile save built
* from it can never carry the task-local effort. `undefined` clears the
* override and rebuilds the handler from the projection.
*
* Not inherited: child, handoff, and resumed tasks construct their own Task and
* start with no override — it lives in this task's private fields and is never
* persisted.
* @param effort - The task-local effort, or `undefined` to clear.
* @param source - Optional provenance label (UI wiring lands in a later PR).
*/
public setRuntimeThinkingEffort(effort: ReasoningEffortExtended | undefined, source?: string): void {
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
Resolved
// Disposal has started (or finished): `disposalPromise` is set once and never cleared, so it
// is the liveness signal. The override is dropped rather than applied — the task serves no
// further request, and the handler set that dispose collects was already snapshotted, so
// rebuilding here would produce a handler nobody owns (still holding its configuration
// listeners) and retire the handler disposal has just disposed.
if (this.disposalPromise !== undefined) {
return
}

const wasActive = this.runtimeThinkingEffort !== undefined
this.runtimeThinkingEffort = effort
this.runtimeThinkingEffortSource = effort === undefined ? undefined : source

if (effort === undefined && !wasActive) {
// Already inactive: nothing to clear.
return
}

// Rebuild the handler with the effective effort applied. The projection
// (this.apiConfiguration) is untouched, so clearing needs no restore: the
// handler is simply rebuilt from the profile-derived configuration. The
// replaced handler is retired like any other swap: handlers with host
// listeners must not leak when the effort changes.
const previousApi = this.api
this.api = buildApiHandler(
effort !== undefined ? { ...this.apiConfiguration, reasoningEffort: effort } : this.apiConfiguration,
)
this.retireApi(previousApi)
Comment thread
easonLiangWorldedtech marked this conversation as resolved.
Resolved
}

/**
* Reads the current task-local thinking effort override.
*/
public getRuntimeThinkingEffort(): { effort?: ReasoningEffortExtended; source?: string } {
return {
effort: this.runtimeThinkingEffort,
source: this.runtimeThinkingEffortSource,
}
}

/**
* Metadata fragment carrying the active task-local effort
* override on a single request. Empty when no override is active, so the
* existing settings resolution applies unchanged.
*/
private getRuntimeThinkingEffortMetadata(): Pick<ApiHandlerCreateMessageMetadata, "reasoningEffort"> {
return this.runtimeThinkingEffort !== undefined ? { reasoningEffort: this.runtimeThinkingEffort } : {}
}

/**
* Marks the start of in-flight API work. The returned release is idempotent;
* the last release disposes every handler replaced while work was running.
Expand Down Expand Up @@ -2694,6 +2792,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
parallelToolCalls: true,
}
: {}),
// Carry the active task-local effort override.
...this.getRuntimeThinkingEffortMetadata(),
}
// Generate environment details to include in the condensed summary
const environmentDetails = await getEnvironmentDetails(this, true)
Expand Down Expand Up @@ -3629,6 +3729,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}

/**
* Centralized task teardown: releases task resources and resets transient
* task-local state.
*/
public dispose(): Promise<void> {
if (this.disposalPromise) {
return this.disposalPromise
Expand Down Expand Up @@ -3740,6 +3844,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
console.error("Error reverting diff changes:", error)
}

// Clear the DTE runtime-effort override so a retained disposed task never
// reports a stale override. The fields are cleared directly instead of via
// setRuntimeThinkingEffort: that setter rebuilds the handler, and during
// disposal the rebuild would (a) create a replacement handler this pass has
// already collected and disposed, and (b) retire the just-disposed handler a
// second time. `apiConfiguration` never carries the override (it stays the
// profile-derived projection), so there is nothing to restore.
this.runtimeThinkingEffort = undefined
this.runtimeThinkingEffortSource = undefined

await pendingCleanup
await this.diffReversionPromise
}
Expand Down Expand Up @@ -4286,7 +4400,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// under the canonical name slip past a name comparison. The first call wins.
const idAlreadyUsed = this.assistantMessageContent.some(
(entry) =>
// Stryker disable next-line ConditionalExpression: equivalent mutant, only tool_use and mcp_tool_use entries carry an id, so the type check cannot change entry.id === event.id
(entry.type === "tool_use" || entry.type === "mcp_tool_use") &&
(entry as { id?: string }).id === event.id,
)
Expand Down Expand Up @@ -4339,7 +4452,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// keep the deprecated single-entry-per-ID lookup.
let streamingKey: string | undefined

// Stryker disable next-line ConditionalExpression: equivalent mutant, event.name is always a string here, so the legacy nameless branch is unreachable
if (event.name !== undefined) {
streamingKey = NativeToolCallParser.makeStreamingKey(event.id, event.name)
} else {
Expand All @@ -4356,7 +4468,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

const partialToolUse =
// Stryker disable next-line ConditionalExpression: equivalent mutant, streamingKey is always a string here (makeStreamingKey), so the null side is unreachable
streamingKey === undefined
? null
: NativeToolCallParser.processStreamingChunk(
Expand All @@ -4365,7 +4476,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
nativeToolCallParserScope,
)

// Stryker disable next-line ConditionalExpression, EqualityOperator: equivalent mutants, streamingKey is always defined here, so both conditions reduce to the partialToolUse check
if (partialToolUse && streamingKey !== undefined) {
// Reuse the compound key built above: it is exactly the key the start event
// registered under, so the delta lookup needs no second encoding.
Expand Down Expand Up @@ -4784,7 +4894,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (event.type === "tool_call_end") {
// Create compound key for deduplication (same pattern as streaming handler).
// End events carry the tool name; fall back to the id for events that don't.
// Stryker disable next-line LogicalOperator: equivalent mutant, finalizeRawChunks emits end events with a string name, so the ?? event.id fallback is unreachable
let eventName = event.name ?? event.id
let dedupKey = NativeToolCallParser.makeStreamingKey(event.id, eventName)

Expand All @@ -4801,13 +4910,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// name (provider quirk), the compound-key lookup above misses and both the
// parser entry and the dedup tracking would stay stale. Resolve the tracked
// entry by id and target the real compound key for finalization and cleanup.
// Stryker disable next-line ConditionalExpression, LogicalOperator, EqualityOperator: equivalent mutants, the both-miss case is unreachable via the raw-chunk pipeline
if (finalToolUse === null && toolUseIndex === undefined) {
const resolved = NativeToolCallParser.getStreamingToolCallById(
event.id,
nativeToolCallParserScope,
)
// Stryker disable next-line ConditionalExpression: unreachable defensive region — getStreamingToolCallById can only resolve when the both-miss case above is reachable, which the raw-chunk pipeline cannot produce (see the directive there).
if (resolved) {
eventName = resolved.name
dedupKey = NativeToolCallParser.makeStreamingKey(resolved.id, resolved.name)
Expand Down Expand Up @@ -5501,6 +5608,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
parallelToolCalls: true,
}
: {}),
// Carry the active task-local effort override.
...this.getRuntimeThinkingEffortMetadata(),
}

try {
Expand Down Expand Up @@ -5779,6 +5888,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
parallelToolCalls: true,
}
: {}),
// Carry the active task-local effort override.
...this.getRuntimeThinkingEffortMetadata(),
}

// Only generate environment details when context management will actually run.
Expand Down Expand Up @@ -5948,6 +6059,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
taskId: this.taskId,
suppressPreviousResponseId: this.skipPrevResponseIdOnce,
abortSignal,
// Carry the active task-local effort override for this request.
...this.getRuntimeThinkingEffortMetadata(),
// Include tools whenever they are present.
...(shouldIncludeTools
? {
Expand Down
Loading
Loading