@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
@@ -0,0 +1,338 @@
1
+ /**
2
+ * /api/background-task/* — BACKGROUND_TASK_RUNNER_DESIGN.md §7.
3
+ *
4
+ * The generic detached-task surface. The DM agent POSTs a self-contained
5
+ * brief, acks, and ends its turn; the runner runs the worker detached and
6
+ * writes an artifact; the delivery boundary surfaces it. GET /:id returns
7
+ * the artifact (the DM agent's "read the result" affordance for precise
8
+ * follow-ups). /clarify relays the owner's answer; /cancel aborts.
9
+ *
10
+ * Routes (§7):
11
+ * POST /api/background-task spawn (or schedule via scheduleAt)
12
+ * GET /api/background-task list
13
+ * GET /api/background-task/:id the artifact (full detail)
14
+ * POST /api/background-task/:id/clarify answer a pending clarification
15
+ * POST /api/background-task/:id/cancel abort in-flight task
16
+ *
17
+ * Excluded from the 100% coverage gate — route glue. The pure pieces
18
+ * (budget envelope, slot manager) are 100%-covered peers.
19
+ */
20
+ import { randomUUID } from "node:crypto";
21
+ import { Hono } from "hono";
22
+ import { z } from "zod";
23
+ import { formatSqliteDatetime } from "@aitne/shared";
24
+ import { countBackgroundTasks, createBackgroundTask, findRecentDuplicateBackgroundTask, getBackgroundTask, listBackgroundTasks, markTerminal, } from "../../db/background-task-store.js";
25
+ import { getOpenClarificationForTask, listClarificationsForTask, resolveClarification, } from "../../db/background-task-clarifications-store.js";
26
+ import { listPrimaryChannels, channelRef, } from "../../db/browser-automation-purchase-primary-channels-store.js";
27
+ import { selectDefaultOwnerChannel } from "../../messaging/owner-channels.js";
28
+ import { createBackgroundTaskTransitionEmitter } from "../../services/background-task/background-task-transition-events.js";
29
+ import { createLogger } from "../../logging.js";
30
+ import { readJsonBody } from "../json-body.js";
31
+ const logger = createLogger("background-task-routes");
32
+ const postBodySchema = z.object({
33
+ brief: z.string().min(1).max(16_384),
34
+ title: z.string().min(1).max(200).optional(),
35
+ notificationPolicy: z
36
+ .enum(["always", "if_significant", "silent"])
37
+ .optional()
38
+ .default("always"),
39
+ // Phase 4 if_significant criteria DSL (§4.3) — optional structured
40
+ // conditions the worker checks one-by-one. Each is a concrete, atomic
41
+ // condition ("any repo's main build is red"); the worker sets
42
+ // notify=true iff ANY is met. Up to 12, each ≤500 chars.
43
+ significanceCriteria: z
44
+ .array(z.string().min(1).max(500))
45
+ .max(12)
46
+ .optional(),
47
+ tier: z.enum(["lite", "medium", "high"]).optional(),
48
+ maxBudgetUsd: z.number().positive().max(15).optional(),
49
+ originatingChannel: z.string().optional(),
50
+ correlationId: z.string().optional(),
51
+ scheduleAt: z.string().datetime({ offset: true }).optional(),
52
+ });
53
+ const clarifyBodySchema = z.object({
54
+ clarificationId: z.string().uuid().optional(),
55
+ answer: z.string().min(1).max(8192),
56
+ });
57
+ const cancelBodySchema = z
58
+ .object({ reason: z.string().max(256).optional() })
59
+ .optional();
60
+ function toWire(row) {
61
+ return {
62
+ id: row.id,
63
+ brief: row.brief,
64
+ title: row.title,
65
+ state: row.state,
66
+ notificationPolicy: row.notificationPolicy,
67
+ significanceCriteria: row.significanceCriteria,
68
+ report: row.report,
69
+ draft: row.draft,
70
+ notify: row.notify,
71
+ significance: row.significance,
72
+ artifactPath: row.artifactPath,
73
+ outcomeDetail: row.outcomeDetail,
74
+ originatingChannel: row.originatingChannel,
75
+ correlationId: row.correlationId,
76
+ scheduleRowId: row.scheduleRowId,
77
+ tier: row.tier,
78
+ maxBudgetUsd: row.maxBudgetUsd,
79
+ createdAt: row.createdAt,
80
+ startedAt: row.startedAt,
81
+ finishedAt: row.finishedAt,
82
+ deliveredAt: row.deliveredAt,
83
+ };
84
+ }
85
+ /** Resolve the originating channel: header / body preferred, intersected
86
+ * with the primary set ∪ the owner's most-recent DM channel. A null
87
+ * result files the artifact but cannot DM it. */
88
+ function resolveOriginatingChannel(deps, headerChannel, bodyChannel) {
89
+ const requested = headerChannel ?? bodyChannel ?? null;
90
+ const primary = listPrimaryChannels(deps.db).map((row) => channelRef(row.platform, row.channelId));
91
+ const ownerDefault = selectDefaultOwnerChannel(deps.db);
92
+ const ownerDefaultRef = ownerDefault
93
+ ? channelRef(ownerDefault.platform, ownerDefault.channelId)
94
+ : null;
95
+ if (requested) {
96
+ if (primary.includes(requested))
97
+ return requested;
98
+ return primary[0] ?? ownerDefaultRef;
99
+ }
100
+ return primary[0] ?? ownerDefaultRef;
101
+ }
102
+ export function createBackgroundTaskRoutes(deps) {
103
+ const app = new Hono();
104
+ const transitionEmitter = createBackgroundTaskTransitionEmitter(deps.eventBroadcaster ?? null);
105
+ // ── POST /api/background-task ─────────────────────────────────────────
106
+ app.post("/background-task", async (c) => {
107
+ const body = await readJsonBody(c);
108
+ if (!body.ok)
109
+ return body.response;
110
+ const parsed = postBodySchema.safeParse(body.body);
111
+ if (!parsed.success) {
112
+ return c.json({ error: "validation_error", details: parsed.error.flatten() }, 400);
113
+ }
114
+ const input = parsed.data;
115
+ const resolvedChannel = resolveOriginatingChannel(deps, c.req.header("x-pa-channel-ref"), input.originatingChannel);
116
+ if (resolvedChannel === null) {
117
+ logger.warn({}, "background-task: no originating channel resolvable — the result will be filed but cannot be DMed");
118
+ }
119
+ const id = randomUUID();
120
+ const title = input.title ?? null;
121
+ // ── Scheduled path ──────────────────────────────────────────────
122
+ if (input.scheduleAt !== undefined) {
123
+ const scheduledAtMs = Date.parse(input.scheduleAt);
124
+ if (!Number.isFinite(scheduledAtMs)) {
125
+ return c.json({ error: "invalid_schedule_at", detail: "scheduleAt must parse as ISO 8601." }, 400);
126
+ }
127
+ const nowMs = Date.now();
128
+ if (scheduledAtMs < nowMs - 60_000) {
129
+ return c.json({
130
+ error: "schedule_at_in_past",
131
+ detail: `scheduleAt resolves to more than 60s in the past (delta=${nowMs - scheduledAtMs}ms).`,
132
+ }, 400);
133
+ }
134
+ const scheduleContext = {
135
+ preGeneratedTaskId: id,
136
+ brief: input.brief,
137
+ title,
138
+ notificationPolicy: input.notificationPolicy,
139
+ significanceCriteria: input.significanceCriteria ?? null,
140
+ tier: input.tier ?? null,
141
+ maxBudgetUsd: input.maxBudgetUsd ?? null,
142
+ originatingChannel: resolvedChannel,
143
+ };
144
+ const correlationId = input.correlationId ?? randomUUID();
145
+ const label = title ?? input.brief.slice(0, 200);
146
+ const insertResult = deps.db
147
+ .prepare(`INSERT INTO agent_schedule
148
+ (scheduled_for, task_type, task_description, task_prompt, task_context, correlation_id, model, status)
149
+ VALUES (?, 'background_task', ?, ?, ?, ?, NULL, 'pending')`)
150
+ .run(formatSqliteDatetime(new Date(scheduledAtMs)), label, input.brief, JSON.stringify(scheduleContext), correlationId);
151
+ const scheduleRowId = Number(insertResult.lastInsertRowid);
152
+ logger.info({ taskId: id, scheduleRowId, scheduledAt: input.scheduleAt }, "background-task scheduled — fire-time row creation deferred to dispatcher");
153
+ return c.json({ taskId: id, status: "scheduled", scheduledFor: scheduledAtMs, scheduleRowId }, 202);
154
+ }
155
+ // ── Brief-dedup (§10.3 / Phase 4) ───────────────────────────────
156
+ // A replayed trigger (the RESEARCH_CLUSTER_COST_FIX_PLAN runaway
157
+ // class) or an over-eager agent POSTing the same brief repeatedly
158
+ // within minutes would otherwise spawn N detached workers. Collapse
159
+ // onto the first still-relevant identical task. `0` disables.
160
+ const dedupWindowMinutes = deps.config?.backgroundTaskDedupWindowMinutes ?? 0;
161
+ if (dedupWindowMinutes > 0) {
162
+ const existing = findRecentDuplicateBackgroundTask(deps.db, {
163
+ brief: input.brief,
164
+ tier: input.tier ?? null,
165
+ sinceMs: Date.now() - dedupWindowMinutes * 60_000,
166
+ });
167
+ if (existing) {
168
+ logger.info({
169
+ taskId: existing.id,
170
+ state: existing.state,
171
+ windowMinutes: dedupWindowMinutes,
172
+ }, "background-task dedup — identical brief within window; returning existing task instead of spawning a duplicate");
173
+ return c.json({
174
+ taskId: existing.id,
175
+ status: existing.state,
176
+ deduplicated: true,
177
+ row: toWire(existing),
178
+ }, 202);
179
+ }
180
+ }
181
+ const createdAt = Date.now();
182
+ const row = createBackgroundTask(deps.db, {
183
+ id,
184
+ brief: input.brief,
185
+ title,
186
+ notificationPolicy: input.notificationPolicy,
187
+ significanceCriteria: input.significanceCriteria ?? null,
188
+ originatingChannel: resolvedChannel,
189
+ correlationId: input.correlationId ?? null,
190
+ scheduleRowId: null,
191
+ tier: input.tier ?? null,
192
+ maxBudgetUsd: input.maxBudgetUsd ?? null,
193
+ createdAt,
194
+ });
195
+ transitionEmitter.emitFromRow(row, createdAt);
196
+ if (deps.backgroundTaskRunner) {
197
+ void deps.backgroundTaskRunner.runFromPost(id).catch((err) => {
198
+ logger.error({ err, taskId: id }, "background-task runner threw on dispatch — task left in pending state");
199
+ });
200
+ }
201
+ else {
202
+ // No runner wired (tests / lite installs) — synthetic terminal so
203
+ // the row doesn't hang in pending.
204
+ const finishedAt = Date.now();
205
+ const terminal = markTerminal(deps.db, {
206
+ id,
207
+ state: "failed",
208
+ outcomeDetail: "runner_unavailable",
209
+ finishedAt,
210
+ });
211
+ transitionEmitter.emitFromRow(terminal, finishedAt);
212
+ }
213
+ const postDispatchRow = getBackgroundTask(deps.db, id) ?? row;
214
+ return c.json({ taskId: id, status: postDispatchRow.state, row: toWire(postDispatchRow) }, 202);
215
+ });
216
+ // ── GET /api/background-task ──────────────────────────────────────────
217
+ app.get("/background-task", (c) => {
218
+ const stateQuery = c.req.query("state");
219
+ const states = stateQuery
220
+ ? stateQuery.split(",").filter((s) => s.length > 0)
221
+ : undefined;
222
+ // §10.5 — the filed-results digest / "did that monitor run?" pull:
223
+ // `notify=false` + `sinceHours=N` narrows to recently-filed results.
224
+ const notifyQuery = c.req.query("notify");
225
+ const notify = notifyQuery === "true" || notifyQuery === "1"
226
+ ? true
227
+ : notifyQuery === "false" || notifyQuery === "0"
228
+ ? false
229
+ : undefined;
230
+ const sinceHours = Number(c.req.query("sinceHours"));
231
+ const finishedSinceMs = Number.isFinite(sinceHours) && sinceHours > 0
232
+ ? Date.now() - sinceHours * 3_600_000
233
+ : undefined;
234
+ const limit = Math.min(200, Math.max(0, Number(c.req.query("limit")) || 50));
235
+ const offset = Math.max(0, Number(c.req.query("offset")) || 0);
236
+ const filter = { states, notify, finishedSinceMs };
237
+ const rows = listBackgroundTasks(deps.db, { ...filter, limit, offset });
238
+ const total = countBackgroundTasks(deps.db, filter);
239
+ return c.json({ tasks: rows.map(toWire), total, limit, offset });
240
+ });
241
+ // ── GET /api/background-task/:id (the artifact) ───────────────────────
242
+ app.get("/background-task/:id", (c) => {
243
+ const id = c.req.param("id");
244
+ const row = getBackgroundTask(deps.db, id);
245
+ if (!row)
246
+ return c.json({ error: "not_found" }, 404);
247
+ return c.json({
248
+ ...toWire(row),
249
+ clarifications: listClarificationsForTask(deps.db, id),
250
+ });
251
+ });
252
+ // ── POST /api/background-task/:id/clarify ─────────────────────────────
253
+ app.post("/background-task/:id/clarify", async (c) => {
254
+ const id = c.req.param("id");
255
+ const body = await readJsonBody(c);
256
+ if (!body.ok)
257
+ return body.response;
258
+ const parsed = clarifyBodySchema.safeParse(body.body);
259
+ if (!parsed.success) {
260
+ return c.json({ error: "validation_error", details: parsed.error.flatten() }, 400);
261
+ }
262
+ const row = getBackgroundTask(deps.db, id);
263
+ if (!row)
264
+ return c.json({ error: "not_found" }, 404);
265
+ if (row.state !== "awaiting_user") {
266
+ return c.json({ error: "not_awaiting_user", currentState: row.state }, 409);
267
+ }
268
+ // Resolve the explicit clarificationId, or the single open one.
269
+ const clarificationId = parsed.data.clarificationId
270
+ ?? getOpenClarificationForTask(deps.db, id)?.id
271
+ ?? null;
272
+ if (!clarificationId) {
273
+ return c.json({ error: "no_open_clarification" }, 409);
274
+ }
275
+ const resolved = resolveClarification(deps.db, {
276
+ id: clarificationId,
277
+ answer: parsed.data.answer,
278
+ answeredAt: Date.now(),
279
+ });
280
+ if (!resolved.ok) {
281
+ const status = resolved.reason === "not_found"
282
+ ? 404
283
+ : resolved.reason === "expired"
284
+ ? 410
285
+ : 409;
286
+ return c.json({ error: resolved.reason ?? "clarify_failed" }, status);
287
+ }
288
+ if (deps.backgroundTaskRunner) {
289
+ void deps.backgroundTaskRunner
290
+ .resumeAfterClarification({
291
+ taskId: id,
292
+ clarificationId,
293
+ answer: parsed.data.answer,
294
+ })
295
+ .catch((err) => {
296
+ logger.error({ err, taskId: id, clarificationId }, "background-task resumeAfterClarification threw — task left parked");
297
+ });
298
+ }
299
+ else {
300
+ logger.warn({ taskId: id, clarificationId }, "background-task clarify recorded but no runner wired — task cannot resume.");
301
+ }
302
+ return c.json({ ok: true, clarification: resolved.row });
303
+ });
304
+ // ── POST /api/background-task/:id/cancel ──────────────────────────────
305
+ app.post("/background-task/:id/cancel", async (c) => {
306
+ const id = c.req.param("id");
307
+ const body = await readJsonBody(c);
308
+ const rawBody = body.ok ? body.body : undefined;
309
+ const parsed = cancelBodySchema.safeParse(rawBody);
310
+ const reason = parsed.success ? parsed.data?.reason ?? "user_cancel" : "user_cancel";
311
+ const row = getBackgroundTask(deps.db, id);
312
+ if (!row)
313
+ return c.json({ error: "not_found" }, 404);
314
+ if (row.state === "completed"
315
+ || row.state === "failed"
316
+ || row.state === "timeout"
317
+ || row.state === "cancelled") {
318
+ return c.json({ error: "already_terminal", currentState: row.state }, 409);
319
+ }
320
+ if (deps.backgroundTaskRunner) {
321
+ await deps.backgroundTaskRunner.cancel(id, reason);
322
+ }
323
+ else {
324
+ const finishedAt = Date.now();
325
+ const updated = markTerminal(deps.db, {
326
+ id,
327
+ state: "cancelled",
328
+ outcomeDetail: reason,
329
+ finishedAt,
330
+ });
331
+ transitionEmitter.emitFromRow(updated, finishedAt);
332
+ return c.json({ ok: true, row: updated ? toWire(updated) : null });
333
+ }
334
+ const after = getBackgroundTask(deps.db, id);
335
+ return c.json({ ok: true, row: after ? toWire(after) : null });
336
+ });
337
+ return app;
338
+ }
@@ -63,10 +63,18 @@ export function createBrowserHistoryRoutes(deps) {
63
63
  return c.json({ error: "not_found" }, 404);
64
64
  }
65
65
  const days = listClusterDailyDeltas(deps.db, detail.rootTaskId, agentDayBoundary(deps), { dayLimit: 31 });
66
+ // RESEARCH_CLUSTER_COST_FIX_PLAN rev5 — stamp each bucket with
67
+ // whether its agent day has ended. The current agent day's bucket is
68
+ // still accumulating (a day-boundary or wake-catch-up run fires
69
+ // mid-day, so the bucket only covers visits up to "now"); consumers
70
+ // that persist day entries (the append-only research journal) must
71
+ // skip incomplete buckets or they freeze an undercounted day forever.
72
+ // ISO agent-day strings compare lexically = chronologically.
73
+ const today = todayKey(deps);
66
74
  return c.json(browserHistoryClusterDeltaResponseSchema.parse({
67
75
  slug,
68
76
  generatedAt: new Date().toISOString(),
69
- days,
77
+ days: days.map((day) => ({ ...day, complete: day.date < today })),
70
78
  }));
71
79
  });
72
80
  app.get("/browser-history/research-clusters/:slug", (c) => {
@@ -215,7 +215,12 @@ export function createCalendarRoutes(deps) {
215
215
  if (date === "today") {
216
216
  date = localDateStr(new Date());
217
217
  }
218
- const days = Math.min(Number(c.req.query("days") ?? "1"), 90);
218
+ // Guard non-finite `days` (e.g. `?days=abc` NaN) — without it the
219
+ // NaN propagates into `new Date(startMs + NaN).toISOString()`, which
220
+ // throws RangeError and surfaces as an opaque 500 instead of a sane
221
+ // bounded window. Clamp finite values to [1, 90]; fall back to 1.
222
+ const daysRaw = Number(c.req.query("days") ?? "1");
223
+ const days = Number.isFinite(daysRaw) ? Math.min(Math.max(daysRaw, 1), 90) : 1;
219
224
  // Append Z to ensure UTC parsing regardless of OS timezone
220
225
  const startMs = new Date(`${date}T00:00:00Z`).getTime();
221
226
  if (Number.isNaN(startMs)) {
@@ -594,7 +599,12 @@ export function createCalendarRoutes(deps) {
594
599
  if (date === "today") {
595
600
  date = localDateStr(new Date());
596
601
  }
597
- const days = Math.min(Number(c.req.query("days") ?? "1"), 90);
602
+ // Guard non-finite `days` (e.g. `?days=abc` NaN) — without it the
603
+ // NaN propagates into `new Date(startMs + NaN).toISOString()`, which
604
+ // throws RangeError and surfaces as an opaque 500 instead of a sane
605
+ // bounded window. Clamp finite values to [1, 90]; fall back to 1.
606
+ const daysRaw = Number(c.req.query("days") ?? "1");
607
+ const days = Number.isFinite(daysRaw) ? Math.min(Math.max(daysRaw, 1), 90) : 1;
598
608
  const startMs = new Date(`${date}T00:00:00Z`).getTime();
599
609
  if (Number.isNaN(startMs)) {
600
610
  return respondWithAgentError(c, 400, [
@@ -50,8 +50,13 @@ export function normalizeContextPath(userPath) {
50
50
  return resolveContextTarget(userPath).base;
51
51
  }
52
52
  export function isDeniedPath(relativePath) {
53
+ // path.relative emits OS-native separators (backslash on Windows); the
54
+ // DENIED_SUBPATH_ROOTS prefixes are forward-slash. Normalize before the
55
+ // compare so nested `.git\config`, `.obsidian\workspace.json`, etc. are
56
+ // caught on Windows. POSIX paths contain no backslashes -> no-op there.
57
+ const normalized = relativePath.replace(/\\/g, "/");
53
58
  for (const root of DENIED_SUBPATH_ROOTS) {
54
- if (relativePath === root || relativePath.startsWith(`${root}/`)) {
59
+ if (normalized === root || normalized.startsWith(`${root}/`)) {
55
60
  return true;
56
61
  }
57
62
  }
@@ -34,6 +34,15 @@ export const CONTEXT_WRITE_PERMISSIONS = {
34
34
  // the captured history (origin DM, why, linked routine) survives.
35
35
  "policies/_index": ["PUT", "PATCH"],
36
36
  "policies/management": ["PUT", "PATCH"],
37
+ // Feedback Learning Loop (FEEDBACK_LEARNING_LOOP_DESIGN.md §4 Phase 2).
38
+ // Global agent-scope lessons folded nightly by routine.evening_review.
39
+ // Without this row the consolidation PATCH/PUT 403s — the per-agent
40
+ // scope already rides the `policies/agents/{slug}/{file}` wildcard, but
41
+ // the global file matches no rule (no blanket `policies/*`). Like every
42
+ // other `policies/` write it trips `shouldRefreshPromptContext`, so a
43
+ // nightly lesson write invalidates the owner-session prompt cache —
44
+ // desirable (new lessons take effect next turn) and it fires at night.
45
+ "policies/agent-lessons": ["PUT", "PATCH"],
37
46
  "policies/mcp": ["PUT", "PATCH"],
38
47
  "policies/redaction": ["PUT", "PATCH"],
39
48
  "policies/journal-format": ["PUT", "PATCH"],
@@ -43,8 +52,9 @@ export const CONTEXT_WRITE_PERMISSIONS = {
43
52
  "policies/management-captures/*": ["PUT", "PATCH"],
44
53
  "policies/routines/_index": ["PUT", "PATCH"],
45
54
  "policies/routines/*": ["PUT", "PATCH"],
46
- // Custom routines support DELETE so the agent can retire a routine
47
- // when the user asks via DM.
55
+ // Legacy custom-routine files (inert since the Agents-hub redesign
56
+ // recurring work is an Agent now). Writes stay validated and DELETE
57
+ // remains so the agent can clean a leftover file up when the user asks.
48
58
  "policies/routines/custom/*": ["PUT", "PATCH", "DELETE"],
49
59
  // User Agent definitions (AGENT_DEFINITIONS_DESIGN.md §9.5 / §3.3). The
50
60
  // dashboard's "+ New Agent" scaffold and the YAML editor write user Agents
@@ -186,9 +186,6 @@ export function registerSnapshotsRoutes(app, ctx) {
186
186
  if (shouldRefreshPromptContext(path, "PUT")) {
187
187
  notifyPromptContextChanged(deps, path, `context_restore_snapshot:${path}`, { path, method: "RESTORE" });
188
188
  }
189
- if (path.startsWith("policies/routines/custom/")) {
190
- deps.onCustomRoutinesChanged?.();
191
- }
192
189
  const writtenStat = statSync(fullPath);
193
190
  logger.info({
194
191
  path,
@@ -320,9 +320,6 @@ export function registerWriteRoutes(app, ctx) {
320
320
  if (shouldRefreshPromptContext(path, "PUT")) {
321
321
  notifyPromptContextChanged(deps, path, `context_put:${path}`, { path, method: "PUT" });
322
322
  }
323
- if (path.startsWith("policies/routines/custom/")) {
324
- deps.onCustomRoutinesChanged?.();
325
- }
326
323
  const writtenStat = statSync(fullPath);
327
324
  logger.info({
328
325
  path,
@@ -530,9 +527,6 @@ export function registerWriteRoutes(app, ctx) {
530
527
  previousContent: fileContent,
531
528
  });
532
529
  }
533
- if (path.startsWith("policies/routines/custom/")) {
534
- deps.onCustomRoutinesChanged?.();
535
- }
536
530
  logger.info({ path, method: "PATCH", mode }, "Content appended to file");
537
531
  return c.json({ status: "appended" });
538
532
  }
@@ -608,9 +602,6 @@ export function registerWriteRoutes(app, ctx) {
608
602
  previousContent: fileContent,
609
603
  });
610
604
  }
611
- if (path.startsWith("policies/routines/custom/")) {
612
- deps.onCustomRoutinesChanged?.();
613
- }
614
605
  logger.info({ path, method: "PATCH", mode }, "Frontmatter merged");
615
606
  return c.json({ status: "merged" });
616
607
  }
@@ -724,9 +715,6 @@ export function registerWriteRoutes(app, ctx) {
724
715
  previousContent: fileContent,
725
716
  });
726
717
  }
727
- if (path.startsWith("policies/routines/custom/")) {
728
- deps.onCustomRoutinesChanged?.();
729
- }
730
718
  deps.onIndexableContextChange?.(`${path}${target.ext}`);
731
719
  const resultStatus = mode === "append" ? "appended" : mode === "replace" ? "replaced" : "cleared";
732
720
  logger.info({ path, method: "PATCH", section, mode, removedCount, trimmedCount }, "Context section " + resultStatus);
@@ -735,8 +723,9 @@ export function registerWriteRoutes(app, ctx) {
735
723
  });
736
724
  // DELETE /context/* — File delete (currently limited to `routines/custom/*`
737
725
  // via the write-permission whitelist). B-007 §5.8 Q3: the agent retires a
738
- // custom routine after the user confirms. The scheduler listens via
739
- // `onCustomRoutinesChanged` and unregisters the cron job on reload.
726
+ // custom routine after the user confirms. (Legacy path custom routines
727
+ // no longer fire; they were converted to user Agents at the Agents-hub
728
+ // redesign. The delete surface stays so leftover files can be removed.)
740
729
  app.delete("/context/*", (c) => {
741
730
  const path = normalizeContextPath(c.req.path.replace("/api/context/", ""));
742
731
  const contextDir = getCurrentContextDir();
@@ -770,9 +759,6 @@ export function registerWriteRoutes(app, ctx) {
770
759
  const existing = readFileSync(fullPath, "utf-8");
771
760
  const snapshotId = saveSnapshot(path, existing, "api_delete", true);
772
761
  unlinkSync(fullPath);
773
- if (path.startsWith("policies/routines/custom/")) {
774
- deps.onCustomRoutinesChanged?.();
775
- }
776
762
  deps.onIndexableContextChange?.(path);
777
763
  logger.info({ path, method: "DELETE", snapshotId: snapshotId ?? undefined }, "Context file deleted");
778
764
  return c.json({ status: "deleted", snapshotId: snapshotId ?? 0 });
@@ -4,6 +4,7 @@ import { EDITABLE_RUNTIME_KEY_TUPLE, getBackendIds, normalizeAgentDisplayName, }
4
4
  import { applyConfigUpdates } from "../../env-writer.js";
5
5
  import { runDefaultSchedulesReconciler } from "../../../core/context/default-schedules-runner.js";
6
6
  import { syncDmSessionTimesToQuietHours } from "../../../core/quiet-hours-sync.js";
7
+ import { retimeDeferredDmRows, retimeDeferredRunRows, } from "../../../db/deferred-dm.js";
7
8
  import { getContextDir } from "../../../config.js";
8
9
  import { CONTEXT_RELATIVE_PATHS } from "../../../core/context-paths.js";
9
10
  import { createLogger, toSafeErrorMessage } from "../../../logging.js";
@@ -31,13 +32,13 @@ const PUBLIC_CONFIG_RUNTIME_KEYS = [
31
32
  "sessionTimeoutDashboardMinutes",
32
33
  "timezone",
33
34
  "dayBoundaryHour",
34
- // Monthly Review kill switch default off; see runtime-settings.ts.
35
- "monthlyReviewEnabled",
36
- "hourlyCheckEnabled",
37
- "hourlyCheckIntervalMinutes",
38
- "hourlyCheckActiveStartHour",
39
- "hourlyCheckActiveEndHour",
40
- "hourlyCheckMinObservations",
35
+ // The legacy activity-scan / monthly-review gate + cadence keys
36
+ // (activityScanEnabled, activityScanIntervalMinutes, activityScanActive*,
37
+ // activityScanMinObservations, monthlyReviewEnabled) left this surface at
38
+ // the Agents-hub redesign: `agents.enabled` + the activity-scan row's
39
+ // runtime_window own them now (PATCH /api/agents/activity-scan). The keys
40
+ // remain valid in runtimeSettingsSchema as resolver fallbacks
41
+ // (AGENTS_HUB_REDESIGN_PLAN.md §2).
41
42
  "authProbeDisabled",
42
43
  "authPreflightFreshnessMs",
43
44
  "maxNotificationsPerHour",
@@ -94,6 +95,16 @@ const PUBLIC_CONFIG_RUNTIME_KEYS = [
94
95
  "delegatedProbeIntervalMinutes",
95
96
  "autonomousDailyCostCapUsd",
96
97
  "autonomousMonthlyCostCapUsd",
98
+ // Feedback Learning Loop (FEEDBACK_LEARNING_LOOP_DESIGN.md §9 Phase 5) —
99
+ // surfaced so the Lessons settings page can read + tune the master toggle,
100
+ // promotion threshold, per-scope byte caps, staleness horizon, and signal
101
+ // retention via the deferred-save EditableField flow.
102
+ "feedbackLearningEnabled",
103
+ "feedbackPromotionThreshold",
104
+ "feedbackLessonMaxBytesGlobal",
105
+ "feedbackLessonMaxBytesPerAgent",
106
+ "feedbackLessonStaleDays",
107
+ "feedbackSignalRetentionDays",
97
108
  "primaryLanguage",
98
109
  "vaultMode",
99
110
  ];
@@ -293,14 +304,18 @@ export function registerConfigRoutes(app, deps) {
293
304
  if (Object.keys(result.errors).length > 0 && result.updated.length === 0) {
294
305
  return c.json({ error: "validation_failed", details: result.errors }, 400);
295
306
  }
296
- // Hot-reload cron schedules when schedule-related config changes
307
+ // Hot-reload cron schedules when schedule-related config changes.
308
+ // The activityScan* entries are legacy-API back-compat only: the dashboard
309
+ // no longer surfaces them (agent-row runtime_window owns the cadence), but
310
+ // a direct PATCH of the deprecated keys must still rebuild the cron so the
311
+ // resolver fallback change takes effect.
297
312
  const SCHEDULE_KEYS = [
298
313
  "dayBoundaryHour",
299
314
  "timezone",
300
- "hourlyCheckEnabled",
301
- "hourlyCheckIntervalMinutes",
302
- "hourlyCheckActiveStartHour",
303
- "hourlyCheckActiveEndHour",
315
+ "activityScanEnabled",
316
+ "activityScanIntervalMinutes",
317
+ "activityScanActiveStartHour",
318
+ "activityScanActiveEndHour",
304
319
  ];
305
320
  if (result.updated.some((k) => SCHEDULE_KEYS.includes(k))) {
306
321
  deps.onScheduleConfigChanged?.();
@@ -332,6 +347,37 @@ export function registerConfigRoutes(app, deps) {
332
347
  logger.warn({ err }, "syncDmSessionTimesToQuietHours threw after dashboard config PATCH");
333
348
  }
334
349
  }
350
+ // QUIET_HOURS_HARDENING_PLAN.md Phase 1 follow-up — pending
351
+ // quiet-hours-deferred DM rows (`task_context.deferred_from`) were
352
+ // stamped with the *old* window's end; retime them so a widened
353
+ // window doesn't fire them inside the new quiet hours and a
354
+ // narrowed one doesn't hold them past the new edge. Phase 2's
355
+ // deferred RUN rows (`task_context.quiet_hours_deferred` — agent.task
356
+ // opt-in + browser_task) get the same treatment; for them only the
357
+ // narrowed/disabled direction matters (a widened window re-defers at
358
+ // claim time anyway). `timezone` is in the trigger set because the
359
+ // window's *absolute* position is tz-relative — an unchanged
360
+ // "22:00→08:00" still moves on the UTC axis when the tz changes, and
361
+ // deferred DM rows are delivered by `handleDirectDm`, which skips the
362
+ // quiet-hours check by design.
363
+ if ((result.updated.includes("quietHoursEnd")
364
+ || result.updated.includes("quietHoursStart")
365
+ || result.updated.includes("timezone"))
366
+ && /^\d{2}:\d{2}$/.test(config.quietHoursStart)
367
+ && /^\d{2}:\d{2}$/.test(config.quietHoursEnd)) {
368
+ const window = {
369
+ start: config.quietHoursStart,
370
+ end: config.quietHoursEnd,
371
+ timezone: config.timezone || undefined,
372
+ };
373
+ try {
374
+ retimeDeferredDmRows(db, window);
375
+ retimeDeferredRunRows(db, window);
376
+ }
377
+ catch (err) {
378
+ logger.warn({ err }, "Retiming quiet-hours-deferred rows threw after dashboard config PATCH");
379
+ }
380
+ }
335
381
  // Hot-refresh DM session skill dirs when enabledMailProviders changes
336
382
  // through the generic config PATCH. The per-endpoint `PATCH /mail/providers`
337
383
  // handler already drives this through MailAccountRegistry.onScopeChanged,