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

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  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 +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -1,6 +1,7 @@
1
1
  import type { Agent, AgentMessage, AgentTool, StreamFn, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type {
3
3
  Context,
4
+ Effort,
4
5
  ImageContent,
5
6
  Message,
6
7
  MessageAttribution,
@@ -105,6 +106,8 @@ export interface AgentSessionConfig {
105
106
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
106
107
  /** Initial session thinking selector. */
107
108
  thinkingLevel?: ConfiguredThinkingLevel;
109
+ /** Hard ceiling on the session's thinking effort (e.g. a task spawn's `task.maxEffort`-capped hint); every later change, including retry-fallback recovery, is re-clamped to it. */
110
+ thinkingLevelCeiling?: Effort;
108
111
  /** Retry chain ownership when startup selected one of its fallback entries. */
109
112
  initialRetryFallback?: InitialRetryFallbackState;
110
113
  /** Prewalk from the starting model to a fast/cheap target after implementation begins. */
@@ -911,6 +911,7 @@ export class AgentSession {
911
911
  this.#models = new ModelControls(modelControlsHost, {
912
912
  scopedModels: config.scopedModels,
913
913
  thinkingLevel: config.thinkingLevel,
914
+ thinkingLevelCeiling: config.thinkingLevelCeiling,
914
915
  serviceTierByFamily: config.serviceTierByFamily,
915
916
  });
916
917
 
@@ -928,6 +929,7 @@ export class AgentSession {
928
929
  thinkingLevel: () => this.thinkingLevel,
929
930
  configuredThinkingLevel: () => this.configuredThinkingLevel(),
930
931
  setThinkingLevel: level => this.setThinkingLevel(level),
932
+ thinkingLevelCeiling: () => this.#models.thinkingLevelCeiling,
931
933
  isDisposed: () => this.#isDisposed,
932
934
  isStreaming: () => this.isStreaming,
933
935
  isCompacting: () => this.isCompacting,
@@ -1471,7 +1473,7 @@ export class AgentSession {
1471
1473
 
1472
1474
  this.#toolChoiceQueue.pushSequence([forced, "none"], {
1473
1475
  label: "user-force",
1474
- onRejected: () => "requeue",
1476
+ onRejected: info => (info.reason === "unavailable" ? "drop_sequence" : "requeue"),
1475
1477
  });
1476
1478
  }
1477
1479
 
@@ -4222,6 +4224,7 @@ export class AgentSession {
4222
4224
 
4223
4225
  /** Drop mutable tool decisions and directives owned by the previous logical session. */
4224
4226
  #clearSessionScopedToolState(): void {
4227
+ this.agent.clearDeferredToolDirectives();
4225
4228
  this.#toolChoiceQueue.clear();
4226
4229
  this.#tools.clearAcpPermissionDecisions();
4227
4230
  }
@@ -5901,6 +5904,10 @@ export class AgentSession {
5901
5904
  additionalDirectories: this.settings.get("workspace.additionalDirectories"),
5902
5905
  });
5903
5906
  this.#bash.markSessionTransition(bashTransition);
5907
+ // The new session owns the transcript from here, so the previous
5908
+ // conversation's advisor spend is retired with it. Clearing at the commit
5909
+ // point keeps the status line honest even if a later step below throws.
5910
+ this.#advisors.clearCost();
5904
5911
  sessionTransitioned = true;
5905
5912
  } finally {
5906
5913
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -6323,7 +6330,7 @@ export class AgentSession {
6323
6330
  activeMessages.splice(0, activeMessages.length, ...sessionContext.messages);
6324
6331
  }
6325
6332
  this.agent.replaceMessages(activeMessages ?? sessionContext.messages);
6326
- this.#advisors.resetSessionState();
6333
+ this.#advisors.resetSessionState({ preserveCost: true });
6327
6334
  this.#todo.syncFromBranch();
6328
6335
  this.#closeCodexProviderSessionsForHistoryRewrite();
6329
6336
  this.#checkpointState = undefined;
@@ -6941,7 +6948,7 @@ export class AgentSession {
6941
6948
  }
6942
6949
 
6943
6950
  this.agent.replaceMessages(sessionContext.messages);
6944
- this.#advisors.resetSessionState();
6951
+ this.#advisors.resetSessionState({ preserveCost: true });
6945
6952
  this.#todo.syncFromBranch();
6946
6953
  if (switchingToDifferentSession) {
6947
6954
  this.#closeAllProviderSessions("session switch");
@@ -7050,6 +7057,9 @@ export class AgentSession {
7050
7057
  error: String(refreshErr),
7051
7058
  });
7052
7059
  }
7060
+ // Only a committed switch retires the previous conversation's advisor spend:
7061
+ // an earlier clear would be lost work if any step above rolled the switch back.
7062
+ if (switchingToDifferentSession) this.#advisors.clearCost();
7053
7063
  this.#bash.finishSessionTransition(bashTransition, true);
7054
7064
  return true;
7055
7065
  } catch (error) {
@@ -7148,6 +7158,7 @@ export class AgentSession {
7148
7158
  this.sessionManager.createBranchedSession(selectedEntry.parentId);
7149
7159
  }
7150
7160
  this.#bash.markSessionTransition(bashTransition);
7161
+ this.#advisors.clearCost();
7151
7162
  sessionTransitioned = true;
7152
7163
  } finally {
7153
7164
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -7245,6 +7256,7 @@ export class AgentSession {
7245
7256
  try {
7246
7257
  this.sessionManager.createBranchedSession(leafId);
7247
7258
  this.#bash.markSessionTransition(bashTransition);
7259
+ this.#advisors.clearCost();
7248
7260
  sessionTransitioned = true;
7249
7261
  } finally {
7250
7262
  this.#bash.finishSessionTransition(bashTransition, sessionTransitioned);
@@ -7574,7 +7586,7 @@ export class AgentSession {
7574
7586
  const displayContext = deobfuscateSessionContext(stateContext, this.#obfuscator);
7575
7587
  this.agent.replaceMessages(displayContext.messages);
7576
7588
  this.#rehydrateCheckpointRewindState();
7577
- this.#advisors.resetSessionState();
7589
+ this.#advisors.resetSessionState({ preserveCost: true });
7578
7590
  this.#todo.syncFromBranch();
7579
7591
  this.#closeCodexProviderSessionsForHistoryRewrite();
7580
7592
 
@@ -8205,6 +8217,11 @@ export class AgentSession {
8205
8217
  getAdvisorStatusOverview(): { configured: boolean; advisors: { name: string; status: AdvisorRuntimeStatus }[] } {
8206
8218
  return this.#advisors.getAdvisorStatusOverview();
8207
8219
  }
8220
+
8221
+ /** Return cumulative cost recorded for the current session's advisor activity. */
8222
+ getAdvisorCost(): number {
8223
+ return this.#advisors.getAdvisorCost();
8224
+ }
8208
8225
  /**
8209
8226
  * Return structured advisor stats for the status command and TUI panel.
8210
8227
  */
@@ -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,
@@ -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
 
@@ -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;
@@ -254,6 +254,7 @@ export class SessionAdvisors {
254
254
  #advisorConfigs: AdvisorConfig[] | undefined;
255
255
  #advisorStatuses = new Map<string, { name: string; status: AdvisorRuntimeStatus }>();
256
256
  #advisorProviderSessionIds = new Map<string, string>();
257
+ #advisorCosts = new Map<string, number>();
257
258
  #advisorRecorderClosed: Promise<void> = Promise.resolve();
258
259
  #advisorAutoResumeSuppressed = false;
259
260
  #preserveAdvisorAdvice = false;
@@ -326,8 +327,13 @@ export class SessionAdvisors {
326
327
  }
327
328
 
328
329
  /** Re-primes advisor transcript views across a conversation boundary. */
329
- resetSessionState(): void {
330
- this.#resetAdvisorSessionState();
330
+ resetSessionState(options: { preserveCost?: boolean } = {}): void {
331
+ this.#resetAdvisorSessionState(options.preserveCost === true);
332
+ }
333
+
334
+ /** Drop the recorded spend once a conversation boundary has committed. */
335
+ clearCost(): void {
336
+ this.#advisorCosts.clear();
331
337
  }
332
338
 
333
339
  /**
@@ -451,7 +457,8 @@ export class SessionAdvisors {
451
457
  * agent steer/follow-up queues, and preserved cards deferred to the next turn —
452
458
  * so none of them inject into the new conversation.
453
459
  */
454
- #resetAdvisorSessionState(): void {
460
+ #resetAdvisorSessionState(preserveCost: boolean): void {
461
+ if (!preserveCost) this.#advisorCosts.clear();
455
462
  // Mute the recorder across the re-prime: AdvisorRuntime.reset() aborts the advisor
456
463
  // loop, and that abort can emit an `aborted` message_end we must not attribute to
457
464
  // either session's transcript. Detach, reset, then re-attach the live agent's feed.
@@ -959,8 +966,7 @@ export class SessionAdvisors {
959
966
  .catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
960
967
  }
961
968
 
962
- /** Re-prime every advisor's transcript view (compaction/shake/rewind) without the
963
- * session-level latch reset {@link #resetAdvisorSessionState} performs. */
969
+ /** Re-prime every advisor's transcript view after an in-conversation history rewrite. */
964
970
  #resetAllAdvisorRuntimes(): void {
965
971
  for (const a of this.#advisors) a.runtime.reset();
966
972
  }
@@ -985,12 +991,18 @@ export class SessionAdvisors {
985
991
  this.#advisorYieldQueueUnsubscribe = undefined;
986
992
  }
987
993
 
994
+ #recordAdvisorCost(advisor: ActiveAdvisor, message: AssistantMessage): void {
995
+ this.#advisorCosts.set(advisor.slug, (this.#advisorCosts.get(advisor.slug) ?? 0) + message.usage.cost.total);
996
+ }
997
+
988
998
  /** Subscribe the advisor agent's finalized messages into the transcript recorder.
989
999
  * Idempotent-by-replacement: callers detach the prior feed first. Kept separate
990
1000
  * so the re-prime path can mute the feed across an abort-driven reset. */
991
1001
  #attachAdvisorRecorderFeed(advisor: ActiveAdvisor): void {
992
1002
  advisor.agentUnsubscribe = advisor.agent.subscribe(event => {
993
- if (event.type === "message_end") advisor.recorder.record(event.message);
1003
+ if (event.type !== "message_end") return;
1004
+ if (event.message.role === "assistant") this.#recordAdvisorCost(advisor, event.message);
1005
+ advisor.recorder.record(event.message);
994
1006
  });
995
1007
  }
996
1008
 
@@ -1507,6 +1519,13 @@ export class SessionAdvisors {
1507
1519
  }));
1508
1520
  return { configured: this.#advisorEnabled, advisors };
1509
1521
  }
1522
+
1523
+ /** Return cumulative advisor cost recorded for the current session. */
1524
+ getAdvisorCost(): number {
1525
+ let cost = 0;
1526
+ for (const advisorCost of this.#advisorCosts.values()) cost += advisorCost;
1527
+ return cost;
1528
+ }
1510
1529
  /**
1511
1530
  * Return structured advisor stats for the status command and TUI panel.
1512
1531
  */
@@ -1530,12 +1549,13 @@ export class SessionAdvisors {
1530
1549
  contextWindow: 0,
1531
1550
  contextTokens: 0,
1532
1551
  tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1533
- cost: 0,
1552
+ cost: this.#advisorCosts.get(slug) ?? 0,
1534
1553
  messages: { user: 0, assistant: 0, total: 0 },
1535
1554
  });
1536
1555
  }
1537
1556
  }
1538
1557
  const active = liveAdvisors.length > 0;
1558
+ const cost = this.getAdvisorCost();
1539
1559
  if (liveAdvisors.length === 0) {
1540
1560
  return {
1541
1561
  configured,
@@ -1543,14 +1563,13 @@ export class SessionAdvisors {
1543
1563
  contextWindow: 0,
1544
1564
  contextTokens: 0,
1545
1565
  tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
1546
- cost: 0,
1566
+ cost,
1547
1567
  messages: { user: 0, assistant: 0, total: 0 },
1548
1568
  advisors: roster,
1549
1569
  };
1550
1570
  }
1551
1571
  const tokens = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
1552
1572
  const messages = { user: 0, assistant: 0, total: 0 };
1553
- let cost = 0;
1554
1573
  let contextTokens = 0;
1555
1574
  for (const a of liveAdvisors) {
1556
1575
  tokens.input += a.tokens.input;
@@ -1562,7 +1581,6 @@ export class SessionAdvisors {
1562
1581
  messages.user += a.messages.user;
1563
1582
  messages.assistant += a.messages.assistant;
1564
1583
  messages.total += a.messages.total;
1565
- cost += a.cost;
1566
1584
  contextTokens += a.contextTokens;
1567
1585
  }
1568
1586
  // Single-advisor displays read the top-level model/window directly; surface the
@@ -1591,7 +1609,6 @@ export class SessionAdvisors {
1591
1609
  let cacheRead = 0;
1592
1610
  let cacheWrite = 0;
1593
1611
  let totalTokens = 0;
1594
- let cost = 0;
1595
1612
  let user = 0;
1596
1613
  let assistant = 0;
1597
1614
  for (const message of messages) {
@@ -1605,7 +1622,6 @@ export class SessionAdvisors {
1605
1622
  cacheRead += assistantMsg.usage.cacheRead;
1606
1623
  cacheWrite += assistantMsg.usage.cacheWrite;
1607
1624
  totalTokens += assistantMsg.usage.totalTokens;
1608
- cost += assistantMsg.usage.cost.total;
1609
1625
  }
1610
1626
  }
1611
1627
  return {
@@ -1619,7 +1635,7 @@ export class SessionAdvisors {
1619
1635
  contextWindow: model.contextWindow ?? 0,
1620
1636
  contextTokens,
1621
1637
  tokens: { input, output, reasoning, cacheRead, cacheWrite, total: totalTokens },
1622
- cost,
1638
+ cost: this.#advisorCosts.get(advisor.slug) ?? 0,
1623
1639
  messages: { user, assistant, total: messages.length },
1624
1640
  sessionId: advisor.agent.sessionId,
1625
1641
  };
@@ -1649,7 +1665,7 @@ export class SessionAdvisors {
1649
1665
  const spendParts = [`${s.tokens.input.toLocaleString()} input`, `${s.tokens.output.toLocaleString()} output`];
1650
1666
  if (s.tokens.cacheRead > 0) spendParts.push(`${s.tokens.cacheRead.toLocaleString()} cache read`);
1651
1667
  if (s.tokens.cacheWrite > 0) spendParts.push(`${s.tokens.cacheWrite.toLocaleString()} cache write`);
1652
- const spendLine = `Spend: ${spendParts.join(", ")}, $${s.cost.toFixed(4)}`;
1668
+ const spendLine = `Spend: ${spendParts.join(", ")}, $${stats.cost.toFixed(4)}`;
1653
1669
  if (!s.model || s.status !== "running") return `Advisor "${s.name}" is ${s.status.replace("_", " ")}.`;
1654
1670
  return `Advisor is enabled (${s.model.provider}/${s.model.id}). ${contextLine}. ${spendLine}.`;
1655
1671
  }
@@ -11,6 +11,7 @@ import type { ExtensionRunner } from "../extensibility/extensions";
11
11
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
12
12
  import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
13
13
  import { type LocalProtocolOptions, XD_URL_PREFIX } from "../internal-urls";
14
+ import { deduplicateMCPToolsByName } from "../mcp/tool-bridge";
14
15
  import { resolveMemoryBackend } from "../memory-backend/resolve";
15
16
  import { MEMORY_BACKEND_TOOL_NAMES } from "../memory-backend/tool-names";
16
17
  import type { MemoryBackendStartOptions } from "../memory-backend/types";
@@ -1040,7 +1041,8 @@ export class SessionTools {
1040
1041
  });
1041
1042
 
1042
1043
  const extensionRunner = this.#host.extensionRunner();
1043
- for (const customTool of mcpTools) {
1044
+ const uniqueMcpTools = deduplicateMCPToolsByName(mcpTools);
1045
+ for (const customTool of uniqueMcpTools) {
1044
1046
  const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
1045
1047
  const finalTool = (
1046
1048
  extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped
@@ -1050,7 +1052,7 @@ export class SessionTools {
1050
1052
 
1051
1053
  // Every connected MCP tool is selected; centralized repartitioning owns
1052
1054
  // presentation pins and write-transport activation/removal.
1053
- const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...mcpTools.map(tool => tool.name)])];
1055
+ const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...uniqueMcpTools.map(tool => tool.name)])];
1054
1056
  try {
1055
1057
  await this.applyActiveToolsByName(nextActive);
1056
1058
  if (this.#host.isDisposed()) restorePreviousMcpTools();
@@ -13,15 +13,15 @@ export interface RejectInfo {
13
13
  reason: "aborted" | "error" | "cleared" | "removed" | "unavailable" | "not_invoked";
14
14
  }
15
15
 
16
- /** "requeue" replays the lost yield next turn; "drop" (or void/undefined) discards it. */
17
- export type RejectOutcome = "requeue" | "drop";
16
+ /** Controls whether rejection replays a yield, drops it, or drops its remaining sequence. */
17
+ export type RejectOutcome = "requeue" | "drop" | "drop_sequence";
18
18
 
19
19
  export interface DirectiveCallbacks {
20
20
  /** Fires when the yield completed; onInvoked directives require the requested tool to run first. */
21
21
  onResolved?: (info: ResolveInfo) => void;
22
22
  /**
23
- * Fires when the yield is being discarded. Return "requeue" to replay the
24
- * same value at the head of the queue for the next turn. Default: "drop".
23
+ * Return "requeue" to replay the same value, or "drop_sequence" to discard
24
+ * its directive including later yields. Default: drop the rejected yield.
25
25
  */
26
26
  onRejected?: (info: RejectInfo) => RejectOutcome | undefined;
27
27
  /**
@@ -39,6 +39,8 @@ export interface ToolChoiceDirective {
39
39
  /** Stable label for targeted removal and debugging (e.g. "user-force"). */
40
40
  label: string;
41
41
  callbacks: DirectiveCallbacks;
42
+ /** Original multi-yield directive retained across one-yield replays. */
43
+ sequenceRoot?: ToolChoiceDirective;
42
44
  }
43
45
 
44
46
  export interface PushOptions {
@@ -83,6 +85,12 @@ interface PendingInvoker {
83
85
 
84
86
  export class ToolChoiceQueue {
85
87
  #queue: ToolChoiceDirective[] = [];
88
+ /**
89
+ * In-flight yield awaiting resolve()/reject(). May outlive the run that
90
+ * claimed it: a pre-model gate stop ends the run without a `turn_end`, and
91
+ * the claim is finalized later — by the next admitted turn's `turn_end`,
92
+ * the abort safety net, or an `unavailable` rejection on redeem.
93
+ */
86
94
  #inFlight: InFlight | undefined;
87
95
  /**
88
96
  * Label of the directive whose last yield was resolved this turn.
@@ -178,6 +186,12 @@ export class ToolChoiceQueue {
178
186
  reason,
179
187
  });
180
188
 
189
+ if (outcome === "drop_sequence") {
190
+ const root = inFlight.directive.sequenceRoot ?? inFlight.directive;
191
+ this.#queue = this.#queue.filter(directive => directive !== root && directive.sequenceRoot !== root);
192
+ return;
193
+ }
194
+
181
195
  if (outcome === "requeue") {
182
196
  // Re-queue only the lost yield, not the rest of the sequence. Carry forward
183
197
  // callbacks so the replayed yield still executes and finalizes correctly,
@@ -185,6 +199,7 @@ export class ToolChoiceQueue {
185
199
  this.#queue.unshift({
186
200
  generator: onceGen(inFlight.yielded),
187
201
  label: `${inFlight.directive.label}-requeued`,
202
+ sequenceRoot: inFlight.directive.sequenceRoot ?? inFlight.directive,
188
203
  callbacks: {
189
204
  onResolved: inFlight.directive.callbacks.onResolved,
190
205
  onInvoked: inFlight.directive.callbacks.onInvoked,
@@ -11,6 +11,7 @@ import type {
11
11
  AssistantRetryRecovery,
12
12
  AssistantRetryRecoveryKind,
13
13
  CodexCompactionContext,
14
+ Effort,
14
15
  Model,
15
16
  TextContent,
16
17
  ToolChoice,
@@ -27,7 +28,12 @@ import type { RecoveredRetryError } from "../extensibility/shared-events";
27
28
  import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
28
29
  import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
29
30
  import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
30
- import type { ConfiguredThinkingLevel } from "../thinking";
31
+ import {
32
+ AUTO_THINKING,
33
+ type ConfiguredThinkingLevel,
34
+ clampThinkingLevelToCeiling,
35
+ modelSupportsEffortCeiling,
36
+ } from "../thinking";
31
37
  import type { AgentSessionEvent } from "./agent-session-events";
32
38
  import type { InitialRetryFallbackState } from "./agent-session-types";
33
39
  import { isEmptyErrorTurn } from "./messages";
@@ -98,6 +104,8 @@ export interface TurnRecoveryHost {
98
104
  thinkingLevel(): ThinkingLevel | undefined;
99
105
  configuredThinkingLevel(): ConfiguredThinkingLevel | undefined;
100
106
  setThinkingLevel(level: ConfiguredThinkingLevel | undefined): void;
107
+ /** Hard per-session effort ceiling; fallback recovery must never raise thinking above it. */
108
+ thinkingLevelCeiling(): Effort | undefined;
101
109
  isDisposed(): boolean;
102
110
  isStreaming(): boolean;
103
111
  isCompacting(): boolean;
@@ -1011,7 +1019,14 @@ export class TurnRecovery {
1011
1019
  // Capture the configured selector (auto-aware) so a fallback chain preserves
1012
1020
  // `auto` instead of collapsing it to the level it resolved to this turn.
1013
1021
  const currentThinkingLevel = this.#host.configuredThinkingLevel();
1014
- const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
1022
+ const requestedThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
1023
+ // A fallback selector's explicit level (or the carried level after the
1024
+ // replacement model's floor clamp) must never exceed the session's
1025
+ // per-spawn effort ceiling.
1026
+ const nextThinkingLevel =
1027
+ requestedThinkingLevel === AUTO_THINKING
1028
+ ? requestedThinkingLevel
1029
+ : clampThinkingLevelToCeiling(candidate, requestedThinkingLevel, this.#host.thinkingLevelCeiling());
1015
1030
  const candidateSelector = formatModelStringWithRouting(candidate);
1016
1031
  this.#host.setModelWithProviderSessionReset(candidate);
1017
1032
  this.#host.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
@@ -1041,11 +1056,15 @@ export class TurnRecovery {
1041
1056
  const role = this.#activeRetryFallback?.role ?? this.resolveRetryFallbackRole(currentSelector);
1042
1057
  if (!role) return false;
1043
1058
 
1059
+ const ceiling = this.#host.thinkingLevelCeiling();
1044
1060
  for (const selector of this.findRetryFallbackCandidates(role, currentSelector)) {
1045
1061
  if (this.isRetryFallbackSelectorSuppressed(selector)) continue;
1046
1062
  const resolved = resolveModelOverride([selector.raw], this.#host.modelRegistry, this.#host.settings);
1047
1063
  const candidate = resolved.model ?? this.#host.modelRegistry.find(selector.provider, selector.id);
1048
1064
  if (!candidate) continue;
1065
+ // A candidate whose effort floor exceeds the per-spawn ceiling would be
1066
+ // clamped UP past the cap by its model floor — skip it entirely.
1067
+ if (ceiling !== undefined && !modelSupportsEffortCeiling(candidate, ceiling)) continue;
1049
1068
  const apiKey = await this.#host.modelRegistry.getApiKey(candidate, this.#host.sessionId());
1050
1069
  if (!apiKey) continue;
1051
1070
  await this.applyRetryFallbackCandidate(role, selector, currentSelector, options);
@@ -2647,9 +2647,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2647
2647
  progress.contextWindow = model.contextWindow;
2648
2648
  }
2649
2649
  // Caller-requested coarse effort maps onto the resolved model's
2650
- // supported range; undefined (no effort, or no controllable effort
2651
- // surface) falls through to the normal selectors below.
2652
- const effortLevel = options.effort !== undefined ? resolveTaskEffortLevel(model, options.effort) : undefined;
2650
+ // supported range, then respects the operator-configured ceiling.
2651
+ // Undefined (no effort, or no controllable effort surface) falls
2652
+ // through to the normal selectors below.
2653
+ // The ceiling outlives initial resolution: it rides into the session so
2654
+ // retry-fallback recovery can never clamp effort back up past it.
2655
+ const spawnEffortCeiling = options.effort !== undefined ? settings.get("task.maxEffort") : undefined;
2656
+ const effortLevel =
2657
+ options.effort !== undefined
2658
+ ? resolveTaskEffortLevel(model, options.effort, spawnEffortCeiling)
2659
+ : undefined;
2653
2660
  if (model) {
2654
2661
  const displayLevel = effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : undefined);
2655
2662
  progress.resolvedModel =
@@ -2773,6 +2780,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2773
2780
  modelPatternDefaultFallbackChain:
2774
2781
  model || modelOverride === undefined ? undefined : defaultRetryFallbackChain,
2775
2782
  thinkingLevel: effectiveThinkingLevel,
2783
+ thinkingLevelCeiling: spawnEffortCeiling,
2776
2784
  toolNames,
2777
2785
  outputSchema,
2778
2786
  outputSchemaMode: options.outputSchemaMode,