@oh-my-pi/pi-coding-agent 17.2.3 → 17.2.5

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 (375) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-q2w43aw3.md} +92 -0
  3. package/dist/cli.js +3979 -3548
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/advisor/advise-tool.d.ts +6 -0
  6. package/dist/types/advisor/runtime.d.ts +4 -4
  7. package/dist/types/capability/mcp.d.ts +9 -0
  8. package/dist/types/cli/args.d.ts +4 -1
  9. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  10. package/dist/types/cli/command-help.d.ts +99 -0
  11. package/dist/types/cli/help-extra.d.ts +2 -0
  12. package/dist/types/cli/models-cli.d.ts +1 -1
  13. package/dist/types/cli/thinking-levels.d.ts +5 -0
  14. package/dist/types/cli/update-cli.d.ts +13 -0
  15. package/dist/types/commands/browser-relay.d.ts +38 -0
  16. package/dist/types/commands/commit.d.ts +3 -0
  17. package/dist/types/commands/complete.d.ts +1 -1
  18. package/dist/types/commands/completions.d.ts +6 -0
  19. package/dist/types/commands/grep.d.ts +3 -0
  20. package/dist/types/commands/join.d.ts +4 -0
  21. package/dist/types/commands/launch-help.d.ts +171 -0
  22. package/dist/types/commands/launch.d.ts +5 -1
  23. package/dist/types/commands/models.d.ts +3 -0
  24. package/dist/types/commands/say.d.ts +10 -0
  25. package/dist/types/commands/worktree.d.ts +3 -0
  26. package/dist/types/config/keybindings.d.ts +6 -1
  27. package/dist/types/config/model-discovery.d.ts +2 -2
  28. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  29. package/dist/types/config/models-config-schema.d.ts +2 -0
  30. package/dist/types/config/models-config.d.ts +1 -0
  31. package/dist/types/config/service-tier.d.ts +2 -0
  32. package/dist/types/config/settings-schema.d.ts +70 -22
  33. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  34. package/dist/types/cursor.d.ts +7 -4
  35. package/dist/types/discovery/helpers.d.ts +6 -0
  36. package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
  37. package/dist/types/edit/index.d.ts +3 -3
  38. package/dist/types/edit/modes/replace.d.ts +22 -15
  39. package/dist/types/edit/renderer.d.ts +1 -1
  40. package/dist/types/eval/py/kernel.d.ts +3 -1
  41. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  42. package/dist/types/extensibility/extensions/loader.d.ts +6 -2
  43. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  44. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  45. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  46. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
  47. package/dist/types/launch/broker.d.ts +1 -1
  48. package/dist/types/launch/client.d.ts +6 -3
  49. package/dist/types/launch/ensure.d.ts +8 -0
  50. package/dist/types/launch/paths.d.ts +3 -2
  51. package/dist/types/launch/protocol.d.ts +2 -2
  52. package/dist/types/lsp/client.d.ts +2 -0
  53. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  54. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  55. package/dist/types/lsp/mux/server.d.ts +18 -0
  56. package/dist/types/lsp/types.d.ts +22 -2
  57. package/dist/types/main.d.ts +2 -0
  58. package/dist/types/mcp/request-id.d.ts +19 -0
  59. package/dist/types/mcp/types.d.ts +14 -0
  60. package/dist/types/memory-backend/index.d.ts +1 -0
  61. package/dist/types/memory-backend/messages.d.ts +15 -0
  62. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  63. package/dist/types/mnemopi/state.d.ts +0 -23
  64. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  65. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  66. package/dist/types/modes/components/model-browser.d.ts +7 -5
  67. package/dist/types/modes/components/model-picker.d.ts +9 -3
  68. package/dist/types/modes/components/read-tool-group.d.ts +11 -0
  69. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  70. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  71. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  72. package/dist/types/modes/interactive-mode.d.ts +1 -0
  73. package/dist/types/modes/types.d.ts +2 -1
  74. package/dist/types/modes/workflow.d.ts +2 -1
  75. package/dist/types/registry/agent-lifecycle.d.ts +14 -5
  76. package/dist/types/registry/agent-registry.d.ts +4 -3
  77. package/dist/types/sdk.d.ts +4 -2
  78. package/dist/types/secrets/index.d.ts +7 -9
  79. package/dist/types/session/agent-session-types.d.ts +3 -1
  80. package/dist/types/session/agent-session.d.ts +6 -1
  81. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  82. package/dist/types/session/model-controls.d.ts +0 -1
  83. package/dist/types/session/session-advisors.d.ts +8 -1
  84. package/dist/types/session/session-context.d.ts +2 -1
  85. package/dist/types/session/session-maintenance.d.ts +2 -0
  86. package/dist/types/session/session-manager.d.ts +16 -5
  87. package/dist/types/session/session-stats.d.ts +10 -0
  88. package/dist/types/session/session-storage.d.ts +15 -3
  89. package/dist/types/system-prompt.d.ts +6 -1
  90. package/dist/types/task/index.d.ts +2 -1
  91. package/dist/types/task/spawn-policy.d.ts +5 -0
  92. package/dist/types/thinking.d.ts +1 -7
  93. package/dist/types/tools/ast-grep.d.ts +1 -1
  94. package/dist/types/tools/browser/registry.d.ts +2 -1
  95. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  96. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  97. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  98. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  99. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  100. package/dist/types/tools/browser/render.d.ts +1 -0
  101. package/dist/types/tools/browser.d.ts +3 -0
  102. package/dist/types/tools/computer/exposure.d.ts +9 -7
  103. package/dist/types/tools/computer/protocol.d.ts +70 -17
  104. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  105. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  106. package/dist/types/tools/computer/worker.d.ts +42 -7
  107. package/dist/types/tools/computer-renderer.d.ts +7 -5
  108. package/dist/types/tools/computer.d.ts +25 -44
  109. package/dist/types/tools/context.d.ts +3 -0
  110. package/dist/types/tools/default-renderer.d.ts +3 -0
  111. package/dist/types/tools/glob.d.ts +1 -1
  112. package/dist/types/tools/grep.d.ts +1 -1
  113. package/dist/types/tools/render-utils.d.ts +2 -0
  114. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  115. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  116. package/dist/types/tools/xdev.d.ts +8 -0
  117. package/dist/types/utils/clipboard.d.ts +2 -2
  118. package/dist/types/web/kagi.d.ts +1 -0
  119. package/dist/types/web/search/index.d.ts +1 -1
  120. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  121. package/dist/types/web/search/providers/base.d.ts +2 -0
  122. package/dist/types/web/search/providers/brave.d.ts +1 -0
  123. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  124. package/dist/types/web/search/providers/codex.d.ts +1 -0
  125. package/dist/types/web/search/providers/exa.d.ts +1 -0
  126. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  127. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  128. package/dist/types/web/search/providers/jina.d.ts +1 -0
  129. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  130. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  131. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  132. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  133. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  134. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  135. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  136. package/dist/types/web/search/providers/utils.d.ts +6 -9
  137. package/dist/types/web/search/providers/zai.d.ts +1 -0
  138. package/dist/types/web/search/types.d.ts +4 -0
  139. package/package.json +12 -12
  140. package/scripts/legacy-pi-virtual-module.ts +12 -2
  141. package/src/advisor/advise-tool.ts +18 -0
  142. package/src/advisor/runtime.ts +6 -6
  143. package/src/capability/mcp.ts +7 -0
  144. package/src/cli/args.ts +7 -87
  145. package/src/cli/browser-relay-cli.ts +119 -0
  146. package/src/cli/command-help.ts +103 -0
  147. package/src/cli/completion-gen.ts +5 -5
  148. package/src/cli/flag-tables.ts +9 -0
  149. package/src/cli/help-extra.ts +89 -0
  150. package/src/cli/models-cli.ts +13 -16
  151. package/src/cli/setup-cli.ts +13 -21
  152. package/src/cli/thinking-levels.ts +7 -0
  153. package/src/cli/update-cli.ts +26 -7
  154. package/src/cli-commands.ts +170 -33
  155. package/src/cli.ts +17 -5
  156. package/src/collab/host.ts +1 -1
  157. package/src/commands/acp.ts +3 -1
  158. package/src/commands/agents.ts +3 -2
  159. package/src/commands/auth-broker.ts +3 -2
  160. package/src/commands/auth-gateway.ts +3 -2
  161. package/src/commands/bench.ts +2 -3
  162. package/src/commands/browser-relay.ts +53 -0
  163. package/src/commands/cleanse.ts +2 -2
  164. package/src/commands/commit.ts +3 -2
  165. package/src/commands/complete.ts +2 -1
  166. package/src/commands/completions.ts +3 -2
  167. package/src/commands/config.ts +3 -2
  168. package/src/commands/dry-balance.ts +2 -2
  169. package/src/commands/gallery.ts +3 -2
  170. package/src/commands/gc.ts +3 -2
  171. package/src/commands/grep.ts +3 -2
  172. package/src/commands/grievances.ts +3 -2
  173. package/src/commands/install.ts +2 -2
  174. package/src/commands/join.ts +3 -2
  175. package/src/commands/launch-help.ts +116 -0
  176. package/src/commands/launch.ts +7 -191
  177. package/src/commands/models.ts +3 -2
  178. package/src/commands/plugin.ts +3 -2
  179. package/src/commands/read.ts +3 -2
  180. package/src/commands/say.ts +3 -2
  181. package/src/commands/setup.ts +3 -2
  182. package/src/commands/shell.ts +3 -2
  183. package/src/commands/ssh.ts +3 -2
  184. package/src/commands/stats.ts +3 -2
  185. package/src/commands/tiny-models.ts +2 -2
  186. package/src/commands/token.ts +2 -2
  187. package/src/commands/ttsr.ts +2 -2
  188. package/src/commands/update.ts +3 -2
  189. package/src/commands/usage.ts +3 -2
  190. package/src/commands/web-search.ts +3 -2
  191. package/src/commands/worktree.ts +3 -2
  192. package/src/config/keybindings.ts +19 -9
  193. package/src/config/mcp-schema.json +5 -0
  194. package/src/config/model-discovery.ts +30 -13
  195. package/src/config/model-registry.ts +16 -6
  196. package/src/config/models-config-schema-bundle.ts +9 -0
  197. package/src/config/resolve-config-value.ts +2 -1
  198. package/src/config/service-tier.ts +5 -0
  199. package/src/config/settings-schema.ts +73 -21
  200. package/src/cursor-bridge-tools.ts +1 -1
  201. package/src/cursor.ts +10 -14
  202. package/src/discovery/builtin.ts +10 -0
  203. package/src/discovery/helpers.ts +10 -0
  204. package/src/discovery/mcp-json.ts +11 -1
  205. package/src/discovery/omp-extension-roots.ts +79 -20
  206. package/src/discovery/omp-plugins.ts +10 -1
  207. package/src/edit/diff.ts +17 -8
  208. package/src/edit/index.ts +20 -7
  209. package/src/edit/modes/replace.ts +93 -40
  210. package/src/edit/renderer.ts +1 -1
  211. package/src/edit/streaming.ts +14 -17
  212. package/src/eval/py/kernel.ts +2 -1
  213. package/src/eval/py/spawn-options.ts +24 -29
  214. package/src/export/html/template.js +8 -26
  215. package/src/extensibility/extensions/loader.ts +66 -28
  216. package/src/extensibility/extensions/wrapper.ts +1 -0
  217. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  218. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  219. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  220. package/src/extensibility/plugins/marketplace/registry.ts +4 -8
  221. package/src/extensibility/skills.ts +11 -0
  222. package/src/extensibility/utils.ts +10 -2
  223. package/src/launch/broker.ts +1 -1
  224. package/src/launch/client.ts +32 -13
  225. package/src/launch/ensure.ts +73 -0
  226. package/src/launch/paths.ts +3 -6
  227. package/src/launch/protocol.ts +2 -2
  228. package/src/lsp/client.ts +23 -2
  229. package/src/lsp/index.ts +8 -0
  230. package/src/lsp/mux/daemon.ts +348 -0
  231. package/src/lsp/mux/protocol.ts +96 -0
  232. package/src/lsp/mux/server.ts +797 -0
  233. package/src/lsp/types.ts +28 -2
  234. package/src/main.ts +31 -24
  235. package/src/mcp/config.ts +1 -0
  236. package/src/mcp/oauth-flow.ts +11 -1
  237. package/src/mcp/request-id.ts +24 -0
  238. package/src/mcp/transports/http.ts +4 -2
  239. package/src/mcp/transports/sse.ts +4 -2
  240. package/src/mcp/transports/stdio.ts +4 -2
  241. package/src/mcp/types.ts +15 -0
  242. package/src/memory-backend/index.ts +1 -0
  243. package/src/memory-backend/messages.ts +19 -0
  244. package/src/mnemopi/embed-client.ts +49 -2
  245. package/src/mnemopi/state.ts +57 -23
  246. package/src/modes/components/agent-hub.ts +1 -1
  247. package/src/modes/components/assistant-message.ts +11 -0
  248. package/src/modes/components/chat-transcript-builder.ts +4 -0
  249. package/src/modes/components/custom-editor.ts +10 -1
  250. package/src/modes/components/model-browser.ts +25 -17
  251. package/src/modes/components/model-picker.ts +10 -6
  252. package/src/modes/components/read-tool-group.ts +36 -0
  253. package/src/modes/components/status-line/component.ts +1 -1
  254. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  255. package/src/modes/components/tool-execution.ts +85 -20
  256. package/src/modes/components/tree-selector.ts +46 -48
  257. package/src/modes/controllers/command-controller.ts +2 -2
  258. package/src/modes/controllers/event-controller.ts +199 -10
  259. package/src/modes/controllers/input-controller.ts +39 -1
  260. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  261. package/src/modes/controllers/selector-controller.ts +47 -6
  262. package/src/modes/interactive-mode.ts +2 -0
  263. package/src/modes/types.ts +6 -1
  264. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  265. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  266. package/src/modes/utils/ui-helpers.ts +6 -10
  267. package/src/modes/workflow.ts +8 -2
  268. package/src/prompts/agents/init.md +1 -1
  269. package/src/prompts/system/plan-mode-active.md +2 -2
  270. package/src/prompts/system/system-prompt.md +26 -52
  271. package/src/prompts/system/workflow-notice.md +3 -3
  272. package/src/prompts/tools/ast-grep.md +1 -1
  273. package/src/prompts/tools/bash.md +1 -1
  274. package/src/prompts/tools/browser.md +1 -0
  275. package/src/prompts/tools/computer.md +21 -21
  276. package/src/prompts/tools/glob.md +1 -1
  277. package/src/prompts/tools/grep.md +1 -1
  278. package/src/prompts/tools/replace.md +5 -4
  279. package/src/prompts/tools/task.md +4 -4
  280. package/src/registry/agent-lifecycle.ts +36 -11
  281. package/src/registry/agent-registry.ts +12 -5
  282. package/src/sdk.ts +102 -35
  283. package/src/secrets/index.ts +16 -18
  284. package/src/session/agent-session-types.ts +3 -1
  285. package/src/session/agent-session.ts +23 -2
  286. package/src/session/agent-storage.ts +6 -4
  287. package/src/session/history-storage.ts +4 -2
  288. package/src/session/indexed-session-storage.ts +9 -0
  289. package/src/session/messages.ts +31 -7
  290. package/src/session/model-controls.ts +0 -1
  291. package/src/session/prewalk.ts +24 -2
  292. package/src/session/session-advisors.ts +21 -3
  293. package/src/session/session-context.ts +16 -12
  294. package/src/session/session-dump-format.ts +1 -1
  295. package/src/session/session-maintenance.ts +41 -4
  296. package/src/session/session-manager.ts +181 -94
  297. package/src/session/session-paths.ts +118 -55
  298. package/src/session/session-stats.ts +48 -3
  299. package/src/session/session-storage.ts +30 -22
  300. package/src/session/session-tools.ts +25 -23
  301. package/src/session/stream-guards.ts +1 -1
  302. package/src/slash-commands/builtin-registry.ts +29 -26
  303. package/src/system-prompt.ts +14 -4
  304. package/src/task/executor.ts +8 -1
  305. package/src/task/index.ts +23 -8
  306. package/src/task/spawn-policy.ts +14 -0
  307. package/src/thinking.ts +2 -8
  308. package/src/tiny/text.ts +12 -0
  309. package/src/tools/ast-grep.ts +10 -4
  310. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  311. package/src/tools/browser/cmux/rpc.ts +2 -8
  312. package/src/tools/browser/registry.ts +53 -2
  313. package/src/tools/browser/relay/bridge.ts +945 -0
  314. package/src/tools/browser/relay/daemon.ts +117 -0
  315. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  316. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  317. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  318. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  319. package/src/tools/browser/relay/kind.ts +41 -0
  320. package/src/tools/browser/relay/protocol.ts +54 -0
  321. package/src/tools/browser/relay/server.ts +141 -0
  322. package/src/tools/browser/render.ts +4 -1
  323. package/src/tools/browser/shared-daemon.ts +7 -58
  324. package/src/tools/browser/tab-supervisor.ts +5 -4
  325. package/src/tools/browser/tab-worker.ts +33 -12
  326. package/src/tools/browser.ts +26 -1
  327. package/src/tools/computer/exposure.ts +11 -35
  328. package/src/tools/computer/protocol.ts +51 -10
  329. package/src/tools/computer/supervisor.ts +227 -124
  330. package/src/tools/computer/worker-entry.ts +11 -6
  331. package/src/tools/computer/worker.ts +695 -81
  332. package/src/tools/computer-renderer.ts +104 -65
  333. package/src/tools/computer.ts +134 -428
  334. package/src/tools/context.ts +3 -0
  335. package/src/tools/default-renderer.ts +19 -4
  336. package/src/tools/glob.ts +9 -2
  337. package/src/tools/grep.ts +12 -6
  338. package/src/tools/index.ts +10 -6
  339. package/src/tools/inspect-image.ts +32 -6
  340. package/src/tools/render-utils.ts +2 -0
  341. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  342. package/src/tools/tool-timeouts.ts +1 -0
  343. package/src/tools/xdev.ts +29 -2
  344. package/src/utils/clipboard.ts +40 -14
  345. package/src/utils/command-args.ts +26 -28
  346. package/src/utils/tool-choice.ts +0 -13
  347. package/src/web/kagi.ts +2 -1
  348. package/src/web/search/index.ts +18 -2
  349. package/src/web/search/providers/anthropic.ts +7 -2
  350. package/src/web/search/providers/base.ts +2 -0
  351. package/src/web/search/providers/brave.ts +3 -1
  352. package/src/web/search/providers/browser-page.ts +6 -4
  353. package/src/web/search/providers/codex.ts +4 -1
  354. package/src/web/search/providers/duckduckgo.ts +2 -1
  355. package/src/web/search/providers/ecosia.ts +2 -1
  356. package/src/web/search/providers/exa.ts +4 -2
  357. package/src/web/search/providers/firecrawl.ts +3 -1
  358. package/src/web/search/providers/gemini.ts +8 -2
  359. package/src/web/search/providers/google.ts +2 -1
  360. package/src/web/search/providers/jina.ts +12 -2
  361. package/src/web/search/providers/kagi.ts +3 -0
  362. package/src/web/search/providers/kimi.ts +5 -1
  363. package/src/web/search/providers/mojeek.ts +2 -1
  364. package/src/web/search/providers/parallel.ts +5 -1
  365. package/src/web/search/providers/perplexity.ts +6 -3
  366. package/src/web/search/providers/public.ts +1 -1
  367. package/src/web/search/providers/searxng.ts +11 -5
  368. package/src/web/search/providers/startpage.ts +10 -4
  369. package/src/web/search/providers/synthetic.ts +11 -5
  370. package/src/web/search/providers/tavily.ts +3 -1
  371. package/src/web/search/providers/tinyfish.ts +3 -1
  372. package/src/web/search/providers/utils.ts +12 -10
  373. package/src/web/search/providers/xai.ts +2 -2
  374. package/src/web/search/providers/zai.ts +18 -3
  375. package/src/web/search/types.ts +6 -0
package/src/lsp/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { ptree } from "@oh-my-pi/pi-utils";
2
1
  import { type } from "arktype";
3
2
  import { TOOL_TIMEOUTS } from "../tools/tool-timeouts";
4
3
 
@@ -370,6 +369,33 @@ export interface ServerConfig {
370
369
  createClient?: LinterClientFactory;
371
370
  }
372
371
 
372
+ // =============================================================================
373
+ // Transport
374
+ // =============================================================================
375
+
376
+ /** Minimal write sink for the server-bound byte stream (satisfied by `Bun.FileSink` and the mux socket adapter). */
377
+ export interface LspWriteSink {
378
+ write(data: string | Uint8Array): number | Promise<number>;
379
+ flush(): number | void | Promise<number | void>;
380
+ }
381
+
382
+ /**
383
+ * Byte transport carrying one LSP JSON-RPC link. Structurally satisfied by
384
+ * `ptree.ChildProcess<"pipe">` (local server spawn) and by the socket adapter
385
+ * in `mux/daemon.ts` (broker-shared server). `exited` may reject (ptree kill).
386
+ */
387
+ export interface LspTransport {
388
+ readonly stdin: LspWriteSink;
389
+ readonly stdout: ReadableStream<Uint8Array>;
390
+ readonly exited: Promise<number>;
391
+ readonly exitCode: number | null;
392
+ readonly pid?: number;
393
+ /** Present and true on broker-shared mux links; `lsp reload` uses it to request a shared-server restart. */
394
+ readonly sharedMux?: boolean;
395
+ kill(): void;
396
+ peekStderr(): string;
397
+ }
398
+
373
399
  // =============================================================================
374
400
  // Client State
375
401
  // =============================================================================
@@ -401,7 +427,7 @@ export interface LspClient {
401
427
  name: string;
402
428
  cwd: string;
403
429
  config: ServerConfig;
404
- proc: ptree.ChildProcess<"pipe">;
430
+ proc: LspTransport;
405
431
  requestId: number;
406
432
  diagnostics: Map<string, PublishedDiagnostics>;
407
433
  diagnosticsVersion: number;
package/src/main.ts CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  type ScopedModel,
42
42
  } from "./config/model-resolver";
43
43
  import { ModelsConfigFile } from "./config/models-config";
44
+ import { serviceTierSettingToTier } from "./config/service-tier";
44
45
  import { getDefault, type SettingPath, Settings, type SettingValue, settings } from "./config/settings";
45
46
  import { initializeWithSettings } from "./discovery";
46
47
  import {
@@ -188,8 +189,9 @@ function applyAcpDefaultSettingOverrides(targetSettings: Settings = settings): v
188
189
  applyDefaultSettingOverrides(HOST_DEFAULTED_SETTING_PATHS, targetSettings);
189
190
  }
190
191
 
191
- async function readPipedInput(): Promise<string | undefined> {
192
- if (process.stdin.isTTY !== false) return undefined;
192
+ /** Reads a non-TTY stdin stream as prompt text. */
193
+ export async function readPipedInput(): Promise<string | undefined> {
194
+ if (process.stdin.isTTY === true) return undefined;
193
195
  // stdin is a pipe: a producer that never writes nor closes would block
194
196
  // startup forever with zero output. Say what we're blocked on after 1s.
195
197
  const notice = setTimeout(() => {
@@ -862,6 +864,9 @@ export async function buildSessionOptions(
862
864
  autoApprove: parsed.autoApprove ?? false,
863
865
  };
864
866
  const restoringSession = Boolean(parsed.continue || parsed.resume || isForeignSessionImport(parsed));
867
+ if (parsed.serviceTier !== undefined) {
868
+ options.openAIServiceTier = serviceTierSettingToTier(parsed.serviceTier) ?? null;
869
+ }
865
870
  const cliDirs = parsed.addDir ?? [];
866
871
  const settingsDirs = activeSettings.get("workspace.additionalDirectories");
867
872
  if (cliDirs.length > 0 || settingsDirs.length > 0) {
@@ -1110,14 +1115,13 @@ export async function buildSessionOptions(
1110
1115
  }
1111
1116
 
1112
1117
  // Additional extension paths from CLI
1113
- const cliExtensionPaths = parsed.noExtensions ? [] : [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
1118
+ const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
1114
1119
  if (cliExtensionPaths.length > 0) {
1115
1120
  options.additionalExtensionPaths = cliExtensionPaths;
1116
1121
  }
1117
1122
 
1118
1123
  if (parsed.noExtensions) {
1119
1124
  options.disableExtensionDiscovery = true;
1120
- options.additionalExtensionPaths = [];
1121
1125
  }
1122
1126
 
1123
1127
  return options;
@@ -1151,10 +1155,6 @@ export async function runRootCommand(
1151
1155
 
1152
1156
  const notifs: (InteractiveModeNotify | null)[] = [];
1153
1157
 
1154
- // Create AuthStorage and ModelRegistry upfront
1155
- const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
1156
- const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
1157
-
1158
1158
  if (parsedArgs.version) {
1159
1159
  writeStartupNotice(parsedArgs, `${VERSION}\n`);
1160
1160
  process.exit(0);
@@ -1197,15 +1197,31 @@ export async function runRootCommand(
1197
1197
  // Register CLI-provided extension package paths (`--extension`, `--hook`) so
1198
1198
  // the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
1199
1199
  // `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
1200
- // `--no-extensions` short-circuits both the factory load and the sub-discovery.
1201
- if (!parsedArgs.noExtensions) {
1202
- const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
1203
- if (cliExtensions.length > 0) {
1204
- injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir());
1205
- }
1206
- }
1200
+ // Explicit roots remain authorized under `--no-extensions`; only ambient
1201
+ // extension discovery is disabled.
1202
+ const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
1203
+ injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
1204
+ mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
1205
+ replace: true,
1206
+ });
1207
1207
 
1208
1208
  let cwd = getProjectDir();
1209
+ // Classify the host before opening auth or settings storage so every
1210
+ // session-critical database connection picks the right busy timeout.
1211
+ // See getDbBusyTimeoutMs().
1212
+ const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
1213
+ // Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
1214
+ const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
1215
+ const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
1216
+ const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
1217
+ // Only the interactive host renders a focusable Agent Hub / subagent session
1218
+ // tree; declare it so headless subagent optimizations (e.g. skipping replan
1219
+ // title refresh) can tell a focusable process from a print/RPC/eval one.
1220
+ setInteractiveHost(isInteractive);
1221
+ // Create AuthStorage and ModelRegistry upfront
1222
+ const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
1223
+ const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
1224
+
1209
1225
  const settingsInstance =
1210
1226
  deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd, configFiles: parsedArgs.config }));
1211
1227
  if (parsedArgs.approvalMode) {
@@ -1228,15 +1244,6 @@ export async function runRootCommand(
1228
1244
  if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
1229
1245
  Bun.env.PI_NO_TITLE = "1";
1230
1246
  }
1231
- const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
1232
- // Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
1233
- const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
1234
- const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
1235
- const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
1236
- // Only the interactive host renders a focusable Agent Hub / subagent session
1237
- // tree; declare it so headless subagent optimizations (e.g. skipping replan
1238
- // title refresh) can tell a focusable process from a print/RPC/eval one.
1239
- setInteractiveHost(isInteractive);
1240
1247
 
1241
1248
  // Initialize discovery system with settings for provider persistence
1242
1249
  logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
package/src/mcp/config.ts CHANGED
@@ -41,6 +41,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
41
41
  const shared = {
42
42
  enabled: server.enabled,
43
43
  timeout: server.timeout,
44
+ requestIdFormat: server.requestIdFormat,
44
45
  auth: server.auth,
45
46
  oauth: server.oauth,
46
47
  };
@@ -489,12 +489,22 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
489
489
  }
490
490
 
491
491
  const data = (await response.json()) as {
492
- access_token: string;
492
+ access_token?: string;
493
493
  refresh_token?: string;
494
494
  expires_in?: number;
495
495
  token_type?: string;
496
+ error?: string;
497
+ error_description?: string;
496
498
  };
497
499
 
500
+ // Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and
501
+ // an `{ ok: false, error }` body. Accepting such a response would store an
502
+ // empty access token and only surface `invalid_token` on a later request.
503
+ if (typeof data.access_token !== "string" || data.access_token.length === 0) {
504
+ const providerError = data.error_description ?? data.error;
505
+ throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
506
+ }
507
+
498
508
  // Calculate expiry timestamp
499
509
  const expiresIn = data.expires_in ?? 3600; // Default to 1 hour
500
510
  const expires = Date.now() + expiresIn * 1000;
@@ -0,0 +1,24 @@
1
+ import { Snowflake } from "@oh-my-pi/pi-utils";
2
+ import type { MCPRequestIdFormat } from "./types";
3
+
4
+ /**
5
+ * Per-transport allocator for outgoing JSON-RPC request ids.
6
+ *
7
+ * JSON-RPC 2.0 permits String and Number ids equally, but the wider MCP
8
+ * ecosystem (reference SDKs included) issues incrementing integers, so servers
9
+ * are best tested against numbers — Apple's `xcrun mcpbridge` even logs
10
+ * `mcpbridge.DecodeError Code=1` and never answers a string id. Integers are
11
+ * therefore the default. `"string"` opts a server back into collision-resistant
12
+ * snowflake ids.
13
+ *
14
+ * The counter is per instance and never reset, so ids stay unique for the life
15
+ * of the transport even though a reconnect rejects and clears every pending
16
+ * request.
17
+ */
18
+ export class RequestIdAllocator {
19
+ #previousNumeric = 0;
20
+
21
+ next(format: MCPRequestIdFormat | undefined): string | number {
22
+ return format === "string" ? Snowflake.next() : ++this.#previousNumeric;
23
+ }
24
+ }
@@ -5,7 +5,7 @@
5
5
  * Based on MCP spec 2025-03-26.
6
6
  */
7
7
  import * as AIError from "@oh-my-pi/pi-ai/error";
8
- import { logger, readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
8
+ import { logger, readSseJson } from "@oh-my-pi/pi-utils";
9
9
  import type {
10
10
  JsonRpcError,
11
11
  JsonRpcMessage,
@@ -17,6 +17,7 @@ import type {
17
17
  MCPTransport,
18
18
  } from "../../mcp/types";
19
19
  import { toJsonRpcError } from "../../mcp/types";
20
+ import { RequestIdAllocator } from "../request-id";
20
21
  import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
21
22
 
22
23
  const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
@@ -42,6 +43,7 @@ export class HttpTransport implements MCPTransport {
42
43
  #connected = false;
43
44
  #sessionId: string | null = null;
44
45
  #sseConnection: AbortController | null = null;
46
+ readonly #requestIds = new RequestIdAllocator();
45
47
 
46
48
  onClose?: () => void;
47
49
  onError?: (error: Error) => void;
@@ -209,7 +211,7 @@ export class HttpTransport implements MCPTransport {
209
211
  throw new Error("Transport not connected");
210
212
  }
211
213
 
212
- const id = Snowflake.next();
214
+ const id = this.#requestIds.next(this.config.requestIdFormat);
213
215
  const body = {
214
216
  jsonrpc: "2.0" as const,
215
217
  id,
@@ -1,5 +1,5 @@
1
1
  import * as AIError from "@oh-my-pi/pi-ai/error";
2
- import { logger, readSseEvents, Snowflake } from "@oh-my-pi/pi-utils";
2
+ import { logger, readSseEvents } from "@oh-my-pi/pi-utils";
3
3
  import type {
4
4
  JsonRpcError,
5
5
  JsonRpcMessage,
@@ -10,6 +10,7 @@ import type {
10
10
  MCPTransport,
11
11
  } from "../../mcp/types";
12
12
  import { toJsonRpcError } from "../../mcp/types";
13
+ import { RequestIdAllocator } from "../request-id";
13
14
  import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
14
15
 
15
16
  interface MCPTimeoutOperation {
@@ -32,6 +33,7 @@ export class LegacySseTransport implements MCPTransport {
32
33
  #sseConnection: AbortController | null = null;
33
34
  #pending = new Map<string | number, PendingLegacySseRequest>();
34
35
  #config: MCPSseServerConfig;
36
+ readonly #requestIds = new RequestIdAllocator();
35
37
 
36
38
  onClose?: () => void;
37
39
  onError?: (error: Error) => void;
@@ -194,7 +196,7 @@ export class LegacySseTransport implements MCPTransport {
194
196
  throw new Error("Transport not connected");
195
197
  }
196
198
 
197
- const id = Snowflake.next();
199
+ const id = this.#requestIds.next(this.#config.requestIdFormat);
198
200
  const body = {
199
201
  jsonrpc: "2.0" as const,
200
202
  id,
@@ -7,7 +7,7 @@
7
7
 
8
8
  import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
- import { getProjectDir, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
10
+ import { getProjectDir, readJsonl } from "@oh-my-pi/pi-utils";
11
11
  import type { Subprocess } from "bun";
12
12
  import { hostHasInheritableConsole } from "../../eval/py/spawn-options";
13
13
  import type {
@@ -20,6 +20,7 @@ import type {
20
20
  MCPTransport,
21
21
  } from "../../mcp/types";
22
22
  import { toJsonRpcError } from "../../mcp/types";
23
+ import { RequestIdAllocator } from "../request-id";
23
24
  import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
24
25
 
25
26
  /** Subprocess argv and platform-derived spawn flags for an MCP stdio server. */
@@ -551,6 +552,7 @@ export class StdioTransport implements MCPTransport {
551
552
  * actually spawned into its own session may target it.
552
553
  */
553
554
  #detached = false;
555
+ readonly #requestIds = new RequestIdAllocator();
554
556
 
555
557
  onClose?: () => void;
556
558
  onError?: (error: Error) => void;
@@ -734,7 +736,7 @@ export class StdioTransport implements MCPTransport {
734
736
  throw new Error("Transport not connected");
735
737
  }
736
738
 
737
- const id = Snowflake.next();
739
+ const id = this.#requestIds.next(this.config.requestIdFormat);
738
740
  const request = {
739
741
  jsonrpc: "2.0" as const,
740
742
  id,
package/src/mcp/types.ts CHANGED
@@ -59,12 +59,27 @@ export interface MCPAuthConfig {
59
59
  resource?: string;
60
60
  }
61
61
 
62
+ /** Encoding used for outgoing JSON-RPC request ids. */
63
+ export type MCPRequestIdFormat = "string" | "number";
64
+
62
65
  /** Base server config with shared options */
63
66
  interface MCPServerConfigBase {
64
67
  /** Whether this server is enabled (default: true) */
65
68
  enabled?: boolean;
66
69
  /** MCP request timeout in milliseconds (default: 30000, 0 to disable) */
67
70
  timeout?: number;
71
+ /**
72
+ * Encoding for outgoing JSON-RPC request ids (default: `"number"`).
73
+ *
74
+ * Set `"string"` for servers that need collision-resistant snowflake string
75
+ * ids instead of per-transport integers. See `RequestIdAllocator` in
76
+ * `./request-id`.
77
+ *
78
+ * OMP-specific, so only the OMP-owned discovery providers parse it (native,
79
+ * standalone `mcp.json`, OMP plugins). Providers that translate another
80
+ * tool's config do not, since the key is not part of those formats.
81
+ */
82
+ requestIdFormat?: MCPRequestIdFormat;
68
83
  /** Authentication configuration (optional) */
69
84
  auth?: MCPAuthConfig;
70
85
  /** OAuth configuration for servers requiring explicit client credentials */
@@ -12,6 +12,7 @@ export type {
12
12
  MnemopiSessionStateOptions,
13
13
  } from "../mnemopi/state";
14
14
  export * from "./local-backend";
15
+ export * from "./messages";
15
16
  export * from "./off-backend";
16
17
  export * from "./resolve";
17
18
  export * from "./runtime";
@@ -0,0 +1,19 @@
1
+ import type { MemoryBackendId } from "./types";
2
+
3
+ /**
4
+ * Fallback text for `/memory stats` and `/memory diagnose` when the active
5
+ * backend's hook is missing or returns nothing.
6
+ *
7
+ * The `off` backend isn't a real backend a user picked among several
8
+ * stats-capable options — it's the no-op state memory falls back to by
9
+ * default — so it gets its own message instead of the generic
10
+ * "not available for the X backend" template, which would otherwise read as
11
+ * the self-contradictory "not available for the off backend".
12
+ *
13
+ * Shared by both the TUI (`CommandController.handleMemoryCommand`) and the
14
+ * ACP/RPC slash-command handler so the two surfaces stay consistent.
15
+ */
16
+ export function memoryStatsUnavailableMessage(backendId: MemoryBackendId, action: "stats" | "diagnose"): string {
17
+ if (backendId === "off") return "Memory backend is off — there is nothing to show.";
18
+ return `Memory ${action} is not available for the ${backendId} backend.`;
19
+ }
@@ -84,6 +84,22 @@ export interface MnemopiSubprocessEmbeddingModel {
84
84
  embed(texts: string[], batchSize?: number): AsyncIterable<number[][]>;
85
85
  }
86
86
 
87
+ /**
88
+ * Upper bound on a steady-state embed IPC round-trip. Initialization is
89
+ * intentionally exempt: bundled installs may spend several minutes installing
90
+ * fastembed and bootstrapping the model, and killing that worker can strand the
91
+ * runtime install lock. Once initialization succeeds, a longer embed stall
92
+ * means a hung native runtime (issue #4792) that would otherwise pin whatever
93
+ * awaits the embed — a turn's memory recall or the headless shutdown
94
+ * consolidation — indefinitely, leaving the process alive with an unreaped
95
+ * `__omp_worker_mnemopi_embed` child (issue #7352). On expiry the embed fails
96
+ * and the worker is SIGKILL-reaped so the next request respawns a fresh one.
97
+ */
98
+ const EMBED_REQUEST_TIMEOUT_MS = 120_000;
99
+
100
+ /** Race marker for {@link MnemopiEmbedClient.#awaitRequest}. */
101
+ const REQUEST_TIMED_OUT = Symbol("mnemopi.embed.timedOut");
102
+
87
103
  export class MnemopiEmbedClient {
88
104
  #worker: MnemopiEmbedWorkerHandle | null = null;
89
105
  #unsubscribeMessage: (() => void) | null = null;
@@ -91,9 +107,14 @@ export class MnemopiEmbedClient {
91
107
  #pending = new Map<string, PendingRequest>();
92
108
  #nextRequestId = 0;
93
109
  #spawnWorker: () => MnemopiEmbedWorkerHandle;
110
+ #requestTimeoutMs: number;
94
111
 
95
- constructor(spawnWorker: () => MnemopiEmbedWorkerHandle = spawnMnemopiEmbedWorker) {
112
+ constructor(
113
+ spawnWorker: () => MnemopiEmbedWorkerHandle = spawnMnemopiEmbedWorker,
114
+ requestTimeoutMs: number = EMBED_REQUEST_TIMEOUT_MS,
115
+ ) {
96
116
  this.#spawnWorker = spawnWorker;
117
+ this.#requestTimeoutMs = requestTimeoutMs;
97
118
  }
98
119
 
99
120
  /**
@@ -166,7 +187,7 @@ export class MnemopiEmbedClient {
166
187
  // worker's "embed before init" guard. Worker `ensureLoaded` is
167
188
  // idempotent so steady-state embeds pay no extra cost.
168
189
  worker.send({ type: "embed", id, model, cacheDir, texts, batchSize });
169
- const result = await promise;
190
+ const result = await this.#awaitRequest(promise);
170
191
  if (result instanceof Error) throw result;
171
192
  return result;
172
193
  } finally {
@@ -174,6 +195,32 @@ export class MnemopiEmbedClient {
174
195
  }
175
196
  }
176
197
 
198
+ /**
199
+ * Await one steady-state embed reply, bounded by
200
+ * {@link EMBED_REQUEST_TIMEOUT_MS}. The timeout timer is `unref`'d so a
201
+ * pending request never keeps the parent event loop alive on its own (the
202
+ * awaiting caller does). On expiry the wedged worker is SIGKILL-reaped via
203
+ * {@link terminate} — faulting any other in-flight request and letting the
204
+ * next call respawn a fresh child — before the request rejects, so a hung
205
+ * native runtime cannot pin a turn's recall or shutdown consolidation
206
+ * forever (issue #7352).
207
+ */
208
+ async #awaitRequest<T>(promise: Promise<T>): Promise<T> {
209
+ const { promise: timedOut, resolve: fire } = Promise.withResolvers<typeof REQUEST_TIMED_OUT>();
210
+ const timer = setTimeout(() => fire(REQUEST_TIMED_OUT), this.#requestTimeoutMs);
211
+ timer.unref();
212
+ try {
213
+ const winner = await Promise.race([promise, timedOut]);
214
+ if (winner === REQUEST_TIMED_OUT) {
215
+ void this.terminate();
216
+ throw new Error("mnemopi embed worker request timed out");
217
+ }
218
+ return winner;
219
+ } finally {
220
+ clearTimeout(timer);
221
+ }
222
+ }
223
+
177
224
  async *#streamEmbed(
178
225
  model: MnemopiEmbedModelId,
179
226
  cacheDir: string | undefined,
@@ -4,7 +4,7 @@ import type * as MnemopiNs from "@oh-my-pi/pi-mnemopi";
4
4
  import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
5
5
  import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
6
6
  import type { LocalModelInitializer } from "@oh-my-pi/pi-mnemopi/core";
7
- import { logger } from "@oh-my-pi/pi-utils";
7
+ import { logger, toError } from "@oh-my-pi/pi-utils";
8
8
  import {
9
9
  composeRecallQuery,
10
10
  formatCurrentTime,
@@ -386,6 +386,8 @@ export class MnemopiSessionState {
386
386
  const merged: RecallResult[] = [];
387
387
  const byId = new Map<string, number>();
388
388
  const byContent = new Map<string, number>();
389
+ const failures: Array<{ bank: string; error: Error }> = [];
390
+ let successfulTargets = 0;
389
391
  const sharedFallbackQuery = deriveSharedRecallFallbackQuery(
390
392
  query,
391
393
  this.scoped.retain.bank,
@@ -394,24 +396,35 @@ export class MnemopiSessionState {
394
396
  for (const target of this.scoped.recall) {
395
397
  const queries =
396
398
  target.bank === this.scoped.global?.bank && sharedFallbackQuery ? [query, sharedFallbackQuery] : [query];
399
+ let targetSucceeded = false;
397
400
  try {
398
401
  for (const recallQuery of queries) {
399
402
  const results = await target.memory.recallEnhanced(recallQuery, this.config.recallLimit, {
400
403
  includeFacts: true,
401
404
  channelId: target.bank,
402
405
  });
406
+ targetSucceeded = true;
403
407
  for (const result of results) {
404
408
  mergeRecallResult(merged, byId, byContent, result);
405
409
  }
406
410
  }
407
411
  } catch (error) {
408
- if (this.config.debug) {
409
- logger.debug("Mnemopi: scoped recall target failed", {
410
- bank: target.bank,
411
- error: String(error),
412
- });
413
- }
412
+ const failure = toError(error);
413
+ failures.push({ bank: target.bank, error: failure });
414
+ logger.warn("Mnemopi: scoped recall target failed", {
415
+ bank: target.bank,
416
+ error: failure.message,
417
+ });
414
418
  }
419
+ if (targetSucceeded) successfulTargets++;
420
+ }
421
+ if (successfulTargets === 0 && failures.length > 0) {
422
+ if (failures.length === 1) throw failures[0].error;
423
+ const details = failures.map(({ bank, error }) => `${bank}: ${error.message}`).join("; ");
424
+ throw new AggregateError(
425
+ failures.map(({ error }) => error),
426
+ `Mnemopi recall failed for all scoped targets (${details})`,
427
+ );
415
428
  }
416
429
  merged.sort(compareRecallResults);
417
430
  if (merged.length > this.config.recallLimit) merged.length = this.config.recallLimit;
@@ -575,7 +588,16 @@ export class MnemopiSessionState {
575
588
  if (!lastUser) return;
576
589
  const query = composeRecallQuery(lastUser.content, messages, this.config.recallContextTurns);
577
590
  const truncated = truncateRecallQuery(query, lastUser.content, this.config.recallMaxQueryChars);
578
- const context = await this.recallForContext(truncated);
591
+ let context: string | undefined;
592
+ try {
593
+ context = await this.recallForContext(truncated);
594
+ } catch (error) {
595
+ logger.warn("Mnemopi: auto-recall failed", {
596
+ bank: this.config.bank,
597
+ error: toError(error).message,
598
+ });
599
+ return;
600
+ }
579
601
  this.hasRecalledForFirstTurn = true;
580
602
  if (!context) return;
581
603
  this.lastRecallSnippet = context;
@@ -639,17 +661,26 @@ export class MnemopiSessionState {
639
661
  * tokens on memories that will be wiped on the next line is wasted work
640
662
  * (PR #2327 review).
641
663
  *
642
- * `timeoutMs` caps how long the consolidate await blocks the caller
643
- * (the user-visible `/quit` / `/exit` shutdown path passes this so
644
- * dispose returns within a UX budget issue #3641). When the cap is
645
- * hit, dispose returns immediately and detaches the still-in-flight
646
- * consolidate; the SQLite handles are closed in the background once
647
- * the consolidate settles so writes never race a closed handle, and
648
- * any pending embeddings are SIGKILL'd along with the embed worker
664
+ * `timeoutMs` caps both synchronous SQLite lock waits during final retention
665
+ * and the asynchronous consolidation drain (the user-visible `/quit`,
666
+ * `/exit`, and print paths pass this so disposal stays within their shutdown
667
+ * budget). When the cap is hit, dispose returns immediately and detaches the
668
+ * still-in-flight consolidate; the SQLite handles are closed in the
669
+ * background once the consolidate settles so writes never race a closed handle,
670
+ * and any pending embeddings are SIGKILL'd along with the embed worker
649
671
  * (a tolerable loss — working memory rows are durable; only the
650
672
  * episodic promotion / embedding for the LAST few turns is skipped,
651
673
  * and `maybeRetainOnAgentEnd` has already retained earlier turns).
652
674
  */
675
+ #boundOwnedBusyTimeout(timeoutMs: number): void {
676
+ // SQLite lock waits block the JS thread, so a Promise race cannot interrupt
677
+ // them. consolidate() flushes every owned bank, so bound each one — not just
678
+ // the retain bank — or a locked shared bank (per-project-tagged) still stalls
679
+ // teardown for Mnemopi's default 5s busy timeout (#7351 review).
680
+ const busyTimeoutMs = Math.max(1, Math.floor(timeoutMs));
681
+ for (const memory of this.scoped.owned) memory.beam.db.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
682
+ }
683
+
653
684
  async dispose(options: { consolidate?: boolean; timeoutMs?: number } = {}): Promise<void> {
654
685
  this.unsubscribe?.();
655
686
  this.unsubscribe = undefined;
@@ -661,19 +692,22 @@ export class MnemopiSessionState {
661
692
  closeOwned();
662
693
  return;
663
694
  }
695
+ const { timeoutMs } = options;
696
+ const boundedTimeoutMs = timeoutMs !== undefined && timeoutMs > 0 ? timeoutMs : undefined;
697
+ const deadline = boundedTimeoutMs !== undefined ? performance.now() + boundedTimeoutMs : undefined;
698
+ if (boundedTimeoutMs !== undefined) this.#boundOwnedBusyTimeout(boundedTimeoutMs);
664
699
  const consolidatePromise = this.consolidate({ full: false, extract: false, sleep: false }).catch(
665
700
  (error: unknown) => {
666
701
  logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
667
702
  },
668
703
  );
669
- const { timeoutMs } = options;
670
- if (timeoutMs !== undefined && timeoutMs > 0) {
671
- const TIMED_OUT = Symbol("mnemopi.dispose.timedOut");
672
- const winner = await Promise.race([
673
- consolidatePromise.then(() => undefined as unknown),
674
- Bun.sleep(timeoutMs).then(() => TIMED_OUT as unknown),
675
- ]);
676
- if (winner === TIMED_OUT) {
704
+ if (deadline !== undefined) {
705
+ const remainingMs = deadline - performance.now();
706
+ const completed =
707
+ remainingMs > 0
708
+ ? await Promise.race([consolidatePromise.then(() => true), Bun.sleep(remainingMs).then(() => false)])
709
+ : false;
710
+ if (!completed) {
677
711
  logger.warn("Mnemopi: consolidate-on-dispose exceeded shutdown budget; detaching to background.", {
678
712
  timeoutMs,
679
713
  });
@@ -636,7 +636,7 @@ export class AgentHubOverlayComponent extends Container {
636
636
  if (ref.status === "running" && ref.session) {
637
637
  await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
638
638
  }
639
- await this.#lifecycle().release(ref.id, ref);
639
+ await this.#lifecycle().release(ref.id, ref, { tombstone: true });
640
640
  } catch (error) {
641
641
  logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
642
642
  this.#notice = error instanceof Error ? error.message : String(error);
@@ -185,6 +185,7 @@ export class AssistantMessageComponent extends Container {
185
185
  #toolImagesByCallId = new Map<string, ImageContent[]>();
186
186
  #convertedKittyImages = new Map<string, ImageContent>();
187
187
  #showImages = true;
188
+ #showToolResultImages = true;
188
189
  #kittyConversionsInFlight = new Set<string>();
189
190
  #transcriptBlockFinalized: boolean;
190
191
  /**
@@ -578,6 +579,15 @@ export class AssistantMessageComponent extends Container {
578
579
  }
579
580
  }
580
581
 
582
+ /** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
583
+ setToolResultImagesVisible(visible: boolean): void {
584
+ if (this.#showToolResultImages === visible) return;
585
+ this.#showToolResultImages = visible;
586
+ if (this.#lastMessage) {
587
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
588
+ }
589
+ }
590
+
581
591
  setToolResultImages(toolCallId: string, images: ImageContent[]): void {
582
592
  if (!toolCallId) return;
583
593
  const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
@@ -649,6 +659,7 @@ export class AssistantMessageComponent extends Container {
649
659
  }
650
660
 
651
661
  #renderToolImages(): void {
662
+ if (!this.#showToolResultImages) return;
652
663
  const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
653
664
  images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
654
665
  );