@aitne/daemon 0.1.10 → 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 (305) 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.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -0,0 +1,338 @@
1
+ /**
2
+ * /api/background-task/* — BACKGROUND_TASK_RUNNER_DESIGN.md §7.
3
+ *
4
+ * The generic detached-task surface. The DM agent POSTs a self-contained
5
+ * brief, acks, and ends its turn; the runner runs the worker detached and
6
+ * writes an artifact; the delivery boundary surfaces it. GET /:id returns
7
+ * the artifact (the DM agent's "read the result" affordance for precise
8
+ * follow-ups). /clarify relays the owner's answer; /cancel aborts.
9
+ *
10
+ * Routes (§7):
11
+ * POST /api/background-task spawn (or schedule via scheduleAt)
12
+ * GET /api/background-task list
13
+ * GET /api/background-task/:id the artifact (full detail)
14
+ * POST /api/background-task/:id/clarify answer a pending clarification
15
+ * POST /api/background-task/:id/cancel abort in-flight task
16
+ *
17
+ * Excluded from the 100% coverage gate — route glue. The pure pieces
18
+ * (budget envelope, slot manager) are 100%-covered peers.
19
+ */
20
+ import { randomUUID } from "node:crypto";
21
+ import { Hono } from "hono";
22
+ import { z } from "zod";
23
+ import { formatSqliteDatetime } from "@aitne/shared";
24
+ import { countBackgroundTasks, createBackgroundTask, findRecentDuplicateBackgroundTask, getBackgroundTask, listBackgroundTasks, markTerminal, } from "../../db/background-task-store.js";
25
+ import { getOpenClarificationForTask, listClarificationsForTask, resolveClarification, } from "../../db/background-task-clarifications-store.js";
26
+ import { listPrimaryChannels, channelRef, } from "../../db/browser-automation-purchase-primary-channels-store.js";
27
+ import { selectDefaultOwnerChannel } from "../../messaging/owner-channels.js";
28
+ import { createBackgroundTaskTransitionEmitter } from "../../services/background-task/background-task-transition-events.js";
29
+ import { createLogger } from "../../logging.js";
30
+ import { readJsonBody } from "../json-body.js";
31
+ const logger = createLogger("background-task-routes");
32
+ const postBodySchema = z.object({
33
+ brief: z.string().min(1).max(16_384),
34
+ title: z.string().min(1).max(200).optional(),
35
+ notificationPolicy: z
36
+ .enum(["always", "if_significant", "silent"])
37
+ .optional()
38
+ .default("always"),
39
+ // Phase 4 if_significant criteria DSL (§4.3) — optional structured
40
+ // conditions the worker checks one-by-one. Each is a concrete, atomic
41
+ // condition ("any repo's main build is red"); the worker sets
42
+ // notify=true iff ANY is met. Up to 12, each ≤500 chars.
43
+ significanceCriteria: z
44
+ .array(z.string().min(1).max(500))
45
+ .max(12)
46
+ .optional(),
47
+ tier: z.enum(["lite", "medium", "high"]).optional(),
48
+ maxBudgetUsd: z.number().positive().max(15).optional(),
49
+ originatingChannel: z.string().optional(),
50
+ correlationId: z.string().optional(),
51
+ scheduleAt: z.string().datetime({ offset: true }).optional(),
52
+ });
53
+ const clarifyBodySchema = z.object({
54
+ clarificationId: z.string().uuid().optional(),
55
+ answer: z.string().min(1).max(8192),
56
+ });
57
+ const cancelBodySchema = z
58
+ .object({ reason: z.string().max(256).optional() })
59
+ .optional();
60
+ function toWire(row) {
61
+ return {
62
+ id: row.id,
63
+ brief: row.brief,
64
+ title: row.title,
65
+ state: row.state,
66
+ notificationPolicy: row.notificationPolicy,
67
+ significanceCriteria: row.significanceCriteria,
68
+ report: row.report,
69
+ draft: row.draft,
70
+ notify: row.notify,
71
+ significance: row.significance,
72
+ artifactPath: row.artifactPath,
73
+ outcomeDetail: row.outcomeDetail,
74
+ originatingChannel: row.originatingChannel,
75
+ correlationId: row.correlationId,
76
+ scheduleRowId: row.scheduleRowId,
77
+ tier: row.tier,
78
+ maxBudgetUsd: row.maxBudgetUsd,
79
+ createdAt: row.createdAt,
80
+ startedAt: row.startedAt,
81
+ finishedAt: row.finishedAt,
82
+ deliveredAt: row.deliveredAt,
83
+ };
84
+ }
85
+ /** Resolve the originating channel: header / body preferred, intersected
86
+ * with the primary set ∪ the owner's most-recent DM channel. A null
87
+ * result files the artifact but cannot DM it. */
88
+ function resolveOriginatingChannel(deps, headerChannel, bodyChannel) {
89
+ const requested = headerChannel ?? bodyChannel ?? null;
90
+ const primary = listPrimaryChannels(deps.db).map((row) => channelRef(row.platform, row.channelId));
91
+ const ownerDefault = selectDefaultOwnerChannel(deps.db);
92
+ const ownerDefaultRef = ownerDefault
93
+ ? channelRef(ownerDefault.platform, ownerDefault.channelId)
94
+ : null;
95
+ if (requested) {
96
+ if (primary.includes(requested))
97
+ return requested;
98
+ return primary[0] ?? ownerDefaultRef;
99
+ }
100
+ return primary[0] ?? ownerDefaultRef;
101
+ }
102
+ export function createBackgroundTaskRoutes(deps) {
103
+ const app = new Hono();
104
+ const transitionEmitter = createBackgroundTaskTransitionEmitter(deps.eventBroadcaster ?? null);
105
+ // ── POST /api/background-task ─────────────────────────────────────────
106
+ app.post("/background-task", async (c) => {
107
+ const body = await readJsonBody(c);
108
+ if (!body.ok)
109
+ return body.response;
110
+ const parsed = postBodySchema.safeParse(body.body);
111
+ if (!parsed.success) {
112
+ return c.json({ error: "validation_error", details: parsed.error.flatten() }, 400);
113
+ }
114
+ const input = parsed.data;
115
+ const resolvedChannel = resolveOriginatingChannel(deps, c.req.header("x-pa-channel-ref"), input.originatingChannel);
116
+ if (resolvedChannel === null) {
117
+ logger.warn({}, "background-task: no originating channel resolvable — the result will be filed but cannot be DMed");
118
+ }
119
+ const id = randomUUID();
120
+ const title = input.title ?? null;
121
+ // ── Scheduled path ──────────────────────────────────────────────
122
+ if (input.scheduleAt !== undefined) {
123
+ const scheduledAtMs = Date.parse(input.scheduleAt);
124
+ if (!Number.isFinite(scheduledAtMs)) {
125
+ return c.json({ error: "invalid_schedule_at", detail: "scheduleAt must parse as ISO 8601." }, 400);
126
+ }
127
+ const nowMs = Date.now();
128
+ if (scheduledAtMs < nowMs - 60_000) {
129
+ return c.json({
130
+ error: "schedule_at_in_past",
131
+ detail: `scheduleAt resolves to more than 60s in the past (delta=${nowMs - scheduledAtMs}ms).`,
132
+ }, 400);
133
+ }
134
+ const scheduleContext = {
135
+ preGeneratedTaskId: id,
136
+ brief: input.brief,
137
+ title,
138
+ notificationPolicy: input.notificationPolicy,
139
+ significanceCriteria: input.significanceCriteria ?? null,
140
+ tier: input.tier ?? null,
141
+ maxBudgetUsd: input.maxBudgetUsd ?? null,
142
+ originatingChannel: resolvedChannel,
143
+ };
144
+ const correlationId = input.correlationId ?? randomUUID();
145
+ const label = title ?? input.brief.slice(0, 200);
146
+ const insertResult = deps.db
147
+ .prepare(`INSERT INTO agent_schedule
148
+ (scheduled_for, task_type, task_description, task_prompt, task_context, correlation_id, model, status)
149
+ VALUES (?, 'background_task', ?, ?, ?, ?, NULL, 'pending')`)
150
+ .run(formatSqliteDatetime(new Date(scheduledAtMs)), label, input.brief, JSON.stringify(scheduleContext), correlationId);
151
+ const scheduleRowId = Number(insertResult.lastInsertRowid);
152
+ logger.info({ taskId: id, scheduleRowId, scheduledAt: input.scheduleAt }, "background-task scheduled — fire-time row creation deferred to dispatcher");
153
+ return c.json({ taskId: id, status: "scheduled", scheduledFor: scheduledAtMs, scheduleRowId }, 202);
154
+ }
155
+ // ── Brief-dedup (§10.3 / Phase 4) ───────────────────────────────
156
+ // A replayed trigger (the RESEARCH_CLUSTER_COST_FIX_PLAN runaway
157
+ // class) or an over-eager agent POSTing the same brief repeatedly
158
+ // within minutes would otherwise spawn N detached workers. Collapse
159
+ // onto the first still-relevant identical task. `0` disables.
160
+ const dedupWindowMinutes = deps.config?.backgroundTaskDedupWindowMinutes ?? 0;
161
+ if (dedupWindowMinutes > 0) {
162
+ const existing = findRecentDuplicateBackgroundTask(deps.db, {
163
+ brief: input.brief,
164
+ tier: input.tier ?? null,
165
+ sinceMs: Date.now() - dedupWindowMinutes * 60_000,
166
+ });
167
+ if (existing) {
168
+ logger.info({
169
+ taskId: existing.id,
170
+ state: existing.state,
171
+ windowMinutes: dedupWindowMinutes,
172
+ }, "background-task dedup — identical brief within window; returning existing task instead of spawning a duplicate");
173
+ return c.json({
174
+ taskId: existing.id,
175
+ status: existing.state,
176
+ deduplicated: true,
177
+ row: toWire(existing),
178
+ }, 202);
179
+ }
180
+ }
181
+ const createdAt = Date.now();
182
+ const row = createBackgroundTask(deps.db, {
183
+ id,
184
+ brief: input.brief,
185
+ title,
186
+ notificationPolicy: input.notificationPolicy,
187
+ significanceCriteria: input.significanceCriteria ?? null,
188
+ originatingChannel: resolvedChannel,
189
+ correlationId: input.correlationId ?? null,
190
+ scheduleRowId: null,
191
+ tier: input.tier ?? null,
192
+ maxBudgetUsd: input.maxBudgetUsd ?? null,
193
+ createdAt,
194
+ });
195
+ transitionEmitter.emitFromRow(row, createdAt);
196
+ if (deps.backgroundTaskRunner) {
197
+ void deps.backgroundTaskRunner.runFromPost(id).catch((err) => {
198
+ logger.error({ err, taskId: id }, "background-task runner threw on dispatch — task left in pending state");
199
+ });
200
+ }
201
+ else {
202
+ // No runner wired (tests / lite installs) — synthetic terminal so
203
+ // the row doesn't hang in pending.
204
+ const finishedAt = Date.now();
205
+ const terminal = markTerminal(deps.db, {
206
+ id,
207
+ state: "failed",
208
+ outcomeDetail: "runner_unavailable",
209
+ finishedAt,
210
+ });
211
+ transitionEmitter.emitFromRow(terminal, finishedAt);
212
+ }
213
+ const postDispatchRow = getBackgroundTask(deps.db, id) ?? row;
214
+ return c.json({ taskId: id, status: postDispatchRow.state, row: toWire(postDispatchRow) }, 202);
215
+ });
216
+ // ── GET /api/background-task ──────────────────────────────────────────
217
+ app.get("/background-task", (c) => {
218
+ const stateQuery = c.req.query("state");
219
+ const states = stateQuery
220
+ ? stateQuery.split(",").filter((s) => s.length > 0)
221
+ : undefined;
222
+ // §10.5 — the filed-results digest / "did that monitor run?" pull:
223
+ // `notify=false` + `sinceHours=N` narrows to recently-filed results.
224
+ const notifyQuery = c.req.query("notify");
225
+ const notify = notifyQuery === "true" || notifyQuery === "1"
226
+ ? true
227
+ : notifyQuery === "false" || notifyQuery === "0"
228
+ ? false
229
+ : undefined;
230
+ const sinceHours = Number(c.req.query("sinceHours"));
231
+ const finishedSinceMs = Number.isFinite(sinceHours) && sinceHours > 0
232
+ ? Date.now() - sinceHours * 3_600_000
233
+ : undefined;
234
+ const limit = Math.min(200, Math.max(0, Number(c.req.query("limit")) || 50));
235
+ const offset = Math.max(0, Number(c.req.query("offset")) || 0);
236
+ const filter = { states, notify, finishedSinceMs };
237
+ const rows = listBackgroundTasks(deps.db, { ...filter, limit, offset });
238
+ const total = countBackgroundTasks(deps.db, filter);
239
+ return c.json({ tasks: rows.map(toWire), total, limit, offset });
240
+ });
241
+ // ── GET /api/background-task/:id (the artifact) ───────────────────────
242
+ app.get("/background-task/:id", (c) => {
243
+ const id = c.req.param("id");
244
+ const row = getBackgroundTask(deps.db, id);
245
+ if (!row)
246
+ return c.json({ error: "not_found" }, 404);
247
+ return c.json({
248
+ ...toWire(row),
249
+ clarifications: listClarificationsForTask(deps.db, id),
250
+ });
251
+ });
252
+ // ── POST /api/background-task/:id/clarify ─────────────────────────────
253
+ app.post("/background-task/:id/clarify", async (c) => {
254
+ const id = c.req.param("id");
255
+ const body = await readJsonBody(c);
256
+ if (!body.ok)
257
+ return body.response;
258
+ const parsed = clarifyBodySchema.safeParse(body.body);
259
+ if (!parsed.success) {
260
+ return c.json({ error: "validation_error", details: parsed.error.flatten() }, 400);
261
+ }
262
+ const row = getBackgroundTask(deps.db, id);
263
+ if (!row)
264
+ return c.json({ error: "not_found" }, 404);
265
+ if (row.state !== "awaiting_user") {
266
+ return c.json({ error: "not_awaiting_user", currentState: row.state }, 409);
267
+ }
268
+ // Resolve the explicit clarificationId, or the single open one.
269
+ const clarificationId = parsed.data.clarificationId
270
+ ?? getOpenClarificationForTask(deps.db, id)?.id
271
+ ?? null;
272
+ if (!clarificationId) {
273
+ return c.json({ error: "no_open_clarification" }, 409);
274
+ }
275
+ const resolved = resolveClarification(deps.db, {
276
+ id: clarificationId,
277
+ answer: parsed.data.answer,
278
+ answeredAt: Date.now(),
279
+ });
280
+ if (!resolved.ok) {
281
+ const status = resolved.reason === "not_found"
282
+ ? 404
283
+ : resolved.reason === "expired"
284
+ ? 410
285
+ : 409;
286
+ return c.json({ error: resolved.reason ?? "clarify_failed" }, status);
287
+ }
288
+ if (deps.backgroundTaskRunner) {
289
+ void deps.backgroundTaskRunner
290
+ .resumeAfterClarification({
291
+ taskId: id,
292
+ clarificationId,
293
+ answer: parsed.data.answer,
294
+ })
295
+ .catch((err) => {
296
+ logger.error({ err, taskId: id, clarificationId }, "background-task resumeAfterClarification threw — task left parked");
297
+ });
298
+ }
299
+ else {
300
+ logger.warn({ taskId: id, clarificationId }, "background-task clarify recorded but no runner wired — task cannot resume.");
301
+ }
302
+ return c.json({ ok: true, clarification: resolved.row });
303
+ });
304
+ // ── POST /api/background-task/:id/cancel ──────────────────────────────
305
+ app.post("/background-task/:id/cancel", async (c) => {
306
+ const id = c.req.param("id");
307
+ const body = await readJsonBody(c);
308
+ const rawBody = body.ok ? body.body : undefined;
309
+ const parsed = cancelBodySchema.safeParse(rawBody);
310
+ const reason = parsed.success ? parsed.data?.reason ?? "user_cancel" : "user_cancel";
311
+ const row = getBackgroundTask(deps.db, id);
312
+ if (!row)
313
+ return c.json({ error: "not_found" }, 404);
314
+ if (row.state === "completed"
315
+ || row.state === "failed"
316
+ || row.state === "timeout"
317
+ || row.state === "cancelled") {
318
+ return c.json({ error: "already_terminal", currentState: row.state }, 409);
319
+ }
320
+ if (deps.backgroundTaskRunner) {
321
+ await deps.backgroundTaskRunner.cancel(id, reason);
322
+ }
323
+ else {
324
+ const finishedAt = Date.now();
325
+ const updated = markTerminal(deps.db, {
326
+ id,
327
+ state: "cancelled",
328
+ outcomeDetail: reason,
329
+ finishedAt,
330
+ });
331
+ transitionEmitter.emitFromRow(updated, finishedAt);
332
+ return c.json({ ok: true, row: updated ? toWire(updated) : null });
333
+ }
334
+ const after = getBackgroundTask(deps.db, id);
335
+ return c.json({ ok: true, row: after ? toWire(after) : null });
336
+ });
337
+ return app;
338
+ }
@@ -63,10 +63,18 @@ export function createBrowserHistoryRoutes(deps) {
63
63
  return c.json({ error: "not_found" }, 404);
64
64
  }
65
65
  const days = listClusterDailyDeltas(deps.db, detail.rootTaskId, agentDayBoundary(deps), { dayLimit: 31 });
66
+ // RESEARCH_CLUSTER_COST_FIX_PLAN rev5 — stamp each bucket with
67
+ // whether its agent day has ended. The current agent day's bucket is
68
+ // still accumulating (a day-boundary or wake-catch-up run fires
69
+ // mid-day, so the bucket only covers visits up to "now"); consumers
70
+ // that persist day entries (the append-only research journal) must
71
+ // skip incomplete buckets or they freeze an undercounted day forever.
72
+ // ISO agent-day strings compare lexically = chronologically.
73
+ const today = todayKey(deps);
66
74
  return c.json(browserHistoryClusterDeltaResponseSchema.parse({
67
75
  slug,
68
76
  generatedAt: new Date().toISOString(),
69
- days,
77
+ days: days.map((day) => ({ ...day, complete: day.date < today })),
70
78
  }));
71
79
  });
72
80
  app.get("/browser-history/research-clusters/:slug", (c) => {
@@ -52,8 +52,9 @@ export const CONTEXT_WRITE_PERMISSIONS = {
52
52
  "policies/management-captures/*": ["PUT", "PATCH"],
53
53
  "policies/routines/_index": ["PUT", "PATCH"],
54
54
  "policies/routines/*": ["PUT", "PATCH"],
55
- // Custom routines support DELETE so the agent can retire a routine
56
- // when the user asks via DM.
55
+ // Legacy custom-routine files (inert since the Agents-hub redesign
56
+ // recurring work is an Agent now). Writes stay validated and DELETE
57
+ // remains so the agent can clean a leftover file up when the user asks.
57
58
  "policies/routines/custom/*": ["PUT", "PATCH", "DELETE"],
58
59
  // User Agent definitions (AGENT_DEFINITIONS_DESIGN.md §9.5 / §3.3). The
59
60
  // dashboard's "+ New Agent" scaffold and the YAML editor write user Agents
@@ -186,9 +186,6 @@ export function registerSnapshotsRoutes(app, ctx) {
186
186
  if (shouldRefreshPromptContext(path, "PUT")) {
187
187
  notifyPromptContextChanged(deps, path, `context_restore_snapshot:${path}`, { path, method: "RESTORE" });
188
188
  }
189
- if (path.startsWith("policies/routines/custom/")) {
190
- deps.onCustomRoutinesChanged?.();
191
- }
192
189
  const writtenStat = statSync(fullPath);
193
190
  logger.info({
194
191
  path,
@@ -320,9 +320,6 @@ export function registerWriteRoutes(app, ctx) {
320
320
  if (shouldRefreshPromptContext(path, "PUT")) {
321
321
  notifyPromptContextChanged(deps, path, `context_put:${path}`, { path, method: "PUT" });
322
322
  }
323
- if (path.startsWith("policies/routines/custom/")) {
324
- deps.onCustomRoutinesChanged?.();
325
- }
326
323
  const writtenStat = statSync(fullPath);
327
324
  logger.info({
328
325
  path,
@@ -530,9 +527,6 @@ export function registerWriteRoutes(app, ctx) {
530
527
  previousContent: fileContent,
531
528
  });
532
529
  }
533
- if (path.startsWith("policies/routines/custom/")) {
534
- deps.onCustomRoutinesChanged?.();
535
- }
536
530
  logger.info({ path, method: "PATCH", mode }, "Content appended to file");
537
531
  return c.json({ status: "appended" });
538
532
  }
@@ -608,9 +602,6 @@ export function registerWriteRoutes(app, ctx) {
608
602
  previousContent: fileContent,
609
603
  });
610
604
  }
611
- if (path.startsWith("policies/routines/custom/")) {
612
- deps.onCustomRoutinesChanged?.();
613
- }
614
605
  logger.info({ path, method: "PATCH", mode }, "Frontmatter merged");
615
606
  return c.json({ status: "merged" });
616
607
  }
@@ -724,9 +715,6 @@ export function registerWriteRoutes(app, ctx) {
724
715
  previousContent: fileContent,
725
716
  });
726
717
  }
727
- if (path.startsWith("policies/routines/custom/")) {
728
- deps.onCustomRoutinesChanged?.();
729
- }
730
718
  deps.onIndexableContextChange?.(`${path}${target.ext}`);
731
719
  const resultStatus = mode === "append" ? "appended" : mode === "replace" ? "replaced" : "cleared";
732
720
  logger.info({ path, method: "PATCH", section, mode, removedCount, trimmedCount }, "Context section " + resultStatus);
@@ -735,8 +723,9 @@ export function registerWriteRoutes(app, ctx) {
735
723
  });
736
724
  // DELETE /context/* — File delete (currently limited to `routines/custom/*`
737
725
  // via the write-permission whitelist). B-007 §5.8 Q3: the agent retires a
738
- // custom routine after the user confirms. The scheduler listens via
739
- // `onCustomRoutinesChanged` and unregisters the cron job on reload.
726
+ // custom routine after the user confirms. (Legacy path custom routines
727
+ // no longer fire; they were converted to user Agents at the Agents-hub
728
+ // redesign. The delete surface stays so leftover files can be removed.)
740
729
  app.delete("/context/*", (c) => {
741
730
  const path = normalizeContextPath(c.req.path.replace("/api/context/", ""));
742
731
  const contextDir = getCurrentContextDir();
@@ -770,9 +759,6 @@ export function registerWriteRoutes(app, ctx) {
770
759
  const existing = readFileSync(fullPath, "utf-8");
771
760
  const snapshotId = saveSnapshot(path, existing, "api_delete", true);
772
761
  unlinkSync(fullPath);
773
- if (path.startsWith("policies/routines/custom/")) {
774
- deps.onCustomRoutinesChanged?.();
775
- }
776
762
  deps.onIndexableContextChange?.(path);
777
763
  logger.info({ path, method: "DELETE", snapshotId: snapshotId ?? undefined }, "Context file deleted");
778
764
  return c.json({ status: "deleted", snapshotId: snapshotId ?? 0 });
@@ -4,6 +4,7 @@ import { EDITABLE_RUNTIME_KEY_TUPLE, getBackendIds, normalizeAgentDisplayName, }
4
4
  import { applyConfigUpdates } from "../../env-writer.js";
5
5
  import { runDefaultSchedulesReconciler } from "../../../core/context/default-schedules-runner.js";
6
6
  import { syncDmSessionTimesToQuietHours } from "../../../core/quiet-hours-sync.js";
7
+ import { retimeDeferredDmRows, retimeDeferredRunRows, } from "../../../db/deferred-dm.js";
7
8
  import { getContextDir } from "../../../config.js";
8
9
  import { CONTEXT_RELATIVE_PATHS } from "../../../core/context-paths.js";
9
10
  import { createLogger, toSafeErrorMessage } from "../../../logging.js";
@@ -31,13 +32,13 @@ const PUBLIC_CONFIG_RUNTIME_KEYS = [
31
32
  "sessionTimeoutDashboardMinutes",
32
33
  "timezone",
33
34
  "dayBoundaryHour",
34
- // Monthly Review kill switch default off; see runtime-settings.ts.
35
- "monthlyReviewEnabled",
36
- "hourlyCheckEnabled",
37
- "hourlyCheckIntervalMinutes",
38
- "hourlyCheckActiveStartHour",
39
- "hourlyCheckActiveEndHour",
40
- "hourlyCheckMinObservations",
35
+ // The legacy activity-scan / monthly-review gate + cadence keys
36
+ // (activityScanEnabled, activityScanIntervalMinutes, activityScanActive*,
37
+ // activityScanMinObservations, monthlyReviewEnabled) left this surface at
38
+ // the Agents-hub redesign: `agents.enabled` + the activity-scan row's
39
+ // runtime_window own them now (PATCH /api/agents/activity-scan). The keys
40
+ // remain valid in runtimeSettingsSchema as resolver fallbacks
41
+ // (AGENTS_HUB_REDESIGN_PLAN.md §2).
41
42
  "authProbeDisabled",
42
43
  "authPreflightFreshnessMs",
43
44
  "maxNotificationsPerHour",
@@ -303,14 +304,18 @@ export function registerConfigRoutes(app, deps) {
303
304
  if (Object.keys(result.errors).length > 0 && result.updated.length === 0) {
304
305
  return c.json({ error: "validation_failed", details: result.errors }, 400);
305
306
  }
306
- // Hot-reload cron schedules when schedule-related config changes
307
+ // Hot-reload cron schedules when schedule-related config changes.
308
+ // The activityScan* entries are legacy-API back-compat only: the dashboard
309
+ // no longer surfaces them (agent-row runtime_window owns the cadence), but
310
+ // a direct PATCH of the deprecated keys must still rebuild the cron so the
311
+ // resolver fallback change takes effect.
307
312
  const SCHEDULE_KEYS = [
308
313
  "dayBoundaryHour",
309
314
  "timezone",
310
- "hourlyCheckEnabled",
311
- "hourlyCheckIntervalMinutes",
312
- "hourlyCheckActiveStartHour",
313
- "hourlyCheckActiveEndHour",
315
+ "activityScanEnabled",
316
+ "activityScanIntervalMinutes",
317
+ "activityScanActiveStartHour",
318
+ "activityScanActiveEndHour",
314
319
  ];
315
320
  if (result.updated.some((k) => SCHEDULE_KEYS.includes(k))) {
316
321
  deps.onScheduleConfigChanged?.();
@@ -342,6 +347,37 @@ export function registerConfigRoutes(app, deps) {
342
347
  logger.warn({ err }, "syncDmSessionTimesToQuietHours threw after dashboard config PATCH");
343
348
  }
344
349
  }
350
+ // QUIET_HOURS_HARDENING_PLAN.md Phase 1 follow-up — pending
351
+ // quiet-hours-deferred DM rows (`task_context.deferred_from`) were
352
+ // stamped with the *old* window's end; retime them so a widened
353
+ // window doesn't fire them inside the new quiet hours and a
354
+ // narrowed one doesn't hold them past the new edge. Phase 2's
355
+ // deferred RUN rows (`task_context.quiet_hours_deferred` — agent.task
356
+ // opt-in + browser_task) get the same treatment; for them only the
357
+ // narrowed/disabled direction matters (a widened window re-defers at
358
+ // claim time anyway). `timezone` is in the trigger set because the
359
+ // window's *absolute* position is tz-relative — an unchanged
360
+ // "22:00→08:00" still moves on the UTC axis when the tz changes, and
361
+ // deferred DM rows are delivered by `handleDirectDm`, which skips the
362
+ // quiet-hours check by design.
363
+ if ((result.updated.includes("quietHoursEnd")
364
+ || result.updated.includes("quietHoursStart")
365
+ || result.updated.includes("timezone"))
366
+ && /^\d{2}:\d{2}$/.test(config.quietHoursStart)
367
+ && /^\d{2}:\d{2}$/.test(config.quietHoursEnd)) {
368
+ const window = {
369
+ start: config.quietHoursStart,
370
+ end: config.quietHoursEnd,
371
+ timezone: config.timezone || undefined,
372
+ };
373
+ try {
374
+ retimeDeferredDmRows(db, window);
375
+ retimeDeferredRunRows(db, window);
376
+ }
377
+ catch (err) {
378
+ logger.warn({ err }, "Retiming quiet-hours-deferred rows threw after dashboard config PATCH");
379
+ }
380
+ }
345
381
  // Hot-refresh DM session skill dirs when enabledMailProviders changes
346
382
  // through the generic config PATCH. The per-endpoint `PATCH /mail/providers`
347
383
  // handler already drives this through MailAccountRegistry.onScopeChanged,
@@ -143,6 +143,10 @@ export function aggregateByBilledModel(rows) {
143
143
  .map(([model, v]) => ({ model, ...v }))
144
144
  .sort((a, b) => b.total_cost - a.total_cost);
145
145
  }
146
+ // Today's spend-driver list is intentionally capped: beyond the top 15 the
147
+ // rows are long-tail noise (the by-process panel covers aggregate share),
148
+ // and an uncapped list would grow unbounded on chatty days.
149
+ const TODAY_TOP_ACTIONS_LIMIT = 15;
146
150
  // `bucketExpr` takes a single shift-modifier parameter (e.g. "+300 minutes")
147
151
  // so each query binds it explicitly — see COST_QUERIES for the rationale.
148
152
  const COST_PERIOD_SPECS = {
@@ -211,6 +215,61 @@ export function registerCostApprovalsRoutes(app, deps) {
211
215
  WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
212
216
  AND cost_usd IS NOT NULL`)
213
217
  .get(bounds.start, bounds.end);
218
+ // ── Today's spend drivers ──
219
+ // All scoped to the same agent-day bounds as the Today card so the
220
+ // numbers reconcile: Σ byEventType.total_cost === today.costUsd.
221
+ // The windowed byEventType above answers "what cost money this month";
222
+ // these answer "what is costing money *right now*" — the question the
223
+ // owner asks when the Today card looks unexpectedly high.
224
+ const todayTopActions = db
225
+ .prepare(`SELECT id, event_id, action_type, trigger, model_used, model_usage_json, cost_usd,
226
+ tokens_input, tokens_output,
227
+ cache_creation_tokens, cache_read_tokens,
228
+ duration_ms, num_turns,
229
+ result, detail, started_at, completed_at, error
230
+ FROM agent_actions
231
+ WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
232
+ AND cost_usd IS NOT NULL AND cost_usd > 0
233
+ ORDER BY cost_usd DESC, datetime(started_at) DESC
234
+ LIMIT ${TODAY_TOP_ACTIONS_LIMIT}`)
235
+ .all(bounds.start, bounds.end);
236
+ const todayByEventType = db
237
+ .prepare(`SELECT action_type as event_type,
238
+ SUM(cost_usd) as total_cost,
239
+ COUNT(*) as session_count
240
+ FROM agent_actions
241
+ WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
242
+ AND cost_usd IS NOT NULL
243
+ GROUP BY action_type
244
+ ORDER BY total_cost DESC`)
245
+ .all(bounds.start, bounds.end);
246
+ const todayByTrigger = db
247
+ .prepare(`SELECT COALESCE(trigger, 'unknown') as trigger,
248
+ SUM(cost_usd) as total_cost,
249
+ COUNT(*) as session_count
250
+ FROM agent_actions
251
+ WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
252
+ AND cost_usd IS NOT NULL
253
+ GROUP BY 1
254
+ ORDER BY total_cost DESC`)
255
+ .all(bounds.start, bounds.end);
256
+ const todayTokens = db
257
+ .prepare(`SELECT COALESCE(SUM(tokens_input), 0) as input,
258
+ COALESCE(SUM(tokens_output), 0) as output,
259
+ COALESCE(SUM(cache_read_tokens), 0) as cacheRead,
260
+ COALESCE(SUM(cache_creation_tokens), 0) as cacheCreation
261
+ FROM agent_actions
262
+ WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
263
+ AND cost_usd IS NOT NULL`)
264
+ .get(bounds.start, bounds.end);
265
+ const todayFailed = db
266
+ .prepare(`SELECT COALESCE(SUM(cost_usd), 0) as cost,
267
+ COUNT(*) as sessions
268
+ FROM agent_actions
269
+ WHERE datetime(started_at) >= ? AND datetime(started_at) < ?
270
+ AND cost_usd IS NOT NULL
271
+ AND result = 'failed'`)
272
+ .get(bounds.start, bounds.end);
214
273
  return c.json({
215
274
  period,
216
275
  today: { costUsd: today.cost, sessions: today.sessions },
@@ -219,6 +278,13 @@ export function registerCostApprovalsRoutes(app, deps) {
219
278
  byEventType,
220
279
  byBackend,
221
280
  byBackendPeriod,
281
+ todayBreakdown: {
282
+ topActions: todayTopActions,
283
+ byEventType: todayByEventType,
284
+ byTrigger: todayByTrigger,
285
+ tokens: todayTokens,
286
+ failed: { costUsd: todayFailed.cost, sessions: todayFailed.sessions },
287
+ },
222
288
  });
223
289
  });
224
290
  // ── Approvals API ──