@aitne/daemon 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/adapters/adapter-watchdog.d.ts +70 -0
  2. package/dist/adapters/adapter-watchdog.js +115 -0
  3. package/dist/adapters/discord.d.ts +17 -1
  4. package/dist/adapters/discord.js +33 -0
  5. package/dist/adapters/notification-manager.d.ts +27 -1
  6. package/dist/adapters/notification-manager.js +54 -39
  7. package/dist/adapters/slack-adapter.d.ts +26 -1
  8. package/dist/adapters/slack-adapter.js +41 -0
  9. package/dist/adapters/telegram-adapter.d.ts +18 -1
  10. package/dist/adapters/telegram-adapter.js +41 -2
  11. package/dist/adapters/types.d.ts +20 -0
  12. package/dist/adapters/whatsapp-adapter.d.ts +26 -7
  13. package/dist/adapters/whatsapp-adapter.js +74 -21
  14. package/dist/api/env-writer.js +8 -5
  15. package/dist/api/helpers/agent-errors-registry.d.ts +5 -5
  16. package/dist/api/helpers/agent-errors-registry.js +5 -5
  17. package/dist/api/routes/agent.js +33 -12
  18. package/dist/api/routes/agents/index.js +75 -16
  19. package/dist/api/routes/agents/views.d.ts +37 -2
  20. package/dist/api/routes/agents/views.js +64 -2
  21. package/dist/api/routes/background-task.d.ts +22 -0
  22. package/dist/api/routes/background-task.js +338 -0
  23. package/dist/api/routes/browser-history.js +9 -1
  24. package/dist/api/routes/context/permissions.js +3 -2
  25. package/dist/api/routes/context/snapshots.js +0 -3
  26. package/dist/api/routes/context/write.js +3 -17
  27. package/dist/api/routes/dashboard/config.js +48 -12
  28. package/dist/api/routes/dashboard/cost-approvals.js +66 -0
  29. package/dist/api/routes/dashboard/notifications.js +9 -9
  30. package/dist/api/routes/integrations/crud-patch.js +5 -1
  31. package/dist/api/routes/integrations-reconcile.js +2 -2
  32. package/dist/api/routes/notion.d.ts +1 -1
  33. package/dist/api/routes/observations.js +7 -7
  34. package/dist/api/routes/obsidian.d.ts +1 -1
  35. package/dist/api/routes/receipts.js +5 -1
  36. package/dist/api/routes/setup-migrate.js +1 -1
  37. package/dist/api/routes/setup.js +1 -1
  38. package/dist/api/routes/task-flows.d.ts +1 -1
  39. package/dist/api/routes/task-flows.js +1 -1
  40. package/dist/api/routes/tuning.d.ts +29 -0
  41. package/dist/api/routes/tuning.js +304 -0
  42. package/dist/api/server.d.ts +44 -16
  43. package/dist/api/server.js +9 -0
  44. package/dist/bootstrap/adapters.d.ts +19 -0
  45. package/dist/bootstrap/adapters.js +61 -0
  46. package/dist/bootstrap/api.d.ts +5 -3
  47. package/dist/bootstrap/api.js +45 -13
  48. package/dist/bootstrap/catchup.d.ts +1 -1
  49. package/dist/bootstrap/catchup.js +11 -11
  50. package/dist/bootstrap/event-pipeline.d.ts +11 -0
  51. package/dist/bootstrap/event-pipeline.js +245 -7
  52. package/dist/bootstrap/observers.js +9 -6
  53. package/dist/bootstrap/schedule-helpers.d.ts +104 -6
  54. package/dist/bootstrap/schedule-helpers.js +172 -19
  55. package/dist/config.js +26 -12
  56. package/dist/core/agent-core.d.ts +33 -1
  57. package/dist/core/agent-core.js +36 -1
  58. package/dist/core/agents/activity-scan-cadence.d.ts +103 -0
  59. package/dist/core/agents/activity-scan-cadence.js +127 -0
  60. package/dist/core/agents/agent-route-override.d.ts +53 -0
  61. package/dist/core/agents/agent-route-override.js +69 -0
  62. package/dist/core/agents/builtin-registry.d.ts +51 -14
  63. package/dist/core/agents/builtin-registry.js +92 -15
  64. package/dist/core/agents/config-gate-reconcile.d.ts +38 -0
  65. package/dist/core/agents/config-gate-reconcile.js +51 -0
  66. package/dist/core/agents/cron-substitute.d.ts +1 -1
  67. package/dist/core/agents/cron-substitute.js +1 -1
  68. package/dist/core/agents/custom-routine-migration.d.ts +60 -0
  69. package/dist/core/agents/custom-routine-migration.js +149 -0
  70. package/dist/core/agents/firing-blocked.d.ts +1 -1
  71. package/dist/core/agents/hourly-cadence.d.ts +102 -0
  72. package/dist/core/agents/hourly-cadence.js +126 -0
  73. package/dist/core/agents/loader-boot.js +23 -0
  74. package/dist/core/agents/loader.d.ts +19 -0
  75. package/dist/core/agents/loader.js +34 -2
  76. package/dist/core/agents/override-merge.d.ts +1 -1
  77. package/dist/core/agents/override-merge.js +9 -1
  78. package/dist/core/agents/recurrence-convert.d.ts +1 -1
  79. package/dist/core/agents/recurrence-convert.js +1 -1
  80. package/dist/core/agents/recurring-schedule-adapter.js +8 -0
  81. package/dist/core/alerts.js +6 -6
  82. package/dist/core/backends/auth-health-monitor.d.ts +2 -2
  83. package/dist/core/backends/auth-health-monitor.js +1 -1
  84. package/dist/core/backends/backend-router.d.ts +27 -1
  85. package/dist/core/backends/backend-router.js +165 -1
  86. package/dist/core/backends/claude-code-core.d.ts +71 -31
  87. package/dist/core/backends/claude-code-core.js +282 -54
  88. package/dist/core/backends/cli-quota-guards.d.ts +29 -1
  89. package/dist/core/backends/cli-quota-guards.js +40 -5
  90. package/dist/core/backends/codex-core.d.ts +6 -0
  91. package/dist/core/backends/codex-core.js +22 -6
  92. package/dist/core/backends/failure-spend.d.ts +58 -0
  93. package/dist/core/backends/failure-spend.js +137 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +6 -0
  95. package/dist/core/backends/gemini-cli-core.js +25 -6
  96. package/dist/core/backends/model-registry.d.ts +1 -1
  97. package/dist/core/backends/model-registry.js +4 -4
  98. package/dist/core/backends/opencode-core.d.ts +1 -1
  99. package/dist/core/backends/opencode-core.js +5 -5
  100. package/dist/core/backends/plan-presets.js +39 -15
  101. package/dist/core/bang-commands/commands-cost.js +3 -1
  102. package/dist/core/bang-commands/commands-report.js +4 -3
  103. package/dist/core/bang-commands/commands-research.js +4 -1
  104. package/dist/core/bang-commands/commands-revert-tuning.d.ts +18 -0
  105. package/dist/core/bang-commands/commands-revert-tuning.js +63 -0
  106. package/dist/core/bang-commands/commands-stop-start.js +3 -3
  107. package/dist/core/bang-commands/commands-task-control.d.ts +19 -0
  108. package/dist/core/bang-commands/commands-task-control.js +147 -0
  109. package/dist/core/bang-commands/commands-wiki.js +5 -5
  110. package/dist/core/bang-commands/index.d.ts +2 -0
  111. package/dist/core/bang-commands/index.js +12 -0
  112. package/dist/core/bang-commands/registry.d.ts +12 -0
  113. package/dist/core/browser-history/research-cluster-fanout.d.ts +28 -14
  114. package/dist/core/browser-history/research-cluster-fanout.js +39 -16
  115. package/dist/core/channel-timeline.d.ts +5 -1
  116. package/dist/core/channel-timeline.js +13 -0
  117. package/dist/core/context/index-reconciler.js +5 -2
  118. package/dist/core/context/policy-index-reconciler.d.ts +6 -4
  119. package/dist/core/context/policy-index-runner.js +25 -6
  120. package/dist/core/context-builder-calendar.js +10 -2
  121. package/dist/core/context-builder-conversation.d.ts +8 -1
  122. package/dist/core/context-builder-conversation.js +41 -7
  123. package/dist/core/context-builder-yesterday.js +4 -3
  124. package/dist/core/context-builder.d.ts +7 -2
  125. package/dist/core/context-builder.js +62 -20
  126. package/dist/core/context-file-serializer.d.ts +1 -1
  127. package/dist/core/context-file-serializer.js +1 -1
  128. package/dist/core/context-health.js +2 -2
  129. package/dist/core/context-paths.d.ts +1 -1
  130. package/dist/core/context-paths.js +1 -1
  131. package/dist/core/context-validation/prepare-write.js +1 -1
  132. package/dist/core/context-validation/routine-rulebook.d.ts +1 -1
  133. package/dist/core/context-vault-aliases.d.ts +0 -13
  134. package/dist/core/context-vault-aliases.js +37 -0
  135. package/dist/core/custom-routines.d.ts +99 -0
  136. package/dist/core/custom-routines.js +187 -0
  137. package/dist/core/daemon-api-cli.js +49 -0
  138. package/dist/core/day-boundary.d.ts +46 -0
  139. package/dist/core/day-boundary.js +40 -0
  140. package/dist/core/dispatcher-activity-scan.d.ts +221 -0
  141. package/dist/core/dispatcher-activity-scan.js +775 -0
  142. package/dist/core/dispatcher-error-handling.d.ts +6 -11
  143. package/dist/core/dispatcher-error-handling.js +38 -62
  144. package/dist/core/dispatcher-hourly-check.js +6 -1
  145. package/dist/core/dispatcher-message-handler.d.ts +10 -0
  146. package/dist/core/dispatcher-message-handler.js +17 -0
  147. package/dist/core/dispatcher-morning-routine.d.ts +6 -6
  148. package/dist/core/dispatcher-morning-routine.js +13 -13
  149. package/dist/core/dispatcher-result-processor.d.ts +33 -0
  150. package/dist/core/dispatcher-result-processor.js +167 -11
  151. package/dist/core/dispatcher-scheduled-background-task.d.ts +42 -0
  152. package/dist/core/dispatcher-scheduled-background-task.js +89 -0
  153. package/dist/core/dispatcher-scheduled-tasks.d.ts +63 -1
  154. package/dist/core/dispatcher-scheduled-tasks.js +213 -6
  155. package/dist/core/dispatcher-task-delivery.d.ts +105 -0
  156. package/dist/core/dispatcher-task-delivery.js +555 -0
  157. package/dist/core/dispatcher-types.d.ts +48 -9
  158. package/dist/core/dispatcher-types.js +3 -3
  159. package/dist/core/dispatcher.d.ts +112 -31
  160. package/dist/core/dispatcher.js +284 -59
  161. package/dist/core/dm-freshness-metrics.d.ts +1 -1
  162. package/dist/core/drift-effects.js +2 -2
  163. package/dist/core/feedback/consolidation-prep.js +17 -5
  164. package/dist/core/feedback/eviction-scorer.js +6 -2
  165. package/dist/core/feedback/lesson-format.js +9 -4
  166. package/dist/core/feedback/lesson-injection.d.ts +1 -1
  167. package/dist/core/feedback/lesson-injection.js +17 -2
  168. package/dist/core/feedback/lesson-store-overview.d.ts +8 -4
  169. package/dist/core/feedback/lesson-store-overview.js +8 -4
  170. package/dist/core/feedback/regeneralization-prep.js +29 -16
  171. package/dist/core/feedback/self-performance-prep.d.ts +186 -0
  172. package/dist/core/feedback/self-performance-prep.js +541 -0
  173. package/dist/core/feedback/tuning-actuator.d.ts +198 -0
  174. package/dist/core/feedback/tuning-actuator.js +432 -0
  175. package/dist/core/feedback/tuning-recommender.d.ts +247 -0
  176. package/dist/core/feedback/tuning-recommender.js +580 -0
  177. package/dist/core/feedback/tuning-revert-monitor.d.ts +90 -0
  178. package/dist/core/feedback/tuning-revert-monitor.js +213 -0
  179. package/dist/core/health-monitor.d.ts +6 -0
  180. package/dist/core/health-monitor.js +1 -1
  181. package/dist/core/injection-policy.d.ts +4 -4
  182. package/dist/core/injection-policy.js +4 -4
  183. package/dist/core/integration-main-backend.js +4 -0
  184. package/dist/core/management-md.d.ts +2 -2
  185. package/dist/core/management-md.js +51 -13
  186. package/dist/core/morning/orchestrator.d.ts +2 -2
  187. package/dist/core/morning/orchestrator.js +2 -2
  188. package/dist/core/notification-gate.d.ts +64 -0
  189. package/dist/core/notification-gate.js +51 -0
  190. package/dist/core/notification-rate-limit.d.ts +40 -0
  191. package/dist/core/notification-rate-limit.js +50 -0
  192. package/dist/core/policy-files.d.ts +1 -1
  193. package/dist/core/policy-files.js +2 -2
  194. package/dist/core/pre-pass-freshness.d.ts +4 -4
  195. package/dist/core/retention.d.ts +5 -0
  196. package/dist/core/retention.js +20 -4
  197. package/dist/core/review-context.d.ts +1 -1
  198. package/dist/core/review-context.js +10 -5
  199. package/dist/core/roadmap-write-lock.d.ts +2 -1
  200. package/dist/core/roadmap-write-lock.js +15 -10
  201. package/dist/core/routine-acquisition-plan.d.ts +47 -1
  202. package/dist/core/routine-acquisition-plan.js +78 -20
  203. package/dist/core/routine-fetch-window-retry.js +7 -4
  204. package/dist/core/routine-fetch-window-runner.d.ts +39 -3
  205. package/dist/core/routine-fetch-window-runner.js +264 -13
  206. package/dist/core/routine-windows.d.ts +2 -2
  207. package/dist/core/routine-windows.js +8 -5
  208. package/dist/core/scheduler.d.ts +175 -16
  209. package/dist/core/scheduler.js +559 -102
  210. package/dist/core/signal-detector.d.ts +12 -0
  211. package/dist/core/signal-detector.js +53 -9
  212. package/dist/core/skills-compiler-denied-tools.js +2 -2
  213. package/dist/core/skills-compiler-skill-index.d.ts +2 -2
  214. package/dist/core/skills-compiler-skill-index.js +2 -2
  215. package/dist/core/skills-compiler-variants.d.ts +1 -1
  216. package/dist/core/skills-compiler-variants.js +8 -0
  217. package/dist/core/skills-compiler.d.ts +29 -26
  218. package/dist/core/skills-compiler.js +117 -81
  219. package/dist/core/skills-manifest.d.ts +37 -0
  220. package/dist/core/skills-manifest.js +73 -2
  221. package/dist/core/sleep-inhibitor.d.ts +79 -0
  222. package/dist/core/sleep-inhibitor.js +132 -0
  223. package/dist/core/slim-system-prompt-loader.d.ts +77 -0
  224. package/dist/core/slim-system-prompt-loader.js +141 -0
  225. package/dist/core/spawn-gates.d.ts +126 -0
  226. package/dist/core/spawn-gates.js +180 -0
  227. package/dist/core/today-direct-writer.d.ts +2 -2
  228. package/dist/core/today-direct-writer.js +1 -1
  229. package/dist/core/today-write-lock.d.ts +4 -2
  230. package/dist/core/today-write-lock.js +30 -20
  231. package/dist/core/wake-detector.d.ts +55 -0
  232. package/dist/core/wake-detector.js +80 -0
  233. package/dist/core/wiki/compile-lock.d.ts +1 -1
  234. package/dist/core/wiki/compile-lock.js +1 -1
  235. package/dist/core/workdir.js +15 -6
  236. package/dist/db/activity-scan-signals.d.ts +77 -0
  237. package/dist/db/activity-scan-signals.js +378 -0
  238. package/dist/db/agents-store.d.ts +28 -0
  239. package/dist/db/agents-store.js +62 -0
  240. package/dist/db/background-task-clarifications-store.d.ts +81 -0
  241. package/dist/db/background-task-clarifications-store.js +152 -0
  242. package/dist/db/background-task-store.d.ts +207 -0
  243. package/dist/db/background-task-store.js +380 -0
  244. package/dist/db/browser-history-store.d.ts +39 -6
  245. package/dist/db/browser-history-store.js +51 -7
  246. package/dist/db/browser-task-clarifications-store.d.ts +12 -0
  247. package/dist/db/browser-task-clarifications-store.js +35 -5
  248. package/dist/db/browser-task-store.d.ts +3 -0
  249. package/dist/db/browser-task-store.js +29 -4
  250. package/dist/db/deferred-dm.d.ts +86 -0
  251. package/dist/db/deferred-dm.js +199 -0
  252. package/dist/db/migrations.js +330 -0
  253. package/dist/db/observations.d.ts +2 -2
  254. package/dist/db/observations.js +3 -3
  255. package/dist/db/schema.js +217 -16
  256. package/dist/db/voice-transcripts-store.d.ts +1 -1
  257. package/dist/index.js +86 -29
  258. package/dist/messaging/browser-task-mcp-notifier.d.ts +12 -70
  259. package/dist/messaging/browser-task-mcp-notifier.js +30 -151
  260. package/dist/messaging/browser-task-screenshot-attachment.d.ts +15 -0
  261. package/dist/messaging/browser-task-screenshot-attachment.js +63 -0
  262. package/dist/observers/delegated-sync-worker.d.ts +6 -6
  263. package/dist/observers/delegated-sync-worker.js +10 -10
  264. package/dist/observers/git-delegated-cron.d.ts +1 -1
  265. package/dist/observers/git-delegated-cron.js +2 -2
  266. package/dist/observers/github-poller-classifier.d.ts +3 -3
  267. package/dist/observers/github-poller-classifier.js +3 -3
  268. package/dist/observers/imminent-event-scheduler.d.ts +1 -1
  269. package/dist/observers/imminent-event-scheduler.js +1 -1
  270. package/dist/observers/mail-poller.d.ts +1 -0
  271. package/dist/observers/mail-poller.js +42 -3
  272. package/dist/observers/observation-summarizer/summarizer-client.d.ts +2 -2
  273. package/dist/observers/observation-summarizer/summarizer-client.js +2 -2
  274. package/dist/observers/observation-summarizer/worker.d.ts +2 -2
  275. package/dist/observers/observation-summarizer/worker.js +4 -4
  276. package/dist/observers/obsidian-watcher.d.ts +1 -1
  277. package/dist/observers/obsidian-watcher.js +1 -1
  278. package/dist/safety/agent-write-tracker.d.ts +4 -4
  279. package/dist/safety/agent-write-tracker.js +4 -4
  280. package/dist/safety/audit.d.ts +43 -5
  281. package/dist/safety/audit.js +86 -18
  282. package/dist/safety/risk-classifier.d.ts +6 -0
  283. package/dist/safety/risk-classifier.js +75 -11
  284. package/dist/scheduler/activity-scan-gate.d.ts +86 -0
  285. package/dist/scheduler/activity-scan-gate.js +132 -0
  286. package/dist/services/background-task/background-task-budget.d.ts +80 -0
  287. package/dist/services/background-task/background-task-budget.js +91 -0
  288. package/dist/services/background-task/background-task-driver.d.ts +105 -0
  289. package/dist/services/background-task/background-task-driver.js +416 -0
  290. package/dist/services/background-task/background-task-runner.d.ts +96 -0
  291. package/dist/services/background-task/background-task-runner.js +673 -0
  292. package/dist/services/background-task/background-task-tools.d.ts +84 -0
  293. package/dist/services/background-task/background-task-tools.js +247 -0
  294. package/dist/services/background-task/background-task-transition-events.d.ts +43 -0
  295. package/dist/services/background-task/background-task-transition-events.js +54 -0
  296. package/dist/services/browser-history/automation/egress-denylist.d.ts +1 -1
  297. package/dist/services/browser-history/automation/egress-denylist.js +16 -6
  298. package/dist/services/browser-history/managed-chromium/sandbox-launcher.js +0 -1
  299. package/dist/services/browser-task/browser-task-runner.js +53 -8
  300. package/dist/services/observations-batch.d.ts +1 -1
  301. package/dist/services/observations-batch.js +2 -2
  302. package/dist/settings/runtime-settings.d.ts +38 -11
  303. package/dist/settings/runtime-settings.js +203 -40
  304. package/dist/settings/settings-store.js +11 -3
  305. package/package.json +4 -4
@@ -0,0 +1,70 @@
1
+ import type { AdapterConnectionState } from "./types.js";
2
+ /** Probe cadence. */
3
+ export declare const ADAPTER_WATCHDOG_INTERVAL_MS: number;
4
+ /**
5
+ * Consecutive "down" observations before forcing a restart. Two ticks
6
+ * (~10 min at the default cadence) gives every library's own reconnect
7
+ * machinery (discord.js gateway resume, @slack/socket-mode backoff,
8
+ * telegraf poll retry) ample time to self-heal a transient blip — the
9
+ * watchdog only steps in for the gave-up / wedged cases.
10
+ */
11
+ export declare const ADAPTER_WATCHDOG_DOWN_TICKS_BEFORE_RESTART = 2;
12
+ export interface WatchedAdapter {
13
+ platform: string;
14
+ /**
15
+ * Live transport probe. Closures typically read the mutable
16
+ * `AdapterState` slot so a reload that swaps the instance is picked up
17
+ * automatically. Return "unknown" when there is nothing to assess
18
+ * (adapter not configured / not started) — the watchdog takes no action
19
+ * and resets the down counter.
20
+ */
21
+ getConnectionState: () => AdapterConnectionState;
22
+ /**
23
+ * Full stop→start cycle (bootstrap's `reload*Adapter(true)`). Expected
24
+ * to manage MessageHub status transitions itself. Errors are caught and
25
+ * logged; the down counter is preserved so the next tick retries.
26
+ */
27
+ restart: () => Promise<void>;
28
+ /**
29
+ * Invoked on observed state transitions (down ↔ ok) so the caller can
30
+ * surface them (e.g. `messageHub.setPlatformRuntimeStatus`). Optional.
31
+ */
32
+ onStateChange?: (state: AdapterConnectionState) => void;
33
+ }
34
+ export interface AdapterWatchdogOptions {
35
+ intervalMs?: number;
36
+ downTicksBeforeRestart?: number;
37
+ }
38
+ /**
39
+ * Daemon-level liveness watchdog for the messaging adapters.
40
+ *
41
+ * Every adapter relies on library-internal reconnection that can end in a
42
+ * permanently dead transport while the daemon still reports "connected":
43
+ * a Slack reconnect chain killed by an unrecoverable start error, a
44
+ * discord.js session invalidation, a telegraf poll loop exited on a
45
+ * non-retryable error — all typically after machine sleep kills the TCP
46
+ * sockets. The pre-watchdog behavior was a silent, indefinite outage that
47
+ * only a daemon restart cleared.
48
+ *
49
+ * The watchdog polls each adapter's `getConnectionState()` and, after
50
+ * {@link ADAPTER_WATCHDOG_DOWN_TICKS_BEFORE_RESTART} consecutive "down"
51
+ * observations, forces a full adapter restart through the bootstrap
52
+ * reloader. Restarts are serialized per adapter (no overlapping cycles)
53
+ * and a failed restart retries on the following tick.
54
+ */
55
+ export declare class AdapterWatchdog {
56
+ private readonly intervalMs;
57
+ private readonly downTicksBeforeRestart;
58
+ private readonly watched;
59
+ private readonly downCounts;
60
+ private readonly lastObserved;
61
+ private readonly restartInFlight;
62
+ private timer;
63
+ constructor(options?: AdapterWatchdogOptions);
64
+ register(adapter: WatchedAdapter): void;
65
+ start(): void;
66
+ stop(): void;
67
+ /** One probe pass over all watched adapters. Exposed for tests. */
68
+ tick(): Promise<void>;
69
+ private probe;
70
+ }
@@ -0,0 +1,115 @@
1
+ import { createLogger } from "../logging.js";
2
+ const logger = createLogger("adapter-watchdog");
3
+ /** Probe cadence. */
4
+ export const ADAPTER_WATCHDOG_INTERVAL_MS = 5 * 60_000;
5
+ /**
6
+ * Consecutive "down" observations before forcing a restart. Two ticks
7
+ * (~10 min at the default cadence) gives every library's own reconnect
8
+ * machinery (discord.js gateway resume, @slack/socket-mode backoff,
9
+ * telegraf poll retry) ample time to self-heal a transient blip — the
10
+ * watchdog only steps in for the gave-up / wedged cases.
11
+ */
12
+ export const ADAPTER_WATCHDOG_DOWN_TICKS_BEFORE_RESTART = 2;
13
+ /**
14
+ * Daemon-level liveness watchdog for the messaging adapters.
15
+ *
16
+ * Every adapter relies on library-internal reconnection that can end in a
17
+ * permanently dead transport while the daemon still reports "connected":
18
+ * a Slack reconnect chain killed by an unrecoverable start error, a
19
+ * discord.js session invalidation, a telegraf poll loop exited on a
20
+ * non-retryable error — all typically after machine sleep kills the TCP
21
+ * sockets. The pre-watchdog behavior was a silent, indefinite outage that
22
+ * only a daemon restart cleared.
23
+ *
24
+ * The watchdog polls each adapter's `getConnectionState()` and, after
25
+ * {@link ADAPTER_WATCHDOG_DOWN_TICKS_BEFORE_RESTART} consecutive "down"
26
+ * observations, forces a full adapter restart through the bootstrap
27
+ * reloader. Restarts are serialized per adapter (no overlapping cycles)
28
+ * and a failed restart retries on the following tick.
29
+ */
30
+ export class AdapterWatchdog {
31
+ intervalMs;
32
+ downTicksBeforeRestart;
33
+ watched = [];
34
+ downCounts = new Map();
35
+ lastObserved = new Map();
36
+ restartInFlight = new Set();
37
+ timer = null;
38
+ constructor(options) {
39
+ this.intervalMs = options?.intervalMs ?? ADAPTER_WATCHDOG_INTERVAL_MS;
40
+ this.downTicksBeforeRestart =
41
+ options?.downTicksBeforeRestart
42
+ ?? ADAPTER_WATCHDOG_DOWN_TICKS_BEFORE_RESTART;
43
+ }
44
+ register(adapter) {
45
+ this.watched.push(adapter);
46
+ }
47
+ start() {
48
+ if (this.timer)
49
+ return;
50
+ this.timer = setInterval(() => void this.tick(), this.intervalMs);
51
+ this.timer.unref?.();
52
+ logger.info({
53
+ platforms: this.watched.map((w) => w.platform),
54
+ intervalMs: this.intervalMs,
55
+ }, "Adapter watchdog started");
56
+ }
57
+ stop() {
58
+ if (!this.timer)
59
+ return;
60
+ clearInterval(this.timer);
61
+ this.timer = null;
62
+ }
63
+ /** One probe pass over all watched adapters. Exposed for tests. */
64
+ async tick() {
65
+ for (const adapter of this.watched) {
66
+ await this.probe(adapter).catch((err) => {
67
+ logger.error({ err, platform: adapter.platform }, "Adapter watchdog probe threw");
68
+ });
69
+ }
70
+ }
71
+ async probe(adapter) {
72
+ if (this.restartInFlight.has(adapter.platform))
73
+ return;
74
+ let state;
75
+ try {
76
+ state = adapter.getConnectionState();
77
+ }
78
+ catch (err) {
79
+ logger.warn({ err, platform: adapter.platform }, "Adapter connection probe threw; treating as unknown");
80
+ state = "unknown";
81
+ }
82
+ const previous = this.lastObserved.get(adapter.platform);
83
+ if (previous !== state) {
84
+ this.lastObserved.set(adapter.platform, state);
85
+ if (state === "down" || previous === "down") {
86
+ logger.warn({ platform: adapter.platform, from: previous ?? "unobserved", to: state }, "Adapter connection state changed");
87
+ adapter.onStateChange?.(state);
88
+ }
89
+ }
90
+ if (state !== "down") {
91
+ this.downCounts.set(adapter.platform, 0);
92
+ return;
93
+ }
94
+ const downCount = (this.downCounts.get(adapter.platform) ?? 0) + 1;
95
+ this.downCounts.set(adapter.platform, downCount);
96
+ if (downCount < this.downTicksBeforeRestart) {
97
+ logger.warn({ platform: adapter.platform, downCount }, "Adapter connection down — waiting for library self-recovery before restart");
98
+ return;
99
+ }
100
+ logger.warn({ platform: adapter.platform, downCount }, "Adapter connection still down — forcing restart");
101
+ this.restartInFlight.add(adapter.platform);
102
+ try {
103
+ await adapter.restart();
104
+ this.downCounts.set(adapter.platform, 0);
105
+ logger.info({ platform: adapter.platform }, "Adapter watchdog restart completed");
106
+ }
107
+ catch (err) {
108
+ // Counter is left at/above the threshold so the next tick retries.
109
+ logger.error({ err, platform: adapter.platform }, "Adapter watchdog restart failed; will retry on next tick");
110
+ }
111
+ finally {
112
+ this.restartInFlight.delete(adapter.platform);
113
+ }
114
+ }
115
+ }
@@ -1,4 +1,4 @@
1
- import type { MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
1
+ import type { AdapterConnectionState, MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
2
2
  import type { AttachmentStore } from "../services/attachments/store.js";
3
3
  export interface DiscordBotInfo {
4
4
  id: string;
@@ -48,6 +48,14 @@ export declare class DiscordAdapter implements MessageAdapter {
48
48
  private readonly onOwnerDetected;
49
49
  private botUserId;
50
50
  private botInfo;
51
+ /**
52
+ * Set when the gateway emits `Events.Invalidated` — discord.js stops
53
+ * reconnecting entirely on an invalidated session, so without external
54
+ * intervention the adapter is permanently deaf. Cleared on `start()`.
55
+ */
56
+ private sessionInvalidated;
57
+ /** True once `client.login()` has resolved; cleared in `stop()`. */
58
+ private startCompleted;
51
59
  private pairingChallenge;
52
60
  private readonly attachmentStore;
53
61
  constructor(opts: DiscordAdapterOptions);
@@ -64,6 +72,14 @@ export declare class DiscordAdapter implements MessageAdapter {
64
72
  static fetchBotInfo(botToken: string): Promise<DiscordBotInfo>;
65
73
  start(): Promise<void>;
66
74
  stop(): Promise<void>;
75
+ /**
76
+ * Live gateway liveness for the adapter watchdog. discord.js resumes /
77
+ * reconnects on its own for transient closes, so a momentary not-ready
78
+ * state is normal — the watchdog only acts on consecutive "down"
79
+ * observations. An invalidated session is reported "down" immediately
80
+ * because the client never recovers from it without a fresh login.
81
+ */
82
+ getConnectionState(): AdapterConnectionState;
67
83
  resolveUserChannel(): Promise<string | null>;
68
84
  sendMessage(params: {
69
85
  channel: string;
@@ -34,6 +34,14 @@ export class DiscordAdapter {
34
34
  onOwnerDetected;
35
35
  botUserId = null;
36
36
  botInfo = null;
37
+ /**
38
+ * Set when the gateway emits `Events.Invalidated` — discord.js stops
39
+ * reconnecting entirely on an invalidated session, so without external
40
+ * intervention the adapter is permanently deaf. Cleared on `start()`.
41
+ */
42
+ sessionInvalidated = false;
43
+ /** True once `client.login()` has resolved; cleared in `stop()`. */
44
+ startCompleted = false;
37
45
  pairingChallenge = null;
38
46
  attachmentStore;
39
47
  constructor(opts) {
@@ -113,6 +121,7 @@ export class DiscordAdapter {
113
121
  // Remove all listeners first to prevent duplicates on re-start
114
122
  this.client.removeAllListeners(Events.Error);
115
123
  this.client.removeAllListeners(Events.Warn);
124
+ this.client.removeAllListeners(Events.Invalidated);
116
125
  rawClient.removeAllListeners("raw");
117
126
  this.client.on(Events.Error, (err) => {
118
127
  logger.error({ error: err.message }, "Discord client error");
@@ -120,6 +129,14 @@ export class DiscordAdapter {
120
129
  this.client.on(Events.Warn, (info) => {
121
130
  logger.warn({ info }, "Discord client warn");
122
131
  });
132
+ // Session invalidation is terminal for discord.js — the client stops
133
+ // reconnecting. Record it so getConnectionState reports "down" and the
134
+ // adapter watchdog performs a full stop→start cycle.
135
+ this.client.on(Events.Invalidated, () => {
136
+ this.sessionInvalidated = true;
137
+ logger.error("Discord gateway session invalidated — client will not reconnect on its own");
138
+ });
139
+ this.sessionInvalidated = false;
123
140
  // We deliberately do NOT subscribe to Events.MessageCreate.
124
141
  //
125
142
  // discord.js 14.26.2 has a bug where MessageCreateAction silently
@@ -154,12 +171,28 @@ export class DiscordAdapter {
154
171
  avatarUrl: this.client.user.avatarURL() ?? null,
155
172
  };
156
173
  }
174
+ this.startCompleted = true;
157
175
  logger.info({ botUser: this.client.user?.tag }, "Discord adapter connected");
158
176
  }
159
177
  async stop() {
178
+ this.startCompleted = false;
160
179
  this.client.destroy();
161
180
  logger.info("Discord adapter disconnected");
162
181
  }
182
+ /**
183
+ * Live gateway liveness for the adapter watchdog. discord.js resumes /
184
+ * reconnects on its own for transient closes, so a momentary not-ready
185
+ * state is normal — the watchdog only acts on consecutive "down"
186
+ * observations. An invalidated session is reported "down" immediately
187
+ * because the client never recovers from it without a fresh login.
188
+ */
189
+ getConnectionState() {
190
+ if (!this.startCompleted)
191
+ return "unknown";
192
+ if (this.sessionInvalidated)
193
+ return "down";
194
+ return this.client.isReady() ? "ok" : "down";
195
+ }
163
196
  async resolveUserChannel() {
164
197
  if (!this.mutableOwnerId)
165
198
  return null;
@@ -5,6 +5,7 @@ import type { INotificationManager, ReplyActivityHandle } from "../core/dispatch
5
5
  import type { MessageReplyTarget } from "../core/dispatcher-types.js";
6
6
  import type { SignalDetector } from "../core/signal-detector.js";
7
7
  import type { MessageHub } from "./message-hub.js";
8
+ import type { OutboundAttachmentRef } from "./types.js";
8
9
  export interface NotificationManagerOptions {
9
10
  /**
10
11
  * Maximum number of times `deliverReply` will attempt the originating
@@ -27,6 +28,24 @@ export interface NotificationManagerOptions {
27
28
  * `setTimeout(...).unref()` so the timer never blocks daemon shutdown.
28
29
  */
29
30
  sleep?: (ms: number) => Promise<void>;
31
+ /**
32
+ * BACKGROUND_TASK_RUNNER_DESIGN.md §2.3 / §13 Decision 4 (Phase 4,
33
+ * opt-in) — router that hands a proactive autonomous forward to the
34
+ * task-delivery gate + activity-branch machinery so the REAL DM agent
35
+ * weaves it into the live conversation. Only invoked when the owner is
36
+ * active AND `config.autonomousForwardNaturalDelivery` is on; returns
37
+ * `true` when it enqueued the weave (this `deliverProactive` then
38
+ * returns without the verbatim send), `false` to fall through to the
39
+ * verbatim path. Wired from the event bus in bootstrap; absent in tests.
40
+ * The IDLE path is never rerouted — it keeps the richer verbatim
41
+ * delivery (multi-destination fan-out, notification_log telemetry,
42
+ * batching cooldown) unchanged.
43
+ */
44
+ routeAutonomousForwardNaturally?: (input: {
45
+ content: string;
46
+ event: import("@aitne/shared").Event;
47
+ originSessionId?: number;
48
+ }) => Promise<boolean>;
30
49
  }
31
50
  type DestinationMode = "default" | "configured_only";
32
51
  export declare class NotificationManager implements INotificationManager {
@@ -61,6 +80,7 @@ export declare class NotificationManager implements INotificationManager {
61
80
  private readonly replyRetryAttempts;
62
81
  private readonly replyRetryBackoffBaseMs;
63
82
  private readonly sleep;
83
+ private readonly routeAutonomousForwardNaturally?;
64
84
  constructor(messageHub: MessageHub, db: Database.Database, config: AgentConfig, options?: NotificationManagerOptions);
65
85
  /** Set the SignalDetector for implicit feedback tracking. */
66
86
  setSignalDetector(detector: SignalDetector): void;
@@ -84,6 +104,7 @@ export declare class NotificationManager implements INotificationManager {
84
104
  destinationMode?: DestinationMode;
85
105
  originSessionId?: number;
86
106
  replyTo?: MessageReplyTarget;
107
+ attachments?: readonly OutboundAttachmentRef[];
87
108
  }): Promise<void>;
88
109
  /**
89
110
  * Direct reply to a MessageEvent — bypasses quiet-hours, rate-limits,
@@ -188,7 +209,12 @@ export declare class NotificationManager implements INotificationManager {
188
209
  * cache invalidates naturally on every call (config can hot-reload
189
210
  * via PATCH /api/config). */
190
211
  private quietHoursWindow;
191
- /** Check if hourly or daily notification limits have been reached */
212
+ /**
213
+ * Check if hourly or daily notification limits have been reached.
214
+ * Counting lives in the shared `core/notification-rate-limit.ts` helper
215
+ * so this gate and the `/api/notify` route gate
216
+ * (QUIET_HOURS_HARDENING_PLAN.md Phase 1) cannot drift.
217
+ */
192
218
  private isRateLimited;
193
219
  /**
194
220
  * Per-type gate. True when this event type has hit
@@ -1,8 +1,11 @@
1
- import { isMessageEvent, getAgentDayBoundsUtc } from "@aitne/shared";
1
+ import { isMessageEvent } from "@aitne/shared";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { isInQuietHoursAt, nextQuietHoursEndMs as computeNextQuietHoursEndMs, } from "../core/quiet-hours.js";
4
+ import { evaluateNotificationRateLimit } from "../core/notification-rate-limit.js";
5
+ import { SAFETY_CATEGORIES } from "../core/notification-gate.js";
4
6
  import { MessageDeliveryError } from "./message-hub.js";
5
7
  import { recordProactiveForwardDeliveries } from "../core/channel-timeline.js";
8
+ import { classifyOwnerDmActivity } from "../core/context-builder-conversation.js";
6
9
  import { createLogger } from "../logging.js";
7
10
  const logger = createLogger("notification-manager");
8
11
  /**
@@ -26,13 +29,6 @@ function logDeliveryFailure(err, msg) {
26
29
  const NOOP_REPLY_ACTIVITY = {
27
30
  stop: async () => { },
28
31
  };
29
- /** Safety categories bypass quiet hours and user preferences */
30
- const SAFETY_CATEGORIES = [
31
- "security",
32
- "deadline",
33
- "error",
34
- "critical",
35
- ];
36
32
  /**
37
33
  * Default bounded-retry shape for `deliverReply`. Sized so the operator's
38
34
  * worst-case wait for an acknowledged DM is small (~600 ms across two
@@ -96,6 +92,7 @@ export class NotificationManager {
96
92
  replyRetryAttempts;
97
93
  replyRetryBackoffBaseMs;
98
94
  sleep;
95
+ routeAutonomousForwardNaturally;
99
96
  constructor(messageHub, db, config, options = {}) {
100
97
  this.messageHub = messageHub;
101
98
  this.db = db;
@@ -108,6 +105,7 @@ export class NotificationManager {
108
105
  this.replyRetryAttempts = Math.max(1, options.replyRetryAttempts ?? DEFAULT_REPLY_RETRY_ATTEMPTS);
109
106
  this.replyRetryBackoffBaseMs = Math.max(0, options.replyRetryBackoffBaseMs ?? DEFAULT_REPLY_RETRY_BACKOFF_BASE_MS);
110
107
  this.sleep = options.sleep ?? defaultSleep;
108
+ this.routeAutonomousForwardNaturally = options.routeAutonomousForwardNaturally;
111
109
  }
112
110
  /** Set the SignalDetector for implicit feedback tracking. */
113
111
  setSignalDetector(detector) {
@@ -150,7 +148,7 @@ export class NotificationManager {
150
148
  // a `failed` notification row, and we fall through to the proactive
151
149
  // path so the reply still reaches the operator.
152
150
  if (options?.replyTo) {
153
- const delivered = await this.deliverDirect(message, event, priority, options.replyTo);
151
+ const delivered = await this.deliverDirect(message, event, priority, options.replyTo, options.attachments);
154
152
  if (delivered)
155
153
  return;
156
154
  logger.info({
@@ -254,11 +252,17 @@ export class NotificationManager {
254
252
  * (`send`'s explicit-replyTo path) to drive the §3.4-bis fallback to
255
253
  * the proactive destinations.
256
254
  */
257
- async deliverDirect(message, event, priority, target) {
255
+ async deliverDirect(message, event, priority, target, attachments) {
258
256
  const dispatchId = randomUUID();
259
257
  const summary = truncateSummary(message);
260
258
  try {
261
- const delivery = await this.messageHub.sendToPlatform(target.platform, target.channel, message, target.threadId ?? undefined);
259
+ // Keep the no-attachment call shape identical to the historical
260
+ // 4-arg form so existing adapters/tests that assert the exact send
261
+ // signature are unaffected; only widen to 5 args when media is
262
+ // present (the task-delivery idle screenshot path).
263
+ const delivery = attachments && attachments.length > 0
264
+ ? await this.messageHub.sendToPlatform(target.platform, target.channel, message, target.threadId ?? undefined, [...attachments])
265
+ : await this.messageHub.sendToPlatform(target.platform, target.channel, message, target.threadId ?? undefined);
262
266
  this.logNotificationRows({
263
267
  dispatchId,
264
268
  event,
@@ -299,6 +303,33 @@ export class NotificationManager {
299
303
  const isSafety = this.isSafetyCategory(category, priority);
300
304
  const dispatchId = randomUUID();
301
305
  const summary = truncateSummary(message);
306
+ // BACKGROUND_TASK_RUNNER_DESIGN.md §2.3 / §13 Decision 4 (Phase 4,
307
+ // opt-in) — when the owner is ACTIVE mid-conversation, hand a
308
+ // non-safety, non-batched proactive forward to the delivery machinery
309
+ // so the real DM agent weaves it into the live thread instead of this
310
+ // verbatim dump. The IDLE path is deliberately left on the richer
311
+ // verbatim delivery below (multi-destination fan-out + telemetry +
312
+ // batching), so this only changes the active case. The router enqueues
313
+ // the weave and returns true; a false (no owner-DM channel / disabled)
314
+ // falls through to the unchanged verbatim path.
315
+ if (!isSafety
316
+ && !fromBatch
317
+ && this.config.autonomousForwardNaturalDelivery
318
+ && this.routeAutonomousForwardNaturally
319
+ && classifyOwnerDmActivity({ db: this.db, config: this.config }) === "active") {
320
+ try {
321
+ const routed = await this.routeAutonomousForwardNaturally({
322
+ content: message,
323
+ event,
324
+ ...(originSessionId !== undefined ? { originSessionId } : {}),
325
+ });
326
+ if (routed)
327
+ return;
328
+ }
329
+ catch (err) {
330
+ logger.warn({ err, eventType: event.type }, "autonomous-forward natural delivery routing threw; falling back to verbatim proactive send");
331
+ }
332
+ }
302
333
  if (!isSafety && this.isQuietHours()) {
303
334
  this.logNotificationRows({
304
335
  dispatchId,
@@ -613,35 +644,19 @@ export class NotificationManager {
613
644
  timezone: this.config.timezone || undefined,
614
645
  };
615
646
  }
616
- /** Check if hourly or daily notification limits have been reached */
647
+ /**
648
+ * Check if hourly or daily notification limits have been reached.
649
+ * Counting lives in the shared `core/notification-rate-limit.ts` helper
650
+ * so this gate and the `/api/notify` route gate
651
+ * (QUIET_HOURS_HARDENING_PLAN.md Phase 1) cannot drift.
652
+ */
617
653
  isRateLimited() {
618
- // Hourly check
619
- const hourlyCount = this.db
620
- .prepare(`SELECT COUNT(DISTINCT CASE
621
- WHEN dispatch_id != '' THEN dispatch_id
622
- ELSE CAST(id AS TEXT)
623
- END) as cnt
624
- FROM notification_log
625
- WHERE status = 'delivered'
626
- AND COALESCE(notification_type, '') != 'message.received'
627
- AND created_at > datetime('now', '-1 hour')`)
628
- .get();
629
- if (hourlyCount.cnt >= this.config.maxNotificationsPerHour) {
630
- return true;
631
- }
632
- // Daily check (timezone-aware agent day)
633
- const bounds = getAgentDayBoundsUtc(this.config.timezone, this.config.dayBoundaryHour);
634
- const dailyCount = this.db
635
- .prepare(`SELECT COUNT(DISTINCT CASE
636
- WHEN dispatch_id != '' THEN dispatch_id
637
- ELSE CAST(id AS TEXT)
638
- END) as cnt
639
- FROM notification_log
640
- WHERE status = 'delivered'
641
- AND COALESCE(notification_type, '') != 'message.received'
642
- AND created_at >= ? AND created_at < ?`)
643
- .get(bounds.start, bounds.end);
644
- return dailyCount.cnt >= this.config.maxNotificationsPerDay;
654
+ return evaluateNotificationRateLimit(this.db, {
655
+ maxNotificationsPerHour: this.config.maxNotificationsPerHour,
656
+ maxNotificationsPerDay: this.config.maxNotificationsPerDay,
657
+ timezone: this.config.timezone,
658
+ dayBoundaryHour: this.config.dayBoundaryHour,
659
+ }).limited;
645
660
  }
646
661
  /**
647
662
  * Per-type gate. True when this event type has hit
@@ -1,4 +1,4 @@
1
- import type { MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
1
+ import type { AdapterConnectionState, MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
2
2
  import type { AttachmentStore } from "../services/attachments/store.js";
3
3
  export interface SlackBotInfo {
4
4
  botUserId: string | null;
@@ -56,6 +56,12 @@ export declare class SlackAdapter implements MessageAdapter {
56
56
  private readonly onMessage;
57
57
  private readonly onOwnerDetected;
58
58
  private app;
59
+ /**
60
+ * True once `app.start()` has resolved (cleared in `stop()`). Gates the
61
+ * watchdog probe: before the first successful start there is no socket
62
+ * to assess, and reporting "down" then would trigger pointless restarts.
63
+ */
64
+ private startCompleted;
59
65
  private botUserId;
60
66
  private botInfo;
61
67
  private pairingChallenge;
@@ -75,6 +81,25 @@ export declare class SlackAdapter implements MessageAdapter {
75
81
  static fetchBotInfo(botToken: string): Promise<SlackBotInfo>;
76
82
  start(): Promise<void>;
77
83
  stop(): Promise<void>;
84
+ /**
85
+ * Live Socket Mode liveness for the adapter watchdog.
86
+ *
87
+ * Bolt's `App` keeps its `SocketModeReceiver` private, but the receiver's
88
+ * `client` (a `@slack/socket-mode` `SocketModeClient`) and the client's
89
+ * `websocket` (a `SlackWebSocket` with a public `isActive()`) are public
90
+ * fields — only the first hop needs a cast. `@slack/socket-mode` v2
91
+ * reconnects on `close` with an UNBOUNDED linear backoff, but a reconnect
92
+ * chain can still die permanently (`UnrecoverableSocketModeStartError`,
93
+ * or an exception escaping the recursive retry), and after machine sleep
94
+ * the ping-pong watchdog may take minutes to notice a dead socket. The
95
+ * adapter watchdog uses this probe to force a full stop→start cycle when
96
+ * the socket stays dead.
97
+ *
98
+ * Returns "unknown" (watchdog: no action) when the receiver shape is not
99
+ * what we expect — a Bolt upgrade must degrade to "no watchdog" rather
100
+ * than to restart loops.
101
+ */
102
+ getConnectionState(): AdapterConnectionState;
78
103
  resolveUserChannel(): Promise<string | null>;
79
104
  sendMessage(params: {
80
105
  channel: string;
@@ -34,6 +34,12 @@ export class SlackAdapter {
34
34
  onMessage;
35
35
  onOwnerDetected;
36
36
  app = null;
37
+ /**
38
+ * True once `app.start()` has resolved (cleared in `stop()`). Gates the
39
+ * watchdog probe: before the first successful start there is no socket
40
+ * to assess, and reporting "down" then would trigger pointless restarts.
41
+ */
42
+ startCompleted = false;
37
43
  botUserId = null;
38
44
  botInfo = null;
39
45
  pairingChallenge = null;
@@ -147,15 +153,50 @@ export class SlackAdapter {
147
153
  await this.handleMessage(message);
148
154
  });
149
155
  await this.app.start();
156
+ this.startCompleted = true;
150
157
  logger.info({ botUserId: this.botUserId }, "Slack adapter connected (Socket Mode)");
151
158
  }
152
159
  async stop() {
160
+ this.startCompleted = false;
153
161
  if (this.app) {
154
162
  await this.app.stop();
155
163
  this.app = null;
156
164
  }
157
165
  logger.info("Slack adapter disconnected");
158
166
  }
167
+ /**
168
+ * Live Socket Mode liveness for the adapter watchdog.
169
+ *
170
+ * Bolt's `App` keeps its `SocketModeReceiver` private, but the receiver's
171
+ * `client` (a `@slack/socket-mode` `SocketModeClient`) and the client's
172
+ * `websocket` (a `SlackWebSocket` with a public `isActive()`) are public
173
+ * fields — only the first hop needs a cast. `@slack/socket-mode` v2
174
+ * reconnects on `close` with an UNBOUNDED linear backoff, but a reconnect
175
+ * chain can still die permanently (`UnrecoverableSocketModeStartError`,
176
+ * or an exception escaping the recursive retry), and after machine sleep
177
+ * the ping-pong watchdog may take minutes to notice a dead socket. The
178
+ * adapter watchdog uses this probe to force a full stop→start cycle when
179
+ * the socket stays dead.
180
+ *
181
+ * Returns "unknown" (watchdog: no action) when the receiver shape is not
182
+ * what we expect — a Bolt upgrade must degrade to "no watchdog" rather
183
+ * than to restart loops.
184
+ */
185
+ getConnectionState() {
186
+ if (!this.app || !this.startCompleted)
187
+ return "unknown";
188
+ const receiver = this.app.receiver;
189
+ const websocket = receiver?.client?.websocket;
190
+ if (!websocket || typeof websocket.isActive !== "function") {
191
+ return "unknown";
192
+ }
193
+ try {
194
+ return websocket.isActive() ? "ok" : "down";
195
+ }
196
+ catch {
197
+ return "unknown";
198
+ }
199
+ }
159
200
  async resolveUserChannel() {
160
201
  if (!this.app || !this.mutableOwnerId)
161
202
  return null;
@@ -1,4 +1,4 @@
1
- import type { MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
1
+ import type { AdapterConnectionState, MessageAdapter, OnMessageCallback, OutboundAttachmentRef } from "./types.js";
2
2
  import type { AttachmentStore } from "../services/attachments/store.js";
3
3
  export interface TelegramBotInfo {
4
4
  id: number;
@@ -59,6 +59,15 @@ export declare class TelegramAdapter implements MessageAdapter {
59
59
  private readonly attachmentStore;
60
60
  private bot;
61
61
  private botInfo;
62
+ /**
63
+ * Non-null when the long-poll loop has died. telegraf's `launch()`
64
+ * promise stays pending while polling runs and settles when the loop
65
+ * exits: it self-retries network errors / 429 / 5xx internally, but any
66
+ * other failure (401 invalid token, 409 conflicting consumer, an
67
+ * unexpected throw) kills the loop for good. Without observing that
68
+ * settlement the daemon would sit "connected" while receiving nothing.
69
+ */
70
+ private pollingDeadError;
62
71
  /** Active pairing challenge (null when pairing isn't in progress). */
63
72
  private pairingChallenge;
64
73
  /** Buffers for Telegram media albums (media_group_id → accumulated items). */
@@ -84,6 +93,14 @@ export declare class TelegramAdapter implements MessageAdapter {
84
93
  start(): Promise<void>;
85
94
  stop(): Promise<void>;
86
95
  resolveUserChannel(): Promise<string | null>;
96
+ /**
97
+ * Live long-poll liveness for the adapter watchdog. "ok" means the
98
+ * polling loop is still running (telegraf retries transient network /
99
+ * 429 / 5xx failures internally, so an offline laptop stays "ok" and
100
+ * recovers on its own); "down" means the loop has exited and only a
101
+ * full stop→start cycle brings messages back.
102
+ */
103
+ getConnectionState(): AdapterConnectionState;
87
104
  sendMessage(params: {
88
105
  channel: string;
89
106
  text: string;