@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
@@ -1,4 +1,4 @@
1
- import { EventPriority, createEvent, getAgentDayBoundsUtc, isMessageEvent, isRoutineEvent, isAgentTaskEvent, isScheduledEvent, isScheduledBrowserTaskEvent, isScheduledDmEvent, isKnowledgeImportEvent, parseSqliteUtcMs, } from "@aitne/shared";
1
+ import { EventPriority, createEvent, getAgentDayBoundsUtc, isBackendId, isMessageEvent, isRoutineEvent, isAgentTaskEvent, isScheduledEvent, isScheduledBrowserTaskEvent, isScheduledBackgroundTaskEvent, isScheduledDmEvent, isTaskDeliveryEvent, isKnowledgeImportEvent, parseSqliteUtcMs, } from "@aitne/shared";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { CONTEXT_RELATIVE_PATHS } from "./context-paths.js";
@@ -23,15 +23,15 @@ export { parseStage2Verdict, };
23
23
  import { PromptAssembler } from "./dispatcher-prompt.js";
24
24
  import { DispatcherErrorRouter } from "./dispatcher-error-handling.js";
25
25
  import { ResultProcessor } from "./dispatcher-result-processor.js";
26
- import { HourlyCheckCoordinator } from "./dispatcher-hourly-check.js";
26
+ import { ActivityScanCoordinator } from "./dispatcher-activity-scan.js";
27
27
  import { morningRoutineRanToday } from "../bootstrap/schedule-helpers.js";
28
28
  /**
29
29
  * Routine names that depend on `routine.morning_routine` having completed
30
30
  * successfully for the current agent-day. The pre-routine gate in
31
31
  * `dispatch()` enqueues a morning_routine wake and skips the dependent
32
- * routine when the predicate trips — hourly_check is gated separately
33
- * inside `HourlyCheckCoordinator.trigger` because it has its own entry
34
- * point (`triggerHourlyCheck`) before any event hits the bus.
32
+ * routine when the predicate trips — activity_scan is gated separately
33
+ * inside `ActivityScanCoordinator.trigger` because it has its own entry
34
+ * point (`triggerActivityScan`) before any event hits the bus.
35
35
  */
36
36
  const REVIEW_ROUTINES_REQUIRING_MORNING = new Set([
37
37
  "evening_review",
@@ -43,8 +43,10 @@ import { MorningRoutinePipelineOrchestrator } from "./morning/orchestrator.js";
43
43
  import { DailyJournalComposer } from "./morning/daily-journal-composer.js";
44
44
  import { randomUUID } from "node:crypto";
45
45
  import { RoutineFetchWindowRunner } from "./routine-fetch-window-runner.js";
46
+ import { AutonomousSpawnGate, } from "./spawn-gates.js";
46
47
  import { ScheduledTaskRunner, SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS, } from "./dispatcher-scheduled-tasks.js";
47
48
  import { MessageHandler } from "./dispatcher-message-handler.js";
49
+ import { TASK_DELIVERY_GATE_KEYS, handleTaskDeliveryInsideGate, } from "./dispatcher-task-delivery.js";
48
50
  export { SKILL_CURATION_OPTIMIZER_ALLOWED_TOOLS };
49
51
  const CURRENT_SETUP_MODE_STATE_KEY = "current_setup_mode";
50
52
  export class EventDispatcher {
@@ -128,6 +130,14 @@ export class EventDispatcher {
128
130
  * instead of leaving the row silently flipped to `failed` in the DB.
129
131
  */
130
132
  browserTaskTerminalNotifier = null;
133
+ /**
134
+ * BACKGROUND_TASK_RUNNER_DESIGN.md §4.2 — the dispatcher routes
135
+ * `scheduled.background_task` events to this runner. Wired at startup
136
+ * from `bootstrap/event-pipeline.ts` via `setBackgroundTaskRunner`.
137
+ * Null when the runner factory has not landed — the dispatch branch
138
+ * flips the row to `failed (runner_unavailable)` so it doesn't park.
139
+ */
140
+ backgroundTaskRunner = null;
131
141
  /**
132
142
  * Current setup mode — scope-agnostic flag that survives internal
133
143
  * direct-message session refresh (day boundary, stale flag, etc). Previously this
@@ -153,42 +163,42 @@ export class EventDispatcher {
153
163
  * keep their own lane. SCHEDULED-DM-IMPLEMENTATION-PLAN §3.6 — also
154
164
  * used by `scheduled.dm` to acquire BOTH owner-facing scopes in
155
165
  * lex-sorted (deadlock-free) order. */
156
- sessionGates = new SessionGateRegistry();
166
+ sessionGates;
157
167
  /** Dedup guard: timestamp of the last roadmap_refresh emission */
158
168
  lastRoadmapRefreshEmitMs = 0;
159
169
  morningRoutineInProgress = false;
160
- hourlyCheckInProgress = false;
170
+ activityScanInProgress = false;
161
171
  /**
162
172
  * Wall-clock timestamp (ms since epoch) of the most recent flip of
163
- * `hourlyCheckInProgress` to `true`, or `null` when the flag is false.
173
+ * `activityScanInProgress` to `true`, or `null` when the flag is false.
164
174
  *
165
- * Paired with `HOURLY_CHECK_FLAG_MAX_AGE_MS` to break the silent-stall
175
+ * Paired with `ACTIVITY_SCAN_FLAG_MAX_AGE_MS` to break the silent-stall
166
176
  * pattern where the flag is set true at enqueue time but the matching
167
177
  * `dispatchSafe` finally never runs — currently possible when the
168
178
  * EventBus evicts/drops the queued routine event under `put()` pressure
169
179
  * (heap-js drops the lowest-priority entry silently when `heap.size() >=
170
180
  * maxSize=1000`). Without the timestamp the flag stays `true` until
171
181
  * process restart and every subsequent hourly tick short-circuits with
172
- * `hourly_check_in_progress`.
182
+ * `activity_scan_in_progress`.
173
183
  *
174
- * Read side (`isHourlyCheckInProgress` callback below) checks the age
184
+ * Read side (`isActivityScanInProgress` callback below) checks the age
175
185
  * and auto-clears when it exceeds the bound, surfacing the recovery via
176
186
  * a warn log so the operator sees the EventBus pressure event.
177
187
  */
178
- hourlyCheckInProgressAt = null;
188
+ activityScanInProgressAt = null;
179
189
  /**
180
- * Upper bound for how long `hourlyCheckInProgress=true` can plausibly
190
+ * Upper bound for how long `activityScanInProgress=true` can plausibly
181
191
  * be valid before we treat it as stuck and force-clear.
182
192
  *
183
- * Sized generously above the realistic Stage-3 hourly_check ceiling
184
- * (fetch_window pre-pass ~30–60 s + Sonnet hourly_check session
193
+ * Sized generously above the realistic Stage-3 activity_scan ceiling
194
+ * (fetch_window pre-pass ~30–60 s + Sonnet activity_scan session
185
195
  * ~1–3 min) so a slow but normal run is never falsely cleared. Sized
186
196
  * well below an entire agent-day so a stuck flag recovers within a
187
197
  * single hourly cron cycle's worst case (default cadence 60 min).
188
198
  * The 30-minute window is comfortably outside any plausible "still
189
199
  * running" interpretation and inside one full hourly slot.
190
200
  */
191
- static HOURLY_CHECK_FLAG_MAX_AGE_MS = 30 * 60 * 1000;
201
+ static ACTIVITY_SCAN_FLAG_MAX_AGE_MS = 30 * 60 * 1000;
192
202
  /**
193
203
  * P22 §3.4 — wired by `index.ts` after the daemon's data dir + skills root
194
204
  * are known. Returns a `{runId, runToken, workdirPath, targetSkills}` tuple
@@ -216,12 +226,19 @@ export class EventDispatcher {
216
226
  * and older code paths that don't wire the store. When null, the
217
227
  * dispatcher skips attachment staging + outbound collection. */
218
228
  attachmentStore = null;
229
+ /** Injected lazily via `setTaskDeliveryAssetResolver` — optional.
230
+ * Resolves a task's deliverable assets (browser-task screenshots +
231
+ * worker-written files) to outbound attachments for the `task.delivery`
232
+ * idle + active branches (the ingest hook is constructed after this
233
+ * dispatcher, so it is wired post-construction like the attachment
234
+ * store). When null, task-delivery DMs are text-only. */
235
+ taskDeliveryAssetResolver = null;
219
236
  /** Injected lazily via `setDelegatedSyncRefresh` — optional. When null,
220
- * hourly check fires without first refreshing delegated-mode snapshots,
237
+ * activity scan fires without first refreshing delegated-mode snapshots,
221
238
  * matching the pre-Phase-9 behaviour. Wired in production when at
222
239
  * least one integration is in delegated mode. See
223
240
  * `docs/design/appendices/delegated-sync-opt-in.md` and the worker's
224
- * `runDisabledCadencesForHourlyCheck` method. */
241
+ * `runDisabledCadencesForActivityScan` method. */
225
242
  delegatedSyncRefresh = null;
226
243
  /**
227
244
  * Injected lazily via `setQueueMorningRoutineWake` (wired in `index.ts`
@@ -240,7 +257,7 @@ export class EventDispatcher {
240
257
  * Injected lazily via `setEventBroadcaster` — optional. When wired, the
241
258
  * dispatcher emits `routine_started` / `routine_completed` SSE events at
242
259
  * the `dispatchSafe` chokepoint so the dashboard can render real-time
243
- * progress for autonomous routines (morning_routine, hourly_check,
260
+ * progress for autonomous routines (morning_routine, activity_scan,
244
261
  * roadmap_refresh, evening/weekly/monthly reviews, etc.).
245
262
  *
246
263
  * Failure to broadcast is non-fatal: the throw is swallowed and logged
@@ -317,21 +334,38 @@ export class EventDispatcher {
317
334
  */
318
335
  resultProcessor;
319
336
  /**
320
- * Phase D-2 coordinator: owns `triggerHourlyCheck` and the
337
+ * Phase D-2 coordinator: owns `triggerActivityScan` and the
321
338
  * cost-reduction-structural §B three-stage gate. Borrows live
322
- * accessors for the dispatcher's `hourlyCheckInProgress` flag so the
339
+ * accessors for the dispatcher's `activityScanInProgress` flag so the
323
340
  * pre-existing C1 atomic check-and-set semantics survive the split.
324
341
  */
325
342
  /**
326
343
  * docs/design/appendices/routine-data-acquisition.md Phase 4 / D1 — shared pre-pass
327
- * runner for `routine.fetch_window`. Injected into HourlyCheckCoordinator
344
+ * runner for `routine.fetch_window`. Injected into ActivityScanCoordinator
328
345
  * (D3), MorningRoutineRunner (D2), and ScheduledTaskRunner (D4) so
329
346
  * every routine that has rows in `ROUTINE_WINDOWS` gets the same
330
347
  * fetcher session ahead of its parent dispatch. Pure helper, no
331
348
  * mutable state of its own.
332
349
  */
333
350
  fetchWindowRunner;
334
- hourlyCheck;
351
+ /**
352
+ * PREPASS_COST_REDUCTION_PLAN.md N2 — offline (backend-API-host DNS) +
353
+ * cached-auth spawn gate for autonomous sessions. Shared with the
354
+ * pre-pass fan-out runner so both layers reuse one DNS verdict cache.
355
+ */
356
+ spawnGate;
357
+ /**
358
+ * Last spawn-gate skip *audit write* per (schedule-or-type, reason) —
359
+ * ms epoch. A released schedule row is re-claimed by the
360
+ * ScheduleWatcher every poll tick (default 5s), so an hours-long
361
+ * offline window would otherwise INSERT thousands of identical
362
+ * `result='skipped'` rows. The DB row release/claim churn is bounded
363
+ * (UPDATEs to one row); the audit INSERT is what must be throttled.
364
+ * In-memory on purpose: worst case after a restart is one extra row.
365
+ */
366
+ spawnGateSkipAuditAt = new Map();
367
+ static SPAWN_GATE_SKIP_AUDIT_THROTTLE_MS = 10 * 60 * 1000;
368
+ activityScan;
335
369
  /**
336
370
  * Phase D-2 coordinator: owns morning-routine execution end-to-end
337
371
  * (lock acquisition, prompt-variant selection, retry chain, today.md
@@ -363,7 +397,7 @@ export class EventDispatcher {
363
397
  * thin shims that forward into this handler.
364
398
  */
365
399
  messageHandler;
366
- constructor(eventBus, agentRouter, contextBuilder, getTaskFlow, notificationMgr, sessionMgr, messageRecorder, audit, db, config, todayWriteLock, services, roadmapWriteLock, writeTracker) {
400
+ constructor(eventBus, agentRouter, contextBuilder, getTaskFlow, notificationMgr, sessionMgr, messageRecorder, audit, db, config, todayWriteLock, services, roadmapWriteLock, writeTracker, sessionGates) {
367
401
  this.eventBus = eventBus;
368
402
  this.agentRouter = agentRouter;
369
403
  this.contextBuilder = contextBuilder;
@@ -378,6 +412,7 @@ export class EventDispatcher {
378
412
  this.services = services;
379
413
  this.roadmapWriteLock = roadmapWriteLock;
380
414
  this.writeTracker = writeTracker;
415
+ this.sessionGates = sessionGates ?? new SessionGateRegistry();
381
416
  this.reactiveSem = new Semaphore(config.maxReactiveSessions);
382
417
  this.autonomousSem = new Semaphore(config.maxConcurrentSessions);
383
418
  const messageColumns = new Set(this.db.pragma("table_info(messages)").map((column) => column.name));
@@ -407,10 +442,17 @@ export class EventDispatcher {
407
442
  recordExecutionOutcome: (event, outcome) => this.agentExecutionTracker?.recordOutcome(event.correlationId, outcome),
408
443
  });
409
444
  // docs/design/appendices/routine-data-acquisition.md Phase 4 / D1 — shared pre-pass
410
- // runner consumed by HourlyCheckCoordinator (D3), MorningRoutineRunner
445
+ // runner consumed by ActivityScanCoordinator (D3), MorningRoutineRunner
411
446
  // (D2), and ScheduledTaskRunner.executeDefault (D4). Constructed
412
447
  // before all three so it can be injected as a dep rather than
413
448
  // lazily resolved.
449
+ // PREPASS_COST_REDUCTION_PLAN.md N2 — shared offline/auth spawn gate.
450
+ // One instance for the dispatcher's autonomous-event gate AND the
451
+ // pre-pass fan-out runner so the per-host DNS verdict cache (~60s)
452
+ // is shared across both layers within a tick.
453
+ this.spawnGate = new AutonomousSpawnGate(this.db, {
454
+ authFreshnessMs: this.config.authPreflightFreshnessMs,
455
+ });
414
456
  this.fetchWindowRunner = new RoutineFetchWindowRunner({
415
457
  db: this.db,
416
458
  config: this.config,
@@ -418,6 +460,7 @@ export class EventDispatcher {
418
460
  agentRouter: this.agentRouter,
419
461
  audit: this.audit,
420
462
  prompt: this.prompt,
463
+ spawnGate: this.spawnGate,
421
464
  getActiveMailAccounts: () => this.getActiveMailAccounts(),
422
465
  // Live accessor so the SSE broadcaster wired later via
423
466
  // `setEventBroadcaster` (after dispatcher construction in
@@ -427,7 +470,7 @@ export class EventDispatcher {
427
470
  // skips its pre-pass progress emits cleanly.
428
471
  getEventBroadcaster: () => this.eventBroadcaster,
429
472
  });
430
- this.hourlyCheck = new HourlyCheckCoordinator({
473
+ this.activityScan = new ActivityScanCoordinator({
431
474
  db: this.db,
432
475
  config: this.config,
433
476
  eventBus: this.eventBus,
@@ -438,27 +481,27 @@ export class EventDispatcher {
438
481
  prompt: this.prompt,
439
482
  fetchWindowRunner: this.fetchWindowRunner,
440
483
  getDelegatedSyncRefresh: () => this.delegatedSyncRefresh,
441
- setHourlyCheckInProgress: (value) => {
442
- this.hourlyCheckInProgress = value;
443
- this.hourlyCheckInProgressAt = value ? Date.now() : null;
484
+ setActivityScanInProgress: (value) => {
485
+ this.activityScanInProgress = value;
486
+ this.activityScanInProgressAt = value ? Date.now() : null;
444
487
  },
445
- isHourlyCheckInProgress: () => {
446
- if (!this.hourlyCheckInProgress)
488
+ isActivityScanInProgress: () => {
489
+ if (!this.activityScanInProgress)
447
490
  return false;
448
- // Stale-flag recovery — see `hourlyCheckInProgressAt` doc-comment.
491
+ // Stale-flag recovery — see `activityScanInProgressAt` doc-comment.
449
492
  // The branch fires only when an enqueued event never reached
450
493
  // `dispatchSafe`'s finally (EventBus eviction is the realistic
451
494
  // cause; a future code path that forgets to reset the flag would
452
495
  // also self-heal here within one cron cycle).
453
- if (this.hourlyCheckInProgressAt !== null) {
454
- const ageMs = Date.now() - this.hourlyCheckInProgressAt;
455
- if (ageMs > EventDispatcher.HOURLY_CHECK_FLAG_MAX_AGE_MS) {
496
+ if (this.activityScanInProgressAt !== null) {
497
+ const ageMs = Date.now() - this.activityScanInProgressAt;
498
+ if (ageMs > EventDispatcher.ACTIVITY_SCAN_FLAG_MAX_AGE_MS) {
456
499
  logger.warn({
457
500
  ageMs,
458
- maxAgeMs: EventDispatcher.HOURLY_CHECK_FLAG_MAX_AGE_MS,
459
- }, "hourlyCheckInProgress flag exceeded max age — auto-clearing (likely EventBus drop or missed dispatchSafe finally)");
460
- this.hourlyCheckInProgress = false;
461
- this.hourlyCheckInProgressAt = null;
501
+ maxAgeMs: EventDispatcher.ACTIVITY_SCAN_FLAG_MAX_AGE_MS,
502
+ }, "activityScanInProgress flag exceeded max age — auto-clearing (likely EventBus drop or missed dispatchSafe finally)");
503
+ this.activityScanInProgress = false;
504
+ this.activityScanInProgressAt = null;
462
505
  return false;
463
506
  }
464
507
  }
@@ -573,7 +616,7 @@ export class EventDispatcher {
573
616
  diagnoseTodayMdState: () => this.scheduledTasks.diagnoseTodayMdState(),
574
617
  isRoadmapStale: () => this.isRoadmapStale(),
575
618
  emitRoadmapRefresh: (source) => this.emitRoadmapRefresh(source),
576
- triggerHourlyCheck: (source) => this.triggerHourlyCheck(source),
619
+ triggerActivityScan: (source) => this.triggerActivityScan(source),
577
620
  pipelineOrchestrator,
578
621
  });
579
622
  this.scheduledTasks = new ScheduledTaskRunner({
@@ -617,6 +660,8 @@ export class EventDispatcher {
617
660
  getBangCommandRegistry: () => this.bangCommandRegistry,
618
661
  getPurchaseHandler: () => this.purchaseHandler,
619
662
  getFinalConfirmHandler: () => this.finalConfirmHandler,
663
+ getBackgroundTaskRunner: () => this.backgroundTaskRunner,
664
+ getBrowserTaskRunner: () => this.browserTaskRunner,
620
665
  getCurrentSetupMode: () => this.currentSetupMode,
621
666
  beginSetupMode: (mode) => this.beginSetupMode(mode),
622
667
  lookupCustomBangCommandForEvent: (event) => this.lookupCustomBangCommandForEvent(event),
@@ -684,6 +729,15 @@ export class EventDispatcher {
684
729
  getBrowserTaskRunner() {
685
730
  return this.browserTaskRunner;
686
731
  }
732
+ /**
733
+ * BACKGROUND_TASK_RUNNER_DESIGN.md §4.2 — wire the generic
734
+ * background-task runner so the `scheduled.background_task` dispatch
735
+ * branch can hand fire-time events to it. Pairs with the
736
+ * `event-pipeline.ts` `createBackgroundTaskRunner` factory call.
737
+ */
738
+ setBackgroundTaskRunner(runner) {
739
+ this.backgroundTaskRunner = runner;
740
+ }
687
741
  /**
688
742
  * BROWSER_TASK_REDESIGN_PLAN.md §7 — wire the terminal-state DM
689
743
  * emitter used by the `scheduled.browser_task` failure paths (see
@@ -713,6 +767,14 @@ export class EventDispatcher {
713
767
  setAttachmentStore(store) {
714
768
  this.attachmentStore = store;
715
769
  }
770
+ /** BACKGROUND_TASK_RUNNER_DESIGN.md Phase 1 (delivery assets) — inject the
771
+ * asset resolver used by the `task.delivery` idle + active branches to
772
+ * attach a task's deliverable files (screenshots, PDF/PPTX/PNG/docs)
773
+ * inline. Wired post-construction because the underlying
774
+ * dashboard-ingest hook is built after this dispatcher. */
775
+ setTaskDeliveryAssetResolver(resolver) {
776
+ this.taskDeliveryAssetResolver = resolver;
777
+ }
716
778
  /** Inject the local-Whisper voice transcriber. Optional — when unset,
717
779
  * inbound audio attachments are passed to the backend with a path-only
718
780
  * reference (the pre-feature behaviour). */
@@ -721,7 +783,7 @@ export class EventDispatcher {
721
783
  }
722
784
  /**
723
785
  * Inject the delegated-sync refresh callback. Called from
724
- * `triggerHourlyCheck` before the gate decision so any cadence the
786
+ * `triggerActivityScan` before the gate decision so any cadence the
725
787
  * operator left opted-OUT (post-Phase-9 default) populates fresh
726
788
  * Gmail / Notion observations the agent can then consume.
727
789
  *
@@ -731,7 +793,7 @@ export class EventDispatcher {
731
793
  * dispatcher holding a stale reference.
732
794
  *
733
795
  * Pass `null` to detach (e.g. when no delegated integration exists).
734
- * The hourly check then proceeds without a refresh — equivalent to the
796
+ * The activity scan then proceeds without a refresh — equivalent to the
735
797
  * pre-injection behaviour.
736
798
  */
737
799
  setDelegatedSyncRefresh(fn) {
@@ -739,7 +801,7 @@ export class EventDispatcher {
739
801
  }
740
802
  /**
741
803
  * Wire the scheduler's `queueMorningRoutineWake` so the pre-routine
742
- * gate (hourly_check + evening/weekly/monthly review) can self-recover
804
+ * gate (activity_scan + evening/weekly/monthly review) can self-recover
743
805
  * after a missed 04:00 cron fire. Wired once in `index.ts` after both
744
806
  * the dispatcher and scheduler are constructed; passing `null` detaches.
745
807
  * When unset, the gate logs a warning and still skips the dependent
@@ -766,6 +828,16 @@ export class EventDispatcher {
766
828
  setAgentExecutionTracker(tracker) {
767
829
  this.agentExecutionTracker = tracker;
768
830
  }
831
+ /**
832
+ * Resolve the user-Agent slug owning an in-flight firing, for stamping
833
+ * `agent_id` into quiet-hours-deferred DM rows (QUIET_HOURS_HARDENING_PLAN
834
+ * Phase 1 — the `/api/notify` gate coalesces per Agent so an hourly Agent
835
+ * firing five times overnight yields one combined DM). `null` when no
836
+ * tracker is wired or no execution is active for the correlation id.
837
+ */
838
+ agentIdForCorrelation(correlationId) {
839
+ return this.agentExecutionTracker?.currentAgentId(correlationId) ?? null;
840
+ }
769
841
  /**
770
842
  * Open an execution row for an agent-resolvable firing (§8.1), called from
771
843
  * `dispatchSafe` after the setup / cost gates pass so a skipped firing never
@@ -974,7 +1046,7 @@ export class EventDispatcher {
974
1046
  /**
975
1047
  * Enter setup mode. Called from `POST /setup/start` so the warm gate
976
1048
  * engages the moment the user opens the dashboard setup flow — before any
977
- * agent turn runs — so concurrent hourly_check / morning routine / scheduled
1049
+ * agent turn runs — so concurrent activity_scan / morning routine / scheduled
978
1050
  * wake work cannot race with the setup conversation. Persisted to
979
1051
  * `runtime_state` so the flag survives daemon restart.
980
1052
  */
@@ -1035,8 +1107,8 @@ export class EventDispatcher {
1035
1107
  if (this.morningRoutineInProgress) {
1036
1108
  executions.push({ kind: "routine", key: "morning_routine" });
1037
1109
  }
1038
- if (this.hourlyCheckInProgress) {
1039
- executions.push({ kind: "routine", key: "hourly_check" });
1110
+ if (this.activityScanInProgress) {
1111
+ executions.push({ kind: "routine", key: "activity_scan" });
1040
1112
  }
1041
1113
  const runningTasks = this.db
1042
1114
  .prepare(`SELECT id, task_type, task_description
@@ -1054,7 +1126,7 @@ export class EventDispatcher {
1054
1126
  return executions;
1055
1127
  }
1056
1128
  /**
1057
- * Gate for autonomous background work (cron routines, hourly_check,
1129
+ * Gate for autonomous background work (cron routines, activity_scan,
1058
1130
  * scheduled wake tasks, startup catchup, calendar-poller reactive events).
1059
1131
  *
1060
1132
  * Two layers:
@@ -1143,7 +1215,7 @@ export class EventDispatcher {
1143
1215
  /**
1144
1216
  * Check whether this autonomous event should be skipped because the daily
1145
1217
  * autonomous cost cap has been exceeded. Uses priority-based degradation:
1146
- * hourly_check (lowest priority, skipped first) → roadmap_refresh →
1218
+ * activity_scan (lowest priority, skipped first) → roadmap_refresh →
1147
1219
  * evening_review → morning_routine (highest, last to be cut).
1148
1220
  *
1149
1221
  * Lower-priority events are skipped at 100% of cap; higher-priority events
@@ -1169,7 +1241,7 @@ export class EventDispatcher {
1169
1241
  ? event.routine
1170
1242
  : null;
1171
1243
  const thresholds = {
1172
- hourly_check: 1.0, // skipped first (at 100% of cap)
1244
+ activity_scan: 1.0, // skipped first (at 100% of cap)
1173
1245
  roadmap_refresh: 1.2, // skipped at 120%
1174
1246
  evening_review: 1.5, // skipped at 150%
1175
1247
  morning_routine: 2.0, // last to be cut (only at 200%)
@@ -1177,6 +1249,45 @@ export class EventDispatcher {
1177
1249
  const threshold = routine ? (thresholds[routine] ?? 1.0) : 1.0;
1178
1250
  return todayCost >= cap * threshold;
1179
1251
  }
1252
+ /**
1253
+ * Resolve the candidate backends for an autonomous event and run the
1254
+ * N2 spawn gates against them. Fail-open on every internal error
1255
+ * (binding resolution included) — the gate exists to save sessions
1256
+ * that would deterministically fail, never to block live ones.
1257
+ * Returns `null` when the gate could not be evaluated.
1258
+ */
1259
+ async evaluateAutonomousSpawnGate(event) {
1260
+ try {
1261
+ // Scheduled rows / integration cron events can pin a backend via
1262
+ // `requestedBackendId`; the router's backend-only override branch
1263
+ // then routes to exactly that backend WITHOUT a fallback. Mirror
1264
+ // that contract here: gating a pinned row on the *default* binding
1265
+ // would keep skipping it while its pinned backend is healthy (and
1266
+ // re-skip every watcher tick until the wrong backend recovered).
1267
+ const pinned = event
1268
+ .requestedBackendId;
1269
+ if (typeof pinned === "string" && isBackendId(pinned)) {
1270
+ return await this.spawnGate.evaluate([pinned]);
1271
+ }
1272
+ // No pin → event-type default binding. Process-key overrides that
1273
+ // some dispatch branches apply (e.g. `agent.task`, morning stage
1274
+ // keys) are approximated by this default: a mismatch is possible
1275
+ // only when the operator routed that specific process key to a
1276
+ // different backend, and the gate's fail-open posture bounds the
1277
+ // cost to one tick of latency during a partial outage.
1278
+ const binding = this.agentRouter.resolveBinding(event);
1279
+ const candidates = [binding.main.backendId];
1280
+ if (binding.fallback
1281
+ && binding.fallback.backendId !== binding.main.backendId) {
1282
+ candidates.push(binding.fallback.backendId);
1283
+ }
1284
+ return await this.spawnGate.evaluate(candidates);
1285
+ }
1286
+ catch (err) {
1287
+ logger.warn({ err, eventType: event.type }, "Spawn-gate binding resolution failed — failing open");
1288
+ return null;
1289
+ }
1290
+ }
1180
1291
  async handleEvent(event) {
1181
1292
  try {
1182
1293
  await this.handleEventInner(event);
@@ -1198,12 +1309,12 @@ export class EventDispatcher {
1198
1309
  }
1199
1310
  }
1200
1311
  /**
1201
- * Public entry point. Delegates to the HourlyCheckCoordinator.
1312
+ * Public entry point. Delegates to the ActivityScanCoordinator.
1202
1313
  * The dispatcher keeps the wrapper because tests + the cron entry
1203
- * call `dispatcher.triggerHourlyCheck(source, opts)` directly.
1314
+ * call `dispatcher.triggerActivityScan(source, opts)` directly.
1204
1315
  */
1205
- async triggerHourlyCheck(source, options = {}) {
1206
- return this.hourlyCheck.trigger(source, options);
1316
+ async triggerActivityScan(source, options = {}) {
1317
+ return this.activityScan.trigger(source, options);
1207
1318
  }
1208
1319
  /**
1209
1320
  * Advisory check: is a morning routine execution or retry currently in
@@ -1218,7 +1329,7 @@ export class EventDispatcher {
1218
1329
  *
1219
1330
  * Public (not private) because Phase 4's `AuthHealthMonitor.checkAll()`
1220
1331
  * shares the same skip-while-morning-routine-active invariant as the
1221
- * hourly check, and injects this method as an option so a probe tick
1332
+ * activity scan, and injects this method as an option so a probe tick
1222
1333
  * running concurrently with morning routine can no-op cleanly. See
1223
1334
  * `docs/design/09-safety-cost.md` §9.5.4.
1224
1335
  */
@@ -1257,6 +1368,38 @@ export class EventDispatcher {
1257
1368
  .run(event.scheduleId);
1258
1369
  }
1259
1370
  }
1371
+ /**
1372
+ * Throttle for spawn-gate skip audit rows. A released schedule row is
1373
+ * due immediately, so the watcher re-claims it every poll tick (5s
1374
+ * default) for the whole outage — without this, one offline day per
1375
+ * pending row writes ~17k identical agent_actions rows. Keyed by
1376
+ * (schedule id | event type) × reason so distinct routines and
1377
+ * distinct reasons each still get their own first row, and a reason
1378
+ * flip (offline → auth_unhealthy) is recorded promptly.
1379
+ */
1380
+ shouldWriteSpawnGateSkipAudit(event, reason) {
1381
+ const subject = isScheduledEvent(event) && event.scheduleId
1382
+ ? `schedule:${event.scheduleId}`
1383
+ : `type:${event.type}`;
1384
+ const key = `${subject}|${reason}`;
1385
+ const now = Date.now();
1386
+ const last = this.spawnGateSkipAuditAt.get(key);
1387
+ if (last !== undefined
1388
+ && now - last < EventDispatcher.SPAWN_GATE_SKIP_AUDIT_THROTTLE_MS) {
1389
+ return false;
1390
+ }
1391
+ // Opportunistic prune so a long outage across many schedule rows
1392
+ // cannot grow the map unbounded.
1393
+ if (this.spawnGateSkipAuditAt.size > 256) {
1394
+ for (const [k, ts] of this.spawnGateSkipAuditAt) {
1395
+ if (now - ts >= EventDispatcher.SPAWN_GATE_SKIP_AUDIT_THROTTLE_MS) {
1396
+ this.spawnGateSkipAuditAt.delete(k);
1397
+ }
1398
+ }
1399
+ }
1400
+ this.spawnGateSkipAuditAt.set(key, now);
1401
+ return true;
1402
+ }
1260
1403
  async dispatchSafe(event) {
1261
1404
  const trigger = this.isReactive(event) ? "reactive" : "autonomous";
1262
1405
  const startMs = Date.now();
@@ -1294,7 +1437,7 @@ export class EventDispatcher {
1294
1437
  }
1295
1438
  // Autonomous daily cost cap — safety net distinct from removed Phase 9
1296
1439
  // maxDailyCostUsd (which blanket-blocked all sessions including DMs).
1297
- // Reactive sessions always pass. Degradation priority: hourly_check is
1440
+ // Reactive sessions always pass. Degradation priority: activity_scan is
1298
1441
  // skipped first, morning_routine last.
1299
1442
  if (this.shouldSkipForCostCap(event)) {
1300
1443
  this.releaseClaimedSchedule(event);
@@ -1302,6 +1445,36 @@ export class EventDispatcher {
1302
1445
  logger.info({ eventType: event.type, source: event.source }, "Event skipped — autonomous daily cost cap exceeded");
1303
1446
  return;
1304
1447
  }
1448
+ // PREPASS_COST_REDUCTION_PLAN.md N2 — offline + auth spawn gates.
1449
+ // Skip the spawn only when EVERY candidate backend (main +
1450
+ // fallback) is non-viable: backend API host unresolvable
1451
+ // (`reason='offline'`) or auth confirmed bad in a fresh cache
1452
+ // (`reason='auth_unhealthy'`). A scheduled row is released back
1453
+ // to `pending` so the next watcher tick retries — the skip costs
1454
+ // at most one tick of latency, and only during a window where
1455
+ // the session would have failed anyway. Reactive work (user DMs)
1456
+ // is exempt by construction: a user-visible attempt + error beats
1457
+ // silent suppression.
1458
+ const gateDecision = await this.evaluateAutonomousSpawnGate(event);
1459
+ if (gateDecision?.skip) {
1460
+ this.releaseClaimedSchedule(event);
1461
+ const reason = gateDecision.reason ?? "offline";
1462
+ if (this.shouldWriteSpawnGateSkipAudit(event, reason)) {
1463
+ this.audit.logSkip(event, reason, trigger, {
1464
+ spawnGate: { backends: gateDecision.backends },
1465
+ });
1466
+ logger.info({
1467
+ eventType: event.type,
1468
+ source: event.source,
1469
+ reason,
1470
+ backends: gateDecision.backends,
1471
+ }, "Event skipped — autonomous spawn gate (offline / auth-unhealthy backends)");
1472
+ }
1473
+ else {
1474
+ logger.debug({ eventType: event.type, source: event.source, reason }, "Event skipped — spawn gate (audit row throttled, same skip already recorded)");
1475
+ }
1476
+ return;
1477
+ }
1305
1478
  }
1306
1479
  // AGENT_DEFINITIONS_DESIGN.md §8.1 — open the execution row after the
1307
1480
  // setup / cost gates pass (a gated firing records no execution) and
@@ -1350,9 +1523,9 @@ export class EventDispatcher {
1350
1523
  await this.errorRouter.handleError(event, err);
1351
1524
  }
1352
1525
  finally {
1353
- if (isRoutineEvent(event) && event.routine === "hourly_check") {
1354
- this.hourlyCheckInProgress = false;
1355
- this.hourlyCheckInProgressAt = null;
1526
+ if (isRoutineEvent(event) && event.routine === "activity_scan") {
1527
+ this.activityScanInProgress = false;
1528
+ this.activityScanInProgressAt = null;
1356
1529
  }
1357
1530
  }
1358
1531
  }
@@ -1428,7 +1601,7 @@ export class EventDispatcher {
1428
1601
  await this.scheduledTasks.executeSkillCurationRoutine(event);
1429
1602
  }
1430
1603
  else {
1431
- // hourly_check, evening_review, weekly_review, monthly_review
1604
+ // activity_scan, evening_review, weekly_review, monthly_review
1432
1605
  // Tier is resolved from process-key defaults by BackendRouter.
1433
1606
  await this.scheduledTasks.executeDefault(event);
1434
1607
  }
@@ -1472,6 +1645,19 @@ export class EventDispatcher {
1472
1645
  await this.scheduledTasks.executeScheduledTask(event);
1473
1646
  });
1474
1647
  }
1648
+ else if (isTaskDeliveryEvent(event)) {
1649
+ await this.runWithSessionGates([...TASK_DELIVERY_GATE_KEYS], async () => {
1650
+ await handleTaskDeliveryInsideGate({
1651
+ db: this.db,
1652
+ config: this.config,
1653
+ notificationMgr: this.notificationMgr,
1654
+ executeScheduledTask: (scheduledEvent) => this.scheduledTasks.executeScheduledTask(scheduledEvent),
1655
+ ...(this.taskDeliveryAssetResolver
1656
+ ? { resolveAssets: this.taskDeliveryAssetResolver }
1657
+ : {}),
1658
+ }, event);
1659
+ });
1660
+ }
1475
1661
  else if (isAgentTaskEvent(event)) {
1476
1662
  // scheduled.task — no gate, retains existing parallel-execution
1477
1663
  // behavior. (scheduled.dm subtype is handled above.)
@@ -1484,10 +1670,49 @@ export class EventDispatcher {
1484
1670
  // logic; here we wire it into the agent_schedule lifecycle.
1485
1671
  await this.handleScheduledBrowserTaskDispatch(event);
1486
1672
  }
1673
+ else if (isScheduledBackgroundTaskEvent(event)) {
1674
+ // BACKGROUND_TASK_RUNNER_DESIGN.md §4.2 — fire-time row creation +
1675
+ // runner handoff, wired into the agent_schedule lifecycle exactly
1676
+ // like scheduled.browser_task.
1677
+ await this.handleScheduledBackgroundTaskDispatch(event);
1678
+ }
1487
1679
  else {
1488
1680
  await this.scheduledTasks.executeDefault(event);
1489
1681
  }
1490
1682
  }
1683
+ /**
1684
+ * BACKGROUND_TASK_RUNNER_DESIGN.md §4.2 — dispatch branch for
1685
+ * `scheduled.background_task`. Defers to `handleScheduledBackgroundTask`
1686
+ * (validation + row creation + runner handoff) and translates the
1687
+ * outcome into the `agent_schedule.status` write.
1688
+ */
1689
+ async handleScheduledBackgroundTaskDispatch(event) {
1690
+ const { handleScheduledBackgroundTask } = await import("./dispatcher-scheduled-background-task.js");
1691
+ const outcome = await handleScheduledBackgroundTask({ db: this.db, runner: this.backgroundTaskRunner }, event);
1692
+ const succeeded = outcome.kind === "dispatched" || outcome.kind === "row_already_exists";
1693
+ this.db
1694
+ .prepare("UPDATE agent_schedule SET status = ? WHERE id = ? AND status = 'running'")
1695
+ .run(succeeded ? "completed" : "failed", event.scheduleId);
1696
+ if (!succeeded) {
1697
+ try {
1698
+ this.db
1699
+ .prepare(`INSERT INTO agent_actions
1700
+ (action_type, detail, result, started_at, completed_at)
1701
+ VALUES (?, ?, 'failure', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`)
1702
+ .run("background_task.scheduled_dispatch_failed", JSON.stringify({
1703
+ scheduleId: event.scheduleId,
1704
+ kind: outcome.kind,
1705
+ ...("taskId" in outcome ? { taskId: outcome.taskId } : {}),
1706
+ ...("reason" in outcome ? { reason: outcome.reason } : {}),
1707
+ }));
1708
+ }
1709
+ catch (auditErr) {
1710
+ /* c8 ignore start -- defensive */
1711
+ logger.warn({ err: auditErr, scheduleId: event.scheduleId, kind: outcome.kind }, "failed to record background_task.scheduled_dispatch_failed audit row");
1712
+ /* c8 ignore stop */
1713
+ }
1714
+ }
1715
+ }
1491
1716
  /**
1492
1717
  * BROWSER_TASK_REDESIGN_PLAN.md §6.2 + §7 — dispatch branch for
1493
1718
  * `scheduled.browser_task`. Defers the heavy lifting to
@@ -51,7 +51,7 @@ export interface DmFreshnessAggregate {
51
51
  * `agent_log_lag_minutes=0` by construction (the snapshot is built at
52
52
  * dispatch time), so including them would drag the percentile toward 0
53
53
  * and hide the cohort the plan §6 acceptance threshold targets
54
- * ("p95 ≤ 60 — i.e. resumed turns are typically within an hourly_check
54
+ * ("p95 ≤ 60 — i.e. resumed turns are typically within an activity_scan
55
55
  * cadence of session start"). When `resumedTurns === 0`, both
56
56
  * percentiles are 0 — there is no lag to report.
57
57
  */