@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,380 @@
1
+ /**
2
+ * Background-task row store — BACKGROUND_TASK_RUNNER_DESIGN.md §6.
3
+ *
4
+ * I/O-bound CRUD over `background_task`. The state machine is enforced
5
+ * at this layer via the CHECK constraint on the `state` column (closed
6
+ * set) plus the per-transition CAS helpers below — `markRunning`,
7
+ * `markAwaitingUser`, `markRunningFromParked`, `markTerminal` — each
8
+ * one's WHERE clause refuses an out-of-order write so a race between two
9
+ * writers cannot flip a row backwards.
10
+ *
11
+ * The genuinely new shape vs `browser_task` is the ARTIFACT: `report`
12
+ * (verbatim result — the fidelity anchor), `draft` (worker-authored
13
+ * summary), `notify` (the worker's disposition vs the spawn-time policy),
14
+ * `significance`, and `artifact_path`. `markTerminal` writes them in the
15
+ * same transition as the terminal state so a worker's `finish` (or the
16
+ * runner's fail-loud synthesis) is atomic.
17
+ *
18
+ * Pure decision logic (slot arithmetic) is reused from
19
+ * `services/browser-task/browser-task-slots.ts`; this module is the SQL
20
+ * wrapper and is excluded from the coverage gate (same posture as
21
+ * `browser-task-store.ts`).
22
+ */
23
+ export const BACKGROUND_TASK_TERMINAL_STATES = new Set(["completed", "failed", "timeout", "cancelled"]);
24
+ export const BACKGROUND_TASK_NON_TERMINAL_STATES = new Set(["pending", "running", "awaiting_user"]);
25
+ const SELECT_COLUMNS = `id, brief, title, state, notification_policy,
26
+ significance_criteria, report, draft, notify, significance,
27
+ artifact_path, outcome_detail, originating_channel, correlation_id,
28
+ schedule_row_id, tier, max_budget_usd, backend_session_id,
29
+ created_at, started_at, finished_at, delivered_at`;
30
+ /** Parse the persisted `significance_criteria` JSON. Tolerant: a malformed
31
+ * or non-array value degrades to null rather than throwing, so a row hand-
32
+ * written by a migration / test can never crash a read. */
33
+ function parseSignificanceCriteria(raw) {
34
+ if (!raw)
35
+ return null;
36
+ try {
37
+ const parsed = JSON.parse(raw);
38
+ if (!Array.isArray(parsed))
39
+ return null;
40
+ const items = parsed.filter((x) => typeof x === "string");
41
+ return items.length > 0 ? items : null;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function fromDbRow(row) {
48
+ return {
49
+ id: row.id,
50
+ brief: row.brief,
51
+ title: row.title,
52
+ state: row.state,
53
+ notificationPolicy: row.notification_policy,
54
+ significanceCriteria: parseSignificanceCriteria(row.significance_criteria),
55
+ report: row.report,
56
+ draft: row.draft,
57
+ notify: row.notify === null ? null : row.notify === 1,
58
+ significance: row.significance,
59
+ artifactPath: row.artifact_path,
60
+ outcomeDetail: row.outcome_detail,
61
+ originatingChannel: row.originating_channel,
62
+ correlationId: row.correlation_id,
63
+ scheduleRowId: row.schedule_row_id,
64
+ tier: row.tier,
65
+ maxBudgetUsd: row.max_budget_usd,
66
+ backendSessionId: row.backend_session_id,
67
+ createdAt: row.created_at,
68
+ startedAt: row.started_at,
69
+ finishedAt: row.finished_at,
70
+ deliveredAt: row.delivered_at,
71
+ };
72
+ }
73
+ /** Insert a fresh row in state=pending. The slot manager promotes it to
74
+ * `running` once a slot frees. */
75
+ export function createBackgroundTask(db, input) {
76
+ const criteria = input.significanceCriteria && input.significanceCriteria.length > 0
77
+ ? JSON.stringify([...input.significanceCriteria])
78
+ : null;
79
+ db.prepare(`INSERT INTO background_task
80
+ (id, brief, title, state, notification_policy, significance_criteria,
81
+ report, draft, notify, significance, artifact_path,
82
+ outcome_detail, originating_channel, correlation_id, schedule_row_id,
83
+ tier, max_budget_usd, backend_session_id,
84
+ created_at, started_at, finished_at, delivered_at)
85
+ VALUES (?, ?, ?, 'pending', ?, ?,
86
+ NULL, NULL, NULL, NULL, NULL,
87
+ NULL, ?, ?, ?,
88
+ ?, ?, NULL,
89
+ ?, NULL, NULL, NULL)`).run(input.id, input.brief, input.title, input.notificationPolicy, criteria, input.originatingChannel, input.correlationId, input.scheduleRowId, input.tier, input.maxBudgetUsd, input.createdAt);
90
+ const row = getBackgroundTask(db, input.id);
91
+ if (!row) {
92
+ throw new Error(`createBackgroundTask: post-insert row for ${input.id} missing`);
93
+ }
94
+ return row;
95
+ }
96
+ /**
97
+ * §10.3 brief-dedup — find a still-relevant task with an IDENTICAL brief
98
+ * spawned inside the dedup window, so a runaway fan-out (the
99
+ * RESEARCH_CLUSTER_COST_FIX_PLAN class: a replayed trigger POSTing the
100
+ * same brief many times in minutes) collapses onto the first task instead
101
+ * of spawning N workers. Matches on `brief` + `tier` (the two inputs that
102
+ * define "the same work" — `notificationPolicy` only affects delivery, not
103
+ * the work done) within `sinceMs`, and excludes the FAIL terminals
104
+ * (`failed`/`timeout`/`cancelled`) so a prior failure is retryable rather
105
+ * than sticky. A `completed` duplicate inside the window IS returned — the
106
+ * answer already exists, re-running would just re-spend. Newest first.
107
+ */
108
+ export function findRecentDuplicateBackgroundTask(db, input) {
109
+ const row = db
110
+ .prepare(`SELECT ${SELECT_COLUMNS}
111
+ FROM background_task
112
+ WHERE brief = ?
113
+ AND tier IS ?
114
+ AND created_at >= ?
115
+ AND state NOT IN ('failed', 'timeout', 'cancelled')
116
+ ORDER BY created_at DESC
117
+ LIMIT 1`)
118
+ .get(input.brief, input.tier, input.sinceMs);
119
+ return row ? fromDbRow(row) : null;
120
+ }
121
+ export function getBackgroundTask(db, id) {
122
+ const row = db
123
+ .prepare(`SELECT ${SELECT_COLUMNS} FROM background_task WHERE id = ?`)
124
+ .get(id);
125
+ return row ? fromDbRow(row) : null;
126
+ }
127
+ /** Shared WHERE builder for list + count so the two never diverge. */
128
+ function buildListWhere(options) {
129
+ const { states, notify, finishedSinceMs } = options;
130
+ const where = [];
131
+ const params = [];
132
+ if (states && states.length > 0) {
133
+ where.push(`state IN (${states.map(() => "?").join(", ")})`);
134
+ params.push(...states);
135
+ }
136
+ if (notify !== undefined) {
137
+ where.push(`notify = ?`);
138
+ params.push(notify ? 1 : 0);
139
+ }
140
+ if (finishedSinceMs !== undefined) {
141
+ where.push(`finished_at >= ?`);
142
+ params.push(finishedSinceMs);
143
+ }
144
+ return {
145
+ clause: where.length ? `WHERE ${where.join(" AND ")}` : "",
146
+ params,
147
+ };
148
+ }
149
+ export function listBackgroundTasks(db, options = {}) {
150
+ const { limit = 50, offset = 0 } = options;
151
+ const { clause, params } = buildListWhere(options);
152
+ const sql = `SELECT ${SELECT_COLUMNS}
153
+ FROM background_task
154
+ ${clause}
155
+ ORDER BY created_at DESC
156
+ LIMIT ? OFFSET ?`;
157
+ const rows = db
158
+ .prepare(sql)
159
+ .all(...params, limit, offset);
160
+ return rows.map(fromDbRow);
161
+ }
162
+ export function countBackgroundTasks(db, options = {}) {
163
+ const { clause, params } = buildListWhere(options);
164
+ const sql = `SELECT COUNT(*) AS c FROM background_task
165
+ ${clause}`;
166
+ const row = db.prepare(sql).get(...params);
167
+ return row?.c ?? 0;
168
+ }
169
+ /** Pending → running. CAS on prior state so a concurrent terminal
170
+ * transition (cancel-while-pending) does not get clobbered. */
171
+ export function markRunning(db, id, startedAt) {
172
+ const result = db
173
+ .prepare(`UPDATE background_task
174
+ SET state = 'running', started_at = COALESCE(started_at, ?)
175
+ WHERE id = ? AND state = 'pending'`)
176
+ .run(startedAt, id);
177
+ return result.changes > 0 ? getBackgroundTask(db, id) : null;
178
+ }
179
+ /** Running → awaiting_user. Slot stays held; resume via /clarify. */
180
+ export function markAwaitingUser(db, id) {
181
+ const result = db
182
+ .prepare(`UPDATE background_task
183
+ SET state = 'awaiting_user'
184
+ WHERE id = ? AND state = 'running'`)
185
+ .run(id);
186
+ return result.changes > 0 ? getBackgroundTask(db, id) : null;
187
+ }
188
+ /** awaiting_user → running. Used by /clarify resume. */
189
+ export function markRunningFromParked(db, id) {
190
+ const result = db
191
+ .prepare(`UPDATE background_task
192
+ SET state = 'running'
193
+ WHERE id = ? AND state = 'awaiting_user'`)
194
+ .run(id);
195
+ return result.changes > 0 ? getBackgroundTask(db, id) : null;
196
+ }
197
+ /** Any non-terminal state → terminal, writing the artifact atomically.
198
+ * Idempotent — re-running on an already-terminal row CAS-misses and
199
+ * returns null. */
200
+ export function markTerminal(db, input) {
201
+ const result = db
202
+ .prepare(`UPDATE background_task
203
+ SET state = ?,
204
+ outcome_detail = ?,
205
+ report = COALESCE(?, report),
206
+ draft = COALESCE(?, draft),
207
+ notify = COALESCE(?, notify),
208
+ significance = COALESCE(?, significance),
209
+ artifact_path = COALESCE(?, artifact_path),
210
+ finished_at = ?
211
+ WHERE id = ?
212
+ AND state IN ('pending', 'running', 'awaiting_user')`)
213
+ .run(input.state, input.outcomeDetail, input.report ?? null, input.draft ?? null, input.notify === undefined ? null : input.notify ? 1 : 0, input.significance ?? null, input.artifactPath ?? null, input.finishedAt, input.id);
214
+ return result.changes > 0 ? getBackgroundTask(db, input.id) : null;
215
+ }
216
+ /** Capture the SDK session id once the first turn streams it, so a
217
+ * /clarify resume can `query({resume})` the warm session. */
218
+ export function setBackendSessionId(db, id, sessionId) {
219
+ db.prepare(`UPDATE background_task SET backend_session_id = ? WHERE id = ?`).run(sessionId, id);
220
+ }
221
+ export function markBackgroundTaskDelivered(db, id, deliveredAt) {
222
+ const result = db
223
+ .prepare(`UPDATE background_task
224
+ SET delivered_at = COALESCE(delivered_at, ?)
225
+ WHERE id = ?`)
226
+ .run(deliveredAt, id);
227
+ return result.changes > 0 ? getBackgroundTask(db, id) : null;
228
+ }
229
+ /** Delivery recovery target — completed rows whose worker stored a
230
+ * notify=true artifact but whose DM was never sent/recorded (§10.2). */
231
+ export function listUndeliveredBackgroundTaskReports(db, limit = 20) {
232
+ const rows = db
233
+ .prepare(`SELECT ${SELECT_COLUMNS}
234
+ FROM background_task
235
+ WHERE state = 'completed'
236
+ AND notify = 1
237
+ AND delivered_at IS NULL
238
+ AND draft IS NOT NULL
239
+ ORDER BY finished_at ASC, created_at ASC
240
+ LIMIT ?`)
241
+ .all(limit);
242
+ return rows.map(fromDbRow);
243
+ }
244
+ /** §10.5 — filed (notify=false) results, for the periodic digest +
245
+ * owner pull ("did that monitor ever run?"). */
246
+ export function listFiledBackgroundTaskResults(db, sinceMs, limit = 50) {
247
+ const rows = db
248
+ .prepare(`SELECT ${SELECT_COLUMNS}
249
+ FROM background_task
250
+ WHERE state = 'completed'
251
+ AND notify = 0
252
+ AND finished_at >= ?
253
+ ORDER BY finished_at DESC
254
+ LIMIT ?`)
255
+ .all(sinceMs, limit);
256
+ return rows.map(fromDbRow);
257
+ }
258
+ /**
259
+ * §10.2 boot re-dispatch-from-brief — reset every non-terminal row to
260
+ * `pending` (clearing the lost in-memory session) so the event-pipeline
261
+ * boot hook can re-run each one's brief through the runner. Returns the
262
+ * ids reset so the caller can fan out the re-dispatch.
263
+ *
264
+ * Unlike browser_task's `sweepNonTerminalRowsForBootRecovery` (which
265
+ * force-fails), background tasks are re-dispatchable because the brief is
266
+ * self-contained. `backend_session_id` is cleared since the prior SDK
267
+ * session is unreachable after a restart.
268
+ *
269
+ * Open clarifications belonging to the reset tasks are resolved in the
270
+ * SAME transaction. The pre-restart run that raised an `ask_user` is
271
+ * gone, so its clarification row is orphaned: a surviving `resolved = 0`
272
+ * row would later trip the deadline scanner (`listOverdueClarifications`
273
+ * → `expireForDeadline`) into transitioning the FRESH re-dispatched run
274
+ * to `timeout` — and because re-dispatch makes the task ACTIVE again
275
+ * (pending→running), that `expireForDeadline` is NOT a no-op the way it
276
+ * is for a terminal row. Clearing them here closes that window.
277
+ */
278
+ export function resetNonTerminalForBootRedispatch(db, nowMs = Date.now()) {
279
+ const txn = db.transaction(() => {
280
+ const rows = db
281
+ .prepare(`SELECT id FROM background_task
282
+ WHERE state IN ('pending', 'running', 'awaiting_user')`)
283
+ .all();
284
+ if (rows.length === 0)
285
+ return [];
286
+ // Abandon orphaned clarifications BEFORE the state reset (so the
287
+ // `task_id IN (non-terminal)` subquery still matches them).
288
+ db.prepare(`UPDATE background_task_clarifications
289
+ SET resolved = 1, answered_at = COALESCE(answered_at, ?)
290
+ WHERE resolved = 0
291
+ AND task_id IN (
292
+ SELECT id FROM background_task
293
+ WHERE state IN ('pending', 'running', 'awaiting_user')
294
+ )`).run(nowMs);
295
+ db.prepare(`UPDATE background_task
296
+ SET state = 'pending',
297
+ started_at = NULL,
298
+ backend_session_id = NULL
299
+ WHERE state IN ('pending', 'running', 'awaiting_user')`).run();
300
+ return rows.map((r) => ({ id: r.id }));
301
+ });
302
+ return txn();
303
+ }
304
+ /**
305
+ * Phase 4 resume-across-restart (§10.2) — the non-terminal rows the boot
306
+ * recovery path partitions into "resume the SDK session" vs "re-dispatch
307
+ * from brief". Returns just the discriminators (id, state, session id) so
308
+ * the caller can decide without loading the full artifact.
309
+ */
310
+ export function listNonTerminalBackgroundTasks(db) {
311
+ return db
312
+ .prepare(`SELECT id, state, backend_session_id
313
+ FROM background_task
314
+ WHERE state IN ('pending', 'running', 'awaiting_user')
315
+ ORDER BY created_at ASC`)
316
+ .all()
317
+ .map((r) => ({ id: r.id, state: r.state, backendSessionId: r.backend_session_id }));
318
+ }
319
+ /**
320
+ * Phase 4 resume-across-restart (§10.2) — reset ONE non-terminal row back
321
+ * to `pending` for re-dispatch-from-brief, clearing its (now unreachable)
322
+ * SDK session id and resolving its orphaned open clarifications in the same
323
+ * transaction (same rationale as the bulk
324
+ * `resetNonTerminalForBootRedispatch`: a surviving `resolved = 0` row would
325
+ * trip the deadline scanner into timing out the FRESH re-dispatched run).
326
+ * Used by the boot path for the rows it re-dispatches and by the runner's
327
+ * resume-failure fallback. Returns the row id when it was non-terminal,
328
+ * else null (idempotent on an already-terminal / missing row).
329
+ */
330
+ export function resetSingleForBootRedispatch(db, id, nowMs = Date.now()) {
331
+ const txn = db.transaction(() => {
332
+ const row = db
333
+ .prepare(`SELECT state FROM background_task WHERE id = ?`)
334
+ .get(id);
335
+ if (!row || !BACKGROUND_TASK_NON_TERMINAL_STATES.has(row.state)) {
336
+ return null;
337
+ }
338
+ db.prepare(`UPDATE background_task_clarifications
339
+ SET resolved = 1, answered_at = COALESCE(answered_at, ?)
340
+ WHERE resolved = 0 AND task_id = ?`).run(nowMs, id);
341
+ db.prepare(`UPDATE background_task
342
+ SET state = 'pending',
343
+ started_at = NULL,
344
+ backend_session_id = NULL
345
+ WHERE id = ?`).run(id);
346
+ return id;
347
+ });
348
+ return txn();
349
+ }
350
+ /**
351
+ * Fold a just-answered clarification into the task's brief so a COLD
352
+ * re-dispatch (when the warm SDK session can't be resumed across a restart,
353
+ * §10.2) still carries the owner's answer and doesn't re-ask the same
354
+ * question. The worker only ever sees the brief, so appending the resolved
355
+ * Q&A is the only way to thread the answer into a fresh run. Idempotency is
356
+ * not required — this runs at most once per clarification per re-dispatch,
357
+ * and the clarify route has already CAS-resolved the row.
358
+ */
359
+ export function appendResolvedClarificationToBrief(db, id, question, answer) {
360
+ const block = `\n\n<resolved_clarification>\n`
361
+ + (question ? `You previously asked: ${question}\n` : "")
362
+ + `The owner answered: ${answer}\n`
363
+ + `</resolved_clarification>`;
364
+ db.prepare(`UPDATE background_task SET brief = brief || ? WHERE id = ?`).run(block, id);
365
+ }
366
+ /**
367
+ * Retention prune for terminal rows older than `cutoffMs`. Children in
368
+ * `background_task_clarifications` go with the parent via ON DELETE
369
+ * CASCADE. Non-terminal rows are never deleted (the boot re-dispatch
370
+ * sweep owns them). `finished_at` is the lifetime anchor with a
371
+ * `created_at` fallback for the rare unset-finished_at terminal.
372
+ */
373
+ export function deleteTerminalBackgroundTasksOlderThan(db, cutoffMs) {
374
+ const result = db
375
+ .prepare(`DELETE FROM background_task
376
+ WHERE state IN ('completed', 'failed', 'timeout', 'cancelled')
377
+ AND COALESCE(finished_at, created_at) < ?`)
378
+ .run(cutoffMs);
379
+ return result.changes;
380
+ }
@@ -184,16 +184,49 @@ export declare function renameResearchCluster(db: Database.Database, slug: strin
184
184
  */
185
185
  export declare function bumpClusterAgentSummaryRevision(db: Database.Database, slug: string): number;
186
186
  /**
187
- * Clusters whose `last_activity_at` is newer than the row's most recent
188
- * cluster_update session i.e. the agent has new visits to journal.
189
- * Surfaces only `active` rows: muted / concluded / dormant clusters do
190
- * not get nightly journal appends. Capped by `limit` so a backlog never
191
- * floods the schedule fan-out.
187
+ * Clusters eligible for a `routine.research_cluster_update` enqueue this
188
+ * agent-day: `active` rows with meaningful activity inside the lookback
189
+ * window that have NOT already been enqueued for `todayAgentDay`
190
+ * (`journal_update_enqueued_on` is the stamp `claimClusterJournalEnqueue`
191
+ * writes at fan-out time; ISO 'YYYY-MM-DD' strings compare lexically =
192
+ * chronologically, so `< todayAgentDay` means "last enqueued on an
193
+ * earlier agent day"). This filter is the efficiency gate for SEQUENTIAL
194
+ * replays (a wake catch-up / morning self-heal re-running after a prior
195
+ * fire completed); the atomic per-row claim in `claimClusterJournalEnqueue`
196
+ * is what additionally protects against two CONCURRENTLY in-flight
197
+ * callbacks — RESEARCH_CLUSTER_COST_FIX_PLAN.md RC1. Muted / concluded / dormant clusters do not get nightly journal
198
+ * appends. Capped by `limit` so a backlog never floods the fan-out.
192
199
  */
193
- export declare function listClustersNeedingUpdate(db: Database.Database, lookbackMs: number, nowMs?: number, limit?: number): Array<{
200
+ export declare function listClustersNeedingUpdate(db: Database.Database, lookbackMs: number, nowMs: number, limit: number, todayAgentDay: string): Array<{
194
201
  slug: string;
195
202
  displayName: string;
196
203
  }>;
204
+ /**
205
+ * Atomically CLAIM `agentDay`'s `routine.research_cluster_update` enqueue
206
+ * slot for this cluster. Returns `true` iff THIS call wrote the stamp —
207
+ * the prior value was NULL or an earlier agent-day; `false` if the slot
208
+ * was already claimed for `agentDay` (or a newer one) or the slug is
209
+ * unknown. Called BEFORE `eventBus.put` (claim-before-enqueue — same
210
+ * rationale as `fireDecision`'s `stampClusterDmFields` call in
211
+ * browser-history-poller.ts).
212
+ *
213
+ * The conditional UPDATE is a single atomic SQLite statement, so two
214
+ * day-boundary callbacks racing on the same cluster cannot both claim
215
+ * it: the 04:00 cron fires `onDayBoundary` fire-and-forget (scheduler.ts)
216
+ * and can overlap a wake catch-up, and each fan-out iterates its own
217
+ * `listClustersNeedingUpdate` snapshot — so the per-row claim, not the
218
+ * snapshot filter, is what guarantees exactly one enqueue per cluster per
219
+ * agent day. Exactly one caller sees `changes === 1` and enqueues; the
220
+ * loser skips. The `< ?` guard doubles as monotonicity — a stale replay
221
+ * carrying an older agent-day matches no row and cannot regress a newer
222
+ * stamp (ISO 'YYYY-MM-DD' strings compare lexically = chronologically).
223
+ *
224
+ * Failure semantics are intentional: the claim persists even if the
225
+ * subsequent `eventBus.put` throws, so a cluster whose enqueue failed
226
+ * retries on the NEXT agent day — bounded, no same-day loop (the F6
227
+ * backfill covers the missed day).
228
+ */
229
+ export declare function claimClusterJournalEnqueue(db: Database.Database, slug: string, agentDay: string): boolean;
197
230
  export declare const OFFER_DEFAULT_TTL_MS: number;
198
231
  export interface PendingOfferInput {
199
232
  slug: string;
@@ -498,22 +498,66 @@ export function bumpClusterAgentSummaryRevision(db, slug) {
498
498
  return row.rev;
499
499
  }
500
500
  /**
501
- * Clusters whose `last_activity_at` is newer than the row's most recent
502
- * cluster_update session i.e. the agent has new visits to journal.
503
- * Surfaces only `active` rows: muted / concluded / dormant clusters do
504
- * not get nightly journal appends. Capped by `limit` so a backlog never
505
- * floods the schedule fan-out.
501
+ * Clusters eligible for a `routine.research_cluster_update` enqueue this
502
+ * agent-day: `active` rows with meaningful activity inside the lookback
503
+ * window that have NOT already been enqueued for `todayAgentDay`
504
+ * (`journal_update_enqueued_on` is the stamp `claimClusterJournalEnqueue`
505
+ * writes at fan-out time; ISO 'YYYY-MM-DD' strings compare lexically =
506
+ * chronologically, so `< todayAgentDay` means "last enqueued on an
507
+ * earlier agent day"). This filter is the efficiency gate for SEQUENTIAL
508
+ * replays (a wake catch-up / morning self-heal re-running after a prior
509
+ * fire completed); the atomic per-row claim in `claimClusterJournalEnqueue`
510
+ * is what additionally protects against two CONCURRENTLY in-flight
511
+ * callbacks — RESEARCH_CLUSTER_COST_FIX_PLAN.md RC1. Muted / concluded / dormant clusters do not get nightly journal
512
+ * appends. Capped by `limit` so a backlog never floods the fan-out.
506
513
  */
507
- export function listClustersNeedingUpdate(db, lookbackMs, nowMs = Date.now(), limit = 25) {
514
+ export function listClustersNeedingUpdate(db, lookbackMs, nowMs, limit, todayAgentDay) {
508
515
  const since = nowMs - lookbackMs;
509
516
  return db
510
517
  .prepare(`SELECT slug, display_name AS displayName
511
518
  FROM browser_research_clusters
512
519
  WHERE status = 'active'
513
520
  AND last_activity_at >= ?
521
+ AND (journal_update_enqueued_on IS NULL
522
+ OR journal_update_enqueued_on < ?)
514
523
  ORDER BY last_activity_at DESC
515
524
  LIMIT ?`)
516
- .all(since, limit);
525
+ .all(since, todayAgentDay, limit);
526
+ }
527
+ /**
528
+ * Atomically CLAIM `agentDay`'s `routine.research_cluster_update` enqueue
529
+ * slot for this cluster. Returns `true` iff THIS call wrote the stamp —
530
+ * the prior value was NULL or an earlier agent-day; `false` if the slot
531
+ * was already claimed for `agentDay` (or a newer one) or the slug is
532
+ * unknown. Called BEFORE `eventBus.put` (claim-before-enqueue — same
533
+ * rationale as `fireDecision`'s `stampClusterDmFields` call in
534
+ * browser-history-poller.ts).
535
+ *
536
+ * The conditional UPDATE is a single atomic SQLite statement, so two
537
+ * day-boundary callbacks racing on the same cluster cannot both claim
538
+ * it: the 04:00 cron fires `onDayBoundary` fire-and-forget (scheduler.ts)
539
+ * and can overlap a wake catch-up, and each fan-out iterates its own
540
+ * `listClustersNeedingUpdate` snapshot — so the per-row claim, not the
541
+ * snapshot filter, is what guarantees exactly one enqueue per cluster per
542
+ * agent day. Exactly one caller sees `changes === 1` and enqueues; the
543
+ * loser skips. The `< ?` guard doubles as monotonicity — a stale replay
544
+ * carrying an older agent-day matches no row and cannot regress a newer
545
+ * stamp (ISO 'YYYY-MM-DD' strings compare lexically = chronologically).
546
+ *
547
+ * Failure semantics are intentional: the claim persists even if the
548
+ * subsequent `eventBus.put` throws, so a cluster whose enqueue failed
549
+ * retries on the NEXT agent day — bounded, no same-day loop (the F6
550
+ * backfill covers the missed day).
551
+ */
552
+ export function claimClusterJournalEnqueue(db, slug, agentDay) {
553
+ const info = db
554
+ .prepare(`UPDATE browser_research_clusters
555
+ SET journal_update_enqueued_on = ?
556
+ WHERE slug = ?
557
+ AND (journal_update_enqueued_on IS NULL
558
+ OR journal_update_enqueued_on < ?)`)
559
+ .run(agentDay, slug, agentDay);
560
+ return info.changes > 0;
517
561
  }
518
562
  // ── browser_pending_offers — materialised view of open offer state ──
519
563
  export const OFFER_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1000;
@@ -26,6 +26,7 @@ export interface BrowserTaskClarificationRow {
26
26
  screenshotKey: string | null;
27
27
  askedAt: number;
28
28
  deadlineAt: number;
29
+ deliveredAt: number | null;
29
30
  answer: string | null;
30
31
  answeredAt: number | null;
31
32
  resolved: boolean;
@@ -65,3 +66,14 @@ export declare function listOverdueClarifications(db: Database.Database, nowMs:
65
66
  * Differs from `resolveClarification` in that it skips the deadline
66
67
  * check — the scanner is calling this BECAUSE the deadline passed. */
67
68
  export declare function expireClarification(db: Database.Database, id: string, nowMs: number): BrowserTaskClarificationRow | null;
69
+ export declare function markClarificationDelivered(db: Database.Database, id: string, deliveredAt: number): BrowserTaskClarificationRow | null;
70
+ /** A recovery-sweep clarification row enriched with the parent task's
71
+ * delivery fields. The list query already INNER JOINs `browser_task`
72
+ * (on `state='awaiting_user'`), so the originating channel + description
73
+ * are folded in here — the sweep needs no second `getBrowserTask` fetch
74
+ * and carries no unreachable "task missing" guard. */
75
+ export interface UndeliveredClarificationRow extends BrowserTaskClarificationRow {
76
+ taskOriginatingChannel: string | null;
77
+ taskDescription: string;
78
+ }
79
+ export declare function listUndeliveredClarifications(db: Database.Database, nowMs: number, limit?: number): readonly UndeliveredClarificationRow[];
@@ -26,6 +26,7 @@ function fromDbRow(row) {
26
26
  screenshotKey: row.screenshot_key,
27
27
  askedAt: row.asked_at,
28
28
  deadlineAt: row.deadline_at,
29
+ deliveredAt: row.delivered_at,
29
30
  answer: row.answer,
30
31
  answeredAt: row.answered_at,
31
32
  resolved: row.resolved === 1,
@@ -38,8 +39,8 @@ export function createClarification(db, input) {
38
39
  const deadline = input.askedAt + CLARIFICATION_TTL_MS;
39
40
  db.prepare(`INSERT INTO browser_task_clarifications
40
41
  (id, task_id, question, context_summary, screenshot_key,
41
- asked_at, deadline_at, answer, answered_at, resolved)
42
- VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0)`).run(input.id, input.taskId, input.question, input.contextSummary, input.screenshotKey, input.askedAt, deadline);
42
+ asked_at, deadline_at, delivered_at, answer, answered_at, resolved)
43
+ VALUES (?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, 0)`).run(input.id, input.taskId, input.question, input.contextSummary, input.screenshotKey, input.askedAt, deadline);
43
44
  const row = getClarification(db, input.id);
44
45
  if (!row) {
45
46
  throw new Error(`createClarification: post-insert row for ${input.id} missing`);
@@ -49,7 +50,7 @@ export function createClarification(db, input) {
49
50
  export function getClarification(db, id) {
50
51
  const row = db
51
52
  .prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
52
- asked_at, deadline_at, answer, answered_at, resolved
53
+ asked_at, deadline_at, delivered_at, answer, answered_at, resolved
53
54
  FROM browser_task_clarifications
54
55
  WHERE id = ?`)
55
56
  .get(id);
@@ -58,7 +59,7 @@ export function getClarification(db, id) {
58
59
  export function listClarificationsForTask(db, taskId) {
59
60
  const rows = db
60
61
  .prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
61
- asked_at, deadline_at, answer, answered_at, resolved
62
+ asked_at, deadline_at, delivered_at, answer, answered_at, resolved
62
63
  FROM browser_task_clarifications
63
64
  WHERE task_id = ?
64
65
  ORDER BY asked_at ASC`)
@@ -96,7 +97,7 @@ export function resolveClarification(db, input) {
96
97
  export function listOverdueClarifications(db, nowMs) {
97
98
  const rows = db
98
99
  .prepare(`SELECT id, task_id, question, context_summary, screenshot_key,
99
- asked_at, deadline_at, answer, answered_at, resolved
100
+ asked_at, deadline_at, delivered_at, answer, answered_at, resolved
100
101
  FROM browser_task_clarifications
101
102
  WHERE resolved = 0 AND deadline_at < ?
102
103
  ORDER BY deadline_at ASC`)
@@ -116,3 +117,32 @@ export function expireClarification(db, id, nowMs) {
116
117
  return null;
117
118
  return getClarification(db, id);
118
119
  }
120
+ export function markClarificationDelivered(db, id, deliveredAt) {
121
+ const result = db
122
+ .prepare(`UPDATE browser_task_clarifications
123
+ SET delivered_at = COALESCE(delivered_at, ?)
124
+ WHERE id = ?`)
125
+ .run(deliveredAt, id);
126
+ return result.changes > 0 ? getClarification(db, id) : null;
127
+ }
128
+ export function listUndeliveredClarifications(db, nowMs, limit = 20) {
129
+ const rows = db
130
+ .prepare(`SELECT c.id, c.task_id, c.question, c.context_summary, c.screenshot_key,
131
+ c.asked_at, c.deadline_at, c.delivered_at,
132
+ c.answer, c.answered_at, c.resolved,
133
+ t.originating_channel, t.description
134
+ FROM browser_task_clarifications c
135
+ JOIN browser_task t ON t.id = c.task_id
136
+ WHERE c.resolved = 0
137
+ AND c.delivered_at IS NULL
138
+ AND c.deadline_at >= ?
139
+ AND t.state = 'awaiting_user'
140
+ ORDER BY c.asked_at ASC
141
+ LIMIT ?`)
142
+ .all(nowMs, limit);
143
+ return rows.map((row) => ({
144
+ ...fromDbRow(row),
145
+ taskOriginatingChannel: row.originating_channel,
146
+ taskDescription: row.description,
147
+ }));
148
+ }
@@ -34,6 +34,7 @@ export interface BrowserTaskRow {
34
34
  createdAt: number;
35
35
  startedAt: number | null;
36
36
  finishedAt: number | null;
37
+ deliveredAt: number | null;
37
38
  }
38
39
  export interface CreateBrowserTaskInput {
39
40
  id: string;
@@ -79,6 +80,8 @@ export interface TerminalTransitionInput {
79
80
  /** Any non-terminal state → terminal. Idempotent — re-running on an
80
81
  * already-terminal row CAS-misses and returns null. */
81
82
  export declare function markTerminal(db: Database.Database, input: TerminalTransitionInput): BrowserTaskRow | null;
83
+ export declare function markBrowserTaskDelivered(db: Database.Database, id: string, deliveredAt: number): BrowserTaskRow | null;
84
+ export declare function listUndeliveredBrowserTaskReports(db: Database.Database, limit?: number): readonly BrowserTaskRow[];
82
85
  /** Increment the per-task CDP-blocked counter. Atomic. */
83
86
  export declare function incrementBlockedRequests(db: Database.Database, id: string, by: number): void;
84
87
  /** Increment the per-task cumulative untrusted-content counter. */