@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
@@ -12,7 +12,7 @@
12
12
  import type { AgentRef } from "../registry/agent-registry";
13
13
  import { AgentRegistry } from "../registry/agent-registry";
14
14
  import { formatSessionHistoryMarkdown } from "../session/session-history-format";
15
- import { loadSessionMessagesReadOnly } from "../session/session-manager";
15
+ import { loadSessionMessagesReadOnly } from "../session/session-loader";
16
16
  import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
17
 
18
18
  /** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
@@ -26,6 +26,20 @@ function toLocalValidationError(error: unknown): Error {
26
26
  const message = error instanceof Error ? error.message : String(error);
27
27
  return new Error(message.replace("skill://", "local://"));
28
28
  }
29
+ const WINDOWS_LOCAL_ROOT_MAX_CHARS = 180;
30
+
31
+ function safeSessionId(options: LocalProtocolOptions): string {
32
+ const raw = options.getSessionId?.() ?? "session";
33
+ const safe = raw.replace(/[^a-zA-Z0-9_.-]/g, "_");
34
+ return safe.length > 0 ? safe : "session";
35
+ }
36
+
37
+ function shortLocalRoot(options: LocalProtocolOptions): string {
38
+ // Derive the short root from the stable session id, never the artifact path,
39
+ // so `SessionManager.moveTo()` and the resume-after-move flow keep finding
40
+ // the same `local://` directory the session wrote pre-move.
41
+ return path.join(os.tmpdir(), "omp-local", safeSessionId(options));
42
+ }
29
43
 
30
44
  function getContentType(filePath: string): InternalResource["contentType"] {
31
45
  const ext = path.extname(filePath).toLowerCase();
@@ -108,20 +122,28 @@ function extractRelativePath(url: InternalUrl): string {
108
122
  return decoded;
109
123
  }
110
124
 
111
- export function resolveLocalRoot(options: LocalProtocolOptions): string {
125
+ /** Resolve the session-scoped local:// root, shortening long Windows artifact paths before writes hit MAX_PATH. */
126
+ export function resolveLocalRoot(options: LocalProtocolOptions, platform: NodeJS.Platform = process.platform): string {
112
127
  const artifactsDir = options.getArtifactsDir?.();
113
128
  if (artifactsDir) {
114
- return path.resolve(artifactsDir, "local");
129
+ const candidate = path.resolve(artifactsDir, "local");
130
+ if (platform === "win32" && candidate.length >= WINDOWS_LOCAL_ROOT_MAX_CHARS) {
131
+ return shortLocalRoot(options);
132
+ }
133
+ return candidate;
115
134
  }
116
135
 
117
- const sessionId = options.getSessionId?.() ?? "session";
118
- const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_");
119
- return path.join(os.tmpdir(), "omp-local", safeSessionId);
136
+ return path.join(os.tmpdir(), "omp-local", safeSessionId(options));
120
137
  }
121
138
 
122
- export function resolveLocalUrlToPath(input: string | InternalUrl, options: LocalProtocolOptions): string {
139
+ /** Resolve a local:// URL to an on-disk path under the active session's local root. */
140
+ export function resolveLocalUrlToPath(
141
+ input: string | InternalUrl,
142
+ options: LocalProtocolOptions,
143
+ platform: NodeJS.Platform = process.platform,
144
+ ): string {
123
145
  const url = typeof input === "string" ? parseLocalUrl(input) : input;
124
- const localRoot = path.resolve(resolveLocalRoot(options));
146
+ const localRoot = path.resolve(resolveLocalRoot(options, platform));
125
147
  const relativePath = extractRelativePath(url);
126
148
 
127
149
  if (!relativePath) {
package/src/lsp/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ptree } from "@oh-my-pi/pi-utils";
2
- import * as z from "zod/v4";
2
+ import { z } from "zod/v4";
3
3
 
4
4
  // =============================================================================
5
5
  // Tool Schema
package/src/main.ts CHANGED
@@ -21,11 +21,10 @@ import {
21
21
  } from "@oh-my-pi/pi-utils";
22
22
  import chalk from "chalk";
23
23
  import { reset as resetCapabilities } from "./capability";
24
- import type { Args } from "./cli/args";
24
+ import { type Args, reportUnrecognizedFlags } from "./cli/args";
25
25
  import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
26
26
  import { processFileArguments } from "./cli/file-processor";
27
27
  import { buildInitialMessage } from "./cli/initial-message";
28
- import { runListModelsCommand } from "./cli/list-models";
29
28
  import { selectSession } from "./cli/session-picker";
30
29
  import { applyStartupCwd } from "./cli/startup-cwd";
31
30
  import { findConfigFile } from "./config";
@@ -65,7 +64,8 @@ import {
65
64
  } from "./sdk";
66
65
  import type { AgentSession } from "./session/agent-session";
67
66
  import type { AuthStorage } from "./session/auth-storage";
68
- import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
67
+ import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
68
+ import { SessionManager } from "./session/session-manager";
69
69
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
70
70
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
71
71
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
@@ -265,7 +265,7 @@ export async function submitInteractiveInput(
265
265
  InteractiveMode,
266
266
  "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError" | "checkShutdownRequested"
267
267
  >,
268
- session: Pick<AgentSession, "prompt" | "promptCustomMessage">,
268
+ session: Pick<AgentSession, "prompt" | "promptCustomMessage" | "isStreaming">,
269
269
  input: SubmittedUserInput,
270
270
  ): Promise<void> {
271
271
  if (input.cancelled) {
@@ -274,22 +274,32 @@ export async function submitInteractiveInput(
274
274
 
275
275
  try {
276
276
  using _keepalive = new EventLoopKeepalive();
277
+ const streamingBehavior = session.isStreaming ? ("followUp" as const) : undefined;
277
278
  // Continue shortcuts submit an already-started synthetic developer prompt with
278
279
  // no optimistic user message.
279
280
  if (!input.started && !mode.markPendingSubmissionStarted(input)) {
280
281
  return;
281
282
  }
282
283
  if (input.customType) {
283
- await session.promptCustomMessage({
284
+ const message = {
284
285
  customType: input.customType,
285
286
  content: input.text,
286
287
  display: input.display ?? false,
287
- attribution: "agent",
288
- });
288
+ attribution: "agent" as const,
289
+ };
290
+ await (streamingBehavior
291
+ ? session.promptCustomMessage(message, { streamingBehavior })
292
+ : session.promptCustomMessage(message));
289
293
  } else if (input.synthetic) {
294
+ // Synthetic continue shortcuts are hidden developer prompts. The streaming
295
+ // queue (#queueUserMessage) only carries user-attributed messages, so we do
296
+ // NOT pass streamingBehavior here: queueing would silently demote the
297
+ // developer directive to a visible user message. A synthetic submit while
298
+ // streaming keeps its prior behavior (rejected as busy) rather than changing
299
+ // its role.
290
300
  await session.prompt(input.text, { synthetic: true, expandPromptTemplates: false });
291
301
  } else {
292
- await session.prompt(input.text, { images: input.images });
302
+ await session.prompt(input.text, { images: input.images, ...(streamingBehavior && { streamingBehavior }) });
293
303
  }
294
304
  } catch (error: unknown) {
295
305
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -920,30 +930,6 @@ export async function runRootCommand(
920
930
  process.exit(0);
921
931
  }
922
932
 
923
- if (parsedArgs.listModels !== undefined) {
924
- const settingsInstance = await logger.time("settings:init:list-models", Settings.init, {
925
- cwd: getProjectDir(),
926
- configFiles: parsedArgs.config,
927
- });
928
- await modelRegistry.refresh("online");
929
- const cliExtensionPaths = parsedArgs.noExtensions
930
- ? []
931
- : [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
932
- const settingsExtensions = settingsInstance.get("extensions") ?? [];
933
- const disabledExtensionIds = settingsInstance.get("disabledExtensions") ?? [];
934
- const searchPattern = typeof parsedArgs.listModels === "string" ? parsedArgs.listModels : undefined;
935
- await runListModelsCommand({
936
- modelRegistry,
937
- cwd: getProjectDir(),
938
- additionalExtensionPaths: cliExtensionPaths,
939
- settingsExtensions,
940
- disabledExtensionIds,
941
- disableExtensionDiscovery: Boolean(parsedArgs.noExtensions),
942
- searchPattern,
943
- });
944
- process.exit(0);
945
- }
946
-
947
933
  if (parsedArgs.export) {
948
934
  let result: string;
949
935
  try {
@@ -1222,6 +1208,15 @@ export async function runRootCommand(
1222
1208
  },
1223
1209
  };
1224
1210
  const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
1211
+ // Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
1212
+ // know the real extension flag set. Without this check the unrecognized
1213
+ // token gets silently consumed and any following positional leaks as the
1214
+ // initial prompt — kicking off a real LLM session, MCP connection, and
1215
+ // tool calls (issue #2459). Exit code 2 matches the conventional
1216
+ // "command line usage error" convention.
1217
+ if (reportUnrecognizedFlags(initialArgs)) {
1218
+ process.exit(2);
1219
+ }
1225
1220
  const processedFiles =
1226
1221
  initialArgs.fileArgs.length > 0
1227
1222
  ? await logger.time("processFileArguments", () =>
@@ -67,9 +67,13 @@ export function validateServerName(name: string): string | undefined {
67
67
  if (name.length > 100) {
68
68
  return "Server name is too long (max 100 characters)";
69
69
  }
70
- // Check for invalid characters (only allow alphanumeric, dash, underscore, dot)
71
- if (!/^[a-zA-Z0-9_.-]+$/.test(name)) {
72
- return "Server name can only contain letters, numbers, dash, underscore, and dot";
70
+ // Check for invalid characters. Colon is allowed so namespaced plugin servers
71
+ // (e.g. "cloudflare:cloudflare-api" from a Claude Code marketplace plugin) can
72
+ // be persisted: the runtime already accepts colons in server names (tool names
73
+ // sanitize them via createMCPToolName) and `/mcp reauth` writes such names back
74
+ // as a user-config override that shadows the discovered entry.
75
+ if (!/^[a-zA-Z0-9_.:-]+$/.test(name)) {
76
+ return "Server name can only contain letters, numbers, dash, underscore, dot, and colon";
73
77
  }
74
78
  return undefined;
75
79
  }
@@ -643,6 +643,17 @@ export class MCPManager {
643
643
  return this.#sources.get(name) ?? this.#connections.get(name)?._source;
644
644
  }
645
645
 
646
+ /**
647
+ * Get the preserved (pre-auth) config for a known server — whether currently
648
+ * connected or merely discovered (a connect was attempted but may have failed,
649
+ * e.g. an OAuth server that has not been authorized yet). Mirrors the
650
+ * reconnect lookup at {@link reconnectServer} so callers like `/mcp reauth`
651
+ * can recover a discovered server's config without re-reading config files.
652
+ */
653
+ getServerConfig(name: string): MCPServerConfig | undefined {
654
+ return this.#connections.get(name)?.config ?? this.#serverConfigs.get(name);
655
+ }
656
+
646
657
  /**
647
658
  * Wait for a connection to complete (or fail).
648
659
  */
@@ -0,0 +1,21 @@
1
+ export const MCP_CONNECTING_EVENT_CHANNEL = "mcp:connecting";
2
+
3
+ export type McpConnectingEvent = { serverNames: string[] };
4
+
5
+ export function formatMCPConnectingMessage(serverNames: string[]): string {
6
+ return `Connecting to MCP servers: ${serverNames.join(", ")}…`;
7
+ }
8
+
9
+ /**
10
+ * Runtime validator for the cross-module event payload. The event bus is
11
+ * untyped at runtime, so the subscriber verifies the shape before formatting
12
+ * rather than trusting a cast — a malformed emit is ignored instead of throwing.
13
+ */
14
+ export function isMcpConnectingEvent(data: unknown): data is McpConnectingEvent {
15
+ return (
16
+ typeof data === "object" &&
17
+ data !== null &&
18
+ Array.isArray((data as { serverNames?: unknown }).serverNames) &&
19
+ (data as { serverNames: unknown[] }).serverNames.every(name => typeof name === "string")
20
+ );
21
+ }
@@ -589,7 +589,8 @@ export class StdioTransport implements MCPTransport {
589
589
  }
590
590
 
591
591
  if (this.#readLoop) {
592
- await this.#readLoop.catch(() => {});
592
+ // Do not block/await the read loop as it can hang indefinitely in some environments
593
+ this.#readLoop.catch(() => {});
593
594
  this.#readLoop = null;
594
595
  }
595
596
  }
@@ -5,11 +5,12 @@ import * as path from "node:path";
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import { type ApiKey, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
7
7
  import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
8
- import { getAgentDbPath, getMemoriesDir, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
8
+ import { getAgentDbPath, getMemoriesDir, isEnoent, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
9
9
 
10
10
  import type { ModelRegistry } from "../config/model-registry";
11
11
  import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
12
12
  import type { Settings } from "../config/settings";
13
+ import type { MemoryBackendSaveInput, MemoryBackendSaveResult } from "../memory-backend/types";
13
14
  import consolidationTemplate from "../prompts/memories/consolidation.md" with { type: "text" };
14
15
  import consolidationSystemTemplate from "../prompts/memories/consolidation_system.md" with { type: "text" };
15
16
  import readPathTemplate from "../prompts/memories/read-path.md" with { type: "text" };
@@ -156,22 +157,31 @@ export async function buildMemoryToolDeveloperInstructions(
156
157
  const cfg = loadMemoryConfig(settings);
157
158
  if (!cfg.enabled) return undefined;
158
159
  const memoryRoot = getMemoryRoot(agentDir, settings.getCwd());
159
- const summaryPath = path.join(memoryRoot, "memory_summary.md");
160
160
 
161
- let text: string;
161
+ let summary = "";
162
162
  try {
163
- text = await Bun.file(summaryPath).text();
163
+ summary = (await Bun.file(path.join(memoryRoot, "memory_summary.md")).text()).trim();
164
164
  } catch {
165
- return undefined;
165
+ // Missing or unreadable summary — injection is best-effort; fall through
166
+ // so any captured lessons still surface on their own.
166
167
  }
167
-
168
- const summary = text.trim();
169
- if (!summary) return undefined;
170
- const truncated = truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit);
171
- if (!truncated.trim()) return undefined;
168
+ const learned = await readLearnedLessons(memoryRoot);
169
+ if (!summary && !learned) return undefined;
170
+
171
+ const summaryOut = summary ? truncateByApproxTokens(summary, cfg.summaryInjectionTokenLimit).trim() : "";
172
+ // Lessons share ONE injection budget with the summary so the combined block
173
+ // stays within `summaryInjectionTokenLimit` (~4 chars/token, matching
174
+ // truncateByApproxTokens). With no summary, lessons get the whole budget.
175
+ // Clamp to 0: truncateByApproxTokens appends a marker, so a truncated summary
176
+ // can exceed `limit * 4` chars and drive the remainder negative — when the
177
+ // summary already fills the budget, lessons are simply dropped.
178
+ const learnedBudget = Math.max(0, cfg.summaryInjectionTokenLimit - Math.ceil(summaryOut.length / 4));
179
+ const learnedOut = learned && learnedBudget > 0 ? truncateByApproxTokens(learned, learnedBudget).trim() : "";
180
+ if (!summaryOut && !learnedOut) return undefined;
172
181
 
173
182
  return prompt.render(readPathTemplate, {
174
- memory_summary: truncated,
183
+ memory_summary: summaryOut,
184
+ learned: learnedOut,
175
185
  });
176
186
  }
177
187
 
@@ -982,6 +992,12 @@ function redactSecrets(input: string): string {
982
992
  /(?:sk|pk|rk|tok|key|secret|token|password)[-_A-Za-z0-9]{12,}/g,
983
993
  /[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g,
984
994
  /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
995
+ // Common provider token prefixes (GitHub, npm, Slack, Google).
996
+ /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
997
+ /github_pat_[A-Za-z0-9_]{20,}/g,
998
+ /npm_[A-Za-z0-9]{30,}/g,
999
+ /xox[baprs]-[A-Za-z0-9-]{10,}/g,
1000
+ /AIza[A-Za-z0-9_-]{30,}/g,
985
1001
  ];
986
1002
  for (const pattern of patterns) {
987
1003
  out = out.replace(pattern, "[REDACTED]");
@@ -1071,7 +1087,9 @@ function truncateByApproxTokens(text: string, tokenLimit: number): string {
1071
1087
 
1072
1088
  function computeModelTokenBudget(model: Model, config: MemoryRuntimeConfig): number {
1073
1089
  const maxTokens =
1074
- Number.isFinite(model.contextWindow) && model.contextWindow > 0 ? model.contextWindow : config.fallbackTokenLimit;
1090
+ model.contextWindow !== null && Number.isFinite(model.contextWindow) && model.contextWindow > 0
1091
+ ? model.contextWindow
1092
+ : config.fallbackTokenLimit;
1075
1093
  return Math.max(2048, Math.floor(maxTokens));
1076
1094
  }
1077
1095
 
@@ -1119,6 +1137,125 @@ export function getMemoryRoot(agentDir: string, cwd: string): string {
1119
1137
  return path.join(getMemoriesDir(agentDir), encodeProjectPath(cwd));
1120
1138
  }
1121
1139
 
1140
+ /**
1141
+ * Filename of the captured-lessons file under a project's memory root.
1142
+ *
1143
+ * Written by the `learn` tool via {@link saveLearnedLesson} and read back by
1144
+ * {@link buildMemoryToolDeveloperInstructions}. Deliberately distinct from the
1145
+ * consolidation artifacts (`MEMORY.md`, `memory_summary.md`, `skills/`) so a
1146
+ * consolidation pass never clobbers manually captured lessons.
1147
+ */
1148
+ const LEARNED_LESSONS_FILE = "learned.md";
1149
+ /** Newest-first cap on retained lessons, bounding file growth by entry count. */
1150
+ const MAX_LEARNED_LESSONS = 100;
1151
+ /** Per-field char caps so a single huge capture can't bloat learned.md. */
1152
+ const MAX_LEARNED_CONTENT_CHARS = 2000;
1153
+ const MAX_LEARNED_CONTEXT_CHARS = 400;
1154
+
1155
+ /**
1156
+ * Strip prompt-injection vectors from a single line of lesson text: control/
1157
+ * format chars, angle brackets (`</skills>`), backticks, and `~~~` fences, then
1158
+ * collapse whitespace. Applied on BOTH write and read (the block renders
1159
+ * unescaped into the system prompt), mirroring managed-skill descriptions.
1160
+ */
1161
+ function neutralizeInjection(text: string): string {
1162
+ return text
1163
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ")
1164
+ .replace(/[<>`]/g, "")
1165
+ .replace(/~{2,}/g, "~")
1166
+ .replace(/\s+/g, " ")
1167
+ .trim();
1168
+ }
1169
+
1170
+ /** Slice to `maxChars`, dropping a trailing unpaired high surrogate. */
1171
+ function boundChars(text: string, maxChars: number): string {
1172
+ if (text.length <= maxChars) return text;
1173
+ const sliced = text.slice(0, maxChars);
1174
+ return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
1175
+ }
1176
+
1177
+ /**
1178
+ * Normalize one lesson field for storage: neutralize injection delimiters
1179
+ * FIRST, then redact secrets (so delimiter stripping can't reassemble a token
1180
+ * the redactor would have caught), then bound the length.
1181
+ */
1182
+ function normalizeLearnedText(text: string, maxChars: number): string {
1183
+ return boundChars(redactSecrets(neutralizeInjection(text)).trim(), maxChars);
1184
+ }
1185
+
1186
+ /** Per-path write chains serializing `learned.md` read-modify-write. */
1187
+ const learnedWriteChains = new Map<string, Promise<unknown>>();
1188
+
1189
+ /**
1190
+ * Append one lesson to the project's `learned.md` (newest-first, deduped,
1191
+ * capped, secret-redacted, injection-neutralized). The file backs the `learn`
1192
+ * tool when `memory.backend` is `local`.
1193
+ */
1194
+ export async function saveLearnedLesson(
1195
+ agentDir: string,
1196
+ cwd: string,
1197
+ input: MemoryBackendSaveInput,
1198
+ ): Promise<MemoryBackendSaveResult> {
1199
+ const content = normalizeLearnedText(input.content, MAX_LEARNED_CONTENT_CHARS);
1200
+ if (!content) {
1201
+ return { backend: "local", stored: 0, message: "Empty lesson; nothing stored." };
1202
+ }
1203
+ const context = input.context ? normalizeLearnedText(input.context, MAX_LEARNED_CONTEXT_CHARS) : "";
1204
+ const line = context ? `- ${content} _(context: ${context})_` : `- ${content}`;
1205
+ const filePath = path.join(getMemoryRoot(agentDir, cwd), LEARNED_LESSONS_FILE);
1206
+
1207
+ // Serialize the read-modify-write per file: parallel `learn` calls (sibling
1208
+ // subagents, or two shared tool calls in one turn) share the project memory
1209
+ // root, so an unguarded RMW would let the last writer drop the other's lesson.
1210
+ const run = (learnedWriteChains.get(filePath) ?? Promise.resolve()).then(() => appendLearnedLine(filePath, line));
1211
+ const guarded = run.catch(() => {});
1212
+ learnedWriteChains.set(filePath, guarded);
1213
+ try {
1214
+ await run;
1215
+ } finally {
1216
+ // Drop the entry once this write is the chain tail, so the map does not
1217
+ // retain one promise per distinct memory root for the process lifetime.
1218
+ if (learnedWriteChains.get(filePath) === guarded) learnedWriteChains.delete(filePath);
1219
+ }
1220
+ return { backend: "local", stored: 1, message: `Lesson saved to ${LEARNED_LESSONS_FILE}.` };
1221
+ }
1222
+
1223
+ async function appendLearnedLine(filePath: string, line: string): Promise<void> {
1224
+ let existing = "";
1225
+ try {
1226
+ existing = await Bun.file(filePath).text();
1227
+ } catch (err) {
1228
+ if (!isEnoent(err)) throw err;
1229
+ }
1230
+ const prior = existing
1231
+ .split("\n")
1232
+ .map(l => l.trim())
1233
+ .filter(l => l.startsWith("- ") && l !== line);
1234
+ const lessons = [line, ...prior].slice(0, MAX_LEARNED_LESSONS);
1235
+ await Bun.write(filePath, `${lessons.join("\n")}\n`);
1236
+ }
1237
+
1238
+ /**
1239
+ * Read `learned.md`, neutralizing each line on read too — a hand-edited or
1240
+ * pre-existing file bypasses write-time normalization and the block renders
1241
+ * unescaped into the system prompt. Returns "" when absent/unreadable.
1242
+ */
1243
+ async function readLearnedLessons(memoryRoot: string): Promise<string> {
1244
+ let raw = "";
1245
+ try {
1246
+ raw = (await Bun.file(path.join(memoryRoot, LEARNED_LESSONS_FILE)).text()).trim();
1247
+ } catch {
1248
+ return "";
1249
+ }
1250
+ if (!raw) return "";
1251
+ // Neutralize delimiters THEN redact per line — mirrors the write path so a
1252
+ // hand-edited line cannot reassemble a token after delimiter stripping.
1253
+ return raw
1254
+ .split("\n")
1255
+ .map(line => redactSecrets(neutralizeInjection(line)))
1256
+ .join("\n");
1257
+ }
1258
+
1122
1259
  function encodeProjectPath(cwd: string): string {
1123
1260
  return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1124
1261
  }
@@ -46,10 +46,11 @@ function globalJobKey(cwd: string): string {
46
46
 
47
47
  export function openMemoryDb(dbPath: string): Database {
48
48
  const db = new Database(dbPath);
49
+ // Install the busy handler BEFORE any lock-taking statement. See #2421.
50
+ db.exec("PRAGMA busy_timeout = 5000");
49
51
  db.exec(`
50
52
  PRAGMA journal_mode=WAL;
51
53
  PRAGMA synchronous=NORMAL;
52
- PRAGMA busy_timeout=5000;
53
54
 
54
55
  CREATE TABLE IF NOT EXISTS threads (
55
56
  id TEXT PRIMARY KEY,
@@ -2,6 +2,7 @@ import {
2
2
  buildMemoryToolDeveloperInstructions,
3
3
  clearMemoryData,
4
4
  enqueueMemoryConsolidation,
5
+ saveLearnedLesson,
5
6
  startMemoryStartupTask,
6
7
  } from "../memories";
7
8
  import type { MemoryBackend } from "./types";
@@ -9,9 +10,10 @@ import type { MemoryBackend } from "./types";
9
10
  /**
10
11
  * Wraps the existing `memories/` module as a `MemoryBackend`.
11
12
  *
12
- * No behavioural change every call delegates to the legacy entry points so
13
- * the local memory pipeline (rollout summarisation SQLite → memory_summary.md)
14
- * keeps working exactly as before.
13
+ * The rollout-summarisation pipeline (rollouts SQLite memory_summary.md) is
14
+ * delegated unchanged. On top of it, `save()` persists `learn`-tool lessons to
15
+ * `learned.md` (so `status()` reports `writable: true`); structured search is
16
+ * still unavailable.
15
17
  */
16
18
  export const localBackend: MemoryBackend = {
17
19
  id: "local",
@@ -27,13 +29,17 @@ export const localBackend: MemoryBackend = {
27
29
  async enqueue(agentDir, cwd) {
28
30
  enqueueMemoryConsolidation(agentDir, cwd);
29
31
  },
32
+ async save(context, input) {
33
+ return saveLearnedLesson(context.agentDir, context.cwd, input);
34
+ },
30
35
  async status() {
31
36
  return {
32
37
  backend: "local" as const,
33
38
  active: true,
34
- writable: false,
39
+ writable: true,
35
40
  searchable: false,
36
- message: "Local rollout-summary memory is active; structured search/save is not available.",
41
+ message:
42
+ "Local rollout-summary memory is active; lessons from the `learn` tool are saved to learned.md. Structured search is not available.",
37
43
  };
38
44
  },
39
45
  };
@@ -305,6 +305,7 @@ function createStatsMemory(config: MnemopiBackendConfig, bank: string): Mnemopi
305
305
  authorType: "agent",
306
306
  channelId: bank,
307
307
  ...providerOptions,
308
+ reconcile: false,
308
309
  } as ConstructorParameters<typeof Mnemopi>[0]);
309
310
  }
310
311