@gajae-code/coding-agent 0.18.11 → 0.19.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 (462) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. package/src/web/search/providers/searxng.ts +62 -10
@@ -0,0 +1,742 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as nodeFs from "node:fs";
3
+ import * as fs from "node:fs/promises";
4
+ import * as path from "node:path";
5
+
6
+ const DEBOUNCE_QUIET_MS = 45;
7
+ const DEBOUNCE_MAX_MS = 300;
8
+ const WATCH_RECOVERY_INITIAL_MS = 100;
9
+ const WATCH_RECOVERY_MAX_MS = 5_000;
10
+ const APPLY_RETRY_DELAYS_MS = [100, 400] as const;
11
+ const TRANSIENT_APPLY_FAILURE_CODES = new Set(["EAGAIN", "EBUSY", "EINTR", "ETIMEDOUT", "ECONNRESET", "EAI_AGAIN"]);
12
+
13
+ export interface ConfigHotReloadPaths {
14
+ readonly configPath: string;
15
+ readonly modelsPath: string;
16
+ }
17
+
18
+ export interface ConfigHotReloadFileSnapshot {
19
+ readonly path: string;
20
+ readonly text: string | null;
21
+ readonly identity: string;
22
+ }
23
+
24
+ export interface ConfigHotReloadCandidate {
25
+ readonly revision: number;
26
+ readonly config: ConfigHotReloadFileSnapshot;
27
+ readonly models: ConfigHotReloadFileSnapshot;
28
+ }
29
+
30
+ export type ConfigHotReloadErrorOperation = "watch" | "read" | "validate" | "apply";
31
+
32
+ /** A path-free diagnostic for watcher, snapshot, and callback failures. */
33
+ export class ConfigHotReloadError extends Error {
34
+ readonly operation: ConfigHotReloadErrorOperation;
35
+ readonly code: string | undefined;
36
+
37
+ constructor(operation: ConfigHotReloadErrorOperation, code?: string) {
38
+ super(
39
+ code
40
+ ? `Configuration hot reload ${operation} failed (${code})`
41
+ : `Configuration hot reload ${operation} failed`,
42
+ );
43
+ this.name = "ConfigHotReloadError";
44
+ this.operation = operation;
45
+ this.code = code;
46
+ }
47
+ }
48
+
49
+ export interface ConfigHotReloadWatcherOptions {
50
+ /** Validate the snapshots without mutating live state before a candidate may supersede queued work. */
51
+ readonly onValidate: (candidate: ConfigHotReloadCandidate) => void | Promise<void>;
52
+ /** Apply these text snapshots, not a second read of their paths, and honor abort before commit. */
53
+ readonly onCandidate: (candidate: ConfigHotReloadCandidate, signal: AbortSignal) => void | Promise<void>;
54
+ readonly onError: (error: ConfigHotReloadError) => void;
55
+ }
56
+
57
+ interface FileSources {
58
+ readonly config: ConfigHotReloadFileSnapshot;
59
+ readonly models: ConfigHotReloadFileSnapshot;
60
+ }
61
+
62
+ interface Binding {
63
+ readonly generation: number;
64
+ readonly paths: ConfigHotReloadPaths;
65
+ readonly watchers: Map<string, nodeFs.FSWatcher>;
66
+ readonly watcherIdentities: Map<string, string>;
67
+ watcherRecoveryTimer: NodeJS.Timeout | undefined;
68
+ watcherRecoveryDelayMs: number;
69
+ watcherDirectoryDiscoveryFailed: boolean;
70
+ watchTargets: Set<string>;
71
+ applyRetryTimer: NodeJS.Timeout | undefined;
72
+ applyRetryCandidate: ConfigHotReloadCandidate | undefined;
73
+ initializing: boolean;
74
+ baselineIdentity: string | undefined;
75
+ initialReadFailed: boolean;
76
+ debounceStartedAt: number | undefined;
77
+ debounceTimer: NodeJS.Timeout | undefined;
78
+ scanning: boolean;
79
+ scanRequested: boolean;
80
+ }
81
+
82
+ interface PendingCandidate {
83
+ readonly binding: Binding;
84
+ readonly candidate: ConfigHotReloadCandidate;
85
+ readonly applyAttempt: number;
86
+ }
87
+
88
+ interface RunningCandidate extends PendingCandidate {
89
+ readonly controller: AbortController;
90
+ }
91
+
92
+ function failureCode(error: unknown): string | undefined {
93
+ if (!error || typeof error !== "object" || !("code" in error) || typeof error.code !== "string") return undefined;
94
+ return /^[A-Z0-9_]+$/.test(error.code) ? error.code : undefined;
95
+ }
96
+
97
+ function missingFile(error: unknown): boolean {
98
+ if (!error || typeof error !== "object" || !("code" in error) || typeof error.code !== "string") return false;
99
+ return error.code === "ENOENT" || error.code === "ENOTDIR";
100
+ }
101
+
102
+ function transientApplyFailure(error: unknown): boolean {
103
+ if (error && typeof error === "object" && "retryable" in error && error.retryable === true) return true;
104
+ const code = failureCode(error);
105
+ return code !== undefined && TRANSIENT_APPLY_FAILURE_CODES.has(code);
106
+ }
107
+
108
+ function identityFor(filePath: string, text: string | null): string {
109
+ const hash = createHash("sha256");
110
+ hash.update(filePath);
111
+ hash.update("\0");
112
+ hash.update(text === null ? "missing" : "present\0");
113
+ if (text !== null) hash.update(text);
114
+ return hash.digest("hex");
115
+ }
116
+
117
+ function pairIdentity(sources: FileSources): string {
118
+ return `${sources.config.identity}:${sources.models.identity}`;
119
+ }
120
+
121
+ function candidateIdentity(candidate: ConfigHotReloadCandidate): string {
122
+ return `${candidate.config.identity}:${candidate.models.identity}`;
123
+ }
124
+
125
+ function pathsIdentity(paths: ConfigHotReloadPaths): string {
126
+ return `${paths.configPath}\0${paths.modelsPath}`;
127
+ }
128
+
129
+ async function existingDirectory(filePath: string): Promise<string> {
130
+ let current = path.dirname(filePath);
131
+ for (;;) {
132
+ try {
133
+ const stat = await fs.stat(current);
134
+ if (stat.isDirectory()) return current;
135
+ } catch (error) {
136
+ if (!missingFile(error)) throw error;
137
+ }
138
+ const parent = path.dirname(current);
139
+ if (parent === current) return current;
140
+ current = parent;
141
+ }
142
+ }
143
+
144
+ async function existingDirectoryAncestors(filePath: string): Promise<Set<string>> {
145
+ let current = await existingDirectory(filePath);
146
+ const directories = new Set<string>();
147
+ for (;;) {
148
+ directories.add(current);
149
+ const parent = path.dirname(current);
150
+ if (parent === current) return directories;
151
+ current = parent;
152
+ }
153
+ }
154
+
155
+ async function canonicalWatchPath(filePath: string, visited = new Set<string>()): Promise<string> {
156
+ try {
157
+ return await fs.realpath(filePath);
158
+ } catch (error) {
159
+ if (!missingFile(error)) throw error;
160
+ }
161
+ if (visited.has(filePath)) return filePath;
162
+ visited.add(filePath);
163
+ try {
164
+ const stat = await fs.lstat(filePath);
165
+ if (stat.isSymbolicLink()) {
166
+ const target = await fs.readlink(filePath);
167
+ return canonicalWatchPath(path.resolve(path.dirname(filePath), target), visited);
168
+ }
169
+ } catch (error) {
170
+ if (!missingFile(error)) throw error;
171
+ }
172
+ const directory = await existingDirectory(filePath);
173
+ const canonicalDirectory = await fs.realpath(directory);
174
+ return path.join(canonicalDirectory, path.relative(directory, filePath));
175
+ }
176
+
177
+ function nextPathComponent(directory: string, filePath: string): string | undefined {
178
+ const relativePath = path.relative(directory, filePath);
179
+ if (
180
+ !relativePath ||
181
+ path.isAbsolute(relativePath) ||
182
+ relativePath === ".." ||
183
+ relativePath.startsWith(`..${path.sep}`)
184
+ ) {
185
+ return undefined;
186
+ }
187
+ return relativePath.split(path.sep, 1)[0];
188
+ }
189
+
190
+ function normalizePaths(paths: ConfigHotReloadPaths): ConfigHotReloadPaths {
191
+ if (!path.isAbsolute(paths.configPath) || !path.isAbsolute(paths.modelsPath)) {
192
+ throw new TypeError("Configuration hot reload paths must be absolute");
193
+ }
194
+ return Object.freeze({
195
+ configPath: path.resolve(paths.configPath),
196
+ modelsPath: path.resolve(paths.modelsPath),
197
+ });
198
+ }
199
+
200
+ /** Watches resolved config and models files and delivers immutable source snapshots. */
201
+ export class ConfigHotReloadWatcher {
202
+ readonly #onCandidate: ConfigHotReloadWatcherOptions["onCandidate"];
203
+ readonly #onValidate: ConfigHotReloadWatcherOptions["onValidate"];
204
+ readonly #onError: ConfigHotReloadWatcherOptions["onError"];
205
+ readonly #reportedErrors = new Set<string>();
206
+ #binding: Binding | undefined;
207
+ #pending: PendingCandidate | undefined;
208
+ #running: RunningCandidate | undefined;
209
+ #revision = 0;
210
+ #highestQueuedRevision = 0;
211
+ #generation = 0;
212
+ #started = false;
213
+ #disposed = false;
214
+
215
+ constructor(options: ConfigHotReloadWatcherOptions) {
216
+ this.#onCandidate = options.onCandidate;
217
+ this.#onValidate = options.onValidate;
218
+ this.#onError = options.onError;
219
+ }
220
+
221
+ /** Establishes the initial fingerprint baseline without emitting a candidate. */
222
+ async start(paths: ConfigHotReloadPaths): Promise<void> {
223
+ if (this.#disposed) throw new Error("Configuration hot reload watcher is disposed");
224
+ if (this.#started) throw new Error("Configuration hot reload watcher has already started");
225
+ const normalizedPaths = normalizePaths(paths);
226
+ this.#started = true;
227
+ const binding = this.#newBinding(normalizedPaths);
228
+ this.#binding = binding;
229
+ await this.#initializeBinding(binding);
230
+ }
231
+
232
+ /** Rebinds after session or configuration-root changes, fencing old work. */
233
+ async rebind(paths: ConfigHotReloadPaths): Promise<void> {
234
+ if (!this.#started) throw new Error("Configuration hot reload watcher has not started");
235
+ if (this.#disposed) throw new Error("Configuration hot reload watcher is disposed");
236
+ const normalizedPaths = normalizePaths(paths);
237
+ this.#retireBinding(this.#binding);
238
+ this.#pending = undefined;
239
+ this.#running?.controller.abort();
240
+ const binding = this.#newBinding(normalizedPaths);
241
+ this.#binding = binding;
242
+ await this.#initializeBinding(binding);
243
+ }
244
+
245
+ /**
246
+ * Test-only: request the same scan an fs event would schedule. Tests that
247
+ * exercise the watcher cannot rely on platform fs-event delivery timing.
248
+ */
249
+ __testRequestScan(): void {
250
+ if (this.#binding && this.#isCurrent(this.#binding)) this.#scheduleScan(this.#binding);
251
+ }
252
+
253
+ /** Closes all watches and cancels or fences pending and in-flight callbacks. */
254
+ dispose(): void {
255
+ if (this.#disposed) return;
256
+ this.#disposed = true;
257
+ this.#generation += 1;
258
+ this.#pending = undefined;
259
+ this.#running?.controller.abort();
260
+ this.#retireBinding(this.#binding);
261
+ this.#binding = undefined;
262
+ }
263
+
264
+ #newBinding(paths: ConfigHotReloadPaths): Binding {
265
+ return {
266
+ generation: ++this.#generation,
267
+ paths,
268
+ watchers: new Map(),
269
+ watcherIdentities: new Map(),
270
+ watcherRecoveryTimer: undefined,
271
+ watcherRecoveryDelayMs: WATCH_RECOVERY_INITIAL_MS,
272
+ watcherDirectoryDiscoveryFailed: false,
273
+ watchTargets: new Set([paths.configPath, paths.modelsPath]),
274
+ applyRetryTimer: undefined,
275
+ applyRetryCandidate: undefined,
276
+ initializing: true,
277
+ baselineIdentity: undefined,
278
+ initialReadFailed: false,
279
+ debounceStartedAt: undefined,
280
+ debounceTimer: undefined,
281
+ scanning: false,
282
+ scanRequested: false,
283
+ };
284
+ }
285
+
286
+ async #initializeBinding(binding: Binding): Promise<void> {
287
+ let before: FileSources | undefined;
288
+ try {
289
+ before = await this.#readSources(binding);
290
+ } catch {
291
+ if (this.#isCurrent(binding)) {
292
+ binding.initialReadFailed = true;
293
+ }
294
+ }
295
+ if (!this.#isCurrent(binding)) return;
296
+ await this.#refreshWatchers(binding);
297
+ if (!this.#isCurrent(binding)) return;
298
+
299
+ let after: FileSources | undefined;
300
+ try {
301
+ after = await this.#readSources(binding);
302
+ } catch {}
303
+ if (!this.#isCurrent(binding)) return;
304
+
305
+ if (before) {
306
+ binding.baselineIdentity = pairIdentity(before);
307
+ if (after && pairIdentity(after) !== binding.baselineIdentity) {
308
+ binding.baselineIdentity = pairIdentity(after);
309
+ this.#validateCandidate(binding, this.#candidate(after));
310
+ }
311
+ } else if (after) {
312
+ binding.baselineIdentity = pairIdentity(after);
313
+ binding.initialReadFailed = false;
314
+ this.#validateCandidate(binding, this.#candidate(after));
315
+ }
316
+
317
+ binding.initializing = false;
318
+ if (binding.scanRequested) void this.#scan(binding);
319
+ this.#pump();
320
+ }
321
+
322
+ #isCurrent(binding: Binding): boolean {
323
+ return !this.#disposed && this.#binding === binding && this.#generation === binding.generation;
324
+ }
325
+
326
+ #retireBinding(binding: Binding | undefined): void {
327
+ if (!binding) return;
328
+ if (binding.debounceTimer) clearTimeout(binding.debounceTimer);
329
+ binding.debounceTimer = undefined;
330
+ if (binding.watcherRecoveryTimer) clearTimeout(binding.watcherRecoveryTimer);
331
+ binding.watcherRecoveryTimer = undefined;
332
+ this.#cancelApplyRetry(binding);
333
+ for (const watcher of binding.watchers.values()) watcher.close();
334
+ binding.watchers.clear();
335
+ binding.watcherIdentities.clear();
336
+ binding.watchTargets.clear();
337
+ }
338
+
339
+ async #readSources(binding: Binding): Promise<FileSources> {
340
+ const read = async (filePath: string): Promise<ConfigHotReloadFileSnapshot> => {
341
+ let text: string | null;
342
+ try {
343
+ text = await Bun.file(filePath).text();
344
+ } catch (error) {
345
+ if (missingFile(error)) {
346
+ text = null;
347
+ } else {
348
+ if (this.#isCurrent(binding)) this.#report("read", error, filePath);
349
+ throw error;
350
+ }
351
+ }
352
+ this.#clearErrors("read", filePath);
353
+ return Object.freeze({ path: filePath, text, identity: identityFor(filePath, text) });
354
+ };
355
+ const [config, models] = await Promise.all([read(binding.paths.configPath), read(binding.paths.modelsPath)]);
356
+ return Object.freeze({ config, models });
357
+ }
358
+
359
+ async #refreshWatchers(binding: Binding): Promise<void> {
360
+ if (!this.#isCurrent(binding)) return;
361
+ let desired = await this.#desiredWatchDirectories(binding);
362
+ if (!this.#isCurrent(binding)) return;
363
+ const attempted = new Set<string>();
364
+ for (let attempt = 0; attempt < 3; attempt += 1) {
365
+ for (const directory of desired.keys()) await this.#openWatcher(binding, directory, attempted);
366
+ const verified = await this.#desiredWatchDirectories(binding);
367
+ if (!this.#isCurrent(binding)) return;
368
+ const stable =
369
+ desired.size === verified.size && [...desired.keys()].every(directory => verified.has(directory));
370
+ if (stable) {
371
+ desired = verified;
372
+ break;
373
+ }
374
+ desired = verified;
375
+ }
376
+ for (const directory of desired.keys()) await this.#openWatcher(binding, directory, attempted);
377
+ for (const [directory, watcher] of binding.watchers) {
378
+ if (desired.has(directory)) continue;
379
+ watcher.close();
380
+ binding.watchers.delete(directory);
381
+ binding.watcherIdentities.delete(directory);
382
+ }
383
+ if (
384
+ binding.watcherDirectoryDiscoveryFailed ||
385
+ desired.size === 0 ||
386
+ [...desired.keys()].some(directory => !binding.watchers.has(directory))
387
+ ) {
388
+ this.#scheduleWatcherRecovery(binding);
389
+ } else {
390
+ this.#clearWatcherRecovery(binding);
391
+ }
392
+ }
393
+
394
+ async #desiredWatchDirectories(binding: Binding): Promise<Map<string, Set<string>>> {
395
+ let lookupFailed = false;
396
+ const desired = new Map<string, Set<string>>();
397
+ const watchTargets = new Set<string>();
398
+ const addTarget = async (filePath: string): Promise<void> => {
399
+ const targetPaths = new Set([filePath]);
400
+ try {
401
+ targetPaths.add(await canonicalWatchPath(filePath));
402
+ } catch (error) {
403
+ if (this.#isCurrent(binding)) {
404
+ lookupFailed = true;
405
+ this.#report("watch", error, filePath);
406
+ }
407
+ }
408
+ for (const targetPath of targetPaths) {
409
+ watchTargets.add(targetPath);
410
+ let directories: Set<string>;
411
+ try {
412
+ directories = await existingDirectoryAncestors(targetPath);
413
+ } catch (error) {
414
+ if (this.#isCurrent(binding)) {
415
+ lookupFailed = true;
416
+ this.#report("watch", error, targetPath);
417
+ }
418
+ continue;
419
+ }
420
+ for (const directory of directories) {
421
+ const component = nextPathComponent(directory, targetPath);
422
+ if (!component) continue;
423
+ const components = desired.get(directory) ?? new Set<string>();
424
+ components.add(component);
425
+ desired.set(directory, components);
426
+ }
427
+ }
428
+ };
429
+ await Promise.all([addTarget(binding.paths.configPath), addTarget(binding.paths.modelsPath)]);
430
+ if (!this.#isCurrent(binding)) return desired;
431
+ binding.watchTargets = watchTargets;
432
+ if (this.#isCurrent(binding)) {
433
+ binding.watcherDirectoryDiscoveryFailed = lookupFailed;
434
+ if (lookupFailed) this.#scheduleWatcherRecovery(binding);
435
+ }
436
+ return desired;
437
+ }
438
+
439
+ #candidate(sources: FileSources): ConfigHotReloadCandidate {
440
+ return Object.freeze({
441
+ revision: ++this.#revision,
442
+ config: sources.config,
443
+ models: sources.models,
444
+ });
445
+ }
446
+
447
+ async #openWatcher(binding: Binding, directory: string, attempted: Set<string>): Promise<void> {
448
+ if (attempted.has(directory)) return;
449
+ try {
450
+ const stat = await fs.stat(directory);
451
+ if (!this.#isCurrent(binding)) return;
452
+ const identity = `${stat.dev}:${stat.ino}`;
453
+ if (binding.watchers.has(directory)) {
454
+ if (binding.watcherIdentities.get(directory) === identity) return;
455
+ binding.watchers.get(directory)!.close();
456
+ binding.watchers.delete(directory);
457
+ binding.watcherIdentities.delete(directory);
458
+ }
459
+ const watcher = nodeFs.watch(directory, (eventType, filename) => {
460
+ if (!this.#isCurrent(binding) || binding.watchers.get(directory) !== watcher) return;
461
+ if (filename === null) {
462
+ this.#invalidateDescendantWatchers(binding, directory);
463
+ this.#scheduleScan(binding);
464
+ return;
465
+ }
466
+ const name = filename.toString();
467
+ if (eventType === "rename" && name === path.basename(directory)) {
468
+ binding.watchers.delete(directory);
469
+ binding.watcherIdentities.delete(directory);
470
+ watcher.close();
471
+ this.#invalidateDescendantWatchers(binding, directory);
472
+ this.#scheduleScan(binding);
473
+ return;
474
+ }
475
+ if (!this.#matchesWatchedPath(binding, directory, name)) return;
476
+ if (eventType === "rename") {
477
+ this.#invalidateDescendantWatchers(binding, path.join(directory, name), true);
478
+ }
479
+ this.#scheduleScan(binding);
480
+ });
481
+ watcher.unref?.();
482
+ watcher.on("error", error => {
483
+ if (!this.#isCurrent(binding) || binding.watchers.get(directory) !== watcher) return;
484
+ binding.watchers.delete(directory);
485
+ binding.watcherIdentities.delete(directory);
486
+ watcher.close();
487
+ this.#report("watch", error, directory);
488
+ this.#scheduleWatcherRecovery(binding);
489
+ });
490
+ binding.watchers.set(directory, watcher);
491
+ binding.watcherIdentities.set(directory, identity);
492
+ this.#clearErrors("watch", directory);
493
+ } catch (error) {
494
+ if (!this.#isCurrent(binding)) return;
495
+ attempted.add(directory);
496
+ this.#report("watch", error, directory);
497
+ this.#scheduleWatcherRecovery(binding);
498
+ }
499
+ }
500
+
501
+ #scheduleWatcherRecovery(binding: Binding): void {
502
+ if (!this.#isCurrent(binding) || binding.watcherRecoveryTimer) return;
503
+ const delay = binding.watcherRecoveryDelayMs;
504
+ binding.watcherRecoveryDelayMs = Math.min(delay * 2, WATCH_RECOVERY_MAX_MS);
505
+ binding.watcherRecoveryTimer = setTimeout(() => {
506
+ binding.watcherRecoveryTimer = undefined;
507
+ if (this.#isCurrent(binding)) void this.#scan(binding);
508
+ }, delay);
509
+ binding.watcherRecoveryTimer.unref?.();
510
+ }
511
+
512
+ #clearWatcherRecovery(binding: Binding): void {
513
+ if (binding.watcherRecoveryTimer) clearTimeout(binding.watcherRecoveryTimer);
514
+ binding.watcherRecoveryTimer = undefined;
515
+ binding.watcherRecoveryDelayMs = WATCH_RECOVERY_INITIAL_MS;
516
+ }
517
+
518
+ #cancelApplyRetry(binding: Binding): void {
519
+ if (binding.applyRetryTimer) clearTimeout(binding.applyRetryTimer);
520
+ binding.applyRetryTimer = undefined;
521
+ binding.applyRetryCandidate = undefined;
522
+ }
523
+
524
+ #matchesWatchedPath(binding: Binding, directory: string, filename: string): boolean {
525
+ for (const filePath of binding.watchTargets) {
526
+ if (nextPathComponent(directory, filePath) === filename) return true;
527
+ }
528
+ return false;
529
+ }
530
+
531
+ #invalidateDescendantWatchers(binding: Binding, directory: string, includeDirectory = false): void {
532
+ for (const [watchedDirectory, watcher] of binding.watchers) {
533
+ if (watchedDirectory !== directory && !nextPathComponent(directory, watchedDirectory)) continue;
534
+ if (watchedDirectory === directory && !includeDirectory) continue;
535
+ watcher.close();
536
+ binding.watchers.delete(watchedDirectory);
537
+ binding.watcherIdentities.delete(watchedDirectory);
538
+ }
539
+ }
540
+
541
+ #scheduleScan(binding: Binding): void {
542
+ if (!this.#isCurrent(binding)) return;
543
+ const now = performance.now();
544
+ binding.debounceStartedAt ??= now;
545
+ if (binding.debounceTimer) clearTimeout(binding.debounceTimer);
546
+ const deadline = Math.min(now + DEBOUNCE_QUIET_MS, binding.debounceStartedAt + DEBOUNCE_MAX_MS);
547
+ binding.debounceTimer = setTimeout(
548
+ () => {
549
+ binding.debounceTimer = undefined;
550
+ binding.debounceStartedAt = undefined;
551
+ void this.#scan(binding);
552
+ },
553
+ Math.max(0, deadline - now),
554
+ );
555
+ binding.debounceTimer.unref?.();
556
+ }
557
+
558
+ async #scan(binding: Binding): Promise<void> {
559
+ if (!this.#isCurrent(binding)) return;
560
+ if (binding.initializing || binding.scanning) {
561
+ binding.scanRequested = true;
562
+ return;
563
+ }
564
+ binding.scanning = true;
565
+ try {
566
+ do {
567
+ binding.scanRequested = false;
568
+ await this.#refreshWatchers(binding);
569
+ if (!this.#isCurrent(binding)) return;
570
+ let sources: FileSources;
571
+ try {
572
+ sources = await this.#readSources(binding);
573
+ } catch {
574
+ if (!this.#isCurrent(binding)) return;
575
+ continue;
576
+ }
577
+ if (!this.#isCurrent(binding)) return;
578
+ const identity = pairIdentity(sources);
579
+ if (binding.applyRetryCandidate && candidateIdentity(binding.applyRetryCandidate) !== identity) {
580
+ this.#cancelApplyRetry(binding);
581
+ }
582
+ const recoveringInitialRead = binding.baselineIdentity === undefined && binding.initialReadFailed;
583
+ if (binding.baselineIdentity === undefined) {
584
+ binding.baselineIdentity = identity;
585
+ if (!recoveringInitialRead) continue;
586
+ binding.initialReadFailed = false;
587
+ }
588
+ if (!recoveringInitialRead && binding.baselineIdentity === identity) continue;
589
+ binding.baselineIdentity = identity;
590
+ this.#validateCandidate(binding, this.#candidate(sources));
591
+ } while (binding.scanRequested && this.#isCurrent(binding));
592
+ } finally {
593
+ binding.scanning = false;
594
+ if (binding.scanRequested && this.#isCurrent(binding)) void this.#scan(binding);
595
+ }
596
+ }
597
+
598
+ #validateCandidate(binding: Binding, candidate: ConfigHotReloadCandidate): void {
599
+ const resource = pathsIdentity(binding.paths);
600
+ void Promise.resolve()
601
+ .then(() => {
602
+ if (!this.#isCurrent(binding)) return;
603
+ return this.#onValidate(candidate);
604
+ })
605
+ .then(() => {
606
+ if (!this.#isCurrent(binding)) return;
607
+ this.#clearErrors("validate", resource);
608
+ this.#queueCandidate(binding, candidate);
609
+ })
610
+ .catch(error => {
611
+ if (this.#isCurrent(binding)) this.#report("validate", error, resource);
612
+ });
613
+ }
614
+
615
+ #queueCandidate(binding: Binding, candidate: ConfigHotReloadCandidate): void {
616
+ const pendingRevision = this.#pending?.binding === binding ? this.#pending.candidate.revision : -1;
617
+ const runningRevision = this.#running?.binding === binding ? this.#running.candidate.revision : -1;
618
+ if (candidate.revision <= Math.max(this.#highestQueuedRevision, pendingRevision, runningRevision)) return;
619
+ if (binding.applyRetryCandidate && candidate.revision > binding.applyRetryCandidate.revision) {
620
+ this.#cancelApplyRetry(binding);
621
+ }
622
+ this.#highestQueuedRevision = candidate.revision;
623
+ this.#pending = { binding, candidate, applyAttempt: 0 };
624
+ const running = this.#running;
625
+ if (running && (running.binding !== binding || running.candidate.revision < candidate.revision)) {
626
+ running.controller.abort();
627
+ }
628
+ this.#pump();
629
+ }
630
+
631
+ #pump(): void {
632
+ if (this.#disposed || this.#running || !this.#pending || this.#pending.binding.initializing) {
633
+ return;
634
+ }
635
+ const pending = this.#pending;
636
+ this.#pending = undefined;
637
+ if (!this.#isCurrent(pending.binding)) return;
638
+ const running: RunningCandidate = { ...pending, controller: new AbortController() };
639
+ this.#running = running;
640
+ let succeeded = false;
641
+ void Promise.resolve()
642
+ .then(async () => {
643
+ if (!this.#isCurrent(running.binding) || running.controller.signal.aborted) return;
644
+ await this.#onCandidate(running.candidate, running.controller.signal);
645
+ succeeded = true;
646
+ })
647
+ .catch(error => {
648
+ if (!running.controller.signal.aborted && this.#isCurrent(running.binding)) {
649
+ this.#report("apply", error, pathsIdentity(running.binding.paths));
650
+ if (transientApplyFailure(error)) this.#scheduleApplyRetry(running);
651
+ }
652
+ })
653
+ .finally(() => {
654
+ if (this.#running === running) this.#running = undefined;
655
+ if (succeeded && !running.controller.signal.aborted && this.#isCurrent(running.binding)) {
656
+ this.#clearErrors("apply", pathsIdentity(running.binding.paths));
657
+ }
658
+ this.#pump();
659
+ });
660
+ }
661
+
662
+ #scheduleApplyRetry(running: RunningCandidate): void {
663
+ const { binding, candidate } = running;
664
+ const delay = APPLY_RETRY_DELAYS_MS[running.applyAttempt];
665
+ if (
666
+ delay === undefined ||
667
+ !this.#isCurrent(binding) ||
668
+ running.controller.signal.aborted ||
669
+ binding.baselineIdentity !== candidateIdentity(candidate) ||
670
+ this.#highestQueuedRevision > candidate.revision
671
+ ) {
672
+ return;
673
+ }
674
+ this.#cancelApplyRetry(binding);
675
+ const nextAttempt = running.applyAttempt + 1;
676
+ binding.applyRetryCandidate = candidate;
677
+ binding.applyRetryTimer = setTimeout(() => {
678
+ binding.applyRetryTimer = undefined;
679
+ void this.#queueApplyRetry(binding, candidate, nextAttempt);
680
+ }, delay);
681
+ binding.applyRetryTimer.unref?.();
682
+ }
683
+
684
+ async #queueApplyRetry(binding: Binding, candidate: ConfigHotReloadCandidate, applyAttempt: number): Promise<void> {
685
+ if (
686
+ !this.#isCurrent(binding) ||
687
+ binding.applyRetryCandidate !== candidate ||
688
+ binding.baselineIdentity !== candidateIdentity(candidate) ||
689
+ this.#highestQueuedRevision > candidate.revision
690
+ ) {
691
+ this.#cancelApplyRetry(binding);
692
+ return;
693
+ }
694
+ let sources: FileSources;
695
+ try {
696
+ sources = await this.#readSources(binding);
697
+ } catch {
698
+ this.#cancelApplyRetry(binding);
699
+ if (this.#isCurrent(binding)) this.#scheduleScan(binding);
700
+ return;
701
+ }
702
+ if (
703
+ !this.#isCurrent(binding) ||
704
+ binding.applyRetryCandidate !== candidate ||
705
+ binding.baselineIdentity !== candidateIdentity(candidate) ||
706
+ pairIdentity(sources) !== candidateIdentity(candidate) ||
707
+ this.#highestQueuedRevision > candidate.revision
708
+ ) {
709
+ this.#cancelApplyRetry(binding);
710
+ if (this.#isCurrent(binding)) this.#scheduleScan(binding);
711
+ return;
712
+ }
713
+ this.#cancelApplyRetry(binding);
714
+ this.#pending = { binding, candidate, applyAttempt };
715
+ this.#pump();
716
+ }
717
+
718
+ #errorKey(operation: ConfigHotReloadErrorOperation, resource: string | undefined, code: string | undefined): string {
719
+ const resourceIdentity = createHash("sha256")
720
+ .update(resource ?? "global")
721
+ .digest("hex");
722
+ return `${operation}:${resourceIdentity}:${code ?? "unknown"}`;
723
+ }
724
+
725
+ #clearErrors(operation: ConfigHotReloadErrorOperation, resource: string): void {
726
+ const prefix = this.#errorKey(operation, resource, undefined).slice(0, -"unknown".length);
727
+ for (const key of this.#reportedErrors) {
728
+ if (key.startsWith(prefix)) this.#reportedErrors.delete(key);
729
+ }
730
+ }
731
+
732
+ #report(operation: ConfigHotReloadErrorOperation, error: unknown, resource?: string): void {
733
+ const key = this.#errorKey(operation, resource, failureCode(error));
734
+ if (this.#reportedErrors.has(key)) return;
735
+ this.#reportedErrors.add(key);
736
+ try {
737
+ this.#onError(new ConfigHotReloadError(operation, failureCode(error)));
738
+ } catch {
739
+ // Diagnostics must not destabilize the watcher.
740
+ }
741
+ }
742
+ }