@aitne/daemon 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.d.ts +1 -0
  15. package/dist/api/env-writer.js +17 -7
  16. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  17. package/dist/api/helpers/agent-errors-registry.js +5 -5
  18. package/dist/api/routes/agent-schedule.js +5 -1
  19. package/dist/api/routes/agent.js +33 -12
  20. package/dist/api/routes/agents/index.js +75 -16
  21. package/dist/api/routes/agents/views.d.ts +37 -2
  22. package/dist/api/routes/agents/views.js +64 -2
  23. package/dist/api/routes/apple-calendar.js +4 -1
  24. package/dist/api/routes/background-task.d.ts +22 -0
  25. package/dist/api/routes/background-task.js +338 -0
  26. package/dist/api/routes/browser-history.js +9 -1
  27. package/dist/api/routes/calendar.js +12 -2
  28. package/dist/api/routes/context/path-resolve.js +6 -1
  29. package/dist/api/routes/context/permissions.js +12 -2
  30. package/dist/api/routes/context/snapshots.js +0 -3
  31. package/dist/api/routes/context/write.js +3 -17
  32. package/dist/api/routes/dashboard/config.js +58 -12
  33. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  34. package/dist/api/routes/dashboard/notifications.js +9 -9
  35. package/dist/api/routes/dashboard/oauth-google.js +5 -3
  36. package/dist/api/routes/feedback.d.ts +3 -0
  37. package/dist/api/routes/feedback.js +349 -0
  38. package/dist/api/routes/git.js +10 -3
  39. package/dist/api/routes/github.js +5 -1
  40. package/dist/api/routes/integrations/crud-patch.js +5 -1
  41. package/dist/api/routes/integrations-reconcile.js +2 -2
  42. package/dist/api/routes/mcp.js +65 -13
  43. package/dist/api/routes/notion.d.ts +1 -1
  44. package/dist/api/routes/observations.js +7 -7
  45. package/dist/api/routes/obsidian.d.ts +1 -1
  46. package/dist/api/routes/receipts.js +5 -1
  47. package/dist/api/routes/setup-migrate.js +1 -1
  48. package/dist/api/routes/setup.js +1 -1
  49. package/dist/api/routes/task-flows.d.ts +1 -1
  50. package/dist/api/routes/task-flows.js +1 -1
  51. package/dist/api/routes/tuning.d.ts +29 -0
  52. package/dist/api/routes/tuning.js +304 -0
  53. package/dist/api/server.d.ts +44 -16
  54. package/dist/api/server.js +12 -0
  55. package/dist/bootstrap/adapters.d.ts +19 -0
  56. package/dist/bootstrap/adapters.js +61 -0
  57. package/dist/bootstrap/api.d.ts +5 -3
  58. package/dist/bootstrap/api.js +45 -13
  59. package/dist/bootstrap/catchup.d.ts +1 -1
  60. package/dist/bootstrap/catchup.js +11 -11
  61. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  62. package/dist/bootstrap/event-pipeline.js +246 -8
  63. package/dist/bootstrap/observers.js +9 -6
  64. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  65. package/dist/bootstrap/schedule-helpers.js +172 -19
  66. package/dist/config.js +32 -12
  67. package/dist/core/agent-core.d.ts +33 -1
  68. package/dist/core/agent-core.js +36 -1
  69. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  70. package/dist/core/agents/activity-scan-cadence.js +127 -0
  71. package/dist/core/agents/agent-route-override.d.ts +53 -0
  72. package/dist/core/agents/agent-route-override.js +69 -0
  73. package/dist/core/agents/builtin-registry.d.ts +51 -14
  74. package/dist/core/agents/builtin-registry.js +92 -15
  75. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  76. package/dist/core/agents/config-gate-reconcile.js +51 -0
  77. package/dist/core/agents/cron-substitute.d.ts +1 -1
  78. package/dist/core/agents/cron-substitute.js +1 -1
  79. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  80. package/dist/core/agents/custom-routine-migration.js +149 -0
  81. package/dist/core/agents/firing-blocked.d.ts +1 -1
  82. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  83. package/dist/core/agents/hourly-cadence.js +126 -0
  84. package/dist/core/agents/loader-boot.js +23 -0
  85. package/dist/core/agents/loader.d.ts +19 -0
  86. package/dist/core/agents/loader.js +34 -2
  87. package/dist/core/agents/override-merge.d.ts +1 -1
  88. package/dist/core/agents/override-merge.js +9 -1
  89. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  90. package/dist/core/agents/recurrence-convert.js +1 -1
  91. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  92. package/dist/core/alerts.js +6 -6
  93. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  94. package/dist/core/backends/auth-health-monitor.js +1 -1
  95. package/dist/core/backends/backend-router.d.ts +27 -1
  96. package/dist/core/backends/backend-router.js +165 -1
  97. package/dist/core/backends/claude-code-core.d.ts +71 -31
  98. package/dist/core/backends/claude-code-core.js +282 -54
  99. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  100. package/dist/core/backends/cli-quota-guards.js +40 -5
  101. package/dist/core/backends/codex-core.d.ts +6 -0
  102. package/dist/core/backends/codex-core.js +22 -6
  103. package/dist/core/backends/failure-spend.d.ts +58 -0
  104. package/dist/core/backends/failure-spend.js +137 -0
  105. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  106. package/dist/core/backends/gemini-cli-core.js +38 -6
  107. package/dist/core/backends/model-registry.d.ts +1 -1
  108. package/dist/core/backends/model-registry.js +4 -4
  109. package/dist/core/backends/opencode-core.d.ts +1 -1
  110. package/dist/core/backends/opencode-core.js +5 -5
  111. package/dist/core/backends/plan-presets.js +47 -18
  112. package/dist/core/bang-commands/commands-cost.js +3 -1
  113. package/dist/core/bang-commands/commands-report.js +4 -3
  114. package/dist/core/bang-commands/commands-research.js +4 -1
  115. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  116. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  117. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  118. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  119. package/dist/core/bang-commands/commands-task-control.js +147 -0
  120. package/dist/core/bang-commands/commands-wiki.js +5 -5
  121. package/dist/core/bang-commands/index.d.ts +2 -0
  122. package/dist/core/bang-commands/index.js +12 -0
  123. package/dist/core/bang-commands/registry.d.ts +12 -0
  124. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  125. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  126. package/dist/core/channel-timeline.d.ts +5 -1
  127. package/dist/core/channel-timeline.js +13 -0
  128. package/dist/core/context/index-reconciler.js +5 -2
  129. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  130. package/dist/core/context/policy-index-runner.js +25 -6
  131. package/dist/core/context-builder-calendar.js +10 -2
  132. package/dist/core/context-builder-conversation.d.ts +8 -1
  133. package/dist/core/context-builder-conversation.js +41 -7
  134. package/dist/core/context-builder-yesterday.js +4 -3
  135. package/dist/core/context-builder.d.ts +7 -2
  136. package/dist/core/context-builder.js +193 -5
  137. package/dist/core/context-file-serializer.d.ts +1 -1
  138. package/dist/core/context-file-serializer.js +1 -1
  139. package/dist/core/context-health.js +2 -2
  140. package/dist/core/context-paths.d.ts +11 -1
  141. package/dist/core/context-paths.js +17 -1
  142. package/dist/core/context-validation/prepare-write.js +1 -1
  143. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  144. package/dist/core/context-vault-aliases.d.ts +0 -13
  145. package/dist/core/context-vault-aliases.js +37 -0
  146. package/dist/core/custom-routines.d.ts +99 -0
  147. package/dist/core/custom-routines.js +187 -0
  148. package/dist/core/daemon-api-cli.js +50 -1
  149. package/dist/core/day-boundary.d.ts +46 -0
  150. package/dist/core/day-boundary.js +40 -0
  151. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  152. package/dist/core/dispatcher-activity-scan.js +775 -0
  153. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  154. package/dist/core/dispatcher-error-handling.js +38 -62
  155. package/dist/core/dispatcher-hourly-check.js +6 -1
  156. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  157. package/dist/core/dispatcher-message-handler.js +24 -0
  158. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  159. package/dist/core/dispatcher-morning-routine.js +13 -13
  160. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  161. package/dist/core/dispatcher-result-processor.js +167 -11
  162. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  163. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  164. package/dist/core/dispatcher-scheduled-tasks.d.ts +104 -1
  165. package/dist/core/dispatcher-scheduled-tasks.js +480 -8
  166. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  167. package/dist/core/dispatcher-task-delivery.js +555 -0
  168. package/dist/core/dispatcher-types.d.ts +48 -9
  169. package/dist/core/dispatcher-types.js +3 -3
  170. package/dist/core/dispatcher.d.ts +112 -31
  171. package/dist/core/dispatcher.js +297 -60
  172. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  173. package/dist/core/drift-effects.js +2 -2
  174. package/dist/core/feedback/consolidation-prep.d.ts +94 -0
  175. package/dist/core/feedback/consolidation-prep.js +254 -0
  176. package/dist/core/feedback/eviction-scorer.d.ts +81 -0
  177. package/dist/core/feedback/eviction-scorer.js +136 -0
  178. package/dist/core/feedback/lesson-format.d.ts +79 -0
  179. package/dist/core/feedback/lesson-format.js +199 -0
  180. package/dist/core/feedback/lesson-injection.d.ts +98 -0
  181. package/dist/core/feedback/lesson-injection.js +174 -0
  182. package/dist/core/feedback/lesson-merge.d.ts +51 -0
  183. package/dist/core/feedback/lesson-merge.js +88 -0
  184. package/dist/core/feedback/lesson-store-overview.d.ts +46 -0
  185. package/dist/core/feedback/lesson-store-overview.js +42 -0
  186. package/dist/core/feedback/promotion-gate.d.ts +69 -0
  187. package/dist/core/feedback/promotion-gate.js +117 -0
  188. package/dist/core/feedback/regeneralization-prep.d.ts +87 -0
  189. package/dist/core/feedback/regeneralization-prep.js +152 -0
  190. package/dist/core/feedback/scope-parser.d.ts +86 -0
  191. package/dist/core/feedback/scope-parser.js +141 -0
  192. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  193. package/dist/core/feedback/self-performance-prep.js +541 -0
  194. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  195. package/dist/core/feedback/tuning-actuator.js +432 -0
  196. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  197. package/dist/core/feedback/tuning-recommender.js +580 -0
  198. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  199. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  200. package/dist/core/health-monitor.d.ts +6 -0
  201. package/dist/core/health-monitor.js +1 -1
  202. package/dist/core/injection-policy.d.ts +83 -1
  203. package/dist/core/injection-policy.js +61 -3
  204. package/dist/core/integration-main-backend.js +4 -0
  205. package/dist/core/management-md.d.ts +2 -2
  206. package/dist/core/management-md.js +51 -13
  207. package/dist/core/morning/orchestrator.d.ts +2 -2
  208. package/dist/core/morning/orchestrator.js +2 -2
  209. package/dist/core/notification-gate.d.ts +64 -0
  210. package/dist/core/notification-gate.js +51 -0
  211. package/dist/core/notification-rate-limit.d.ts +40 -0
  212. package/dist/core/notification-rate-limit.js +50 -0
  213. package/dist/core/policy-files.d.ts +1 -1
  214. package/dist/core/policy-files.js +2 -2
  215. package/dist/core/pre-pass-freshness.d.ts +4 -4
  216. package/dist/core/retention.d.ts +5 -0
  217. package/dist/core/retention.js +20 -4
  218. package/dist/core/review-context.d.ts +1 -1
  219. package/dist/core/review-context.js +10 -5
  220. package/dist/core/roadmap-write-lock.d.ts +2 -1
  221. package/dist/core/roadmap-write-lock.js +15 -10
  222. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  223. package/dist/core/routine-acquisition-plan.js +78 -20
  224. package/dist/core/routine-fetch-window-retry.js +7 -4
  225. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  226. package/dist/core/routine-fetch-window-runner.js +264 -13
  227. package/dist/core/routine-windows.d.ts +2 -2
  228. package/dist/core/routine-windows.js +8 -5
  229. package/dist/core/scheduler.d.ts +175 -16
  230. package/dist/core/scheduler.js +559 -102
  231. package/dist/core/signal-detector.d.ts +51 -1
  232. package/dist/core/signal-detector.js +321 -24
  233. package/dist/core/skills-compiler-denied-tools.js +2 -2
  234. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  235. package/dist/core/skills-compiler-skill-index.js +2 -2
  236. package/dist/core/skills-compiler-variants.d.ts +1 -1
  237. package/dist/core/skills-compiler-variants.js +8 -0
  238. package/dist/core/skills-compiler.d.ts +29 -26
  239. package/dist/core/skills-compiler.js +117 -81
  240. package/dist/core/skills-manifest.d.ts +37 -0
  241. package/dist/core/skills-manifest.js +73 -2
  242. package/dist/core/sleep-inhibitor.d.ts +79 -0
  243. package/dist/core/sleep-inhibitor.js +132 -0
  244. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  245. package/dist/core/slim-system-prompt-loader.js +141 -0
  246. package/dist/core/spawn-gates.d.ts +126 -0
  247. package/dist/core/spawn-gates.js +180 -0
  248. package/dist/core/today-direct-writer.d.ts +60 -14
  249. package/dist/core/today-direct-writer.js +90 -13
  250. package/dist/core/today-write-lock.d.ts +4 -2
  251. package/dist/core/today-write-lock.js +30 -20
  252. package/dist/core/wake-detector.d.ts +55 -0
  253. package/dist/core/wake-detector.js +80 -0
  254. package/dist/core/wiki/compile-lock.d.ts +1 -1
  255. package/dist/core/wiki/compile-lock.js +1 -1
  256. package/dist/core/wiki/wiki-fts.js +13 -6
  257. package/dist/core/workdir.js +15 -6
  258. package/dist/db/activity-scan-signals.d.ts +77 -0
  259. package/dist/db/activity-scan-signals.js +378 -0
  260. package/dist/db/agents-store.d.ts +28 -0
  261. package/dist/db/agents-store.js +62 -0
  262. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  263. package/dist/db/background-task-clarifications-store.js +152 -0
  264. package/dist/db/background-task-store.d.ts +207 -0
  265. package/dist/db/background-task-store.js +380 -0
  266. package/dist/db/browser-history-store.d.ts +39 -6
  267. package/dist/db/browser-history-store.js +51 -7
  268. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  269. package/dist/db/browser-task-clarifications-store.js +35 -5
  270. package/dist/db/browser-task-store.d.ts +3 -0
  271. package/dist/db/browser-task-store.js +29 -4
  272. package/dist/db/deferred-dm.d.ts +86 -0
  273. package/dist/db/deferred-dm.js +199 -0
  274. package/dist/db/feedback-signals-store.d.ts +77 -0
  275. package/dist/db/feedback-signals-store.js +144 -0
  276. package/dist/db/migrations.js +380 -0
  277. package/dist/db/observations.d.ts +2 -2
  278. package/dist/db/observations.js +3 -3
  279. package/dist/db/schema.js +260 -22
  280. package/dist/db/voice-transcripts-store.d.ts +1 -1
  281. package/dist/index.js +86 -29
  282. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  283. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  284. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  285. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  286. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  287. package/dist/observers/delegated-sync-worker.js +10 -10
  288. package/dist/observers/git-delegated-cron.d.ts +1 -1
  289. package/dist/observers/git-delegated-cron.js +2 -2
  290. package/dist/observers/github-poller-classifier.d.ts +3 -3
  291. package/dist/observers/github-poller-classifier.js +3 -3
  292. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  293. package/dist/observers/imminent-event-scheduler.js +1 -1
  294. package/dist/observers/mail-poller.d.ts +1 -0
  295. package/dist/observers/mail-poller.js +42 -3
  296. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  297. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  298. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  299. package/dist/observers/observation-summarizer/worker.js +4 -4
  300. package/dist/observers/obsidian-watcher.d.ts +1 -1
  301. package/dist/observers/obsidian-watcher.js +1 -1
  302. package/dist/safety/agent-write-tracker.d.ts +4 -4
  303. package/dist/safety/agent-write-tracker.js +4 -4
  304. package/dist/safety/always-disallowed.d.ts +1 -1
  305. package/dist/safety/always-disallowed.js +39 -0
  306. package/dist/safety/audit.d.ts +43 -5
  307. package/dist/safety/audit.js +86 -18
  308. package/dist/safety/risk-classifier.d.ts +6 -0
  309. package/dist/safety/risk-classifier.js +97 -18
  310. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  311. package/dist/scheduler/activity-scan-gate.js +132 -0
  312. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  313. package/dist/services/background-task/background-task-budget.js +91 -0
  314. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  315. package/dist/services/background-task/background-task-driver.js +416 -0
  316. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  317. package/dist/services/background-task/background-task-runner.js +673 -0
  318. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  319. package/dist/services/background-task/background-task-tools.js +247 -0
  320. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  321. package/dist/services/background-task/background-task-transition-events.js +54 -0
  322. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  323. package/dist/services/browser-history/automation/egress-denylist.js +34 -8
  324. package/dist/services/browser-history/lifecycle/platform.js +44 -2
  325. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  326. package/dist/services/browser-task/browser-task-runner.js +53 -8
  327. package/dist/services/mcp/probe.js +30 -8
  328. package/dist/services/observations-batch.d.ts +1 -1
  329. package/dist/services/observations-batch.js +2 -2
  330. package/dist/settings/runtime-settings.d.ts +45 -12
  331. package/dist/settings/runtime-settings.js +215 -40
  332. package/dist/settings/settings-store.js +11 -3
  333. package/package.json +4 -4
@@ -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({
@@ -587,6 +630,7 @@ export class EventDispatcher {
587
630
  morningRoutine: this.morningRoutine,
588
631
  fetchWindowRunner: this.fetchWindowRunner,
589
632
  roadmapWriteLock: this.roadmapWriteLock,
633
+ todayWriteLock: this.todayWriteLock,
590
634
  writeTracker: this.writeTracker,
591
635
  getConfiguredServices: () => this.getConfiguredServices(),
592
636
  getActiveMailAccounts: () => this.getActiveMailAccounts(),
@@ -616,6 +660,8 @@ export class EventDispatcher {
616
660
  getBangCommandRegistry: () => this.bangCommandRegistry,
617
661
  getPurchaseHandler: () => this.purchaseHandler,
618
662
  getFinalConfirmHandler: () => this.finalConfirmHandler,
663
+ getBackgroundTaskRunner: () => this.backgroundTaskRunner,
664
+ getBrowserTaskRunner: () => this.browserTaskRunner,
619
665
  getCurrentSetupMode: () => this.currentSetupMode,
620
666
  beginSetupMode: (mode) => this.beginSetupMode(mode),
621
667
  lookupCustomBangCommandForEvent: (event) => this.lookupCustomBangCommandForEvent(event),
@@ -683,6 +729,15 @@ export class EventDispatcher {
683
729
  getBrowserTaskRunner() {
684
730
  return this.browserTaskRunner;
685
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
+ }
686
741
  /**
687
742
  * BROWSER_TASK_REDESIGN_PLAN.md §7 — wire the terminal-state DM
688
743
  * emitter used by the `scheduled.browser_task` failure paths (see
@@ -712,6 +767,14 @@ export class EventDispatcher {
712
767
  setAttachmentStore(store) {
713
768
  this.attachmentStore = store;
714
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
+ }
715
778
  /** Inject the local-Whisper voice transcriber. Optional — when unset,
716
779
  * inbound audio attachments are passed to the backend with a path-only
717
780
  * reference (the pre-feature behaviour). */
@@ -720,7 +783,7 @@ export class EventDispatcher {
720
783
  }
721
784
  /**
722
785
  * Inject the delegated-sync refresh callback. Called from
723
- * `triggerHourlyCheck` before the gate decision so any cadence the
786
+ * `triggerActivityScan` before the gate decision so any cadence the
724
787
  * operator left opted-OUT (post-Phase-9 default) populates fresh
725
788
  * Gmail / Notion observations the agent can then consume.
726
789
  *
@@ -730,7 +793,7 @@ export class EventDispatcher {
730
793
  * dispatcher holding a stale reference.
731
794
  *
732
795
  * Pass `null` to detach (e.g. when no delegated integration exists).
733
- * The hourly check then proceeds without a refresh — equivalent to the
796
+ * The activity scan then proceeds without a refresh — equivalent to the
734
797
  * pre-injection behaviour.
735
798
  */
736
799
  setDelegatedSyncRefresh(fn) {
@@ -738,7 +801,7 @@ export class EventDispatcher {
738
801
  }
739
802
  /**
740
803
  * Wire the scheduler's `queueMorningRoutineWake` so the pre-routine
741
- * gate (hourly_check + evening/weekly/monthly review) can self-recover
804
+ * gate (activity_scan + evening/weekly/monthly review) can self-recover
742
805
  * after a missed 04:00 cron fire. Wired once in `index.ts` after both
743
806
  * the dispatcher and scheduler are constructed; passing `null` detaches.
744
807
  * When unset, the gate logs a warning and still skips the dependent
@@ -765,6 +828,16 @@ export class EventDispatcher {
765
828
  setAgentExecutionTracker(tracker) {
766
829
  this.agentExecutionTracker = tracker;
767
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
+ }
768
841
  /**
769
842
  * Open an execution row for an agent-resolvable firing (§8.1), called from
770
843
  * `dispatchSafe` after the setup / cost gates pass so a skipped firing never
@@ -803,10 +876,21 @@ export class EventDispatcher {
803
876
  const scheduleRowId = isScheduledEvent(event) && event.scheduleId !== undefined
804
877
  ? event.scheduleId
805
878
  : null;
806
- this.agentExecutionTracker.begin(event.correlationId, resolution, {
879
+ // `begin` opens the rollup row AND returns the resolved Agent slug (or null
880
+ // for a firing that resolves to no Agent — reactive DMs, legacy tasks).
881
+ const agentId = this.agentExecutionTracker.begin(event.correlationId, resolution, {
807
882
  scheduleRowId,
808
883
  trigger: this.resolveExecutionTrigger(event, taskContext),
809
884
  });
885
+ // FEEDBACK_LEARNING_LOOP_DESIGN.md §5 Phase 4 — thread the resolved slug onto
886
+ // the event so `ContextBuilder` can inject this Agent's own
887
+ // `policies/agents/<slug>/lessons.md` (scope `agent:<slug>`). This runs in
888
+ // `dispatchSafe` before `dispatch(event)`, so the stamp is visible to the
889
+ // builder downstream; the morning routine propagates it to Stage A via the
890
+ // `{...parent.data}` spread in `composeStageAEvent`. No-op for unbound runs.
891
+ if (agentId !== null) {
892
+ event.data.agentId = agentId;
893
+ }
810
894
  }
811
895
  /** Classify an execution's trigger for the rollup row (§5.2). */
812
896
  resolveExecutionTrigger(event, taskContext) {
@@ -962,7 +1046,7 @@ export class EventDispatcher {
962
1046
  /**
963
1047
  * Enter setup mode. Called from `POST /setup/start` so the warm gate
964
1048
  * engages the moment the user opens the dashboard setup flow — before any
965
- * agent turn runs — so concurrent hourly_check / morning routine / scheduled
1049
+ * agent turn runs — so concurrent activity_scan / morning routine / scheduled
966
1050
  * wake work cannot race with the setup conversation. Persisted to
967
1051
  * `runtime_state` so the flag survives daemon restart.
968
1052
  */
@@ -1023,8 +1107,8 @@ export class EventDispatcher {
1023
1107
  if (this.morningRoutineInProgress) {
1024
1108
  executions.push({ kind: "routine", key: "morning_routine" });
1025
1109
  }
1026
- if (this.hourlyCheckInProgress) {
1027
- executions.push({ kind: "routine", key: "hourly_check" });
1110
+ if (this.activityScanInProgress) {
1111
+ executions.push({ kind: "routine", key: "activity_scan" });
1028
1112
  }
1029
1113
  const runningTasks = this.db
1030
1114
  .prepare(`SELECT id, task_type, task_description
@@ -1042,7 +1126,7 @@ export class EventDispatcher {
1042
1126
  return executions;
1043
1127
  }
1044
1128
  /**
1045
- * Gate for autonomous background work (cron routines, hourly_check,
1129
+ * Gate for autonomous background work (cron routines, activity_scan,
1046
1130
  * scheduled wake tasks, startup catchup, calendar-poller reactive events).
1047
1131
  *
1048
1132
  * Two layers:
@@ -1131,7 +1215,7 @@ export class EventDispatcher {
1131
1215
  /**
1132
1216
  * Check whether this autonomous event should be skipped because the daily
1133
1217
  * autonomous cost cap has been exceeded. Uses priority-based degradation:
1134
- * hourly_check (lowest priority, skipped first) → roadmap_refresh →
1218
+ * activity_scan (lowest priority, skipped first) → roadmap_refresh →
1135
1219
  * evening_review → morning_routine (highest, last to be cut).
1136
1220
  *
1137
1221
  * Lower-priority events are skipped at 100% of cap; higher-priority events
@@ -1157,7 +1241,7 @@ export class EventDispatcher {
1157
1241
  ? event.routine
1158
1242
  : null;
1159
1243
  const thresholds = {
1160
- hourly_check: 1.0, // skipped first (at 100% of cap)
1244
+ activity_scan: 1.0, // skipped first (at 100% of cap)
1161
1245
  roadmap_refresh: 1.2, // skipped at 120%
1162
1246
  evening_review: 1.5, // skipped at 150%
1163
1247
  morning_routine: 2.0, // last to be cut (only at 200%)
@@ -1165,6 +1249,45 @@ export class EventDispatcher {
1165
1249
  const threshold = routine ? (thresholds[routine] ?? 1.0) : 1.0;
1166
1250
  return todayCost >= cap * threshold;
1167
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
+ }
1168
1291
  async handleEvent(event) {
1169
1292
  try {
1170
1293
  await this.handleEventInner(event);
@@ -1186,12 +1309,12 @@ export class EventDispatcher {
1186
1309
  }
1187
1310
  }
1188
1311
  /**
1189
- * Public entry point. Delegates to the HourlyCheckCoordinator.
1312
+ * Public entry point. Delegates to the ActivityScanCoordinator.
1190
1313
  * The dispatcher keeps the wrapper because tests + the cron entry
1191
- * call `dispatcher.triggerHourlyCheck(source, opts)` directly.
1314
+ * call `dispatcher.triggerActivityScan(source, opts)` directly.
1192
1315
  */
1193
- async triggerHourlyCheck(source, options = {}) {
1194
- return this.hourlyCheck.trigger(source, options);
1316
+ async triggerActivityScan(source, options = {}) {
1317
+ return this.activityScan.trigger(source, options);
1195
1318
  }
1196
1319
  /**
1197
1320
  * Advisory check: is a morning routine execution or retry currently in
@@ -1206,7 +1329,7 @@ export class EventDispatcher {
1206
1329
  *
1207
1330
  * Public (not private) because Phase 4's `AuthHealthMonitor.checkAll()`
1208
1331
  * shares the same skip-while-morning-routine-active invariant as the
1209
- * 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
1210
1333
  * running concurrently with morning routine can no-op cleanly. See
1211
1334
  * `docs/design/09-safety-cost.md` §9.5.4.
1212
1335
  */
@@ -1245,6 +1368,38 @@ export class EventDispatcher {
1245
1368
  .run(event.scheduleId);
1246
1369
  }
1247
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
+ }
1248
1403
  async dispatchSafe(event) {
1249
1404
  const trigger = this.isReactive(event) ? "reactive" : "autonomous";
1250
1405
  const startMs = Date.now();
@@ -1282,7 +1437,7 @@ export class EventDispatcher {
1282
1437
  }
1283
1438
  // Autonomous daily cost cap — safety net distinct from removed Phase 9
1284
1439
  // maxDailyCostUsd (which blanket-blocked all sessions including DMs).
1285
- // Reactive sessions always pass. Degradation priority: hourly_check is
1440
+ // Reactive sessions always pass. Degradation priority: activity_scan is
1286
1441
  // skipped first, morning_routine last.
1287
1442
  if (this.shouldSkipForCostCap(event)) {
1288
1443
  this.releaseClaimedSchedule(event);
@@ -1290,6 +1445,36 @@ export class EventDispatcher {
1290
1445
  logger.info({ eventType: event.type, source: event.source }, "Event skipped — autonomous daily cost cap exceeded");
1291
1446
  return;
1292
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
+ }
1293
1478
  }
1294
1479
  // AGENT_DEFINITIONS_DESIGN.md §8.1 — open the execution row after the
1295
1480
  // setup / cost gates pass (a gated firing records no execution) and
@@ -1338,9 +1523,9 @@ export class EventDispatcher {
1338
1523
  await this.errorRouter.handleError(event, err);
1339
1524
  }
1340
1525
  finally {
1341
- if (isRoutineEvent(event) && event.routine === "hourly_check") {
1342
- this.hourlyCheckInProgress = false;
1343
- this.hourlyCheckInProgressAt = null;
1526
+ if (isRoutineEvent(event) && event.routine === "activity_scan") {
1527
+ this.activityScanInProgress = false;
1528
+ this.activityScanInProgressAt = null;
1344
1529
  }
1345
1530
  }
1346
1531
  }
@@ -1416,7 +1601,7 @@ export class EventDispatcher {
1416
1601
  await this.scheduledTasks.executeSkillCurationRoutine(event);
1417
1602
  }
1418
1603
  else {
1419
- // hourly_check, evening_review, weekly_review, monthly_review
1604
+ // activity_scan, evening_review, weekly_review, monthly_review
1420
1605
  // Tier is resolved from process-key defaults by BackendRouter.
1421
1606
  await this.scheduledTasks.executeDefault(event);
1422
1607
  }
@@ -1460,6 +1645,19 @@ export class EventDispatcher {
1460
1645
  await this.scheduledTasks.executeScheduledTask(event);
1461
1646
  });
1462
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
+ }
1463
1661
  else if (isAgentTaskEvent(event)) {
1464
1662
  // scheduled.task — no gate, retains existing parallel-execution
1465
1663
  // behavior. (scheduled.dm subtype is handled above.)
@@ -1472,10 +1670,49 @@ export class EventDispatcher {
1472
1670
  // logic; here we wire it into the agent_schedule lifecycle.
1473
1671
  await this.handleScheduledBrowserTaskDispatch(event);
1474
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
+ }
1475
1679
  else {
1476
1680
  await this.scheduledTasks.executeDefault(event);
1477
1681
  }
1478
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
+ }
1479
1716
  /**
1480
1717
  * BROWSER_TASK_REDESIGN_PLAN.md §6.2 + §7 — dispatch branch for
1481
1718
  * `scheduled.browser_task`. Defers the heavy lifting to