@oh-my-pi/pi-coding-agent 15.12.3 → 15.13.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 (457) hide show
  1. package/CHANGELOG.md +347 -7
  2. package/dist/cli.js +1615 -1231
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  8. package/dist/types/autoresearch/tools/log-experiment.d.ts +1 -1
  9. package/dist/types/autoresearch/tools/run-experiment.d.ts +1 -1
  10. package/dist/types/autoresearch/tools/update-notes.d.ts +1 -1
  11. package/dist/types/autoresearch/types.d.ts +1 -1
  12. package/dist/types/cli/args.d.ts +19 -2
  13. package/dist/types/cli/models-cli.d.ts +49 -0
  14. package/dist/types/cli/session-picker.d.ts +1 -1
  15. package/dist/types/cli/setup-cli.d.ts +1 -1
  16. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  17. package/dist/types/collab/protocol.d.ts +1 -1
  18. package/dist/types/commands/launch.d.ts +0 -3
  19. package/dist/types/commands/models.d.ts +33 -0
  20. package/dist/types/commands/say.d.ts +24 -0
  21. package/dist/types/commands/token.d.ts +25 -0
  22. package/dist/types/commit/agentic/tools/analyze-file.d.ts +1 -1
  23. package/dist/types/commit/agentic/tools/git-file-diff.d.ts +1 -1
  24. package/dist/types/commit/agentic/tools/git-hunk.d.ts +1 -1
  25. package/dist/types/commit/agentic/tools/git-overview.d.ts +1 -1
  26. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +1 -1
  27. package/dist/types/commit/agentic/tools/propose-commit.d.ts +1 -1
  28. package/dist/types/commit/agentic/tools/recent-commits.d.ts +1 -1
  29. package/dist/types/commit/agentic/tools/schemas.d.ts +1 -1
  30. package/dist/types/commit/agentic/tools/split-commit.d.ts +1 -1
  31. package/dist/types/commit/changelog/generate.d.ts +1 -1
  32. package/dist/types/commit/shared-llm.d.ts +1 -1
  33. package/dist/types/config/keybindings.d.ts +3 -3
  34. package/dist/types/config/model-registry.d.ts +17 -0
  35. package/dist/types/config/models-config-schema.d.ts +13 -1
  36. package/dist/types/config/models-config.d.ts +8 -2
  37. package/dist/types/config/settings-schema.d.ts +281 -58
  38. package/dist/types/edit/hashline/params.d.ts +1 -1
  39. package/dist/types/edit/modes/apply-patch.d.ts +1 -1
  40. package/dist/types/edit/modes/patch.d.ts +1 -1
  41. package/dist/types/edit/modes/replace.d.ts +1 -1
  42. package/dist/types/export/html/index.d.ts +2 -1
  43. package/dist/types/extensibility/custom-commands/types.d.ts +2 -2
  44. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  46. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  47. package/dist/types/extensibility/extensions/types.d.ts +49 -3
  48. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  49. package/dist/types/extensibility/hooks/types.d.ts +2 -2
  50. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  51. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  52. package/dist/types/extensibility/shared-events.d.ts +1 -1
  53. package/dist/types/extensibility/skills.d.ts +10 -0
  54. package/dist/types/goals/guided-setup.d.ts +18 -0
  55. package/dist/types/goals/state.d.ts +1 -1
  56. package/dist/types/goals/tools/goal-tool.d.ts +1 -1
  57. package/dist/types/hindsight/transcript.d.ts +1 -1
  58. package/dist/types/index.d.ts +5 -0
  59. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  60. package/dist/types/lsp/types.d.ts +1 -1
  61. package/dist/types/main.d.ts +4 -3
  62. package/dist/types/mcp/manager.d.ts +8 -0
  63. package/dist/types/mcp/startup-events.d.ts +11 -0
  64. package/dist/types/memories/index.d.ts +7 -0
  65. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  66. package/dist/types/mnemopi/config.d.ts +28 -0
  67. package/dist/types/modes/acp/acp-agent.d.ts +1 -2
  68. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  69. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  70. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  71. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  72. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +1 -0
  74. package/dist/types/modes/components/logout-account-selector.d.ts +8 -0
  75. package/dist/types/modes/components/session-selector.d.ts +1 -1
  76. package/dist/types/modes/components/status-line/component.d.ts +9 -5
  77. package/dist/types/modes/components/status-line/types.d.ts +2 -1
  78. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  79. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  80. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  81. package/dist/types/modes/components/usage-row.d.ts +3 -0
  82. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  83. package/dist/types/modes/controllers/event-controller.d.ts +0 -17
  84. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  85. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  86. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  87. package/dist/types/modes/image-references.d.ts +6 -0
  88. package/dist/types/modes/interactive-mode.d.ts +27 -6
  89. package/dist/types/modes/magic-keywords.d.ts +13 -1
  90. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  91. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  92. package/dist/types/modes/runtime-init.d.ts +4 -0
  93. package/dist/types/modes/theme/theme.d.ts +13 -2
  94. package/dist/types/modes/types.d.ts +8 -7
  95. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  96. package/dist/types/registry/agent-registry.d.ts +17 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +28 -35
  99. package/dist/types/session/agent-storage.d.ts +2 -1
  100. package/dist/types/session/indexed-session-storage.d.ts +3 -3
  101. package/dist/types/session/messages.d.ts +8 -10
  102. package/dist/types/session/session-context.d.ts +39 -0
  103. package/dist/types/session/session-entries.d.ts +159 -0
  104. package/dist/types/session/session-listing.d.ts +69 -0
  105. package/dist/types/session/session-loader.d.ts +16 -0
  106. package/dist/types/session/session-manager.d.ts +85 -462
  107. package/dist/types/session/session-migrations.d.ts +12 -0
  108. package/dist/types/session/session-paths.d.ts +25 -0
  109. package/dist/types/session/session-persistence.d.ts +8 -0
  110. package/dist/types/session/session-storage.d.ts +11 -7
  111. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  112. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  113. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  114. package/dist/types/slash-commands/helpers/logout.d.ts +15 -0
  115. package/dist/types/stt/asr-client.d.ts +90 -0
  116. package/dist/types/stt/asr-protocol.d.ts +97 -0
  117. package/dist/types/stt/asr-worker.d.ts +2 -0
  118. package/dist/types/stt/downloader.d.ts +38 -0
  119. package/dist/types/stt/endpointer.d.ts +59 -0
  120. package/dist/types/stt/index.d.ts +5 -1
  121. package/dist/types/stt/models.d.ts +120 -0
  122. package/dist/types/stt/recorder.d.ts +17 -0
  123. package/dist/types/stt/stt-controller.d.ts +6 -0
  124. package/dist/types/stt/transcriber.d.ts +5 -7
  125. package/dist/types/stt/wav.d.ts +29 -0
  126. package/dist/types/system-prompt.d.ts +4 -0
  127. package/dist/types/task/executor.d.ts +2 -0
  128. package/dist/types/task/index.d.ts +9 -1
  129. package/dist/types/task/types.d.ts +37 -1
  130. package/dist/types/tools/ask.d.ts +1 -1
  131. package/dist/types/tools/ast-edit.d.ts +1 -1
  132. package/dist/types/tools/ast-grep.d.ts +1 -1
  133. package/dist/types/tools/bash.d.ts +3 -3
  134. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +202 -0
  135. package/dist/types/tools/browser/cmux/rpc.d.ts +70 -0
  136. package/dist/types/tools/browser/cmux/socket-client.d.ts +19 -0
  137. package/dist/types/tools/browser/registry.d.ts +16 -3
  138. package/dist/types/tools/browser/render.d.ts +2 -0
  139. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  140. package/dist/types/tools/browser/tab-supervisor.d.ts +16 -4
  141. package/dist/types/tools/browser.d.ts +3 -1
  142. package/dist/types/tools/checkpoint.d.ts +1 -1
  143. package/dist/types/tools/debug.d.ts +1 -1
  144. package/dist/types/tools/eval-render.d.ts +1 -1
  145. package/dist/types/tools/eval.d.ts +1 -1
  146. package/dist/types/tools/find.d.ts +1 -1
  147. package/dist/types/tools/gh.d.ts +1 -1
  148. package/dist/types/tools/image-gen.d.ts +1 -1
  149. package/dist/types/tools/index.d.ts +14 -2
  150. package/dist/types/tools/inspect-image.d.ts +1 -1
  151. package/dist/types/tools/irc.d.ts +2 -1
  152. package/dist/types/tools/job.d.ts +1 -1
  153. package/dist/types/tools/learn.d.ts +51 -0
  154. package/dist/types/tools/manage-skill.d.ts +40 -0
  155. package/dist/types/tools/memory-edit.d.ts +1 -1
  156. package/dist/types/tools/memory-recall.d.ts +1 -1
  157. package/dist/types/tools/memory-reflect.d.ts +1 -1
  158. package/dist/types/tools/memory-retain.d.ts +1 -1
  159. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  160. package/dist/types/tools/read.d.ts +1 -1
  161. package/dist/types/tools/render-mermaid.d.ts +1 -1
  162. package/dist/types/tools/renderers.d.ts +7 -11
  163. package/dist/types/tools/resolve.d.ts +1 -1
  164. package/dist/types/tools/review.d.ts +1 -1
  165. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  166. package/dist/types/tools/search.d.ts +1 -1
  167. package/dist/types/tools/ssh.d.ts +2 -2
  168. package/dist/types/tools/todo.d.ts +2 -2
  169. package/dist/types/tools/tts.d.ts +26 -1
  170. package/dist/types/tools/write.d.ts +2 -2
  171. package/dist/types/tts/downloader.d.ts +20 -0
  172. package/dist/types/tts/index.d.ts +8 -0
  173. package/dist/types/tts/models.d.ts +82 -0
  174. package/dist/types/tts/player.d.ts +32 -0
  175. package/dist/types/tts/runtime.d.ts +6 -0
  176. package/dist/types/tts/streaming-player.d.ts +41 -0
  177. package/dist/types/tts/tts-client.d.ts +93 -0
  178. package/dist/types/tts/tts-protocol.d.ts +95 -0
  179. package/dist/types/tts/tts-worker.d.ts +2 -0
  180. package/dist/types/tts/vocalizer.d.ts +41 -0
  181. package/dist/types/tts/wav.d.ts +8 -0
  182. package/dist/types/utils/clipboard.d.ts +4 -3
  183. package/dist/types/utils/image-loading.d.ts +18 -1
  184. package/dist/types/utils/thinking-display.d.ts +17 -0
  185. package/dist/types/utils/tool-choice.d.ts +8 -0
  186. package/dist/types/utils/tools-manager.d.ts +2 -1
  187. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  188. package/dist/types/web/scrapers/github.d.ts +1 -1
  189. package/dist/types/web/search/index.d.ts +1 -1
  190. package/package.json +17 -16
  191. package/src/async/job-manager.ts +49 -0
  192. package/src/autolearn/controller.ts +139 -0
  193. package/src/autolearn/managed-skills.ts +257 -0
  194. package/src/autoresearch/state.ts +1 -1
  195. package/src/autoresearch/storage.ts +2 -1
  196. package/src/autoresearch/tools/init-experiment.ts +1 -1
  197. package/src/autoresearch/tools/log-experiment.ts +1 -1
  198. package/src/autoresearch/tools/run-experiment.ts +1 -1
  199. package/src/autoresearch/tools/update-notes.ts +1 -1
  200. package/src/autoresearch/types.ts +1 -1
  201. package/src/cli/args.ts +56 -10
  202. package/src/cli/auth-gateway-cli.ts +1 -1
  203. package/src/cli/bench-cli.ts +1 -1
  204. package/src/cli/dry-balance-cli.ts +1 -1
  205. package/src/cli/models-cli.ts +427 -0
  206. package/src/cli/session-picker.ts +2 -1
  207. package/src/cli/setup-cli.ts +148 -47
  208. package/src/cli/setup-model-picker.ts +43 -0
  209. package/src/cli-commands.ts +3 -0
  210. package/src/cli.ts +45 -13
  211. package/src/collab/host.ts +10 -13
  212. package/src/collab/protocol.ts +1 -1
  213. package/src/commands/launch.ts +0 -3
  214. package/src/commands/models.ts +61 -0
  215. package/src/commands/say.ts +102 -0
  216. package/src/commands/setup.ts +1 -1
  217. package/src/commands/token.ts +89 -0
  218. package/src/commit/agentic/tools/analyze-file.ts +4 -1
  219. package/src/commit/agentic/tools/git-file-diff.ts +1 -1
  220. package/src/commit/agentic/tools/git-hunk.ts +1 -1
  221. package/src/commit/agentic/tools/git-overview.ts +1 -1
  222. package/src/commit/agentic/tools/propose-changelog.ts +1 -1
  223. package/src/commit/agentic/tools/propose-commit.ts +1 -1
  224. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  225. package/src/commit/agentic/tools/schemas.ts +1 -1
  226. package/src/commit/agentic/tools/split-commit.ts +1 -1
  227. package/src/commit/analysis/summary.ts +1 -1
  228. package/src/commit/changelog/generate.ts +1 -1
  229. package/src/commit/shared-llm.ts +1 -1
  230. package/src/config/keybindings.ts +2 -2
  231. package/src/config/model-discovery.ts +11 -5
  232. package/src/config/model-registry.ts +79 -21
  233. package/src/config/model-resolver.ts +2 -2
  234. package/src/config/models-config-schema.ts +5 -2
  235. package/src/config/models-config.ts +2 -1
  236. package/src/config/settings-schema.ts +266 -32
  237. package/src/config/settings.ts +10 -0
  238. package/src/discovery/builtin.ts +23 -1
  239. package/src/discovery/claude-plugins.ts +44 -5
  240. package/src/discovery/helpers.ts +41 -1
  241. package/src/edit/hashline/params.ts +1 -1
  242. package/src/edit/modes/apply-patch.ts +1 -1
  243. package/src/edit/modes/patch.ts +1 -1
  244. package/src/edit/modes/replace.ts +1 -1
  245. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  246. package/src/eval/agent-bridge.ts +1 -1
  247. package/src/eval/completion-bridge.ts +1 -1
  248. package/src/eval/js/shared/prelude.txt +69 -17
  249. package/src/export/html/index.ts +3 -6
  250. package/src/export/html/template.js +24 -2
  251. package/src/export/html/tool-views.generated.js +2 -2
  252. package/src/extensibility/custom-commands/loader.ts +1 -1
  253. package/src/extensibility/custom-commands/types.ts +2 -2
  254. package/src/extensibility/custom-tools/loader.ts +1 -1
  255. package/src/extensibility/custom-tools/types.ts +2 -2
  256. package/src/extensibility/extensions/loader.ts +2 -2
  257. package/src/extensibility/extensions/model-api.ts +41 -0
  258. package/src/extensibility/extensions/runner.ts +4 -0
  259. package/src/extensibility/extensions/types.ts +54 -3
  260. package/src/extensibility/extensions/wrapper.ts +41 -5
  261. package/src/extensibility/hooks/index.ts +2 -1
  262. package/src/extensibility/hooks/loader.ts +1 -1
  263. package/src/extensibility/hooks/types.ts +2 -2
  264. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  265. package/src/extensibility/plugins/loader.ts +30 -19
  266. package/src/extensibility/plugins/manager.ts +221 -90
  267. package/src/extensibility/shared-events.ts +1 -1
  268. package/src/extensibility/skills.ts +101 -5
  269. package/src/goals/guided-setup.ts +133 -0
  270. package/src/goals/state.ts +1 -1
  271. package/src/goals/tools/goal-tool.ts +1 -1
  272. package/src/hindsight/transcript.ts +1 -1
  273. package/src/index.ts +5 -0
  274. package/src/internal-urls/docs-index.generated.ts +13 -10
  275. package/src/internal-urls/history-protocol.ts +1 -1
  276. package/src/internal-urls/local-protocol.ts +29 -7
  277. package/src/lsp/types.ts +1 -1
  278. package/src/main.ts +27 -32
  279. package/src/mcp/config-writer.ts +7 -3
  280. package/src/mcp/manager.ts +11 -0
  281. package/src/mcp/startup-events.ts +21 -0
  282. package/src/mcp/transports/stdio.ts +2 -1
  283. package/src/memories/index.ts +149 -12
  284. package/src/memories/storage.ts +2 -1
  285. package/src/memory-backend/local-backend.ts +11 -5
  286. package/src/mnemopi/backend.ts +1 -0
  287. package/src/mnemopi/config.ts +112 -12
  288. package/src/modes/acp/acp-agent.ts +8 -53
  289. package/src/modes/acp/acp-event-mapper.ts +5 -1
  290. package/src/modes/components/agent-hub.ts +51 -5
  291. package/src/modes/components/assistant-message.ts +12 -44
  292. package/src/modes/components/compaction-summary-message.ts +125 -26
  293. package/src/modes/components/custom-editor.test.ts +96 -0
  294. package/src/modes/components/custom-editor.ts +164 -8
  295. package/src/modes/components/index.ts +1 -0
  296. package/src/modes/components/logout-account-selector.ts +130 -0
  297. package/src/modes/components/mcp-add-wizard.ts +1 -1
  298. package/src/modes/components/model-selector.ts +2 -2
  299. package/src/modes/components/session-selector.ts +1 -1
  300. package/src/modes/components/settings-defs.ts +7 -0
  301. package/src/modes/components/status-line/component.ts +54 -157
  302. package/src/modes/components/status-line/segments.ts +1 -1
  303. package/src/modes/components/status-line/types.ts +2 -1
  304. package/src/modes/components/tool-execution.ts +82 -43
  305. package/src/modes/components/transcript-container.ts +70 -1
  306. package/src/modes/components/tree-selector.ts +1 -1
  307. package/src/modes/components/usage-row.ts +18 -0
  308. package/src/modes/components/user-message.ts +4 -2
  309. package/src/modes/controllers/command-controller.ts +14 -16
  310. package/src/modes/controllers/event-controller.ts +101 -73
  311. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  312. package/src/modes/controllers/input-controller.ts +311 -57
  313. package/src/modes/controllers/mcp-command-controller.ts +44 -3
  314. package/src/modes/controllers/selector-controller.ts +68 -12
  315. package/src/modes/controllers/streaming-reveal.ts +4 -3
  316. package/src/modes/gradient-highlight.ts +21 -9
  317. package/src/modes/image-references.ts +20 -0
  318. package/src/modes/interactive-mode.ts +288 -48
  319. package/src/modes/magic-keywords.ts +27 -5
  320. package/src/modes/rpc/rpc-mode.ts +146 -14
  321. package/src/modes/rpc/rpc-subagents.ts +2 -2
  322. package/src/modes/rpc/rpc-types.ts +8 -2
  323. package/src/modes/runtime-init.ts +28 -3
  324. package/src/modes/theme/theme.ts +99 -51
  325. package/src/modes/types.ts +6 -7
  326. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  327. package/src/modes/utils/ui-helpers.ts +36 -7
  328. package/src/priority.json +5 -1
  329. package/src/prompts/agents/task.md +1 -0
  330. package/src/prompts/goals/guided-goal-interview.md +8 -0
  331. package/src/prompts/goals/guided-goal-system.md +12 -0
  332. package/src/prompts/memories/read-path.md +6 -0
  333. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  334. package/src/prompts/system/autolearn-guidance.md +7 -0
  335. package/src/prompts/system/autolearn-nudge.md +3 -0
  336. package/src/prompts/system/eager-task.md +7 -0
  337. package/src/prompts/system/eager-todo.md +11 -6
  338. package/src/prompts/system/empty-stop-retry.md +4 -6
  339. package/src/prompts/system/subagent-system-prompt.md +4 -0
  340. package/src/prompts/system/system-prompt.md +10 -5
  341. package/src/prompts/system/title-marker-instruction.md +1 -0
  342. package/src/prompts/system/title-system-marker.md +16 -0
  343. package/src/prompts/tools/job.md +1 -0
  344. package/src/prompts/tools/learn.md +7 -0
  345. package/src/prompts/tools/manage-skill.md +9 -0
  346. package/src/prompts/tools/task.md +3 -0
  347. package/src/registry/agent-registry.ts +30 -0
  348. package/src/sdk.ts +103 -43
  349. package/src/secrets/obfuscator.ts +1 -1
  350. package/src/session/agent-session.ts +331 -318
  351. package/src/session/agent-storage.ts +18 -9
  352. package/src/session/history-storage.ts +3 -2
  353. package/src/session/indexed-session-storage.ts +7 -10
  354. package/src/session/messages.ts +9 -11
  355. package/src/session/session-context.ts +352 -0
  356. package/src/session/session-dump-format.ts +4 -2
  357. package/src/session/session-entries.ts +194 -0
  358. package/src/session/session-listing.ts +588 -0
  359. package/src/session/session-loader.ts +106 -0
  360. package/src/session/session-manager.ts +968 -3064
  361. package/src/session/session-migrations.ts +78 -0
  362. package/src/session/session-paths.ts +193 -0
  363. package/src/session/session-persistence.ts +131 -0
  364. package/src/session/session-storage.ts +91 -30
  365. package/src/session/snapcompact-inline.ts +21 -1
  366. package/src/session/snapcompact-savings-journal.ts +113 -0
  367. package/src/session/tool-choice-queue.ts +23 -11
  368. package/src/slash-commands/builtin-registry.ts +40 -4
  369. package/src/slash-commands/helpers/logout.ts +88 -0
  370. package/src/stt/asr-client.ts +520 -0
  371. package/src/stt/asr-protocol.ts +65 -0
  372. package/src/stt/asr-worker.ts +790 -0
  373. package/src/stt/downloader.ts +107 -47
  374. package/src/stt/endpointer.ts +259 -0
  375. package/src/stt/index.ts +5 -1
  376. package/src/stt/models.ts +150 -0
  377. package/src/stt/recorder.ts +247 -60
  378. package/src/stt/stt-controller.ts +201 -22
  379. package/src/stt/transcriber.ts +37 -68
  380. package/src/stt/wav.ts +173 -0
  381. package/src/system-prompt.ts +8 -0
  382. package/src/task/agents.ts +1 -2
  383. package/src/task/executor.ts +49 -15
  384. package/src/task/index.ts +60 -6
  385. package/src/task/render.ts +83 -8
  386. package/src/task/types.ts +54 -1
  387. package/src/tools/ask.ts +9 -1
  388. package/src/tools/ast-edit.ts +1 -1
  389. package/src/tools/ast-grep.ts +1 -1
  390. package/src/tools/bash.ts +5 -4
  391. package/src/tools/browser/cmux/cmux-tab.ts +1264 -0
  392. package/src/tools/browser/cmux/rpc.ts +156 -0
  393. package/src/tools/browser/cmux/socket-client.ts +309 -0
  394. package/src/tools/browser/registry.ts +37 -3
  395. package/src/tools/browser/render.ts +6 -1
  396. package/src/tools/browser/tab-protocol.ts +2 -0
  397. package/src/tools/browser/tab-supervisor.ts +189 -18
  398. package/src/tools/browser/tab-worker.ts +1 -1
  399. package/src/tools/browser.ts +16 -1
  400. package/src/tools/checkpoint.ts +1 -1
  401. package/src/tools/debug.ts +1 -1
  402. package/src/tools/eval-render.ts +4 -3
  403. package/src/tools/eval.ts +11 -6
  404. package/src/tools/fetch.ts +13 -2
  405. package/src/tools/find.ts +1 -1
  406. package/src/tools/gh.ts +1 -1
  407. package/src/tools/github-cache.ts +2 -1
  408. package/src/tools/image-gen.ts +1 -1
  409. package/src/tools/index.ts +43 -5
  410. package/src/tools/inspect-image.ts +3 -1
  411. package/src/tools/irc.ts +11 -3
  412. package/src/tools/job.ts +15 -3
  413. package/src/tools/learn.ts +144 -0
  414. package/src/tools/manage-skill.ts +104 -0
  415. package/src/tools/memory-edit.ts +1 -1
  416. package/src/tools/memory-recall.ts +1 -1
  417. package/src/tools/memory-reflect.ts +1 -1
  418. package/src/tools/memory-retain.ts +1 -1
  419. package/src/tools/plan-mode-guard.ts +53 -19
  420. package/src/tools/read.ts +8 -2
  421. package/src/tools/render-mermaid.ts +1 -1
  422. package/src/tools/renderers.ts +7 -11
  423. package/src/tools/report-tool-issue.ts +3 -2
  424. package/src/tools/resolve.ts +1 -1
  425. package/src/tools/review.ts +1 -1
  426. package/src/tools/search-tool-bm25.ts +1 -1
  427. package/src/tools/search.ts +1 -1
  428. package/src/tools/ssh.ts +5 -4
  429. package/src/tools/todo.ts +2 -2
  430. package/src/tools/tts.ts +204 -93
  431. package/src/tools/write.ts +19 -3
  432. package/src/tts/downloader.ts +64 -0
  433. package/src/tts/index.ts +8 -0
  434. package/src/tts/models.ts +137 -0
  435. package/src/tts/player.ts +137 -0
  436. package/src/tts/runtime.ts +21 -0
  437. package/src/tts/streaming-player.ts +266 -0
  438. package/src/tts/tts-client.ts +647 -0
  439. package/src/tts/tts-protocol.ts +60 -0
  440. package/src/tts/tts-worker.ts +497 -0
  441. package/src/tts/vocalizer.ts +162 -0
  442. package/src/tts/wav.ts +58 -0
  443. package/src/utils/clipboard.ts +35 -18
  444. package/src/utils/image-loading.ts +35 -4
  445. package/src/utils/thinking-display.ts +37 -0
  446. package/src/utils/title-generator.ts +48 -5
  447. package/src/utils/tool-choice.ts +16 -0
  448. package/src/utils/tools-manager.test.ts +25 -0
  449. package/src/utils/tools-manager.ts +19 -1
  450. package/src/web/scrapers/github.ts +96 -0
  451. package/src/web/search/index.ts +14 -1
  452. package/src/web/search/providers/searxng.ts +13 -1
  453. package/dist/types/cli/list-models.d.ts +0 -30
  454. package/dist/types/stt/setup.d.ts +0 -18
  455. package/src/cli/list-models.ts +0 -194
  456. package/src/stt/setup.ts +0 -52
  457. package/src/stt/transcribe.py +0 -70
@@ -0,0 +1,647 @@
1
+ import * as path from "node:path";
2
+ import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
3
+ import type { Subprocess } from "bun";
4
+ import { settings } from "../config/settings";
5
+ import { tinyWorkerEnvOverlay } from "../tiny/title-client";
6
+ import { isTtsLocalModelKey, type TtsLocalModelKey } from "./models";
7
+ import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
8
+
9
+ /** Decoded PCM returned by a local synthesis request. */
10
+ export interface TtsAudio {
11
+ pcm: Float32Array;
12
+ sampleRate: number;
13
+ }
14
+
15
+ /**
16
+ * Abstraction over the TTS subprocess. The runtime implementation is a Bun child
17
+ * process so `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
18
+ * address space — that destructor segfaults Bun during shutdown (issue #1606).
19
+ */
20
+ interface WorkerHandle {
21
+ send(message: TtsWorkerInbound): void;
22
+ onMessage(handler: (message: TtsWorkerOutbound) => void): () => void;
23
+ onError(handler: (error: Error) => void): () => void;
24
+ /** Re-reference the subprocess so a pending request keeps the parent event loop alive. */
25
+ ref(): void;
26
+ /** Drop the reference once the worker is idle so it never blocks process exit. */
27
+ unref(): void;
28
+ terminate(): Promise<void>;
29
+ }
30
+
31
+ type PendingRequest =
32
+ | { kind: "synthesize"; modelKey: TtsLocalModelKey; resolve: (audio: TtsAudio | null) => void }
33
+ | { kind: "download"; modelKey: TtsLocalModelKey; resolve: (ok: boolean) => void }
34
+ | { kind: "stream"; modelKey: TtsLocalModelKey; channel: AudioChunkChannel };
35
+
36
+ export interface TtsSynthesizeOptions {
37
+ voice?: string;
38
+ signal?: AbortSignal;
39
+ }
40
+
41
+ export interface TtsDownloadOptions {
42
+ signal?: AbortSignal;
43
+ onProgress?: (event: TtsProgressEvent) => void;
44
+ }
45
+
46
+ export interface TtsStreamOptions {
47
+ voice?: string;
48
+ signal?: AbortSignal;
49
+ }
50
+
51
+ /** One synthesized sentence of a streaming session, in emission order. */
52
+ export interface TtsAudioChunk {
53
+ index: number;
54
+ text: string;
55
+ pcm: Float32Array;
56
+ sampleRate: number;
57
+ }
58
+
59
+ /**
60
+ * A live streaming-synthesis session. Feed text incrementally with {@link push}
61
+ * and close the input with {@link end}; `chunks` yields each synthesized
62
+ * sentence's audio as soon as it is ready, then completes once the worker
63
+ * finishes draining the closed input.
64
+ */
65
+ export interface TtsStreamHandle {
66
+ push(text: string): void;
67
+ end(): void;
68
+ chunks: AsyncIterableIterator<TtsAudioChunk>;
69
+ }
70
+
71
+ /**
72
+ * Single-producer/single-consumer async queue bridging the worker's IPC
73
+ * `audio-chunk` messages to an async iterator. Chunks pushed while no consumer
74
+ * is awaiting are buffered in order; {@link close} ends the iterator and
75
+ * {@link fail} surfaces an error to the awaiting (or next) consumer.
76
+ */
77
+ class AudioChunkChannel {
78
+ #queue: TtsAudioChunk[] = [];
79
+ #waiters: Array<{
80
+ resolve: (result: IteratorResult<TtsAudioChunk>) => void;
81
+ reject: (error: Error) => void;
82
+ }> = [];
83
+ #error: Error | null = null;
84
+ #settled = false;
85
+ #onSettle: (() => void) | undefined;
86
+
87
+ constructor(onSettle?: () => void) {
88
+ this.#onSettle = onSettle;
89
+ }
90
+
91
+ push(chunk: TtsAudioChunk): void {
92
+ if (this.#settled) return;
93
+ const waiter = this.#waiters.shift();
94
+ if (waiter) waiter.resolve({ value: chunk, done: false });
95
+ else this.#queue.push(chunk);
96
+ }
97
+
98
+ close(): void {
99
+ this.#settle(null);
100
+ }
101
+
102
+ fail(error: Error): void {
103
+ this.#settle(error);
104
+ }
105
+
106
+ #settle(error: Error | null): void {
107
+ if (this.#settled) return;
108
+ this.#settled = true;
109
+ this.#error = error;
110
+ for (const waiter of this.#waiters) {
111
+ if (error) waiter.reject(error);
112
+ else waiter.resolve({ value: undefined, done: true });
113
+ }
114
+ this.#waiters = [];
115
+ this.#onSettle?.();
116
+ }
117
+
118
+ async *iterator(): AsyncIterableIterator<TtsAudioChunk> {
119
+ while (true) {
120
+ const buffered = this.#queue.shift();
121
+ if (buffered) {
122
+ yield buffered;
123
+ continue;
124
+ }
125
+ if (this.#error) throw this.#error;
126
+ if (this.#settled) return;
127
+ const { promise, resolve, reject } = Promise.withResolvers<IteratorResult<TtsAudioChunk>>();
128
+ this.#waiters.push({ resolve, reject });
129
+ const result = await promise;
130
+ if (result.done) return;
131
+ yield result.value;
132
+ }
133
+ }
134
+ }
135
+
136
+ // Cold-starting the worker from a compiled binary (decompress + module graph load)
137
+ // is slow on contended CI runners; the probe only proves the worker spawns and
138
+ // ponges, so a generous bound removes flakes without weakening the check.
139
+ const SMOKE_TEST_TIMEOUT_MS = 30_000;
140
+
141
+ /**
142
+ * Hidden subcommand on the main CLI that boots the TTS worker in the spawned
143
+ * subprocess. Kept in sync with the dispatch in `cli.ts` (Main-owned).
144
+ */
145
+ export const TTS_WORKER_ARG = "__omp_tts_worker";
146
+
147
+ function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
148
+ try {
149
+ const value = settings.get(path);
150
+ return typeof value === "string" ? value : undefined;
151
+ } catch {
152
+ // Settings may be uninitialized (e.g. `omp --smoke-test`); fall back to env/default.
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Env handed to the TTS subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env
159
+ * vars win; otherwise the persisted `providers.tinyModelDevice` /
160
+ * `providers.tinyModelDtype` settings are mapped onto those vars so the
161
+ * subprocess's env-based resolution governs speech the same way it governs the
162
+ * tiny LLM worker.
163
+ */
164
+ function ttsWorkerEnv(): Record<string, string> {
165
+ const overlay = tinyWorkerEnvOverlay(
166
+ $env,
167
+ readTinyModelSetting("providers.tinyModelDevice"),
168
+ readTinyModelSetting("providers.tinyModelDtype"),
169
+ );
170
+ const base = $env as Record<string, string | undefined>;
171
+ const merged: Record<string, string> = {};
172
+ for (const key in base) {
173
+ const value = base[key];
174
+ if (typeof value === "string") merged[key] = value;
175
+ }
176
+ for (const key in overlay) merged[key] = overlay[key];
177
+ return merged;
178
+ }
179
+
180
+ interface TtsWorkerSpawnCommand {
181
+ cmd: string[];
182
+ cwd?: string;
183
+ }
184
+
185
+ /**
186
+ * Resolve the command used to relaunch the agent CLI into TTS-worker mode. In a
187
+ * compiled binary the entry point is the binary itself; otherwise re-enter the
188
+ * declared worker-host entry (cwd-relative for reliable Bun IPC), falling back
189
+ * to this package's own `src/cli.ts` when no host entry is declared (bun test).
190
+ */
191
+ function ttsWorkerSpawnCmd(): TtsWorkerSpawnCommand {
192
+ if (isCompiledBinary()) return { cmd: [process.execPath, TTS_WORKER_ARG] };
193
+ const hostEntry = workerHostEntry();
194
+ if (hostEntry) {
195
+ return { cmd: [process.execPath, path.basename(hostEntry), TTS_WORKER_ARG], cwd: path.dirname(hostEntry) };
196
+ }
197
+ const packageRoot = path.resolve(import.meta.dir, "..", "..");
198
+ return { cmd: [process.execPath, "src/cli.ts", TTS_WORKER_ARG], cwd: packageRoot };
199
+ }
200
+
201
+ interface SpawnedSubprocess {
202
+ proc: Subprocess<"ignore", "ignore", "ignore">;
203
+ inbound: Set<(message: TtsWorkerOutbound) => void>;
204
+ errors: Set<(error: Error) => void>;
205
+ /** Flipped to `true` right before the deliberate SIGKILL so `onExit` can tell it apart from a crash. */
206
+ intentionalExit: { value: boolean };
207
+ }
208
+
209
+ /**
210
+ * Spawn the TTS worker as a subprocess. Exported for tests and the smoke probe;
211
+ * production callers go through {@link spawnTtsWorker}.
212
+ */
213
+ export function createTtsSubprocess(): SpawnedSubprocess {
214
+ const inbound = new Set<(message: TtsWorkerOutbound) => void>();
215
+ const errors = new Set<(error: Error) => void>();
216
+ const intentionalExit = { value: false };
217
+ const spawnCommand = ttsWorkerSpawnCmd();
218
+ const proc = Bun.spawn({
219
+ cmd: spawnCommand.cmd,
220
+ cwd: spawnCommand.cwd,
221
+ env: ttsWorkerEnv(),
222
+ stdin: "ignore",
223
+ stdout: "ignore",
224
+ stderr: "ignore",
225
+ serialization: "advanced",
226
+ windowsHide: true,
227
+ ipc(message) {
228
+ for (const handler of inbound) handler(message as TtsWorkerOutbound);
229
+ },
230
+ onExit(_proc, exitCode, signalCode) {
231
+ if (exitCode === 0) return;
232
+ if (exitCode === null && intentionalExit.value) return;
233
+ const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
234
+ const err = new Error(`tts subprocess exited with ${reason}`);
235
+ for (const handler of errors) handler(err);
236
+ },
237
+ });
238
+ // Don't keep the parent event loop alive on an idle worker; the dispose path
239
+ // calls `terminate()` explicitly. Bun's test runner starves IPC for unref'd
240
+ // subprocesses, so keep it referenced only under tests.
241
+ if (!isBunTestRuntime()) proc.unref();
242
+ return { proc, inbound, errors, intentionalExit };
243
+ }
244
+
245
+ function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
246
+ return {
247
+ send(message) {
248
+ try {
249
+ proc.send(message);
250
+ } catch (error) {
251
+ logger.debug("tts: send to subprocess failed", {
252
+ error: error instanceof Error ? error.message : String(error),
253
+ });
254
+ }
255
+ },
256
+ onMessage(handler) {
257
+ inbound.add(handler);
258
+ return () => inbound.delete(handler);
259
+ },
260
+ onError(handler) {
261
+ errors.add(handler);
262
+ return () => errors.delete(handler);
263
+ },
264
+ ref() {
265
+ try {
266
+ proc.ref();
267
+ } catch {
268
+ // Already gone.
269
+ }
270
+ },
271
+ unref() {
272
+ try {
273
+ proc.unref();
274
+ } catch {
275
+ // Already gone.
276
+ }
277
+ },
278
+ async terminate() {
279
+ // SIGKILL: the point of subprocess isolation is that the parent never
280
+ // runs `onnxruntime-node`'s NAPI finalizer (it crashes Bun on Windows).
281
+ // Hard-kill instead; the OS reclaims the model memory.
282
+ intentionalExit.value = true;
283
+ try {
284
+ proc.kill("SIGKILL");
285
+ } catch {
286
+ // Already gone.
287
+ }
288
+ },
289
+ };
290
+ }
291
+
292
+ function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
293
+ const listeners = new Set<(message: TtsWorkerOutbound) => void>();
294
+ const errorMessage = error instanceof Error ? error.message : String(error);
295
+ const emit = (message: TtsWorkerOutbound): void => {
296
+ for (const listener of listeners) listener(message);
297
+ };
298
+ return {
299
+ send(message) {
300
+ queueMicrotask(() => {
301
+ if (message.type === "ping") {
302
+ emit({ type: "pong", id: message.id });
303
+ return;
304
+ }
305
+ emit({ type: "error", id: message.id, error: errorMessage });
306
+ });
307
+ },
308
+ onMessage(handler) {
309
+ listeners.add(handler);
310
+ return () => listeners.delete(handler);
311
+ },
312
+ onError() {
313
+ return () => {};
314
+ },
315
+ ref() {},
316
+ unref() {},
317
+ async terminate() {
318
+ listeners.clear();
319
+ },
320
+ };
321
+ }
322
+
323
+ function spawnTtsWorker(): WorkerHandle {
324
+ try {
325
+ return wrapSubprocess(createTtsSubprocess());
326
+ } catch (error) {
327
+ logger.warn("TTS worker spawn failed; local TTS disabled", {
328
+ error: error instanceof Error ? error.message : String(error),
329
+ });
330
+ return spawnInlineUnavailableWorker(error);
331
+ }
332
+ }
333
+
334
+ function logWorkerMessage(message: Extract<TtsWorkerOutbound, { type: "log" }>): void {
335
+ if (message.level === "debug") logger.debug(message.msg, message.meta);
336
+ else if (message.level === "warn") logger.warn(message.msg, message.meta);
337
+ else logger.error(message.msg, message.meta);
338
+ }
339
+
340
+ export class TtsClient {
341
+ #worker: WorkerHandle | null = null;
342
+ #unsubscribeMessage: (() => void) | null = null;
343
+ #unsubscribeError: (() => void) | null = null;
344
+ #pending = new Map<string, PendingRequest>();
345
+ #progressListeners = new Set<(event: TtsProgressEvent) => void>();
346
+ #nextRequestId = 0;
347
+ #refed = false;
348
+ #spawnWorker: () => WorkerHandle;
349
+
350
+ constructor(spawnWorker: () => WorkerHandle = spawnTtsWorker) {
351
+ this.#spawnWorker = spawnWorker;
352
+ }
353
+
354
+ onProgress(listener: (event: TtsProgressEvent) => void): () => void {
355
+ this.#progressListeners.add(listener);
356
+ return () => this.#progressListeners.delete(listener);
357
+ }
358
+
359
+ async synthesize(modelKey: string, text: string, options: TtsSynthesizeOptions = {}): Promise<TtsAudio | null> {
360
+ if (!isTtsLocalModelKey(modelKey)) return null;
361
+ if (options.signal?.aborted) return null;
362
+
363
+ try {
364
+ const worker = this.#ensureWorker();
365
+ const id = String(++this.#nextRequestId);
366
+ const { promise, resolve } = Promise.withResolvers<TtsAudio | null>();
367
+ this.#addPending(id, { kind: "synthesize", modelKey, resolve });
368
+ const abort = (): void => {
369
+ const pending = this.#pending.get(id);
370
+ if (pending?.kind !== "synthesize") return;
371
+ this.#deletePending(id);
372
+ pending.resolve(null);
373
+ };
374
+ options.signal?.addEventListener("abort", abort, { once: true });
375
+ try {
376
+ const request: TtsWorkerInbound = options.voice
377
+ ? { type: "synthesize", id, modelKey, text, voice: options.voice }
378
+ : { type: "synthesize", id, modelKey, text };
379
+ worker.send(request);
380
+ return await promise;
381
+ } finally {
382
+ options.signal?.removeEventListener("abort", abort);
383
+ this.#deletePending(id);
384
+ }
385
+ } catch (error) {
386
+ logger.debug("tts: local synthesis failed", {
387
+ modelKey,
388
+ error: error instanceof Error ? error.message : String(error),
389
+ });
390
+ return null;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Open a streaming-synthesis session. Text is fed incrementally through the
396
+ * returned handle's `push`/`end`; audio is emitted one synthesized sentence at
397
+ * a time via `chunks`, so playback can begin before the full text is known.
398
+ * Returns an inert handle (immediately-ended `chunks`) for unknown models or
399
+ * an already-aborted signal, and fails the iterator if the worker cannot spawn.
400
+ */
401
+ synthesizeStream(modelKey: string, options: TtsStreamOptions = {}): TtsStreamHandle {
402
+ if (!isTtsLocalModelKey(modelKey) || options.signal?.aborted) {
403
+ const channel = new AudioChunkChannel();
404
+ channel.close();
405
+ return { push: () => {}, end: () => {}, chunks: channel.iterator() };
406
+ }
407
+
408
+ let worker: WorkerHandle;
409
+ try {
410
+ worker = this.#ensureWorker();
411
+ } catch (error) {
412
+ logger.debug("tts: stream synthesis failed to start", {
413
+ modelKey,
414
+ error: error instanceof Error ? error.message : String(error),
415
+ });
416
+ const channel = new AudioChunkChannel();
417
+ channel.fail(error instanceof Error ? error : new Error(String(error)));
418
+ return { push: () => {}, end: () => {}, chunks: channel.iterator() };
419
+ }
420
+
421
+ const id = String(++this.#nextRequestId);
422
+ const signal = options.signal;
423
+ let closed = false;
424
+ let ended = false;
425
+ const abort = (): void => {
426
+ if (closed) return;
427
+ closed = true;
428
+ ended = true;
429
+ if (!this.#pending.has(id)) return;
430
+ this.#deletePending(id);
431
+ worker.send({ type: "stream-cancel", id });
432
+ channel.close();
433
+ };
434
+ const channel = new AudioChunkChannel(() => signal?.removeEventListener("abort", abort));
435
+ this.#addPending(id, { kind: "stream", modelKey, channel });
436
+ signal?.addEventListener("abort", abort, { once: true });
437
+
438
+ const start: TtsWorkerInbound = options.voice
439
+ ? { type: "stream-start", id, modelKey, voice: options.voice }
440
+ : { type: "stream-start", id, modelKey };
441
+ worker.send(start);
442
+
443
+ return {
444
+ push: (text: string) => {
445
+ if (!closed && !ended) worker.send({ type: "stream-push", id, text });
446
+ },
447
+ end: () => {
448
+ if (closed || ended) return;
449
+ ended = true;
450
+ worker.send({ type: "stream-end", id });
451
+ },
452
+ chunks: channel.iterator(),
453
+ };
454
+ }
455
+
456
+ async downloadModel(modelKey: string, options: TtsDownloadOptions = {}): Promise<boolean> {
457
+ if (!isTtsLocalModelKey(modelKey)) return false;
458
+ if (options.signal?.aborted) return false;
459
+
460
+ const unsubscribe = options.onProgress ? this.onProgress(options.onProgress) : undefined;
461
+ try {
462
+ const worker = this.#ensureWorker();
463
+ const id = String(++this.#nextRequestId);
464
+ const { promise, resolve } = Promise.withResolvers<boolean>();
465
+ this.#addPending(id, { kind: "download", modelKey, resolve });
466
+ const abort = (): void => {
467
+ const pending = this.#pending.get(id);
468
+ if (pending?.kind !== "download") return;
469
+ this.#deletePending(id);
470
+ pending.resolve(false);
471
+ };
472
+ options.signal?.addEventListener("abort", abort, { once: true });
473
+ try {
474
+ worker.send({ type: "download", id, modelKey });
475
+ return await promise;
476
+ } finally {
477
+ options.signal?.removeEventListener("abort", abort);
478
+ this.#deletePending(id);
479
+ }
480
+ } catch (error) {
481
+ logger.debug("tts: local model download failed", {
482
+ modelKey,
483
+ error: error instanceof Error ? error.message : String(error),
484
+ });
485
+ return false;
486
+ } finally {
487
+ unsubscribe?.();
488
+ }
489
+ }
490
+
491
+ async terminate(): Promise<void> {
492
+ const worker = this.#worker;
493
+ this.#worker = null;
494
+ this.#unsubscribeMessage?.();
495
+ this.#unsubscribeMessage = null;
496
+ this.#unsubscribeError?.();
497
+ this.#unsubscribeError = null;
498
+ for (const pending of this.#pending.values()) {
499
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
500
+ if (pending.kind === "synthesize") pending.resolve(null);
501
+ else if (pending.kind === "download") pending.resolve(false);
502
+ else pending.channel.close();
503
+ }
504
+ this.#pending.clear();
505
+ this.#refed = false;
506
+ try {
507
+ await worker?.terminate();
508
+ } catch {
509
+ // Already gone.
510
+ }
511
+ }
512
+
513
+ #ensureWorker(): WorkerHandle {
514
+ if (this.#worker) return this.#worker;
515
+ const worker = this.#spawnWorker();
516
+ this.#worker = worker;
517
+ this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
518
+ this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
519
+ return worker;
520
+ }
521
+
522
+ /** Register a pending request and keep the worker referenced while work is in flight. */
523
+ #addPending(id: string, request: PendingRequest): void {
524
+ this.#pending.set(id, request);
525
+ this.#syncWorkerRef();
526
+ }
527
+
528
+ /** Drop a pending request and unref the worker once nothing is in flight. */
529
+ #deletePending(id: string): void {
530
+ if (this.#pending.delete(id)) this.#syncWorkerRef();
531
+ }
532
+
533
+ /**
534
+ * The TTS subprocess is spawned `unref`'d so an idle worker never blocks
535
+ * process exit. A short-lived CLI command (`omp say`) awaiting a request would
536
+ * otherwise let the event loop drain and exit before the audio arrives, so we
537
+ * `ref` the worker exactly while at least one request is pending.
538
+ */
539
+ #syncWorkerRef(): void {
540
+ const worker = this.#worker;
541
+ if (!worker) return;
542
+ const shouldRef = this.#pending.size > 0;
543
+ if (shouldRef === this.#refed) return;
544
+ this.#refed = shouldRef;
545
+ if (shouldRef) worker.ref();
546
+ else worker.unref();
547
+ }
548
+
549
+ #handleMessage(message: TtsWorkerOutbound): void {
550
+ if (message.type === "log") {
551
+ logWorkerMessage(message);
552
+ return;
553
+ }
554
+ if (message.type === "progress") {
555
+ this.#emitProgress(message.event);
556
+ return;
557
+ }
558
+ if (message.type === "pong") return;
559
+
560
+ const pending = this.#pending.get(message.id);
561
+ if (!pending) return;
562
+
563
+ // Streaming chunks are non-terminal: keep the session registered until
564
+ // `stream-done` (or an error) so later chunks still route to its channel.
565
+ if (message.type === "audio-chunk") {
566
+ if (pending.kind === "stream") {
567
+ pending.channel.push({
568
+ index: message.index,
569
+ text: message.text,
570
+ pcm: message.pcm,
571
+ sampleRate: message.sampleRate,
572
+ });
573
+ }
574
+ return;
575
+ }
576
+
577
+ this.#deletePending(message.id);
578
+ if (message.type === "stream-done") {
579
+ if (pending.kind === "stream") pending.channel.close();
580
+ return;
581
+ }
582
+ if (message.type === "audio") {
583
+ if (pending.kind === "synthesize") pending.resolve({ pcm: message.pcm, sampleRate: message.sampleRate });
584
+ return;
585
+ }
586
+ if (message.type === "downloaded") {
587
+ if (pending.kind === "download") pending.resolve(true);
588
+ return;
589
+ }
590
+ logger.debug("tts: worker returned error", { error: message.error });
591
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
592
+ if (pending.kind === "synthesize") pending.resolve(null);
593
+ else if (pending.kind === "download") pending.resolve(false);
594
+ else pending.channel.fail(new Error(message.error));
595
+ void this.terminate();
596
+ }
597
+
598
+ #emitProgress(event: TtsProgressEvent): void {
599
+ for (const listener of this.#progressListeners) listener(event);
600
+ }
601
+
602
+ #handleWorkerError(error: Error): void {
603
+ logger.warn("tts: worker error", { error: error.message });
604
+ for (const pending of this.#pending.values()) {
605
+ this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
606
+ if (pending.kind === "synthesize") pending.resolve(null);
607
+ else if (pending.kind === "download") pending.resolve(false);
608
+ else pending.channel.fail(error);
609
+ }
610
+ this.#pending.clear();
611
+ void this.terminate();
612
+ }
613
+ }
614
+
615
+ export const ttsClient = new TtsClient();
616
+
617
+ export async function shutdownTtsClient(): Promise<void> {
618
+ await ttsClient.terminate();
619
+ }
620
+
621
+ export async function smokeTestTtsWorker({
622
+ timeoutMs = SMOKE_TEST_TIMEOUT_MS,
623
+ }: {
624
+ timeoutMs?: number;
625
+ } = {}): Promise<void> {
626
+ const handle = wrapSubprocess(createTtsSubprocess());
627
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
628
+ const timer = setTimeout(() => reject(new Error(`tts worker did not pong within ${timeoutMs}ms`)), timeoutMs);
629
+ const unsubscribeMessage = handle.onMessage(message => {
630
+ if (message.type === "pong") {
631
+ resolve();
632
+ return;
633
+ }
634
+ if (message.type === "log") return;
635
+ reject(new Error(`tts worker: expected pong, got ${JSON.stringify(message)}`));
636
+ });
637
+ const unsubscribeError = handle.onError(reject);
638
+ try {
639
+ handle.send({ type: "ping", id: "smoke" } satisfies TtsWorkerInbound);
640
+ await promise;
641
+ } finally {
642
+ clearTimeout(timer);
643
+ unsubscribeMessage();
644
+ unsubscribeError();
645
+ await handle.terminate();
646
+ }
647
+ }
@@ -0,0 +1,60 @@
1
+ import type { TtsLocalModelKey } from "./models";
2
+
3
+ export type TtsProgressStatus = "initiate" | "download" | "progress" | "progress_total" | "done" | "ready" | "error";
4
+
5
+ export interface TtsProgressFileState {
6
+ loaded: number;
7
+ total: number;
8
+ }
9
+
10
+ export interface TtsProgressEvent {
11
+ modelKey: TtsLocalModelKey;
12
+ status: TtsProgressStatus;
13
+ name?: string;
14
+ file?: string;
15
+ progress?: number;
16
+ loaded?: number;
17
+ total?: number;
18
+ files?: Record<string, TtsProgressFileState>;
19
+ task?: string;
20
+ model?: string;
21
+ }
22
+
23
+ export type TtsWorkerInbound =
24
+ | { type: "ping"; id: string }
25
+ | { type: "synthesize"; id: string; modelKey: TtsLocalModelKey; text: string; voice?: string }
26
+ | { type: "download"; id: string; modelKey: TtsLocalModelKey }
27
+ // Streaming synthesis: a session is opened with `stream-start`, fed incrementally
28
+ // with `stream-push`, and closed with `stream-end`. `stream-cancel` interrupts
29
+ // without a final drain. The worker emits an `audio-chunk` per synthesized
30
+ // sentence and a final `stream-done` only for non-cancelled sessions.
31
+ | { type: "stream-start"; id: string; modelKey: TtsLocalModelKey; voice?: string }
32
+ | { type: "stream-push"; id: string; text: string }
33
+ | { type: "stream-end"; id: string }
34
+ | { type: "stream-cancel"; id: string };
35
+
36
+ export type TtsWorkerOutbound =
37
+ | { type: "pong"; id: string }
38
+ | { type: "audio"; id: string; pcm: Float32Array; sampleRate: number }
39
+ | { type: "downloaded"; id: string }
40
+ | { type: "error"; id: string; error: string }
41
+ | { type: "progress"; id: string; event: TtsProgressEvent }
42
+ | { type: "log"; level: "debug" | "warn" | "error"; msg: string; meta?: Record<string, unknown> }
43
+ // One synthesized sentence of a streaming session, in emission order, followed
44
+ // by a single `stream-done` once the input stream is closed and drained.
45
+ | { type: "audio-chunk"; id: string; index: number; text: string; pcm: Float32Array; sampleRate: number }
46
+ | { type: "stream-done"; id: string };
47
+
48
+ /**
49
+ * Wire transport between the parent (`TtsClient`) and the local TTS subprocess.
50
+ * The parent owns the subprocess lifecycle (graceful work, hard SIGKILL on
51
+ * shutdown); the protocol carries no explicit close handshake — once the parent
52
+ * decides to terminate, it signals the OS to reap the child so
53
+ * `onnxruntime-node`'s NAPI finalizer never runs in the main agent address
54
+ * space (it segfaults Bun on shutdown — issue #1606). See `tts-client.ts` for
55
+ * the spawn/kill glue.
56
+ */
57
+ export interface TtsTransport {
58
+ send(message: TtsWorkerOutbound): void;
59
+ onMessage(handler: (message: TtsWorkerInbound) => void): () => void;
60
+ }