@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.0

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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
@@ -22,10 +22,17 @@ import { normalizeLocalScheme } from "../tools/path-utils";
22
22
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
23
23
  import { callTool } from "./client";
24
24
  import { renderMCPCall, renderMCPResult } from "./render";
25
- import type { MCPContent, MCPServerConnection, MCPToolCallParams, MCPToolCallResult, MCPToolDefinition } from "./types";
25
+ import type {
26
+ MCPAuthChallenge,
27
+ MCPContent,
28
+ MCPServerConnection,
29
+ MCPToolCallParams,
30
+ MCPToolCallResult,
31
+ MCPToolDefinition,
32
+ } from "./types";
26
33
 
27
- /** Reconnect callback: tears down stale connection, returns new one or null. */
28
- export type MCPReconnect = () => Promise<MCPServerConnection | null>;
34
+ /** Reconnect callback: tears down a stale connection, optionally authorizing first. */
35
+ export type MCPReconnect = (options?: { authChallenge?: MCPAuthChallenge }) => Promise<MCPServerConnection | null>;
29
36
 
30
37
  /**
31
38
  * Network-level and stale-session errors that warrant a reconnect + single retry.
@@ -174,6 +181,8 @@ export interface MCPToolDetails {
174
181
  isError?: boolean;
175
182
  /** Raw content from MCP response */
176
183
  rawContent?: MCPContent[];
184
+ /** Structured metadata from the MCP response */
185
+ mcpMeta?: Record<string, unknown>;
177
186
  /** Provider ID (e.g., "claude", "mcp-json") */
178
187
  provider?: string;
179
188
  /** Provider display name (e.g., "Claude Code", "MCP Config") */
@@ -222,6 +231,7 @@ function buildResult(
222
231
  mcpToolName,
223
232
  isError: result.isError,
224
233
  rawContent: result.content,
234
+ mcpMeta: result._meta,
225
235
  provider,
226
236
  providerName,
227
237
  };
@@ -249,6 +259,51 @@ function buildErrorResult(
249
259
  };
250
260
  }
251
261
 
262
+ type MCPToolCallAttempt = {
263
+ connection: MCPServerConnection;
264
+ result?: MCPToolCallResult;
265
+ error?: unknown;
266
+ };
267
+
268
+ function getMcpAuthChallenge(result: MCPToolCallResult): MCPAuthChallenge | undefined {
269
+ if (!result.isError) return undefined;
270
+ const values = result._meta?.["mcp/www_authenticate"];
271
+ if (!Array.isArray(values)) return undefined;
272
+ const wwwAuthenticate = values.filter((value): value is string => typeof value === "string" && value.trim() !== "");
273
+ return wwwAuthenticate.length > 0 ? { wwwAuthenticate } : undefined;
274
+ }
275
+
276
+ async function callToolWithAuthRetry(
277
+ connection: MCPServerConnection,
278
+ toolName: string,
279
+ args: MCPToolArgs,
280
+ reconnect: MCPReconnect | undefined,
281
+ signal?: AbortSignal,
282
+ ): Promise<MCPToolCallAttempt> {
283
+ const result = await callTool(connection, toolName, args, { signal });
284
+ const authChallenge = getMcpAuthChallenge(result);
285
+ if (!authChallenge || !reconnect) return { connection, result };
286
+
287
+ let newConnection: MCPServerConnection | null;
288
+ try {
289
+ newConnection = await reconnectWithAbort(reconnect, signal, { authChallenge });
290
+ } catch (error) {
291
+ rethrowIfAborted(error, signal);
292
+ return { connection, error };
293
+ }
294
+ if (!newConnection) return { connection, result };
295
+
296
+ try {
297
+ return {
298
+ connection: newConnection,
299
+ result: await callTool(newConnection, toolName, args, { signal }),
300
+ };
301
+ } catch (error) {
302
+ rethrowIfAborted(error, signal);
303
+ return { connection: newConnection, error };
304
+ }
305
+ }
306
+
252
307
  /** Re-throw abort-related errors so they bypass error-result handling. */
253
308
  function rethrowIfAborted(error: unknown, signal?: AbortSignal): void {
254
309
  if (error instanceof ToolAbortError) throw error;
@@ -256,9 +311,13 @@ function rethrowIfAborted(error: unknown, signal?: AbortSignal): void {
256
311
  if (signal?.aborted) throw new ToolAbortError();
257
312
  }
258
313
 
259
- async function reconnectWithAbort(reconnect: MCPReconnect, signal?: AbortSignal): Promise<MCPServerConnection | null> {
314
+ async function reconnectWithAbort(
315
+ reconnect: MCPReconnect,
316
+ signal?: AbortSignal,
317
+ options?: { authChallenge?: MCPAuthChallenge },
318
+ ): Promise<MCPServerConnection | null> {
260
319
  try {
261
- return await untilAborted(signal, reconnect);
320
+ return await untilAborted(signal, () => reconnect(options));
262
321
  } catch (error) {
263
322
  rethrowIfAborted(error, signal);
264
323
  return null;
@@ -379,8 +438,27 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
379
438
  const providerName = this.connection._source?.providerName;
380
439
 
381
440
  try {
382
- const result = await callTool(this.connection, this.tool.name, args, { signal });
383
- return buildResult(result, this.connection.name, this.tool.name, provider, providerName);
441
+ const attempt = await callToolWithAuthRetry(this.connection, this.tool.name, args, this.reconnect, signal);
442
+ if (attempt.error !== undefined) {
443
+ return buildErrorResult(attempt.error, this.connection.name, this.tool.name, provider, providerName);
444
+ }
445
+ if (!attempt.result) {
446
+ return buildErrorResult(
447
+ new Error("MCP tool call returned no result"),
448
+ this.connection.name,
449
+ this.tool.name,
450
+ provider,
451
+ providerName,
452
+ );
453
+ }
454
+ this.connection = attempt.connection;
455
+ return buildResult(
456
+ attempt.result,
457
+ attempt.connection.name,
458
+ this.tool.name,
459
+ attempt.connection._source?.provider ?? provider,
460
+ attempt.connection._source?.providerName ?? providerName,
461
+ );
384
462
  } catch (error) {
385
463
  rethrowIfAborted(error, signal);
386
464
  if (this.reconnect && isRetriableConnectionError(error)) {
@@ -483,13 +561,31 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
483
561
  const connection = await untilAborted(signal, () => this.getConnection());
484
562
  throwIfAborted(signal);
485
563
  try {
486
- const result = await callTool(connection, this.tool.name, args, { signal });
564
+ const attempt = await callToolWithAuthRetry(connection, this.tool.name, args, this.reconnect, signal);
565
+ if (attempt.error !== undefined) {
566
+ return buildErrorResult(
567
+ attempt.error,
568
+ this.serverName,
569
+ this.tool.name,
570
+ attempt.connection._source?.provider ?? provider,
571
+ attempt.connection._source?.providerName ?? providerName,
572
+ );
573
+ }
574
+ if (!attempt.result) {
575
+ return buildErrorResult(
576
+ new Error("MCP tool call returned no result"),
577
+ this.serverName,
578
+ this.tool.name,
579
+ provider,
580
+ providerName,
581
+ );
582
+ }
487
583
  return buildResult(
488
- result,
584
+ attempt.result,
489
585
  this.serverName,
490
586
  this.tool.name,
491
- connection._source?.provider ?? provider,
492
- connection._source?.providerName ?? providerName,
587
+ attempt.connection._source?.provider ?? provider,
588
+ attempt.connection._source?.providerName ?? providerName,
493
589
  );
494
590
  } catch (callError) {
495
591
  rethrowIfAborted(callError, signal);
package/src/mcp/types.ts CHANGED
@@ -206,10 +206,17 @@ export interface MCPResourceContent {
206
206
 
207
207
  export type MCPContent = MCPTextContent | MCPImageContent | MCPResourceContent;
208
208
 
209
+ /** Structured authentication challenge returned in a tool result. */
210
+ export interface MCPAuthChallenge {
211
+ /** Values from `_meta["mcp/www_authenticate"]`. */
212
+ readonly wwwAuthenticate: readonly string[];
213
+ }
214
+
209
215
  /** tools/call response */
210
216
  export interface MCPToolCallResult {
211
217
  content: MCPContent[];
212
218
  isError?: boolean;
219
+ _meta?: Record<string, unknown>;
213
220
  }
214
221
 
215
222
  // =============================================================================
@@ -142,9 +142,12 @@ export function startMemoryStartupTask(options: {
142
142
  return;
143
143
  }
144
144
 
145
- void runMemoryStartup({ session, settings, modelRegistry, agentDir, config: cfg }).catch(error => {
146
- logger.warn("Memory startup failed", { error: String(error) });
147
- });
145
+ const signal = session.beginLocalMemoryStartup?.() ?? new AbortController().signal;
146
+ void runMemoryStartup({ session, settings, modelRegistry, agentDir, config: cfg, signal })
147
+ .catch(error => {
148
+ if (!signal.aborted) logger.warn("Memory startup failed", { error: String(error) });
149
+ })
150
+ .finally(() => session.endLocalMemoryStartup?.(signal));
148
151
  }
149
152
 
150
153
  interface MemoryInstructionSession {
@@ -315,26 +318,32 @@ export function enqueueMemoryConsolidation(agentDir: string, cwd: string, source
315
318
  }
316
319
  }
317
320
 
318
- async function runMemoryStartup(options: {
321
+ interface MemoryStartupOptions {
319
322
  session: AgentSession;
320
323
  settings: Settings;
321
324
  modelRegistry: ModelRegistry;
322
325
  agentDir: string;
323
326
  config: MemoryRuntimeConfig;
324
- }): Promise<void> {
327
+ signal: AbortSignal;
328
+ }
329
+
330
+ function isMemoryStartupActive(options: MemoryStartupOptions): boolean {
331
+ return !options.signal.aborted && !options.session.isDisposed && options.settings.get("memory.backend") === "local";
332
+ }
333
+
334
+ async function runMemoryStartup(options: MemoryStartupOptions): Promise<void> {
335
+ if (!isMemoryStartupActive(options)) return;
325
336
  await runPhase1(options);
337
+ if (!isMemoryStartupActive(options)) return;
326
338
  await runPhase2(options);
339
+ if (!isMemoryStartupActive(options)) return;
327
340
  await refreshMemoryToolDeveloperInstructionsCacheAfterStartup(options.session, options.agentDir, options.settings);
341
+ if (!isMemoryStartupActive(options)) return;
328
342
  await options.session.refreshBaseSystemPrompt?.();
329
343
  }
330
344
 
331
- async function runPhase1(options: {
332
- session: AgentSession;
333
- settings: Settings;
334
- modelRegistry: ModelRegistry;
335
- agentDir: string;
336
- config: MemoryRuntimeConfig;
337
- }): Promise<void> {
345
+ async function runPhase1(options: MemoryStartupOptions): Promise<void> {
346
+ if (!isMemoryStartupActive(options)) return;
338
347
  const { session, modelRegistry, agentDir, config } = options;
339
348
  const db = openMemoryDb(getAgentDbPath(agentDir));
340
349
  const nowSec = unixNow();
@@ -344,6 +353,7 @@ async function runPhase1(options: {
344
353
 
345
354
  try {
346
355
  const threads = await collectThreads(session, currentThreadId);
356
+ if (!isMemoryStartupActive(options)) return;
347
357
  upsertThreads(db, threads);
348
358
 
349
359
  const phase1Model = await resolveMemoryModel({
@@ -364,6 +374,7 @@ async function runPhase1(options: {
364
374
  return;
365
375
  }
366
376
 
377
+ if (!isMemoryStartupActive(options)) return;
367
378
  const claims = claimStage1Jobs(db, {
368
379
  nowSec,
369
380
  threadScanLimit: config.threadScanLimit,
@@ -387,6 +398,7 @@ async function runPhase1(options: {
387
398
  };
388
399
 
389
400
  await runWithConcurrency(claims, config.stage1Concurrency, async claim => {
401
+ if (!isMemoryStartupActive(options)) return;
390
402
  const result = await runStage1Job({
391
403
  claim,
392
404
  model: phase1Model,
@@ -395,6 +407,7 @@ async function runPhase1(options: {
395
407
  config,
396
408
  metadata: session.agent?.metadataForProvider(phase1Model.provider),
397
409
  });
410
+ if (!isMemoryStartupActive(options)) return;
398
411
 
399
412
  if (result.kind === "failed") {
400
413
  logger.error("Memory phase1 stage1 job failed", {
@@ -460,13 +473,8 @@ async function runPhase1(options: {
460
473
  }
461
474
  }
462
475
 
463
- async function runPhase2(options: {
464
- session: AgentSession;
465
- settings: Settings;
466
- modelRegistry: ModelRegistry;
467
- agentDir: string;
468
- config: MemoryRuntimeConfig;
469
- }): Promise<void> {
476
+ async function runPhase2(options: MemoryStartupOptions): Promise<void> {
477
+ if (!isMemoryStartupActive(options)) return;
470
478
  const { session, modelRegistry, agentDir, config } = options;
471
479
  const cwd = session.sessionManager.getCwd();
472
480
  const db = openMemoryDb(getAgentDbPath(agentDir));
@@ -488,8 +496,10 @@ async function runPhase2(options: {
488
496
  const newWatermark = computeCompletionWatermark(claim.inputWatermark, outputs);
489
497
 
490
498
  await syncPhase2Artifacts(memoryRoot, outputs);
499
+ if (!isMemoryStartupActive(options)) return;
491
500
  if (outputs.length === 0) {
492
501
  await cleanupConsolidatedArtifacts(memoryRoot);
502
+ if (!isMemoryStartupActive(options)) return;
493
503
  const marked = markGlobalPhase2Succeeded(db, {
494
504
  ownershipToken: claim.ownershipToken,
495
505
  newWatermark,
@@ -502,6 +512,7 @@ async function runPhase2(options: {
502
512
  return;
503
513
  }
504
514
 
515
+ if (!isMemoryStartupActive(options)) return;
505
516
  const phase2Model = await resolveMemoryModel({
506
517
  modelRegistry,
507
518
  session,
@@ -529,8 +540,13 @@ async function runPhase2(options: {
529
540
  return;
530
541
  }
531
542
 
543
+ if (!isMemoryStartupActive(options)) return;
532
544
  let heartbeatLostOwnership = false;
533
545
  const heartbeat = setInterval(() => {
546
+ if (!isMemoryStartupActive(options)) {
547
+ clearInterval(heartbeat);
548
+ return;
549
+ }
534
550
  const ok = heartbeatGlobalJob(db, {
535
551
  ownershipToken: claim.ownershipToken,
536
552
  leaseSeconds: config.phase2LeaseSeconds,
@@ -544,13 +560,16 @@ async function runPhase2(options: {
544
560
  }, config.phase2HeartbeatSeconds * 1000);
545
561
 
546
562
  try {
563
+ if (!isMemoryStartupActive(options)) return;
547
564
  const consolidated = await runConsolidationModel({
548
565
  memoryRoot,
549
566
  model: phase2Model,
550
567
  apiKey: modelRegistry.resolver(phase2Model, session.sessionId),
551
568
  metadata: session.agent?.metadataForProvider(phase2Model.provider),
552
569
  });
570
+ if (!isMemoryStartupActive(options)) return;
553
571
  await applyConsolidation(memoryRoot, consolidated);
572
+ if (!isMemoryStartupActive(options)) return;
554
573
  if (heartbeatLostOwnership) {
555
574
  throw new Error("Phase2 lease ownership lost before completion");
556
575
  }
@@ -564,6 +583,7 @@ async function runPhase2(options: {
564
583
  throw new Error("Phase2 could not mark success: ownership lost");
565
584
  }
566
585
  } catch (error) {
586
+ if (!isMemoryStartupActive(options)) return;
567
587
  markPhase2FailureWithFallback(db, {
568
588
  claim,
569
589
  retryDelaySeconds: config.phase2RetryDelaySeconds,
@@ -1227,7 +1247,7 @@ async function resolveMemoryModel(options: {
1227
1247
 
1228
1248
  function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
1229
1249
  return {
1230
- enabled: settings.get("memory.backend") === "local" || settings.get("memories.enabled") === true,
1250
+ enabled: settings.get("memory.backend") === "local",
1231
1251
  maxRolloutsPerStartup: settings.get("memories.maxRolloutsPerStartup") ?? DEFAULTS.maxRolloutsPerStartup,
1232
1252
  maxRolloutAgeDays: settings.get("memories.maxRolloutAgeDays") ?? DEFAULTS.maxRolloutAgeDays,
1233
1253
  minRolloutIdleHours: settings.get("memories.minRolloutIdleHours") ?? DEFAULTS.minRolloutIdleHours,
@@ -0,0 +1,2 @@
1
+ /** Built-in tools whose availability depends on the selected memory backend. */
2
+ export const MEMORY_BACKEND_TOOL_NAMES = ["retain", "recall", "reflect", "memory_edit", "learn"] as const;
@@ -62,6 +62,20 @@ const STATIC_INSTRUCTIONS = [
62
62
  "",
63
63
  ].join("\n");
64
64
 
65
+ async function installMnemopiState(session: AgentSession, config: MnemopiBackendConfig): Promise<MnemopiSessionState> {
66
+ const state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
67
+ const previous = setMnemopiSessionState(session, state);
68
+ await previous?.dispose();
69
+ try {
70
+ state.attachSessionListeners();
71
+ return state;
72
+ } catch (error) {
73
+ setMnemopiSessionState(session, undefined);
74
+ await state.dispose({ consolidate: false });
75
+ throw error;
76
+ }
77
+ }
78
+
65
79
  export const mnemopiBackend: MemoryBackend = {
66
80
  id: "mnemopi",
67
81
 
@@ -90,10 +104,7 @@ export const mnemopiBackend: MemoryBackend = {
90
104
  try {
91
105
  const config = await loadMnemopiConfigWithProviders(settings, agentDir, modelRegistry, sessionId);
92
106
  await Promise.all([loadMnemopi(), loadMnemopiCore()]);
93
- const state = new MnemopiSessionState({ sessionId, config, session });
94
- const previous = setMnemopiSessionState(session, state);
95
- await previous?.dispose();
96
- state.attachSessionListeners();
107
+ await installMnemopiState(session, config);
97
108
  } catch (error) {
98
109
  logger.warn("Mnemopi: backend startup failed; memory backend inert.", { error: String(error) });
99
110
  }
@@ -129,12 +140,19 @@ export const mnemopiBackend: MemoryBackend = {
129
140
  requireMnemopiCore().resetMemoryForTests();
130
141
  await Bun.sleep(0);
131
142
  await removeDbFiles(getMnemopiScopedDbPaths(config));
143
+ if (!session?.sessionId || previous?.aliasOf || session.settings.get("memory.backend") !== "mnemopi") return;
144
+ try {
145
+ await Promise.all([loadMnemopi(), loadMnemopiCore()]);
146
+ await installMnemopiState(session, config);
147
+ } catch (error) {
148
+ logger.warn("Mnemopi: clear rehydrate failed; memory backend inert.", { error: String(error) });
149
+ }
132
150
  },
133
151
 
134
152
  async enqueue(agentDir, _cwd, session): Promise<void> {
135
153
  try {
136
154
  let state = getMnemopiSessionState(session);
137
- if (!state && session) {
155
+ if (!state && session?.sessionId) {
138
156
  const config = await loadMnemopiConfigWithProviders(
139
157
  session.settings,
140
158
  agentDir,
@@ -142,8 +160,7 @@ export const mnemopiBackend: MemoryBackend = {
142
160
  session.sessionId,
143
161
  );
144
162
  await Promise.all([loadMnemopi(), loadMnemopiCore()]);
145
- state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
146
- setMnemopiSessionState(session, state);
163
+ state = await installMnemopiState(session, config);
147
164
  }
148
165
  await state?.consolidate({ full: true });
149
166
  } catch (error) {
@@ -164,6 +164,10 @@ function isPromptTurnInFlight(turn: PromptTurnState | undefined): turn is Prompt
164
164
  type ManagedSessionRecord = {
165
165
  session: AgentSession;
166
166
  mcpManager: MCPManager | undefined;
167
+ // Ordered queue of MCP tool refreshes for this record. Rebuilt per
168
+ // `#configureMcpServers` call; drained on reconfigure so a stale in-flight
169
+ // refresh can never land after a newer configuration's tools.
170
+ mcpRefreshChain: Promise<void> | undefined;
167
171
  promptTurn: PromptTurnState | undefined;
168
172
  promptQueue: PromptQueueState;
169
173
  liveMessageId: string | undefined;
@@ -1132,6 +1136,7 @@ export class AcpAgent implements Agent {
1132
1136
  return {
1133
1137
  session,
1134
1138
  mcpManager: undefined,
1139
+ mcpRefreshChain: undefined,
1135
1140
  promptTurn: undefined,
1136
1141
  promptQueue: { promise: Promise.resolve(), release: undefined },
1137
1142
  liveMessageId: undefined,
@@ -2275,6 +2280,24 @@ export class AcpAgent implements Agent {
2275
2280
  return;
2276
2281
  }
2277
2282
 
2283
+ const uiContext = createAcpExtensionUiContext(
2284
+ this.#connection,
2285
+ () => record.session.sessionId,
2286
+ this.#clientCapabilities,
2287
+ );
2288
+ if (this.#clientCapabilities?.elicitation?.form != null) {
2289
+ record.session.setUsageFallbackConfirmer(confirmation => {
2290
+ const reserve =
2291
+ confirmation.remainingPercent === undefined
2292
+ ? "inside the configured reserve margin"
2293
+ : `${confirmation.remainingPercent.toFixed(1)}% remaining`;
2294
+ return uiContext.confirm(
2295
+ "Coding-plan reserve reached",
2296
+ `${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
2297
+ );
2298
+ });
2299
+ }
2300
+
2278
2301
  const extensionRunner = record.session.extensionRunner;
2279
2302
  if (!extensionRunner) {
2280
2303
  record.extensionsConfigured = true;
@@ -2311,6 +2334,8 @@ export class AcpAgent implements Agent {
2311
2334
  },
2312
2335
  getThinkingLevel: () => record.session.thinkingLevel,
2313
2336
  setThinkingLevel: level => record.session.setThinkingLevel(level),
2337
+ getServiceTiers: () => record.session.serviceTierByFamily,
2338
+ setServiceTier: (family, tier) => record.session.setServiceTierFamily(family, tier),
2314
2339
  getSessionName: () => record.session.sessionManager.getSessionName(),
2315
2340
  setSessionName: async name => {
2316
2341
  await record.session.sessionManager.setSessionName(name, "user");
@@ -2355,15 +2380,7 @@ export class AcpAgent implements Agent {
2355
2380
  },
2356
2381
  compact: instructionsOrOptions => runExtensionCompact(record.session, instructionsOrOptions),
2357
2382
  },
2358
- // Per-session getter: `record.session.sessionId` reads through to
2359
- // `sessionManager.getSessionId()` (it's a getter, not a field), so an
2360
- // extension command that calls `ctx.newSession` / `ctx.switchSession`
2361
- // — both exposed in the block just above — mutates the underlying id
2362
- // mid-flight. Reading lazily on each elicitation matches every other
2363
- // `sessionUpdate` call in this file. Hoisting the factory to an
2364
- // `AcpAgent` field would still be wrong because it would also lose
2365
- // the per-`record` binding.
2366
- createAcpExtensionUiContext(this.#connection, () => record.session.sessionId, this.#clientCapabilities),
2383
+ uiContext,
2367
2384
  );
2368
2385
  await extensionRunner.emit({ type: "session_start" });
2369
2386
  record.extensionsConfigured = true;
@@ -2373,6 +2390,11 @@ export class AcpAgent implements Agent {
2373
2390
  if (record.mcpManager) {
2374
2391
  await record.mcpManager.disconnectAll();
2375
2392
  }
2393
+ // Drain any in-flight refresh queued by a previous configuration: a refresh
2394
+ // that already passed its manager guard could otherwise finish applying a
2395
+ // stale tool set after this reconfiguration installs the new one.
2396
+ await record.mcpRefreshChain;
2397
+ record.mcpRefreshChain = undefined;
2376
2398
  if (servers.length === 0) {
2377
2399
  record.mcpManager = undefined;
2378
2400
  await record.session.refreshMCPTools([]);
@@ -2380,6 +2402,30 @@ export class AcpAgent implements Agent {
2380
2402
  }
2381
2403
 
2382
2404
  const manager = new MCPManager(record.session.sessionManager.getCwd());
2405
+ // MCP servers connect and reconnect independently, so `onToolsChanged` can fire
2406
+ // several times back to back. Each firing is chained onto `record.mcpRefreshChain`
2407
+ // so refreshes apply in order, and each one re-reads `manager.getTools()` at the
2408
+ // time it actually runs rather than the snapshot from when it was queued — so a
2409
+ // refresh can never apply a stale, smaller tool set after a newer one already landed.
2410
+ // The returned promise propagates failures (the initial awaited refresh below must
2411
+ // fail session setup, as the pre-queue code did); the stored chain swallows them
2412
+ // after logging so background firings only warn and the chain never rejects.
2413
+ const enqueueMcpToolsRefresh = (): Promise<void> => {
2414
+ const run = (record.mcpRefreshChain ?? Promise.resolve()).then(async () => {
2415
+ if (record.mcpManager !== manager) return;
2416
+ await record.session.refreshMCPTools(manager.getTools());
2417
+ });
2418
+ record.mcpRefreshChain = run.catch(error => {
2419
+ logger.warn("ACP MCP tool refresh failed", {
2420
+ error: error instanceof Error ? error.message : String(error),
2421
+ });
2422
+ });
2423
+ return run;
2424
+ };
2425
+ manager.setOnToolsChanged(() => {
2426
+ // Failures are logged once via the stored chain's catch above.
2427
+ enqueueMcpToolsRefresh().catch(() => {});
2428
+ });
2383
2429
  const configs: MCPConfigMap = {};
2384
2430
  const sources: MCPSourceMap = {};
2385
2431
  for (const server of servers) {
@@ -2402,7 +2448,7 @@ export class AcpAgent implements Agent {
2402
2448
  }
2403
2449
 
2404
2450
  record.mcpManager = manager;
2405
- await record.session.refreshMCPTools(result.tools);
2451
+ await enqueueMcpToolsRefresh();
2406
2452
  }
2407
2453
 
2408
2454
  #toMcpConfig(server: McpServer): MCPServerConfig {
@@ -369,6 +369,7 @@ const todoStatusMap: Record<TodoStatus, "pending" | "in_progress" | "completed">
369
369
  in_progress: "in_progress",
370
370
  completed: "completed",
371
371
  abandoned: "completed",
372
+ blocked: "pending",
372
373
  };
373
374
 
374
375
  function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "completed" {
@@ -432,7 +433,13 @@ function extractTodoEntries(phases: unknown[]): Array<{ content: string; status:
432
433
  }
433
434
 
434
435
  function isTodoStatus(status: unknown): status is TodoStatus {
435
- return status === "pending" || status === "in_progress" || status === "completed" || status === "abandoned";
436
+ return (
437
+ status === "pending" ||
438
+ status === "in_progress" ||
439
+ status === "completed" ||
440
+ status === "abandoned" ||
441
+ status === "blocked"
442
+ );
436
443
  }
437
444
  export function buildToolCallStartUpdate(input: {
438
445
  toolCallId: string;
@@ -76,26 +76,43 @@ function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): st
76
76
  return `${model} ${theme.getThinkingBorderColor(level)(display)}`;
77
77
  }
78
78
 
79
+ /** Format a resolved selector, preserving provider identity when requested. */
80
+ function formatResolvedModelBadge(resolved: string, preserveProvider = false): string {
81
+ // Model ids may themselves contain colons (`qwen3:14b`), so only treat the
82
+ // suffix as a thinking level when it parses as one.
83
+ const colon = resolved.lastIndexOf(":");
84
+ const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
85
+ const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
86
+ const label = preserveProvider ? selector : selector.slice(selector.indexOf("/") + 1);
87
+ return formatModelBadge(label, level);
88
+ }
89
+
79
90
  /**
80
91
  * Active model + reasoning level for a hub row: live session state when the
81
92
  * agent is attached, else the executor-reported `resolvedModel` selector
82
- * (`provider/id`, optionally `:<level>`). Undefined when neither is known
93
+ * (`provider/id`, optionally `:<level>`). Active retry fallbacks retain their
94
+ * provider and carry an explicit marker. Undefined when no model is known
83
95
  * (e.g. a parked historical agent restored from disk).
84
96
  */
85
97
  function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
98
+ const progress = observed?.progress;
99
+ // Prefer the live session's own resolved fallback selector; else honor the
100
+ // executor-reported fallback flag. The latter covers observer-only rows (no
101
+ // live session) AND live rows whose fallback armed no session retry state —
102
+ // e.g. the Fireworks Fast → base degrade, which emits `retry_fallback_applied`
103
+ // without populating `#activeRetryFallback`, so `retryFallbackModel` is undefined.
104
+ const fallbackSelector =
105
+ ref.session?.retryFallbackModel ?? (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined);
106
+ if (fallbackSelector) {
107
+ return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true)}`;
108
+ }
86
109
  const model = ref.session?.model;
87
110
  if (model) {
88
111
  const level = model.thinking ? ref.session?.thinkingLevel : undefined;
89
112
  return formatModelBadge(model.id, level);
90
113
  }
91
- const resolved = observed?.progress?.resolvedModel;
92
- if (!resolved) return undefined;
93
- // Model ids may themselves contain colons (`qwen3:14b`), so only treat the
94
- // suffix as a thinking level when it parses as one.
95
- const colon = resolved.lastIndexOf(":");
96
- const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
97
- const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
98
- return formatModelBadge(selector.slice(selector.indexOf("/") + 1), level);
114
+ const resolved = progress?.resolvedModel;
115
+ return resolved ? formatResolvedModelBadge(resolved) : undefined;
99
116
  }
100
117
 
101
118
  /** Result of one host-backed transcript read for the Agent Hub viewer. */
@@ -619,7 +636,7 @@ export class AgentHubOverlayComponent extends Container {
619
636
  if (ref.status === "running" && ref.session) {
620
637
  await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
621
638
  }
622
- await this.#lifecycle().release(ref.id);
639
+ await this.#lifecycle().release(ref.id, ref);
623
640
  } catch (error) {
624
641
  logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
625
642
  this.#notice = error instanceof Error ? error.message : String(error);
@@ -1,6 +1,7 @@
1
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
2
- import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
2
+ import { Container, getKeybindings, Input, Spacer, Text, type TUI, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
3
3
  import { theme } from "../../modes/theme/theme";
4
+ import { urlHyperlinkAlways, WidthAwareText } from "../../tui";
4
5
  import { openPath } from "../../utils/open";
5
6
  import { DynamicBorder } from "./dynamic-border";
6
7
 
@@ -75,13 +76,22 @@ export class LoginDialogComponent extends Container {
75
76
  * is offered as an additional local shortcut so narrow local terminals still
76
77
  * have a truncation-safe copy target (viewport clipping on a long authorize
77
78
  * URL silently drops trailing OAuth query parameters — e.g.
78
- * `code_challenge_method=S256`). The OSC 8 hyperlink carries the full URL
79
- * for terminals that support click-through.
79
+ * `code_challenge_method=S256`). Every physical URL row carries its own OSC 8
80
+ * link to the full URL, so clicking any wrapped fragment opens the same target.
80
81
  */
81
82
  showAuth(url: string, instructions?: string, launchUrl?: string): void {
82
83
  this.#contentContainer.clear();
83
84
  this.#contentContainer.addChild(new Spacer(1));
84
- this.#contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
85
+ this.#contentContainer.addChild(
86
+ new WidthAwareText(
87
+ contentWidth =>
88
+ wrapTextWithAnsi(url, contentWidth)
89
+ .map(row => theme.fg("accent", urlHyperlinkAlways(url, row)))
90
+ .join("\n"),
91
+ 1,
92
+ 0,
93
+ ),
94
+ );
85
95
 
86
96
  const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
87
97
  const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;