@aitne/daemon 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -37,6 +37,15 @@ export class TelegramAdapter {
37
37
  attachmentStore;
38
38
  bot = null;
39
39
  botInfo = null;
40
+ /**
41
+ * Non-null when the long-poll loop has died. telegraf's `launch()`
42
+ * promise stays pending while polling runs and settles when the loop
43
+ * exits: it self-retries network errors / 429 / 5xx internally, but any
44
+ * other failure (401 invalid token, 409 conflicting consumer, an
45
+ * unexpected throw) kills the loop for good. Without observing that
46
+ * settlement the daemon would sit "connected" while receiving nothing.
47
+ */
48
+ pollingDeadError = null;
40
49
  /** Active pairing challenge (null when pairing isn't in progress). */
41
50
  pairingChallenge = null;
42
51
  /** Buffers for Telegram media albums (media_group_id → accumulated items). */
@@ -134,8 +143,26 @@ export class TelegramAdapter {
134
143
  logger.error({ err }, "telegram message handler threw");
135
144
  });
136
145
  });
137
- // Start long polling (non-blocking)
138
- this.bot.launch({ dropPendingUpdates: true });
146
+ // Start long polling (non-blocking). The launch() promise settles only
147
+ // when the poll loop exits — track that settlement so a dead loop is
148
+ // observable instead of becoming an unhandled rejection with the
149
+ // adapter silently deaf until daemon restart.
150
+ this.pollingDeadError = null;
151
+ const bot = this.bot;
152
+ bot.launch({ dropPendingUpdates: true }).then(() => {
153
+ // Resolves on stop() (AbortError path). If we did NOT initiate the
154
+ // stop, the loop ended on its own — mark the adapter down.
155
+ if (this.bot === bot) {
156
+ this.pollingDeadError = "Telegram polling loop exited unexpectedly";
157
+ logger.error("Telegram polling loop exited unexpectedly");
158
+ }
159
+ }, (err) => {
160
+ if (this.bot === bot) {
161
+ this.pollingDeadError =
162
+ err instanceof Error ? err.message : String(err);
163
+ logger.error({ err }, "Telegram polling loop died");
164
+ }
165
+ });
139
166
  logger.info({ botUsername: this.botInfo?.username }, "Telegram adapter connected (Long Polling)");
140
167
  }
141
168
  async stop() {
@@ -153,6 +180,18 @@ export class TelegramAdapter {
153
180
  async resolveUserChannel() {
154
181
  return this.mutableOwnerId;
155
182
  }
183
+ /**
184
+ * Live long-poll liveness for the adapter watchdog. "ok" means the
185
+ * polling loop is still running (telegraf retries transient network /
186
+ * 429 / 5xx failures internally, so an offline laptop stays "ok" and
187
+ * recovers on its own); "down" means the loop has exited and only a
188
+ * full stop→start cycle brings messages back.
189
+ */
190
+ getConnectionState() {
191
+ if (!this.bot)
192
+ return "unknown";
193
+ return this.pollingDeadError === null ? "ok" : "down";
194
+ }
156
195
  async sendMessage(params) {
157
196
  if (!this.bot) {
158
197
  throw new Error("Telegram bot not started");
@@ -24,6 +24,20 @@ export interface NotificationRuntimeStatus {
24
24
  runtimeState: "ok" | "error" | "connecting";
25
25
  error: string | null;
26
26
  }
27
+ /**
28
+ * Live transport-level connection state, read by the adapter watchdog
29
+ * (`adapter-watchdog.ts`).
30
+ *
31
+ * - `"ok"` — the underlying socket / poll loop is alive right now.
32
+ * - `"down"` — the library reports the connection dead (socket closed,
33
+ * poll loop exited, gateway session invalidated). The
34
+ * watchdog restarts the adapter after consecutive `"down"`
35
+ * observations.
36
+ * - `"unknown"` — liveness cannot be determined (adapter not started, or
37
+ * the library's internals are not introspectable). The
38
+ * watchdog takes no action on `"unknown"`.
39
+ */
40
+ export type AdapterConnectionState = "ok" | "down" | "unknown";
27
41
  /**
28
42
  * MessageAdapter — platform-specific messaging integration.
29
43
  *
@@ -50,6 +64,12 @@ export interface MessageAdapter {
50
64
  * generic registered/error state tracked by MessageHub.
51
65
  */
52
66
  getNotificationRuntimeStatus?(): NotificationRuntimeStatus;
67
+ /**
68
+ * Cheap synchronous probe of the live transport state, for the adapter
69
+ * watchdog. Adapters that cannot introspect their library (or manage
70
+ * their own reconnection, like WhatsApp) omit this.
71
+ */
72
+ getConnectionState?(): AdapterConnectionState;
53
73
  /** Start receiving messages (connects to platform) */
54
74
  start(): Promise<void>;
55
75
  /** Graceful shutdown */
@@ -110,9 +110,17 @@ export declare class WhatsAppAdapter implements MessageAdapter {
110
110
  * watch's async connectivity probe) is in flight, and we have NOT
111
111
  * permanently given up. Lets the status accessors report "connecting"
112
112
  * during the brief timer-less window of a sustained probe instead of
113
- * flashing a red error. Cleared on open / give-up / logout / stop.
113
+ * flashing a red error. Cleared on open / logout / stop.
114
114
  */
115
115
  private reconnecting;
116
+ /**
117
+ * True while the slow app-layer watch is armed (fast phase exhausted on a
118
+ * non-network close). Unlike the network-class sustained watch, this state
119
+ * IS surfaced as an error by {@link getStatusError} — WhatsApp is actively
120
+ * rejecting us and the user may want to act (re-pair, check the dashboard)
121
+ * rather than wait out the ~30-min retry cadence.
122
+ */
123
+ private appLayerWatch;
116
124
  /**
117
125
  * Connectivity probe used by the sustained watch. Injectable so tests can
118
126
  * drive the offline / back-online transitions without real DNS; production
@@ -141,9 +149,10 @@ export declare class WhatsAppAdapter implements MessageAdapter {
141
149
  * - `logged_out` → terminal, surface the error.
142
150
  * - `disconnected` → recovery is in flight when a reconnect timer is
143
151
  * pending OR a sustained-watch attempt is mid-probe ({@link reconnecting}
144
- * covers the brief timer-less DNS window). Only when neither holds have
145
- * we permanently given up (app-layer rejection past the fast cap) a
146
- * real, user-actionable failure.
152
+ * covers the brief timer-less DNS window) EXCEPT the slow app-layer
153
+ * watch ({@link appLayerWatch}), which is surfaced even though a timer
154
+ * is pending: WhatsApp is actively rejecting us and the ~30-min retry
155
+ * cadence is slow enough that the user may want to act first.
147
156
  */
148
157
  getStatusError(): string | null;
149
158
  getNotificationRuntimeStatus(): NotificationRuntimeStatus;
@@ -233,9 +242,13 @@ export declare class WhatsAppAdapter implements MessageAdapter {
233
242
  * on its own instead of wedging in an error state.
234
243
  *
235
244
  * If the fast phase exhausts on a *non*-network close (WhatsApp rejecting us
236
- * at the app layer — bad version, bad session, throttle) we stop and surface
237
- * an error: hammering a relay that keeps closing us is the fast path to an
238
- * IP-level block and needs operator action (re-pair / wait it out).
245
+ * at the app layer — bad version, bad session, throttle) we drop into the
246
+ * slow **app-layer watch**: same timer machinery as the sustained watch but
247
+ * on a much longer cadence ({@link RECONNECT_APP_LAYER_WATCH_DELAY_MS}) and
248
+ * with the error surfaced to the dashboard. Hammering a relay that keeps
249
+ * closing us is the fast path to an IP-level block, but a permanent stop
250
+ * would wedge the adapter until daemon restart over conditions that
251
+ * routinely clear on their own (throttles, server restarts, version churn).
239
252
  */
240
253
  private scheduleReconnect;
241
254
  /**
@@ -252,6 +265,12 @@ export declare class WhatsAppAdapter implements MessageAdapter {
252
265
  * `exhausted && !network` in {@link scheduleReconnect} and falsely give up.
253
266
  * A fresh fast phase gives those handshake closes their proper retry budget
254
267
  * (a clean `open` doesn't fire before a 515, so we can't rely on it here).
268
+ *
269
+ * The app-layer watch (`appLayerWatch=true`) reuses this path but grants
270
+ * only {@link RECONNECT_APP_LAYER_RETRY_BUDGET} fast attempts instead of a
271
+ * full fresh phase: the network never went away, so a still-broken app
272
+ * layer should fall back into the slow watch after a short burst rather
273
+ * than re-earning ten rapid connects every watch tick.
255
274
  */
256
275
  private runReconnectAttempt;
257
276
  /**
@@ -39,14 +39,28 @@ const WA_VERSION_TTL_MS = 12 * 60 * 60 * 1000;
39
39
  * networks) drops into the sustained connectivity watch below, which never
40
40
  * gives up and recovers on its own once the network returns;
41
41
  * - any other close (WhatsApp rejected us at the app layer — bad version,
42
- * bad session, throttle) surfaces an error and stops, because hammering a
43
- * relay that keeps closing us is the fast path to an IP-level ban.
42
+ * bad session, throttle) surfaces an error and drops into the slow
43
+ * app-layer watch below — hammering a relay that keeps closing us is the
44
+ * fast path to an IP-level ban, but stopping forever turns a transient
45
+ * server-side condition into a wedge only a daemon restart clears.
44
46
  */
45
47
  const RECONNECT_INITIAL_DELAY_MS = 1_000;
46
48
  const RECONNECT_MAX_DELAY_MS = 60_000;
47
49
  const RECONNECT_BACKOFF_FACTOR = 2;
48
50
  const RECONNECT_JITTER_MS = 500;
49
51
  const RECONNECT_MAX_ATTEMPTS = 10;
52
+ /**
53
+ * App-layer watch tunables. Once the fast phase exhausts on a *non*-network
54
+ * close (WhatsApp rejecting us at the app layer), we retry on this long
55
+ * cadence instead of giving up: throttles lift, server-side restarts settle,
56
+ * and version rejections clear after the cache refetch. Each watch tick
57
+ * grants only {@link RECONNECT_APP_LAYER_RETRY_BUDGET} fast attempts (enough
58
+ * for the 515 → version-refetch → connect handshake chain) rather than a full
59
+ * fresh fast phase, keeping the worst-case connect rate low enough to pose no
60
+ * ban risk while still recovering unattended.
61
+ */
62
+ const RECONNECT_APP_LAYER_WATCH_DELAY_MS = 30 * 60_000;
63
+ const RECONNECT_APP_LAYER_RETRY_BUDGET = 3;
50
64
  /**
51
65
  * Sustained-watch tunables. Once the fast phase exhausts on a *network-class*
52
66
  * close we keep watching for connectivity indefinitely on this long, fixed
@@ -282,9 +296,17 @@ export class WhatsAppAdapter {
282
296
  * watch's async connectivity probe) is in flight, and we have NOT
283
297
  * permanently given up. Lets the status accessors report "connecting"
284
298
  * during the brief timer-less window of a sustained probe instead of
285
- * flashing a red error. Cleared on open / give-up / logout / stop.
299
+ * flashing a red error. Cleared on open / logout / stop.
286
300
  */
287
301
  reconnecting = false;
302
+ /**
303
+ * True while the slow app-layer watch is armed (fast phase exhausted on a
304
+ * non-network close). Unlike the network-class sustained watch, this state
305
+ * IS surfaced as an error by {@link getStatusError} — WhatsApp is actively
306
+ * rejecting us and the user may want to act (re-pair, check the dashboard)
307
+ * rather than wait out the ~30-min retry cadence.
308
+ */
309
+ appLayerWatch = false;
288
310
  /**
289
311
  * Connectivity probe used by the sustained watch. Injectable so tests can
290
312
  * drive the offline / back-online transitions without real DNS; production
@@ -323,9 +345,10 @@ export class WhatsAppAdapter {
323
345
  * - `logged_out` → terminal, surface the error.
324
346
  * - `disconnected` → recovery is in flight when a reconnect timer is
325
347
  * pending OR a sustained-watch attempt is mid-probe ({@link reconnecting}
326
- * covers the brief timer-less DNS window). Only when neither holds have
327
- * we permanently given up (app-layer rejection past the fast cap) a
328
- * real, user-actionable failure.
348
+ * covers the brief timer-less DNS window) EXCEPT the slow app-layer
349
+ * watch ({@link appLayerWatch}), which is surfaced even though a timer
350
+ * is pending: WhatsApp is actively rejecting us and the ~30-min retry
351
+ * cadence is slow enough that the user may want to act first.
329
352
  */
330
353
  getStatusError() {
331
354
  switch (this.connectionState) {
@@ -337,6 +360,8 @@ export class WhatsAppAdapter {
337
360
  case "logged_out":
338
361
  return this.lastError ?? "WhatsApp logged out";
339
362
  case "disconnected":
363
+ if (this.appLayerWatch)
364
+ return this.lastError ?? "WhatsApp disconnected";
340
365
  if (this.reconnectTimer !== null || this.reconnecting)
341
366
  return null;
342
367
  return this.lastError ?? "WhatsApp disconnected";
@@ -392,6 +417,7 @@ export class WhatsAppAdapter {
392
417
  this.lastError = null;
393
418
  this.reconnectAttempts = 0;
394
419
  this.reconnecting = false;
420
+ this.appLayerWatch = false;
395
421
  this.lastCloseWasNetwork = false;
396
422
  if (this.reconnectTimer) {
397
423
  clearTimeout(this.reconnectTimer);
@@ -484,6 +510,7 @@ export class WhatsAppAdapter {
484
510
  }
485
511
  this.reconnectAttempts = 0;
486
512
  this.reconnecting = false;
513
+ this.appLayerWatch = false;
487
514
  this.lastCloseWasNetwork = false;
488
515
  this.sentMessageIds.clear();
489
516
  this.clearQrSnapshot();
@@ -923,6 +950,7 @@ export class WhatsAppAdapter {
923
950
  this.lastError = null;
924
951
  this.reconnectAttempts = 0;
925
952
  this.reconnecting = false;
953
+ this.appLayerWatch = false;
926
954
  this.lastCloseWasNetwork = false;
927
955
  this.clearQrSnapshot();
928
956
  this.clearQrFile();
@@ -956,6 +984,7 @@ export class WhatsAppAdapter {
956
984
  this.lastError = `WhatsApp logged out (status ${statusCode ?? "unknown"}) — re-pair required`;
957
985
  this.reconnectAttempts = 0;
958
986
  this.reconnecting = false;
987
+ this.appLayerWatch = false;
959
988
  logger.error({ statusCode }, "whatsapp connection closed: logged out");
960
989
  if (this.onLoggedOut) {
961
990
  try {
@@ -971,6 +1000,7 @@ export class WhatsAppAdapter {
971
1000
  this.connectionState = "disabled";
972
1001
  this.reconnectAttempts = 0;
973
1002
  this.reconnecting = false;
1003
+ this.appLayerWatch = false;
974
1004
  return;
975
1005
  }
976
1006
  // Classify the close: a transport/network failure (or a raw socket error
@@ -1013,9 +1043,13 @@ export class WhatsAppAdapter {
1013
1043
  * on its own instead of wedging in an error state.
1014
1044
  *
1015
1045
  * If the fast phase exhausts on a *non*-network close (WhatsApp rejecting us
1016
- * at the app layer — bad version, bad session, throttle) we stop and surface
1017
- * an error: hammering a relay that keeps closing us is the fast path to an
1018
- * IP-level block and needs operator action (re-pair / wait it out).
1046
+ * at the app layer — bad version, bad session, throttle) we drop into the
1047
+ * slow **app-layer watch**: same timer machinery as the sustained watch but
1048
+ * on a much longer cadence ({@link RECONNECT_APP_LAYER_WATCH_DELAY_MS}) and
1049
+ * with the error surfaced to the dashboard. Hammering a relay that keeps
1050
+ * closing us is the fast path to an IP-level block, but a permanent stop
1051
+ * would wedge the adapter until daemon restart over conditions that
1052
+ * routinely clear on their own (throttles, server restarts, version churn).
1019
1053
  */
1020
1054
  scheduleReconnect() {
1021
1055
  if (this.reconnectTimer || this.shuttingDown)
@@ -1023,15 +1057,25 @@ export class WhatsAppAdapter {
1023
1057
  if (this.connectionState === "logged_out")
1024
1058
  return;
1025
1059
  const exhaustedFastPhase = this.reconnectAttempts >= RECONNECT_MAX_ATTEMPTS;
1026
- if (exhaustedFastPhase && !this.lastCloseWasNetwork) {
1027
- const previousError = this.lastError ?? "unknown error";
1028
- this.lastError = `WhatsApp reconnect gave up after ${RECONNECT_MAX_ATTEMPTS} attempts (${previousError})`;
1029
- this.reconnecting = false;
1030
- logger.error({ attempts: this.reconnectAttempts, lastError: previousError }, "whatsapp reconnect: max attempts exceeded");
1031
- return;
1032
- }
1060
+ const appLayerWatch = exhaustedFastPhase && !this.lastCloseWasNetwork;
1033
1061
  let delayMs;
1034
- if (exhaustedFastPhase) {
1062
+ if (appLayerWatch) {
1063
+ // Wrap lastError and log only on ENTRY into the watch. Re-scheduling
1064
+ // from within it (the probe-unreachable path of runReconnectAttempt
1065
+ // calls scheduleReconnect on every watch tick) must not re-wrap —
1066
+ // each pass would nest the previous wrapped message inside the new
1067
+ // one, growing lastError without bound, and would re-emit the
1068
+ // error-level log every ~30 min for one underlying condition.
1069
+ if (!this.appLayerWatch) {
1070
+ const previousError = this.lastError ?? "unknown error";
1071
+ this.lastError = `WhatsApp reconnect: ${RECONNECT_MAX_ATTEMPTS} fast attempts failed at the app layer (${previousError}); retrying every ~${Math.round(RECONNECT_APP_LAYER_WATCH_DELAY_MS / 60_000)} min`;
1072
+ logger.error({ attempts: this.reconnectAttempts, lastError: previousError }, "whatsapp reconnect: fast phase exhausted at app layer; entering slow watch");
1073
+ }
1074
+ delayMs =
1075
+ RECONNECT_APP_LAYER_WATCH_DELAY_MS
1076
+ + Math.floor(Math.random() * RECONNECT_SUSTAINED_JITTER_MS);
1077
+ }
1078
+ else if (exhaustedFastPhase) {
1035
1079
  // Sustained network watch — fixed cadence, counter left pinned at the
1036
1080
  // cap so we stay in this branch without growing reconnectAttempts.
1037
1081
  delayMs =
@@ -1044,14 +1088,15 @@ export class WhatsAppAdapter {
1044
1088
  this.reconnectAttempts += 1;
1045
1089
  }
1046
1090
  this.reconnecting = true;
1047
- logger.info({ attempt: this.reconnectAttempts, delayMs, sustained: exhaustedFastPhase }, "whatsapp reconnect scheduled");
1091
+ this.appLayerWatch = appLayerWatch;
1092
+ logger.info({ attempt: this.reconnectAttempts, delayMs, sustained: exhaustedFastPhase, appLayerWatch }, "whatsapp reconnect scheduled");
1048
1093
  this.reconnectTimer = setTimeout(() => {
1049
1094
  this.reconnectTimer = null;
1050
1095
  if (this.shuttingDown || this.connectionState === "logged_out") {
1051
1096
  this.reconnecting = false;
1052
1097
  return;
1053
1098
  }
1054
- void this.runReconnectAttempt(exhaustedFastPhase);
1099
+ void this.runReconnectAttempt(exhaustedFastPhase, appLayerWatch);
1055
1100
  }, delayMs);
1056
1101
  this.reconnectTimer.unref?.();
1057
1102
  }
@@ -1069,8 +1114,14 @@ export class WhatsAppAdapter {
1069
1114
  * `exhausted && !network` in {@link scheduleReconnect} and falsely give up.
1070
1115
  * A fresh fast phase gives those handshake closes their proper retry budget
1071
1116
  * (a clean `open` doesn't fire before a 515, so we can't rely on it here).
1117
+ *
1118
+ * The app-layer watch (`appLayerWatch=true`) reuses this path but grants
1119
+ * only {@link RECONNECT_APP_LAYER_RETRY_BUDGET} fast attempts instead of a
1120
+ * full fresh phase: the network never went away, so a still-broken app
1121
+ * layer should fall back into the slow watch after a short burst rather
1122
+ * than re-earning ten rapid connects every watch tick.
1072
1123
  */
1073
- async runReconnectAttempt(sustained) {
1124
+ async runReconnectAttempt(sustained, appLayerWatch = false) {
1074
1125
  if (sustained) {
1075
1126
  const reachable = await this.isNetworkReachable();
1076
1127
  // shuttingDown / logged_out may have flipped during the async probe.
@@ -1084,7 +1135,9 @@ export class WhatsAppAdapter {
1084
1135
  return;
1085
1136
  }
1086
1137
  logger.info("whatsapp reconnect: network reachable, reconnecting");
1087
- this.reconnectAttempts = 0;
1138
+ this.reconnectAttempts = appLayerWatch
1139
+ ? RECONNECT_MAX_ATTEMPTS - RECONNECT_APP_LAYER_RETRY_BUDGET
1140
+ : 0;
1088
1141
  }
1089
1142
  try {
1090
1143
  await this.connect();
@@ -67,14 +67,17 @@ function isRestartRequiredKey(key) {
67
67
  const NUMERIC_RANGE = {
68
68
  dayBoundaryHour: { min: 0, max: 9, label: "0–9 (before 10:00)" },
69
69
  executeTimeoutMinutes: { min: 1, max: 1440, label: "1–1440 minutes" },
70
- hourlyCheckActiveStartHour: { min: 0, max: 23, label: "0–23" },
71
- hourlyCheckActiveEndHour: { min: 1, max: 24, label: "1–24" },
70
+ activityScanActiveStartHour: { min: 0, max: 23, label: "0–23" },
71
+ activityScanActiveEndHour: { min: 1, max: 24, label: "1–24" },
72
72
  // Any positive integer up to one day. Intervals that evenly divide 60
73
73
  // produce a tight cron; arbitrary intervals run a minute-tick cron with an
74
74
  // in-callback modulo gate anchored at activeStartHour. See
75
- // scheduler.ts:buildHourlyCronExpr / shouldFireHourlyTickAt.
76
- hourlyCheckIntervalMinutes: { min: 1, max: 1440, label: "1–1440 minutes" },
77
- hourlyCheckMinObservations: { min: 0, max: 1000, label: "0–1000" },
75
+ // scheduler.ts:buildActivityScanCronExpr / shouldFireActivityScanTickAt.
76
+ activityScanIntervalMinutes: { min: 1, max: 1440, label: "1–1440 minutes" },
77
+ activityScanMinObservations: { min: 0, max: 1000, label: "0–1000" },
78
+ // Twin of the runtimeSettingsSchema bound; cap 480 = the self-tuning R1
79
+ // freshness ladder's top notch (SELF_TUNING_REVIEW_CYCLE_DESIGN.md D2).
80
+ activityScanPrePassFreshnessMinutes: { min: 0, max: 480, label: "0–480 minutes" },
78
81
  gitPollIntervalSeconds: { min: 60, max: 86400, label: "60–86400 seconds" },
79
82
  githubPollIntervalSeconds: { min: 60, max: 86400, label: "60–86400 seconds" },
80
83
  // 0 disables the observer; 20160 min = 14 days which is already well past
@@ -1261,7 +1261,7 @@ export declare const AGENT_ERROR_REGISTRY: {
1261
1261
  };
1262
1262
  readonly "observations.invalid_actor": {
1263
1263
  readonly expected: "actor = 'user' | 'agent' | 'system'";
1264
- readonly hint: "The `?actor=` filter accepts only `user`, `agent`, or `system`. `user` is the hourly-check default (excludes agent's own writes). Omit to get all observations.";
1264
+ readonly hint: "The `?actor=` filter accepts only `user`, `agent`, or `system`. `user` is the activity-scan default (excludes agent's own writes). Omit to get all observations.";
1265
1265
  readonly skillAnchor: "observations#actor-filter";
1266
1266
  readonly legacyErrorCode: "invalid_actor";
1267
1267
  readonly constraint: {
@@ -1868,11 +1868,11 @@ export declare const AGENT_ERROR_REGISTRY: {
1868
1868
  readonly legacyErrorCode: "daemon_starting";
1869
1869
  readonly retryable: true;
1870
1870
  };
1871
- readonly "agent.hourly_check_unavailable": {
1872
- readonly expected: "triggerHourlyCheck wired into the API server";
1873
- readonly hint: "POST /agent/run-now/hourly was called before the hourly-check engine was wired (typically only the first ~1s of boot). Wait ~3s and retry.";
1871
+ readonly "agent.activity_scan_unavailable": {
1872
+ readonly expected: "triggerActivityScan wired into the API server";
1873
+ readonly hint: "POST /api/agent/run-now was called before the activity-scan engine was wired (typically only the first ~1s of boot). Wait ~3s and retry.";
1874
1874
  readonly skillAnchor: "agent#run-now";
1875
- readonly legacyErrorCode: "hourly_check_unavailable";
1875
+ readonly legacyErrorCode: "activity_scan_unavailable";
1876
1876
  readonly retryable: true;
1877
1877
  };
1878
1878
  readonly "agent.roadmap_maintenance_unavailable": {
@@ -1194,7 +1194,7 @@ export const AGENT_ERROR_REGISTRY = {
1194
1194
  // ── /api/observations/* — Phase 9 pending/consume.
1195
1195
  "observations.invalid_actor": {
1196
1196
  expected: "actor = 'user' | 'agent' | 'system'",
1197
- hint: "The `?actor=` filter accepts only `user`, `agent`, or `system`. `user` is the hourly-check default (excludes agent's own writes). Omit to get all observations.",
1197
+ hint: "The `?actor=` filter accepts only `user`, `agent`, or `system`. `user` is the activity-scan default (excludes agent's own writes). Omit to get all observations.",
1198
1198
  skillAnchor: "observations#actor-filter",
1199
1199
  legacyErrorCode: "invalid_actor",
1200
1200
  constraint: { type: "enum", enum: ["user", "agent", "system"] },
@@ -1794,11 +1794,11 @@ export const AGENT_ERROR_REGISTRY = {
1794
1794
  legacyErrorCode: "daemon_starting",
1795
1795
  retryable: true,
1796
1796
  },
1797
- "agent.hourly_check_unavailable": {
1798
- expected: "triggerHourlyCheck wired into the API server",
1799
- hint: "POST /agent/run-now/hourly was called before the hourly-check engine was wired (typically only the first ~1s of boot). Wait ~3s and retry.",
1797
+ "agent.activity_scan_unavailable": {
1798
+ expected: "triggerActivityScan wired into the API server",
1799
+ hint: "POST /api/agent/run-now was called before the activity-scan engine was wired (typically only the first ~1s of boot). Wait ~3s and retry.",
1800
1800
  skillAnchor: "agent#run-now",
1801
- legacyErrorCode: "hourly_check_unavailable",
1801
+ legacyErrorCode: "activity_scan_unavailable",
1802
1802
  retryable: true,
1803
1803
  },
1804
1804
  "agent.roadmap_maintenance_unavailable": {
@@ -24,10 +24,10 @@ export function createAgentRoutes(deps) {
24
24
  },
25
25
  });
26
26
  }
27
- if (!deps.triggerHourlyCheck) {
27
+ if (!deps.triggerActivityScan) {
28
28
  return respondWithAgentError(c, 503, [
29
- composeIssue("agent.hourly_check_unavailable", {
30
- field: "triggerHourlyCheck",
29
+ composeIssue("agent.activity_scan_unavailable", {
30
+ field: "triggerActivityScan",
31
31
  received: "<unavailable>",
32
32
  }),
33
33
  ]);
@@ -37,7 +37,7 @@ export function createAgentRoutes(deps) {
37
37
  ? body.reason.trim()
38
38
  : "api";
39
39
  // Default to `force: true` for manual runs — the user explicitly asked
40
- // us to check now, so bypass the `hourlyCheckMinObservations` threshold.
40
+ // us to check now, so bypass the `activityScanMinObservations` threshold.
41
41
  // Callers may pass `{ force: false }` to respect the threshold (e.g.,
42
42
  // for a gentle cron-style "check if anything is pending" ping).
43
43
  const force = body.force === undefined ? true : body.force === true;
@@ -61,7 +61,7 @@ export function createAgentRoutes(deps) {
61
61
  });
62
62
  }
63
63
  }
64
- const result = await deps.triggerHourlyCheck(`manual:${reason}`, {
64
+ const result = await deps.triggerActivityScan(`manual:${reason}`, {
65
65
  force,
66
66
  ...(requestedModel ? { requestedModel } : {}),
67
67
  });
@@ -251,6 +251,7 @@ export function createAgentRoutes(deps) {
251
251
  const normalizedPlatforms = platforms ?? (platform ? [platform] : undefined);
252
252
  const messageSummary = message.slice(0, 200);
253
253
  const originSessionId = parsePositiveIntegerHeader(c.req.header("x-pa-session-id"));
254
+ const correlationId = c.req.header("x-pa-event-correlation-id");
254
255
  let dispatchId = randomUUID();
255
256
  let notificationId = dispatchId;
256
257
  if (sendNotification) {
@@ -260,7 +261,29 @@ export function createAgentRoutes(deps) {
260
261
  priority: priority ?? "normal",
261
262
  notificationType: "agent",
262
263
  ...(originSessionId !== null ? { originSessionId } : {}),
264
+ ...(correlationId ? { correlationId } : {}),
263
265
  });
266
+ // QUIET_HOURS_HARDENING_PLAN.md Phase 1 — quiet-hours deferral. The
267
+ // message WILL deliver (durable `task_type='dm'` row at the
268
+ // quiet-hours edge, visible on /schedule), so the event is marked
269
+ // notified exactly like the sent path: not marking would double-send
270
+ // once the dispatcher's implicit final-text forward fires.
271
+ if (result.status === "deferred_quiet_hours") {
272
+ if (deps.markEventNotified && correlationId) {
273
+ deps.markEventNotified(correlationId);
274
+ }
275
+ return c.json({
276
+ status: "deferred_quiet_hours",
277
+ scheduleId: result.scheduleId,
278
+ deliverAfter: result.deliverAfter,
279
+ });
280
+ }
281
+ // Rate-limited: nothing was sent and nothing was queued — the live
282
+ // session can adapt (write to today.md, retry later). The event is
283
+ // deliberately NOT marked notified.
284
+ if (result.status === "rate_limited") {
285
+ return c.json({ status: "rate_limited", retryAfter: result.retryAfter }, 429);
286
+ }
264
287
  dispatchId = result.dispatchId;
265
288
  notificationId = dispatchId;
266
289
  }
@@ -283,13 +306,11 @@ export function createAgentRoutes(deps) {
283
306
  // Notify-dedup — when the agent's shim auto-attached the correlation
284
307
  // header, mark the event so the dispatcher skips the implicit
285
308
  // final-text DM forward in processResult. We only mark on the
286
- // success branch (sendNotification path); the warn-fallback branch
287
- // is daemon-misconfiguration, not user-visible delivery.
288
- if (sendNotification && deps.markEventNotified) {
289
- const correlationId = c.req.header("x-pa-event-correlation-id");
290
- if (correlationId) {
291
- deps.markEventNotified(correlationId);
292
- }
309
+ // delivery branches (sent above-the-fold here, deferred earlier); the
310
+ // warn-fallback branch is daemon-misconfiguration, not user-visible
311
+ // delivery, and the rate-limited branch never delivers.
312
+ if (sendNotification && deps.markEventNotified && correlationId) {
313
+ deps.markEventNotified(correlationId);
293
314
  }
294
315
  return c.json({
295
316
  status: "sent",