@aitne/daemon 0.1.9 → 0.1.11

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 (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -45,6 +45,7 @@ function fromDbRow(row) {
45
45
  createdAt: row.created_at,
46
46
  startedAt: row.started_at,
47
47
  finishedAt: row.finished_at,
48
+ deliveredAt: row.delivered_at,
48
49
  };
49
50
  }
50
51
  /** Insert a fresh row in state=pending. The slot manager promotes it
@@ -55,8 +56,8 @@ export function createBrowserTask(db, input) {
55
56
  originating_channel, schedule_row_id, require_final_confirm,
56
57
  state, outcome_detail, report, effective_allowlist_regex,
57
58
  blocked_requests_count, extract_chars_total,
58
- created_at, started_at, finished_at)
59
- VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, 0, 0, ?, NULL, NULL)`).run(input.id, input.description, input.siteKey, JSON.stringify([...input.extraAllowedHosts]), input.originatingChannel, input.scheduleRowId, input.requireFinalConfirm ? 1 : 0, input.effectiveAllowlistRegex, input.createdAt);
59
+ created_at, started_at, finished_at, delivered_at)
60
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, 0, 0, ?, NULL, NULL, NULL)`).run(input.id, input.description, input.siteKey, JSON.stringify([...input.extraAllowedHosts]), input.originatingChannel, input.scheduleRowId, input.requireFinalConfirm ? 1 : 0, input.effectiveAllowlistRegex, input.createdAt);
60
61
  const row = getBrowserTask(db, input.id);
61
62
  if (!row) {
62
63
  throw new Error(`createBrowserTask: post-insert row for ${input.id} missing`);
@@ -69,7 +70,7 @@ export function getBrowserTask(db, id) {
69
70
  originating_channel, schedule_row_id, require_final_confirm,
70
71
  state, outcome_detail, report, effective_allowlist_regex,
71
72
  blocked_requests_count, extract_chars_total,
72
- created_at, started_at, finished_at
73
+ created_at, started_at, finished_at, delivered_at
73
74
  FROM browser_task
74
75
  WHERE id = ?`)
75
76
  .get(id);
@@ -96,7 +97,7 @@ export function listBrowserTasks(db, options = {}) {
96
97
  originating_channel, schedule_row_id, require_final_confirm,
97
98
  state, outcome_detail, report, effective_allowlist_regex,
98
99
  blocked_requests_count, extract_chars_total,
99
- created_at, started_at, finished_at
100
+ created_at, started_at, finished_at, delivered_at
100
101
  FROM browser_task
101
102
  ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
102
103
  ORDER BY created_at DESC
@@ -183,6 +184,30 @@ export function markTerminal(db, input) {
183
184
  .run(input.state, input.outcomeDetail, input.report, input.finishedAt, input.id);
184
185
  return result.changes > 0 ? getBrowserTask(db, input.id) : null;
185
186
  }
187
+ export function markBrowserTaskDelivered(db, id, deliveredAt) {
188
+ const result = db
189
+ .prepare(`UPDATE browser_task
190
+ SET delivered_at = COALESCE(delivered_at, ?)
191
+ WHERE id = ?`)
192
+ .run(deliveredAt, id);
193
+ return result.changes > 0 ? getBrowserTask(db, id) : null;
194
+ }
195
+ export function listUndeliveredBrowserTaskReports(db, limit = 20) {
196
+ const rows = db
197
+ .prepare(`SELECT id, description, site_key, extra_allowed_hosts_json,
198
+ originating_channel, schedule_row_id, require_final_confirm,
199
+ state, outcome_detail, report, effective_allowlist_regex,
200
+ blocked_requests_count, extract_chars_total,
201
+ created_at, started_at, finished_at, delivered_at
202
+ FROM browser_task
203
+ WHERE state = 'completed'
204
+ AND report IS NOT NULL
205
+ AND delivered_at IS NULL
206
+ ORDER BY finished_at ASC, created_at ASC
207
+ LIMIT ?`)
208
+ .all(limit);
209
+ return rows.map(fromDbRow);
210
+ }
186
211
  /** Increment the per-task CDP-blocked counter. Atomic. */
187
212
  export function incrementBlockedRequests(db, id, by) {
188
213
  db.prepare(`UPDATE browser_task
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Quiet-hours DM deferral — QUIET_HOURS_HARDENING_PLAN.md Phase 1.
3
+ *
4
+ * When an autonomous outbound DM fires inside the quiet-hours window, the
5
+ * message is NOT sent and NOT dropped: it is persisted as a
6
+ * `task_type='dm'` `agent_schedule` row scheduled for the quiet-hours end
7
+ * boundary. That reuses the existing zero-cost pre-composed-DM machinery
8
+ * end to end — full message in `task_description` (no truncation),
9
+ * restart-safe, visible on the `/schedule` dashboard page, delivered by
10
+ * the scheduler's `handleDirectDm` (whose deliberate quiet-hours skip is
11
+ * correct here: the row fires *at* the quiet-hours edge).
12
+ *
13
+ * Pile-up guard: a second deferral from the same origin (same
14
+ * `deferred_from` + agent id / origin session id) appends to the existing
15
+ * pending row (two-blank-line join, the batch-flush convention) instead
16
+ * of inserting a sibling — an hourly Agent firing five times overnight
17
+ * yields one combined DM at the edge, not five.
18
+ *
19
+ * Shared by the `/api/notify` gate (Phase 1) and, later, the
20
+ * NotificationManager `scheduled.task` final-text branch (Phase 1b) —
21
+ * one implementation instead of bespoke `agent_schedule` SQL per caller.
22
+ */
23
+ import type Database from "better-sqlite3";
24
+ import { type QuietHoursWindow } from "../core/quiet-hours.js";
25
+ export interface DeferDmParams {
26
+ /** Full message body — persisted untruncated in `task_description`. */
27
+ message: string;
28
+ /** Explicit platform targets; omitted → MessageHub default destinations. */
29
+ platforms?: string[] | undefined;
30
+ /** Origin marker for audit + coalescing, e.g. `"api.notify"`. */
31
+ deferredFrom: string;
32
+ /** Session that produced the message, when known. */
33
+ originSessionId?: number | undefined;
34
+ /** Owning user-Agent slug, when resolvable. */
35
+ agentId?: string | null | undefined;
36
+ }
37
+ export interface DeferredDmResult {
38
+ scheduleId: string;
39
+ /** SQLite-format UTC datetime the row fires at (quiet-hours end). */
40
+ deliverAfter: string;
41
+ /** True when appended to an existing pending deferred row. */
42
+ coalesced: boolean;
43
+ }
44
+ /**
45
+ * Gate decision + deferred-row insert. Returns `null` when `now` is NOT
46
+ * inside the quiet-hours window (caller proceeds with the immediate
47
+ * path); otherwise persists the message for the quiet-hours edge and
48
+ * returns the row handle.
49
+ */
50
+ export declare function deferDmToQuietHoursEnd(db: Database.Database, window: QuietHoursWindow, params: DeferDmParams, now?: Date): DeferredDmResult | null;
51
+ /**
52
+ * Retime pending quiet-hours-deferred DM rows after a quiet-hours config
53
+ * change (the `syncDmSessionTimesToQuietHours` sibling for this module's
54
+ * rows). The deferral premise — "the row fires *at* the quiet-hours edge,
55
+ * so `handleDirectDm`'s quiet-hours skip is correct" — only holds while
56
+ * the window that produced `scheduled_for` is still the configured one:
57
+ *
58
+ * - window extended (end moved later) → without retiming the row fires
59
+ * *inside* the new quiet window;
60
+ * - window shortened (end moved earlier) → the row waits past the new
61
+ * edge for no reason.
62
+ *
63
+ * Rule: inside the new window every deferred row moves to the new edge;
64
+ * outside it, future-dated rows are pulled up to `now` (the next scheduler
65
+ * tick delivers them) and already-due rows are left for the tick to claim.
66
+ * User-scheduled `dm` rows (no `deferred_from` marker) are never touched.
67
+ * Returns the number of rows retimed.
68
+ */
69
+ export declare function retimeDeferredDmRows(db: Database.Database, window: QuietHoursWindow, now?: Date): number;
70
+ /**
71
+ * Retime pending quiet-hours-deferred RUN rows (`agent.task` opt-in and
72
+ * `browser_task`) after a quiet-hours config change — the
73
+ * `retimeDeferredDmRows` sibling for rows the ScheduleWatcher's
74
+ * `deferClaimedRowForQuietHours` pushed to the old window's end. That helper
75
+ * stamps `task_context.quiet_hours_deferred` at deferral time for exactly
76
+ * this purpose: rows that merely *carry* the `defer_in_quiet_hours` opt-in on
77
+ * a future cron slot were never deferred and keep their cron-scheduled time.
78
+ *
79
+ * Unlike deferred DM rows (delivered by `handleDirectDm`, which skips the
80
+ * quiet-hours check by design), run rows re-check the window at claim time,
81
+ * so a *widened* window self-corrects — retiming it here merely skips the
82
+ * wasted claim/re-defer cycle and its duplicate audit row. The
83
+ * narrowed/disabled direction is the real fix: without retiming, the run
84
+ * waits at the old window's end for no reason.
85
+ */
86
+ export declare function retimeDeferredRunRows(db: Database.Database, window: QuietHoursWindow, now?: Date): number;
@@ -0,0 +1,199 @@
1
+ import { formatSqliteDatetime } from "@aitne/shared";
2
+ import { nextQuietHoursEndMs, } from "../core/quiet-hours.js";
3
+ import { createLogger } from "../logging.js";
4
+ const logger = createLogger("deferred-dm");
5
+ /**
6
+ * Coalescing identity: prefer the Agent slug (stable across an Agent's
7
+ * overnight firings), fall back to the origin session, else an anonymous
8
+ * shared bucket. Unrelated anonymous system DMs deferring into one
9
+ * combined message is intended — that's the pile-up guard, not a bug.
10
+ */
11
+ function coalesceKey(agentId, originSessionId) {
12
+ if (agentId)
13
+ return `agent:${agentId}`;
14
+ if (originSessionId !== null && originSessionId !== undefined) {
15
+ return `session:${originSessionId}`;
16
+ }
17
+ return "anonymous";
18
+ }
19
+ /** Union of explicit platform targets; either side defaulting (null /
20
+ * undefined = MessageHub default destinations) keeps the default. */
21
+ function mergePlatforms(existing, incoming) {
22
+ if (!Array.isArray(existing) || incoming === undefined)
23
+ return null;
24
+ return [...new Set([...existing, ...incoming])];
25
+ }
26
+ /**
27
+ * Gate decision + deferred-row insert. Returns `null` when `now` is NOT
28
+ * inside the quiet-hours window (caller proceeds with the immediate
29
+ * path); otherwise persists the message for the quiet-hours edge and
30
+ * returns the row handle.
31
+ */
32
+ export function deferDmToQuietHoursEnd(db, window, params, now = new Date()) {
33
+ const quietEndMs = nextQuietHoursEndMs(now, window);
34
+ if (quietEndMs === null)
35
+ return null;
36
+ const key = coalesceKey(params.agentId, params.originSessionId);
37
+ // `json_valid` guard first: a hand-edited row with broken JSON would
38
+ // otherwise make `json_extract` throw for the whole query. Such a row
39
+ // simply never coalesces; a fresh row carries this message.
40
+ const pending = db
41
+ .prepare(`SELECT id, task_description, task_context, scheduled_for
42
+ FROM agent_schedule
43
+ WHERE status = 'pending'
44
+ AND task_type = 'dm'
45
+ AND task_context IS NOT NULL
46
+ AND json_valid(task_context)
47
+ AND json_extract(task_context, '$.deferred_from') = ?
48
+ ORDER BY id ASC`)
49
+ .all(params.deferredFrom);
50
+ for (const row of pending) {
51
+ // json_valid in the WHERE clause guarantees parseability here.
52
+ const ctx = JSON.parse(row.task_context);
53
+ const rowKey = coalesceKey(typeof ctx.agent_id === "string" ? ctx.agent_id : null, typeof ctx.origin_session_id === "number" ? ctx.origin_session_id : null);
54
+ if (rowKey !== key)
55
+ continue;
56
+ ctx.platforms = mergePlatforms(ctx.platforms, params.platforms);
57
+ db.prepare(`UPDATE agent_schedule
58
+ SET task_description = ?, task_context = ?
59
+ WHERE id = ?`).run(`${row.task_description}\n\n${params.message}`, JSON.stringify(ctx), row.id);
60
+ const result = {
61
+ scheduleId: String(row.id),
62
+ deliverAfter: row.scheduled_for,
63
+ coalesced: true,
64
+ };
65
+ recordDeferralAudit(db, window, params, result);
66
+ return result;
67
+ }
68
+ const deliverAfter = formatSqliteDatetime(new Date(quietEndMs));
69
+ const taskContext = {
70
+ platforms: params.platforms ?? null,
71
+ // Matches the `/schedule/dm` default — deferred pings stay out of
72
+ // roadmap `Scheduled:` entries.
73
+ importance: "transient",
74
+ deferred_from: params.deferredFrom,
75
+ ...(params.originSessionId !== undefined
76
+ ? { origin_session_id: params.originSessionId }
77
+ : {}),
78
+ ...(params.agentId ? { agent_id: params.agentId } : {}),
79
+ };
80
+ const inserted = db
81
+ .prepare(
82
+ // task_type='dm' is consumed directly by `handleDirectDm` — the LLM
83
+ // never runs, so `model` is NULL (same shape as POST /schedule/dm).
84
+ `INSERT INTO agent_schedule (scheduled_for, task_type, task_description, task_context, model, status)
85
+ VALUES (?, 'dm', ?, ?, NULL, 'pending')`)
86
+ .run(deliverAfter, params.message, JSON.stringify(taskContext));
87
+ const result = {
88
+ scheduleId: String(inserted.lastInsertRowid),
89
+ deliverAfter,
90
+ coalesced: false,
91
+ };
92
+ recordDeferralAudit(db, window, params, result);
93
+ return result;
94
+ }
95
+ /**
96
+ * Retime pending quiet-hours-deferred DM rows after a quiet-hours config
97
+ * change (the `syncDmSessionTimesToQuietHours` sibling for this module's
98
+ * rows). The deferral premise — "the row fires *at* the quiet-hours edge,
99
+ * so `handleDirectDm`'s quiet-hours skip is correct" — only holds while
100
+ * the window that produced `scheduled_for` is still the configured one:
101
+ *
102
+ * - window extended (end moved later) → without retiming the row fires
103
+ * *inside* the new quiet window;
104
+ * - window shortened (end moved earlier) → the row waits past the new
105
+ * edge for no reason.
106
+ *
107
+ * Rule: inside the new window every deferred row moves to the new edge;
108
+ * outside it, future-dated rows are pulled up to `now` (the next scheduler
109
+ * tick delivers them) and already-due rows are left for the tick to claim.
110
+ * User-scheduled `dm` rows (no `deferred_from` marker) are never touched.
111
+ * Returns the number of rows retimed.
112
+ */
113
+ export function retimeDeferredDmRows(db, window, now = new Date()) {
114
+ const rows = db
115
+ .prepare(`SELECT id, scheduled_for
116
+ FROM agent_schedule
117
+ WHERE status = 'pending'
118
+ AND task_type = 'dm'
119
+ AND task_context IS NOT NULL
120
+ AND json_valid(task_context)
121
+ AND json_extract(task_context, '$.deferred_from') IS NOT NULL`)
122
+ .all();
123
+ return retimeRowsToWindowEdge(db, window, now, rows, "dm");
124
+ }
125
+ /**
126
+ * Retime pending quiet-hours-deferred RUN rows (`agent.task` opt-in and
127
+ * `browser_task`) after a quiet-hours config change — the
128
+ * `retimeDeferredDmRows` sibling for rows the ScheduleWatcher's
129
+ * `deferClaimedRowForQuietHours` pushed to the old window's end. That helper
130
+ * stamps `task_context.quiet_hours_deferred` at deferral time for exactly
131
+ * this purpose: rows that merely *carry* the `defer_in_quiet_hours` opt-in on
132
+ * a future cron slot were never deferred and keep their cron-scheduled time.
133
+ *
134
+ * Unlike deferred DM rows (delivered by `handleDirectDm`, which skips the
135
+ * quiet-hours check by design), run rows re-check the window at claim time,
136
+ * so a *widened* window self-corrects — retiming it here merely skips the
137
+ * wasted claim/re-defer cycle and its duplicate audit row. The
138
+ * narrowed/disabled direction is the real fix: without retiming, the run
139
+ * waits at the old window's end for no reason.
140
+ */
141
+ export function retimeDeferredRunRows(db, window, now = new Date()) {
142
+ const rows = db
143
+ .prepare(`SELECT id, scheduled_for
144
+ FROM agent_schedule
145
+ WHERE status = 'pending'
146
+ AND task_context IS NOT NULL
147
+ AND json_valid(task_context)
148
+ AND json_extract(task_context, '$.quiet_hours_deferred') = 1`)
149
+ .all();
150
+ return retimeRowsToWindowEdge(db, window, now, rows, "run");
151
+ }
152
+ /**
153
+ * Shared retime rule: inside the new window every row moves to the new edge;
154
+ * outside it, future-dated rows are pulled up to `now` (the next scheduler
155
+ * tick handles them) and already-due rows are left for the tick to claim.
156
+ */
157
+ function retimeRowsToWindowEdge(db, window, now, rows, kind) {
158
+ if (rows.length === 0)
159
+ return 0;
160
+ const quietEndMs = nextQuietHoursEndMs(now, window);
161
+ const target = formatSqliteDatetime(quietEndMs !== null ? new Date(quietEndMs) : now);
162
+ const update = db.prepare("UPDATE agent_schedule SET scheduled_for = ? WHERE id = ?");
163
+ let retimed = 0;
164
+ for (const row of rows) {
165
+ // Outside quiet hours only future rows move — an already-due row is
166
+ // the scheduler's to claim; rewriting it would just delay delivery.
167
+ if (quietEndMs === null && row.scheduled_for <= target)
168
+ continue;
169
+ if (row.scheduled_for === target)
170
+ continue;
171
+ update.run(target, row.id);
172
+ retimed++;
173
+ }
174
+ if (retimed > 0) {
175
+ logger.info({ retimed, target, insideQuietHours: quietEndMs !== null, kind }, "Retimed pending quiet-hours-deferred rows after quiet-hours change");
176
+ }
177
+ return retimed;
178
+ }
179
+ /** One `agent_actions` row per deferral so the user can see the delay —
180
+ * mirrors the scheduler's `browser_task.deferred_for_quiet_hours`. */
181
+ function recordDeferralAudit(db, window, params, result) {
182
+ try {
183
+ db.prepare(`INSERT INTO agent_actions
184
+ (action_type, detail, result, started_at, completed_at)
185
+ VALUES (?, ?, 'success', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`).run("notify.deferred_for_quiet_hours", JSON.stringify({
186
+ scheduleId: result.scheduleId,
187
+ deferredFrom: params.deferredFrom,
188
+ originSessionId: params.originSessionId ?? null,
189
+ agentId: params.agentId ?? null,
190
+ deliverAfter: result.deliverAfter,
191
+ coalesced: result.coalesced,
192
+ quietHoursStart: window.start,
193
+ quietHoursEnd: window.end,
194
+ }));
195
+ }
196
+ catch (err) {
197
+ logger.warn({ err, scheduleId: result.scheduleId }, "Failed to record notify.deferred_for_quiet_hours audit — deferred row already persisted");
198
+ }
199
+ }
@@ -0,0 +1,77 @@
1
+ import type Database from "better-sqlite3";
2
+ export type FeedbackSignalSource = "behavioral" | "explicit" | "self_critique";
3
+ export type FeedbackSignalValence = "positive" | "negative" | "neutral" | "correction";
4
+ export type FeedbackScopeType = "user" | "agent" | "agent_slug" | "channel" | "task" | "integration";
5
+ export type FeedbackActionKind = "notification" | "agent_execution" | "vault_write" | "dm_reply";
6
+ export interface FeedbackSignalRow {
7
+ id: number;
8
+ created_at: string;
9
+ source: FeedbackSignalSource;
10
+ valence: FeedbackSignalValence | null;
11
+ scope_type: FeedbackScopeType;
12
+ scope_ref: string | null;
13
+ action_kind: FeedbackActionKind | null;
14
+ action_ref: string | null;
15
+ agent_id: string | null;
16
+ summary: string;
17
+ evidence_json: string | null;
18
+ consumed_at: string | null;
19
+ lesson_ref: string | null;
20
+ }
21
+ export interface RecordFeedbackSignalParams {
22
+ source: FeedbackSignalSource;
23
+ valence?: FeedbackSignalValence | null;
24
+ scopeType: FeedbackScopeType;
25
+ scopeRef?: string | null;
26
+ actionKind?: FeedbackActionKind | null;
27
+ actionRef?: string | null;
28
+ agentId?: string | null;
29
+ summary: string;
30
+ evidence?: unknown;
31
+ }
32
+ export interface RecentFeedbackSignalLookup {
33
+ scopeType: FeedbackScopeType;
34
+ scopeRef?: string | null;
35
+ summary: string;
36
+ withinSeconds: number;
37
+ }
38
+ export declare function recordFeedbackSignal(db: Database.Database, params: RecordFeedbackSignalParams): number;
39
+ export declare function findRecentFeedbackSignal(db: Database.Database, params: RecentFeedbackSignalLookup): FeedbackSignalRow | null;
40
+ export declare function hasFeedbackSignalForAction(db: Database.Database, params: {
41
+ source: FeedbackSignalSource;
42
+ actionKind: FeedbackActionKind;
43
+ actionRef: string;
44
+ valence?: FeedbackSignalValence | null;
45
+ userReaction?: string;
46
+ }): boolean;
47
+ export declare function getPendingFeedbackSignals(db: Database.Database, params?: {
48
+ scopeType?: FeedbackScopeType;
49
+ scopeRef?: string | null;
50
+ limit?: number;
51
+ offset?: number;
52
+ }): FeedbackSignalRow[];
53
+ /**
54
+ * Count unconsumed signals, optionally narrowed to one scope type. Drives the
55
+ * `GET /api/feedback/lessons` "N signals awaiting tonight's consolidation"
56
+ * health figure (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5) without loading
57
+ * the rows. Uses the same `consumed_at IS NULL` partial index as
58
+ * {@link getPendingFeedbackSignals}.
59
+ */
60
+ export declare function countPendingFeedbackSignals(db: Database.Database, params?: {
61
+ scopeType?: FeedbackScopeType;
62
+ }): number;
63
+ export declare function consumeFeedbackSignals(db: Database.Database, ids: number[], lessonRef?: string | null): {
64
+ consumed: number;
65
+ notFound: number[];
66
+ };
67
+ export declare function sweepConsumedFeedbackSignals(db: Database.Database, cutoff: string): number;
68
+ /**
69
+ * Compute the retention cutoff ISO timestamp for
70
+ * {@link sweepConsumedFeedbackSignals} from the `feedbackSignalRetentionDays`
71
+ * knob. Returns `null` when the knob is missing or non-finite (NaN / Infinity),
72
+ * so the caller degrades to "skip the sweep" instead of throwing on
73
+ * `new Date(NaN).toISOString()` and abandoning the whole nightly consolidation
74
+ * (FEEDBACK_LEARNING_LOOP_DESIGN.md §11 v1.3 robustness). `nowMs` is injected so
75
+ * the math is deterministically testable.
76
+ */
77
+ export declare function feedbackRetentionCutoff(retentionDays: number | undefined, nowMs: number): string | null;
@@ -0,0 +1,144 @@
1
+ export function recordFeedbackSignal(db, params) {
2
+ const evidenceJson = params.evidence === undefined ? "{}" : JSON.stringify(params.evidence);
3
+ const row = db
4
+ .prepare(`INSERT INTO feedback_signals (
5
+ source,
6
+ valence,
7
+ scope_type,
8
+ scope_ref,
9
+ action_kind,
10
+ action_ref,
11
+ agent_id,
12
+ summary,
13
+ evidence_json
14
+ )
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16
+ RETURNING id`)
17
+ .get(params.source, params.valence ?? null, params.scopeType, params.scopeRef ?? null, params.actionKind ?? null, params.actionRef ?? null, params.agentId ?? null, params.summary, evidenceJson);
18
+ return row.id;
19
+ }
20
+ export function findRecentFeedbackSignal(db, params) {
21
+ const row = db
22
+ .prepare(`SELECT *
23
+ FROM feedback_signals
24
+ WHERE scope_type = ?
25
+ AND COALESCE(scope_ref, '') = COALESCE(?, '')
26
+ AND summary = ?
27
+ AND datetime(created_at) >= datetime('now', '-' || ? || ' seconds')
28
+ ORDER BY datetime(created_at) DESC, id DESC
29
+ LIMIT 1`)
30
+ .get(params.scopeType, params.scopeRef ?? null, params.summary, Math.max(0, Math.floor(params.withinSeconds)));
31
+ return row ?? null;
32
+ }
33
+ export function hasFeedbackSignalForAction(db, params) {
34
+ const where = [
35
+ "source = ?",
36
+ "action_kind = ?",
37
+ "action_ref = ?",
38
+ ];
39
+ const values = [params.source, params.actionKind, params.actionRef];
40
+ if (params.valence !== undefined) {
41
+ where.push(params.valence === null ? "valence IS NULL" : "valence = ?");
42
+ if (params.valence !== null)
43
+ values.push(params.valence);
44
+ }
45
+ if (params.userReaction !== undefined) {
46
+ where.push("json_extract(evidence_json, '$.userReaction') = ?");
47
+ values.push(params.userReaction);
48
+ }
49
+ const row = db
50
+ .prepare(`SELECT 1 AS present
51
+ FROM feedback_signals
52
+ WHERE ${where.join(" AND ")}
53
+ LIMIT 1`)
54
+ .get(...values);
55
+ return row !== undefined;
56
+ }
57
+ export function getPendingFeedbackSignals(db, params = {}) {
58
+ const where = ["consumed_at IS NULL"];
59
+ const values = [];
60
+ if (params.scopeType !== undefined) {
61
+ where.push("scope_type = ?");
62
+ values.push(params.scopeType);
63
+ }
64
+ if (params.scopeRef !== undefined) {
65
+ where.push("COALESCE(scope_ref, '') = COALESCE(?, '')");
66
+ values.push(params.scopeRef);
67
+ }
68
+ const limit = Math.min(Math.max(params.limit ?? 100, 1), 500);
69
+ const offset = Math.max(params.offset ?? 0, 0);
70
+ values.push(limit, offset);
71
+ return db
72
+ .prepare(`SELECT *
73
+ FROM feedback_signals
74
+ WHERE ${where.join(" AND ")}
75
+ ORDER BY datetime(created_at) ASC, id ASC
76
+ LIMIT ? OFFSET ?`)
77
+ .all(...values);
78
+ }
79
+ /**
80
+ * Count unconsumed signals, optionally narrowed to one scope type. Drives the
81
+ * `GET /api/feedback/lessons` "N signals awaiting tonight's consolidation"
82
+ * health figure (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5) without loading
83
+ * the rows. Uses the same `consumed_at IS NULL` partial index as
84
+ * {@link getPendingFeedbackSignals}.
85
+ */
86
+ export function countPendingFeedbackSignals(db, params = {}) {
87
+ const where = ["consumed_at IS NULL"];
88
+ const values = [];
89
+ if (params.scopeType !== undefined) {
90
+ where.push("scope_type = ?");
91
+ values.push(params.scopeType);
92
+ }
93
+ const row = db
94
+ .prepare(`SELECT COUNT(*) AS n
95
+ FROM feedback_signals
96
+ WHERE ${where.join(" AND ")}`)
97
+ .get(...values);
98
+ return row.n;
99
+ }
100
+ export function consumeFeedbackSignals(db, ids, lessonRef) {
101
+ if (ids.length === 0)
102
+ return { consumed: 0, notFound: [] };
103
+ const placeholders = ids.map(() => "?").join(",");
104
+ const existing = db
105
+ .prepare(`SELECT id
106
+ FROM feedback_signals
107
+ WHERE id IN (${placeholders}) AND consumed_at IS NULL`)
108
+ .all(...ids);
109
+ const existingIds = new Set(existing.map((row) => row.id));
110
+ const notFound = ids.filter((id) => !existingIds.has(id));
111
+ if (existingIds.size === 0)
112
+ return { consumed: 0, notFound };
113
+ const updateIds = Array.from(existingIds);
114
+ const updatePlaceholders = updateIds.map(() => "?").join(",");
115
+ const consumed = db
116
+ .prepare(`UPDATE feedback_signals
117
+ SET consumed_at = CURRENT_TIMESTAMP, lesson_ref = COALESCE(?, lesson_ref)
118
+ WHERE id IN (${updatePlaceholders}) AND consumed_at IS NULL`)
119
+ .run(lessonRef ?? null, ...updateIds).changes;
120
+ return { consumed, notFound };
121
+ }
122
+ export function sweepConsumedFeedbackSignals(db, cutoff) {
123
+ return db
124
+ .prepare(`DELETE FROM feedback_signals
125
+ WHERE consumed_at IS NOT NULL
126
+ AND datetime(consumed_at) < datetime(?)`)
127
+ .run(cutoff).changes;
128
+ }
129
+ /**
130
+ * Compute the retention cutoff ISO timestamp for
131
+ * {@link sweepConsumedFeedbackSignals} from the `feedbackSignalRetentionDays`
132
+ * knob. Returns `null` when the knob is missing or non-finite (NaN / Infinity),
133
+ * so the caller degrades to "skip the sweep" instead of throwing on
134
+ * `new Date(NaN).toISOString()` and abandoning the whole nightly consolidation
135
+ * (FEEDBACK_LEARNING_LOOP_DESIGN.md §11 v1.3 robustness). `nowMs` is injected so
136
+ * the math is deterministically testable.
137
+ */
138
+ export function feedbackRetentionCutoff(retentionDays, nowMs) {
139
+ if (typeof retentionDays !== "number" || !Number.isFinite(retentionDays)) {
140
+ return null;
141
+ }
142
+ const retentionMs = retentionDays * 24 * 60 * 60 * 1000;
143
+ return new Date(nowMs - retentionMs).toISOString();
144
+ }