@aitne/daemon 0.1.3 → 0.1.6

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 (421) hide show
  1. package/dist/adapters/notification-manager.d.ts +12 -0
  2. package/dist/adapters/notification-manager.d.ts.map +1 -1
  3. package/dist/adapters/notification-manager.js +39 -1
  4. package/dist/adapters/notification-manager.js.map +1 -1
  5. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  6. package/dist/adapters/whatsapp-adapter.js +0 -1
  7. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  8. package/dist/api/integration-route-gate.d.ts +15 -11
  9. package/dist/api/integration-route-gate.d.ts.map +1 -1
  10. package/dist/api/integration-route-gate.js +60 -23
  11. package/dist/api/integration-route-gate.js.map +1 -1
  12. package/dist/api/json-body.d.ts +22 -7
  13. package/dist/api/json-body.d.ts.map +1 -1
  14. package/dist/api/json-body.js +27 -8
  15. package/dist/api/json-body.js.map +1 -1
  16. package/dist/api/routes/agent.d.ts.map +1 -1
  17. package/dist/api/routes/agent.js +25 -0
  18. package/dist/api/routes/agent.js.map +1 -1
  19. package/dist/api/routes/backends.d.ts.map +1 -1
  20. package/dist/api/routes/backends.js +96 -1
  21. package/dist/api/routes/backends.js.map +1 -1
  22. package/dist/api/routes/books.js +1 -1
  23. package/dist/api/routes/books.js.map +1 -1
  24. package/dist/api/routes/commands.d.ts.map +1 -1
  25. package/dist/api/routes/commands.js +16 -13
  26. package/dist/api/routes/commands.js.map +1 -1
  27. package/dist/api/routes/context.d.ts.map +1 -1
  28. package/dist/api/routes/context.js +26 -3
  29. package/dist/api/routes/context.js.map +1 -1
  30. package/dist/api/routes/dashboard.d.ts.map +1 -1
  31. package/dist/api/routes/dashboard.js +103 -5
  32. package/dist/api/routes/dashboard.js.map +1 -1
  33. package/dist/api/routes/fs.d.ts +23 -0
  34. package/dist/api/routes/fs.d.ts.map +1 -0
  35. package/dist/api/routes/fs.js +156 -0
  36. package/dist/api/routes/fs.js.map +1 -0
  37. package/dist/api/routes/fs.logic.d.ts +62 -0
  38. package/dist/api/routes/fs.logic.d.ts.map +1 -0
  39. package/dist/api/routes/fs.logic.js +137 -0
  40. package/dist/api/routes/fs.logic.js.map +1 -0
  41. package/dist/api/routes/github.d.ts.map +1 -1
  42. package/dist/api/routes/github.js +38 -5
  43. package/dist/api/routes/github.js.map +1 -1
  44. package/dist/api/routes/health.d.ts.map +1 -1
  45. package/dist/api/routes/health.js +4 -2
  46. package/dist/api/routes/health.js.map +1 -1
  47. package/dist/api/routes/integrations.d.ts +35 -6
  48. package/dist/api/routes/integrations.d.ts.map +1 -1
  49. package/dist/api/routes/integrations.js +192 -15
  50. package/dist/api/routes/integrations.js.map +1 -1
  51. package/dist/api/routes/mail.d.ts.map +1 -1
  52. package/dist/api/routes/mail.js +112 -46
  53. package/dist/api/routes/mail.js.map +1 -1
  54. package/dist/api/routes/metrics.d.ts +1 -0
  55. package/dist/api/routes/metrics.d.ts.map +1 -1
  56. package/dist/api/routes/metrics.js +24 -0
  57. package/dist/api/routes/metrics.js.map +1 -1
  58. package/dist/api/routes/observations.d.ts.map +1 -1
  59. package/dist/api/routes/observations.js +696 -30
  60. package/dist/api/routes/observations.js.map +1 -1
  61. package/dist/api/routes/setup-migrate.d.ts +9 -1
  62. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  63. package/dist/api/routes/setup-migrate.js +4 -2
  64. package/dist/api/routes/setup-migrate.js.map +1 -1
  65. package/dist/api/routes/skills.d.ts +9 -1
  66. package/dist/api/routes/skills.d.ts.map +1 -1
  67. package/dist/api/routes/skills.js +77 -17
  68. package/dist/api/routes/skills.js.map +1 -1
  69. package/dist/api/routes/voice.d.ts.map +1 -1
  70. package/dist/api/routes/voice.js +62 -4
  71. package/dist/api/routes/voice.js.map +1 -1
  72. package/dist/api/routes/wiki.d.ts +4 -0
  73. package/dist/api/routes/wiki.d.ts.map +1 -0
  74. package/dist/api/routes/wiki.js +1075 -0
  75. package/dist/api/routes/wiki.js.map +1 -0
  76. package/dist/api/server.d.ts +13 -0
  77. package/dist/api/server.d.ts.map +1 -1
  78. package/dist/api/server.js +27 -1
  79. package/dist/api/server.js.map +1 -1
  80. package/dist/bootstrap/adapters.d.ts +109 -0
  81. package/dist/bootstrap/adapters.d.ts.map +1 -0
  82. package/dist/bootstrap/adapters.js +237 -0
  83. package/dist/bootstrap/adapters.js.map +1 -0
  84. package/dist/bootstrap/catchup.d.ts +23 -0
  85. package/dist/bootstrap/catchup.d.ts.map +1 -0
  86. package/dist/bootstrap/catchup.js +124 -0
  87. package/dist/bootstrap/catchup.js.map +1 -0
  88. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  89. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  90. package/dist/bootstrap/schedule-helpers.js +96 -0
  91. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  92. package/dist/bootstrap/services.d.ts +60 -0
  93. package/dist/bootstrap/services.d.ts.map +1 -0
  94. package/dist/bootstrap/services.js +209 -0
  95. package/dist/bootstrap/services.js.map +1 -0
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +26 -0
  98. package/dist/config.js.map +1 -1
  99. package/dist/core/agent-core.d.ts +25 -0
  100. package/dist/core/agent-core.d.ts.map +1 -1
  101. package/dist/core/agent-core.js.map +1 -1
  102. package/dist/core/backends/backend-router.d.ts +28 -1
  103. package/dist/core/backends/backend-router.d.ts.map +1 -1
  104. package/dist/core/backends/backend-router.js +58 -4
  105. package/dist/core/backends/backend-router.js.map +1 -1
  106. package/dist/core/backends/claude-auth.d.ts +70 -0
  107. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  108. package/dist/core/backends/claude-auth.js +198 -0
  109. package/dist/core/backends/claude-auth.js.map +1 -0
  110. package/dist/core/backends/claude-code-core.d.ts +47 -119
  111. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  112. package/dist/core/backends/claude-code-core.js +166 -1561
  113. package/dist/core/backends/claude-code-core.js.map +1 -1
  114. package/dist/core/backends/claude-delegated.d.ts +86 -0
  115. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  116. package/dist/core/backends/claude-delegated.js +801 -0
  117. package/dist/core/backends/claude-delegated.js.map +1 -0
  118. package/dist/core/backends/claude-errors.d.ts +39 -0
  119. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  120. package/dist/core/backends/claude-errors.js +71 -0
  121. package/dist/core/backends/claude-errors.js.map +1 -0
  122. package/dist/core/backends/claude-probe.d.ts +103 -0
  123. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  124. package/dist/core/backends/claude-probe.js +336 -0
  125. package/dist/core/backends/claude-probe.js.map +1 -0
  126. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  127. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  128. package/dist/core/backends/claude-tool-collection.js +1093 -0
  129. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  130. package/dist/core/backends/codex-core.d.ts.map +1 -1
  131. package/dist/core/backends/codex-core.js +36 -0
  132. package/dist/core/backends/codex-core.js.map +1 -1
  133. package/dist/core/backends/gemini-cli-core.d.ts +45 -5
  134. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  135. package/dist/core/backends/gemini-cli-core.js +146 -36
  136. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  137. package/dist/core/backends/plan-presets.d.ts +3 -1
  138. package/dist/core/backends/plan-presets.d.ts.map +1 -1
  139. package/dist/core/backends/plan-presets.js +42 -2
  140. package/dist/core/backends/plan-presets.js.map +1 -1
  141. package/dist/core/backends/prompt-utils.d.ts +1 -0
  142. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  143. package/dist/core/backends/prompt-utils.js +60 -3
  144. package/dist/core/backends/prompt-utils.js.map +1 -1
  145. package/dist/core/bang-commands/commands-help.d.ts +5 -0
  146. package/dist/core/bang-commands/commands-help.d.ts.map +1 -0
  147. package/dist/core/bang-commands/commands-help.js +69 -0
  148. package/dist/core/bang-commands/commands-help.js.map +1 -0
  149. package/dist/core/bang-commands/commands-wiki.d.ts +75 -0
  150. package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -0
  151. package/dist/core/bang-commands/commands-wiki.js +574 -0
  152. package/dist/core/bang-commands/commands-wiki.js.map +1 -0
  153. package/dist/core/bang-commands/index.d.ts +4 -2
  154. package/dist/core/bang-commands/index.d.ts.map +1 -1
  155. package/dist/core/bang-commands/index.js +15 -1
  156. package/dist/core/bang-commands/index.js.map +1 -1
  157. package/dist/core/bang-commands/registry.d.ts +47 -4
  158. package/dist/core/bang-commands/registry.d.ts.map +1 -1
  159. package/dist/core/bang-commands/registry.js +85 -15
  160. package/dist/core/bang-commands/registry.js.map +1 -1
  161. package/dist/core/context-builder.d.ts +53 -12
  162. package/dist/core/context-builder.d.ts.map +1 -1
  163. package/dist/core/context-builder.js +240 -92
  164. package/dist/core/context-builder.js.map +1 -1
  165. package/dist/core/daemon-api-cli.d.ts.map +1 -1
  166. package/dist/core/daemon-api-cli.js +50 -2
  167. package/dist/core/daemon-api-cli.js.map +1 -1
  168. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  169. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  170. package/dist/core/dispatcher-date-utils.js +132 -0
  171. package/dist/core/dispatcher-date-utils.js.map +1 -0
  172. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  173. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  174. package/dist/core/dispatcher-error-handling.js +393 -0
  175. package/dist/core/dispatcher-error-handling.js.map +1 -0
  176. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  177. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  178. package/dist/core/dispatcher-hourly-check.js +665 -0
  179. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  180. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  181. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  182. package/dist/core/dispatcher-message-handler.js +1064 -0
  183. package/dist/core/dispatcher-message-handler.js.map +1 -0
  184. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  185. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  186. package/dist/core/dispatcher-morning-routine.js +449 -0
  187. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  188. package/dist/core/dispatcher-prompt.d.ts +107 -0
  189. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  190. package/dist/core/dispatcher-prompt.js +227 -0
  191. package/dist/core/dispatcher-prompt.js.map +1 -0
  192. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  193. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  194. package/dist/core/dispatcher-repository-helpers.js +86 -0
  195. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  196. package/dist/core/dispatcher-result-processor.d.ts +168 -0
  197. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  198. package/dist/core/dispatcher-result-processor.js +533 -0
  199. package/dist/core/dispatcher-result-processor.js.map +1 -0
  200. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  201. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  202. package/dist/core/dispatcher-scheduled-tasks.js +1032 -0
  203. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  204. package/dist/core/dispatcher-types.d.ts +411 -0
  205. package/dist/core/dispatcher-types.d.ts.map +1 -0
  206. package/dist/core/dispatcher-types.js +106 -0
  207. package/dist/core/dispatcher-types.js.map +1 -0
  208. package/dist/core/dispatcher.d.ts +122 -610
  209. package/dist/core/dispatcher.d.ts.map +1 -1
  210. package/dist/core/dispatcher.js +365 -3521
  211. package/dist/core/dispatcher.js.map +1 -1
  212. package/dist/core/integration-health.d.ts +18 -10
  213. package/dist/core/integration-health.d.ts.map +1 -1
  214. package/dist/core/integration-health.js +31 -1
  215. package/dist/core/integration-health.js.map +1 -1
  216. package/dist/core/integration-lifecycle.d.ts +65 -0
  217. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  218. package/dist/core/integration-lifecycle.js +163 -14
  219. package/dist/core/integration-lifecycle.js.map +1 -1
  220. package/dist/core/integration-main-backend.d.ts +40 -0
  221. package/dist/core/integration-main-backend.d.ts.map +1 -1
  222. package/dist/core/integration-main-backend.js +89 -2
  223. package/dist/core/integration-main-backend.js.map +1 -1
  224. package/dist/core/management-md.d.ts +51 -17
  225. package/dist/core/management-md.d.ts.map +1 -1
  226. package/dist/core/management-md.js +233 -56
  227. package/dist/core/management-md.js.map +1 -1
  228. package/dist/core/metrics.d.ts +127 -0
  229. package/dist/core/metrics.d.ts.map +1 -1
  230. package/dist/core/metrics.js +256 -1
  231. package/dist/core/metrics.js.map +1 -1
  232. package/dist/core/output-language-policy.d.ts +74 -0
  233. package/dist/core/output-language-policy.d.ts.map +1 -0
  234. package/dist/core/output-language-policy.js +194 -0
  235. package/dist/core/output-language-policy.js.map +1 -0
  236. package/dist/core/prompts.d.ts +3 -1
  237. package/dist/core/prompts.d.ts.map +1 -1
  238. package/dist/core/prompts.js +161 -3
  239. package/dist/core/prompts.js.map +1 -1
  240. package/dist/core/repository-management-docs.d.ts +24 -0
  241. package/dist/core/repository-management-docs.d.ts.map +1 -1
  242. package/dist/core/repository-management-docs.js +210 -26
  243. package/dist/core/repository-management-docs.js.map +1 -1
  244. package/dist/core/roadmap-validate.js +13 -1
  245. package/dist/core/roadmap-validate.js.map +1 -1
  246. package/dist/core/routine-acquisition-plan.d.ts +182 -0
  247. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  248. package/dist/core/routine-acquisition-plan.js +367 -0
  249. package/dist/core/routine-acquisition-plan.js.map +1 -0
  250. package/dist/core/routine-fetch-window-retry.d.ts +109 -0
  251. package/dist/core/routine-fetch-window-retry.d.ts.map +1 -0
  252. package/dist/core/routine-fetch-window-retry.js +210 -0
  253. package/dist/core/routine-fetch-window-retry.js.map +1 -0
  254. package/dist/core/routine-fetch-window-runner.d.ts +427 -0
  255. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  256. package/dist/core/routine-fetch-window-runner.js +1591 -0
  257. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  258. package/dist/core/routine-windows.d.ts +171 -0
  259. package/dist/core/routine-windows.d.ts.map +1 -0
  260. package/dist/core/routine-windows.js +377 -0
  261. package/dist/core/routine-windows.js.map +1 -0
  262. package/dist/core/scheduler.d.ts +50 -2
  263. package/dist/core/scheduler.d.ts.map +1 -1
  264. package/dist/core/scheduler.js +88 -7
  265. package/dist/core/scheduler.js.map +1 -1
  266. package/dist/core/skill-curation/declarations.d.ts.map +1 -1
  267. package/dist/core/skill-curation/declarations.js +11 -12
  268. package/dist/core/skill-curation/declarations.js.map +1 -1
  269. package/dist/core/skill-source-paths.d.ts +14 -0
  270. package/dist/core/skill-source-paths.d.ts.map +1 -0
  271. package/dist/core/skill-source-paths.js +82 -0
  272. package/dist/core/skill-source-paths.js.map +1 -0
  273. package/dist/core/skills-compiler.d.ts +29 -0
  274. package/dist/core/skills-compiler.d.ts.map +1 -1
  275. package/dist/core/skills-compiler.js +166 -30
  276. package/dist/core/skills-compiler.js.map +1 -1
  277. package/dist/core/skills-manifest.d.ts.map +1 -1
  278. package/dist/core/skills-manifest.js +72 -0
  279. package/dist/core/skills-manifest.js.map +1 -1
  280. package/dist/core/system-reset.d.ts +25 -0
  281. package/dist/core/system-reset.d.ts.map +1 -1
  282. package/dist/core/system-reset.js +72 -2
  283. package/dist/core/system-reset.js.map +1 -1
  284. package/dist/core/wiki/approval-queue.d.ts +31 -0
  285. package/dist/core/wiki/approval-queue.d.ts.map +1 -0
  286. package/dist/core/wiki/approval-queue.js +44 -0
  287. package/dist/core/wiki/approval-queue.js.map +1 -0
  288. package/dist/core/wiki/bridge.d.ts +74 -0
  289. package/dist/core/wiki/bridge.d.ts.map +1 -0
  290. package/dist/core/wiki/bridge.js +405 -0
  291. package/dist/core/wiki/bridge.js.map +1 -0
  292. package/dist/core/wiki/compile-lock.d.ts +42 -0
  293. package/dist/core/wiki/compile-lock.d.ts.map +1 -0
  294. package/dist/core/wiki/compile-lock.js +55 -0
  295. package/dist/core/wiki/compile-lock.js.map +1 -0
  296. package/dist/core/wiki/compile-preview.d.ts +8 -0
  297. package/dist/core/wiki/compile-preview.d.ts.map +1 -0
  298. package/dist/core/wiki/compile-preview.js +200 -0
  299. package/dist/core/wiki/compile-preview.js.map +1 -0
  300. package/dist/core/wiki/cost-estimate.d.ts +30 -0
  301. package/dist/core/wiki/cost-estimate.d.ts.map +1 -0
  302. package/dist/core/wiki/cost-estimate.js +243 -0
  303. package/dist/core/wiki/cost-estimate.js.map +1 -0
  304. package/dist/core/wiki/dispatcher.d.ts +48 -0
  305. package/dist/core/wiki/dispatcher.d.ts.map +1 -0
  306. package/dist/core/wiki/dispatcher.js +92 -0
  307. package/dist/core/wiki/dispatcher.js.map +1 -0
  308. package/dist/core/wiki/git-precompile.d.ts +86 -0
  309. package/dist/core/wiki/git-precompile.d.ts.map +1 -0
  310. package/dist/core/wiki/git-precompile.js +96 -0
  311. package/dist/core/wiki/git-precompile.js.map +1 -0
  312. package/dist/core/wiki/import-migrate.d.ts +38 -0
  313. package/dist/core/wiki/import-migrate.d.ts.map +1 -0
  314. package/dist/core/wiki/import-migrate.js +310 -0
  315. package/dist/core/wiki/import-migrate.js.map +1 -0
  316. package/dist/core/wiki/import-probe.d.ts +76 -0
  317. package/dist/core/wiki/import-probe.d.ts.map +1 -0
  318. package/dist/core/wiki/import-probe.js +245 -0
  319. package/dist/core/wiki/import-probe.js.map +1 -0
  320. package/dist/core/wiki/index-cache.d.ts +39 -0
  321. package/dist/core/wiki/index-cache.d.ts.map +1 -0
  322. package/dist/core/wiki/index-cache.js +152 -0
  323. package/dist/core/wiki/index-cache.js.map +1 -0
  324. package/dist/core/wiki/multi-url-dispatch.d.ts +52 -0
  325. package/dist/core/wiki/multi-url-dispatch.d.ts.map +1 -0
  326. package/dist/core/wiki/multi-url-dispatch.js +72 -0
  327. package/dist/core/wiki/multi-url-dispatch.js.map +1 -0
  328. package/dist/core/wiki/wiki-fts.d.ts +75 -0
  329. package/dist/core/wiki/wiki-fts.d.ts.map +1 -0
  330. package/dist/core/wiki/wiki-fts.js +265 -0
  331. package/dist/core/wiki/wiki-fts.js.map +1 -0
  332. package/dist/core/wiki/workspaces.d.ts +101 -0
  333. package/dist/core/wiki/workspaces.d.ts.map +1 -0
  334. package/dist/core/wiki/workspaces.js +352 -0
  335. package/dist/core/wiki/workspaces.js.map +1 -0
  336. package/dist/core/wiki/write-strategy.d.ts +70 -0
  337. package/dist/core/wiki/write-strategy.d.ts.map +1 -0
  338. package/dist/core/wiki/write-strategy.js +112 -0
  339. package/dist/core/wiki/write-strategy.js.map +1 -0
  340. package/dist/core/workdir.d.ts +8 -1
  341. package/dist/core/workdir.d.ts.map +1 -1
  342. package/dist/core/workdir.js +4 -1
  343. package/dist/core/workdir.js.map +1 -1
  344. package/dist/db/observations.d.ts +45 -2
  345. package/dist/db/observations.d.ts.map +1 -1
  346. package/dist/db/observations.js +112 -14
  347. package/dist/db/observations.js.map +1 -1
  348. package/dist/db/schema.d.ts.map +1 -1
  349. package/dist/db/schema.js +135 -25
  350. package/dist/db/schema.js.map +1 -1
  351. package/dist/db/wiki-store.d.ts +3 -0
  352. package/dist/db/wiki-store.d.ts.map +1 -0
  353. package/dist/db/wiki-store.js +7 -0
  354. package/dist/db/wiki-store.js.map +1 -0
  355. package/dist/index.js +159 -610
  356. package/dist/index.js.map +1 -1
  357. package/dist/messaging/url-extract.d.ts +8 -0
  358. package/dist/messaging/url-extract.d.ts.map +1 -0
  359. package/dist/messaging/url-extract.js +41 -0
  360. package/dist/messaging/url-extract.js.map +1 -0
  361. package/dist/observers/delegated-sync-worker.d.ts +52 -1
  362. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  363. package/dist/observers/delegated-sync-worker.js +75 -18
  364. package/dist/observers/delegated-sync-worker.js.map +1 -1
  365. package/dist/observers/imminent-event-scheduler.d.ts +20 -7
  366. package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
  367. package/dist/observers/imminent-event-scheduler.js +134 -29
  368. package/dist/observers/imminent-event-scheduler.js.map +1 -1
  369. package/dist/observers/mail-poller.d.ts +12 -5
  370. package/dist/observers/mail-poller.d.ts.map +1 -1
  371. package/dist/observers/mail-poller.js +36 -14
  372. package/dist/observers/mail-poller.js.map +1 -1
  373. package/dist/observers/manager.d.ts +37 -5
  374. package/dist/observers/manager.d.ts.map +1 -1
  375. package/dist/observers/manager.js +28 -10
  376. package/dist/observers/manager.js.map +1 -1
  377. package/dist/safety/always-disallowed.d.ts +65 -0
  378. package/dist/safety/always-disallowed.d.ts.map +1 -1
  379. package/dist/safety/always-disallowed.js +106 -10
  380. package/dist/safety/always-disallowed.js.map +1 -1
  381. package/dist/safety/audit.d.ts +46 -1
  382. package/dist/safety/audit.d.ts.map +1 -1
  383. package/dist/safety/audit.js +79 -16
  384. package/dist/safety/audit.js.map +1 -1
  385. package/dist/safety/risk-classifier.d.ts.map +1 -1
  386. package/dist/safety/risk-classifier.js +29 -0
  387. package/dist/safety/risk-classifier.js.map +1 -1
  388. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  389. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  390. package/dist/services/delegated-backend-invoker.js +41 -480
  391. package/dist/services/delegated-backend-invoker.js.map +1 -1
  392. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  393. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  394. package/dist/services/delegated-invoker-audit.js +238 -0
  395. package/dist/services/delegated-invoker-audit.js.map +1 -0
  396. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  397. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  398. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  399. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  400. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  401. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  402. package/dist/services/delegated-invoker-janitors.js +104 -0
  403. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  404. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  405. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  406. package/dist/services/delegated-invoker-utils.js +100 -0
  407. package/dist/services/delegated-invoker-utils.js.map +1 -0
  408. package/dist/services/delegated-task-runtime.d.ts +1 -1
  409. package/dist/services/delegated-task-runtime.js +1 -1
  410. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  411. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  412. package/dist/services/integrations/snapshot-partitions.js +12 -0
  413. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  414. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  415. package/dist/services/voice/transcriber-impl.js +7 -8
  416. package/dist/services/voice/transcriber-impl.js.map +1 -1
  417. package/dist/settings/runtime-settings.d.ts +12 -1
  418. package/dist/settings/runtime-settings.d.ts.map +1 -1
  419. package/dist/settings/runtime-settings.js +59 -1
  420. package/dist/settings/runtime-settings.js.map +1 -1
  421. package/package.json +2 -2
@@ -0,0 +1,1064 @@
1
+ /**
2
+ * `MessageHandler` — owns the dispatcher's reactive message-event path:
3
+ * the bang-command interceptor, the cross-platform setup lockout, the
4
+ * `/auth` command surface (`handleAuthCommand`), the resume-vs-fresh-
5
+ * execute decision, the user/assistant message persistence, and the
6
+ * STAGE-C DM freshness telemetry (`collectDmFreshnessTelemetry`).
7
+ *
8
+ * Extracted from `core/dispatcher.ts` as part of phase D-3 of
9
+ * `docs/design/appendices/file-split-plan.md`. Pattern B (stateful
10
+ * coordinator): the handler owns its own logic but borrows live
11
+ * accessors back into the dispatcher for state that is either lazily
12
+ * injected after the dispatcher is constructed (dashboard stream,
13
+ * attachment store, signal detector, docs-QA lookup, auth recovery /
14
+ * health monitor, bang-command registry) or that the dispatcher
15
+ * continues to own as a process-wide flag (`currentSetupMode`).
16
+ *
17
+ * Dispatcher entry points served:
18
+ * - `dispatch.handleMessage` (every owner DM / channel mention /
19
+ * dashboard chat / docs_qa turn) routes through `handle`;
20
+ * - `dispatcher.test.ts` reaches `handleAuthCommand` directly through
21
+ * a private-access cast — preserved as a shim on the dispatcher
22
+ * that forwards to this handler.
23
+ *
24
+ * Shared-state references held (live, not by-value):
25
+ * - `currentSetupMode` getter + `beginSetupMode` setter — the
26
+ * dispatcher owns the persisted-to-runtime_state flag; the handler
27
+ * reads the current value and triggers the same setter the
28
+ * dashboard wizard uses.
29
+ * - Lazy accessors (`getSignalDetector`, `getDashboardStream`,
30
+ * `getAttachmentStore`, `getDocsCitationLookup`,
31
+ * `getAuthRecovery`, `getAuthHealthMonitor`,
32
+ * `getBangCommandRegistry`) — each is null until `index.ts` finishes
33
+ * wiring; reading through the closure ensures the handler sees the
34
+ * current value on every call.
35
+ * - Method delegates (`lookupCustomBangCommandForEvent`,
36
+ * `getConfiguredServices`, `getActiveMailAccounts`,
37
+ * `readLastInsertedMessageId`) — these remain on the dispatcher
38
+ * for now; the handler invokes them via callbacks so the move
39
+ * stays a verbatim relocation.
40
+ *
41
+ * No behavior change. See §7 D-3 of file-split-plan.md for the staged
42
+ * "move now, refine later" plan.
43
+ */
44
+ import { existsSync } from "node:fs";
45
+ import { join } from "node:path";
46
+ import { formatSqliteDatetime, isDocsQAMessage, isMessageEvent, parseSqliteUtcMs, resolveProcessKey, } from "@aitne/shared";
47
+ import { getModelLabel } from "./backends/model-registry.js";
48
+ import { parseGeminiAuthCode } from "./backends/auth-recovery.js";
49
+ import { tryHandle as tryHandleBangCommand } from "./bang-commands/registry.js";
50
+ import { CUSTOM_BANG_COMMAND_SOURCE, createUserBangCommandEvent, resolveCommandSkillSlugs, } from "./bang-commands/user-commands.js";
51
+ import { logInvalidCitations, validateAndRewrite, } from "./docs/citation-validator.js";
52
+ import { countContextWritesInWindow, didRefetchTodayDuringTurn, matchesRecentActivityTrigger, } from "./dm-freshness-metrics.js";
53
+ import { ensureSessionWorkdir, getSessionWorkdirPath, syncAllUserSkills, } from "./workdir.js";
54
+ import { upsertOwnerChannel } from "../messaging/owner-channels.js";
55
+ import { readIntegrations } from "../db/integrations-store.js";
56
+ import { createLogger } from "../logging.js";
57
+ const logger = createLogger("dispatcher-message");
58
+ export class MessageHandler {
59
+ db;
60
+ config;
61
+ eventBus;
62
+ agentRouter;
63
+ contextBuilder;
64
+ notificationMgr;
65
+ sessionMgr;
66
+ messageRecorder;
67
+ audit;
68
+ prompt;
69
+ errorRouter;
70
+ resultProcessor;
71
+ getSignalDetector;
72
+ getDashboardStream;
73
+ getAttachmentStore;
74
+ getDocsCitationLookup;
75
+ getAuthRecovery;
76
+ getAuthHealthMonitor;
77
+ getBangCommandRegistry;
78
+ getCurrentSetupMode;
79
+ beginSetupMode;
80
+ lookupCustomBangCommandForEvent;
81
+ getConfiguredServices;
82
+ getActiveMailAccounts;
83
+ readLastInsertedMessageId;
84
+ constructor(deps) {
85
+ this.db = deps.db;
86
+ this.config = deps.config;
87
+ this.eventBus = deps.eventBus;
88
+ this.agentRouter = deps.agentRouter;
89
+ this.contextBuilder = deps.contextBuilder;
90
+ this.notificationMgr = deps.notificationMgr;
91
+ this.sessionMgr = deps.sessionMgr;
92
+ this.messageRecorder = deps.messageRecorder;
93
+ this.audit = deps.audit;
94
+ this.prompt = deps.prompt;
95
+ this.errorRouter = deps.errorRouter;
96
+ this.resultProcessor = deps.resultProcessor;
97
+ this.getSignalDetector = deps.getSignalDetector;
98
+ this.getDashboardStream = deps.getDashboardStream;
99
+ this.getAttachmentStore = deps.getAttachmentStore;
100
+ this.getDocsCitationLookup = deps.getDocsCitationLookup;
101
+ this.getAuthRecovery = deps.getAuthRecovery;
102
+ this.getAuthHealthMonitor = deps.getAuthHealthMonitor;
103
+ this.getBangCommandRegistry = deps.getBangCommandRegistry;
104
+ this.getCurrentSetupMode = deps.getCurrentSetupMode;
105
+ this.beginSetupMode = deps.beginSetupMode;
106
+ this.lookupCustomBangCommandForEvent = deps.lookupCustomBangCommandForEvent;
107
+ this.getConfiguredServices = deps.getConfiguredServices;
108
+ this.getActiveMailAccounts = deps.getActiveMailAccounts;
109
+ this.readLastInsertedMessageId = deps.readLastInsertedMessageId;
110
+ }
111
+ /**
112
+ * Phase 5 — intercept owner `/auth …` DMs before they reach the agent
113
+ * backend. Returns `true` when the DM was handled (caller must short-
114
+ * circuit), `false` to fall through to normal message processing.
115
+ *
116
+ * Verbatim move from `dispatcher.ts:handleAuthCommand` — no semantic
117
+ * change. See file-split-plan.md §7 D-3.
118
+ */
119
+ async handleAuthCommand(event) {
120
+ const authRecovery = this.getAuthRecovery();
121
+ const authHealthMonitor = this.getAuthHealthMonitor();
122
+ const text = event.content.trim().toLowerCase();
123
+ // `/auth status` — show current auth state
124
+ if (text === "/auth status") {
125
+ const summary = authHealthMonitor
126
+ ? authHealthMonitor.renderStatusSummary()
127
+ : "Check auth status on the dashboard or via `GET /api/backends`.";
128
+ await this.notificationMgr.send(summary, event);
129
+ return true;
130
+ }
131
+ // `/auth fix claude` — start Claude browser auth recovery (Phase 9)
132
+ if (text === "/auth fix claude") {
133
+ if (!authRecovery)
134
+ return false;
135
+ if (authRecovery.isRecoveryActive("claude")) {
136
+ const active = authRecovery.getActiveRecovery("claude");
137
+ await this.notificationMgr.send(`Claude auth recovery already in progress.\n` +
138
+ `URL: ${active?.authUrl}`, event);
139
+ return true;
140
+ }
141
+ try {
142
+ const recovery = await authRecovery.initiateClaudeAuth();
143
+ await this.notificationMgr.send(`Claude auth recovery started.\n` +
144
+ `Open the following URL in your browser to sign in:\n${recovery.authUrl}` +
145
+ `\n(timeout in ${recovery.expiresMinutes} min)`, event);
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : "Unknown error";
149
+ await this.notificationMgr.send(`Failed to start Claude auth recovery: ${msg}`, event);
150
+ }
151
+ return true;
152
+ }
153
+ // `/auth fix codex` — start Codex device auth recovery
154
+ if (text === "/auth fix codex") {
155
+ if (!authRecovery)
156
+ return false;
157
+ if (authRecovery.isRecoveryActive("codex")) {
158
+ const active = authRecovery.getActiveRecovery("codex");
159
+ await this.notificationMgr.send(`Codex auth recovery already in progress.\n` +
160
+ `URL: ${active?.authUrl}\nCode: ${active?.userCode}`, event);
161
+ return true;
162
+ }
163
+ try {
164
+ const recovery = await authRecovery.initiateCodexDeviceAuth();
165
+ // The recovery itself sends a notification with URL/code,
166
+ // but also reply directly to the DM for immediate feedback.
167
+ await this.notificationMgr.send(`Codex auth recovery started.\n` +
168
+ `Open ${recovery.authUrl} in your browser and enter code ${recovery.userCode}.` +
169
+ `\n(expires in ${recovery.expiresMinutes} min)`, event);
170
+ }
171
+ catch (err) {
172
+ const msg = err instanceof Error ? err.message : "Unknown error";
173
+ await this.notificationMgr.send(`Failed to start Codex auth recovery: ${msg}`, event);
174
+ }
175
+ return true;
176
+ }
177
+ // `/auth fix all` — recover all expired backends sequentially
178
+ if (text === "/auth fix all") {
179
+ if (!authRecovery || !authHealthMonitor)
180
+ return false;
181
+ const expired = authHealthMonitor.listExpiredBackends();
182
+ if (expired.length === 0) {
183
+ await this.notificationMgr.send("All backends are healthy. No recovery needed.", event);
184
+ return true;
185
+ }
186
+ const results = [];
187
+ for (const bid of expired) {
188
+ // Skip backends that already have an active recovery session
189
+ if (authRecovery.isRecoveryActive(bid)) {
190
+ results.push(`🔄 ${bid} — Recovery already in progress.`);
191
+ continue;
192
+ }
193
+ try {
194
+ if (bid === "claude") {
195
+ const recovery = await authRecovery.initiateClaudeAuth();
196
+ results.push(`✅ claude — Recovery started. Open the following URL in your browser to sign in:\n${recovery.authUrl}\n(timeout in ${recovery.expiresMinutes} min)`);
197
+ }
198
+ else if (bid === "codex") {
199
+ const recovery = await authRecovery.initiateCodexDeviceAuth();
200
+ results.push(`✅ codex — Recovery started. Open ${recovery.authUrl} in your browser and enter code ${recovery.userCode} (expires in ${recovery.expiresMinutes} min).`);
201
+ }
202
+ else if (bid === "gemini") {
203
+ const recovery = await authRecovery.initiateGeminiAuth();
204
+ results.push(`✅ gemini — Recovery started. Open the following URL in your browser and authenticate, then send the code here:\n${recovery.authUrl}\n(expires in ${recovery.expiresMinutes} min)`);
205
+ }
206
+ else {
207
+ results.push(`⚠️ ${bid} — No automated recovery available for this backend.`);
208
+ }
209
+ }
210
+ catch (err) {
211
+ const msg = err instanceof Error ? err.message : "Unknown error";
212
+ results.push(`❌ ${bid} — Failed to start recovery: ${msg}`);
213
+ }
214
+ }
215
+ const summary = authHealthMonitor.renderStatusSummary();
216
+ await this.notificationMgr.send(`Auth recovery results:\n\n${results.join("\n\n")}\n\n---\n${summary}`, event);
217
+ return true;
218
+ }
219
+ // `/auth fix gemini` — start Gemini OAuth recovery
220
+ if (text === "/auth fix gemini") {
221
+ if (!authRecovery)
222
+ return false;
223
+ if (authRecovery.isRecoveryActive("gemini")) {
224
+ const active = authRecovery.getActiveRecovery("gemini");
225
+ await this.notificationMgr.send(`Gemini auth recovery already in progress.\n` +
226
+ `Open the following URL in your browser to authenticate:\n${active?.authUrl}\n` +
227
+ `Then send the authorization code here.`, event);
228
+ return true;
229
+ }
230
+ try {
231
+ const recovery = await authRecovery.initiateGeminiAuth();
232
+ await this.notificationMgr.send(`Gemini auth recovery started.\n` +
233
+ `Open the following URL in your browser and sign in with your Google account:\n${recovery.authUrl}\n` +
234
+ `Then send the authorization code here.` +
235
+ `\n(expires in ${recovery.expiresMinutes} min)`, event);
236
+ }
237
+ catch (err) {
238
+ const msg = err instanceof Error ? err.message : "Unknown error";
239
+ await this.notificationMgr.send(`Failed to start Gemini auth recovery: ${msg}`, event);
240
+ }
241
+ return true;
242
+ }
243
+ // `/auth cancel` — cancel active recovery
244
+ if (text === "/auth cancel" || text.startsWith("/auth cancel ")) {
245
+ if (!authRecovery)
246
+ return false;
247
+ const parts = text.split(/\s+/);
248
+ const backendHint = parts[2];
249
+ // Cancel all active recoveries, or a specific one
250
+ let cancelled = false;
251
+ for (const bid of ["codex", "gemini", "claude"]) {
252
+ if (backendHint && bid !== backendHint)
253
+ continue;
254
+ if (authRecovery.cancelRecovery(bid))
255
+ cancelled = true;
256
+ }
257
+ await this.notificationMgr.send(cancelled ? "Auth recovery cancelled." : "No active auth recovery to cancel.", event);
258
+ return true;
259
+ }
260
+ // Not an auth command
261
+ return false;
262
+ }
263
+ /**
264
+ * Process a reactive message event end-to-end: bang commands, setup
265
+ * lockout, `/auth` interception, session resume/fresh-execute, message
266
+ * persistence, attachment plumbing, dashboard streaming, and the §4.5
267
+ * delegated-connector health DM.
268
+ *
269
+ * Verbatim move from `dispatcher.ts:handleMessage`. The dispatcher
270
+ * keeps a thin `handleMessage` shim that forwards here so private-
271
+ * access test casts continue to work.
272
+ */
273
+ async handle(event) {
274
+ // Bang-command interceptor — runs first so `!stop` / `!cost` / `!report`
275
+ // succeed even mid-setup, mid-auth-recovery, etc., and so non-bang DMs
276
+ // received while the agent is paused short-circuit before reaching the
277
+ // backend (I-3). See docs/design/backlog/messaging-bang-commands.md §6.2.
278
+ const bangCommandRegistry = this.getBangCommandRegistry();
279
+ if (bangCommandRegistry) {
280
+ const handled = await tryHandleBangCommand(bangCommandRegistry, {
281
+ event,
282
+ db: this.db,
283
+ config: this.config,
284
+ audit: this.audit,
285
+ rawSend: (text) => this.notificationMgr.send(text, event),
286
+ enqueueUserBangCommand: async (command, sourceEvent) => {
287
+ await this.eventBus.put(createUserBangCommandEvent(sourceEvent, command));
288
+ },
289
+ enqueueWikiEvent: async (wikiEvent) => {
290
+ await this.eventBus.put(wikiEvent);
291
+ },
292
+ enqueueWikiApproval: async (approvalInput) => {
293
+ // WIKI_BUILDER_DESIGN.md §5.5 / §P2.E — escalate to Approve tier
294
+ // via the existing agent_schedule approval queue. The dashboard
295
+ // `/approvals` endpoint (dashboard.ts) is the consumer surface.
296
+ const { enqueueWikiApproval } = await import("./wiki/approval-queue.js");
297
+ enqueueWikiApproval(this.db, approvalInput);
298
+ },
299
+ });
300
+ if (handled)
301
+ return;
302
+ }
303
+ // Cross-platform DM lockout during setup.
304
+ // The owner-DM scope is singular across platforms (Slack/Discord/Telegram/
305
+ // WhatsApp/dashboard all share one conversation_sessions row). While a
306
+ // dashboard setup conversation is in progress, a DM from any other
307
+ // platform would otherwise be routed through the active `setup.initial`
308
+ // / `setup.update` prompt — taking a Slack "ping" and feeding it to the
309
+ // rules-generator agent. Reject non-dashboard DMs with a fixed message
310
+ // so the user knows why we are stalling and where to finish setup.
311
+ // Dashboard messages are exempt so the user can still progress setup.
312
+ // Channel mentions (not DMs) are also exempt — they have their own
313
+ // session scope and do not interact with the owner-DM row.
314
+ //
315
+ // `let` (not `const`): the defensive-sync branch below calls
316
+ // `this.beginSetupMode(eventSetupMode)`, which mutates the dispatcher's
317
+ // live `currentSetupMode`. The original `dispatcher.handleMessage` read
318
+ // `this.currentSetupMode` afresh on every reference; the extraction
319
+ // captures it into a local for readability but must keep that local
320
+ // in sync with the live state so later checks (notably the §4.5
321
+ // connector-warnings consult below) see the post-sync value, not the
322
+ // pre-sync snapshot. Without the re-assignment, the warning consult
323
+ // would fire during a defensive-sync setup turn — a regression vs.
324
+ // the pre-D-3 behaviour.
325
+ let currentSetupMode = this.getCurrentSetupMode();
326
+ if (event.isDm &&
327
+ event.platform !== "dashboard" &&
328
+ currentSetupMode !== null) {
329
+ logger.info({ platform: event.platform, mode: currentSetupMode }, "Non-dashboard DM rejected — setup in progress");
330
+ this.audit.logSkip(event, "setup_in_progress", "reactive");
331
+ await this.notificationMgr.send("Setup is in progress. Please complete setup on the dashboard first, then try again.", event);
332
+ return;
333
+ }
334
+ // Phase 6 §5.2: intercept Google OAuth auth codes during pending Gemini
335
+ // recovery. Must come before `/auth` command check so the code isn't
336
+ // treated as an unknown command or routed to the agent backend.
337
+ const authRecovery = this.getAuthRecovery();
338
+ if (event.isDm && authRecovery?.isRecoveryActive("gemini")) {
339
+ const code = parseGeminiAuthCode(event.content);
340
+ if (code) {
341
+ try {
342
+ const result = await authRecovery.handleGeminiAuthCode(code);
343
+ const icon = result.ok ? "✅" : "❌";
344
+ await this.notificationMgr.send(`${icon} Gemini auth: ${result.detail}`, event);
345
+ }
346
+ catch (err) {
347
+ const msg = err instanceof Error ? err.message : "Unknown error";
348
+ await this.notificationMgr.send(`Failed to process Gemini auth code: ${msg}`, event);
349
+ }
350
+ return;
351
+ }
352
+ }
353
+ // Phase 5: intercept `/auth` commands before they reach the agent backend.
354
+ // Gated on DM + at least one auth subsystem being available (/auth status
355
+ // only needs the monitor; /auth fix needs the recovery manager).
356
+ if (event.isDm && (authRecovery || this.getAuthHealthMonitor())) {
357
+ const authResult = await this.handleAuthCommand(event);
358
+ if (authResult)
359
+ return;
360
+ }
361
+ // Check for explicit close command before processing.
362
+ // Use findActive (not getOrCreate) to avoid creating an orphan session.
363
+ if (this.sessionMgr.isCloseCommand(event.content)) {
364
+ const existing = await this.sessionMgr.findActive({
365
+ platform: event.platform,
366
+ channel: event.channel,
367
+ threadId: event.threadId,
368
+ isDm: event.isDm,
369
+ intent: event.intent,
370
+ });
371
+ if (existing) {
372
+ // recordMessage persists the row and touches
373
+ // last_message_at/message_count in a single transaction, so
374
+ // retention + dashboard sidebar stay consistent with the actual
375
+ // `messages` row count. closeSession then flips status.
376
+ this.messageRecorder.recordMessage({
377
+ sessionId: existing.id,
378
+ role: "user",
379
+ content: event.content,
380
+ platform: event.platform,
381
+ senderId: event.sender,
382
+ });
383
+ this.sessionMgr.closeSession(existing.id);
384
+ }
385
+ await this.notificationMgr.send("Session closed.", event);
386
+ return;
387
+ }
388
+ const replyActivity = await this.notificationMgr.beginReplyActivity(event);
389
+ let turnToken = null;
390
+ // STAGE-C-DM-FRESHNESS-PLAN §Task 4 — capture the turn-start reference
391
+ // BEFORE any context_write/context_read row could be written during
392
+ // this turn. Used as the upper bound when counting writes the agent
393
+ // missed pre-resume, and as the lower bound when detecting whether
394
+ // the agent issued a refetch during the current turn.
395
+ const turnStartedAtSqlite = formatSqliteDatetime(new Date());
396
+ try {
397
+ // Docs-QA traffic is a side-channel that must never participate in
398
+ // setup state. Two invariants enforced here:
399
+ // 1. A docs_qa event with a smuggled `data.setupMode` must NOT
400
+ // flip the dispatcher's global `currentSetupMode` — that would
401
+ // hijack subsequent owner DMs into the rules-generator agent.
402
+ // 2. A docs_qa event arriving while `currentSetupMode` is already
403
+ // set (operator opens Docs QA in another tab during setup)
404
+ // must still resolve via `dashboard.docs_qa` so TIER_LOCKED
405
+ // fires and the QA workdir/skill set is materialized — not the
406
+ // setup processKey/light tier/setup skill set. Without this
407
+ // gate, the §11.2 promptKey fix would load the QA prompt while
408
+ // the binding/workdir came from setup, producing an incoherent
409
+ // "QA prompt + setup tools" execution.
410
+ const eventSetupMode = event.data?.setupMode;
411
+ const isDocsQA = isDocsQAMessage(event);
412
+ if (eventSetupMode && currentSetupMode === null && !isDocsQA) {
413
+ // Defensive sync — normally `/setup/start` has already called
414
+ // beginSetupMode, but this keeps prompt selection consistent even if
415
+ // a future caller bypasses the helper and only sets event.data.
416
+ this.beginSetupMode(eventSetupMode);
417
+ // Mirror the just-applied mutation into the local so the
418
+ // §4.5 connector-warnings consult below observes the same
419
+ // value the dispatcher's `this.currentSetupMode` now holds.
420
+ currentSetupMode = eventSetupMode;
421
+ }
422
+ const setupMode = isDocsQA
423
+ ? null
424
+ : (eventSetupMode ?? currentSetupMode);
425
+ const processKey = setupMode === "initial" || setupMode === "update"
426
+ ? "setup"
427
+ : resolveProcessKey(event);
428
+ // Honor the dashboard chat model picker. MessageEvent.requestedModel
429
+ // and the (requestedBackendId, requestedModelId) pair are only
430
+ // populated by the dashboard adapter (see POST /chat/messages in
431
+ // api/routes/sse.ts); other platforms never set them. Defense-in-depth:
432
+ // even if a future adapter were to set them, we gate on platform here
433
+ // so Slack/Telegram/Discord/WhatsApp can never force a specific model
434
+ // through these fields. Setup mode also ignores them — setup runs on
435
+ // the configured setup process key regardless of the user's pick.
436
+ //
437
+ // When both the explicit (backendId, modelId) pair and the legacy
438
+ // requestedModel are set, the pair wins: it is the superset that
439
+ // supports all backends and models, not just Claude sonnet/opus.
440
+ const honorOverride = (event.platform === "dashboard" || event.source === CUSTOM_BANG_COMMAND_SOURCE)
441
+ && !setupMode;
442
+ const requestedTier = honorOverride && event.requestedModel
443
+ ? event.requestedModel === "sonnet"
444
+ ? "medium"
445
+ : "high"
446
+ : undefined;
447
+ const overrideBackendId = honorOverride && event.requestedBackendId && event.requestedModelId
448
+ ? event.requestedBackendId
449
+ : undefined;
450
+ const overrideModelId = honorOverride && event.requestedBackendId && event.requestedModelId
451
+ ? event.requestedModelId
452
+ : undefined;
453
+ const route = this.agentRouter.resolveBinding(event, {
454
+ processKey,
455
+ ...(requestedTier ? { requestedTier } : {}),
456
+ ...(overrideBackendId && overrideModelId
457
+ ? { requestedBackendId: overrideBackendId, requestedModelId: overrideModelId }
458
+ : {}),
459
+ });
460
+ const session = await this.sessionMgr.getOrCreate({
461
+ platform: event.platform,
462
+ channel: event.channel,
463
+ threadId: event.threadId,
464
+ isDm: event.isDm,
465
+ intent: event.intent,
466
+ requiredBackend: route.main.backendId,
467
+ requiredModel: route.main.modelId,
468
+ });
469
+ const forwardContextAvailable = this.resultProcessor.hasRecentProactiveForwardContext(event, session.id);
470
+ // Custom messaging bang command (`!commandname`): the owner's
471
+ // saved row carries an opt-in skill set + an optional custom
472
+ // profile body. We forward those to `ensureSessionWorkdir` as a
473
+ // re-materialize override so the agent runs with the row's
474
+ // configuration for THIS turn. The override forces re-write of
475
+ // CLAUDE.md / AGENTS.md / GEMINI.md and the skill dirs even when
476
+ // the workdir already exists (regular DMs share the same dir).
477
+ // The next regular DM turn detects the bang stamp file written
478
+ // by `ensureSessionWorkdir` and re-materializes back to manifest
479
+ // defaults — keeping `!cmd` configurations from leaking into a
480
+ // natural conversation that follows.
481
+ const customBangCommand = this.lookupCustomBangCommandForEvent(event);
482
+ const workdirOverride = customBangCommand
483
+ ? {
484
+ skillSlugs: [...resolveCommandSkillSlugs(customBangCommand)],
485
+ profileBody: customBangCommand.instructionMd,
486
+ }
487
+ : undefined;
488
+ // Skip the owner-channel pairing record for docs_qa: the QA panel
489
+ // is not a messaging-app surface and would otherwise clutter
490
+ // /connections/messaging with synthetic "dashboard" pairings.
491
+ //
492
+ // `pendingConnectorWarnings` is captured here so both the resume and
493
+ // fresh-execute branches below can call the §4.5 DM dispatch via
494
+ // `dispatchPendingConnectorHealth()` AFTER each branch's user-message
495
+ // recordMessage — the dispatch's persist must follow the user message
496
+ // in DB-timestamp order or the dashboard's chat_meta history reload
497
+ // reorders the bubbles.
498
+ let pendingConnectorWarnings = [];
499
+ const dispatchPendingConnectorHealth = () => {
500
+ if (pendingConnectorWarnings.length === 0)
501
+ return;
502
+ this.errorRouter.runDelegatedConnectorWarningDispatch(pendingConnectorWarnings, event, route.main.backendId, session.id);
503
+ };
504
+ if (event.isDm && !isDocsQAMessage(event)) {
505
+ upsertOwnerChannel(this.db, {
506
+ platform: event.platform,
507
+ senderId: event.sender,
508
+ channelId: event.channel,
509
+ metadata: { threadId: event.threadId },
510
+ touchInbound: true,
511
+ });
512
+ // DELEGATED-MODE-V2-DESIGN.md §4.5 — at every DM dispatch, consult
513
+ // the cached probe for delegated integrations whose effective
514
+ // backend matches the session backend. If the cached probe shows
515
+ // missing required capabilities (the wizard / a future periodic
516
+ // re-probe wrote `present=false`), fire a one-shot DM warning the
517
+ // owner that same-backend mode is non-functional. The helper
518
+ // dedupes via `runtime_state` so resume-vs-fresh-execute do not
519
+ // spam the user. Cheap, synchronous DB-only inspection — runs on
520
+ // the hot path so the warning lands before the agent's reply.
521
+ //
522
+ // Skipped while the dispatcher is in setup mode: the wizard's
523
+ // background `probeLive` call may have just landed a `present=false`
524
+ // row for a connector the user is in the middle of authorising, and
525
+ // a DM telling them to "Re-authorize from your … connector
526
+ // settings, then re-run the integration probe from the dashboard"
527
+ // is wrong-tense for the in-flight setup conversation. The §10
528
+ // post-setup sign-out scenario the check exists for fires correctly
529
+ // on the first DM after `clearSetupMode` runs.
530
+ //
531
+ // Two-phase: consult the cached probe NOW (synchronous DB read),
532
+ // but defer the actual DM dispatch + dashboard messages-table
533
+ // persist until both branches below have recorded the inbound user
534
+ // message. Otherwise the warning's persist row carries a
535
+ // CURRENT_TIMESTAMP that lands BEFORE the user-message row's, and
536
+ // the dashboard's chat_meta history reload re-orders the bubbles
537
+ // (warning above user) — a one-time UX flicker.
538
+ pendingConnectorWarnings =
539
+ currentSetupMode === null
540
+ ? this.errorRouter.consultDelegatedConnectorWarnings(route.main.backendId)
541
+ : [];
542
+ }
543
+ // `event.channel` is captured at the moment the user POSTed their
544
+ // message. If the tab navigates away and reconnects, the SSE route
545
+ // calls `rebindSessionChannel` to update `conversation_sessions.
546
+ // channel_id` to the new UUID — but our closure here still holds
547
+ // the old value. `resolveDashboardChannel` reads the live DB value
548
+ // on every send so stream/meta/info/error events reach whichever
549
+ // tab is currently connected for this session.
550
+ const resolveDashboardChannel = () => this.sessionMgr.getActiveChannelIdForSession(session.id) ?? event.channel;
551
+ // Send resolved model info + DB session ID to dashboard so the
552
+ // sidebar badge is accurate and the frontend can persist the session.
553
+ const dashboardStream = this.getDashboardStream();
554
+ if (event.platform === "dashboard" && dashboardStream?.sendSessionInfo) {
555
+ dashboardStream.sendSessionInfo(resolveDashboardChannel(), {
556
+ sessionId: session.id,
557
+ model: route.main.modelId,
558
+ backend: route.main.backendId,
559
+ modelLabel: getModelLabel(route.main.backendId, route.main.modelId),
560
+ });
561
+ }
562
+ // Feed user message to SignalDetector for implicit feedback
563
+ // detection. Docs-QA messages are docs lookups, not feedback
564
+ // signals, so they bypass the detector entirely.
565
+ if (!isDocsQAMessage(event)) {
566
+ this.getSignalDetector()?.onUserMessage({
567
+ platform: event.platform,
568
+ content: event.content,
569
+ });
570
+ }
571
+ // Create stream callbacks for dashboard events (real-time SSE text).
572
+ // Each callback re-resolves the channel on invocation so a user
573
+ // who navigates away and returns mid-execute still receives the
574
+ // tail of the stream on their new tab.
575
+ let didStream = false;
576
+ const streamCb = event.platform === "dashboard" && dashboardStream
577
+ ? {
578
+ onText: (text) => {
579
+ didStream = true;
580
+ dashboardStream.sendStreamChunk(resolveDashboardChannel(), text);
581
+ },
582
+ onEnd: () => {
583
+ dashboardStream.sendStreamEnd(resolveDashboardChannel());
584
+ },
585
+ }
586
+ : undefined;
587
+ // Chat-attachments Phase 1 — issue a per-turn capability token the
588
+ // agent's `attach` skill will present via `X-Turn-Token`. Valid only
589
+ // while this turn is running; always cleared in the outer `finally`
590
+ // below so leakage is bounded to the lifetime of the turn.
591
+ const attachmentStore = this.getAttachmentStore();
592
+ turnToken = attachmentStore
593
+ ? this.prompt.issueAttachmentTurnToken(session.id)
594
+ : null;
595
+ // Can we resume an existing SDK session?
596
+ // Resume whenever this conversation already has a stored SDK session.
597
+ // Never resume on the FIRST message of a new setup — event.data.setupMode means
598
+ // "start a new setup", not "continue an existing one".
599
+ //
600
+ // Also require the session's persistent workdir to exist on disk. If
601
+ // it was removed out of band (manual cleanup, stale-workdir scanner
602
+ // bug, disk failure), attempting to resume would land the SDK in a
603
+ // freshly-created empty directory with no CLAUDE.md / AGENTS.md /
604
+ // skills tree, producing confusing output. Fall back to the fresh-
605
+ // execute branch, which re-materializes the workdir via
606
+ // `ensureSessionWorkdir`.
607
+ const isNewSetupStart = !!event.data?.setupMode;
608
+ const existingSessionDirPresent = session.isActive
609
+ && existsSync(getSessionWorkdirPath(this.config.dataDir, session.id));
610
+ const canResume = session.isActive
611
+ && session.sessionId
612
+ && existingSessionDirPresent
613
+ && !isNewSetupStart;
614
+ if (session.isActive && session.sessionId && !existingSessionDirPresent) {
615
+ logger.warn({ sessionId: session.id }, "Session marked resumable but workdir missing — falling back to fresh execute");
616
+ }
617
+ let result;
618
+ let userMessageId = null;
619
+ // STAGE-C-DM-FRESHNESS-PLAN §Task 2 — `<turn_context>` is injected on
620
+ // resume only. The resume payload is the bare user-message text; the
621
+ // SDK's cached system prompt holds the original `<current_time>` and
622
+ // the snapshot anchored by `<today snapshot_at="...">` (Task 1), both
623
+ // frozen at session start. Without a per-turn fresh-clock anchor, the
624
+ // model cannot compute "how stale is my snapshot" and answers from
625
+ // an out-of-date view of `## Agent Log`. On the fresh-execute branch,
626
+ // the system prompt's `<current_time>` is built at the moment of
627
+ // dispatch — adding `<turn_context>` there would be redundant AND
628
+ // would diverge the prompt prefix per turn, defeating prompt caching.
629
+ // If a future change rebuilds `<today>` mid-session, this code must
630
+ // be revisited because `started_at` would no longer be the snapshot
631
+ // reference.
632
+ let resumeTurnContext = null;
633
+ let resumeSnapshotAgeMinutes = 0;
634
+ if (canResume) {
635
+ // ── Resume existing SDK session ──
636
+ const proactiveForwardContext = forwardContextAvailable
637
+ ? await this.contextBuilder.build(event)
638
+ : null;
639
+ const userMsgRecorded = this.messageRecorder.recordMessage({
640
+ sessionId: session.id,
641
+ role: "user",
642
+ content: event.content,
643
+ platform: event.platform,
644
+ senderId: event.sender,
645
+ });
646
+ if (userMsgRecorded) {
647
+ userMessageId = this.readLastInsertedMessageId(session.id);
648
+ }
649
+ // Compute the freshness anchors for this resumed turn. `started_at`
650
+ // is the moment `<today>` was captured (the fresh-execute branch
651
+ // builds the system prompt then). Reading from the session row
652
+ // (rather than the in-memory `session` value) keeps this side-
653
+ // effect-free: the row was just fetched by `getOrCreate` and is
654
+ // authoritative.
655
+ const turnNow = new Date();
656
+ const sessionTimingRow = this.db
657
+ .prepare(`SELECT started_at FROM conversation_sessions WHERE id = ?`)
658
+ .get(session.id);
659
+ const sessionStartedAtSqlite = sessionTimingRow?.started_at ?? null;
660
+ const sessionStartedAtMs = sessionStartedAtSqlite
661
+ ? parseSqliteUtcMs(sessionStartedAtSqlite)
662
+ : turnNow.getTime();
663
+ resumeSnapshotAgeMinutes = Math.max(0, Math.round((turnNow.getTime() - sessionStartedAtMs) / 60_000));
664
+ resumeTurnContext =
665
+ `<turn_context current_time="${turnNow.toISOString()}" `
666
+ + `snapshot_age_minutes="${resumeSnapshotAgeMinutes}" />`;
667
+ // §4.5 connector-health DM is dispatched AFTER recordMessage so the
668
+ // warning's messages-table row carries a strictly-later timestamp
669
+ // than the user message. See `consultDelegatedConnectorWarnings`.
670
+ dispatchPendingConnectorHealth();
671
+ const sessionDir = ensureSessionWorkdir(this.config.workspaceDir, this.config.dataDir, session.id, event.type, {
672
+ backendId: session.backend ?? "claude",
673
+ processKey: route.processKey,
674
+ configuredServices: this.getConfiguredServices(),
675
+ mailAccounts: this.getActiveMailAccounts(),
676
+ integrations: readIntegrations(this.db),
677
+ character: this.config.character,
678
+ ...(workdirOverride ? { override: workdirOverride } : {}),
679
+ });
680
+ // Sync user-authored skills into the workdir before resuming, so any
681
+ // skill added/edited/deleted via /api/skills since the last turn is
682
+ // visible to the SDK's `.claude/skills/` discovery. Cheap and idempotent.
683
+ syncAllUserSkills(sessionDir, join(this.config.dataDir, "skills"));
684
+ // Phase 1 — stage inbound attachments + bind rows + append
685
+ // bracketed prompt block. For resume we can't prepend to the
686
+ // task-flow template (there isn't one on this path), so the
687
+ // attachment block is appended to the user's message text. A
688
+ // Claude SDK `query()` call sees `prompt` as a single string, so
689
+ // this is the only surface available.
690
+ const resumeStaged = isMessageEvent(event)
691
+ ? this.prompt.stageInboundAttachments(event, sessionDir)
692
+ : [];
693
+ if (resumeStaged.length > 0 && userMessageId !== null && attachmentStore) {
694
+ attachmentStore.bindInbound({
695
+ attachmentIds: resumeStaged.map((r) => r.id),
696
+ sessionId: session.id,
697
+ messageId: userMessageId,
698
+ });
699
+ }
700
+ const resumeTranscripts = await this.prompt.transcribeAttachments(resumeStaged);
701
+ const resumeMessage = resumeStaged.length > 0
702
+ ? `${event.content}\n${this.prompt.buildAttachmentPromptBlock(resumeStaged, resumeTranscripts)}`
703
+ : event.content;
704
+ const resumeMessageWithForwardContext = proactiveForwardContext
705
+ ? `${resumeTurnContext}\n\n${proactiveForwardContext}\n\n<current_user_message>\n${resumeMessage}\n</current_user_message>`
706
+ : `${resumeTurnContext}\n\n${resumeMessage}`;
707
+ const resumeStagedForBackend = resumeStaged.length > 0
708
+ ? resumeStaged.map((row) => ({
709
+ id: row.id,
710
+ safeFilename: row.safeFilename,
711
+ mimeType: row.mimeType,
712
+ absolutePath: `${sessionDir}/_attachments/${row.safeFilename}`,
713
+ relativePath: `_attachments/${row.safeFilename}`,
714
+ }))
715
+ : [];
716
+ result = await this.errorRouter.executeWithRetry(() => this.agentRouter.executeResume({
717
+ backendId: session.backend ?? "claude",
718
+ sessionId: session.sessionId,
719
+ message: resumeMessageWithForwardContext,
720
+ modelId: route.main.modelId,
721
+ maxTurns: route.main.maxTurns,
722
+ maxBudgetUsd: route.main.maxBudgetUsd,
723
+ sessionDir,
724
+ sessionDbId: session.id,
725
+ eventCorrelationId: event.correlationId,
726
+ ...(turnToken ? { turnToken } : {}),
727
+ ...(resumeStagedForBackend.length > 0
728
+ ? { stagedAttachments: resumeStagedForBackend }
729
+ : {}),
730
+ }, streamCb), event);
731
+ }
732
+ else {
733
+ // ── Fresh execute ──
734
+ // Docs-QA branches FIRST. Without this gate, `event.isDm` would
735
+ // route the QA event into the generic DM task flow and the
736
+ // agent would run without the QA system prompt (citation
737
+ // enforcement, search budget, "no write tools"). The
738
+ // `dashboard.docs_qa` task flow lives at
739
+ // agent-assets/task-flows/dashboard.docs_qa.md.
740
+ const promptKey = isDocsQAMessage(event)
741
+ ? "dashboard.docs_qa"
742
+ : setupMode === "initial"
743
+ ? "setup.initial"
744
+ : setupMode === "update"
745
+ ? "setup.update"
746
+ : event.isDm && !session.isActive
747
+ ? "message.received.dm_first"
748
+ : event.isDm
749
+ ? "message.received.dm"
750
+ : event.type;
751
+ const context = await this.contextBuilder.build(event);
752
+ // Setup flows route through processKey="setup" for backend binding,
753
+ // but the workdir must materialize with the mode-specific processKey
754
+ // so `setup.update` doesn't inherit `setup.initial`'s skill set via
755
+ // PROCESS_TO_EVENT_TYPE["setup"]="setup.initial".
756
+ const workdirEventType = setupMode ? `setup.${setupMode}` : promptKey;
757
+ const workdirProcessKey = setupMode
758
+ ? `setup.${setupMode}`
759
+ : route.processKey;
760
+ const reassemblePrompt = (bid) => this.prompt.assemble(promptKey, route.processKey, bid);
761
+ const prompt = reassemblePrompt(route.main.backendId);
762
+ // DMs need persistent workdirs/session ids for real resume semantics.
763
+ // Channel/thread conversations only persist high-tier sessions.
764
+ const shouldPersistSessionState = event.isDm || route.resolvedTier === "high";
765
+ const sessionDir = shouldPersistSessionState
766
+ ? ensureSessionWorkdir(this.config.workspaceDir, this.config.dataDir, session.id, workdirEventType, {
767
+ backendId: route.main.backendId,
768
+ processKey: workdirProcessKey,
769
+ configuredServices: this.getConfiguredServices(),
770
+ mailAccounts: this.getActiveMailAccounts(),
771
+ integrations: readIntegrations(this.db),
772
+ character: this.config.character,
773
+ ...(workdirOverride ? { override: workdirOverride } : {}),
774
+ })
775
+ : undefined;
776
+ // Re-sync user skills on every Opus message. ensureSessionWorkdir is
777
+ // idempotent and skips the copy step on subsequent calls, so without
778
+ // this explicit sync a skill created mid-session (via POST /api/skills)
779
+ // would never reach the session's `.claude/skills/` tree and the SDK
780
+ // wouldn't discover it. The sync is a cheap diff operation backed by
781
+ // a manifest file inside the workdir.
782
+ if (sessionDir) {
783
+ syncAllUserSkills(sessionDir, join(this.config.dataDir, "skills"));
784
+ }
785
+ // Docs-QA sessions are stateless lookups (DOCS_QA_B7_DESIGN.md
786
+ // §11.6 — "QA panel state lives in React state, not the DB").
787
+ // After a docs_qa session reset (day boundary, model switch),
788
+ // session-manager's `requiresHistoryInjection` would still fire
789
+ // because prior messages exist in the docs_qa scope; without
790
+ // this gate they'd bleed back into the prompt as cross-session
791
+ // history, contradicting the stateless contract and silently
792
+ // ballooning the QA token budget across days.
793
+ const conversationHistory = session.requiresHistoryInjection && !isDocsQAMessage(event)
794
+ ? this.resultProcessor.buildCrossSessionConversationHistory(event)
795
+ : null;
796
+ // Record user message AFTER context/history build (avoids injecting
797
+ // the current turn into cross-session history) but BEFORE execute
798
+ // (ensures DB has the message even if execute crashes).
799
+ const freshUserMsgRecorded = this.messageRecorder.recordMessage({
800
+ sessionId: session.id,
801
+ role: "user",
802
+ content: event.content,
803
+ platform: event.platform,
804
+ senderId: event.sender,
805
+ });
806
+ if (freshUserMsgRecorded) {
807
+ userMessageId = this.readLastInsertedMessageId(session.id);
808
+ }
809
+ // §4.5 connector-health DM is dispatched AFTER recordMessage so the
810
+ // warning's messages-table row carries a strictly-later timestamp
811
+ // than the user message. See `consultDelegatedConnectorWarnings`.
812
+ dispatchPendingConnectorHealth();
813
+ // Phase 1 — stage inbound attachments + bind rows + append
814
+ // bracketed prompt block to the prompt body.
815
+ const freshStaged = isMessageEvent(event)
816
+ ? this.prompt.stageInboundAttachments(event, sessionDir)
817
+ : [];
818
+ if (freshStaged.length > 0 && userMessageId !== null && attachmentStore) {
819
+ attachmentStore.bindInbound({
820
+ attachmentIds: freshStaged.map((r) => r.id),
821
+ sessionId: session.id,
822
+ messageId: userMessageId,
823
+ });
824
+ }
825
+ const freshTranscripts = await this.prompt.transcribeAttachments(freshStaged);
826
+ const executePrompt = freshStaged.length > 0
827
+ ? `${prompt}\n${this.prompt.buildAttachmentPromptBlock(freshStaged, freshTranscripts)}`
828
+ : prompt;
829
+ // DMs should always persist backend sessions so same-session resume and
830
+ // dashboard history continue do not fall back to history reinjection.
831
+ const persistSession = shouldPersistSessionState;
832
+ const freshStagedForBackend = freshStaged.length > 0 && sessionDir
833
+ ? freshStaged.map((row) => ({
834
+ id: row.id,
835
+ safeFilename: row.safeFilename,
836
+ mimeType: row.mimeType,
837
+ absolutePath: `${sessionDir}/_attachments/${row.safeFilename}`,
838
+ relativePath: `_attachments/${row.safeFilename}`,
839
+ }))
840
+ : [];
841
+ result = await this.errorRouter.executeWithRetry(() => this.agentRouter.execute({
842
+ prompt: executePrompt,
843
+ context,
844
+ event,
845
+ processKey: setupMode === "initial" || setupMode === "update"
846
+ ? "setup"
847
+ : resolveProcessKey(event),
848
+ sessionDir,
849
+ sessionDbId: session.id,
850
+ persistSession,
851
+ conversationHistory: conversationHistory ?? undefined,
852
+ preResolvedBinding: route,
853
+ workdirEventType,
854
+ workdirProcessKey,
855
+ reassemblePrompt,
856
+ ...(turnToken ? { turnToken } : {}),
857
+ ...(freshStagedForBackend.length > 0
858
+ ? { stagedAttachments: freshStagedForBackend }
859
+ : {}),
860
+ }, streamCb), event);
861
+ // Store SDK sessionId for future resume, including normal owner DMs.
862
+ if (persistSession && result.sessionId) {
863
+ await this.sessionMgr.updateSession(session.id, result.sessionId, result.modelId ?? result.model, result.backendId);
864
+ }
865
+ else if (persistSession && !result.sessionId) {
866
+ // Successful DM/heavy execute, but the backend didn't emit a
867
+ // resumable session id (observed with certain Gemini CLI
868
+ // streams where the `init` event fired without `session_id`).
869
+ // The row keeps its previous `backend_session_id` (possibly
870
+ // NULL) and the next turn will fall through to fresh-execute
871
+ // + history injection — still resumable from the sidebar via
872
+ // the relaxed gate. Log so this stops being invisible.
873
+ logger.warn({
874
+ sessionId: session.id,
875
+ backend: result.backendId,
876
+ model: result.modelId ?? result.model,
877
+ }, "Execute completed without a backend session id — next resume will rebuild via history injection");
878
+ }
879
+ }
880
+ // Record assistant response. `recordMessage` also bumps the
881
+ // session's `last_message_at` and `message_count` in the same
882
+ // transaction, so nothing else needs to touch the session row here.
883
+ let assistantMessageId = null;
884
+ let assistantOutput = result.output.trim();
885
+ // Docs-QA persistence-side citation validator (DOCS_QA_B7_DESIGN.md
886
+ // §11.1). The streaming side runs in DocsQAAdapter.sendStreamChunk;
887
+ // this one-shot pass guarantees the persisted `messages.content`
888
+ // matches what the dashboard rendered on reload — without it, an
889
+ // invalid `[doc:slug]` token would be stripped from the SSE wire
890
+ // but reappear in history. Slug-missing tokens are also logged to
891
+ // `agent_actions(action_type='qa_invalid_citation')`.
892
+ const docsCitationLookup = this.getDocsCitationLookup();
893
+ if (isDocsQAMessage(event)
894
+ && docsCitationLookup
895
+ && assistantOutput.length > 0) {
896
+ const validation = validateAndRewrite(assistantOutput, docsCitationLookup);
897
+ assistantOutput = validation.text;
898
+ logInvalidCitations(this.db, validation, { sessionId: session.id });
899
+ }
900
+ if (assistantOutput.length > 0) {
901
+ const persisted = this.messageRecorder.recordMessage({
902
+ sessionId: session.id,
903
+ role: "assistant",
904
+ content: assistantOutput,
905
+ platform: event.platform,
906
+ backend: result.backendId,
907
+ modelId: result.modelId ?? result.model,
908
+ });
909
+ if (persisted) {
910
+ assistantMessageId = this.readLastInsertedMessageId(session.id);
911
+ if (forwardContextAvailable) {
912
+ this.resultProcessor.logProactiveForwardDisavowalIfMatched(session.id, assistantOutput);
913
+ }
914
+ }
915
+ if (!persisted && event.platform === "dashboard" && dashboardStream?.sendError) {
916
+ // The agent produced a response but we couldn't persist it. The
917
+ // dashboard tab has no other signal that the turn finished —
918
+ // without this inline surfacing the user would watch the reply
919
+ // stream in, then hit the 120s waiting timeout on refresh with
920
+ // no history row to reconcile against. Tell them directly.
921
+ dashboardStream.sendError(resolveDashboardChannel(), "The agent's reply could not be saved. Please try again.");
922
+ }
923
+ }
924
+ else {
925
+ // Agent returned no output — send error feedback so the user isn't left waiting
926
+ const errorMsg = "Could not generate a response. Please try again.";
927
+ logger.warn({ sessionId: session.id, isError: result.isError, stopReason: result.stopReason }, "Agent returned empty output for message event");
928
+ this.messageRecorder.recordMessage({
929
+ sessionId: session.id,
930
+ role: "assistant",
931
+ content: errorMsg,
932
+ platform: event.platform,
933
+ backend: result.backendId,
934
+ modelId: result.modelId ?? result.model,
935
+ });
936
+ // Send error to dashboard chat so the user sees it inline
937
+ if (event.platform === "dashboard" && dashboardStream?.sendError) {
938
+ dashboardStream.sendError(resolveDashboardChannel(), errorMsg);
939
+ }
940
+ await this.notificationMgr.send(errorMsg, event);
941
+ }
942
+ // Send message metadata to dashboard for per-message footer display.
943
+ // This is also the client's cue to refetch history after a mid-execute
944
+ // reconnect — the chunks that arrived before the user reopened the tab
945
+ // were dropped into the old channel, so the live messages state may be
946
+ // missing content that is already in the DB.
947
+ if (event.platform === "dashboard" && dashboardStream?.sendMessageMeta) {
948
+ dashboardStream.sendMessageMeta(resolveDashboardChannel(), {
949
+ backend: result.backendId,
950
+ model: result.modelId ?? result.model,
951
+ durationMs: result.durationMs,
952
+ costUsd: result.costUsd,
953
+ });
954
+ }
955
+ // Update session-level model info with actual execution result.
956
+ // This corrects the pre-execution estimate when fallback kicked in,
957
+ // and pushes the cumulative costUsd to the sidebar badge.
958
+ if (event.platform === "dashboard" && dashboardStream?.sendSessionInfo) {
959
+ const actualModel = result.modelId ?? result.model;
960
+ const actualBackend = result.backendId ?? route.main.backendId;
961
+ dashboardStream.sendSessionInfo(resolveDashboardChannel(), {
962
+ model: actualModel,
963
+ backend: actualBackend,
964
+ modelLabel: getModelLabel(actualBackend, actualModel),
965
+ costUsd: result.costUsd,
966
+ });
967
+ }
968
+ // Chat-attachments Phase 1 — collect outbound files the agent
969
+ // produced during this turn and deliver them via the originating
970
+ // adapter. Currently only the Dashboard adapter delivers outbound
971
+ // attachments on-wire; other platforms ignore the `attachments`
972
+ // field until Phase 2.
973
+ if (turnToken
974
+ && attachmentStore
975
+ && assistantMessageId !== null
976
+ && assistantOutput.length > 0) {
977
+ const outboundRows = attachmentStore.collectOutboundForTurn({
978
+ turnToken,
979
+ sessionId: session.id,
980
+ });
981
+ if (outboundRows.length > 0) {
982
+ for (const row of outboundRows) {
983
+ attachmentStore.bindOutboundToMessage(row.id, assistantMessageId);
984
+ }
985
+ if (event.platform === "dashboard" && dashboardStream?.sendAttachments) {
986
+ dashboardStream.sendAttachments(resolveDashboardChannel(), outboundRows.map((row) => ({
987
+ id: row.id,
988
+ originalFilename: row.originalFilename,
989
+ mimeType: row.mimeType,
990
+ sizeBytes: row.sizeBytes,
991
+ ...(row.caption ? { caption: row.caption } : {}),
992
+ })));
993
+ }
994
+ }
995
+ }
996
+ // STAGE-C-DM-FRESHNESS-PLAN §Task 4 — collect the per-turn DM
997
+ // freshness telemetry before notification + audit. Limited to DM
998
+ // events: the metric only makes sense for the resume-or-fresh-
999
+ // execute decision the message dispatch makes. We compute counts
1000
+ // bounded by the captured `turnStartedAtSqlite` so writes the
1001
+ // agent itself made during THIS turn are not folded back in.
1002
+ const dmFreshness = event.isDm
1003
+ ? this.collectDmFreshnessTelemetry({
1004
+ sessionId: session.id,
1005
+ canResume: Boolean(canResume),
1006
+ resumeSnapshotAgeMinutes,
1007
+ turnStartedAtSqlite,
1008
+ userContent: event.content,
1009
+ })
1010
+ : undefined;
1011
+ // Skip notification if we already streamed (avoids duplicate message)
1012
+ await this.resultProcessor.processResult(result, event, didStream, {
1013
+ originSessionId: session.id,
1014
+ ...(dmFreshness ? { dmFreshness } : {}),
1015
+ });
1016
+ }
1017
+ finally {
1018
+ // Always release the turn token, even on error paths. Any outbound
1019
+ // rows the agent posted that weren't collected above fall into the
1020
+ // orphan reaper's domain on the next daemon restart.
1021
+ if (turnToken) {
1022
+ this.prompt.releaseAttachmentTurnToken(turnToken);
1023
+ this.getAttachmentStore()?.releaseTurnToken(turnToken);
1024
+ }
1025
+ await replyActivity.stop();
1026
+ }
1027
+ }
1028
+ /**
1029
+ * STAGE-C-DM-FRESHNESS-PLAN §Task 4 — assemble the DM-only freshness
1030
+ * telemetry payload that gets persisted into `agent_actions.detail`.
1031
+ * Pulled into its own method so the message-dispatch path stays
1032
+ * readable and so unit tests can exercise the SQL aggregation in
1033
+ * isolation.
1034
+ *
1035
+ * Verbatim move from `dispatcher.ts:collectDmFreshnessTelemetry`.
1036
+ */
1037
+ collectDmFreshnessTelemetry(input) {
1038
+ const sessionRow = this.db
1039
+ .prepare(`SELECT started_at FROM conversation_sessions WHERE id = ?`)
1040
+ .get(input.sessionId);
1041
+ // Fall back to turnStart so a missing started_at yields zero counts
1042
+ // instead of poisoning the aggregation with a wide-open lower bound.
1043
+ const sessionStartedAtSqlite = sessionRow?.started_at ?? input.turnStartedAtSqlite;
1044
+ const writeCounts = countContextWritesInWindow(this.db, sessionStartedAtSqlite, input.turnStartedAtSqlite);
1045
+ // Bound the refetch window at "now" so a context_read that lands
1046
+ // AFTER this turn's executeWithRetry returns (e.g. from a future
1047
+ // parallel dispatcher, an unrelated routine, or a dashboard reload)
1048
+ // is not wrongly attributed to this turn.
1049
+ const turnEndSqlite = formatSqliteDatetime(new Date());
1050
+ const refetchedToday = didRefetchTodayDuringTurn(this.db, input.turnStartedAtSqlite, turnEndSqlite);
1051
+ return {
1052
+ resumed: input.canResume,
1053
+ // Fresh-execute branch sets resumeSnapshotAgeMinutes=0 by default;
1054
+ // that's the correct lag because the system prompt's <today> was
1055
+ // built at this very turn.
1056
+ agentLogLagMinutes: input.canResume ? input.resumeSnapshotAgeMinutes : 0,
1057
+ loudWritesSinceSessionStart: writeCounts.loud,
1058
+ quietWritesSinceSessionStart: writeCounts.quiet,
1059
+ refetchedToday,
1060
+ triggerMatched: matchesRecentActivityTrigger(input.userContent),
1061
+ };
1062
+ }
1063
+ }
1064
+ //# sourceMappingURL=dispatcher-message-handler.js.map