@oh-my-pi/pi-coding-agent 15.10.10 → 15.10.12

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 (415) hide show
  1. package/CHANGELOG.md +142 -7
  2. package/dist/cli.js +23108 -0
  3. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  4. package/dist/types/async/job-manager.d.ts +18 -0
  5. package/dist/types/cli/args.d.ts +2 -1
  6. package/dist/types/cli/dry-balance-cli.d.ts +1 -1
  7. package/dist/types/cli/gallery-cli.d.ts +1 -1
  8. package/dist/types/cli/gallery-fixtures/types.d.ts +1 -1
  9. package/dist/types/cli/usage-cli.d.ts +72 -0
  10. package/dist/types/cli-commands.d.ts +12 -0
  11. package/dist/types/commands/launch.d.ts +5 -1
  12. package/dist/types/commands/read.d.ts +1 -1
  13. package/dist/types/commands/usage.d.ts +25 -0
  14. package/dist/types/config/api-key-resolver.d.ts +3 -0
  15. package/dist/types/config/append-only-context-mode.d.ts +2 -1
  16. package/dist/types/config/model-discovery.d.ts +55 -0
  17. package/dist/types/config/model-registry.d.ts +8 -219
  18. package/dist/types/config/model-resolver.d.ts +34 -10
  19. package/dist/types/config/model-roles.d.ts +28 -0
  20. package/dist/types/config/models-config-schema.d.ts +523 -42
  21. package/dist/types/config/models-config.d.ts +385 -0
  22. package/dist/types/config/settings-schema.d.ts +41 -8
  23. package/dist/types/config/settings.d.ts +8 -1
  24. package/dist/types/debug/log-viewer.d.ts +1 -1
  25. package/dist/types/debug/raw-sse.d.ts +1 -1
  26. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  27. package/dist/types/eval/backend.d.ts +0 -2
  28. package/dist/types/eval/idle-timeout.d.ts +0 -4
  29. package/dist/types/eval/js/shared/rewrite-imports.d.ts +6 -6
  30. package/dist/types/eval/py/executor.d.ts +5 -0
  31. package/dist/types/eval/py/kernel.d.ts +6 -1
  32. package/dist/types/eval/py/runtime.d.ts +9 -0
  33. package/dist/types/exec/bash-executor.d.ts +2 -0
  34. package/dist/types/export/html/template.generated.d.ts +1 -1
  35. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  36. package/dist/types/extensibility/extensions/types.d.ts +6 -3
  37. package/dist/types/hindsight/mental-models.d.ts +17 -8
  38. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
  39. package/dist/types/internal-urls/types.d.ts +1 -1
  40. package/dist/types/lsp/edits.d.ts +9 -0
  41. package/dist/types/lsp/index.d.ts +2 -2
  42. package/dist/types/lsp/types.d.ts +2 -0
  43. package/dist/types/lsp/utils.d.ts +3 -0
  44. package/dist/types/mcp/json-rpc.d.ts +5 -0
  45. package/dist/types/memory-backend/index.d.ts +1 -0
  46. package/dist/types/memory-backend/runtime.d.ts +4 -0
  47. package/dist/types/memory-backend/types.d.ts +66 -1
  48. package/dist/types/mnemopi/state.d.ts +11 -1
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  51. package/dist/types/modes/components/bash-execution.d.ts +1 -1
  52. package/dist/types/modes/components/copy-selector.d.ts +1 -1
  53. package/dist/types/modes/components/dynamic-border.d.ts +1 -1
  54. package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -1
  55. package/dist/types/modes/components/extensions/extension-list.d.ts +1 -1
  56. package/dist/types/modes/components/extensions/inspector-panel.d.ts +1 -1
  57. package/dist/types/modes/components/footer.d.ts +1 -1
  58. package/dist/types/modes/components/hook-editor.d.ts +5 -0
  59. package/dist/types/modes/components/hook-input.d.ts +4 -0
  60. package/dist/types/modes/components/hook-selector.d.ts +1 -1
  61. package/dist/types/modes/components/model-selector.d.ts +1 -1
  62. package/dist/types/modes/components/plan-review-overlay.d.ts +1 -1
  63. package/dist/types/modes/components/session-observer-overlay.d.ts +1 -1
  64. package/dist/types/modes/components/session-selector.d.ts +1 -1
  65. package/dist/types/modes/components/status-line/component.d.ts +1 -1
  66. package/dist/types/modes/components/tiny-title-download-progress.d.ts +1 -1
  67. package/dist/types/modes/components/transcript-container.d.ts +25 -6
  68. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  69. package/dist/types/modes/components/user-message-selector.d.ts +1 -1
  70. package/dist/types/modes/components/user-message.d.ts +2 -1
  71. package/dist/types/modes/components/visual-truncate.d.ts +1 -1
  72. package/dist/types/modes/components/welcome.d.ts +19 -3
  73. package/dist/types/modes/controllers/mcp-command-controller.d.ts +1 -1
  74. package/dist/types/modes/controllers/streaming-reveal.d.ts +1 -1
  75. package/dist/types/modes/index.d.ts +3 -3
  76. package/dist/types/modes/interactive-mode.d.ts +8 -3
  77. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  78. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  79. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  80. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  81. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  82. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  83. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  84. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  85. package/dist/types/modes/setup-wizard/scenes/types.d.ts +1 -1
  86. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  87. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +1 -1
  88. package/dist/types/modes/types.d.ts +4 -1
  89. package/dist/types/secrets/index.d.ts +1 -1
  90. package/dist/types/secrets/obfuscator.d.ts +8 -2
  91. package/dist/types/session/agent-session.d.ts +15 -3
  92. package/dist/types/session/auth-broker-config.d.ts +4 -0
  93. package/dist/types/session/session-manager.d.ts +1 -1
  94. package/dist/types/session/streaming-output.d.ts +23 -0
  95. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  96. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  97. package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +13 -0
  98. package/dist/types/slash-commands/types.d.ts +1 -1
  99. package/dist/types/ssh/connection-manager.d.ts +8 -0
  100. package/dist/types/system-prompt.d.ts +2 -0
  101. package/dist/types/task/executor.d.ts +1 -0
  102. package/dist/types/task/index.d.ts +2 -2
  103. package/dist/types/task/parallel.d.ts +2 -2
  104. package/dist/types/task/types.d.ts +8 -0
  105. package/dist/types/task/worktree.d.ts +2 -0
  106. package/dist/types/thinking.d.ts +4 -0
  107. package/dist/types/tiny/title-client.d.ts +11 -0
  108. package/dist/types/tiny/title-protocol.d.ts +1 -0
  109. package/dist/types/tools/ask.d.ts +4 -0
  110. package/dist/types/tools/conflict-detect.d.ts +16 -0
  111. package/dist/types/tools/github-cache.d.ts +7 -0
  112. package/dist/types/tools/index.d.ts +6 -0
  113. package/dist/types/tools/sqlite-reader.d.ts +3 -0
  114. package/dist/types/tui/output-block.d.ts +3 -3
  115. package/dist/types/utils/changelog.d.ts +8 -0
  116. package/dist/types/utils/git.d.ts +15 -2
  117. package/dist/types/utils/title-generator.d.ts +3 -2
  118. package/dist/types/web/scrapers/readthedocs.d.ts +3 -0
  119. package/dist/types/web/scrapers/types.d.ts +12 -0
  120. package/dist/types/web/search/providers/codex.d.ts +1 -1
  121. package/dist/types/web/search/providers/gemini.d.ts +1 -1
  122. package/examples/extensions/tools.ts +5 -4
  123. package/package.json +14 -11
  124. package/scripts/build-binary.ts +18 -23
  125. package/scripts/bundle-dist.ts +81 -0
  126. package/scripts/{dev-launch → omp} +1 -1
  127. package/scripts/{dev-launch-preload.ts → omp.ts} +1 -1
  128. package/src/async/job-manager.ts +57 -3
  129. package/src/auto-thinking/classifier.ts +1 -0
  130. package/src/autoresearch/dashboard.ts +1 -1
  131. package/src/autoresearch/prompt-setup.md +6 -6
  132. package/src/autoresearch/prompt.md +6 -6
  133. package/src/capability/fs.ts +10 -0
  134. package/src/cli/args.ts +4 -1
  135. package/src/cli/auth-gateway-cli.ts +1 -3
  136. package/src/cli/dry-balance-cli.ts +1 -1
  137. package/src/cli/gallery-cli.ts +1 -1
  138. package/src/cli/gallery-fixtures/fs.ts +1 -1
  139. package/src/cli/gallery-fixtures/types.ts +5 -1
  140. package/src/cli/list-models.ts +2 -1
  141. package/src/cli/usage-cli.ts +603 -0
  142. package/src/cli-commands.ts +30 -0
  143. package/src/cli.ts +76 -13
  144. package/src/commands/complete.ts +1 -1
  145. package/src/commands/launch.ts +5 -1
  146. package/src/commands/read.ts +6 -3
  147. package/src/commands/usage.ts +35 -0
  148. package/src/commit/agentic/agent.ts +1 -1
  149. package/src/commit/model-selection.ts +4 -3
  150. package/src/config/api-key-resolver.ts +8 -6
  151. package/src/config/append-only-context-mode.ts +6 -12
  152. package/src/config/model-discovery.ts +554 -0
  153. package/src/config/model-registry.ts +320 -1041
  154. package/src/config/model-resolver.ts +173 -156
  155. package/src/config/model-roles.ts +74 -0
  156. package/src/config/models-config-schema.ts +57 -8
  157. package/src/config/models-config.ts +129 -0
  158. package/src/config/settings-schema.ts +61 -19
  159. package/src/config/settings.ts +98 -4
  160. package/src/dap/client.ts +124 -37
  161. package/src/dap/session.ts +259 -158
  162. package/src/debug/log-viewer.ts +1 -1
  163. package/src/debug/raw-sse.ts +1 -1
  164. package/src/edit/diff.ts +47 -3
  165. package/src/edit/hashline/block-resolver.ts +20 -1
  166. package/src/edit/hashline/diff.ts +36 -1
  167. package/src/edit/hashline/execute.ts +47 -4
  168. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  169. package/src/edit/index.ts +16 -1
  170. package/src/edit/modes/patch.ts +52 -0
  171. package/src/edit/modes/replace.ts +56 -22
  172. package/src/edit/notebook.ts +22 -2
  173. package/src/edit/renderer.ts +36 -10
  174. package/src/eval/__tests__/completion-bridge.test.ts +1 -1
  175. package/src/eval/backend.ts +0 -2
  176. package/src/eval/completion-bridge.ts +3 -1
  177. package/src/eval/idle-timeout.ts +2 -9
  178. package/src/eval/js/context-manager.ts +6 -8
  179. package/src/eval/js/executor.ts +6 -2
  180. package/src/eval/js/index.ts +0 -2
  181. package/src/eval/js/shared/helpers.ts +5 -6
  182. package/src/eval/js/shared/local-module-loader.ts +1 -1
  183. package/src/eval/js/shared/prelude.txt +62 -1
  184. package/src/eval/js/shared/rewrite-imports.ts +40 -22
  185. package/src/eval/js/shared/runtime.ts +1 -1
  186. package/src/eval/py/executor.ts +29 -7
  187. package/src/eval/py/index.ts +6 -3
  188. package/src/eval/py/kernel.ts +43 -4
  189. package/src/eval/py/runner.py +107 -3
  190. package/src/eval/py/runtime.ts +37 -0
  191. package/src/exec/bash-executor.ts +85 -4
  192. package/src/export/html/template.generated.ts +1 -1
  193. package/src/export/html/template.js +3 -1
  194. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  195. package/src/extensibility/extensions/runner.ts +6 -1
  196. package/src/extensibility/extensions/types.ts +6 -2
  197. package/src/extensibility/plugins/legacy-pi-compat.ts +20 -3
  198. package/src/hindsight/bank.ts +17 -2
  199. package/src/hindsight/mental-models.ts +59 -12
  200. package/src/hindsight/state.ts +6 -1
  201. package/src/internal-urls/artifact-protocol.ts +11 -2
  202. package/src/internal-urls/docs-index.generated.ts +11 -11
  203. package/src/internal-urls/issue-pr-protocol.ts +12 -5
  204. package/src/internal-urls/router.ts +1 -1
  205. package/src/internal-urls/types.ts +1 -1
  206. package/src/lib/xai-http.ts +1 -1
  207. package/src/lsp/client.ts +118 -38
  208. package/src/lsp/clients/biome-client.ts +101 -39
  209. package/src/lsp/edits.ts +143 -95
  210. package/src/lsp/index.ts +31 -22
  211. package/src/lsp/render.ts +1 -1
  212. package/src/lsp/types.ts +2 -0
  213. package/src/lsp/utils.ts +28 -10
  214. package/src/main.ts +183 -23
  215. package/src/mcp/json-rpc.ts +35 -5
  216. package/src/mcp/transports/stdio.ts +7 -1
  217. package/src/memories/index.ts +4 -1
  218. package/src/memory-backend/index.ts +1 -0
  219. package/src/memory-backend/local-backend.ts +9 -0
  220. package/src/memory-backend/off-backend.ts +9 -0
  221. package/src/memory-backend/runtime.ts +66 -0
  222. package/src/memory-backend/types.ts +81 -1
  223. package/src/mnemopi/backend.ts +176 -7
  224. package/src/mnemopi/state.ts +38 -2
  225. package/src/modes/acp/acp-agent.ts +119 -11
  226. package/src/modes/components/agent-dashboard.ts +10 -7
  227. package/src/modes/components/assistant-message.ts +32 -28
  228. package/src/modes/components/bash-execution.ts +1 -1
  229. package/src/modes/components/copy-selector.ts +1 -1
  230. package/src/modes/components/diff.ts +13 -2
  231. package/src/modes/components/dynamic-border.ts +12 -3
  232. package/src/modes/components/extensions/extension-dashboard.ts +8 -5
  233. package/src/modes/components/extensions/extension-list.ts +1 -1
  234. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  235. package/src/modes/components/footer.ts +4 -2
  236. package/src/modes/components/history-search.ts +1 -1
  237. package/src/modes/components/hook-editor.ts +8 -0
  238. package/src/modes/components/hook-input.ts +8 -0
  239. package/src/modes/components/hook-selector.ts +2 -2
  240. package/src/modes/components/model-selector.ts +4 -2
  241. package/src/modes/components/plan-review-overlay.ts +1 -1
  242. package/src/modes/components/session-observer-overlay.ts +2 -2
  243. package/src/modes/components/session-selector.ts +1 -1
  244. package/src/modes/components/settings-selector.ts +5 -1
  245. package/src/modes/components/status-line/component.ts +119 -35
  246. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  247. package/src/modes/components/transcript-container.ts +258 -53
  248. package/src/modes/components/tree-selector.ts +3 -3
  249. package/src/modes/components/user-message-selector.ts +1 -1
  250. package/src/modes/components/user-message.ts +17 -5
  251. package/src/modes/components/visual-truncate.ts +1 -1
  252. package/src/modes/components/welcome.ts +108 -26
  253. package/src/modes/controllers/command-controller.ts +11 -4
  254. package/src/modes/controllers/event-controller.ts +73 -4
  255. package/src/modes/controllers/input-controller.ts +2 -1
  256. package/src/modes/controllers/mcp-command-controller.ts +39 -4
  257. package/src/modes/controllers/selector-controller.ts +1 -1
  258. package/src/modes/controllers/streaming-reveal.ts +85 -18
  259. package/src/modes/index.ts +3 -21
  260. package/src/modes/interactive-mode.ts +42 -18
  261. package/src/modes/oauth-manual-input.ts +30 -3
  262. package/src/modes/rpc/rpc-client.ts +154 -3
  263. package/src/modes/rpc/rpc-mode.ts +97 -12
  264. package/src/modes/rpc/rpc-subagents.ts +265 -0
  265. package/src/modes/rpc/rpc-types.ts +81 -1
  266. package/src/modes/setup-wizard/index.ts +12 -2
  267. package/src/modes/setup-wizard/lazy.ts +16 -0
  268. package/src/modes/setup-wizard/scenes/glyph.ts +1 -1
  269. package/src/modes/setup-wizard/scenes/providers.ts +1 -1
  270. package/src/modes/setup-wizard/scenes/sign-in.ts +1 -1
  271. package/src/modes/setup-wizard/scenes/theme.ts +1 -1
  272. package/src/modes/setup-wizard/scenes/types.ts +1 -1
  273. package/src/modes/setup-wizard/scenes/web-search.ts +1 -1
  274. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  275. package/src/modes/types.ts +4 -1
  276. package/src/prompts/agents/explore.md +2 -2
  277. package/src/prompts/agents/librarian.md +1 -2
  278. package/src/prompts/agents/oracle.md +1 -1
  279. package/src/prompts/agents/plan.md +5 -5
  280. package/src/prompts/agents/task.md +5 -5
  281. package/src/prompts/ci-green-request.md +5 -7
  282. package/src/prompts/goals/goal-budget-limit.md +2 -2
  283. package/src/prompts/goals/goal-continuation.md +4 -4
  284. package/src/prompts/goals/goal-mode-active.md +1 -1
  285. package/src/prompts/memories/read-path.md +1 -1
  286. package/src/prompts/memories/stage_one_system.md +2 -2
  287. package/src/prompts/review-custom-request.md +1 -1
  288. package/src/prompts/system/agent-creation-architect.md +2 -2
  289. package/src/prompts/system/auto-continue.md +1 -1
  290. package/src/prompts/system/background-tan-dispatch.md +1 -1
  291. package/src/prompts/system/btw-user.md +2 -2
  292. package/src/prompts/system/commit-message-system.md +13 -1
  293. package/src/prompts/system/custom-system-prompt.md +1 -1
  294. package/src/prompts/system/eager-todo.md +2 -2
  295. package/src/prompts/system/irc-incoming.md +1 -1
  296. package/src/prompts/system/manual-continue.md +1 -1
  297. package/src/prompts/system/omfg-user.md +3 -4
  298. package/src/prompts/system/orchestrate-notice.md +9 -9
  299. package/src/prompts/system/plan-mode-active.md +4 -4
  300. package/src/prompts/system/plan-mode-subagent.md +4 -5
  301. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  302. package/src/prompts/system/project-prompt.md +2 -2
  303. package/src/prompts/system/subagent-system-prompt.md +4 -4
  304. package/src/prompts/system/system-prompt.md +13 -24
  305. package/src/prompts/system/title-system.md +2 -2
  306. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  307. package/src/prompts/system/workflow-notice.md +1 -1
  308. package/src/prompts/tools/ast-edit.md +1 -1
  309. package/src/prompts/tools/ast-grep.md +2 -2
  310. package/src/prompts/tools/bash.md +5 -7
  311. package/src/prompts/tools/browser.md +7 -7
  312. package/src/prompts/tools/debug.md +1 -1
  313. package/src/prompts/tools/eval.md +3 -3
  314. package/src/prompts/tools/find.md +0 -1
  315. package/src/prompts/tools/github.md +8 -7
  316. package/src/prompts/tools/goal.md +1 -1
  317. package/src/prompts/tools/image-gen.md +1 -1
  318. package/src/prompts/tools/inspect-image-system.md +1 -1
  319. package/src/prompts/tools/irc.md +15 -15
  320. package/src/prompts/tools/lsp.md +2 -2
  321. package/src/prompts/tools/patch.md +2 -2
  322. package/src/prompts/tools/read.md +3 -4
  323. package/src/prompts/tools/recall.md +1 -1
  324. package/src/prompts/tools/reflect.md +1 -1
  325. package/src/prompts/tools/render-mermaid.md +2 -2
  326. package/src/prompts/tools/replace.md +4 -10
  327. package/src/prompts/tools/rewind.md +2 -2
  328. package/src/prompts/tools/search-tool-bm25.md +1 -9
  329. package/src/prompts/tools/search.md +0 -1
  330. package/src/prompts/tools/ssh.md +0 -4
  331. package/src/prompts/tools/task.md +2 -3
  332. package/src/prompts/tools/todo.md +1 -1
  333. package/src/sdk.ts +31 -11
  334. package/src/secrets/index.ts +8 -1
  335. package/src/secrets/obfuscator.ts +39 -18
  336. package/src/session/agent-session.ts +223 -64
  337. package/src/session/auth-broker-config.ts +30 -1
  338. package/src/session/session-manager.ts +2 -2
  339. package/src/session/streaming-output.ts +188 -11
  340. package/src/slash-commands/acp-builtins.ts +24 -0
  341. package/src/slash-commands/builtin-registry.ts +40 -0
  342. package/src/slash-commands/helpers/stats-dashboard.ts +85 -0
  343. package/src/slash-commands/types.ts +1 -1
  344. package/src/ssh/connection-manager.ts +27 -0
  345. package/src/system-prompt.ts +14 -0
  346. package/src/task/commands.ts +2 -1
  347. package/src/task/executor.ts +74 -65
  348. package/src/task/index.ts +146 -68
  349. package/src/task/parallel.ts +3 -3
  350. package/src/task/render.ts +20 -5
  351. package/src/task/types.ts +9 -0
  352. package/src/task/worktree.ts +64 -56
  353. package/src/thinking.ts +9 -1
  354. package/src/tiny/title-client.ts +60 -16
  355. package/src/tiny/title-protocol.ts +1 -1
  356. package/src/tiny/worker.ts +6 -4
  357. package/src/tools/archive-reader.ts +30 -2
  358. package/src/tools/ask.ts +104 -21
  359. package/src/tools/ast-edit.ts +25 -5
  360. package/src/tools/auto-generated-guard.ts +20 -3
  361. package/src/tools/bash-interactive.ts +27 -7
  362. package/src/tools/bash.ts +100 -18
  363. package/src/tools/browser/launch.ts +11 -2
  364. package/src/tools/browser/readable.ts +19 -2
  365. package/src/tools/browser/registry.ts +4 -1
  366. package/src/tools/browser/render.ts +2 -2
  367. package/src/tools/browser/tab-supervisor.ts +55 -16
  368. package/src/tools/conflict-detect.ts +50 -4
  369. package/src/tools/debug.ts +1 -1
  370. package/src/tools/eval-render.ts +5 -5
  371. package/src/tools/eval.ts +0 -2
  372. package/src/tools/fetch.ts +33 -10
  373. package/src/tools/gh-cache-invalidation.ts +63 -8
  374. package/src/tools/gh-renderer.ts +1 -1
  375. package/src/tools/gh.ts +172 -29
  376. package/src/tools/github-cache.ts +70 -6
  377. package/src/tools/image-gen.ts +14 -13
  378. package/src/tools/index.ts +13 -1
  379. package/src/tools/inspect-image.ts +1 -0
  380. package/src/tools/irc.ts +5 -1
  381. package/src/tools/job.ts +1 -1
  382. package/src/tools/read.ts +202 -61
  383. package/src/tools/render-utils.ts +3 -3
  384. package/src/tools/resolve.ts +1 -1
  385. package/src/tools/search.ts +92 -29
  386. package/src/tools/sqlite-reader.ts +17 -5
  387. package/src/tools/ssh.ts +8 -8
  388. package/src/tools/todo.ts +38 -8
  389. package/src/tools/write.ts +118 -18
  390. package/src/tui/output-block.ts +4 -4
  391. package/src/utils/changelog.ts +27 -1
  392. package/src/utils/commit-message-generator.ts +1 -0
  393. package/src/utils/file-mentions.ts +2 -1
  394. package/src/utils/git.ts +267 -13
  395. package/src/utils/title-generator.ts +24 -5
  396. package/src/web/scrapers/arxiv.ts +1 -1
  397. package/src/web/scrapers/go-pkg.ts +1 -1
  398. package/src/web/scrapers/iacr.ts +1 -1
  399. package/src/web/scrapers/readthedocs.ts +1 -1
  400. package/src/web/scrapers/twitter.ts +2 -1
  401. package/src/web/scrapers/types.ts +87 -8
  402. package/src/web/scrapers/wikipedia.ts +1 -1
  403. package/src/web/scrapers/youtube.ts +6 -1
  404. package/src/web/search/index.ts +1 -1
  405. package/src/web/search/providers/codex.ts +2 -1
  406. package/src/web/search/providers/gemini.ts +2 -3
  407. package/src/web/search/render.ts +8 -6
  408. package/dist/types/config/model-equivalence.d.ts +0 -24
  409. package/dist/types/config/model-id-affixes.d.ts +0 -12
  410. package/dist/types/config/model-provider-priority.d.ts +0 -1
  411. package/dist/types/exec/idle-timeout-watchdog.d.ts +0 -18
  412. package/src/config/model-equivalence.ts +0 -875
  413. package/src/config/model-id-affixes.ts +0 -81
  414. package/src/config/model-provider-priority.ts +0 -56
  415. package/src/exec/idle-timeout-watchdog.ts +0 -126
package/src/tools/gh.ts CHANGED
@@ -17,7 +17,7 @@ import githubDescription from "../prompts/tools/github.md" with { type: "text" }
17
17
  import * as git from "../utils/git";
18
18
  import type { ToolSession } from ".";
19
19
  import { formatShortSha } from "./gh-format";
20
- import { type CacheStatus, getOrFetchView, resolveGithubCacheAuthKey } from "./github-cache";
20
+ import { type CacheStatus, getOrFetchView, invalidateAllForNumber, resolveGithubCacheAuthKey } from "./github-cache";
21
21
  import type { OutputMeta } from "./output-meta";
22
22
  import { ToolError, throwIfAborted } from "./tool-errors";
23
23
  import { toolResult } from "./tool-result";
@@ -192,6 +192,10 @@ const SEARCH_LIMIT_DEFAULT = 10;
192
192
  const SEARCH_LIMIT_MAX = 50;
193
193
  const FILE_PREVIEW_LIMIT = 50;
194
194
  const RUN_WATCH_INTERVAL_DEFAULT = 3;
195
+ const RUN_WATCH_INTERVAL_SLOW = 15;
196
+ const RUN_WATCH_FAST_WINDOW_MS = 60_000;
197
+ const RUN_WATCH_NO_RUNS_GIVE_UP_MS = 90_000;
198
+ const RUN_WATCH_MAX_POLL_FAILURES = 5;
195
199
  const RUN_WATCH_GRACE_DEFAULT = 5;
196
200
  const RUN_WATCH_TAIL_DEFAULT = 15;
197
201
  const RUN_WATCH_TAIL_MAX = 200;
@@ -716,7 +720,9 @@ export function parseSearchDateBound(raw: string, now: Date = new Date()): strin
716
720
 
717
721
  const parsedMs = Date.parse(trimmed);
718
722
  if (!Number.isNaN(parsedMs)) {
719
- return new Date(parsedMs).toISOString();
723
+ // GitHub search qualifiers accept seconds precision only
724
+ // (`YYYY-MM-DDTHH:MM:SSZ`); strip the milliseconds toISOString emits.
725
+ return new Date(parsedMs).toISOString().replace(/\.\d{3}Z$/, "Z");
720
726
  }
721
727
 
722
728
  throw new ToolError(
@@ -1277,6 +1283,16 @@ function isFailedJob(job: GhRunJobSnapshot): boolean {
1277
1283
  return job.conclusion !== undefined && JOB_FAILURE_CONCLUSIONS.has(job.conclusion);
1278
1284
  }
1279
1285
 
1286
+ const GH_RATE_LIMIT_ERROR_PATTERN = /rate limit|HTTP 429|abuse detection/i;
1287
+
1288
+ /**
1289
+ * Rate-limit / secondary-limit gh failures are transient; the run_watch poll
1290
+ * loops back off and retry them instead of discarding the whole watch.
1291
+ */
1292
+ function isRateLimitedGhError(err: unknown): boolean {
1293
+ return err instanceof ToolError && GH_RATE_LIMIT_ERROR_PATTERN.test(err.message);
1294
+ }
1295
+
1280
1296
  function formatJobState(job: GhRunJobSnapshot): string {
1281
1297
  return job.conclusion ?? job.status ?? "unknown";
1282
1298
  }
@@ -1800,6 +1816,7 @@ async function fetchRunsForCommit(
1800
1816
  repo: string,
1801
1817
  headSha: string,
1802
1818
  signal?: AbortSignal,
1819
+ completedRunJobsCache?: Map<number, GhRunJobSnapshot[]>,
1803
1820
  ): Promise<GhRunSnapshot[]> {
1804
1821
  // Filter only by `head_sha`. The SHA uniquely identifies the commit, so
1805
1822
  // adding the GitHub `branch=` filter would wrongly exclude workflow runs
@@ -1826,7 +1843,19 @@ async function fetchRunsForCommit(
1826
1843
  (response.workflow_runs ?? [])
1827
1844
  .filter((run): run is GhActionsRunApi & { id: number } => typeof run.id === "number")
1828
1845
  .map(async run => {
1829
- const jobs = await fetchRunJobs(cwd, repo, run.id, signal);
1846
+ // Completed runs' job lists are stable until a re-run flips
1847
+ // `status` off "completed"; reuse them across watch polls so a
1848
+ // long watch does not refetch every finished run's jobs. A run
1849
+ // observed non-completed evicts its entry — when the re-run
1850
+ // completes, `status` flips back to "completed" and a stale
1851
+ // entry would serve the FIRST attempt's jobs and logs forever.
1852
+ const completed = run.status === "completed";
1853
+ if (!completed) completedRunJobsCache?.delete(run.id);
1854
+ let jobs = completed ? completedRunJobsCache?.get(run.id) : undefined;
1855
+ if (!jobs) {
1856
+ jobs = await fetchRunJobs(cwd, repo, run.id, signal);
1857
+ if (completed) completedRunJobsCache?.set(run.id, jobs);
1858
+ }
1830
1859
  return normalizeRunSnapshot(run, jobs);
1831
1860
  }),
1832
1861
  );
@@ -1857,12 +1886,13 @@ async function fetchRunJobs(
1857
1886
  signal,
1858
1887
  { repoProvided: true },
1859
1888
  );
1860
- const pageJobs = (response.jobs ?? [])
1861
- .map(job => normalizeRunJob(job))
1862
- .filter((job): job is GhRunJobSnapshot => job !== null);
1889
+ const rawPage = response.jobs ?? [];
1890
+ const pageJobs = rawPage.map(job => normalizeRunJob(job)).filter((job): job is GhRunJobSnapshot => job !== null);
1863
1891
  jobs.push(...pageJobs);
1864
1892
 
1865
- if (pageJobs.length < RUN_JOBS_PAGE_SIZE) {
1893
+ // Compare the raw page length: normalizeRunJob drops malformed items,
1894
+ // and a post-filter short page must not end pagination early.
1895
+ if (rawPage.length < RUN_JOBS_PAGE_SIZE) {
1866
1896
  break;
1867
1897
  }
1868
1898
 
@@ -1907,7 +1937,9 @@ async function fetchPrReviewComments(
1907
1937
  .filter((comment): comment is GhPrReviewComment => comment !== null);
1908
1938
  reviewComments.push(...pageComments);
1909
1939
 
1910
- if (pageComments.length < REVIEW_COMMENTS_PAGE_SIZE) {
1940
+ // Compare the raw page length: a dropped malformed item must not end
1941
+ // pagination early and silently lose the remaining pages.
1942
+ if (response.length < REVIEW_COMMENTS_PAGE_SIZE) {
1911
1943
  break;
1912
1944
  }
1913
1945
 
@@ -2548,6 +2580,9 @@ async function fetchPrViewFresh(
2548
2580
  */
2549
2581
  export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<ViewLookupResult<GhIssueViewData>> {
2550
2582
  const identifier = requireNonEmpty(options.issue, "issue");
2583
+ if (identifier.startsWith("-")) {
2584
+ throw new ToolError(`invalid issue identifier: ${identifier}. Pass an issue number or URL.`);
2585
+ }
2551
2586
  const includeComments = options.includeComments ?? true;
2552
2587
  const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
2553
2588
  const urlParse = parseIssueUrl(identifier);
@@ -2885,7 +2920,10 @@ async function fetchPrDiffFresh(
2885
2920
  appendRepoFlag(args, repo, String(number));
2886
2921
  const text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
2887
2922
  const payload = parsePrUnifiedDiff(text);
2888
- return { rendered: text, sourceUrl: undefined, payload };
2923
+ // `rendered` already carries the verbatim diff; blank the payload copy so
2924
+ // the cache row stores a potentially huge diff once instead of twice.
2925
+ // `getOrFetchPrDiff` rehydrates `unified` from `rendered`.
2926
+ return { rendered: text, sourceUrl: undefined, payload: { unified: "", files: payload.files } };
2889
2927
  }
2890
2928
 
2891
2929
  /**
@@ -2909,7 +2947,8 @@ export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<Vi
2909
2947
  return {
2910
2948
  rendered: lookup.rendered,
2911
2949
  sourceUrl: lookup.sourceUrl,
2912
- payload: lookup.payload,
2950
+ // Rehydrate the unified text from `rendered` (stored once per row).
2951
+ payload: { unified: lookup.rendered, files: lookup.payload.files },
2913
2952
  status: lookup.status,
2914
2953
  fetchedAt: lookup.fetchedAt,
2915
2954
  };
@@ -2930,9 +2969,35 @@ async function executePrCheckout(
2930
2969
  const prRefs = prList.length > 0 ? prList : [undefined];
2931
2970
  const isMulti = prRefs.length > 1;
2932
2971
 
2933
- const outcomes = await Promise.all(
2972
+ const settled = await Promise.allSettled(
2934
2973
  prRefs.map(prRef => checkoutPullRequest(session, signal, { prRef, repo, force })),
2935
2974
  );
2975
+ const outcomes: PrCheckoutOutcome[] = [];
2976
+ const failures: Array<{ prRef: string | undefined; reason: unknown }> = [];
2977
+ for (let i = 0; i < settled.length; i++) {
2978
+ const entry = settled[i];
2979
+ if (entry.status === "fulfilled") outcomes.push(entry.value);
2980
+ else failures.push({ prRef: prRefs[i], reason: entry.reason });
2981
+ }
2982
+ if (failures.length > 0) {
2983
+ throwIfAborted(signal);
2984
+ const failureLines = failures.map(
2985
+ f => `- ${f.prRef ?? "(current branch)"}: ${f.reason instanceof Error ? f.reason.message : String(f.reason)}`,
2986
+ );
2987
+ if (outcomes.length === 0) {
2988
+ if (failures.length === 1) throw failures[0].reason;
2989
+ throw new ToolError(`all ${failures.length} PR checkouts failed:\n${failureLines.join("\n")}`);
2990
+ }
2991
+ // Partial success: report the worktrees that did get created alongside
2992
+ // the failures so the agent does not lose track of them.
2993
+ const sections = outcomes.map(formatPrCheckoutResult);
2994
+ const header = `# ${outcomes.length}/${settled.length} Pull Request Worktrees checked out (${failures.length} failed)`;
2995
+ const text = [header, "", ...joinSections(sections), "", "## Failed", ...failureLines].join("\n").trim();
2996
+ return buildTextResult(text, undefined, {
2997
+ repo,
2998
+ checkouts: outcomes.map(outcomeToSummary),
2999
+ });
3000
+ }
2936
3001
 
2937
3002
  if (!isMulti) {
2938
3003
  const [outcome] = outcomes;
@@ -2983,6 +3048,9 @@ async function checkoutPullRequest(
2983
3048
  options: PrCheckoutOptions,
2984
3049
  ): Promise<PrCheckoutOutcome> {
2985
3050
  const { prRef, repo, force } = options;
3051
+ if (prRef?.startsWith("-")) {
3052
+ throw new ToolError(`invalid PR identifier: ${prRef}. Pass a PR number, URL, or branch name.`);
3053
+ }
2986
3054
  const args = ["pr", "view"];
2987
3055
  if (prRef) args.push(prRef);
2988
3056
  appendRepoFlag(args, repo, prRef);
@@ -3122,6 +3190,14 @@ async function executePrPush(
3122
3190
  signal,
3123
3191
  });
3124
3192
 
3193
+ // A successful push changes what `pr://N` and `pr://N/diff` should show;
3194
+ // drop the cached rows so the canonical "push → re-read diff" flow sees
3195
+ // fresh data instead of a soft-TTL stale snapshot.
3196
+ const pushedPr = parsePullRequestUrl(target.prUrl);
3197
+ if (pushedPr.prNumber !== undefined) {
3198
+ invalidateAllForNumber(pushedPr.prNumber, pushedPr.repo);
3199
+ }
3200
+
3125
3201
  return buildTextResult(
3126
3202
  formatPrPushResult({
3127
3203
  localBranch,
@@ -3376,9 +3452,24 @@ async function executeRunWatch(
3376
3452
  const explicitRepo = normalizeOptionalString(params.repo);
3377
3453
  const runReference = parseRunReference(params.run);
3378
3454
  const repo = await resolveGitHubRepo(session.cwd, explicitRepo, runReference.repo, signal);
3379
- const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
3380
3455
  const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
3381
3456
  const tail = resolveTailLimit(params.tail);
3457
+ const watchStartMs = Date.now();
3458
+ // Fast polls for the first minute for snappy feedback, then back off:
3459
+ // every commit-watch poll is one runs-list call plus one jobs call per
3460
+ // non-completed run, and long builds must not burn the shared
3461
+ // authenticated REST quota.
3462
+ const currentIntervalSeconds = () =>
3463
+ Date.now() - watchStartMs < RUN_WATCH_FAST_WINDOW_MS ? RUN_WATCH_INTERVAL_DEFAULT : RUN_WATCH_INTERVAL_SLOW;
3464
+ let consecutivePollFailures = 0;
3465
+ const handlePollError = async (err: unknown): Promise<void> => {
3466
+ if (signal?.aborted) throw err;
3467
+ consecutivePollFailures += 1;
3468
+ if (!isRateLimitedGhError(err) || consecutivePollFailures > RUN_WATCH_MAX_POLL_FAILURES) throw err;
3469
+ // Rate-limited: back off with the slow interval and retry instead of
3470
+ // discarding the whole watch (and its accumulated context).
3471
+ await scheduler.wait(RUN_WATCH_INTERVAL_SLOW * 1000, { signal });
3472
+ };
3382
3473
  if (runReference.runId !== undefined) {
3383
3474
  const runId = runReference.runId;
3384
3475
  let pollCount = 0;
@@ -3387,7 +3478,14 @@ async function executeRunWatch(
3387
3478
  throwIfAborted(signal);
3388
3479
  pollCount += 1;
3389
3480
 
3390
- let run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
3481
+ let run: GhRunSnapshot;
3482
+ try {
3483
+ run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
3484
+ } catch (err) {
3485
+ await handlePollError(err);
3486
+ continue;
3487
+ }
3488
+ consecutivePollFailures = 0;
3391
3489
  const details = buildRunWatchDetails(repo, run, {
3392
3490
  state: "watching",
3393
3491
  pollCount,
@@ -3397,7 +3495,7 @@ async function executeRunWatch(
3397
3495
  details,
3398
3496
  });
3399
3497
 
3400
- const failedJobs = run.jobs.filter(isFailedJob);
3498
+ let failedJobs = run.jobs.filter(isFailedJob);
3401
3499
  const runCompleted = run.status === "completed";
3402
3500
 
3403
3501
  if (failedJobs.length > 0) {
@@ -3417,13 +3515,28 @@ async function executeRunWatch(
3417
3515
  }),
3418
3516
  });
3419
3517
  await scheduler.wait(graceSeconds * 1000, { signal });
3420
- run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
3518
+ try {
3519
+ const refetched = await fetchRunSnapshot(session.cwd, repo, runId, signal);
3520
+ const refetchedFailed = refetched.jobs.filter(isFailedJob);
3521
+ // An auto-retry can reset job conclusions between
3522
+ // detection and refetch; keep the originally-detected
3523
+ // failure list (and its snapshot) when the refetch no
3524
+ // longer shows any failures so the watch never ends
3525
+ // with a failure result and zero logs.
3526
+ if (refetchedFailed.length > 0) {
3527
+ run = refetched;
3528
+ failedJobs = refetchedFailed;
3529
+ }
3530
+ } catch (err) {
3531
+ if (signal?.aborted) throw err;
3532
+ // Refetch failure: report from the original snapshot.
3533
+ }
3421
3534
  }
3422
3535
 
3423
3536
  const failedJobLogs = await fetchFailedJobLogs(
3424
3537
  session.cwd,
3425
3538
  repo,
3426
- run.jobs.filter(isFailedJob).map(job => ({ run, job })),
3539
+ failedJobs.map(job => ({ run, job })),
3427
3540
  tail,
3428
3541
  signal,
3429
3542
  );
@@ -3451,7 +3564,7 @@ async function executeRunWatch(
3451
3564
  return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
3452
3565
  }
3453
3566
 
3454
- await scheduler.wait(intervalSeconds * 1000, { signal });
3567
+ await scheduler.wait(currentIntervalSeconds() * 1000, { signal });
3455
3568
  }
3456
3569
  }
3457
3570
 
@@ -3479,12 +3592,22 @@ async function executeRunWatch(
3479
3592
  }
3480
3593
  let pollCount = 0;
3481
3594
  let settledSuccessSignature: string | undefined;
3595
+ let everSawRuns = false;
3596
+ const completedRunJobsCache = new Map<number, GhRunJobSnapshot[]>();
3482
3597
 
3483
3598
  while (true) {
3484
3599
  throwIfAborted(signal);
3485
3600
  pollCount += 1;
3486
3601
 
3487
- let runs = await fetchRunsForCommit(session.cwd, repo, headSha, signal);
3602
+ let runs: GhRunSnapshot[];
3603
+ try {
3604
+ runs = await fetchRunsForCommit(session.cwd, repo, headSha, signal, completedRunJobsCache);
3605
+ } catch (err) {
3606
+ await handlePollError(err);
3607
+ continue;
3608
+ }
3609
+ consecutivePollFailures = 0;
3610
+ if (runs.length > 0) everSawRuns = true;
3488
3611
  const details = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
3489
3612
  state: "watching",
3490
3613
  pollCount,
@@ -3496,6 +3619,7 @@ async function executeRunWatch(
3496
3619
 
3497
3620
  const outcome = getRunCollectionOutcome(runs);
3498
3621
  if (outcome === "failure") {
3622
+ let failedPairs = runs.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job })));
3499
3623
  if (graceSeconds > 0) {
3500
3624
  const note = `Failure detected. Waiting ${graceSeconds}s to capture concurrent failures before fetching logs.`;
3501
3625
  onUpdate?.({
@@ -3512,16 +3636,23 @@ async function executeRunWatch(
3512
3636
  }),
3513
3637
  });
3514
3638
  await scheduler.wait(graceSeconds * 1000, { signal });
3515
- runs = await fetchRunsForCommit(session.cwd, repo, headSha, signal);
3639
+ try {
3640
+ const refetched = await fetchRunsForCommit(session.cwd, repo, headSha, signal, completedRunJobsCache);
3641
+ const refetchedPairs = refetched.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job })));
3642
+ // Keep the originally-detected failure list when an
3643
+ // auto-retry reset the conclusions during the grace window
3644
+ // (see the run-id branch above).
3645
+ if (refetchedPairs.length > 0) {
3646
+ runs = refetched;
3647
+ failedPairs = refetchedPairs;
3648
+ }
3649
+ } catch (err) {
3650
+ if (signal?.aborted) throw err;
3651
+ // Refetch failure: report from the original snapshots.
3652
+ }
3516
3653
  }
3517
3654
 
3518
- const failedJobLogs = await fetchFailedJobLogs(
3519
- session.cwd,
3520
- repo,
3521
- runs.flatMap(run => run.jobs.filter(isFailedJob).map(job => ({ run, job }))),
3522
- tail,
3523
- signal,
3524
- );
3655
+ const failedJobLogs = await fetchFailedJobLogs(session.cwd, repo, failedPairs, tail, signal);
3525
3656
  const finalDetails = buildCommitRunWatchDetails(repo, headSha, branch, runs, {
3526
3657
  state: "completed",
3527
3658
  failedJobLogs,
@@ -3553,7 +3684,8 @@ async function executeRunWatch(
3553
3684
  }
3554
3685
 
3555
3686
  settledSuccessSignature = signature;
3556
- const note = `All known workflow runs completed successfully. Waiting ${intervalSeconds}s to ensure no additional runs appear for this commit.`;
3687
+ const confirmWaitSeconds = currentIntervalSeconds();
3688
+ const note = `All known workflow runs completed successfully. Waiting ${confirmWaitSeconds}s to ensure no additional runs appear for this commit.`;
3557
3689
  onUpdate?.({
3558
3690
  content: [
3559
3691
  {
@@ -3567,11 +3699,22 @@ async function executeRunWatch(
3567
3699
  note,
3568
3700
  }),
3569
3701
  });
3570
- await scheduler.wait(intervalSeconds * 1000, { signal });
3702
+ await scheduler.wait(confirmWaitSeconds * 1000, { signal });
3571
3703
  continue;
3572
3704
  }
3573
3705
 
3574
3706
  settledSuccessSignature = undefined;
3575
- await scheduler.wait(intervalSeconds * 1000, { signal });
3707
+ if (!everSawRuns && Date.now() - watchStartMs >= RUN_WATCH_NO_RUNS_GIVE_UP_MS) {
3708
+ // A repo with no Actions configured (or Actions disabled) never
3709
+ // produces a run for this commit; give up with a clear message
3710
+ // instead of polling forever.
3711
+ const elapsedSec = Math.round((Date.now() - watchStartMs) / 1000);
3712
+ return buildTextResult(
3713
+ `No workflow runs found for ${repo}@${formatShortSha(headSha) ?? headSha} after ${elapsedSec}s (${pollCount} polls). The commit may not trigger any GitHub Actions workflows, or Actions may be disabled for this repository. Pass \`run\` to watch a specific run.`,
3714
+ undefined,
3715
+ buildCommitRunWatchDetails(repo, headSha, branch, runs, { state: "completed", pollCount }),
3716
+ );
3717
+ }
3718
+ await scheduler.wait(currentIntervalSeconds() * 1000, { signal });
3576
3719
  }
3577
3720
  }
@@ -174,6 +174,21 @@ function hashCacheIdentity(parts: string[]): string {
174
174
  return Bun.hash(parts.map(part => `${part.length}:${part}`).join("|")).toString(36);
175
175
  }
176
176
 
177
+ /**
178
+ * Memo for {@link resolveGithubCacheAuthKey}. Recomputed only when the token
179
+ * env vars or the hosts.yml path/mtime change, so the per-lookup cost on the
180
+ * cache hot path is four env reads plus one `stat` instead of a full file
181
+ * read + hash.
182
+ */
183
+ interface AuthKeyMemoEntry {
184
+ envSig: string;
185
+ hostsPath: string;
186
+ hostsMtimeMs: number;
187
+ value: string | undefined;
188
+ }
189
+ const AUTH_KEY_TOKEN_ENV_VARS = ["GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN"];
190
+ const authKeyMemo = new Map<string, AuthKeyMemoEntry>();
191
+
177
192
  /**
178
193
  * Best-effort local fingerprint for the active GitHub CLI credentials.
179
194
  *
@@ -185,16 +200,32 @@ function hashCacheIdentity(parts: string[]): string {
185
200
  * credential source is visible, callers should pass `null` to bypass caching.
186
201
  */
187
202
  export function resolveGithubCacheAuthKey(host: string = process.env.GH_HOST || "github.com"): string | undefined {
203
+ const hostsPath = path.join(getGhConfigDir(), "hosts.yml");
204
+ let envSig = "";
205
+ for (const name of AUTH_KEY_TOKEN_ENV_VARS) {
206
+ const value = process.env[name];
207
+ if (value) envSig += `${name}=${value.length}:${value}\0`;
208
+ }
209
+ let hostsMtimeMs = -1;
210
+ try {
211
+ hostsMtimeMs = fs.statSync(hostsPath, { throwIfNoEntry: false })?.mtimeMs ?? -1;
212
+ } catch (err) {
213
+ logger.debug("github cache: failed to stat gh hosts config for cache identity", { err: String(err) });
214
+ }
215
+ const memo = authKeyMemo.get(host);
216
+ if (memo && memo.envSig === envSig && memo.hostsPath === hostsPath && memo.hostsMtimeMs === hostsMtimeMs) {
217
+ return memo.value;
218
+ }
219
+
188
220
  const parts: string[] = [`host:${host}`];
189
221
  let hasCredentialMaterial = false;
190
- for (const name of ["GH_TOKEN", "GITHUB_TOKEN", "GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN"]) {
222
+ for (const name of AUTH_KEY_TOKEN_ENV_VARS) {
191
223
  const value = process.env[name];
192
224
  if (!value) continue;
193
225
  hasCredentialMaterial = true;
194
226
  parts.push(`${name}:${value}`);
195
227
  }
196
228
  try {
197
- const hostsPath = path.join(getGhConfigDir(), "hosts.yml");
198
229
  const hosts = fs.readFileSync(hostsPath, "utf8");
199
230
  hasCredentialMaterial = true;
200
231
  parts.push(`hosts:${hosts}`);
@@ -203,8 +234,9 @@ export function resolveGithubCacheAuthKey(host: string = process.env.GH_HOST ||
203
234
  logger.debug("github cache: failed to read gh hosts config for cache identity", { err: String(err) });
204
235
  }
205
236
  }
206
- if (!hasCredentialMaterial) return undefined;
207
- return `${host}:${hashCacheIdentity(parts)}`;
237
+ const value = hasCredentialMaterial ? `${host}:${hashCacheIdentity(parts)}` : undefined;
238
+ authKeyMemo.set(host, { envSig, hostsPath, hostsMtimeMs, value });
239
+ return value;
208
240
  }
209
241
 
210
242
  function normalizeRepo(repo: string): string {
@@ -352,6 +384,26 @@ export function clearAll(): void {
352
384
  }
353
385
  }
354
386
 
387
+ /**
388
+ * Drop every cached row for a repo, or all rows when the repo is unknown.
389
+ * Fallback for current-branch `gh pr merge`/`gh pr close`-style mutations
390
+ * where the bash command names no PR number or URL, so the target row cannot
391
+ * be identified. Over-invalidation is deliberate (see module header).
392
+ */
393
+ export function invalidateAllForRepo(repo?: string): void {
394
+ const db = openDb();
395
+ if (!db) return;
396
+ try {
397
+ if (repo === undefined) {
398
+ db.prepare("DELETE FROM github_view_cache").run();
399
+ } else {
400
+ db.prepare("DELETE FROM github_view_cache WHERE repo = ?").run(normalizeRepo(repo));
401
+ }
402
+ } catch (err) {
403
+ logger.debug("github cache: invalidateAllForRepo failed", { err: String(err) });
404
+ }
405
+ }
406
+
355
407
  /**
356
408
  * Test/maintenance helper. Closes and forgets the cached connection so the
357
409
  * next access reopens against (possibly) a different DB path.
@@ -367,6 +419,7 @@ export function resetForTests(): void {
367
419
  cachedDb = null;
368
420
  openAttempted = false;
369
421
  lastSweepAt = 0;
422
+ authKeyMemo.clear();
370
423
  }
371
424
 
372
425
  // ────────────────────────────────────────────────────────────────────────────
@@ -467,6 +520,12 @@ function storeResult<T>(
467
520
  });
468
521
  }
469
522
 
523
+ /**
524
+ * In-flight background refreshes keyed by row identity. N concurrent stale
525
+ * reads of the same row must spawn one `gh` subprocess, not N identical ones.
526
+ */
527
+ const inflightRefreshes = new Set<string>();
528
+
470
529
  function scheduleBackgroundRefresh<T>(
471
530
  authKey: string,
472
531
  repo: string,
@@ -475,9 +534,11 @@ function scheduleBackgroundRefresh<T>(
475
534
  includeComments: boolean,
476
535
  fetchFresh: () => Promise<FreshResult<T>>,
477
536
  ): void {
537
+ const key = `${authKey}|${normalizeRepo(repo)}|${kind}|${number}|${includeComments ? 1 : 0}`;
538
+ if (inflightRefreshes.has(key)) return;
539
+ inflightRefreshes.add(key);
478
540
  queueMicrotask(() => {
479
- const promise = fetchFresh();
480
- promise
541
+ fetchFresh()
481
542
  .then(fresh => {
482
543
  storeResult(authKey, repo, kind, number, includeComments, fresh, Date.now());
483
544
  })
@@ -488,6 +549,9 @@ function scheduleBackgroundRefresh<T>(
488
549
  kind,
489
550
  number,
490
551
  });
552
+ })
553
+ .finally(() => {
554
+ inflightRefreshes.delete(key);
491
555
  });
492
556
  });
493
557
  }
@@ -1,20 +1,14 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import {
4
- type ApiKey,
5
- type FetchImpl,
6
- getAntigravityUserAgent,
7
- getEnvApiKey,
8
- type Model,
9
- withAuth,
10
- } from "@oh-my-pi/pi-ai";
3
+ import { type ApiKey, type FetchImpl, getEnvApiKey, type Model, withAuth } from "@oh-my-pi/pi-ai";
11
4
  import {
12
5
  CODEX_BASE_URL,
13
6
  getCodexAccountId,
14
7
  OPENAI_HEADER_VALUES,
15
8
  OPENAI_HEADERS,
16
9
  URL_PATHS,
17
- } from "@oh-my-pi/pi-ai/providers/openai-codex/constants";
10
+ } from "@oh-my-pi/pi-catalog/wire/codex";
11
+ import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
18
12
  import {
19
13
  $env,
20
14
  isEnoent,
@@ -478,8 +472,13 @@ function parseAntigravityCredentials(raw: string): ParsedAntigravityCredentials
478
472
  return null;
479
473
  }
480
474
 
481
- async function findAntigravityCredentials(modelRegistry: ModelRegistry): Promise<ImageApiKey | null> {
482
- const apiKey = await modelRegistry.getApiKeyForProvider("google-antigravity");
475
+ async function findAntigravityCredentials(
476
+ modelRegistry: ModelRegistry,
477
+ sessionId?: string,
478
+ ): Promise<ImageApiKey | null> {
479
+ const apiKey = await modelRegistry.getApiKeyForProvider("google-antigravity", sessionId, {
480
+ modelId: DEFAULT_ANTIGRAVITY_MODEL,
481
+ });
483
482
  if (!apiKey) return null;
484
483
 
485
484
  const parsed = parseAntigravityCredentials(apiKey);
@@ -529,7 +528,7 @@ async function findImageApiKey(
529
528
  if (openAI) return openAI;
530
529
  // Fall through to auto-detect if preferred provider key not found.
531
530
  } else if (preferredImageProvider === "antigravity" && modelRegistry) {
532
- const antigravity = await findAntigravityCredentials(modelRegistry);
531
+ const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
533
532
  if (antigravity) return antigravity;
534
533
  // Fall through to auto-detect if preferred provider key not found.
535
534
  } else if (preferredImageProvider === "gemini") {
@@ -553,7 +552,7 @@ async function findImageApiKey(
553
552
  if (openAI) return openAI;
554
553
 
555
554
  if (modelRegistry) {
556
- const antigravity = await findAntigravityCredentials(modelRegistry);
555
+ const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
557
556
  if (antigravity) return antigravity;
558
557
  }
559
558
 
@@ -1058,6 +1057,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1058
1057
  const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel.provider, {
1059
1058
  sessionId,
1060
1059
  baseUrl: hostedModel.baseUrl,
1060
+ modelId: hostedModel.id,
1061
1061
  });
1062
1062
 
1063
1063
  const parsed = await withAuth(
@@ -1119,6 +1119,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1119
1119
  const prompt = assemblePrompt(params);
1120
1120
  const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1121
1121
  sessionId,
1122
+ modelId: DEFAULT_ANTIGRAVITY_MODEL,
1122
1123
  });
1123
1124
 
1124
1125
  const response = await withAuth(
@@ -320,6 +320,13 @@ export interface ToolSession {
320
320
  * model for each file. Lazily initialized by `getDiagnosticsLedger`. */
321
321
  diagnosticsLedger?: import("../lsp/diagnostics-ledger").DiagnosticsLedger;
322
322
 
323
+ /** Per-session ledger of consecutive byte-identical no-op edits, keyed by
324
+ * canonical file path. The hashline executor escalates a soft no-op hint
325
+ * to a thrown error once the same payload no-ops `NOOP_HARD_LIMIT` times,
326
+ * breaking subagent loops that ignore the textual hint (issue #2081).
327
+ * Lazily initialized by `getNoopLoopGuard`. */
328
+ noopLoopGuard?: import("../edit/hashline/noop-loop-guard").NoopLoopGuard;
329
+
323
330
  /** Queue a hidden message to be injected at the next agent turn. */
324
331
  queueDeferredMessage?(message: CustomMessage): void;
325
332
  /** Queue late LSP diagnostics (arrived after an edit/write returned) to be shown
@@ -463,7 +470,12 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
463
470
  !allowJs &&
464
471
  (requestedTools === undefined || requestedTools.includes("eval"))
465
472
  ) {
466
- const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
473
+ const availability = await logger.time(
474
+ "createTools:pythonCheck",
475
+ checkPythonKernelAvailability,
476
+ session.cwd,
477
+ session.settings.get("python.interpreter")?.trim() || undefined,
478
+ );
467
479
  pythonAvailable = availability.ok;
468
480
  if (!availability.ok) {
469
481
  logger.warn("Python kernel unavailable and JS backend disabled; eval will be unavailable", {
@@ -141,6 +141,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
141
141
  apiKey: modelRegistry.resolver(model.provider, {
142
142
  sessionId: this.session.getSessionId?.() ?? undefined,
143
143
  baseUrl: model.baseUrl,
144
+ modelId: model.id,
144
145
  }),
145
146
  signal,
146
147
  },
package/src/tools/irc.ts CHANGED
@@ -244,11 +244,15 @@ function errorResult(text: string, details: IrcDetails): AgentToolResult<IrcDeta
244
244
  return {
245
245
  content: [{ type: "text", text }],
246
246
  details,
247
+ isError: true,
247
248
  };
248
249
  }
249
250
 
250
251
  function normalizeIrcTimeoutMs(value: number): number {
251
- if (!Number.isFinite(value) || value === 0) return value === 0 ? 0 : DEFAULT_IRC_TIMEOUT_MS;
252
+ if (value === 0) return 0; // 0 = timeout disabled
253
+ // Negative or non-finite settings are misconfigurations — fall back to the
254
+ // default instead of producing an instant 1 ms timeout.
255
+ if (!Number.isFinite(value) || value < 0) return DEFAULT_IRC_TIMEOUT_MS;
252
256
  return Math.max(1, Math.trunc(value));
253
257
  }
254
258
 
package/src/tools/job.ts CHANGED
@@ -454,7 +454,7 @@ export const jobToolRenderer = {
454
454
 
455
455
  let cached: RenderCache | undefined;
456
456
  return {
457
- render(width: number): string[] {
457
+ render(width: number): readonly string[] {
458
458
  const expanded = options.expanded;
459
459
  const spinnerFrame = options.spinnerFrame ?? 0;
460
460
  const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).digest();