diff --git a/.changeset/fn-7770-lane-thinking.md b/.changeset/fn-7770-lane-thinking.md
new file mode 100644
index 0000000000..f7627f1b20
--- /dev/null
+++ b/.changeset/fn-7770-lane-thinking.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: Add per-lane thinking effort overrides to Settings model lane dropdowns.
+category: feature
+dev: Adds optional lane thinking settings and runtime precedence task > lane > global default.
diff --git a/docs/settings-reference.md b/docs/settings-reference.md
index 102d19282f..35e7fdbde9 100644
--- a/docs/settings-reference.md
+++ b/docs/settings-reference.md
@@ -108,12 +108,16 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio
| `grokCliBinaryPath` | `string` | `undefined` | Optional global, machine-local Grok CLI executable override used by Settings → Authentication, status/enable validation, probes, and model discovery. Leave unset/blank to auto-detect `grok` on PATH. Invalid non-empty saves are rejected with bounded diagnostics. |
| `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. |
| `executionGlobalModelId` | `string` | `undefined` | Global baseline model ID for task execution. |
+| `executionGlobalThinkingLevel` | `ThinkingLevel` | `undefined` | Optional global execution-lane thinking override. Inherits `defaultThinkingLevel` when unset. |
| `planningGlobalProvider` | `string` | `undefined` | Global baseline provider for planning. Project `planningProvider` overrides this. |
| `planningGlobalModelId` | `string` | `undefined` | Global baseline model ID for planning. |
+| `planningGlobalThinkingLevel` | `ThinkingLevel` | `undefined` | Optional global planning-lane thinking override. Inherits `defaultThinkingLevel` when unset. |
| `validatorGlobalProvider` | `string` | `undefined` | Global baseline provider for validator/reviewer runs. Project `validatorProvider` overrides this. |
| `validatorGlobalModelId` | `string` | `undefined` | Global baseline model ID for validator/reviewer runs. |
+| `validatorGlobalThinkingLevel` | `ThinkingLevel` | `undefined` | Optional global reviewer-lane thinking override. Inherits `defaultThinkingLevel` when unset. |
| `titleSummarizerGlobalProvider` | `string` | `undefined` | Global baseline provider for title summarization. Project `titleSummarizerProvider` overrides this. |
| `titleSummarizerGlobalModelId` | `string` | `undefined` | Global baseline model ID for title summarization. |
+| `titleSummarizerGlobalThinkingLevel` | `ThinkingLevel` | `undefined` | Optional global summarization-lane thinking override. Inherits `defaultThinkingLevel` when unset. |
| `daemonToken` | `string` | `undefined` | Daemon authentication token (`fn_<32 hex chars>`) used by CLI clients. |
| `daemonPort` | `number` | `4040` | Port for daemon/serve mode binding. |
| `daemonHost` | `string` | `"127.0.0.1"` | Host for daemon/serve mode binding. Defaults to localhost only; pass `"0.0.0.0"` to expose on all interfaces. |
@@ -527,6 +531,7 @@ Default notes:
| `planningFallbackModelId` | `string` | `undefined` | Fallback model ID for planning. |
| `defaultProviderOverride` | `string` | `undefined` | Project-level override for global default provider baseline. |
| `defaultModelIdOverride` | `string` | `undefined` | Project-level override for global default model baseline. |
+| `defaultThinkingLevelOverride` | `ThinkingLevel` | `undefined` | Optional project default-lane thinking override used when a task does not set `thinkingLevel`; inherits `defaultThinkingLevel` when unset. |
| `executionProvider` | `string` | `undefined` | Provider for task execution agents. |
| `executionModelId` | `string` | `undefined` | Model ID for task execution agents. |
| `validatorProvider` | `string` | `undefined` | Provider for plan/code reviewers. |
@@ -643,6 +648,7 @@ GitLab configuration examples: leave both URL fields blank for GitLab.com (`http
| `useAiMergeCommitSummary` | `boolean` | `true` | Use AI-generated merge commit summaries (subject + bullet body + diff-stat) instead of raw step-commit subject lists. |
| `titleSummarizerProvider` | `string` | `undefined` | Provider for title summarization. |
| `titleSummarizerModelId` | `string` | `undefined` | Model ID for title summarization. |
+| `titleSummarizerThinkingLevel` | `ThinkingLevel` | `undefined` | Optional project summarization-lane thinking override. Inherits `titleSummarizerGlobalThinkingLevel` or `defaultThinkingLevel` when unset. |
| `titleSummarizerFallbackProvider` | `string` | `undefined` | Fallback provider for title summarization. |
| `titleSummarizerFallbackModelId` | `string` | `undefined` | Fallback model ID for title summarization. |
| `prTitlePromptInstructions` | `string` | `undefined` | Optional project guidance appended to the Create PR dialog's AI metadata system prompt for the generated PR title. Blank or whitespace-only values are treated as unset and keep the default prompt behavior. |
@@ -965,6 +971,8 @@ Short-lived token bounds are enforced server-side:
Fusion resolves task models through workflow-backed lane values first, then global lane defaults, then the project/global default model fallback. The common workflow lanes are stored as setting values on the project's default workflow and can be edited with dropdown controls from Settings -> Project Models -> Default workflow model lanes (persisted by the Settings modal's primary Save) or from workflow editor -> Settings -> Values for declared workflow lanes and fallbacks. General-scope fallback selection remains the global Fallback Model picker in Settings -> General Models.
+Settings model lanes can also carry optional thinking/reasoning effort overrides in the same model dropdown. Empty thinking values inherit the global `defaultThinkingLevel`; explicit lane values are cleared by the lane reset action. Runtime thinking precedence is task `thinkingLevel` > lane thinking override > `defaultThinkingLevel`, and the value still flows through pi.ts' existing thinking/reasoning-conflict fallback (Fusion retries without the explicit level when a provider rejects conflicting thinking parameters).
+
When the planning lane has neither `planningFallback*` nor a global `fallback*` pair configured, triage now derives an **implicit fallback** from the resolved project/global default (execution) model (FN-7719). This lets a retryable primary planner-model failure (e.g. a provider 404/429) recover via one distinct swap instead of permanently failing triage with "no fallback configured" — the operator's chosen primary planner lane is unchanged, and the implicit fallback is skipped when it would equal the primary model or when test mode is active.
Z.ai's built-in provider uses the existing `zai` auth entry / `ZAI_API_KEY` environment variable and includes `zai/glm-5.2` as a selectable model in the same dropdowns and workflow lane controls as the other built-in GLM models. If a pi extension also registers the `zai` provider, Fusion preserves the extension's models and re-adds any missing built-in Z.ai models so built-in GLM choices remain available.
diff --git a/packages/core/src/__tests__/settings-parity.test.ts b/packages/core/src/__tests__/settings-parity.test.ts
index 902829ef53..5ba9cd4012 100644
--- a/packages/core/src/__tests__/settings-parity.test.ts
+++ b/packages/core/src/__tests__/settings-parity.test.ts
@@ -100,6 +100,23 @@ describe("settings key parity", () => {
}
});
+
+ it("keeps model-lane thinking overrides in their owning settings scopes", () => {
+ expect(DEFAULT_PROJECT_SETTINGS.defaultThinkingLevelOverride).toBeUndefined();
+ expect(DEFAULT_PROJECT_SETTINGS.titleSummarizerThinkingLevel).toBeUndefined();
+ expect(DEFAULT_GLOBAL_SETTINGS.executionGlobalThinkingLevel).toBeUndefined();
+ expect(DEFAULT_GLOBAL_SETTINGS.planningGlobalThinkingLevel).toBeUndefined();
+ expect(DEFAULT_GLOBAL_SETTINGS.validatorGlobalThinkingLevel).toBeUndefined();
+ expect(DEFAULT_GLOBAL_SETTINGS.titleSummarizerGlobalThinkingLevel).toBeUndefined();
+
+ expect(isProjectSettingsKey("defaultThinkingLevelOverride")).toBe(true);
+ expect(isProjectSettingsKey("titleSummarizerThinkingLevel")).toBe(true);
+ expect(isGlobalSettingsKey("executionGlobalThinkingLevel")).toBe(true);
+ expect(isGlobalSettingsKey("planningGlobalThinkingLevel")).toBe(true);
+ expect(isGlobalSettingsKey("validatorGlobalThinkingLevel")).toBe(true);
+ expect(isGlobalSettingsKey("titleSummarizerGlobalThinkingLevel")).toBe(true);
+ });
+
it("defaults persisted thinking logs to disabled", () => {
expect(DEFAULT_GLOBAL_SETTINGS.persistAgentThinkingLog).toBe(false);
expect(DEFAULT_GLOBAL_SETTINGS.persistAgentThinkingLogPermanent).toBe(false);
diff --git a/packages/core/src/__tests__/store-settings.test.ts b/packages/core/src/__tests__/store-settings.test.ts
index 9e56c0f441..12ed57ced7 100644
--- a/packages/core/src/__tests__/store-settings.test.ts
+++ b/packages/core/src/__tests__/store-settings.test.ts
@@ -35,6 +35,48 @@ describe("TaskStore", () => {
expect(settings.defaultProvider).toBeUndefined();
expect(settings.defaultModelId).toBeUndefined();
});
+
+ it("round-trips per-lane thinking overrides and clears them with null-as-delete", async () => {
+ await harness.store().updateSettings({
+ defaultThinkingLevelOverride: "high",
+ titleSummarizerThinkingLevel: "low",
+ });
+ await harness.store().updateGlobalSettings({
+ executionGlobalThinkingLevel: "medium",
+ titleSummarizerGlobalThinkingLevel: "minimal",
+ });
+
+ let settings = await harness.store().getSettings();
+ expect(settings.defaultThinkingLevelOverride).toBe("high");
+ expect(settings.titleSummarizerThinkingLevel).toBe("low");
+ expect(settings.executionGlobalThinkingLevel).toBe("medium");
+ expect(settings.titleSummarizerGlobalThinkingLevel).toBe("minimal");
+
+ const scoped = await harness.store().getSettingsByScope();
+ expect(scoped.project.defaultThinkingLevelOverride).toBe("high");
+ expect(scoped.project.titleSummarizerThinkingLevel).toBe("low");
+ expect(scoped.global.executionGlobalThinkingLevel).toBe("medium");
+ expect(scoped.global.titleSummarizerGlobalThinkingLevel).toBe("minimal");
+
+ await harness.store().updateSettings({
+ // @ts-expect-error - null intentionally clears optional project settings.
+ defaultThinkingLevelOverride: null,
+ // @ts-expect-error - null intentionally clears optional project settings.
+ titleSummarizerThinkingLevel: null,
+ });
+ await harness.store().updateGlobalSettings({
+ // @ts-expect-error - null intentionally clears optional global settings.
+ executionGlobalThinkingLevel: null,
+ // @ts-expect-error - null intentionally clears optional global settings.
+ titleSummarizerGlobalThinkingLevel: null,
+ });
+
+ settings = await harness.store().getSettings();
+ expect(settings.defaultThinkingLevelOverride).toBeUndefined();
+ expect(settings.titleSummarizerThinkingLevel).toBeUndefined();
+ expect(settings.executionGlobalThinkingLevel).toBeUndefined();
+ expect(settings.titleSummarizerGlobalThinkingLevel).toBeUndefined();
+ });
});
describe("worktreeInitCommand setting", () => {
diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts
index 1162086f5c..b8b0e95174 100644
--- a/packages/core/src/settings-schema.ts
+++ b/packages/core/src/settings-schema.ts
@@ -182,6 +182,14 @@ export const DEFAULT_GLOBAL_SETTINGS = {
validatorGlobalModelId: undefined,
titleSummarizerGlobalProvider: undefined,
titleSummarizerGlobalModelId: undefined,
+ /*
+ FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ Global model lanes can override the default thinking effort independently. Undefined preserves the existing inheritance to `defaultThinkingLevel`.
+ */
+ executionGlobalThinkingLevel: undefined,
+ planningGlobalThinkingLevel: undefined,
+ validatorGlobalThinkingLevel: undefined,
+ titleSummarizerGlobalThinkingLevel: undefined,
// Daemon mode settings
daemonToken: undefined,
daemonPort: 4040,
@@ -400,6 +408,11 @@ export const DEFAULT_PROJECT_SETTINGS = {
// Project-level default override (NOT moved — stays project-scoped)
defaultProviderOverride: undefined,
defaultModelIdOverride: undefined,
+ /*
+ FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ Project model lanes can carry optional thinking overrides. Undefined means inherit the global default thinking effort; runtime precedence remains task > lane override > global default.
+ */
+ defaultThinkingLevelOverride: undefined,
modelPresets: [],
autoSelectModelPreset: false,
completionDocumentationMode: "off",
@@ -561,6 +574,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
// Title-summarizer model lanes stay project-scoped (not moved in U4).
titleSummarizerProvider: undefined,
titleSummarizerModelId: undefined,
+ titleSummarizerThinkingLevel: undefined,
titleSummarizerFallbackProvider: undefined,
titleSummarizerFallbackModelId: undefined,
prTitlePromptInstructions: undefined,
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 5427947c81..d87c191bf5 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -3470,6 +3470,14 @@ export interface GlobalSettings {
/** Global baseline AI model ID for title summarization.
* Must be set together with `titleSummarizerGlobalProvider`. */
titleSummarizerGlobalModelId?: string;
+ /** Optional global execution-lane thinking override. Inherits `defaultThinkingLevel` when unset. */
+ executionGlobalThinkingLevel?: ThinkingLevel;
+ /** Optional global planning-lane thinking override. Inherits `defaultThinkingLevel` when unset. */
+ planningGlobalThinkingLevel?: ThinkingLevel;
+ /** Optional global reviewer-lane thinking override. Inherits `defaultThinkingLevel` when unset. */
+ validatorGlobalThinkingLevel?: ThinkingLevel;
+ /** Optional global summarization-lane thinking override. Inherits `defaultThinkingLevel` when unset. */
+ titleSummarizerGlobalThinkingLevel?: ThinkingLevel;
/** The daemon authentication token (format: fn_<32 hex chars>).
* Used for authenticating CLI clients to the daemon server. */
daemonToken?: string;
@@ -4072,6 +4080,12 @@ export interface ProjectSettings {
/** Project-level override for the base default AI model ID.
* Must be set together with `defaultProviderOverride`. */
defaultModelIdOverride?: string;
+ /**
+ * FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ * Settings model lanes carry optional thinking overrides that inherit `defaultThinkingLevel` when unset. Runtime precedence is task `thinkingLevel` > lane thinking override > global `defaultThinkingLevel`.
+ * Optional project default-lane thinking override used when a task does not set its own thinking level.
+ */
+ defaultThinkingLevelOverride?: ThinkingLevel;
/** Project-level AI model provider for task execution (executor agent).
* This is the execution lane that overrides the global `executionGlobalProvider`.
* Must be set together with `executionModelId`. Falls back to
@@ -4601,6 +4615,8 @@ export interface ProjectSettings {
* Must be set together with `titleSummarizerProvider`. Falls back to planningModelId,
* then defaultModelId if not specified. */
titleSummarizerModelId?: string;
+ /** Optional project summarization-lane thinking override. Inherits `defaultThinkingLevel` when unset. */
+ titleSummarizerThinkingLevel?: ThinkingLevel;
/** Fallback model provider for title summarization. When unset, falls back to
* planning fallback, then global fallback. Must be set together with
* `titleSummarizerFallbackModelId`. */
diff --git a/packages/dashboard/app/__tests__/settings-sections.test.tsx b/packages/dashboard/app/__tests__/settings-sections.test.tsx
index 215c8f685b..50953fb72c 100644
--- a/packages/dashboard/app/__tests__/settings-sections.test.tsx
+++ b/packages/dashboard/app/__tests__/settings-sections.test.tsx
@@ -19,6 +19,7 @@ import { NotificationsSection } from "../components/settings/sections/Notificati
import { ExperimentalSection } from "../components/settings/sections/ExperimentalSection";
import { MovedSettingsStub } from "../components/settings/sections/MovedSettingsStub";
import { ProjectModelsSection } from "../components/settings/sections/ProjectModelsSection";
+import { GlobalModelsSection } from "../components/settings/sections/GlobalModelsSection";
import { PromptsSection } from "../components/settings/sections/PromptsSection";
import { SecretsSection } from "../components/settings/sections/SecretsSection";
import { WorktreesSection } from "../components/settings/sections/WorktreesSection";
@@ -46,16 +47,26 @@ vi.mock("../api", async (importOriginal) => {
};
});
vi.mock("../components/CustomModelDropdown", () => ({
- CustomModelDropdown: ({ id, label, value, onChange, menuWidth = "trigger" }: { id?: string; label: string; value?: string; onChange?: (value: string) => void; menuWidth?: "trigger" | "readable" }) => (
-
+ CustomModelDropdown: ({ id, label, value, onChange, menuWidth = "trigger", showThinkingLevel = false, thinkingLevel = "", onThinkingLevelChange, defaultThinkingLevel }: { id?: string; label: string; value?: string; onChange?: (value: string) => void; menuWidth?: "trigger" | "readable"; showThinkingLevel?: boolean; thinkingLevel?: string; onThinkingLevelChange?: (value: string) => void; defaultThinkingLevel?: string }) => (
+
+
+ {showThinkingLevel ? (
+
+ ) : null}
+
),
}));
@@ -285,6 +296,36 @@ describe("WorktreesSection", () => {
});
});
+describe("GlobalModelsSection", () => {
+ it("wires global model lane thinking overrides", () => {
+ const updateLaneThinkingValue = vi.fn();
+ render(
+ "medium"}
+ updateLaneThinkingValue={updateLaneThinkingValue}
+ resetLaneThinkingValue={vi.fn()}
+ favoriteProviders={[]}
+ favoriteModels={[]}
+ onToggleFavorite={vi.fn()}
+ onToggleModelFavorite={vi.fn()}
+ addToast={vi.fn()}
+ />,
+ );
+
+ expect(screen.getByTestId("mock-model-dropdown-global-execution-model")).toHaveAttribute("data-thinking-visible", "true");
+ expect(screen.getByTestId("mock-model-dropdown-global-execution-model")).toHaveAttribute("data-thinking-value", "medium");
+ expect(screen.getByTestId("mock-model-dropdown-global-execution-model")).toHaveAttribute("data-default-thinking", "low");
+ fireEvent.click(screen.getByTestId("mock-thinking-global-execution-model"));
+ expect(updateLaneThinkingValue).toHaveBeenCalledWith(expect.objectContaining({ laneId: "execution" }), "");
+ });
+});
+
describe("ProjectModelsSection", () => {
const models = {
modelLanes: [],
@@ -292,6 +333,9 @@ describe("ProjectModelsSection", () => {
getLaneValue: () => "",
updateLaneValue: vi.fn(),
resetLaneValue: vi.fn(),
+ getLaneThinkingValue: () => "",
+ updateLaneThinkingValue: vi.fn(),
+ resetLaneThinkingValue: vi.fn(),
availableModels: [],
modelsLoading: false,
favoriteProviders: [],
@@ -331,6 +375,42 @@ describe("ProjectModelsSection", () => {
expect(screen.getByTestId("mock-model-dropdown-preset-validator-model")).toHaveAttribute("data-menu-width", "readable");
});
+ it("wires project model lane thinking overrides and reset", () => {
+ const updateLaneThinkingValue = vi.fn();
+ const resetLaneThinkingValue = vi.fn();
+ const resetLaneValue = vi.fn();
+ render(
+ "high",
+ updateLaneThinkingValue,
+ resetLaneThinkingValue,
+ resetLaneValue,
+ availableModels: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }],
+ }}
+ addToast={vi.fn()}
+ />,
+ );
+
+ expect(screen.getByTestId("mock-model-dropdown-defaultModel")).toHaveAttribute("data-thinking-visible", "true");
+ expect(screen.getByTestId("mock-model-dropdown-defaultModel")).toHaveAttribute("data-thinking-value", "high");
+ expect(screen.getByTestId("mock-model-dropdown-defaultModel")).toHaveAttribute("data-default-thinking", "medium");
+
+ fireEvent.click(screen.getByTestId("mock-thinking-defaultModel"));
+ expect(updateLaneThinkingValue).toHaveBeenCalledWith(expect.objectContaining({ laneId: "default" }), "");
+
+ fireEvent.click(screen.getByText(/Reset/));
+ expect(resetLaneValue).toHaveBeenCalledWith(expect.objectContaining({ laneId: "default" }));
+ expect(resetLaneThinkingValue).toHaveBeenCalledWith(expect.objectContaining({ laneId: "default" }));
+ });
+
it("opts default workflow model lane dropdowns into readable menu width", async () => {
vi.mocked(fetchWorkflow).mockResolvedValueOnce({
id: "builtin:coding",
diff --git a/packages/dashboard/app/components/CustomModelDropdown.css b/packages/dashboard/app/components/CustomModelDropdown.css
index 34653c6e5b..1c5c1cc11f 100644
--- a/packages/dashboard/app/components/CustomModelDropdown.css
+++ b/packages/dashboard/app/components/CustomModelDropdown.css
@@ -351,3 +351,4 @@ FN-7760 requires the portaled model list to remain the touch-scroll owner on mob
width: min(360px, calc(100vw - 32px));
}
}
+
diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx
index 3c8f437413..b7475baf80 100644
--- a/packages/dashboard/app/components/SettingsModal.tsx
+++ b/packages/dashboard/app/components/SettingsModal.tsx
@@ -2404,8 +2404,10 @@ export function SettingsModal({
label: string;
globalProviderKey: keyof GlobalSettings;
globalModelKey: keyof GlobalSettings;
+ globalThinkingKey?: keyof GlobalSettings;
projectProviderKey: keyof Settings;
projectModelKey: keyof Settings;
+ projectThinkingKey?: keyof Settings;
helperText: string;
fallbackOrder: string;
}
@@ -2419,6 +2421,7 @@ export function SettingsModal({
globalModelKey: "defaultModelId",
projectProviderKey: "defaultProviderOverride",
projectModelKey: "defaultModelIdOverride",
+ projectThinkingKey: "defaultThinkingLevelOverride",
helperText: "Default AI model used for task execution when no per-task override is set.",
fallbackOrder: "Project override → Global default lane → Automatic resolution",
},
@@ -2427,6 +2430,7 @@ export function SettingsModal({
label: "Execution Model",
globalProviderKey: "executionGlobalProvider",
globalModelKey: "executionGlobalModelId",
+ globalThinkingKey: "executionGlobalThinkingLevel",
projectProviderKey: "executionProvider",
projectModelKey: "executionModelId",
helperText: "AI model used for task implementation (executor agent).",
@@ -2437,6 +2441,7 @@ export function SettingsModal({
label: "Planning Model",
globalProviderKey: "planningGlobalProvider",
globalModelKey: "planningGlobalModelId",
+ globalThinkingKey: "planningGlobalThinkingLevel",
projectProviderKey: "planningProvider",
projectModelKey: "planningModelId",
helperText: "AI model used for task planning.",
@@ -2447,6 +2452,7 @@ export function SettingsModal({
label: "Reviewer Model",
globalProviderKey: "validatorGlobalProvider",
globalModelKey: "validatorGlobalModelId",
+ globalThinkingKey: "validatorGlobalThinkingLevel",
projectProviderKey: "validatorProvider",
projectModelKey: "validatorModelId",
helperText: "AI model used for code and specification review.",
@@ -2457,8 +2463,10 @@ export function SettingsModal({
label: "Title and Git Commit Message Summarization Model",
globalProviderKey: "titleSummarizerGlobalProvider",
globalModelKey: "titleSummarizerGlobalModelId",
+ globalThinkingKey: "titleSummarizerGlobalThinkingLevel",
projectProviderKey: "titleSummarizerProvider",
projectModelKey: "titleSummarizerModelId",
+ projectThinkingKey: "titleSummarizerThinkingLevel",
helperText: "AI model used for auto-generating task titles and merge commit summaries.",
fallbackOrder: "Project override → Global summarization lane → Project planning lane → Project default lane → Global default lane → Automatic resolution",
},
@@ -2531,6 +2539,29 @@ export function SettingsModal({
}));
}
+
+ /**
+ * FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ * SettingsModal owns lane thinking persistence just like lane model persistence. Empty values clear the override so the engine falls back through task > lane > global default without sections writing settings directly.
+ */
+ function getLaneThinkingValue(lane: ModelLane, scope: "project" | "global" = "project"): string {
+ const key = scope === "project" ? lane.projectThinkingKey : lane.globalThinkingKey;
+ return key ? ((form[key as keyof Settings] as string | undefined) ?? "") : "";
+ }
+
+ function updateLaneThinkingValue(lane: ModelLane, level: string, scope: "project" | "global" = "project"): void {
+ const key = scope === "project" ? lane.projectThinkingKey : lane.globalThinkingKey;
+ if (!key) return;
+ setForm((f) => ({
+ ...f,
+ [key]: level || undefined,
+ }));
+ }
+
+ function resetLaneThinkingValue(lane: ModelLane, scope: "project" | "global" = "project"): void {
+ updateLaneThinkingValue(lane, "", scope);
+ }
+
const openOverlapPathPicker = useCallback((index: number) => {
setOverlapPathPickerIndex(index);
setOverlapPathPickerPath(".");
@@ -3218,6 +3249,9 @@ export function SettingsModal({
availableModels={availableModels}
modelsLoading={modelsLoading}
globalModelLanes={MODEL_LANES.filter((lane) => lane.laneId !== "default")}
+ getLaneThinkingValue={(lane) => getLaneThinkingValue(lane, "global")}
+ updateLaneThinkingValue={(lane, level) => updateLaneThinkingValue(lane, level, "global")}
+ resetLaneThinkingValue={(lane) => resetLaneThinkingValue(lane, "global")}
favoriteProviders={favoriteProviders}
favoriteModels={favoriteModels}
onToggleFavorite={handleToggleFavorite}
@@ -3267,6 +3301,9 @@ export function SettingsModal({
getLaneValue,
updateLaneValue,
resetLaneValue,
+ getLaneThinkingValue,
+ updateLaneThinkingValue,
+ resetLaneThinkingValue,
availableModels,
modelsLoading,
favoriteProviders,
diff --git a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx
index c428ecbcec..f45c27536c 100644
--- a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx
@@ -20,6 +20,9 @@ export interface GlobalModelsSectionProps extends SectionBaseProps {
modelsLoading: boolean;
/** Global model lanes (i.e. MODEL_LANES without the `default` lane). */
globalModelLanes: ModelLane[];
+ getLaneThinkingValue: (lane: ModelLane) => string;
+ updateLaneThinkingValue: (lane: ModelLane, level: string) => void;
+ resetLaneThinkingValue: (lane: ModelLane) => void;
favoriteProviders: string[];
favoriteModels: string[];
onToggleFavorite: (provider: string) => void;
@@ -27,7 +30,7 @@ export interface GlobalModelsSectionProps extends SectionBaseProps {
addToast: (message: string, type?: ToastType) => void;
projectId?: string;
}
-export function GlobalModelsSection({ scopeBanner, form, setForm, availableModels, modelsLoading, globalModelLanes, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, addToast, projectId, }: GlobalModelsSectionProps) {
+export function GlobalModelsSection({ scopeBanner, form, setForm, availableModels, modelsLoading, globalModelLanes, getLaneThinkingValue, updateLaneThinkingValue, resetLaneThinkingValue, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, addToast, projectId, }: GlobalModelsSectionProps) {
const { t } = useTranslation("app");
const selectedValue = form.defaultProvider && form.defaultModelId
? `${form.defaultProvider}/${form.defaultModelId}`
@@ -103,6 +106,7 @@ export function GlobalModelsSection({ scopeBanner, form, setForm, availableModel
const provider = form[lane.globalProviderKey as keyof Settings] as string | undefined;
const model = form[lane.globalModelKey as keyof Settings] as string | undefined;
const value = provider && model ? `${provider}/${model}` : "";
+ const thinkingValue = getLaneThinkingValue(lane);
return (
{
@@ -112,6 +116,7 @@ export function GlobalModelsSection({ scopeBanner, form, setForm, availableModel
[lane.globalProviderKey]: undefined,
[lane.globalModelKey]: undefined,
}));
+ resetLaneThinkingValue(lane);
return;
}
const slashIdx = selected.indexOf("/");
@@ -120,7 +125,7 @@ export function GlobalModelsSection({ scopeBanner, form, setForm, availableModel
[lane.globalProviderKey]: selected.slice(0, slashIdx),
[lane.globalModelKey]: selected.slice(slashIdx + 1),
}));
- }} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite}/>
+ }} placeholder={t("settings.globalModels.useDefault", "Use default")} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} showThinkingLevel={Boolean(lane.globalThinkingKey)} thinkingLevel={thinkingValue} onThinkingLevelChange={(level) => updateLaneThinkingValue(lane, level)} defaultThinkingLevel={form.defaultThinkingLevel}/>
{lane.helperText}
);
})}
diff --git a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
index 76f40c9293..967d675371 100644
--- a/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
+++ b/packages/dashboard/app/components/settings/sections/ProjectModelsSection.tsx
@@ -94,6 +94,9 @@ export interface ProjectModelsSectionModelProps {
getLaneValue: (lane: ModelLane) => string;
updateLaneValue: (lane: ModelLane, value: string) => void;
resetLaneValue: (lane: ModelLane) => void;
+ getLaneThinkingValue: (lane: ModelLane) => string;
+ updateLaneThinkingValue: (lane: ModelLane, level: string) => void;
+ resetLaneThinkingValue: (lane: ModelLane) => void;
availableModels: ModelInfo[];
modelsLoading: boolean;
favoriteProviders: string[];
@@ -129,7 +132,7 @@ export interface ProjectModelsSectionProps extends SectionBaseProps {
}
export function ProjectModelsSection({ scopeBanner, form, setForm, models, projectId, onOpenWorkflowSettings, registerWorkflowLaneSaver, }: ProjectModelsSectionProps) {
const { t } = useTranslation("app");
- const { modelLanes, getLaneStatus, getLaneValue, updateLaneValue, resetLaneValue, availableModels, modelsLoading, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, editingPresetId, setEditingPresetId, presetDraft, setPresetDraft, onSavePresetDraft, confirmDelete, } = models;
+ const { modelLanes, getLaneStatus, getLaneValue, updateLaneValue, resetLaneValue, getLaneThinkingValue, updateLaneThinkingValue, resetLaneThinkingValue, availableModels, modelsLoading, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, editingPresetId, setEditingPresetId, presetDraft, setPresetDraft, onSavePresetDraft, confirmDelete, } = models;
const presets = form.modelPresets || [];
const presetOptions = presets.map((preset) => ({ id: preset.id, name: preset.name }));
const inUsePresetIds = new Set(Object.values(form.defaultPresetBySize || {}).filter(Boolean));
@@ -296,7 +299,8 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
{projectModelLanes.map((lane) => {
const status = getLaneStatus(lane);
const value = getLaneValue(lane);
- const isOverridden = status === "overridden";
+ const thinkingValue = getLaneThinkingValue(lane);
+ const isOverridden = status === "overridden" || Boolean(thinkingValue);
const laneLabel = getProjectLaneLabel(lane);
return (
@@ -307,9 +311,9 @@ export function ProjectModelsSection({ scopeBanner, form, setForm, models, proje
- updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable"/>
+ updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} menuWidth="readable" showThinkingLevel={Boolean(lane.projectThinkingKey)} thinkingLevel={thinkingValue} onThinkingLevelChange={(level) => updateLaneThinkingValue(lane, level)} defaultThinkingLevel={form.defaultThinkingLevel}/>
- {isOverridden && (
)}
+ {isOverridden && (
)}
{getProjectLaneHelperText(lane)}{t("settings.projectModels.fallsBackTo", " Falls back to: ")}{lane.fallbackOrder}.
diff --git a/packages/dashboard/app/components/settings/sections/context.ts b/packages/dashboard/app/components/settings/sections/context.ts
index 17253ea239..b11a6540e7 100644
--- a/packages/dashboard/app/components/settings/sections/context.ts
+++ b/packages/dashboard/app/components/settings/sections/context.ts
@@ -23,8 +23,10 @@ export interface ModelLane {
label: string;
globalProviderKey: keyof GlobalSettings;
globalModelKey: keyof GlobalSettings;
+ globalThinkingKey?: keyof GlobalSettings;
projectProviderKey: keyof Settings;
projectModelKey: keyof Settings;
+ projectThinkingKey?: keyof Settings;
helperText: string;
fallbackOrder: string;
}
diff --git a/packages/engine/src/__tests__/agent-session-helpers.test.ts b/packages/engine/src/__tests__/agent-session-helpers.test.ts
index 8df218a726..c53b3f63de 100644
--- a/packages/engine/src/__tests__/agent-session-helpers.test.ts
+++ b/packages/engine/src/__tests__/agent-session-helpers.test.ts
@@ -3,11 +3,15 @@ import {
extractRuntimeHint,
extractRuntimeModel,
resolveExecutorSessionModel,
+ resolveExecutorThinkingLevel,
resolveHeartbeatSessionModels,
resolveImplicitPlanningFallbackModel,
resolveMergerSessionModel,
resolvePlanningSessionModel,
+ resolvePlanningThinkingLevel,
+ resolveTitleSummarizerThinkingLevel,
resolveValidatorSessionModel,
+ resolveValidatorThinkingLevel,
} from "../agent-session-helpers.js";
const { resolveRuntimeMock } = vi.hoisted(() => ({
@@ -22,6 +26,38 @@ vi.mock("../runtime-resolution.js", async () => {
};
});
+
+describe("resolve model-lane thinking levels", () => {
+ it("applies task > execution lane > project default lane > global default precedence", () => {
+ const settings = {
+ defaultThinkingLevel: "low",
+ defaultThinkingLevelOverride: "medium",
+ executionGlobalThinkingLevel: "high",
+ } as const;
+
+ expect(resolveExecutorThinkingLevel("xhigh", settings)).toBe("xhigh");
+ expect(resolveExecutorThinkingLevel(undefined, settings)).toBe("high");
+ expect(resolveExecutorThinkingLevel(undefined, { defaultThinkingLevelOverride: "medium", defaultThinkingLevel: "low" })).toBe("medium");
+ expect(resolveExecutorThinkingLevel(undefined, { defaultThinkingLevel: "low" })).toBe("low");
+ });
+
+ it("resolves planning, reviewer, and summarization lane overrides before the global default", () => {
+ expect(resolvePlanningThinkingLevel({ planningGlobalThinkingLevel: "minimal", defaultThinkingLevel: "high" })).toBe("minimal");
+ expect(resolveValidatorThinkingLevel(undefined, { validatorGlobalThinkingLevel: "medium", defaultThinkingLevel: "low" })).toBe("medium");
+ expect(resolveValidatorThinkingLevel("xhigh", { validatorGlobalThinkingLevel: "medium", defaultThinkingLevel: "low" })).toBe("xhigh");
+ expect(resolveTitleSummarizerThinkingLevel({
+ titleSummarizerThinkingLevel: "high",
+ titleSummarizerGlobalThinkingLevel: "medium",
+ defaultThinkingLevel: "low",
+ })).toBe("high");
+ expect(resolveTitleSummarizerThinkingLevel({
+ titleSummarizerGlobalThinkingLevel: "medium",
+ defaultThinkingLevelOverride: "minimal",
+ defaultThinkingLevel: "low",
+ })).toBe("medium");
+ });
+});
+
describe("extractRuntimeHint", () => {
it("returns undefined for undefined config", () => {
expect(extractRuntimeHint(undefined)).toBeUndefined();
diff --git a/packages/engine/src/agent-session-helpers.ts b/packages/engine/src/agent-session-helpers.ts
index 081a14a565..071d1ccf7e 100644
--- a/packages/engine/src/agent-session-helpers.ts
+++ b/packages/engine/src/agent-session-helpers.ts
@@ -23,6 +23,7 @@ import {
TEST_MODE_RESOLVED,
type ResolvedModelSelection,
type Settings,
+ type ThinkingLevel,
} from "@fusion/core";
import { resolveRuntime, buildRuntimeResolutionContext, isMockProviderId, type SessionPurpose } from "./runtime-resolution.js";
import { createLogger } from "./logger.js";
@@ -148,6 +149,65 @@ export function extractRuntimeModel(
};
}
+
+function firstThinkingLevel(...levels: Array): string | undefined {
+ for (const level of levels) {
+ if (typeof level === "string" && level.trim().length > 0) {
+ return level.trim();
+ }
+ }
+ return undefined;
+}
+
+/**
+ * FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ * Model-lane thinking overrides must resolve through the same session option (`defaultThinkingLevel`) pi.ts already guards with the thinking/reasoning conflict fallback. Keep task-level thinking first, then the lane override, then the global default.
+ */
+export function resolveExecutorThinkingLevel(
+ taskThinkingLevel: ThinkingLevel | string | undefined,
+ settings: Partial | undefined,
+): string | undefined {
+ return firstThinkingLevel(
+ taskThinkingLevel,
+ settings?.executionGlobalThinkingLevel,
+ settings?.defaultThinkingLevelOverride,
+ settings?.defaultThinkingLevel,
+ );
+}
+
+export function resolvePlanningThinkingLevel(settings: Partial | undefined): string | undefined {
+ return firstThinkingLevel(settings?.planningGlobalThinkingLevel, settings?.defaultThinkingLevelOverride, settings?.defaultThinkingLevel);
+}
+
+export function resolveValidatorThinkingLevel(
+ taskThinkingLevel: ThinkingLevel | string | undefined,
+ settings: Partial | undefined,
+): string | undefined {
+ return firstThinkingLevel(taskThinkingLevel, settings?.validatorGlobalThinkingLevel, settings?.defaultThinkingLevelOverride, settings?.defaultThinkingLevel);
+}
+
+export function resolveTitleSummarizerThinkingLevel(settings: Partial | undefined): string | undefined {
+ return firstThinkingLevel(
+ settings?.titleSummarizerThinkingLevel,
+ settings?.titleSummarizerGlobalThinkingLevel,
+ settings?.defaultThinkingLevelOverride,
+ settings?.defaultThinkingLevel,
+ );
+}
+
+/**
+ * FNXC:Settings-ThinkingLevel 2026-07-10-00:00:
+ * `resolveMergerSessionModel` intentionally resolves the merger's model from the
+ * project/global DEFAULT lane, not the title-summarizer lane. The thinking level
+ * threaded into merger sessions (mutating merge agent, stash-conflict resolver,
+ * commit agent, PR-response agent) must follow that same default-lane precedence
+ * so a `titleSummarizerThinkingLevel` override (meant only for title/commit-message
+ * summarization sessions) does not leak into full merge-agent runs.
+ */
+export function resolveMergerThinkingLevel(settings: Partial | undefined): string | undefined {
+ return firstThinkingLevel(settings?.defaultThinkingLevelOverride, settings?.defaultThinkingLevel);
+}
+
function hasCompleteRuntimeModel(
model: ResolvedModelSelection,
): model is { provider: string; modelId: string } {
diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts
index 54f1a82483..d15f1a33eb 100644
--- a/packages/engine/src/executor.ts
+++ b/packages/engine/src/executor.ts
@@ -84,6 +84,8 @@ import {
createResolvedAgentSession,
extractRuntimeHint,
resolveExecutorSessionModel,
+ resolveExecutorThinkingLevel,
+ resolveValidatorThinkingLevel,
} from "./agent-session-helpers.js";
import { buildSessionSkillContext } from "./session-skill-context.js";
import type { SkillSelectionContext } from "./skill-resolver.js";
@@ -6658,7 +6660,7 @@ export class TaskExecutor {
defaultModelId: settings.defaultModelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: detail.thinkingLevel ?? settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveExecutorThinkingLevel(detail.thinkingLevel, settings),
taskValidatorProvider: detail.validatorModelProvider,
taskValidatorModelId: detail.validatorModelId,
projectValidatorProvider: settings.validatorProvider,
@@ -10541,7 +10543,7 @@ export class TaskExecutor {
);
const executorFallbackProvider = settings.fallbackProvider;
const executorFallbackModelId = settings.fallbackModelId;
- const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
+ const executorThinkingLevel = resolveExecutorThinkingLevel(detail.thinkingLevel, settings);
// U1 telemetry: now that the session model/provider/node are resolved,
// give the agent logger the context it needs to emit usage_events tool
@@ -13560,7 +13562,7 @@ export class TaskExecutor {
defaultModelId: settings.defaultModelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: latestDetailForReview.thinkingLevel ?? settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveValidatorThinkingLevel(latestDetailForReview.thinkingLevel, settings),
// Task-level validator override (from task)
taskValidatorProvider: latestDetailForReview.validatorModelProvider,
taskValidatorModelId: latestDetailForReview.validatorModelId,
@@ -13988,7 +13990,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
onToolEnd: logger.onToolEnd,
defaultProvider: executorProvider,
defaultModelId: executorModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveExecutorThinkingLevel(task.thinkingLevel, settings),
runAuditor: createRunAuditor(this.store, this.getRunContextFor(task.id)),
settings,
taskEnv: extraEnv,
@@ -15078,7 +15080,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
defaultModelId: modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveExecutorThinkingLevel(task.thinkingLevel, settings),
runAuditor: createRunAuditor(this.store, this.getRunContextFor(task.id)),
settings,
taskEnv: stepEnv,
@@ -15096,7 +15098,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
const workflowModelDetails = formatModelMarkerDetails(
describeModel(session),
- settings.defaultThinkingLevel,
+ resolveExecutorThinkingLevel(task.thinkingLevel, settings),
[
useOverride && attemptLabel === "primary" ? "workflow step override" : "",
attemptLabel === "fallback" ? "fallback after timeout" : "",
diff --git a/packages/engine/src/merger-ai.ts b/packages/engine/src/merger-ai.ts
index 70a0fb2be6..b4b69dda3a 100644
--- a/packages/engine/src/merger-ai.ts
+++ b/packages/engine/src/merger-ai.ts
@@ -59,7 +59,7 @@ import { selectUserCommentsForAgentContext } from "./agent-user-comments.js";
import { resolveTaskWorkingBranch } from "./worktree-names.js";
import { resolveIntegrationBranch } from "./integration-branch.js";
import { advanceIntegrationBranchRef } from "./merger-ref-update-advance.js";
-import { createResolvedAgentSession, resolveMergerSessionModel } from "./agent-session-helpers.js";
+import { createResolvedAgentSession, resolveMergerSessionModel, resolveMergerThinkingLevel, resolveValidatorThinkingLevel } from "./agent-session-helpers.js";
import { promptWithFallback } from "./pi.js";
import { AgentLogger } from "./agent-logger.js";
import { withRateLimitRetry } from "./rate-limit-retry.js";
@@ -268,7 +268,7 @@ function makeMutatingAgent(store: TaskStore, settings: Settings, taskId: string,
defaultModelId: model.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: audit,
settings,
// FNXC:McpConfig 2026-06-25-22:48: merger-ai is the production merge path, so the mutating agent resolves enabled MCP servers at session creation and relies on the shared runtime guard for unsupported providers.
@@ -296,6 +296,12 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
// that lane resolves to nothing.
const validator = resolveValidatorSettingsModel(settings);
const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings);
+ // FNXC:Settings-ThinkingLevel 2026-07-10-00:00: The review agent's model falls back
+ // between the validator lane and the merger default lane, so its thinking level
+ // must follow the same lane it actually resolved a model from.
+ const reviewThinkingLevel = validator.provider && validator.modelId
+ ? resolveValidatorThinkingLevel(undefined, settings)
+ : resolveMergerThinkingLevel(settings);
let captured = "";
const logger = new AgentLogger({
store,
@@ -327,7 +333,7 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
defaultModelId: model.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: reviewThinkingLevel,
runAuditor: audit,
settings,
// FNXC:McpConfig 2026-06-25-22:48: The production merge reviewer receives the same materialized MCP set as the mutating merge agent, preserving all-lane forwarding without logging server contents.
diff --git a/packages/engine/src/merger.ts b/packages/engine/src/merger.ts
index 099dc53605..cc66412989 100644
--- a/packages/engine/src/merger.ts
+++ b/packages/engine/src/merger.ts
@@ -109,7 +109,7 @@ import { evaluateAutoMergeFactProviders } from "./auto-merge-fact-providers.js";
import { resolveMergePolicy, type MergeFileScopeMode } from "./merge-trait.js";
import { describeModel, promptWithFallback } from "./pi.js";
import { accumulateSessionTokenUsage } from "./session-token-usage.js";
-import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel } from "./agent-session-helpers.js";
+import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel, resolveMergerThinkingLevel } from "./agent-session-helpers.js";
import { createFallbackModelObserver } from "./fallback-model-observer.js";
import { buildSessionSkillContext } from "./session-skill-context.js";
import { resolveMcpServersForStore } from "./mcp-resolution.js";
@@ -1969,7 +1969,7 @@ Do not refactor, rename broadly, or make opportunistic improvements.
defaultModelId: mergerSessionModel.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: createRunAuditor(store, {
runId: mergeRunContext?.runId ?? generateSyntheticRunId("merge", taskId),
agentId: mergeRunContext?.agentId ?? "merger",
@@ -3163,7 +3163,7 @@ ${fileList}
defaultModelId: mergerSessionModel.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: createRunAuditor(store, {
runId: generateSyntheticRunId("merge", taskId),
agentId: "merger",
@@ -3580,7 +3580,7 @@ ${fileList}
defaultModelId: mergerSessionModel.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: createRunAuditor(store, {
runId: generateSyntheticRunId("merge", taskId),
agentId: "merger",
@@ -7084,7 +7084,7 @@ You are assisting with a paused \`git pull --rebase\`.
defaultModelId: mergerSessionModel.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: createRunAuditor(store, {
runId: generateSyntheticRunId("merge", taskId),
agentId: "merger",
@@ -12039,7 +12039,7 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
defaultModelId: mergerSessionModel.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
runAuditor: createRunAuditor(store, {
runId: generateSyntheticRunId("merge", taskId),
agentId: "merger",
diff --git a/packages/engine/src/pr-response-run-ops.ts b/packages/engine/src/pr-response-run-ops.ts
index fc4ef07183..f9dcab1e04 100644
--- a/packages/engine/src/pr-response-run-ops.ts
+++ b/packages/engine/src/pr-response-run-ops.ts
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
import { promisify } from "node:util";
import type { PrEntity, Settings, TaskStore } from "@fusion/core";
import { resolveAgentPrompt } from "@fusion/core";
-import { createResolvedAgentSession, resolveMergerSessionModel } from "./agent-session-helpers.js";
+import { createResolvedAgentSession, resolveMergerSessionModel, resolveMergerThinkingLevel } from "./agent-session-helpers.js";
import { resolveMcpServersForStore } from "./mcp-resolution.js";
import { promptWithFallback } from "./pi.js";
import { withRateLimitRetry } from "./rate-limit-retry.js";
@@ -125,7 +125,7 @@ export function makePrResponseAgentRunner(
defaultModelId: model.modelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveMergerThinkingLevel(settings),
settings,
taskId,
mcpServers,
diff --git a/packages/engine/src/step-session-executor.ts b/packages/engine/src/step-session-executor.ts
index d3909ac22a..e2674cfac0 100644
--- a/packages/engine/src/step-session-executor.ts
+++ b/packages/engine/src/step-session-executor.ts
@@ -26,6 +26,7 @@ import {
describeAgentModel,
promptWithAutoRetry,
resolveExecutorSessionModel,
+ resolveExecutorThinkingLevel,
} from "./agent-session-helpers.js";
import type { AgentActionGateContext } from "./agent-action-gate.js";
import type { SkillSelectionContext } from "./skill-resolver.js";
@@ -1344,7 +1345,7 @@ Follow instructions precisely and avoid unrelated changes.`,
defaultModelId: executorModelId,
fallbackProvider: settings.fallbackProvider,
fallbackModelId: settings.fallbackModelId,
- defaultThinkingLevel: taskDetail.thinkingLevel ?? settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolveExecutorThinkingLevel(taskDetail.thinkingLevel, settings),
runAuditor: createRunAuditor(this.store, {
runId: generateSyntheticRunId("workflow-step", taskDetail.id),
// Column-agent attribution (U4): the effective column agent is the
diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts
index b82ef1fae2..b976415ad8 100644
--- a/packages/engine/src/triage.ts
+++ b/packages/engine/src/triage.ts
@@ -89,6 +89,7 @@ import {
extractRuntimeHint,
resolveImplicitPlanningFallbackModel,
resolvePlanningSessionModel,
+ resolvePlanningThinkingLevel,
} from "./agent-session-helpers.js";
import { mergeEffectiveSettings } from "./effective-settings.js";
import { detectDanglingTaskDocReferences, formatDanglingDiagnostic } from "./spec-validation/task-document-references.js";
@@ -1160,7 +1161,7 @@ export class TriageProcessor {
fallbackModelId: hasExplicitPlanningFallback
? settings.planningFallbackModelId
: (hasExplicitGlobalFallback ? settings.fallbackModelId : implicitPlanningFallback.modelId),
- defaultThinkingLevel: settings.defaultThinkingLevel,
+ defaultThinkingLevel: resolvePlanningThinkingLevel(settings),
runAuditor,
settings,
// FNXC:McpConfig 2026-06-25-23:17: Primary triage planning is an AI lane, so it receives the store-resolved MCP set while the pi runtime-support guard decides whether to forward it without logging secret material.
@@ -1178,7 +1179,7 @@ export class TriageProcessor {
}),
});
- const modelDesc = formatModelMarkerDetails(describeModel(session), settings.defaultThinkingLevel);
+ const modelDesc = formatModelMarkerDetails(describeModel(session), resolvePlanningThinkingLevel(settings));
planLog.log(`${task.id}: using model ${modelDesc}`);
await this.store.logEntry(task.id, `Triage using model: ${modelDesc}`);
await this.store.appendAgentLog(
diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json
index 05dad8c1ef..aa73095b4d 100644
--- a/packages/i18n/locales/en/app.json
+++ b/packages/i18n/locales/en/app.json
@@ -3967,7 +3967,8 @@
"medium": "Medium",
"minimal": "Minimal",
"off": "Off",
- "xhigh": "Very High"
+ "xhigh": "Very High",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "Select executor model…",
diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json
index aa98d5cdcc..e2addb9c68 100644
--- a/packages/i18n/locales/es/app.json
+++ b/packages/i18n/locales/es/app.json
@@ -3957,7 +3957,8 @@
"medium": "Medio",
"minimal": "Mínimo",
"off": "Desactivado",
- "xhigh": "Muy alto"
+ "xhigh": "Muy alto",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "Seleccione modelo de ejecución…",
diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json
index 8862e1a847..378989540d 100644
--- a/packages/i18n/locales/fr/app.json
+++ b/packages/i18n/locales/fr/app.json
@@ -3957,7 +3957,8 @@
"medium": "Moyen",
"minimal": "Minimal",
"off": "Désactivé",
- "xhigh": "Très élevé"
+ "xhigh": "Très élevé",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "Sélectionnez un modèle d'exécuteur…",
diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json
index 8a69694d1b..73b8431602 100644
--- a/packages/i18n/locales/ko/app.json
+++ b/packages/i18n/locales/ko/app.json
@@ -3957,7 +3957,8 @@
"medium": "중간",
"minimal": "최소",
"off": "끔",
- "xhigh": "매우 높음"
+ "xhigh": "매우 높음",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "실행자 모델 선택…",
diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json
index 830163b1c3..6e202b334d 100644
--- a/packages/i18n/locales/zh-CN/app.json
+++ b/packages/i18n/locales/zh-CN/app.json
@@ -3957,7 +3957,8 @@
"medium": "中等",
"minimal": "最小",
"off": "关闭",
- "xhigh": "极高"
+ "xhigh": "极高",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "选择执行器模型…",
diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json
index e23b70d3eb..a20f05e378 100644
--- a/packages/i18n/locales/zh-TW/app.json
+++ b/packages/i18n/locales/zh-TW/app.json
@@ -3957,7 +3957,8 @@
"medium": "中等",
"minimal": "最小",
"off": "關閉",
- "xhigh": "極高"
+ "xhigh": "極高",
+ "defaultWithLevel": "Default ({{level}})"
},
"placeholders": {
"selectExecutor": "選擇執行器模型…",
diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts
index 1714e43822..77904665e4 100644
--- a/packages/i18n/src/resources.d.ts
+++ b/packages/i18n/src/resources.d.ts
@@ -3937,6 +3937,8 @@ export default interface Resources {
"preset": "Preset",
"reviewerModel": "Reviewer Model",
"reviewerPlaceholder": "Select reviewer model…",
+ "thinkingDefault": "Default ({{level}})",
+ "thinkingModel": "Thinking",
"title": "Select Models",
"useDefault": "Use default",
"usingDefault": "Using default"
@@ -3980,6 +3982,7 @@ export default interface Resources {
"noResults": "No models match '{{filter}}'",
"options": {
"default": "Default",
+ "defaultWithLevel": "Default ({{level}})",
"high": "High",
"low": "Low",
"medium": "Medium",