@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -35,6 +35,8 @@ import {
35
35
  type AgentTurnEndContext,
36
36
  AppendOnlyContextManager,
37
37
  type AsideMessage,
38
+ type BeforeToolCallContext,
39
+ type BeforeToolCallResult,
38
40
  resolveTelemetry,
39
41
  type StreamFn,
40
42
  TERMINAL_TOOL_RESULT_ABORT_REASON,
@@ -70,6 +72,7 @@ import type {
70
72
  ToolChoice,
71
73
  ToolResultMessage,
72
74
  UsageReport,
75
+ UserMessage,
73
76
  } from "@oh-my-pi/pi-ai";
74
77
  import { type Effort, streamSimple } from "@oh-my-pi/pi-ai";
75
78
  import * as AIError from "@oh-my-pi/pi-ai/error";
@@ -133,6 +136,7 @@ import type { CompactOptions, ContextUsage } from "../extensibility/extensions/t
133
136
  import type { HookCommandContext } from "../extensibility/hooks/types";
134
137
  import type { Skill, SkillWarning } from "../extensibility/skills";
135
138
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
139
+ import { normalizeToolEventInput, resolveToolEventInput } from "../extensibility/tool-event-input";
136
140
  import { GoalRuntime } from "../goals/runtime";
137
141
  import type { GoalModeState } from "../goals/state";
138
142
  import type { HindsightSessionState } from "../hindsight/state";
@@ -176,6 +180,7 @@ import {
176
180
  toReasoningEffort,
177
181
  } from "../thinking";
178
182
  import { shutdownTinyTitleClient } from "../tiny/title-client";
183
+ import { resolveApproval } from "../tools/approval";
179
184
  import { type AskToolDetails, type AskToolInput, recoverAskQuestions } from "../tools/ask";
180
185
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
181
186
  import type { CheckpointState, CompletedRewindState } from "../tools/checkpoint";
@@ -196,6 +201,7 @@ import type { EditMode } from "../utils/edit-mode";
196
201
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
197
202
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
198
203
  import { normalizeModelContextImages } from "../utils/image-loading";
204
+ import type { InspectImageMode } from "../utils/inspect-image-mode";
199
205
  import { generateSessionTitle } from "../utils/title-generator";
200
206
  import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
201
207
  import type { VibeModeState } from "../vibe/state";
@@ -429,6 +435,8 @@ export class AgentSession {
429
435
  #scheduledHiddenNextTurnGeneration: number | undefined = undefined;
430
436
  #queuedMessageDrainScheduled = false;
431
437
  #planModeState: PlanModeState | undefined;
438
+ /** Session-scoped `/vision` override; undefined = follow persisted `inspect_image.mode`. */
439
+ #inspectImageModeOverride: InspectImageMode | undefined;
432
440
  #vibeModeState: VibeModeState | undefined;
433
441
  #goalModeState: GoalModeState | undefined;
434
442
  #goalRuntime: GoalRuntime;
@@ -911,6 +919,7 @@ export class AgentSession {
911
919
  this.#models = new ModelControls(modelControlsHost, {
912
920
  scopedModels: config.scopedModels,
913
921
  thinkingLevel: config.thinkingLevel,
922
+ thinkingLevelCeiling: config.thinkingLevelCeiling,
914
923
  serviceTierByFamily: config.serviceTierByFamily,
915
924
  });
916
925
 
@@ -928,6 +937,7 @@ export class AgentSession {
928
937
  thinkingLevel: () => this.thinkingLevel,
929
938
  configuredThinkingLevel: () => this.configuredThinkingLevel(),
930
939
  setThinkingLevel: level => this.setThinkingLevel(level),
940
+ thinkingLevelCeiling: () => this.#models.thinkingLevelCeiling,
931
941
  isDisposed: () => this.#isDisposed,
932
942
  isStreaming: () => this.isStreaming,
933
943
  isCompacting: () => this.isCompacting,
@@ -1079,20 +1089,24 @@ export class AgentSession {
1079
1089
  emitNotice: (level, message, source) => this.emitNotice(level, message, source),
1080
1090
  notifyCommandMetadataChanged: () => this.#notifyCommandMetadataChanged(),
1081
1091
  localProtocolOptions: () => this.#localProtocolOptions(),
1092
+ getInspectImageModeOverride: () => this.#inspectImageModeOverride,
1093
+ setInspectImageModeOverride: mode => {
1094
+ this.#inspectImageModeOverride = mode;
1095
+ },
1082
1096
  };
1083
1097
  this.#tools = new SessionTools(sessionToolsHost, {
1084
1098
  autoApprove: config.autoApprove,
1085
1099
  toolRegistry: config.toolRegistry,
1086
1100
  createVibeTools: config.createVibeTools,
1087
1101
  createComputerTool: config.createComputerTool,
1102
+ createInspectImageTool: config.createInspectImageTool,
1088
1103
  builtInToolNames: config.builtInToolNames,
1089
1104
  presentationPinnedToolNames: config.presentationPinnedToolNames,
1090
1105
  ensureWriteRegistered: config.ensureWriteRegistered,
1091
1106
  rebuildSystemPrompt: config.rebuildSystemPrompt,
1092
1107
  getLocalCalendarDate: config.getLocalCalendarDate,
1093
1108
  getMcpServerInstructions: config.getMcpServerInstructions,
1094
- xdevRegistry: config.xdevRegistry,
1095
- initialMountedXdevToolNames: config.initialMountedXdevToolNames,
1109
+ xdev: config.xdev,
1096
1110
  setActiveToolNames: config.setActiveToolNames,
1097
1111
  baseSystemPrompt: this.agent.state.systemPrompt,
1098
1112
  skills: config.skills,
@@ -1175,6 +1189,10 @@ export class AgentSession {
1175
1189
  });
1176
1190
  // Tool-result hook owns synchronous post-tool actions that must affect the current loop.
1177
1191
  this.agent.afterToolCall = ctx => this.#afterToolCall(ctx);
1192
+ // Pre-scheduling tool_call wiring: extension handlers run at arg-prep
1193
+ // time so a block/revision lands before concurrency resolution,
1194
+ // tool_execution_start, and the wrapper's approval gate.
1195
+ this.agent.beforeToolCall = ctx => this.#beforeToolCall(ctx);
1178
1196
  this.agent.providerSessionState = this.#providerSessionState;
1179
1197
  this.#syncAgentSessionId();
1180
1198
  this.#todo.syncFromBranch();
@@ -1471,7 +1489,7 @@ export class AgentSession {
1471
1489
 
1472
1490
  this.#toolChoiceQueue.pushSequence([forced, "none"], {
1473
1491
  label: "user-force",
1474
- onRejected: () => "requeue",
1492
+ onRejected: info => (info.reason === "unavailable" ? "drop_sequence" : "requeue"),
1475
1493
  });
1476
1494
  }
1477
1495
 
@@ -2959,6 +2977,50 @@ export class AgentSession {
2959
2977
  }
2960
2978
  return this.#ttsr.afterToolCall(ctx);
2961
2979
  }
2980
+ /**
2981
+ * Emits the extension `tool_call` event for a loop-dispatched call at
2982
+ * arg-prep time — before concurrency scheduling, `tool_execution_start`,
2983
+ * and the wrapper's approval gate. A handler block becomes a blocked tool
2984
+ * result; a handler `input` revision becomes the arguments the loop
2985
+ * schedules, displays, persists, and executes, so approval resolves against
2986
+ * what actually runs. Marks the dispatch so `ExtensionToolWrapper` does not
2987
+ * emit a second event (nested xd:// device dispatches and direct non-loop
2988
+ * execution still emit there).
2989
+ */
2990
+ async #beforeToolCall(ctx: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> {
2991
+ const runner = this.#extensionRunner;
2992
+ if (!runner?.hasHandlers("tool_call")) return undefined;
2993
+ const metadata = ctx.toolCall.providerMetadata;
2994
+ const computer = metadata?.type === "computer" ? metadata : undefined;
2995
+ // Parity with the wrapper's pre-emit short-circuit: an already-denied
2996
+ // call never reaches extensions. Deny is mode-independent (tool decision
2997
+ // or user policy), so resolving under the most permissive mode is exact;
2998
+ // the wrapper still enforces the mode-accurate gate before execution.
2999
+ const userPolicies = (this.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
3000
+ const approvalArgs = computer ? { actions: computer.actions } : ctx.args;
3001
+ if (resolveApproval(ctx.tool, approvalArgs, "yolo", userPolicies).policy === "deny") {
3002
+ return undefined;
3003
+ }
3004
+ const eventArgs = computer
3005
+ ? { actions: computer.actions, pendingSafetyChecks: computer.pendingSafetyChecks }
3006
+ : ctx.args;
3007
+ runner.markToolCallEmitted(ctx.toolCall.id, ctx.tool.name);
3008
+ const callResult = await runner.emitToolCall({
3009
+ type: "tool_call",
3010
+ toolName: ctx.tool.name,
3011
+ toolCallId: ctx.toolCall.id,
3012
+ input: normalizeToolEventInput(ctx.tool.name, resolveToolEventInput(ctx.tool, eventArgs)),
3013
+ });
3014
+ if (callResult?.block) {
3015
+ return { block: true, reason: callResult.reason || "Tool execution was blocked by an extension" };
3016
+ }
3017
+ // A computer call's event input is a synthetic {actions, pendingSafetyChecks}
3018
+ // view, not the execution params — a revision cannot map back onto them.
3019
+ if (callResult?.input !== undefined && !computer) {
3020
+ return { args: callResult.input };
3021
+ }
3022
+ return undefined;
3023
+ }
2962
3024
 
2963
3025
  /** Find the last assistant message in agent state (including aborted ones) */
2964
3026
  #findLastAssistantMessage(): AssistantMessage | undefined {
@@ -3948,6 +4010,34 @@ export class AgentSession {
3948
4010
  return this.#tools.setComputerToolEnabled(enabled);
3949
4011
  }
3950
4012
 
4013
+ /**
4014
+ * Session-scoped inspect_image mode (`/vision`). `auto` clears the override
4015
+ * and returns to the persisted `inspect_image.mode` setting; `on`/`off`
4016
+ * force the tool for this session only. See {@link SessionTools.setInspectImageMode}.
4017
+ */
4018
+ setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
4019
+ return this.#tools.setInspectImageMode(mode);
4020
+ }
4021
+
4022
+ /** Effective inspect_image state for `/vision status`. */
4023
+ inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
4024
+ return this.#tools.inspectImageState();
4025
+ }
4026
+
4027
+ /** Session-scoped `/vision` override; undefined means "follow the persisted setting". */
4028
+ getInspectImageModeOverride(): InspectImageMode | undefined {
4029
+ return this.#inspectImageModeOverride;
4030
+ }
4031
+
4032
+ /**
4033
+ * Reconciles the inspect_image tool set after the persisted
4034
+ * `inspect_image.mode` setting changed (e.g. via the settings selector), so
4035
+ * the new value takes effect immediately instead of on the next model switch.
4036
+ */
4037
+ applyInspectImageModeChange(): Promise<boolean> {
4038
+ return this.#tools.reconcileInspectImageTool();
4039
+ }
4040
+
3951
4041
  /** Cancels the local rollout-memory startup owned by this session. */
3952
4042
  cancelLocalMemoryStartup(): void {
3953
4043
  this.#memory.cancelLocalMemoryStartup();
@@ -4222,6 +4312,7 @@ export class AgentSession {
4222
4312
 
4223
4313
  /** Drop mutable tool decisions and directives owned by the previous logical session. */
4224
4314
  #clearSessionScopedToolState(): void {
4315
+ this.agent.clearDeferredToolDirectives();
4225
4316
  this.#toolChoiceQueue.clear();
4226
4317
  this.#tools.clearAcpPermissionDecisions();
4227
4318
  }
@@ -5901,6 +5992,10 @@ export class AgentSession {
5901
5992
  additionalDirectories: this.settings.get("workspace.additionalDirectories"),
5902
5993
  });
5903
5994
  this.#bash.markSessionTransition(bashTransition);
5995
+ // The new session owns the transcript from here, so the previous
5996
+ // conversation's advisor spend is retired with it. Clearing at the commit
5997
+ // point keeps the status line honest even if a later step below throws.
5998
+ this.#advisors.clearCost();
5904
5999
  sessionTransitioned = true;
5905
6000
  } finally {
5906
6001
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -6323,7 +6418,7 @@ export class AgentSession {
6323
6418
  activeMessages.splice(0, activeMessages.length, ...sessionContext.messages);
6324
6419
  }
6325
6420
  this.agent.replaceMessages(activeMessages ?? sessionContext.messages);
6326
- this.#advisors.resetSessionState();
6421
+ this.#advisors.resetSessionState({ preserveCost: true });
6327
6422
  this.#todo.syncFromBranch();
6328
6423
  this.#closeCodexProviderSessionsForHistoryRewrite();
6329
6424
  this.#checkpointState = undefined;
@@ -6398,7 +6493,7 @@ export class AgentSession {
6398
6493
  return true;
6399
6494
  }
6400
6495
 
6401
- #setModelWithProviderSessionReset(model: Model): void {
6496
+ async #setModelWithProviderSessionReset(model: Model): Promise<void> {
6402
6497
  const currentModel = this.model;
6403
6498
  if (currentModel) {
6404
6499
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
@@ -6410,6 +6505,16 @@ export class AgentSession {
6410
6505
 
6411
6506
  // Re-evaluate append-only context mode — provider or setting may have changed
6412
6507
  this.#syncAppendOnlyContext(model);
6508
+
6509
+ // inspect_image auto mode keys off model image capability. Reconcile
6510
+ // centrally here so retry-fallback model changes (turn-recovery.ts),
6511
+ // which bypass syncAfterModelChange, cannot leave the tool set stale —
6512
+ // callers await, so a scheduled retry never races the reconciled slate.
6513
+ try {
6514
+ await this.#tools.reconcileInspectImageAfterModelChange();
6515
+ } catch (error) {
6516
+ logger.warn("inspect_image reconcile after model change failed", { error: String(error) });
6517
+ }
6413
6518
  }
6414
6519
 
6415
6520
  #closeCodexProviderSessionsForHistoryRewrite(): void {
@@ -6941,7 +7046,7 @@ export class AgentSession {
6941
7046
  }
6942
7047
 
6943
7048
  this.agent.replaceMessages(sessionContext.messages);
6944
- this.#advisors.resetSessionState();
7049
+ this.#advisors.resetSessionState({ preserveCost: true });
6945
7050
  this.#todo.syncFromBranch();
6946
7051
  if (switchingToDifferentSession) {
6947
7052
  this.#closeAllProviderSessions("session switch");
@@ -6974,7 +7079,7 @@ export class AgentSession {
6974
7079
  currentModel.id !== match.id ||
6975
7080
  currentModel.api !== match.api));
6976
7081
  if (shouldResetProviderState) {
6977
- this.#setModelWithProviderSessionReset(match);
7082
+ await this.#setModelWithProviderSessionReset(match);
6978
7083
  } else {
6979
7084
  this.agent.setModel(match);
6980
7085
  }
@@ -7050,6 +7155,9 @@ export class AgentSession {
7050
7155
  error: String(refreshErr),
7051
7156
  });
7052
7157
  }
7158
+ // Only a committed switch retires the previous conversation's advisor spend:
7159
+ // an earlier clear would be lost work if any step above rolled the switch back.
7160
+ if (switchingToDifferentSession) this.#advisors.clearCost();
7053
7161
  this.#bash.finishSessionTransition(bashTransition, true);
7054
7162
  return true;
7055
7163
  } catch (error) {
@@ -7098,10 +7206,12 @@ export class AgentSession {
7098
7206
  * @param entryId ID of the entry to branch from
7099
7207
  * @returns Object with:
7100
7208
  * - selectedText: The text of the selected user message (for editor pre-fill)
7209
+ * - selectedImages: Image attachments of the selected user message (for editor draft restore)
7101
7210
  * - cancelled: True if a hook cancelled the branch
7102
7211
  */
7103
7212
  async branch(entryId: string): Promise<{
7104
7213
  selectedText: string;
7214
+ selectedImages: ImageContent[];
7105
7215
  cancelled: boolean;
7106
7216
  }> {
7107
7217
  const previousSessionFile = this.sessionFile;
@@ -7112,6 +7222,7 @@ export class AgentSession {
7112
7222
  }
7113
7223
 
7114
7224
  const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
7225
+ const selectedImages = this.#extractUserMessageImages(selectedEntry.message.content);
7115
7226
 
7116
7227
  let skipConversationRestore = false;
7117
7228
 
@@ -7123,7 +7234,7 @@ export class AgentSession {
7123
7234
  })) as SessionBeforeBranchResult | undefined;
7124
7235
 
7125
7236
  if (result?.cancel) {
7126
- return { selectedText, cancelled: true };
7237
+ return { selectedText, selectedImages, cancelled: true };
7127
7238
  }
7128
7239
  skipConversationRestore = result?.skipConversationRestore ?? false;
7129
7240
  }
@@ -7148,6 +7259,7 @@ export class AgentSession {
7148
7259
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
7149
7260
  }
7150
7261
  this.#bash.markSessionTransition(bashTransition);
7262
+ this.#advisors.clearCost();
7151
7263
  sessionTransitioned = true;
7152
7264
  } finally {
7153
7265
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -7178,7 +7290,7 @@ export class AgentSession {
7178
7290
  this.#closeCodexProviderSessionsForHistoryRewrite();
7179
7291
  }
7180
7292
 
7181
- return { selectedText, cancelled: false };
7293
+ return { selectedText, selectedImages, cancelled: false };
7182
7294
  }
7183
7295
 
7184
7296
  async branchFromBtw(
@@ -7245,6 +7357,7 @@ export class AgentSession {
7245
7357
  try {
7246
7358
  this.sessionManager.createBranchedSession(leafId);
7247
7359
  this.#bash.markSessionTransition(bashTransition);
7360
+ this.#advisors.clearCost();
7248
7361
  sessionTransitioned = true;
7249
7362
  } finally {
7250
7363
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -7292,7 +7405,7 @@ export class AgentSession {
7292
7405
  * @param targetId The entry ID to navigate to
7293
7406
  * @param options.summarize Whether user wants to summarize abandoned branch
7294
7407
  * @param options.customInstructions Custom instructions for summarizer
7295
- * @returns Result with editorText (if user message) and cancelled status
7408
+ * @returns Result with editorText/editorImages (if user message) and cancelled status
7296
7409
  */
7297
7410
  async navigateTree(
7298
7411
  targetId: string,
@@ -7322,6 +7435,8 @@ export class AgentSession {
7322
7435
  } = {},
7323
7436
  ): Promise<{
7324
7437
  editorText?: string;
7438
+ /** Image attachments of the target user message, parallel to the positional `[Image #N]` markers in {@link editorText}. */
7439
+ editorImages?: ImageContent[];
7325
7440
  cancelled: boolean;
7326
7441
  aborted?: boolean;
7327
7442
  summaryEntry?: BranchSummaryEntry;
@@ -7494,6 +7609,7 @@ export class AgentSession {
7494
7609
  // Determine the new leaf position based on target type
7495
7610
  let newLeafId: string | null;
7496
7611
  let editorText: string | undefined;
7612
+ let editorImages: ImageContent[] | undefined;
7497
7613
  // Set when the second-pass `ask` re-answer branch below actually commits a
7498
7614
  // new sibling answer — the trigger for resuming the agent afterwards so the
7499
7615
  // model consumes it, mirroring a live `ask` completion (issue #6483).
@@ -7503,6 +7619,8 @@ export class AgentSession {
7503
7619
  // User message: leaf = parent (null if root), text goes to editor
7504
7620
  newLeafId = targetEntry.parentId;
7505
7621
  editorText = this.#extractUserMessageText(targetEntry.message.content);
7622
+ const targetImages = this.#extractUserMessageImages(targetEntry.message.content);
7623
+ if (targetImages.length > 0) editorImages = targetImages;
7506
7624
  } else if (targetEntry.type === "custom_message" && targetEntry.customType !== SKILL_PROMPT_MESSAGE_TYPE) {
7507
7625
  // Custom message: leaf = parent (null if root), text goes to editor
7508
7626
  newLeafId = targetEntry.parentId;
@@ -7574,7 +7692,7 @@ export class AgentSession {
7574
7692
  const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
7575
7693
  this.agent.replaceMessages(displayContext.messages);
7576
7694
  this.#rehydrateCheckpointRewindState();
7577
- this.#advisors.resetSessionState();
7695
+ this.#advisors.resetSessionState({ preserveCost: true });
7578
7696
  this.#todo.syncFromBranch();
7579
7697
  this.#closeCodexProviderSessionsForHistoryRewrite();
7580
7698
 
@@ -7602,6 +7720,7 @@ export class AgentSession {
7602
7720
  const rawContext = this.sessionManager.buildSessionContext();
7603
7721
  return {
7604
7722
  editorText,
7723
+ editorImages,
7605
7724
  cancelled: false,
7606
7725
  summaryEntry,
7607
7726
  sessionContext: rawContext,
@@ -7610,6 +7729,7 @@ export class AgentSession {
7610
7729
  }
7611
7730
  return {
7612
7731
  editorText,
7732
+ editorImages,
7613
7733
  cancelled: false,
7614
7734
  summaryEntry,
7615
7735
  sessionContext: stateContext,
@@ -7728,6 +7848,14 @@ export class AgentSession {
7728
7848
  return "";
7729
7849
  }
7730
7850
 
7851
+ /** Image parts of a stored user message, in submission order — index N-1 backs the
7852
+ * `[Image #N]` marker in the message text, so restoring them alongside the text keeps
7853
+ * positional markers resolvable on resubmit. */
7854
+ #extractUserMessageImages(content: UserMessage["content"]): ImageContent[] {
7855
+ if (!Array.isArray(content)) return [];
7856
+ return content.filter((c): c is ImageContent => c.type === "image");
7857
+ }
7858
+
7731
7859
  /**
7732
7860
  * Get session statistics.
7733
7861
  */
@@ -8205,6 +8333,11 @@ export class AgentSession {
8205
8333
  getAdvisorStatusOverview(): { configured: boolean; advisors: { name: string; status: AdvisorRuntimeStatus }[] } {
8206
8334
  return this.#advisors.getAdvisorStatusOverview();
8207
8335
  }
8336
+
8337
+ /** Return cumulative cost recorded for the current session's advisor activity. */
8338
+ getAdvisorCost(): number {
8339
+ return this.#advisors.getAdvisorCost();
8340
+ }
8208
8341
  /**
8209
8342
  * Return structured advisor stats for the status command and TUI panel.
8210
8343
  */
@@ -27,6 +27,7 @@ import {
27
27
  AUTO_THINKING,
28
28
  type ConfiguredThinkingLevel,
29
29
  clampAutoThinkingEffort,
30
+ clampThinkingLevelToCeiling,
30
31
  resolveProvisionalAutoLevel,
31
32
  resolveThinkingLevelForModel,
32
33
  shouldDisableReasoning,
@@ -51,7 +52,7 @@ export interface ModelControlsHost {
51
52
  promptGeneration(): number;
52
53
  resolveActiveEditMode(): EditMode;
53
54
  syncAfterModelChange(previousEditMode: EditMode): Promise<void>;
54
- setModelWithProviderSessionReset(model: Model): void;
55
+ setModelWithProviderSessionReset(model: Model): Promise<void>;
55
56
  clearActiveRetryFallback(): void;
56
57
  clearInheritedProviderPromptCacheKey(): void;
57
58
  magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean;
@@ -65,6 +66,8 @@ export class ModelControls {
65
66
  readonly #host: ModelControlsHost;
66
67
  #scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
67
68
  #thinkingLevel: ThinkingLevel | undefined;
69
+ /** Hard per-session effort ceiling (e.g. a task spawn's `task.maxEffort` cap); recovery paths re-clamp to it. */
70
+ readonly #thinkingLevelCeiling: Effort | undefined;
68
71
  #autoThinking = false;
69
72
  #autoResolvedLevel: Effort | undefined;
70
73
  #serviceTierByFamily: ServiceTierByFamily;
@@ -74,18 +77,28 @@ export class ModelControls {
74
77
  options: {
75
78
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
76
79
  thinkingLevel?: ConfiguredThinkingLevel;
80
+ thinkingLevelCeiling?: Effort;
77
81
  serviceTierByFamily?: ServiceTierByFamily;
78
82
  },
79
83
  ) {
80
84
  this.#host = host;
81
85
  this.#scopedModels = options.scopedModels ?? [];
82
86
  this.#serviceTierByFamily = options.serviceTierByFamily ?? {};
87
+ this.#thinkingLevelCeiling = options.thinkingLevelCeiling;
83
88
  if (options.thinkingLevel === AUTO_THINKING) {
84
89
  // Keep auto pending until the first turn while exposing a valid wire effort.
85
90
  this.#autoThinking = true;
86
- this.#thinkingLevel = resolveProvisionalAutoLevel(this.#model);
91
+ this.#thinkingLevel = clampThinkingLevelToCeiling(
92
+ this.#model,
93
+ resolveProvisionalAutoLevel(this.#model),
94
+ this.#thinkingLevelCeiling,
95
+ );
87
96
  } else {
88
- this.#thinkingLevel = options.thinkingLevel;
97
+ this.#thinkingLevel = clampThinkingLevelToCeiling(
98
+ this.#model,
99
+ options.thinkingLevel,
100
+ this.#thinkingLevelCeiling,
101
+ );
89
102
  }
90
103
  this.#applyThinkingLevelToAgent(this.#thinkingLevel);
91
104
  }
@@ -99,6 +112,11 @@ export class ModelControls {
99
112
  return this.#thinkingLevel;
100
113
  }
101
114
 
115
+ /** Hard per-session effort ceiling every thinking-level change is clamped to. */
116
+ get thinkingLevelCeiling(): Effort | undefined {
117
+ return this.#thinkingLevelCeiling;
118
+ }
119
+
102
120
  /** Configured selector, preserving `auto` while classification is active. */
103
121
  configuredThinkingLevel(): ConfiguredThinkingLevel | undefined {
104
122
  return this.#autoThinking ? AUTO_THINKING : this.#thinkingLevel;
@@ -130,8 +148,15 @@ export class ModelControls {
130
148
  this.#autoResolvedLevel = undefined;
131
149
  this.#thinkingLevel =
132
150
  level === AUTO_THINKING
133
- ? resolveProvisionalAutoLevel(this.#model)
134
- : resolveThinkingLevelForModel(this.#model, level);
151
+ ? clampThinkingLevelToCeiling(
152
+ this.#model,
153
+ resolveProvisionalAutoLevel(this.#model),
154
+ this.#thinkingLevelCeiling,
155
+ )
156
+ : resolveThinkingLevelForModel(
157
+ this.#model,
158
+ clampThinkingLevelToCeiling(this.#model, level, this.#thinkingLevelCeiling),
159
+ );
135
160
  this.#applyThinkingLevelToAgent(this.#thinkingLevel);
136
161
  }
137
162
 
@@ -195,7 +220,7 @@ export class ModelControls {
195
220
 
196
221
  this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
197
222
  this.#host.clearActiveRetryFallback();
198
- this.#host.setModelWithProviderSessionReset(targetModel);
223
+ await this.#host.setModelWithProviderSessionReset(targetModel);
199
224
  this.#host.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
200
225
  if (options?.persist) {
201
226
  this.#host.settings.setModelRole(
@@ -240,7 +265,7 @@ export class ModelControls {
240
265
 
241
266
  this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
242
267
  this.#host.clearActiveRetryFallback();
243
- this.#host.setModelWithProviderSessionReset(targetModel);
268
+ await this.#host.setModelWithProviderSessionReset(targetModel);
244
269
  this.#host.sessionManager.appendModelChange(
245
270
  `${targetModel.provider}/${targetModel.id}`,
246
271
  options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
@@ -400,7 +425,7 @@ export class ModelControls {
400
425
  // Apply model
401
426
  this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
402
427
  this.#host.clearActiveRetryFallback();
403
- this.#host.setModelWithProviderSessionReset(next.model);
428
+ await this.#host.setModelWithProviderSessionReset(next.model);
404
429
  this.#host.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
405
430
  this.#host.settings.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
406
431
 
@@ -431,7 +456,7 @@ export class ModelControls {
431
456
 
432
457
  this.#host.modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
433
458
  this.#host.clearActiveRetryFallback();
434
- this.#host.setModelWithProviderSessionReset(nextModel);
459
+ await this.#host.setModelWithProviderSessionReset(nextModel);
435
460
  this.#host.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
436
461
  this.#host.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
437
462
  // Re-apply the current thinking level (or auto) for the newly selected model
@@ -469,7 +494,11 @@ export class ModelControls {
469
494
  */
470
495
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined, persist: boolean = false): void {
471
496
  if (level === AUTO_THINKING) {
472
- const provisional = resolveProvisionalAutoLevel(this.#model);
497
+ const provisional = clampThinkingLevelToCeiling(
498
+ this.#model,
499
+ resolveProvisionalAutoLevel(this.#model),
500
+ this.#thinkingLevelCeiling,
501
+ );
473
502
  const wasAuto = this.#autoThinking;
474
503
  const previousLevel = this.#thinkingLevel;
475
504
  this.#autoThinking = true;
@@ -493,7 +522,10 @@ export class ModelControls {
493
522
  const wasAuto = this.#autoThinking;
494
523
  this.#autoThinking = false;
495
524
  this.#autoResolvedLevel = undefined;
496
- const effectiveLevel = resolveThinkingLevelForModel(this.#model, level);
525
+ const effectiveLevel = resolveThinkingLevelForModel(
526
+ this.#model,
527
+ clampThinkingLevelToCeiling(this.#model, level, this.#thinkingLevelCeiling),
528
+ );
497
529
  // Leaving auto must persist even when the resolved effort is unchanged (e.g.
498
530
  // auto resolved to medium, then the user pins medium): otherwise the latest
499
531
  // session entry keeps `configured: "auto"` and resume re-enables auto.
@@ -591,7 +623,11 @@ export class ModelControls {
591
623
  // Drop the result if the turn was aborted/superseded while classifying.
592
624
  if (this.#host.promptGeneration() !== generation || !this.#autoThinking) return;
593
625
 
594
- const effort = resolved ?? resolveProvisionalAutoLevel(model);
626
+ const effort = clampThinkingLevelToCeiling(
627
+ model,
628
+ resolved ?? resolveProvisionalAutoLevel(model),
629
+ this.#thinkingLevelCeiling,
630
+ );
595
631
  if (effort === undefined) return;
596
632
  const shouldPersistResolution = this.#thinkingLevel !== effort;
597
633
  this.#autoResolvedLevel = effort;