@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
@@ -1,7 +1,9 @@
1
1
  import { Hono } from "hono";
2
+ import { INTEGRATION_KEYS, isIntegrationKey, } from "@aitne/shared";
2
3
  import { consumeObservations, getNoveltyDistribution, getObservationStats, getPendingObservations, getSummaryStatusCounts, recordObservation, } from "../../db/observations.js";
4
+ import { readIntegrationFlipLock } from "../../core/integration-lifecycle.js";
3
5
  import { createLogger } from "../../logging.js";
4
- import { readJsonBody } from "../json-body.js";
6
+ import { DEFAULT_JSON_BODY_MAX_BYTES, readJsonBody } from "../json-body.js";
5
7
  const logger = createLogger("observations-api");
6
8
  function parseBoolean(value, defaultValue) {
7
9
  if (value === undefined)
@@ -22,6 +24,80 @@ function parsePayload(payload) {
22
24
  return payload;
23
25
  }
24
26
  }
27
+ /**
28
+ * ROUTINE_DATA_ACQUISITION_DESIGN.md §6.7 — turn the comma-separated
29
+ * `?source_prefix=gmail:,outlook_mail:` query string into a list the
30
+ * db helper can OR via per-prefix `LIKE` predicates. Returns
31
+ * `undefined` (not `[]`) when the param is absent so the db helper can
32
+ * cleanly distinguish "no multi-prefix filter requested" from "empty
33
+ * prefix list".
34
+ *
35
+ * Empty / whitespace-only segments are dropped here (the db helper
36
+ * trims again as a defense-in-depth measure) — a caller passing
37
+ * `?source_prefix=,gmail:,` shouldn't widen the query to every row.
38
+ */
39
+ function parseSourcePrefixes(raw) {
40
+ if (raw === undefined)
41
+ return undefined;
42
+ const parts = raw
43
+ .split(",")
44
+ .map((p) => p.trim())
45
+ .filter((p) => p.length > 0);
46
+ return parts.length > 0 ? parts : undefined;
47
+ }
48
+ /**
49
+ * ROUTINE_DATA_ACQUISITION_DESIGN.md CR2 — resolve `since=` /
50
+ * `observed_at_after=` from the request, validate the format, and
51
+ * coerce empty strings to `undefined` so the db filter is skipped
52
+ * rather than emitted as `datetime(observed_at) >= datetime("")`
53
+ * (which evaluates to NULL and silently drops every row).
54
+ *
55
+ * Returns:
56
+ * - `{ ok: true, value: undefined }` — neither param present, or
57
+ * both were empty / whitespace-only.
58
+ * - `{ ok: true, value: "<iso>" }` — a non-empty, parseable timestamp
59
+ * (canonical `since` wins over the `observed_at_after` alias when
60
+ * both are supplied).
61
+ * - `{ ok: false, raw, param }` — a non-empty value was provided but
62
+ * `Date.parse` rejected it. The route turns this into 400.
63
+ */
64
+ function resolveSinceParam(rawSince, rawAlias) {
65
+ const sinceClean = rawSince !== undefined ? rawSince.trim() : "";
66
+ const aliasClean = rawAlias !== undefined ? rawAlias.trim() : "";
67
+ if (sinceClean.length > 0) {
68
+ return Number.isFinite(Date.parse(sinceClean))
69
+ ? { ok: true, value: sinceClean }
70
+ : { ok: false, raw: sinceClean, param: "since" };
71
+ }
72
+ if (aliasClean.length > 0) {
73
+ return Number.isFinite(Date.parse(aliasClean))
74
+ ? { ok: true, value: aliasClean }
75
+ : { ok: false, raw: aliasClean, param: "observed_at_after" };
76
+ }
77
+ return { ok: true, value: undefined };
78
+ }
79
+ /**
80
+ * INTEGRATION_NATIVE_MODE_DESIGN.md §11.3.1 — map an observation `source`
81
+ * string to the integration key whose flip lock would gate writes against
82
+ * it. The agent's hourly_check / native-mode skill always uses one of the
83
+ * registry's integration keys verbatim as the `source` value (e.g.
84
+ * `"gmail"`, `"google_calendar"`, `"notion"`). Sources outside the
85
+ * registry (Obsidian, Git, messaging adapter, system) are never locked
86
+ * and return null.
87
+ *
88
+ * Matches the exact key first, then falls back to a colon prefix (e.g.
89
+ * `"gmail:account-1"`) so per-account / per-database source values still
90
+ * resolve. Returns null for anything else.
91
+ */
92
+ function inferIntegrationKeyFromSource(source) {
93
+ if (isIntegrationKey(source))
94
+ return source;
95
+ for (const key of INTEGRATION_KEYS) {
96
+ if (source.startsWith(`${key}:`))
97
+ return key;
98
+ }
99
+ return null;
100
+ }
25
101
  /**
26
102
  * cost-reduction-structural §A "Failure modes" — the summary may be
27
103
  * outdated when the worker lags far behind the observation moment (e.g.
@@ -63,18 +139,36 @@ export function createObservationRoutes(deps) {
63
139
  const limit = Math.min(Math.max(parseNumber(c.req.query("limit"), 20), 1), 100);
64
140
  const offset = Math.max(parseNumber(c.req.query("offset"), 0), 0);
65
141
  const source = c.req.query("source");
142
+ const sourcePrefixRaw = c.req.query("source_prefix");
66
143
  const actor = c.req.query("actor");
67
- const since = c.req.query("since");
144
+ // ROUTINE_DATA_ACQUISITION_DESIGN.md §6.7 / CR2 — `since=` is the
145
+ // canonical name; `observed_at_after=` is the routine-side alias
146
+ // (`since` wins when both are supplied). Empty / whitespace-only
147
+ // values are coerced to "no filter" rather than `datetime("")
148
+ // → NULL → drop all rows", and an unparseable string returns 400
149
+ // so misconfigured callers fail loud instead of silently filtering
150
+ // everything out.
151
+ const sinceResolution = resolveSinceParam(c.req.query("since"), c.req.query("observed_at_after"));
152
+ if (!sinceResolution.ok) {
153
+ return c.json({
154
+ error: "invalid_since",
155
+ param: sinceResolution.param,
156
+ value: sinceResolution.raw,
157
+ message: `'${sinceResolution.param}' must be an ISO 8601 timestamp or SQL UTC datetime`,
158
+ }, 400);
159
+ }
68
160
  if (actor && !["user", "agent", "system", "unknown"].includes(actor)) {
69
161
  return c.json({ error: "invalid_actor" }, 400);
70
162
  }
163
+ const sourceFilterPrefixes = parseSourcePrefixes(sourcePrefixRaw);
71
164
  const observations = getPendingObservations(db, {
72
165
  pending,
73
166
  limit,
74
167
  offset,
75
168
  sourceFilter: source,
169
+ sourceFilterPrefixes,
76
170
  actorFilter: actor,
77
- since,
171
+ since: sinceResolution.value,
78
172
  }).map((row) => ({
79
173
  id: row.id,
80
174
  source: row.source,
@@ -104,61 +198,633 @@ export function createObservationRoutes(deps) {
104
198
  *
105
199
  * Used by `routine.hourly_check` to queue `roadmap_candidate` signals
106
200
  * (long-horizon intents too weak to write to roadmap.md directly;
107
- * ROADMAP-REDESIGN §3.4 RFC-C). The DB layer UPSERTs on
108
- * `(source, ref)` where `consumed_at IS NULL`, so re-posting the same
109
- * candidate across hourly ticks coalesces instead of duplicating.
201
+ * ROADMAP-REDESIGN §3.4 RFC-C) AND by INTEGRATION_NATIVE_MODE_DESIGN.md
202
+ * §8.3 native-mode hourly_check turns to persist the materialised mail
203
+ * thread / calendar event list the agent just fetched via the main
204
+ * backend's MCP. The DB layer UPSERTs on `(source, ref)` where
205
+ * `consumed_at IS NULL`, so re-posting the same candidate across hourly
206
+ * ticks coalesces instead of duplicating.
207
+ *
208
+ * §8.3 server-side hash: the daemon computes `contentHash` from the
209
+ * canonical payload via the shared util in `@aitne/shared/observations-hash`
210
+ * and returns it in the response. Pollers and the delegated-sync-worker
211
+ * route through the same util so hashes are comparable across modes — a
212
+ * `delegated → native` flip dedups against pre-flip observations.
110
213
  *
111
214
  * Actor defaults to `agent` here — the user and system channels have
112
215
  * their own writers (vault watchers, mail poller, etc.). Permitting
113
216
  * only `agent` / `system` guards against prompt-injection attempts to
114
217
  * forge user-authored observations.
218
+ *
219
+ * §11.3.1 defensive lock-window check: if a mode-flip is currently in
220
+ * progress for the integration owning this `source`, reject the write
221
+ * with 409 to prevent straggler observations from landing under the
222
+ * old mode label. Sources outside the integration registry (Obsidian,
223
+ * Git, messaging adapter) are never rejected.
115
224
  */
225
+ const RECORD_EXPECTED_SHAPE = '{"source": string, "ref": string, "changeType"?: "created"|"modified"|"deleted", "actor"?: "agent"|"system", "payload"?: unknown}';
226
+ const RECORD_EXAMPLE = '{"source":"roadmap_candidate:travel","ref":"trip-portland-2026-summer","changeType":"created","actor":"agent","payload":{"note":"DM mentioned Portland trip"}}';
116
227
  app.post("/observations", async (c) => {
117
- const parsedBody = await readJsonBody(c);
118
- if (!parsedBody.ok)
119
- return parsedBody.response;
120
- const body = parsedBody.body;
121
- if (!body ||
122
- typeof body.source !== "string" ||
123
- typeof body.ref !== "string" ||
124
- (typeof body.changeType !== "string" &&
125
- body.changeType !== undefined)) {
126
- return c.json({ error: "validation_error" }, 400);
228
+ // Peek at the raw body BEFORE delegating to `readJsonBody` so we can
229
+ // turn a query-string-shaped body ("limit=30", "actor=user&limit=20")
230
+ // into a method-confusion hint. Production telemetry showed the
231
+ // hourly_check agent sending `POST /api/observations` with body
232
+ // `limit=30`, expecting it to fetch. Forwarding readJsonBody's
233
+ // generic "Unexpected token 'l'" message gave the agent no signal
234
+ // that the right call was `GET /api/observations?limit=30`.
235
+ //
236
+ // Size cap mirrors `readJsonBody`'s defense-in-depth: declared
237
+ // Content-Length AND post-read byteLength are both checked against
238
+ // `DEFAULT_JSON_BODY_MAX_BYTES` (1 MiB). Without this an inline
239
+ // reader would happily buffer a malicious 10 MB body and pin RAM.
240
+ const declared = c.req.header("content-length");
241
+ if (declared !== undefined) {
242
+ const declaredN = Number.parseInt(declared, 10);
243
+ if (Number.isFinite(declaredN) && declaredN > DEFAULT_JSON_BODY_MAX_BYTES) {
244
+ return c.json({
245
+ error: "body_too_large",
246
+ maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
247
+ actualBytes: declaredN,
248
+ }, 413);
249
+ }
250
+ }
251
+ let raw;
252
+ try {
253
+ raw = await c.req.text();
254
+ }
255
+ catch (err) {
256
+ const detail = err instanceof Error ? err.message : String(err);
257
+ return c.json({ error: "invalid_json_body", message: detail }, 400);
258
+ }
259
+ const actualBytes = Buffer.byteLength(raw, "utf-8");
260
+ if (actualBytes > DEFAULT_JSON_BODY_MAX_BYTES) {
261
+ return c.json({
262
+ error: "body_too_large",
263
+ maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
264
+ actualBytes,
265
+ }, 413);
266
+ }
267
+ const trimmed = raw.trim();
268
+ if (trimmed.length > 0 && trimmed[0] !== "{" && trimmed[0] !== "[") {
269
+ // A bare `key=value(&key=value)+` body shape is unambiguous:
270
+ // there is no JSON document that starts with a bare identifier
271
+ // followed by `=`. Suggest the GET form verbatim so the agent
272
+ // can copy-paste it.
273
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
274
+ return c.json({
275
+ error: "method_confusion",
276
+ message: "POST /api/observations records a new observation (JSON body). Your body looks like a query string — did you mean to GET?",
277
+ hint: `Use GET /api/observations?${trimmed} to fetch, or POST with a JSON body matching expectedShape to record.`,
278
+ expectedShape: RECORD_EXPECTED_SHAPE,
279
+ example: RECORD_EXAMPLE,
280
+ }, 400);
281
+ }
282
+ return c.json({
283
+ error: "invalid_json_body",
284
+ message: `Body must be a JSON object starting with '{' — received '${trimmed.slice(0, 32)}…'`,
285
+ expectedShape: RECORD_EXPECTED_SHAPE,
286
+ example: RECORD_EXAMPLE,
287
+ }, 400);
288
+ }
289
+ let body;
290
+ try {
291
+ body = trimmed.length === 0 ? null : JSON.parse(trimmed);
292
+ }
293
+ catch (err) {
294
+ const detail = err instanceof Error ? err.message : String(err);
295
+ return c.json({
296
+ error: "invalid_json_body",
297
+ message: detail,
298
+ expectedShape: RECORD_EXPECTED_SHAPE,
299
+ example: RECORD_EXAMPLE,
300
+ }, 400);
301
+ }
302
+ const issues = [];
303
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
304
+ return c.json({
305
+ error: "validation_error",
306
+ message: "Body must be a JSON object",
307
+ expectedShape: RECORD_EXPECTED_SHAPE,
308
+ example: RECORD_EXAMPLE,
309
+ }, 400);
310
+ }
311
+ if (typeof body.source !== "string" || body.source.length === 0) {
312
+ issues.push({
313
+ field: "source",
314
+ expected: "non-empty string",
315
+ got: body.source === undefined ? "missing" : typeof body.source,
316
+ hint: "Use a registry-aware prefix like 'roadmap_candidate:<subkind>' or an integration key like 'gmail:<account>'",
317
+ });
318
+ }
319
+ if (typeof body.ref !== "string" || body.ref.length === 0) {
320
+ issues.push({
321
+ field: "ref",
322
+ expected: "non-empty string",
323
+ got: body.ref === undefined ? "missing" : typeof body.ref,
324
+ hint: "A stable identifier within the source — e.g. message id, file path, candidate slug",
325
+ });
326
+ }
327
+ if (typeof body.changeType !== "string" &&
328
+ body.changeType !== undefined) {
329
+ issues.push({
330
+ field: "changeType",
331
+ expected: "'created' | 'modified' | 'deleted' (optional, defaults to 'created')",
332
+ got: typeof body.changeType,
333
+ });
127
334
  }
335
+ if (issues.length > 0) {
336
+ return c.json({
337
+ error: "validation_error",
338
+ message: "Request body failed schema validation",
339
+ expectedShape: RECORD_EXPECTED_SHAPE,
340
+ example: RECORD_EXAMPLE,
341
+ issues,
342
+ }, 400);
343
+ }
344
+ // The `issues` array above guarantees `body.source` and `body.ref`
345
+ // are non-empty strings; cast for the rest of the handler.
346
+ const source = body.source;
347
+ const ref = body.ref;
128
348
  const changeType = typeof body.changeType === "string" ? body.changeType : "created";
129
349
  if (!["created", "modified", "deleted"].includes(changeType)) {
130
- return c.json({ error: "invalid_change_type" }, 400);
350
+ return c.json({
351
+ error: "invalid_change_type",
352
+ message: `'changeType' must be one of 'created', 'modified', 'deleted' — received '${changeType}'`,
353
+ hint: "Omit the field to default to 'created'",
354
+ }, 400);
131
355
  }
132
356
  const actor = typeof body.actor === "string" ? body.actor : "agent";
133
357
  if (!["agent", "system"].includes(actor)) {
134
- return c.json({ error: "invalid_actor" }, 400);
358
+ return c.json({
359
+ error: "invalid_actor",
360
+ message: `'actor' must be 'agent' or 'system' — received '${actor}'`,
361
+ hint: "User-originated observations come in through the vault / mail watchers; this endpoint only accepts agent/system writes",
362
+ }, 400);
363
+ }
364
+ // §11.3.1 — defensive flip-lock check. The integration key inferred
365
+ // from the source is the same key the PATCH route would have locked.
366
+ // If a flip is mid-flight, return 409 so the agent retries after the
367
+ // drain completes. Sources that don't map to a registered integration
368
+ // are pass-through (Obsidian / Git / messaging never lock).
369
+ const lockedKey = inferIntegrationKeyFromSource(source);
370
+ if (lockedKey) {
371
+ const lock = readIntegrationFlipLock(db, lockedKey);
372
+ if (lock) {
373
+ logger.warn({ source: body.source, ref: body.ref, lockedKey, lock }, "Observation write rejected — integration flip lock held");
374
+ return c.json({
375
+ error: "integration_flip_in_progress",
376
+ integration: lockedKey,
377
+ heldBy: lock,
378
+ message: `A mode flip for '${lockedKey}' is in progress; retry shortly.`,
379
+ }, 409);
380
+ }
135
381
  }
136
- recordObservation(db, {
137
- source: body.source,
138
- ref: body.ref,
382
+ const result = recordObservation(db, {
383
+ source,
384
+ ref,
139
385
  changeType: changeType,
140
386
  actor: actor,
141
387
  payload: body.payload,
142
388
  });
143
- logger.info({ source: body.source, ref: body.ref, actor }, "Observation recorded via API");
144
- return c.json({ ok: true });
389
+ logger.info({
390
+ source,
391
+ ref,
392
+ actor,
393
+ contentHash: result.contentHash,
394
+ action: result.action,
395
+ }, "Observation recorded via API");
396
+ // ROUTINE_DATA_ACQUISITION_DESIGN.md CR1 — surface payload-identical
397
+ // re-posts as 409 so the routine pre-pass fetcher's JSON return
398
+ // shape (`{"fetched":N,"posted":M,"duplicates":K,"errors":[…]}`)
399
+ // can count them. The 409 body's `error` field distinguishes this
400
+ // case from the §11.3.1 `integration_flip_in_progress` 409 above;
401
+ // callers that don't care about the distinction still read `error`
402
+ // before counting.
403
+ if (result.action === "duplicate") {
404
+ return c.json({
405
+ error: "duplicate",
406
+ contentHash: result.contentHash,
407
+ id: result.id,
408
+ message: "Same (source, ref) pending row already stores this payload",
409
+ }, 409);
410
+ }
411
+ return c.json({
412
+ ok: true,
413
+ contentHash: result.contentHash,
414
+ id: result.id,
415
+ action: result.action,
416
+ });
417
+ });
418
+ /**
419
+ * POST /observations/batch — record many agent-originated observations in a
420
+ * single transaction.
421
+ *
422
+ * Reason this exists: the routine.fetch_window pre-pass on Haiku posts
423
+ * many observations per integration window (~20 mail messages, ~6 calendar
424
+ * events). Calling `POST /observations` once per item collided with two
425
+ * orthogonal safety layers in `claude-tool-collection.ts:bashCurlHook`:
426
+ *
427
+ * 1. The "one curl per Bash invocation" cap blocks `cat | bash`, chained
428
+ * `curl … ; curl …`, and `for` loops containing curl.
429
+ * 2. URL extraction strips heredoc bodies before validation, so a
430
+ * `cat > /tmp/script.sh << 'EOF' … curl http://localhost:8321/… EOF`
431
+ * batching shape blocks with "curl command must contain an explicit
432
+ * localhost URL" — the URL lives in the stdin payload, not argv.
433
+ *
434
+ * Production telemetry on 2026-05-13 morning routine showed Haiku
435
+ * burning four budget cycles and posting zero observations because every
436
+ * batching shape it tried was blocked, leaving `today.md` empty. The
437
+ * single-curl-with-array endpoint resolves the cardinality mismatch
438
+ * without weakening either hook.
439
+ *
440
+ * Body: `{ "observations": [...] }` with up to 200 entries per call.
441
+ * Per-item validation mirrors POST /observations exactly. The whole
442
+ * batch executes inside one `db.transaction()`; any per-item failure is
443
+ * recorded in the response and the rest of the batch proceeds.
444
+ *
445
+ * Response is always 200 (or 400 for a malformed envelope). Per-item
446
+ * outcomes live in `results[*].status` so the agent doesn't retry the
447
+ * whole batch on a partial failure.
448
+ */
449
+ const BATCH_MAX_OBSERVATIONS = 200;
450
+ const BATCH_EXPECTED_SHAPE = '{"observations": [{"source": string, "ref": string, "changeType"?: "created"|"modified"|"deleted", "actor"?: "agent"|"system", "payload"?: unknown}, ...]}';
451
+ const BATCH_EXAMPLE = '{"observations":[{"source":"google_calendar:primary","ref":"evt-1","payload":{"kind":"calendar","providerId":"primary","raw":{"title":"…"}}},{"source":"google_calendar:primary","ref":"evt-2","payload":{"kind":"calendar","providerId":"primary","raw":{"title":"…"}}}]}';
452
+ function validateBatchItem(item, index) {
453
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
454
+ return {
455
+ ok: false,
456
+ result: {
457
+ index,
458
+ status: "validation_error",
459
+ error: "item must be a JSON object",
460
+ hint: BATCH_EXPECTED_SHAPE,
461
+ },
462
+ };
463
+ }
464
+ const obj = item;
465
+ if (typeof obj.source !== "string" || obj.source.length === 0) {
466
+ return {
467
+ ok: false,
468
+ result: {
469
+ index,
470
+ status: "validation_error",
471
+ error: "'source' must be a non-empty string",
472
+ hint: "Use 'gmail:<account>', 'google_calendar:<calendarId>', 'notion:<dbId>', etc.",
473
+ },
474
+ };
475
+ }
476
+ if (typeof obj.ref !== "string" || obj.ref.length === 0) {
477
+ return {
478
+ ok: false,
479
+ result: {
480
+ index,
481
+ status: "validation_error",
482
+ source: obj.source,
483
+ error: "'ref' must be a non-empty string",
484
+ hint: "Stable id within the source — e.g. message id, event id",
485
+ },
486
+ };
487
+ }
488
+ const changeType = typeof obj.changeType === "string" ? obj.changeType : "created";
489
+ if (!["created", "modified", "deleted"].includes(changeType)) {
490
+ return {
491
+ ok: false,
492
+ result: {
493
+ index,
494
+ status: "validation_error",
495
+ source: obj.source,
496
+ ref: obj.ref,
497
+ error: `'changeType' must be 'created'|'modified'|'deleted' — received '${changeType}'`,
498
+ },
499
+ };
500
+ }
501
+ const actor = typeof obj.actor === "string" ? obj.actor : "agent";
502
+ if (!["agent", "system"].includes(actor)) {
503
+ return {
504
+ ok: false,
505
+ result: {
506
+ index,
507
+ status: "validation_error",
508
+ source: obj.source,
509
+ ref: obj.ref,
510
+ error: `'actor' must be 'agent' or 'system' — received '${actor}'`,
511
+ },
512
+ };
513
+ }
514
+ return {
515
+ ok: true,
516
+ source: obj.source,
517
+ ref: obj.ref,
518
+ changeType: changeType,
519
+ actor: actor,
520
+ payload: obj.payload,
521
+ };
522
+ }
523
+ app.post("/observations/batch", async (c) => {
524
+ // Size-cap defense mirrors POST /observations.
525
+ const declared = c.req.header("content-length");
526
+ if (declared !== undefined) {
527
+ const declaredN = Number.parseInt(declared, 10);
528
+ if (Number.isFinite(declaredN) && declaredN > DEFAULT_JSON_BODY_MAX_BYTES) {
529
+ return c.json({
530
+ error: "body_too_large",
531
+ maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
532
+ actualBytes: declaredN,
533
+ }, 413);
534
+ }
535
+ }
536
+ let raw;
537
+ try {
538
+ raw = await c.req.text();
539
+ }
540
+ catch (err) {
541
+ const detail = err instanceof Error ? err.message : String(err);
542
+ return c.json({ error: "invalid_json_body", message: detail }, 400);
543
+ }
544
+ const actualBytes = Buffer.byteLength(raw, "utf-8");
545
+ if (actualBytes > DEFAULT_JSON_BODY_MAX_BYTES) {
546
+ return c.json({
547
+ error: "body_too_large",
548
+ maxBytes: DEFAULT_JSON_BODY_MAX_BYTES,
549
+ actualBytes,
550
+ }, 413);
551
+ }
552
+ let envelope;
553
+ try {
554
+ envelope = raw.trim().length === 0 ? null : JSON.parse(raw);
555
+ }
556
+ catch (err) {
557
+ const detail = err instanceof Error ? err.message : String(err);
558
+ return c.json({
559
+ error: "invalid_json_body",
560
+ message: detail,
561
+ expectedShape: BATCH_EXPECTED_SHAPE,
562
+ example: BATCH_EXAMPLE,
563
+ }, 400);
564
+ }
565
+ if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
566
+ return c.json({
567
+ error: "validation_error",
568
+ message: "Body must be a JSON object with an 'observations' array",
569
+ expectedShape: BATCH_EXPECTED_SHAPE,
570
+ example: BATCH_EXAMPLE,
571
+ }, 400);
572
+ }
573
+ if (!Array.isArray(envelope.observations)) {
574
+ return c.json({
575
+ error: "validation_error",
576
+ message: "'observations' must be an array",
577
+ expectedShape: BATCH_EXPECTED_SHAPE,
578
+ example: BATCH_EXAMPLE,
579
+ hint: "Wrap your observation objects in an 'observations' array — POST {\"observations\":[…]}",
580
+ }, 400);
581
+ }
582
+ if (envelope.observations.length === 0) {
583
+ // Empty batch is a documented no-op so the pre-pass can emit a
584
+ // zero-event window without a 400 stutter.
585
+ return c.json({
586
+ results: [],
587
+ fetched: 0,
588
+ posted: 0,
589
+ duplicates: 0,
590
+ errors: 0,
591
+ });
592
+ }
593
+ if (envelope.observations.length > BATCH_MAX_OBSERVATIONS) {
594
+ return c.json({
595
+ error: "batch_too_large",
596
+ message: `Batch size ${envelope.observations.length} exceeds maximum ${BATCH_MAX_OBSERVATIONS}`,
597
+ maxItems: BATCH_MAX_OBSERVATIONS,
598
+ hint: "Split the batch into chunks of at most 200 items.",
599
+ }, 400);
600
+ }
601
+ const results = [];
602
+ let posted = 0;
603
+ let duplicates = 0;
604
+ let errorCount = 0;
605
+ // Single explicit transaction wraps the whole batch — better-sqlite3
606
+ // transactions amortise the per-statement overhead to ~30-100x for
607
+ // bulk inserts, which is the primary perf win this endpoint offers
608
+ // beyond bypassing the bashCurlHook one-curl cap. The flip-lock check
609
+ // stays per-item so a mixed-integration batch (defensive — the
610
+ // pre-pass scopes each sub-session to one integration, but the
611
+ // endpoint must hold under any caller) does not block on the first
612
+ // unrelated lock.
613
+ const writeBatch = db.transaction((items) => {
614
+ for (let i = 0; i < items.length; i++) {
615
+ const validated = validateBatchItem(items[i], i);
616
+ if (!validated.ok) {
617
+ results.push(validated.result);
618
+ errorCount += 1;
619
+ continue;
620
+ }
621
+ const lockedKey = inferIntegrationKeyFromSource(validated.source);
622
+ if (lockedKey) {
623
+ const lock = readIntegrationFlipLock(db, lockedKey);
624
+ if (lock) {
625
+ logger.warn({ source: validated.source, ref: validated.ref, lockedKey, lock }, "Batch observation write rejected — integration flip lock held");
626
+ results.push({
627
+ index: i,
628
+ status: "flip_locked",
629
+ source: validated.source,
630
+ ref: validated.ref,
631
+ error: `Integration '${lockedKey}' flip in progress`,
632
+ });
633
+ errorCount += 1;
634
+ continue;
635
+ }
636
+ }
637
+ const result = recordObservation(db, {
638
+ source: validated.source,
639
+ ref: validated.ref,
640
+ changeType: validated.changeType,
641
+ actor: validated.actor,
642
+ payload: validated.payload,
643
+ });
644
+ if (result.action === "duplicate") {
645
+ duplicates += 1;
646
+ results.push({
647
+ index: i,
648
+ status: "duplicate",
649
+ source: validated.source,
650
+ ref: validated.ref,
651
+ contentHash: result.contentHash,
652
+ id: result.id,
653
+ });
654
+ }
655
+ else {
656
+ posted += 1;
657
+ results.push({
658
+ index: i,
659
+ status: result.action,
660
+ source: validated.source,
661
+ ref: validated.ref,
662
+ contentHash: result.contentHash,
663
+ id: result.id,
664
+ });
665
+ }
666
+ }
667
+ });
668
+ writeBatch(envelope.observations);
669
+ logger.info({
670
+ count: envelope.observations.length,
671
+ posted,
672
+ duplicates,
673
+ errors: errorCount,
674
+ }, "Observations batch recorded via API");
675
+ return c.json({
676
+ results,
677
+ fetched: envelope.observations.length,
678
+ posted,
679
+ duplicates,
680
+ errors: errorCount,
681
+ });
145
682
  });
683
+ /**
684
+ * Field-level validation contract for `POST /observations/consume`.
685
+ *
686
+ * Production telemetry (2026-05) showed a single Stage-3 hourly_check
687
+ * burning $0.58 / 25 turns retrying this endpoint with shape variants
688
+ * (`correlation_id` snake_case, stringified ids, the angle-bracket
689
+ * placeholder copied verbatim, per-id paths, etc.). The legacy
690
+ * `{ error: "validation_error" }` response gave the agent zero signal
691
+ * about which field was wrong, so it would mutate a random field and
692
+ * retry. Returning the full schema + the specific issue + a one-line
693
+ * hint lets the agent self-correct on the next turn instead of the
694
+ * eighth.
695
+ */
696
+ const CONSUME_EXPECTED_SHAPE = '{"ids": number[], "correlationId": string}';
697
+ const CONSUME_EXAMPLE = '{"ids":[14,17],"correlationId":"hourly-2026-04-23T15:00:00Z-7af3"}';
146
698
  app.post("/observations/consume", async (c) => {
147
699
  const parsedBody = await readJsonBody(c);
148
700
  if (!parsedBody.ok)
149
701
  return parsedBody.response;
150
702
  const body = parsedBody.body;
151
- if (!body || !Array.isArray(body.ids) || typeof body.correlationId !== "string") {
152
- return c.json({ error: "validation_error" }, 400);
703
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
704
+ return c.json({
705
+ error: "validation_error",
706
+ message: "Body must be a JSON object",
707
+ expectedShape: CONSUME_EXPECTED_SHAPE,
708
+ example: CONSUME_EXAMPLE,
709
+ }, 400);
710
+ }
711
+ const issues = [];
712
+ if (body.correlation_id !== undefined &&
713
+ body.correlationId === undefined) {
714
+ issues.push({
715
+ field: "correlationId",
716
+ expected: "string (camelCase)",
717
+ got: "received 'correlation_id' (snake_case) instead",
718
+ hint: "Rename the field to camelCase 'correlationId' — value is the verbatim id from the <event_correlation_id> tag in your prompt context",
719
+ });
720
+ }
721
+ else if (typeof body.correlationId !== "string") {
722
+ issues.push({
723
+ field: "correlationId",
724
+ expected: "string",
725
+ got: body.correlationId === undefined
726
+ ? "missing"
727
+ : typeof body.correlationId,
728
+ hint: "Copy verbatim from <event_correlation_id>…</event_correlation_id> in your prompt context",
729
+ });
153
730
  }
154
- const ids = body.ids.filter((id) => typeof id === "number" && Number.isInteger(id));
155
- if (ids.length !== body.ids.length) {
156
- return c.json({ error: "validation_error" }, 400);
731
+ else if (body.correlationId.trim().length === 0) {
732
+ issues.push({
733
+ field: "correlationId",
734
+ expected: "non-empty string",
735
+ got: "empty string",
736
+ hint: "Copy verbatim from <event_correlation_id>…</event_correlation_id> in your prompt context",
737
+ });
157
738
  }
158
- const result = consumeObservations(db, ids, body.correlationId);
159
- logger.info({ consumed: result.consumed, correlationId: body.correlationId }, "Observations consumed");
739
+ else if (body.correlationId.startsWith("<") &&
740
+ body.correlationId.endsWith(">")) {
741
+ issues.push({
742
+ field: "correlationId",
743
+ expected: "verbatim correlation id",
744
+ got: `placeholder text '${body.correlationId}'`,
745
+ hint: "Use the real id from <event_correlation_id>…</event_correlation_id>, not the angle-bracket placeholder",
746
+ });
747
+ }
748
+ if (!Array.isArray(body.ids)) {
749
+ issues.push({
750
+ field: "ids",
751
+ expected: "number[]",
752
+ got: body.ids === undefined ? "missing" : typeof body.ids,
753
+ hint: "Array of integer observation row ids — e.g. [14, 17]",
754
+ });
755
+ }
756
+ else if (body.ids.length > 0) {
757
+ // Empty array is a documented no-op (preserves the legacy
758
+ // contract: `consumeObservations` returns `{consumed:0,notFound:[]}`
759
+ // for an empty list). Only validate element shape when the array
760
+ // actually has content.
761
+ const stringIds = body.ids.filter((id) => typeof id === "string");
762
+ if (stringIds.length > 0) {
763
+ issues.push({
764
+ field: "ids",
765
+ expected: "number[]",
766
+ got: `array contains strings (e.g. ${JSON.stringify(stringIds.slice(0, 3))})`,
767
+ hint: 'Use integers, not strings — [14, 17] not ["14", "17"]',
768
+ });
769
+ }
770
+ else {
771
+ const nonInt = body.ids.find((id) => typeof id !== "number" || !Number.isInteger(id));
772
+ if (nonInt !== undefined) {
773
+ issues.push({
774
+ field: "ids",
775
+ expected: "number[] (integers)",
776
+ got: `array contains non-integer value ${JSON.stringify(nonInt)}`,
777
+ hint: "ids must be integer observation row ids returned by GET /api/observations",
778
+ });
779
+ }
780
+ }
781
+ }
782
+ if (issues.length > 0) {
783
+ return c.json({
784
+ error: "validation_error",
785
+ message: "Request body failed schema validation",
786
+ expectedShape: CONSUME_EXPECTED_SHAPE,
787
+ example: CONSUME_EXAMPLE,
788
+ issues,
789
+ }, 400);
790
+ }
791
+ const ids = body.ids;
792
+ const correlationId = body.correlationId;
793
+ const result = consumeObservations(db, ids, correlationId);
794
+ logger.info({ consumed: result.consumed, correlationId }, "Observations consumed");
160
795
  return c.json(result);
161
796
  });
797
+ /**
798
+ * Helpful 405 for the per-id consume shape the agent has reached for in
799
+ * production (`POST /api/observations/:id/consume`). Without an explicit
800
+ * handler this path falls through to Hono's 404 with body
801
+ * `"404 Not Found"`, which gives the agent nothing to act on. Returning
802
+ * a 405 with the canonical bulk-endpoint hint pulls the agent back onto
803
+ * the correct shape on the next turn.
804
+ */
805
+ app.all("/observations/:id/consume", (c) => {
806
+ const id = c.req.param("id");
807
+ return c.json({
808
+ error: "use_bulk_endpoint",
809
+ message: "Per-id consume is not supported. Use the bulk endpoint with a single-element ids array.",
810
+ expectedShape: CONSUME_EXPECTED_SHAPE,
811
+ example: CONSUME_EXAMPLE,
812
+ hint: `POST /api/observations/consume with body {"ids":[${id}],"correlationId":"<copy from <event_correlation_id>>"}`,
813
+ }, 405);
814
+ });
815
+ /**
816
+ * Helpful 405 for `GET /api/observations/consume`. The bulk consume is
817
+ * POST-only — without this handler the request 404s with no actionable
818
+ * detail, and the agent's recovery loop produced 8x retries in one
819
+ * routine.hourly_check session.
820
+ */
821
+ app.get("/observations/consume", (c) => c.json({
822
+ error: "method_not_allowed",
823
+ message: "GET is not supported on /api/observations/consume — use POST.",
824
+ expectedShape: CONSUME_EXPECTED_SHAPE,
825
+ example: CONSUME_EXAMPLE,
826
+ hint: `POST /api/observations/consume with body ${CONSUME_EXAMPLE}`,
827
+ }, 405, { Allow: "POST" }));
162
828
  app.get("/observations/stats", (c) => {
163
829
  const stats = getObservationStats(db);
164
830
  // cost-reduction-structural §A telemetry — surface summarizer health