You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/20 09:20:47 UTC

(maka) branch main updated: fix(desktop): surface WorkHub failures and use per-model thinking defaults (#5530)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 64f5e83de6 fix(desktop): surface WorkHub failures and use per-model thinking defaults (#5530)
64f5e83de6 is described below

commit 64f5e83de6f70105f470f243ac94c7b06c5ebf34
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Sep 20 17:20:42 2026 +0800

    fix(desktop): surface WorkHub failures and use per-model thinking defaults (#5530)
    
    * fix(desktop): clarify WorkHub failures and thinking defaults
    
    Generated-by: Codex
    
    * fix(runtime-host): advance compatibility epoch
    
    Remove low-signal duplicate tests while updating the protocol boundary.
    
    Generated-by: Codex
    
    * refactor(desktop): preserve new-task thinking intent
    
    Keep the resolved model default as presentation state while sending the untouched provider-default/model-default intent to the Host.
    
    Generated-by: Codex
    
    * fix(desktop): keep thinking intent handoff minimal
    
    Pass the raw three-state thinking intent through the existing IPC normalization without adding conditional AppShell debt.
    
    Generated-by: Codex
---
 apps/desktop/renderer-architecture.json            |  9 +--
 .../main/runtime-host-session-catalog-ipc-main.ts  |  1 +
 apps/desktop/src/main/session-local-service.ts     |  4 +-
 .../desktop/src/renderer/app-shell-chat-actions.ts |  5 +-
 apps/desktop/src/renderer/app-shell.tsx            |  5 +-
 .../provider-add-model-dialog.tsx                  |  1 +
 .../provider-capability-editor.tsx                 | 36 ++++++++-
 .../connection-settings/settings-provider-copy.ts  |  9 +++
 .../features/task-entry/ui/task-entry-provider.tsx |  3 +-
 .../features/workhub/locales/workhub-live-copy.ts  |  6 +-
 .../features/workhub/model/turn-presentation.ts    | 43 +++++++++++
 .../features/workhub/ui/workhub-conversation.tsx   |  2 +
 .../renderer/features/workhub/ui/workhub-root.tsx  |  6 +-
 .../renderer/locales/settings-preferences-copy.ts  | 16 ++--
 .../renderer/settings/general-settings-page.tsx    | 70 +----------------
 .../settings/provider-connection-detail.tsx        |  1 +
 apps/desktop/src/renderer/use-shell-appearance.ts  |  5 --
 apps/desktop/src/renderer/use-shell-chat-model.ts  | 45 ++++++-----
 apps/desktop/stories/workhub.stories.tsx           |  1 +
 packages/core/src/__tests__/model-thinking.test.ts | 22 ++++++
 .../src/__tests__/runtime-policy-codec.test.ts     |  3 +-
 packages/core/src/__tests__/settings.test.ts       |  2 +-
 packages/core/src/chat-model-choice.ts             |  5 ++
 packages/core/src/model-catalog.ts                 | 13 +++-
 packages/core/src/model-thinking.ts                | 23 ++++++
 packages/core/src/runtime-inputs.ts                |  4 +-
 packages/core/src/runtime-policy.ts                |  1 +
 .../src/runtime-policy/connection-catalog-codec.ts |  7 ++
 .../runtime-policy/model-catalog-entry-codec.ts    | 22 +++++-
 packages/core/src/settings.ts                      | 15 +---
 .../__tests__/session-catalog-coordinator.test.ts  | 87 ++++++++++++++++++++++
 .../src/__tests__/session-catalog-protocol.test.ts | 22 ++++++
 packages/runtime-host/src/protocol/index.ts        |  2 +-
 .../runtime-host/src/protocol/session-catalog.ts   |  5 +-
 .../src/server/session-catalog-coordinator.ts      | 48 +++++++++---
 35 files changed, 389 insertions(+), 160 deletions(-)

diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json
index 8990fb0a69..d5f458bd1f 100644
--- a/apps/desktop/renderer-architecture.json
+++ b/apps/desktop/renderer-architecture.json
@@ -337,7 +337,7 @@
           "@maka/ui": 1
         },
         "importSpecifiers": 10,
-        "nonTriviaTokens": 3603
+        "nonTriviaTokens": 3596
       },
       "src/renderer/app-shell-chrome-actions.tsx": {
         "importDeclarations": 4,
@@ -861,7 +861,7 @@
           "react": 1
         },
         "importSpecifiers": 100,
-        "nonTriviaTokens": 13019
+        "nonTriviaTokens": 13003
       },
       "src/renderer/use-app-shell-composer-quotes.ts": {
         "importDeclarations": 0,
@@ -2534,8 +2534,7 @@
           "./use-action-guard": 1,
           "./use-optimistic-settings-draft": 1,
           "@maka/core/chat-model-choice": 1,
-          "@maka/core/model-thinking": 1,
-          "@maka/ui": 2,
+          "@maka/ui": 1,
           "react": 1
         }
       },
@@ -4047,7 +4046,7 @@
         },
         "environmentCapabilities": {},
         "hookCalls": {
-          "useState": 6
+          "useState": 5
         },
         "lifecycleMethods": {},
         "unresolvedDependencies": 0,
diff --git a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
index a221e8fc03..b0a67aea14 100644
--- a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts
@@ -387,6 +387,7 @@ function normalizeCreateThinkingLevel(
   value: unknown,
 ): Pick<SessionCreateInput, 'thinkingLevel'> | Record<string, never> {
   if (value === undefined) return {};
+  if (value === null) return { thinkingLevel: null };
   if (!isThinkingLevel(value)) throw new Error(`Invalid thinking level: ${String(value)}`);
   return { thinkingLevel: value };
 }
diff --git a/apps/desktop/src/main/session-local-service.ts b/apps/desktop/src/main/session-local-service.ts
index 8c9e289d38..7470afe20f 100644
--- a/apps/desktop/src/main/session-local-service.ts
+++ b/apps/desktop/src/main/session-local-service.ts
@@ -553,7 +553,9 @@ export function registerDesktopSessionLocalIpc(deps: {
         permissionMode: creation.permissionMode ?? 'bypass',
         collaborationMode: creation.collaborationMode,
         orchestrationMode: creation.orchestrationMode,
-        thinkingLevel: creation.thinkingLevel,
+        ...(creation.thinkingLevel === null
+          ? {}
+          : { thinkingLevel: creation.thinkingLevel }),
       };
       service.store.saveSession(target.partition, summary, creation);
       deps.changed(target.scope, creation.sessionId);
diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts
index 8dff1f4450..d21263b9cd 100644
--- a/apps/desktop/src/renderer/app-shell-chat-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts
@@ -78,7 +78,7 @@ type PendingNewChatModel = {
   model: string;
 } | null;
 
-type PendingNewChatThinkingLevel = ThinkingLevel | null;
+type PendingNewChatThinkingLevel = ThinkingLevel | null | undefined;
 type DesktopNewTaskTarget = DesktopBridge.DesktopNewTaskTarget;
 type DesktopSessionSummary = DesktopBridge.DesktopSessionSummary;
 type InteractionFormResponse = Parameters<
@@ -183,6 +183,7 @@ export function createAppShellChatActions(deps: {
   ) => void;
   toastApi: ToastApi;
   newChatModel: PendingNewChatModel;
+  /** Undefined applies the Host's model default; null explicitly keeps the provider default. */
   pendingNewChatThinkingLevel: PendingNewChatThinkingLevel;
   /**
    * The user's explicit choice for this draft, or undefined when they made
@@ -387,7 +388,7 @@ export function createAppShellChatActions(deps: {
                 model: newChatModel.model,
               }
             : {}),
-          ...(pendingNewChatThinkingLevel ? { thinkingLevel: pendingNewChatThinkingLevel } : {}),
+          thinkingLevel: pendingNewChatThinkingLevel,
           ...(newChatPermissionChoice ? { permissionMode: newChatPermissionChoice } : {}),
           collaborationMode: newChatCollaborationMode,
           orchestrationMode: newChatOrchestrationMode,
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index af68edb12c..22ec2bf498 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -743,9 +743,9 @@ function AppShellContent({
     newChatModelLabel,
     newChatThinkingLevels,
     newChatThinkingLevel,
+    pendingNewChatThinkingLevel,
     composerSupportsVision,
     setPendingNewChatModel,
-    pendingNewChatThinkingLevel,
     setPendingNewChatThinkingLevel,
     sessionHealthNotice,
   } = useShellChatModel({
@@ -762,7 +762,6 @@ function AppShellContent({
     sessionHealthSession: activeSession,
     persistedComposerDefaults,
     usePersistedComposerDefaults: modelSettingsOwnsComposerHost,
-    defaultThinkingLevel: taskEntry.selectors.selectedHost?.chatDefaults.thinkingLevel,
     connectionSnapshotReady: activeId
       ? sessionHostConnections.projection.status === 'ready'
       : true,
@@ -1426,7 +1425,7 @@ function AppShellContent({
     showModelSetupToast,
     toastApi,
     newChatModel: newChatModel ?? null,
-    pendingNewChatThinkingLevel: newChatThinkingLevel ?? null,
+    pendingNewChatThinkingLevel,
     newChatPermissionChoice: newTaskPermissionChoice,
     clearNewChatPermissionChoice: clearNewTaskPermissionChoice,
     newChatCollaborationMode: newChatPlanModeActive ? 'plan' : 'agent',
diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx b/apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx
index cf312a9318..6dbed62d7a 100644
--- a/apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx
+++ b/apps/desktop/src/renderer/features/connection-settings/provider-add-model-dialog.tsx
@@ -127,6 +127,7 @@ export function AddModelDialog(props: {
         disabled={isSaving}
         showsFastMode={supportsRelayFastServiceTier(props.providerType, trimmedId)}
         defaultVision={undefined}
+        thinkingLevels={profile.thinkingLevels ?? []}
         onContextWindowInput={setContextWindowInput}
       >
         <TextInput
diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-capability-editor.tsx b/apps/desktop/src/renderer/features/connection-settings/provider-capability-editor.tsx
index 082bf55aee..9a18853c04 100644
--- a/apps/desktop/src/renderer/features/connection-settings/provider-capability-editor.tsx
+++ b/apps/desktop/src/renderer/features/connection-settings/provider-capability-editor.tsx
@@ -41,6 +41,7 @@ export function CapabilityEditor(props: {
   onNumericInput(field: 'inputLimit' | 'compactionThreshold' | 'maxOutputTokens', input: string): void;
   defaultContextWindow?: number;
   defaultInputLimit?: number;
+  thinkingLevels: readonly ThinkingLevel[];
   limitsConflict?: boolean;
   contextWindowError?: string;
   disabled: boolean;
@@ -63,6 +64,13 @@ export function CapabilityEditor(props: {
       (DECLARABLE_RELAY_THINKING_LEVELS as readonly ThinkingLevel[]).includes(level) ||
       draftLevels.includes(level),
   );
+  const defaultThinkingLevels = props.isRelay && declared?.thinkingLevels !== undefined
+    ? declared.thinkingLevels
+    : props.thinkingLevels;
+  const defaultThinkingLevel = declared?.defaultThinkingLevel !== undefined &&
+    defaultThinkingLevels.includes(declared.defaultThinkingLevel)
+    ? declared.defaultThinkingLevel
+    : '';
   return (
     <FormLayout direction="vertical" defaultOptionality="optional">
       {props.children}
@@ -166,10 +174,15 @@ export function CapabilityEditor(props: {
                 aria-label={`${modelId} ${level}`}
                 value={draftLevels.includes(level)}
                 onChange={(checked) => {
+                  const thinkingLevels = checked
+                    ? [...draftLevels, level]
+                    : draftLevels.filter((existing) => existing !== level);
                   props.onChange({
-                    thinkingLevels: checked
-                      ? [...draftLevels, level]
-                      : draftLevels.filter((existing) => existing !== level),
+                    thinkingLevels,
+                    ...(declared?.defaultThinkingLevel !== undefined &&
+                    !thinkingLevels.includes(declared.defaultThinkingLevel)
+                      ? { defaultThinkingLevel: undefined }
+                      : {}),
                   });
                 }}
                 isDisabled={props.disabled}
@@ -178,6 +191,23 @@ export function CapabilityEditor(props: {
           </DropdownMenu>
         </Field>
       )}
+      {defaultThinkingLevels.length > 0 && (
+        <Selector
+          label={copy.defaultThinkingLevel}
+          labelTooltip={copy.defaultThinkingLevelHelp}
+          size="sm"
+          width="100%"
+          options={[
+            { value: '', label: copy.providerDefaultThinking },
+            ...defaultThinkingLevels.map((level) => ({ value: level, label: level })),
+          ]}
+          value={defaultThinkingLevel}
+          onChange={(value) => props.onChange({
+            defaultThinkingLevel: value === '' ? undefined : value as ThinkingLevel,
+          })}
+          isDisabled={props.disabled}
+        />
+      )}
       {props.showsFastMode && (
         <Selector
           label={copy.fastMode}
diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
index 64f29337d5..0661ccc60a 100644
--- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
+++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts
@@ -45,6 +45,9 @@ const zhCapabilitiesCopy = {
   thinkingEffortHelp: '勾选服务商支持的强度,供对话时选择。留空时按模型资料设置。',
   thinkingUndeclared: '自动',
   thinkingSelectedCount: (count: number) => `已选 ${count} 项`,
+  defaultThinkingLevel: '默认思考级别',
+  defaultThinkingLevelHelp: '新任务使用此模型时采用的思考级别;任务内仍可单独切换。',
+  providerDefaultThinking: '服务商默认',
   visionInput: '图片识别',
   visionInputHelp: '此服务商的模型是否支持图片。自动按模型资料判断,缺少资料时不发送图片。',
   visionDefaultOption: (supported: boolean | undefined) =>
@@ -74,6 +77,9 @@ const zhTwCapabilitiesCopy = {
   thinkingEffortHelp: '勾選服務商支援的強度,供對話時選擇。留空時依模型資料設定。',
   thinkingUndeclared: '自動',
   thinkingSelectedCount: (count: number) => `已選 ${count} 項`,
+  defaultThinkingLevel: '預設思考級別',
+  defaultThinkingLevelHelp: '新任務使用此模型時採用的思考級別;任務內仍可單獨切換。',
+  providerDefaultThinking: '服務商預設',
   visionInput: '圖片辨識',
   visionInputHelp: '此服務商的模型是否支援圖片。自動依模型資料判斷,缺少資料時不傳送圖片。',
   visionDefaultOption: (supported: boolean | undefined) =>
@@ -102,6 +108,9 @@ const enCapabilitiesCopy = {
   thinkingEffortHelp: 'These levels appear in the conversation’s thinking selector. Select only levels supported by this provider. Leave all unchecked to use existing model information.',
   thinkingUndeclared: 'Use model options',
   thinkingSelectedCount: (count: number) => `${count} levels available`,
+  defaultThinkingLevel: 'Default thinking level',
+  defaultThinkingLevelHelp: 'Thinking level for new tasks that use this model. It can still be changed per task.',
+  providerDefaultThinking: 'Provider default',
   visionInput: 'Send images to the model',
   visionInputHelp: 'Use provider and model information to decide whether to send images. Images are not sent when information is missing. Before choosing “Allow images”, confirm this provider supports images for this model.',
   visionDefaultOption: (supported: boolean | undefined) =>
diff --git a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx
index 81738db4b7..3eafe6db4b 100644
--- a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx
+++ b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx
@@ -192,8 +192,7 @@ function sameSelectedHost(
       previous.hostId === next.hostId &&
       previous.name === next.name &&
       previous.kind === next.kind &&
-      previous.chatDefaults.permissionMode === next.chatDefaults.permissionMode &&
-      previous.chatDefaults.thinkingLevel === next.chatDefaults.thinkingLevel,
+      previous.chatDefaults.permissionMode === next.chatDefaults.permissionMode,
   );
 }
 
diff --git a/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts b/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
index a71146b6b2..0bc50eb730 100644
--- a/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
+++ b/apps/desktop/src/renderer/features/workhub/locales/workhub-live-copy.ts
@@ -28,7 +28,7 @@ export const workHubLiveCopy = {
     undo: 'Undo last change',
     controlFailed: 'The action could not be completed. Review the conversation and try again.',
     progressTitle: 'WorkHub progress', progressWorking: 'Working', progressPaused: 'Paused', progressError: 'Needs attention', progressDone: 'Finished', progressHint: 'You can keep working in Maka.', progressOpen: 'View conversation', progressClose: 'Hide progress for this task',
-    title: 'WorkHub',
+    title: 'WorkHub', turnFailed: 'This turn failed. Details follow.',
     progressInput: 'Add a message…',
     float: 'Float WorkHub',
     dock: 'Return to Maka',
@@ -54,7 +54,7 @@ export const workHubLiveCopy = {
     undo: '撤销上次修改',
     controlFailed: '操作未完成,请查看对话后重试。',
     progressTitle: 'WorkHub 进度', progressWorking: '正在处理', progressPaused: '已暂停', progressError: '需要查看', progressDone: '本轮已结束', progressHint: '你可以继续在 Maka 中操作。', progressOpen: '查看对话', progressClose: '本轮不再显示进度',
-    title: '工作台',
+    title: '工作台', turnFailed: '本轮执行失败,详情如下。',
     progressInput: '补充一句…',
     float: '浮出工作台',
     dock: '收回 Maka',
@@ -80,7 +80,7 @@ export const workHubLiveCopy = {
     undo: '復原上次修改',
     controlFailed: '操作未完成,請查看對話後重試。',
     progressTitle: 'WorkHub 進度', progressWorking: '正在處理', progressPaused: '已暫停', progressError: '需要查看', progressDone: '本輪已結束', progressHint: '你可以繼續在 Maka 中操作。', progressOpen: '查看對話', progressClose: '本輪不再顯示進度',
-    title: '工作台',
+    title: '工作台', turnFailed: '本輪執行失敗,詳情如下。',
     progressInput: '補充一句…',
     float: '浮出工作台',
     dock: '收回 Maka',
diff --git a/apps/desktop/src/renderer/features/workhub/model/turn-presentation.ts b/apps/desktop/src/renderer/features/workhub/model/turn-presentation.ts
new file mode 100644
index 0000000000..121613c912
--- /dev/null
+++ b/apps/desktop/src/renderer/features/workhub/model/turn-presentation.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { UiLocale } from '@maka/core/ui-locale';
+import type { TurnPresentation, TurnViewModel } from '@maka/ui';
+import { workHubLiveCopy } from '../locales/workhub-live-copy.js';
+
+/** WorkHub needs failure diagnostics, but not ordinary Session footer actions. */
+export function deriveWorkHubTurnPresentation(
+  turns: readonly TurnViewModel[],
+  locale: UiLocale,
+): TurnPresentation {
+  const failedReasonLabels: Record<string, string> = {};
+  const failedSeverities: Record<string, 'error' | 'warning'> = {};
+  for (const turn of turns) {
+    if (turn.status !== 'failed') continue;
+    failedReasonLabels[turn.turnId] = workHubLiveCopy[locale].turnFailed;
+    failedSeverities[turn.turnId] = 'error';
+  }
+  return {
+    footerActionsByTurn: {},
+    failedReasonLabels,
+    failedSeverities,
+    failedExecutionStateLabels: {},
+    lineageBadgesByTurn: {},
+  };
+}
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-conversation.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-conversation.tsx
index e0bf5b5fc1..7ece6e98a2 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-conversation.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-conversation.tsx
@@ -24,6 +24,7 @@ import { Button, Link, Text } from '@astryxdesign/core';
 import { WorkHubHighlightContext, useWorkHubIdentityHue } from './workhub-work-identity.js';
 import type { WorkHubDelegationState, WorkHubLinkedWork } from '../model/linked-work.js';
 import { workHubLiveCopy } from '../locales/workhub-live-copy.js';
+import { deriveWorkHubTurnPresentation } from '../model/turn-presentation.js';
 
 export function WorkHubDelegationStatus(props: {
   work: WorkHubLinkedWork;
@@ -129,6 +130,7 @@ export function WorkHubConversation(props: ComponentProps<typeof ChatView> & { w
     liveTurns={liveTurns}
     transientMessages={selected ? chat.transientMessages?.filter((message) => message.hostTurnId && matchingTurns.has(message.hostTurnId)) : chat.transientMessages}
     activeTurn={activeTurn}
+    deriveTurnPresentation={(turns) => deriveWorkHubTurnPresentation(turns, locale)}
     emptyOverride={selected ? <p>{copy.noWorkConversation}</p> : chat.emptyOverride}
     turnDecorations={turnDecorations}
     promptRailDecorations={promptRailDecorations}
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
index 16136c78db..91344a3a34 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
@@ -376,9 +376,9 @@ export function WorkHubRoot() {
                 metadataContextWindow: modelChoice?.contextWindow,
                 onOpen: () => call(services.presentation.openUsage()),
               } : undefined}
-              activeThinkingLevels={thinkingLevels}
-              activeThinkingLevel={thinkingLevel}
-              onThinkingLevelChange={controller.changeThinkingLevel}
+              activeThinkingLevels={progress ? [] : thinkingLevels}
+              activeThinkingLevel={progress ? undefined : thinkingLevel}
+              onThinkingLevelChange={progress ? undefined : controller.changeThinkingLevel}
               modelSwitchHasHistory={transcript.messages.length > 0}
               footerAccessory={
                 <div className="workHubComposerActions">
diff --git a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
index 09fc2c183f..03191132e0 100644
--- a/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-preferences-copy.ts
@@ -179,11 +179,7 @@ export type SettingsPreferencesCopy = {
     saveDefaultModelFailed: string;
     defaultPermission: string;
     defaultPermissionHelp: string;
-    defaultThinking: string;
-    defaultThinkingHelp: string;
     codeModeHelp: string;
-    followModelDefault: string;
-    saveDefaultThinkingFailed: string;
     saveDefaultPermissionFailed: string;
     shellPreference: string;
     shellPreferenceHelp: string;
@@ -282,7 +278,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     sections: {
       identity: '身份', identityHelp: 'Maka 如何称呼你,以及界面语言和回答语气。',
       privacy: '隐私与通知', privacyHelp: '本地数据的读写范围,以及桌面通知时机。',
-      chatDefaults: '任务默认', chatDefaultsHelp: '新任务的起始模型、权限模式与思考级别。',
+      chatDefaults: '任务默认', chatDefaultsHelp: '新任务的起始模型与权限模式。每个模型的默认思考级别在模型设置中配置。',
       shell: '命令行环境', shellHelp: '选择 Runtime Host 执行 Bash 工具和终端命令时使用的 shell。',
       network: '网络', networkHelp: 'AI 模型请求走的网络通道。',
       theme: '主题', themeHelp: '界面跟随系统,还是固定浅色或深色。',
@@ -341,7 +337,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     general: {
       codeModeHelp: '开启后,新任务通过代码编排和调用工具。WorkHub 从下一轮起采用新设置,其他已有任务保持不变。',
       incognito: '隐身模式', incognitoHelp: '开启后暂停本地记忆读写、联网搜索和定时任务触发。', enableIncognito: '启用隐身模式', incognitoFailed: '隐身模式切换失败', notifications: '完成时发送系统通知', notificationsHelp: '窗口不在前台时,在回答完成或出错后发送桌面通知。', notificationsFailed: '通知设置切换失败', workspaceInstructions: '遵循项目指令', workspaceInstructionsHelp: '自动读取每个项目中已有的 AGENTS.md、CLAUDE.md 或 GEMINI.md;文件仍由各自项目管理。', workspaceInstructionsFailed: '项目指令设置切换失败', workHub: '启用 WorkHub', workHubHelp: 'WorkHub 目前仍不可用。此开关仅供开发测试,开启后也不能保证正常使用。', workHubFailed: 'Wor [...]
-      defaultModel: '默认模型', defaultModelHelp: '新任务默认使用的模型。', notSet: '未设置', saveDefaultModelFailed: '保存默认模型失败', defaultPermission: '默认权限模式', defaultPermissionHelp: '新任务默认使用的权限模式;可在任务内随时切换。', saveDefaultPermissionFailed: '保存默认权限模式失败', defaultThinking: '默认思考级别', defaultThinkingHelp: '新任务的思考级别;当前模型不支持所选级别时用模型默认。', followModelDefault: '跟随模型默认', saveDefaultThinkingFailed: '保存默认思考级别失败',
+      defaultModel: '默认模型', defaultModelHelp: '新任务默认使用的模型。', notSet: '未设置', saveDefaultModelFailed: '保存默认模型失败', defaultPermission: '默认权限模式', defaultPermissionHelp: '新任务默认使用的权限模式;可在任务内随时切换。', saveDefaultPermissionFailed: '保存默认权限模式失败',
       shellPreference: 'Bash 工具 shell', shellPreferenceHelp: '自动模式保持 Windows 的 PowerShell 优先规则;Git Bash 是仅对当前 Runtime Host 生效的显式覆盖。', shellAuto: '自动(推荐)', shellGitBash: 'Git Bash', shellExecutable: 'Git Bash 可执行文件', shellExecutableHelp: '填写 Runtime Host 所在 Windows 机器上 bash.exe 的绝对路径。也支持该机器上的旧版 System32 WSL Bash;保存时会验证 GNU Bash。', saveShell: '保存 shell 设置', savingShell: '正在保存…', shellSaved: '已保存', saveShellFailed: '保存 shell 设置失败', shellExecutableRejected: '当前 Runtime Host 无法把该路径作为 GNU Bash [...]
       proxy: '代理服务器', proxyHelp: '为 AI 模型请求配置网络代理', enableProxy: '启用代理服务器', saveNetworkFailed: '保存网络设置失败', proxyProtocol: '代理协议', serverAddress: '服务器地址', port: '端口', proxyAuth: '代理认证', proxyAuthHelp: '需要用户名和密码时开启。', enableProxyAuth: '启用代理认证', username: '用户名', password: '密码', bypassList: '代理白名单', bypassHelp: '这些域名将绕过代理直连,多个用逗号分隔。', autoBypass: (count) => `已自动添加 ${count} 个域名。代理仅作用于 AI 模型请求。`, testing: '测试中…', testCurrent: '测试当前配置', proxyReachable: '代理可达', proxyTestFailed: '代理测试失败', proxyTe [...]
       passwordSavedPlaceholder: '密码已保存;输入新密码以替换',
@@ -387,7 +383,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     sections: {
       identity: '身份', identityHelp: 'Maka 如何稱呼你,以及介面語言和回答語氣。',
       privacy: '隱私與通知', privacyHelp: '本地資料的讀寫範圍,以及桌面通知時機。',
-      chatDefaults: '任務預設', chatDefaultsHelp: '新任務的起始模型、權限模式與思考級別。',
+      chatDefaults: '任務預設', chatDefaultsHelp: '新任務的起始模型與權限模式。每個模型的預設思考級別在模型設定中配置。',
       shell: '命令列環境', shellHelp: '選擇 Runtime Host 執行 Bash 工具和終端命令時使用的 shell。',
       network: '網路', networkHelp: 'AI 模型請求走的網路通道。',
       theme: '主題', themeHelp: '介面跟隨系統,還是固定淺色或深色。',
@@ -446,7 +442,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     general: {
       codeModeHelp: '開啟後,新任務透過程式碼編排和呼叫工具。WorkHub 從下一輪起套用新設定,其他既有任務保持不變。',
       incognito: '隱身模式', incognitoHelp: '開啟後暫停本地記憶讀寫、聯網搜尋和定時任務觸發。', enableIncognito: '啟用隱身模式', incognitoFailed: '隱身模式切換失敗', notifications: '完成時傳送系統通知', notificationsHelp: '視窗不在前臺時,在回答完成或出錯後傳送桌面通知。', notificationsFailed: '通知設定切換失敗', workspaceInstructions: '遵循專案指令', workspaceInstructionsHelp: '自動讀取每個專案中已有的 AGENTS.md、CLAUDE.md 或 GEMINI.md;檔案仍由各自專案管理。', workspaceInstructionsFailed: '專案指令設定切換失敗', workHub: '啟用 WorkHub', workHubHelp: '在一個入口檢視已有工作,並將新輸入保守地送往普通任務。', workHubFailed: 'WorkHub 設定切換失敗 [...]
-      defaultModel: '預設模型', defaultModelHelp: '新任務預設使用的模型。', notSet: '未設定', saveDefaultModelFailed: '儲存預設模型失敗', defaultPermission: '預設權限模式', defaultPermissionHelp: '新任務預設使用的權限模式;可在任務內隨時切換。', saveDefaultPermissionFailed: '儲存預設權限模式失敗', defaultThinking: '預設思考級別', defaultThinkingHelp: '新任務的思考級別;目前模型不支援所選級別時用模型預設。', followModelDefault: '跟隨模型預設', saveDefaultThinkingFailed: '儲存預設思考級別失敗',
+      defaultModel: '預設模型', defaultModelHelp: '新任務預設使用的模型。', notSet: '未設定', saveDefaultModelFailed: '儲存預設模型失敗', defaultPermission: '預設權限模式', defaultPermissionHelp: '新任務預設使用的權限模式;可在任務內隨時切換。', saveDefaultPermissionFailed: '儲存預設權限模式失敗',
       shellPreference: 'Bash 工具 shell', shellPreferenceHelp: '自動模式保持 Windows 的 PowerShell 優先規則;Git Bash 是僅對目前 Runtime Host 生效的顯式覆蓋。', shellAuto: '自動(推薦)', shellGitBash: 'Git Bash', shellExecutable: 'Git Bash 執行檔', shellExecutableHelp: '填寫 Runtime Host 所在 Windows 機器上 bash.exe 的絕對路徑。也支援該機器上的舊版 System32 WSL Bash;儲存時會驗證 GNU Bash。', saveShell: '儲存 shell 設定', savingShell: '正在儲存…', shellSaved: '已儲存', saveShellFailed: '儲存 shell 設定失敗', shellExecutableRejected: '目前 Runtime Host 無法把該路徑作為 GNU Bash 執 [...]
       proxy: '代理伺服器', proxyHelp: '為 AI 模型請求設定網路代理', enableProxy: '啟用代理伺服器', saveNetworkFailed: '儲存網路設定失敗', proxyProtocol: '代理協議', serverAddress: '伺服器地址', port: '埠', proxyAuth: '代理認證', proxyAuthHelp: '需要使用者名稱和密碼時開啟。', enableProxyAuth: '啟用代理認證', username: '使用者名稱', password: '密碼', bypassList: '代理白名單', bypassHelp: '這些域名將繞過代理直連,多個用逗號分隔。', autoBypass: (count) => `已自動新增 ${count} 個域名。代理僅作用於 AI 模型請求。`, testing: '測試中…', testCurrent: '測試目前設定', proxyReachable: '代理可達', proxyTestFailed: '代理測試失敗', prox [...]
       passwordSavedPlaceholder: '密碼已儲存;輸入新密碼以替換',
@@ -482,7 +478,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     sections: {
       identity: 'Identity', identityHelp: 'How Maka addresses you, plus interface language and response tone.',
       privacy: 'Privacy and notifications', privacyHelp: 'What Maka may read and write locally, and when it notifies you.',
-      chatDefaults: 'Task defaults', chatDefaultsHelp: 'The model, permission mode, and thinking level a new task starts on.',
+      chatDefaults: 'Task defaults', chatDefaultsHelp: 'The model and permission mode a new task starts on. Configure thinking defaults per model in Model settings.',
       shell: 'Command environment', shellHelp: 'Choose the shell the Runtime Host uses for Bash tools and terminal commands.',
       network: 'Network', networkHelp: 'The network path AI model requests take.',
       theme: 'Theme', themeHelp: 'Follow the system appearance, or stay on light or dark.',
@@ -507,7 +503,7 @@ const SETTINGS_PREFERENCES_COPY_BY_LOCALE = {
     },
     general: {
       codeModeHelp: 'New tasks use code to compose and call tools. WorkHub applies changes on its next turn; other existing tasks keep their mode.',
-      incognito: 'Incognito mode', incognitoHelp: 'Pause local memory, web search, and scheduled task triggers.', enableIncognito: 'Enable incognito mode', incognitoFailed: 'Could not change incognito mode', notifications: 'Send a system notification when finished', notificationsHelp: 'Notify when a response finishes or fails while the window is in the background.', notificationsFailed: 'Could not change notification settings', workspaceInstructions: 'Follow project instructions', worksp [...]
+      incognito: 'Incognito mode', incognitoHelp: 'Pause local memory, web search, and scheduled task triggers.', enableIncognito: 'Enable incognito mode', incognitoFailed: 'Could not change incognito mode', notifications: 'Send a system notification when finished', notificationsHelp: 'Notify when a response finishes or fails while the window is in the background.', notificationsFailed: 'Could not change notification settings', workspaceInstructions: 'Follow project instructions', worksp [...]
       shellPreference: 'Bash tool shell', shellPreferenceHelp: 'Automatic keeps the PowerShell-first Windows default. Git Bash is an explicit override for the current Runtime Host.', shellAuto: 'Automatic (recommended)', shellGitBash: 'Git Bash', shellExecutable: 'Git Bash executable', shellExecutableHelp: 'Enter the absolute path to bash.exe on the Windows machine running the Runtime Host. The legacy System32 WSL Bash shim is also recognized; Maka verifies GNU Bash before saving.', save [...]
       passwordSavedPlaceholder: 'Password saved; enter a new password to replace it',
     },
diff --git a/apps/desktop/src/renderer/settings/general-settings-page.tsx b/apps/desktop/src/renderer/settings/general-settings-page.tsx
index 65a6cbcb4f..22288c3a27 100644
--- a/apps/desktop/src/renderer/settings/general-settings-page.tsx
+++ b/apps/desktop/src/renderer/settings/general-settings-page.tsx
@@ -34,7 +34,6 @@ import type {
   RuntimeHostNetworkProxySettings,
   UpdateAppSettingsResult,
 } from '@maka/core/settings';
-import { THINKING_LEVELS, type ThinkingLevel } from '@maka/core/model-thinking';
 import type {
   IdentifiedLlmConnection,
   ProjectedLlmConnection,
@@ -59,7 +58,6 @@ import {
 } from "@maka/ui";
 import { ProviderBrandMark } from "./provider-brand-marks";
 import { PasswordInput } from "./password-input";
-import { getConversationCopy } from '@maka/ui';
 import { settingsActionErrorMessage } from "./settings-error-copy";
 import { useActionGuard, useKeyedActionGuard } from "./use-action-guard";
 import { useOptimisticSettingsDraft } from "./use-optimistic-settings-draft";
@@ -305,7 +303,6 @@ export function GeneralSettingsPage(props: {
           showSettingsPlaceholder={showRuntimeHostSettingsPlaceholder}
           onRefresh={props.onRefreshConnections}
           permissionMode={props.settings.chatDefaults.permissionMode}
-          thinkingLevel={props.settings.chatDefaults.thinkingLevel}
           codeModeEnabled={props.settings.chatDefaults.codeModeEnabled === true}
           onUpdate={props.onUpdate}
         />
@@ -487,9 +484,6 @@ function isRejectedShellPreference(error: unknown): boolean {
  * `PermissionModeSelect` so labels, hints, and markup can't drift from the
  * composer picker.
  */
-/** Sentinel for "no preference" — Selector needs a value, absence is not one. */
-const FOLLOW_MODEL_DEFAULT = "__follow_model__";
-
 function GeneralDefaultsCard(props: {
   connections: readonly ProjectedLlmConnection[];
   defaultSlug: string | null;
@@ -502,7 +496,6 @@ function GeneralDefaultsCard(props: {
   showSettingsPlaceholder: boolean;
   onRefresh(): Promise<void>;
   permissionMode: ChatDefaultPermissionMode;
-  thinkingLevel?: ThinkingLevel;
   codeModeEnabled: boolean;
   onUpdate(
     patch: Parameters<typeof window.maka.settings.update>[0],
@@ -511,14 +504,11 @@ function GeneralDefaultsCard(props: {
   const host = useOptionalRuntimeHostSettingsTarget();
   const locale = useUiLocale();
   const copy = getSettingsPreferencesCopy(locale).general;
-  // Level names come from the composer's own map — one vocabulary for the
-  // levels, so the settings row and the in-chat menu can never disagree.
-  const conversationCopy = getConversationCopy(locale);
   const sections = getSettingsPreferencesCopy(locale).sections;
   const boundaryCopy = getShellCopy(locale).sessionSettingsActions;
   const toast = useToast();
   const mountedRef = useMountedRef();
-  type SaveKey = "default-model" | "permission-mode" | "thinking-level";
+  type SaveKey = "default-model" | "permission-mode";
   const persistGuard = useKeyedActionGuard<SaveKey>();
   const [savingRows, setSavingRows] = useState<Partial<Record<SaveKey, boolean>>>({});
   function setRowSaving(key: SaveKey, saving: boolean) {
@@ -630,28 +620,6 @@ function GeneralDefaultsCard(props: {
     }
   }
 
-  async function persistThinkingLevel(next: ThinkingLevel | undefined) {
-    if (!props.settingsInteractive) return;
-    const releaseSave = persistGuard.begin("thinking-level");
-    if (!releaseSave) return;
-    setRowSaving("thinking-level", true);
-    try {
-      await props.onUpdate({ chatDefaults: { thinkingLevel: next } });
-    } catch (error) {
-      if (mountedRef.current) {
-        toast.error(
-          copy.saveDefaultThinkingFailed,
-          settingsActionErrorMessage(error, locale),
-          undefined,
-          host ? { profileId: host.profileId } : undefined,
-        );
-      }
-    } finally {
-      releaseSave();
-      if (mountedRef.current) setRowSaving("thinking-level", false);
-    }
-  }
-
   async function persistCodeMode(codeModeEnabled: boolean) {
     if (!props.settingsInteractive) return;
     try {
@@ -733,42 +701,6 @@ function GeneralDefaultsCard(props: {
           width="7rem"
         />
       ) : null}
-      {/* The absent option is first and means exactly that: no preference, so
-          each model uses its own. It is not a level, which is why the composer
-          menu now calls that same state 模型默认 rather than 默认 — the old
-          wording promised a knob that did not exist anywhere. */}
-      {props.settingsAvailable ? (
-        <SettingsRow
-          label={copy.defaultThinking}
-          description={copy.defaultThinkingHelp}
-          end={
-            <Selector
-              label={copy.defaultThinking}
-              isLabelHidden
-              value={props.thinkingLevel ?? FOLLOW_MODEL_DEFAULT}
-              onChange={(value) => {
-                void persistThinkingLevel(
-                  value === FOLLOW_MODEL_DEFAULT ? undefined : (value as ThinkingLevel),
-                );
-              }}
-              options={[
-                { value: FOLLOW_MODEL_DEFAULT, label: copy.followModelDefault },
-                ...THINKING_LEVELS.map((level) => ({
-                  value: level,
-                  label: conversationCopy.model.level[level],
-                })),
-              ]}
-              isDisabled={savingRows["thinking-level"] || !props.settingsInteractive}
-            />
-          }
-        />
-      ) : props.showSettingsPlaceholder ? (
-        <SettingsRowSkeleton
-          label={copy.defaultThinking}
-          description={copy.defaultThinkingHelp}
-          width="7rem"
-        />
-      ) : null}
     </SettingsSection>
   );
 }
diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
index e2d036621d..76657e7ede 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
+++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
@@ -723,6 +723,7 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
           limitsConflict={limitsConflict}
           defaultContextWindow={modelEntry?.defaultContextWindow}
           defaultInputLimit={modelEntry?.defaultInputLimit}
+          thinkingLevels={modelEntry?.thinkingLevels ?? []}
           onChange={(patch) => setDraftParameters(editingModelId, patch)}
           contextWindowInput={contextWindowInput ?? String(declared?.contextWindow ?? '')}
           contextWindowInputInvalid={contextWindowInputInvalid}
diff --git a/apps/desktop/src/renderer/use-shell-appearance.ts b/apps/desktop/src/renderer/use-shell-appearance.ts
index 9461740f61..6347a78419 100644
--- a/apps/desktop/src/renderer/use-shell-appearance.ts
+++ b/apps/desktop/src/renderer/use-shell-appearance.ts
@@ -24,7 +24,6 @@ import {
   type ThemePalette,
   type ThemePreference,
 } from '@maka/core/settings';
-import type { ThinkingLevel } from '@maka/core/model-thinking';
 import type { UiLocale, UiLocalePreference } from '@maka/core/ui-locale';
 import { createUiLocaleUpdateGate } from './settings/ui-locale-update-gate';
 import { applyTerminalFontSize, applyTheme, applyThemePalette, applyUiFontSize } from './theme';
@@ -65,8 +64,6 @@ export function useShellAppearance({
   const [uiLocaleUpdateGate] = useState(createUiLocaleUpdateGate);
   const [userLabel, setUserLabel] = useState<string>('');
   const [appearanceHydrated, setAppearanceHydrated] = useState(false);
-  // undefined = the user expressed no preference, so each model uses its own.
-  const [defaultThinkingLevel, setDefaultThinkingLevel] = useState<ThinkingLevel | undefined>(undefined);
 
   async function refreshShellSettings() {
     const uiLocaleHydration = uiLocaleUpdateGate.beginHydration();
@@ -120,7 +117,6 @@ export function useShellAppearance({
     if (runtimeHostResult.ok) {
       const next = runtimeHostResult.settings;
       setUserLabel(next.personalization.displayName ?? '');
-      setDefaultThinkingLevel(next.chatDefaults.thinkingLevel);
     }
   }
 
@@ -133,7 +129,6 @@ export function useShellAppearance({
     appearanceHydrated,
     userLabel,
     setUserLabel,
-    defaultThinkingLevel,
     refreshShellSettings,
   };
 }
diff --git a/apps/desktop/src/renderer/use-shell-chat-model.ts b/apps/desktop/src/renderer/use-shell-chat-model.ts
index 5b9cc00b83..c0904afbdc 100644
--- a/apps/desktop/src/renderer/use-shell-chat-model.ts
+++ b/apps/desktop/src/renderer/use-shell-chat-model.ts
@@ -78,8 +78,6 @@ export function useShellChatModel(options: {
   sessionHealthSession: SessionSummary | undefined;
   persistedComposerDefaults: ComposerDefaults | null;
   usePersistedComposerDefaults: boolean;
-  /** Settings → 通用 → 默认思考级别; undefined means "no preference". */
-  defaultThinkingLevel?: ThinkingLevel;
   connectionSnapshotReady: boolean;
   modelPickerDisabled: boolean;
   openSettingsSection: (section: SettingsSection) => void;
@@ -97,10 +95,11 @@ export function useShellChatModel(options: {
   newChatModelLabel: string | undefined;
   newChatThinkingLevels: readonly ThinkingLevel[];
   newChatThinkingLevel: ThinkingLevel | undefined;
+  /** Raw draft intent; unlike the display value above, undefined stays untouched. */
+  pendingNewChatThinkingLevel: ThinkingLevel | null | undefined;
   composerSupportsVision: boolean | undefined;
   pendingNewChatModel: NewChatModelCandidate | null;
   setPendingNewChatModel: (next: NewChatModelCandidate | null) => void;
-  pendingNewChatThinkingLevel: ThinkingLevel | null;
   setPendingNewChatThinkingLevel: (next: ThinkingLevel | null) => void;
   sessionHealthNotice: SessionHealthNoticeView | undefined;
 } {
@@ -116,7 +115,7 @@ export function useShellChatModel(options: {
     openModelPicker,
   } = options;
   const conversationCopy = getDesktopConversationCopy(uiLocale);
-  const [pendingNewChatModelChoice, setPendingNewChatModel] = useNewTaskChoice<
+  const [pendingNewChatModelChoice, setPendingNewChatModelChoice] = useNewTaskChoice<
     NewChatModelCandidate | null
   >(
     options.newTaskKey,
@@ -147,21 +146,19 @@ export function useShellChatModel(options: {
   // candidate wins before the legacy catalog default and first offered choice.
   // Renderer-only — it never mutates the persisted Settings · 模型 default.
   // Three states, because two cannot say this: `undefined` is an untouched
-  // picker, so Settings → 通用 → 默认思考级别 applies; `null` is the user
-  // explicitly choosing the per-chat `默认` option (use the model default),
-  // which must beat the configured Settings default or the picker could not
-  // undo it.
+  // picker, so this exact model's configured default applies; `null` is the
+  // user explicitly choosing the provider's default for this draft.
   //
   // The pick carries its target key so a Host or Project switch cannot apply it
   // to a different execution authority, even for an identically named model.
-  const [pendingNewChatThinkingLevel, setPendingNewChatThinkingLevel] =
+  const [pendingNewChatThinkingLevel, setPendingNewChatThinkingLevel, clearPendingNewChatThinkingLevel] =
     useNewTaskChoice<ThinkingLevel | null>(
       options.newTaskKey,
     );
-  const requestedNewChatThinkingLevel =
-    pendingNewChatThinkingLevel === undefined
-      ? options.defaultThinkingLevel ?? null
-      : pendingNewChatThinkingLevel;
+  const setPendingNewChatModel = (next: NewChatModelCandidate | null) => {
+    setPendingNewChatModelChoice(next);
+    clearPendingNewChatThinkingLevel();
+  };
   // A pick only stays in effect while it is still an offered choice. If the user
   // later disables/removes that connection or model, fall through to another
   // offered candidate so the home chip never shows — nor sends — a stale model.
@@ -181,6 +178,15 @@ export function useShellChatModel(options: {
     catalogDefault: catalogDefaultNewChatModel,
     choices: chatModelChoices,
   });
+  const newChatModelChoice = chatModelChoices.find(
+    (choice) =>
+      choice.connectionId === newChatModel?.llmConnectionId &&
+      choice.connectionSlug === newChatModel?.llmConnectionSlug &&
+      choice.model === newChatModel?.model,
+  );
+  const requestedNewChatThinkingLevel = pendingNewChatThinkingLevel === undefined
+    ? newChatModelChoice?.defaultThinkingLevel ?? null
+    : pendingNewChatThinkingLevel;
   // A task whose backend was retired has no model to name (#3211). That verdict
   // comes from the readiness projection, not from reading `activeSession.backend`
   // here: the projection is the single authority on whether a task is usable,
@@ -226,14 +232,9 @@ export function useShellChatModel(options: {
   const newChatThinkingLevels = useMemo(
     () => {
       if (!newChatModel) return [];
-      return chatModelChoices.find(
-        (choice) =>
-          choice.connectionId === newChatModel.llmConnectionId &&
-          choice.connectionSlug === newChatModel.llmConnectionSlug &&
-          choice.model === newChatModel.model,
-      )?.thinkingLevels ?? [];
+      return newChatModelChoice?.thinkingLevels ?? [];
     },
-    [newChatModel, chatModelChoices],
+    [newChatModel, newChatModelChoice],
   );
   // The membership check is what keeps a configured default honest: a level the
   // current model does not offer falls through to that model's own default
@@ -320,12 +321,10 @@ export function useShellChatModel(options: {
     newChatModelLabel,
     newChatThinkingLevels,
     newChatThinkingLevel,
+    pendingNewChatThinkingLevel,
     composerSupportsVision,
     pendingNewChatModel,
     setPendingNewChatModel,
-    // Resolved, not raw: callers want the level the next chat would actually
-    // request, and must not have to re-apply the settings fallback themselves.
-    pendingNewChatThinkingLevel: requestedNewChatThinkingLevel,
     setPendingNewChatThinkingLevel,
     sessionHealthNotice,
   };
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index d53507ed50..4ffbcf2c26 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -275,6 +275,7 @@ export const ProgressModelPicker: Story = {
   play: async ({ canvasElement }) => {
     const canvas = within(canvasElement);
     await waitFor(() => expect(canvasElement.querySelector('.workHubLive')).toHaveAttribute('data-progress', 'true'));
+    expect(canvas.queryByRole('combobox', { name: /思考级别/ })).toBeNull();
     const editor = canvasElement.querySelector('[contenteditable="true"]') as HTMLElement;
     await userEvent.click(editor);
     await userEvent.type(editor, 'Keep this draft readable while choosing a model.');
diff --git a/packages/core/src/__tests__/model-thinking.test.ts b/packages/core/src/__tests__/model-thinking.test.ts
index 034995cd4d..4691c26c75 100644
--- a/packages/core/src/__tests__/model-thinking.test.ts
+++ b/packages/core/src/__tests__/model-thinking.test.ts
@@ -21,6 +21,7 @@ import assert from 'node:assert/strict';
 import { test } from 'node:test';
 import {
   type ConnectionThinkingContext,
+  defaultThinkingLevelForConnection,
   normalizeModelOverrides,
   modelOverride,
   resolveThinkingLevel,
@@ -52,6 +53,27 @@ test('relay profiles preserve the fast service tier declaration', () => {
   assert.deepEqual(normalizeModelOverrides({ m: { serviceTier: 'unknown' } }), { m: {} });
 });
 
+test('per-model thinking defaults resolve only when the model offers the level', () => {
+  const connection = {
+    providerType: 'openai-compatible',
+    modelOverrides: {
+      reasoner: { thinkingLevels: ['low', 'high'], defaultThinkingLevel: 'high' },
+      stale: { thinkingLevels: ['low'], defaultThinkingLevel: 'high' },
+      invalid: { thinkingLevels: ['low'], defaultThinkingLevel: 'turbo' },
+    },
+  } as unknown as ConnectionThinkingContext;
+  assert.equal(defaultThinkingLevelForConnection(connection, 'reasoner'), 'high');
+  assert.equal(defaultThinkingLevelForConnection(connection, 'stale'), undefined);
+  assert.equal(defaultThinkingLevelForConnection(connection, 'invalid'), undefined);
+  assert.deepEqual(normalizeModelOverrides(connection.modelOverrides)?.reasoner, {
+    thinkingLevels: ['low', 'high'],
+    defaultThinkingLevel: 'high',
+  });
+  assert.deepEqual(normalizeModelOverrides(connection.modelOverrides)?.invalid, {
+    thinkingLevels: ['low'],
+  });
+});
+
 test('Fast visibility mirrors the pinned OpenAI SDK priority-processing families', () => {
   const cases = [
     ['gpt-4o', true],
diff --git a/packages/core/src/__tests__/runtime-policy-codec.test.ts b/packages/core/src/__tests__/runtime-policy-codec.test.ts
index 2fd2e91173..699262c941 100644
--- a/packages/core/src/__tests__/runtime-policy-codec.test.ts
+++ b/packages/core/src/__tests__/runtime-policy-codec.test.ts
@@ -81,7 +81,7 @@ test('Code Mode is opt-in and survives policy decoding', () => {
   );
 });
 
-test('preserves a valid default thinking level and rejects unknown levels', () => {
+test('preserves the legacy chat thinking field but rejects unknown levels', () => {
   const policy = {
     ...createDefaultRuntimePolicy(),
     chatDefaults: { permissionMode: 'ask' as const, thinkingLevel: 'high' as const },
@@ -282,6 +282,7 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', (
   const table = {
     'relay-reasoner': {
       thinkingLevels: ['minimal', 'low'],
+      defaultThinkingLevel: 'low',
       vision: true,
       contextWindow: 128_000,
       serviceTier: 'fast',
diff --git a/packages/core/src/__tests__/settings.test.ts b/packages/core/src/__tests__/settings.test.ts
index 1346f19b93..6fb133251f 100644
--- a/packages/core/src/__tests__/settings.test.ts
+++ b/packages/core/src/__tests__/settings.test.ts
@@ -171,7 +171,7 @@ test('shell settings default, normalize, and merge through their shared boundary
   );
 });
 
-test('a chat-default thinking level the app does not recognize drops to no preference', () => {
+test('an unrecognized legacy chat thinking field drops to no preference', () => {
   const normalized = normalizeSettings({
     chatDefaults: { thinkingLevel: 'ultra' as unknown as undefined },
   });
diff --git a/packages/core/src/chat-model-choice.ts b/packages/core/src/chat-model-choice.ts
index 04b1e916ac..f9573ca405 100644
--- a/packages/core/src/chat-model-choice.ts
+++ b/packages/core/src/chat-model-choice.ts
@@ -38,6 +38,8 @@ export interface ChatModelChoice {
   connectionName?: string;
   isDefault: boolean;
   thinkingLevels: readonly ThinkingLevel[];
+  /** Per-model default applied when creating a new Session. */
+  defaultThinkingLevel?: ThinkingLevel;
   /** Exact capability projection used by model-facing attachment composition. */
   supportsVision?: boolean;
   /** Provider/model metadata shown beside the user-declared context setting. */
@@ -67,6 +69,9 @@ export function buildChatModelChoices(
         ...(provider.authKind === 'oauth_token' ? {} : { connectionName: connection.name }),
         isDefault: entry.isDefault,
         thinkingLevels: entry.thinkingLevels,
+        ...(entry.defaultThinkingLevel === undefined
+          ? {}
+          : { defaultThinkingLevel: entry.defaultThinkingLevel }),
         supportsVision: entry.supportsVision,
         ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
         ...(declaredWindow !== undefined ? { declaredContextWindow: declaredWindow } : {}),
diff --git a/packages/core/src/model-catalog.ts b/packages/core/src/model-catalog.ts
index 647540029b..09a1e11fba 100644
--- a/packages/core/src/model-catalog.ts
+++ b/packages/core/src/model-catalog.ts
@@ -24,7 +24,11 @@ import type {
   ProviderDefaults,
   ProviderType,
 } from './llm-connections.js';
-import { applyModelOverride, resolveModelLimits } from './model-thinking.js';
+import {
+  applyModelOverride,
+  defaultThinkingLevelForConnection,
+  resolveModelLimits,
+} from './model-thinking.js';
 import {
   CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS,
   PROVIDER_REGISTRY,
@@ -81,6 +85,8 @@ export interface ModelCatalogEntry {
    * thinking projection honoured.
    */
   thinkingLevels: readonly ThinkingLevel[];
+  /** Host-owned preference used when a new Session starts on this model. */
+  defaultThinkingLevel?: ThinkingLevel;
   contextWindow?: number;
   inputLimit?: number;
   defaultContextWindow?: number;
@@ -394,6 +400,10 @@ function makeEntry(
       capabilities,
       ...(modalities !== undefined ? { modalities } : {}),
     });
+  const defaultThinkingLevel = defaultThinkingLevelForConnection(
+    thinkingContext,
+    normalizedModel.id,
+  );
   return {
     id: normalizedModel.id,
     ...displayNameForModel(input.providerType, normalizedModel),
@@ -408,6 +418,7 @@ function makeEntry(
       ? {}
       : { compactionThreshold: input.modelOverrides[normalizedModel.id]!.compactionThreshold }),
     thinkingLevels: thinkingVariantsForConnection(thinkingContext, normalizedModel.id),
+    ...(defaultThinkingLevel === undefined ? {} : { defaultThinkingLevel }),
     ...limits,
     ...(defaults.contextWindow === undefined
       ? {}
diff --git a/packages/core/src/model-thinking.ts b/packages/core/src/model-thinking.ts
index 27beaabb63..db48c4759f 100644
--- a/packages/core/src/model-thinking.ts
+++ b/packages/core/src/model-thinking.ts
@@ -123,6 +123,8 @@ export interface ModelOverride {
   readonly capabilities?: Omit<NonNullable<ModelInfo['capabilities']>, 'vision'>;
   readonly modalities?: ModelInfo['modalities'];
   readonly thinkingLevels?: readonly ThinkingLevel[];
+  /** Thinking level used when a new Session starts on this exact model. */
+  readonly defaultThinkingLevel?: ThinkingLevel;
   readonly vision?: boolean;
   readonly contextWindow?: number;
   readonly compactionThreshold?: number;
@@ -149,6 +151,7 @@ function normalizeModelOverride(entry: unknown): ModelOverride | undefined {
     capabilities?: ModelOverride['capabilities'];
     modalities?: ModelOverride['modalities'];
     thinkingLevels?: readonly ThinkingLevel[];
+    defaultThinkingLevel?: ThinkingLevel;
     vision?: boolean;
     contextWindow?: number;
     compactionThreshold?: number;
@@ -179,6 +182,9 @@ function normalizeModelOverride(entry: unknown): ModelOverride | undefined {
       );
     }
   }
+  if (isThinkingLevel(entry.defaultThinkingLevel)) {
+    declared.defaultThinkingLevel = entry.defaultThinkingLevel;
+  }
   if (typeof entry.vision === 'boolean') declared.vision = entry.vision;
   for (const field of [
     'contextWindow',
@@ -307,6 +313,22 @@ export function thinkingVariantsForConnection(
   return thinkingVariantsForModel(connection.providerType, modelId);
 }
 
+/**
+ * Resolve the configured new-Session default against the model's effective
+ * capability ladder. A stale declaration falls back to the provider default
+ * instead of reaching Session creation as an unsupported level.
+ */
+export function defaultThinkingLevelForConnection(
+  connection: ConnectionThinkingContext,
+  modelId: string,
+): ThinkingLevel | undefined {
+  const configured = modelOverride(connection, modelId)?.defaultThinkingLevel;
+  return configured !== undefined &&
+    thinkingVariantsForConnection(connection, modelId).includes(configured)
+    ? configured
+    : undefined;
+}
+
 /**
  * Discard-semantics gate: returns the level when the model offers it,
  * `undefined` otherwise. Callers that must *reject* a bad level (IPC/session
@@ -361,6 +383,7 @@ export function applyModelOverride(
   if (!override) return model;
   const {
     thinkingLevels: _thinking,
+    defaultThinkingLevel: _defaultThinking,
     serviceTier: _tier,
     compactionThreshold: _threshold,
     maxOutputTokens: _outputBudget,
diff --git a/packages/core/src/runtime-inputs.ts b/packages/core/src/runtime-inputs.ts
index 36e66ccd55..37229f0287 100644
--- a/packages/core/src/runtime-inputs.ts
+++ b/packages/core/src/runtime-inputs.ts
@@ -99,7 +99,9 @@ export interface CreateSessionInput {
  * type-checks against `bridge-contract.d.ts` alone, so a field added on one
  * side and not the other is a type error nowhere.
  */
-export type CreateSessionRequestInput = Partial<CreateSessionInput> & {
+export type CreateSessionRequestInput = Omit<Partial<CreateSessionInput>, 'thinkingLevel'> & {
+  /** `null` explicitly bypasses the configured per-model default. */
+  thinkingLevel?: ThinkingLevel | null;
   mode?: SessionStartMode;
 };
 
diff --git a/packages/core/src/runtime-policy.ts b/packages/core/src/runtime-policy.ts
index 78cdc3e425..dddae35461 100644
--- a/packages/core/src/runtime-policy.ts
+++ b/packages/core/src/runtime-policy.ts
@@ -154,6 +154,7 @@ export interface RuntimePolicy {
   };
   readonly chatDefaults: {
     readonly permissionMode: ChatDefaultPermissionMode;
+    /** @deprecated Wire compatibility only; task creation ignores this field. */
     readonly thinkingLevel?: ThinkingLevel;
     readonly codeModeEnabled?: boolean;
   };
diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts
index 813dce30d2..2d76173357 100644
--- a/packages/core/src/runtime-policy/connection-catalog-codec.ts
+++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts
@@ -266,6 +266,7 @@ export function decodeModelOverridesTable(value: unknown): Readonly<Record<strin
       `relay model profile for ${modelId}`,
       [
         'thinkingLevels',
+        'defaultThinkingLevel',
         'vision',
         'contextWindow',
         'serviceTier',
@@ -302,6 +303,12 @@ export function decodeModelOverridesTable(value: unknown): Readonly<Record<strin
       }
       declared.thinkingLevels = [...entry.thinkingLevels] as ThinkingLevel[];
     }
+    if (entry.defaultThinkingLevel !== undefined) {
+      if (!isThinkingLevel(entry.defaultThinkingLevel)) {
+        throw domainError(`default thinking level for ${modelId} is invalid`);
+      }
+      declared.defaultThinkingLevel = entry.defaultThinkingLevel;
+    }
     if (entry.vision !== undefined) {
       declared.vision = booleanValue(entry.vision, `declared vision for ${modelId}`);
     }
diff --git a/packages/core/src/runtime-policy/model-catalog-entry-codec.ts b/packages/core/src/runtime-policy/model-catalog-entry-codec.ts
index 4e06d04ed4..794396e621 100644
--- a/packages/core/src/runtime-policy/model-catalog-entry-codec.ts
+++ b/packages/core/src/runtime-policy/model-catalog-entry-codec.ts
@@ -42,6 +42,7 @@ export function decodeModelCatalogEntry(value: unknown): ModelCatalogEntry {
       'defaultSupportsVision',
       'compactionThreshold',
       'thinkingLevels',
+      'defaultThinkingLevel',
       'contextWindow',
       'inputLimit',
       'defaultContextWindow',
@@ -57,6 +58,7 @@ export function decodeModelCatalogEntry(value: unknown): ModelCatalogEntry {
     id: item.id,
     ...pick(item, ['displayName', 'description', 'contextWindow', 'inputLimit', 'knowledgeCutoff']),
   });
+  const thinkingLevels = decodeThinkingLevels(item.thinkingLevels);
   return {
     ...shared,
     ...Object.fromEntries(
@@ -87,10 +89,28 @@ export function decodeModelCatalogEntry(value: unknown): ModelCatalogEntry {
           ),
         }
       : {}),
-    thinkingLevels: decodeThinkingLevels(item.thinkingLevels),
+    thinkingLevels,
+    ...(item.defaultThinkingLevel === undefined
+      ? {}
+      : {
+          defaultThinkingLevel: decodeDefaultThinkingLevel(
+            item.defaultThinkingLevel,
+            thinkingLevels,
+          ),
+        }),
   };
 }
 
+function decodeDefaultThinkingLevel(
+  value: unknown,
+  available: readonly ThinkingLevel[],
+): ThinkingLevel {
+  if (!isThinkingLevel(value) || !available.includes(value)) {
+    throw domainError('entry default thinking level is invalid');
+  }
+  return value;
+}
+
 function decodeThinkingLevels(value: unknown): readonly ThinkingLevel[] {
   if (!Array.isArray(value)) throw domainError('entry thinking levels must be an array');
   const levels = value.map((level) => {
diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts
index 9557416bac..fded36e497 100644
--- a/packages/core/src/settings.ts
+++ b/packages/core/src/settings.ts
@@ -513,15 +513,7 @@ export interface ChatDefaultsSettings {
   permissionMode: ChatDefaultPermissionMode;
   /** Applies only when a new task is created. */
   codeModeEnabled?: boolean;
-  /**
-   * Seeds new sessions' thinking level. `undefined` means "whatever the model
-   * does on its own" — the absence of a preference, not a level.
-   *
-   * A chosen level is a wish, not a guarantee: models expose different ladders,
-   * so one that does not offer the chosen rung falls back to its own default
-   * for that session rather than being forced to the nearest neighbour. The
-   * composer already resolves it that way for the per-session picker.
-   */
+  /** @deprecated Read-only compatibility for older settings; new tasks ignore it. */
   thinkingLevel?: ThinkingLevel;
 }
 
@@ -1150,9 +1142,8 @@ function defaultChatDefaultsSettings(): ChatDefaultsSettings {
 function normalizeChatDefaultsSettings(settings: ChatDefaultsSettings): ChatDefaultsSettings {
   return {
     ...(settings.codeModeEnabled === true ? { codeModeEnabled: true } : {}),
-    // Same fail-closed reasoning as the mode below: a garbage persisted level
-    // drops to "no preference" (the model's own default) rather than reaching
-    // session creation as a rung no picker recognizes.
+    // Preserve the retired field while older settings documents still carry it.
+    // No task creation path consumes it; defaults now live on model overrides.
     thinkingLevel: isThinkingLevel(settings.thinkingLevel) ? settings.thinkingLevel : undefined,
     // A retired mode is decoded (not rejected) so an existing settings file
     // keeps working; knowing which modes are retired lives in one place.
diff --git a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
index 55a293a189..79fd2c809d 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts
@@ -790,6 +790,93 @@ test('creation on a relay connection honours declared levels via the catalog pro
   assert.equal(persistedConnectionId, 'connection-1');
 });
 
+test("creation applies the selected model's configured thinking default", async () => {
+  let persistedThinkingLevel: unknown;
+  const fixture = createFixture({
+    connection: {
+      providerType: 'openai-compatible',
+      enabledModelIds: ['relay-model'],
+      models: [{ id: 'relay-model' }],
+      modelOverrides: {
+        'relay-model': {
+          thinkingLevels: ['low', 'high'],
+          defaultThinkingLevel: 'high',
+        },
+      },
+    },
+    stores: {
+      createStableSession: async (args) => {
+        persistedThinkingLevel = args.input.thinkingLevel;
+        return {
+          kind: 'existing' as const,
+          record: headerSnapshot(sessionHeader(args.sessionId, ['user-label']), 1),
+        };
+      },
+    },
+  });
+
+  const outcome = await fixture.coordinator.handlers['session.create'](
+    {
+      sessionId: fixture.sessionId,
+      workspace: { kind: 'host_path', path: process.cwd() },
+      modelTarget: {
+        kind: 'explicit',
+        connectionId: 'connection-1',
+        connectionSlug: 'test',
+        model: 'relay-model',
+      },
+    },
+    context,
+  );
+
+  assert.equal(outcome.ok, true);
+  assert.equal(persistedThinkingLevel, 'high');
+});
+
+test('creation can explicitly bypass a configured model thinking default', async () => {
+  let persistedThinkingLevel: unknown = 'not-called';
+  const fixture = createFixture({
+    connection: {
+      providerType: 'openai-compatible',
+      enabledModelIds: ['relay-model'],
+      models: [{ id: 'relay-model' }],
+      modelOverrides: {
+        'relay-model': {
+          thinkingLevels: ['low', 'high'],
+          defaultThinkingLevel: 'high',
+        },
+      },
+    },
+    stores: {
+      createStableSession: async (args) => {
+        persistedThinkingLevel = args.input.thinkingLevel;
+        return {
+          kind: 'existing' as const,
+          record: headerSnapshot(sessionHeader(args.sessionId, ['user-label']), 1),
+        };
+      },
+    },
+  });
+
+  const outcome = await fixture.coordinator.handlers['session.create'](
+    {
+      sessionId: fixture.sessionId,
+      workspace: { kind: 'host_path', path: process.cwd() },
+      modelTarget: {
+        kind: 'explicit',
+        connectionId: 'connection-1',
+        connectionSlug: 'test',
+        model: 'relay-model',
+      },
+      thinkingLevel: null,
+    },
+    context,
+  );
+
+  assert.equal(outcome.ok, true);
+  assert.equal(persistedThinkingLevel, undefined);
+});
+
 test('plugin executor creation bypasses model resolution and persists the executor route', async () => {
   let persistedInput: Parameters<CatalogStores['createStableSession']>[0]['input'] | undefined;
   const externalHeader = (sessionId: string): SessionHeader => {
diff --git a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
index f1c05b5e1b..318f87f36f 100644
--- a/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/session-catalog-protocol.test.ts
@@ -533,6 +533,28 @@ describe('Session catalog protocol', () => {
     );
   });
 
+  test('distinguishes a model thinking default from an explicit provider default', () => {
+    const decode = (thinkingLevel: 'high' | null | undefined) => {
+      const decoded = decodeClientFrame({
+        requestId: `request-thinking-${String(thinkingLevel)}`,
+        operation: 'session.create',
+        input: {
+          sessionId: `session-thinking-${String(thinkingLevel)}`,
+          workspace: { kind: 'host_path', path: '/workspace' },
+          modelTarget: { kind: 'default' },
+          ...(thinkingLevel === undefined ? {} : { thinkingLevel }),
+        },
+      });
+      if ('kind' in decoded || decoded.operation !== 'session.create') {
+        assert.fail('Expected Session create frame');
+      }
+      return decoded.input;
+    };
+    assert.equal(Object.hasOwn(decode(undefined), 'thinkingLevel'), false);
+    assert.equal(decode(null).thinkingLevel, null);
+    assert.equal(decode('high').thinkingLevel, 'high');
+  });
+
   test('correlates committed and conflicting update outputs with the request Session', () => {
     const session = projection();
     assert.deepEqual(
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index fc58fa3020..f4ed0e72a3 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -101,7 +101,7 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
 export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
 // Increment when the same protocol version no longer guarantees safe Client-Host
 // interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 167 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 168 as const;
 // 167: Removed the turn.regenerate operation. Older peers can no longer
 // safely interoperate because they may submit or advertise that operation.
 // 166: Connection usage reads add an operation, an accepted availability reason
diff --git a/packages/runtime-host/src/protocol/session-catalog.ts b/packages/runtime-host/src/protocol/session-catalog.ts
index eb65be7e54..52750d38e2 100644
--- a/packages/runtime-host/src/protocol/session-catalog.ts
+++ b/packages/runtime-host/src/protocol/session-catalog.ts
@@ -160,7 +160,8 @@ export interface SessionCreateInput {
   readonly modelTarget?: SessionModelTarget;
   /** Named black-box executor contributed by a Host plugin. */
   readonly executorId?: string;
-  readonly thinkingLevel?: ThinkingLevel;
+  /** Omitted applies the model preference; null explicitly uses the provider default. */
+  readonly thinkingLevel?: ThinkingLevel | null;
   readonly toolProfile?: SessionToolProfile;
   readonly permissionMode?: PermissionMode;
   readonly collaborationMode?: CollaborationMode;
@@ -549,7 +550,7 @@ export function decodeSessionCreateInput(value: unknown): SessionCreateInput {
     ...(target ? { modelTarget: target } : {}),
     ...(executorId ? { executorId } : {}),
     ...(Object.hasOwn(input, 'thinkingLevel')
-      ? { thinkingLevel: thinkingLevel(input.thinkingLevel) }
+      ? { thinkingLevel: input.thinkingLevel === null ? null : thinkingLevel(input.thinkingLevel) }
       : {}),
     ...(Object.hasOwn(input, 'toolProfile')
       ? { toolProfile: sessionToolProfile(input.toolProfile) }
diff --git a/packages/runtime-host/src/server/session-catalog-coordinator.ts b/packages/runtime-host/src/server/session-catalog-coordinator.ts
index f5bac03366..19119a4d0d 100644
--- a/packages/runtime-host/src/server/session-catalog-coordinator.ts
+++ b/packages/runtime-host/src/server/session-catalog-coordinator.ts
@@ -23,7 +23,10 @@ import { RuntimeHostProtocolError } from '../protocol/errors.js';
 import { createHash } from 'node:crypto';
 import { authorizeConnectionModel, connectionEnabledModelIds } from '@maka/core/llm-connections';
 import { isModelExplicitlyUnsupportedForChat } from '@maka/core/model-catalog';
-import { thinkingVariantsForConnection } from '@maka/core/model-thinking';
+import {
+  defaultThinkingLevelForConnection,
+  thinkingVariantsForConnection,
+} from '@maka/core/model-thinking';
 import {
   executionBoundaryDisplayMode,
   type ExecutionBoundary,
@@ -209,6 +212,7 @@ interface ResolvedSessionModel {
   readonly connectionId: string;
   readonly connectionSlug: string;
   readonly model: string;
+  readonly thinkingLevel: SessionHeader['thinkingLevel'];
 }
 
 /** A connection+model the import path may attempt, in preference order. */
@@ -349,7 +353,9 @@ export class HostSessionCatalogCoordinator {
    */
   async resolveDefaultCreateTarget(): Promise<Omit<CreateSessionInput, 'cwd' | 'name'>> {
     try {
-      return await this.#composeCreateTarget(this.#resolveModel({ kind: 'default' }, undefined));
+      return await this.#composeCreateTarget(
+        this.#resolveModel({ kind: 'default' }, undefined, true),
+      );
     } catch (error) {
       if (
         error instanceof SessionOperationFailure &&
@@ -369,6 +375,7 @@ export class HostSessionCatalogCoordinator {
       llmConnectionId: model.connectionId,
       llmConnectionSlug: model.connectionSlug,
       model: model.model,
+      ...(model.thinkingLevel === undefined ? {} : { thinkingLevel: model.thinkingLevel }),
       permissionMode: policy.policy.chatDefaults.permissionMode,
       toolMode: policy.policy.chatDefaults.codeModeEnabled ? 'code_mode' : 'direct',
       collaborationMode: 'agent',
@@ -406,7 +413,7 @@ export class HostSessionCatalogCoordinator {
           ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
           llmConnectionSlug: model.connectionSlug,
           model: model.model,
-          ...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
+          ...(model.thinkingLevel === undefined ? {} : { thinkingLevel: model.thinkingLevel }),
           ...(input.toolProfile === undefined ? {} : { toolProfile: input.toolProfile }),
           permissionMode: prepared.permissionMode ?? policy.policy.chatDefaults.permissionMode,
           toolMode: policy.policy.chatDefaults.codeModeEnabled ? 'code_mode' : 'direct',
@@ -640,7 +647,7 @@ export class HostSessionCatalogCoordinator {
               ...(model.connectionId ? { llmConnectionId: model.connectionId } : {}),
               llmConnectionSlug: model.connectionSlug,
               model: model.model,
-              ...(input.thinkingLevel === undefined ? {} : { thinkingLevel: input.thinkingLevel }),
+              ...(model.thinkingLevel === undefined ? {} : { thinkingLevel: model.thinkingLevel }),
               ...(input.toolProfile === undefined ? {} : { toolProfile: input.toolProfile }),
               permissionMode: prepared.permissionMode ?? policy.policy.chatDefaults.permissionMode,
               toolMode:
@@ -1070,6 +1077,7 @@ export class HostSessionCatalogCoordinator {
             model: candidate.modelId,
           },
           undefined,
+          true,
         );
       } catch (error) {
         if (!(error instanceof SessionOperationFailure)) throw error;
@@ -1098,7 +1106,8 @@ export class HostSessionCatalogCoordinator {
 
   async #resolveModel(
     target: SessionModelTarget,
-    thinkingLevel: SessionCreateInput['thinkingLevel'],
+    thinkingLevel: Exclude<SessionCreateInput['thinkingLevel'], null>,
+    applyConfiguredDefault = false,
   ): Promise<ResolvedSessionModel> {
     const selected = await this.#selectModelTarget(target);
     const readiness = await this.#runtimePolicy.operations.resolveExecutionConnection({
@@ -1170,25 +1179,37 @@ export class HostSessionCatalogCoordinator {
     // rejected — execution-model-authority rebuilds the runtime connection
     // from the same table, so whatever passes here is exactly what the wire
     // can send.
+    const resolvedThinkingLevel =
+      thinkingLevel ??
+      (applyConfiguredDefault
+        ? defaultThinkingLevelForConnection(
+            {
+              providerType: connection.providerType,
+              modelOverrides: connection.modelOverrides,
+            },
+            selected.modelId,
+          )
+        : undefined);
     if (
-      thinkingLevel !== undefined &&
+      resolvedThinkingLevel !== undefined &&
       !thinkingVariantsForConnection(
         {
           providerType: connection.providerType,
           modelOverrides: connection.modelOverrides,
         },
         selected.modelId,
-      ).includes(thinkingLevel)
+      ).includes(resolvedThinkingLevel)
     ) {
       throw new SessionOperationFailure(
         'invalid_request',
-        `Session model does not support thinking level ${thinkingLevel}`,
+        `Session model does not support thinking level ${resolvedThinkingLevel}`,
       );
     }
     return {
       connectionId: connection.connectionId,
       connectionSlug: connection.slug,
       model: selected.modelId,
+      thinkingLevel: resolvedThinkingLevel,
     };
   }
 
@@ -1298,6 +1319,7 @@ export class HostSessionCatalogCoordinator {
     readonly connectionId?: string;
     readonly connectionSlug: string;
     readonly model: string;
+    readonly thinkingLevel?: SessionHeader['thinkingLevel'];
   }> {
     if (input.executorId) {
       try {
@@ -1320,7 +1342,11 @@ export class HostSessionCatalogCoordinator {
         'Session creation requires a model target or executor id',
       );
     }
-    return await this.#resolveModel(input.modelTarget, input.thinkingLevel);
+    return await this.#resolveModel(
+      input.modelTarget,
+      input.thinkingLevel ?? undefined,
+      input.thinkingLevel !== null,
+    );
   }
 
   async #readRuntimePolicy(): Promise<
@@ -1406,7 +1432,7 @@ function createRequestFingerprint(
   prepared: PreparedSessionCreate,
 ): string {
   const identity = [
-    'session.create.v4',
+    'session.create.v5',
     input.sessionId,
     input.workspace.kind === 'project'
       ? ['project', input.workspace.projectId]
@@ -1423,7 +1449,7 @@ function createRequestFingerprint(
             input.modelTarget!.connectionSlug,
             input.modelTarget!.model,
           ],
-    input.thinkingLevel ?? null,
+    input.thinkingLevel === undefined ? ['model_default'] : input.thinkingLevel,
     input.toolProfile ?? null,
     prepared.permissionMode ?? ['runtime_default'],
     input.collaborationMode ?? 'agent',


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org