@aitne/daemon 0.1.2 → 0.1.4

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 (253) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  3. package/dist/adapters/whatsapp-adapter.js +0 -1
  4. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  5. package/dist/api/integration-route-gate.d.ts +15 -11
  6. package/dist/api/integration-route-gate.d.ts.map +1 -1
  7. package/dist/api/integration-route-gate.js +60 -23
  8. package/dist/api/integration-route-gate.js.map +1 -1
  9. package/dist/api/json-body.d.ts +22 -7
  10. package/dist/api/json-body.d.ts.map +1 -1
  11. package/dist/api/json-body.js +27 -8
  12. package/dist/api/json-body.js.map +1 -1
  13. package/dist/api/routes/agent.d.ts.map +1 -1
  14. package/dist/api/routes/agent.js +18 -0
  15. package/dist/api/routes/agent.js.map +1 -1
  16. package/dist/api/routes/backends.d.ts.map +1 -1
  17. package/dist/api/routes/backends.js +96 -1
  18. package/dist/api/routes/backends.js.map +1 -1
  19. package/dist/api/routes/books.js +1 -1
  20. package/dist/api/routes/books.js.map +1 -1
  21. package/dist/api/routes/context.d.ts.map +1 -1
  22. package/dist/api/routes/context.js +13 -1
  23. package/dist/api/routes/context.js.map +1 -1
  24. package/dist/api/routes/dashboard.d.ts.map +1 -1
  25. package/dist/api/routes/dashboard.js +75 -5
  26. package/dist/api/routes/dashboard.js.map +1 -1
  27. package/dist/api/routes/github.d.ts.map +1 -1
  28. package/dist/api/routes/github.js +38 -5
  29. package/dist/api/routes/github.js.map +1 -1
  30. package/dist/api/routes/integrations.d.ts +35 -6
  31. package/dist/api/routes/integrations.d.ts.map +1 -1
  32. package/dist/api/routes/integrations.js +191 -16
  33. package/dist/api/routes/integrations.js.map +1 -1
  34. package/dist/api/routes/mail.d.ts.map +1 -1
  35. package/dist/api/routes/mail.js +112 -46
  36. package/dist/api/routes/mail.js.map +1 -1
  37. package/dist/api/routes/observations.d.ts.map +1 -1
  38. package/dist/api/routes/observations.js +161 -8
  39. package/dist/api/routes/observations.js.map +1 -1
  40. package/dist/api/routes/setup-migrate.d.ts +9 -1
  41. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  42. package/dist/api/routes/setup-migrate.js +4 -2
  43. package/dist/api/routes/setup-migrate.js.map +1 -1
  44. package/dist/api/routes/skills.d.ts.map +1 -1
  45. package/dist/api/routes/skills.js +39 -1
  46. package/dist/api/routes/skills.js.map +1 -1
  47. package/dist/api/routes/voice.d.ts.map +1 -1
  48. package/dist/api/routes/voice.js +154 -14
  49. package/dist/api/routes/voice.js.map +1 -1
  50. package/dist/bootstrap/adapters.d.ts +109 -0
  51. package/dist/bootstrap/adapters.d.ts.map +1 -0
  52. package/dist/bootstrap/adapters.js +237 -0
  53. package/dist/bootstrap/adapters.js.map +1 -0
  54. package/dist/bootstrap/catchup.d.ts +23 -0
  55. package/dist/bootstrap/catchup.d.ts.map +1 -0
  56. package/dist/bootstrap/catchup.js +124 -0
  57. package/dist/bootstrap/catchup.js.map +1 -0
  58. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  59. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  60. package/dist/bootstrap/schedule-helpers.js +96 -0
  61. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  62. package/dist/bootstrap/services.d.ts +60 -0
  63. package/dist/bootstrap/services.d.ts.map +1 -0
  64. package/dist/bootstrap/services.js +209 -0
  65. package/dist/bootstrap/services.js.map +1 -0
  66. package/dist/core/backends/backend-router.d.ts +23 -0
  67. package/dist/core/backends/backend-router.d.ts.map +1 -1
  68. package/dist/core/backends/backend-router.js +48 -3
  69. package/dist/core/backends/backend-router.js.map +1 -1
  70. package/dist/core/backends/claude-auth.d.ts +70 -0
  71. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  72. package/dist/core/backends/claude-auth.js +198 -0
  73. package/dist/core/backends/claude-auth.js.map +1 -0
  74. package/dist/core/backends/claude-code-core.d.ts +47 -119
  75. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  76. package/dist/core/backends/claude-code-core.js +112 -1565
  77. package/dist/core/backends/claude-code-core.js.map +1 -1
  78. package/dist/core/backends/claude-delegated.d.ts +86 -0
  79. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  80. package/dist/core/backends/claude-delegated.js +801 -0
  81. package/dist/core/backends/claude-delegated.js.map +1 -0
  82. package/dist/core/backends/claude-errors.d.ts +39 -0
  83. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  84. package/dist/core/backends/claude-errors.js +71 -0
  85. package/dist/core/backends/claude-errors.js.map +1 -0
  86. package/dist/core/backends/claude-probe.d.ts +103 -0
  87. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  88. package/dist/core/backends/claude-probe.js +336 -0
  89. package/dist/core/backends/claude-probe.js.map +1 -0
  90. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  91. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  92. package/dist/core/backends/claude-tool-collection.js +831 -0
  93. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  94. package/dist/core/backends/gemini-cli-core.d.ts +21 -0
  95. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  96. package/dist/core/backends/gemini-cli-core.js +84 -6
  97. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  98. package/dist/core/backends/prompt-utils.d.ts +1 -0
  99. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  100. package/dist/core/backends/prompt-utils.js +60 -3
  101. package/dist/core/backends/prompt-utils.js.map +1 -1
  102. package/dist/core/context-builder.d.ts +36 -12
  103. package/dist/core/context-builder.d.ts.map +1 -1
  104. package/dist/core/context-builder.js +179 -89
  105. package/dist/core/context-builder.js.map +1 -1
  106. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  107. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  108. package/dist/core/dispatcher-date-utils.js +132 -0
  109. package/dist/core/dispatcher-date-utils.js.map +1 -0
  110. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  111. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  112. package/dist/core/dispatcher-error-handling.js +393 -0
  113. package/dist/core/dispatcher-error-handling.js.map +1 -0
  114. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  115. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  116. package/dist/core/dispatcher-hourly-check.js +665 -0
  117. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  118. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  119. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  120. package/dist/core/dispatcher-message-handler.js +1054 -0
  121. package/dist/core/dispatcher-message-handler.js.map +1 -0
  122. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  123. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  124. package/dist/core/dispatcher-morning-routine.js +434 -0
  125. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  126. package/dist/core/dispatcher-prompt.d.ts +107 -0
  127. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  128. package/dist/core/dispatcher-prompt.js +227 -0
  129. package/dist/core/dispatcher-prompt.js.map +1 -0
  130. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  131. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  132. package/dist/core/dispatcher-repository-helpers.js +86 -0
  133. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  134. package/dist/core/dispatcher-result-processor.d.ts +145 -0
  135. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  136. package/dist/core/dispatcher-result-processor.js +414 -0
  137. package/dist/core/dispatcher-result-processor.js.map +1 -0
  138. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  139. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  140. package/dist/core/dispatcher-scheduled-tasks.js +998 -0
  141. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  142. package/dist/core/dispatcher-types.d.ts +296 -0
  143. package/dist/core/dispatcher-types.d.ts.map +1 -0
  144. package/dist/core/dispatcher-types.js +106 -0
  145. package/dist/core/dispatcher-types.js.map +1 -0
  146. package/dist/core/dispatcher.d.ts +86 -610
  147. package/dist/core/dispatcher.d.ts.map +1 -1
  148. package/dist/core/dispatcher.js +293 -3542
  149. package/dist/core/dispatcher.js.map +1 -1
  150. package/dist/core/integration-health.d.ts +18 -10
  151. package/dist/core/integration-health.d.ts.map +1 -1
  152. package/dist/core/integration-health.js +31 -1
  153. package/dist/core/integration-health.js.map +1 -1
  154. package/dist/core/integration-lifecycle.d.ts +65 -0
  155. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  156. package/dist/core/integration-lifecycle.js +167 -16
  157. package/dist/core/integration-lifecycle.js.map +1 -1
  158. package/dist/core/integration-main-backend.d.ts +40 -0
  159. package/dist/core/integration-main-backend.d.ts.map +1 -1
  160. package/dist/core/integration-main-backend.js +89 -2
  161. package/dist/core/integration-main-backend.js.map +1 -1
  162. package/dist/core/management-md.d.ts +51 -17
  163. package/dist/core/management-md.d.ts.map +1 -1
  164. package/dist/core/management-md.js +233 -56
  165. package/dist/core/management-md.js.map +1 -1
  166. package/dist/core/output-language-policy.d.ts +74 -0
  167. package/dist/core/output-language-policy.d.ts.map +1 -0
  168. package/dist/core/output-language-policy.js +194 -0
  169. package/dist/core/output-language-policy.js.map +1 -0
  170. package/dist/core/prompts.d.ts +1 -0
  171. package/dist/core/prompts.d.ts.map +1 -1
  172. package/dist/core/prompts.js +121 -3
  173. package/dist/core/prompts.js.map +1 -1
  174. package/dist/core/repository-management-docs.d.ts +24 -0
  175. package/dist/core/repository-management-docs.d.ts.map +1 -1
  176. package/dist/core/repository-management-docs.js +210 -26
  177. package/dist/core/repository-management-docs.js.map +1 -1
  178. package/dist/core/routine-acquisition-plan.d.ts +131 -0
  179. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  180. package/dist/core/routine-acquisition-plan.js +268 -0
  181. package/dist/core/routine-acquisition-plan.js.map +1 -0
  182. package/dist/core/routine-fetch-window-runner.d.ts +201 -0
  183. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  184. package/dist/core/routine-fetch-window-runner.js +661 -0
  185. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  186. package/dist/core/routine-windows.d.ts +156 -0
  187. package/dist/core/routine-windows.d.ts.map +1 -0
  188. package/dist/core/routine-windows.js +330 -0
  189. package/dist/core/routine-windows.js.map +1 -0
  190. package/dist/core/skills-compiler.d.ts +11 -0
  191. package/dist/core/skills-compiler.d.ts.map +1 -1
  192. package/dist/core/skills-compiler.js +102 -13
  193. package/dist/core/skills-compiler.js.map +1 -1
  194. package/dist/core/skills-manifest.d.ts.map +1 -1
  195. package/dist/core/skills-manifest.js +26 -0
  196. package/dist/core/skills-manifest.js.map +1 -1
  197. package/dist/core/system-reset.d.ts.map +1 -1
  198. package/dist/core/system-reset.js +25 -2
  199. package/dist/core/system-reset.js.map +1 -1
  200. package/dist/db/observations.d.ts +45 -2
  201. package/dist/db/observations.d.ts.map +1 -1
  202. package/dist/db/observations.js +112 -14
  203. package/dist/db/observations.js.map +1 -1
  204. package/dist/db/schema.d.ts.map +1 -1
  205. package/dist/db/schema.js +13 -25
  206. package/dist/db/schema.js.map +1 -1
  207. package/dist/index.js +83 -610
  208. package/dist/index.js.map +1 -1
  209. package/dist/observers/delegated-sync-worker.d.ts +45 -2
  210. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  211. package/dist/observers/delegated-sync-worker.js +71 -21
  212. package/dist/observers/delegated-sync-worker.js.map +1 -1
  213. package/dist/observers/mail-poller.d.ts +12 -5
  214. package/dist/observers/mail-poller.d.ts.map +1 -1
  215. package/dist/observers/mail-poller.js +36 -14
  216. package/dist/observers/mail-poller.js.map +1 -1
  217. package/dist/observers/manager.d.ts +37 -5
  218. package/dist/observers/manager.d.ts.map +1 -1
  219. package/dist/observers/manager.js +28 -10
  220. package/dist/observers/manager.js.map +1 -1
  221. package/dist/safety/risk-classifier.d.ts.map +1 -1
  222. package/dist/safety/risk-classifier.js +5 -0
  223. package/dist/safety/risk-classifier.js.map +1 -1
  224. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  225. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  226. package/dist/services/delegated-backend-invoker.js +41 -480
  227. package/dist/services/delegated-backend-invoker.js.map +1 -1
  228. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  229. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  230. package/dist/services/delegated-invoker-audit.js +238 -0
  231. package/dist/services/delegated-invoker-audit.js.map +1 -0
  232. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  233. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  234. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  235. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  236. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  237. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  238. package/dist/services/delegated-invoker-janitors.js +104 -0
  239. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  240. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  241. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  242. package/dist/services/delegated-invoker-utils.js +100 -0
  243. package/dist/services/delegated-invoker-utils.js.map +1 -0
  244. package/dist/services/delegated-task-runtime.d.ts +1 -1
  245. package/dist/services/delegated-task-runtime.js +1 -1
  246. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  247. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  248. package/dist/services/integrations/snapshot-partitions.js +12 -0
  249. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  250. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  251. package/dist/services/voice/transcriber-impl.js +46 -0
  252. package/dist/services/voice/transcriber-impl.js.map +1 -1
  253. package/package.json +12 -12
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { serve } from "@hono/node-server";
2
- import { existsSync, readFileSync } from "node:fs";
3
2
  import { join, resolve } from "node:path";
4
3
  import { randomBytes } from "node:crypto";
5
4
  import { loadConfig, getContextDir, isRoadmapStale, mergeRuntimeSettingsFromDb, runVaultHealthProbe, validateExternalObsidianVaultPath, } from "./config.js";
@@ -30,21 +29,14 @@ import { RepositoryManagementCron } from "./observers/repository-management-cron
30
29
  import { ObservationSummarizerWorker, AnthropicSummarizerClient, UnsupportedSummarizerClient, } from "./observers/observation-summarizer/index.js";
31
30
  import { CalendarPoller } from "./observers/calendar-poller.js";
32
31
  import { ImminentEventScheduler } from "./observers/imminent-event-scheduler.js";
33
- import { DelegatedSyncWorker, hasActiveDelegatedSyncIntegration, } from "./observers/delegated-sync-worker.js";
32
+ import { DelegatedSyncWorker, hasActiveCadenceSyncIntegration, } from "./observers/delegated-sync-worker.js";
34
33
  import { applyIntegrationModeChange, shouldStartObserversFor, } from "./core/integration-lifecycle.js";
35
34
  import { NotionPoller } from "./observers/notion-poller.js";
36
35
  import { DiscordAdapter } from "./adapters/discord.js";
37
36
  import { SlackAdapter } from "./adapters/slack-adapter.js";
38
37
  import { TelegramAdapter } from "./adapters/telegram-adapter.js";
39
38
  import { DashboardAdapter } from "./adapters/dashboard-adapter.js";
40
- import { WhatsAppAdapter } from "./adapters/whatsapp-adapter.js";
41
- import { CalendarService } from "./services/calendar.js";
42
- import { AppleCalendarService } from "./services/apple-calendar/index.js";
43
- import { GmailService } from "./services/gmail.js";
44
- import { detectGoogleCredentialType } from "./services/google-auth.js";
45
39
  import { ObsidianService } from "./services/obsidian.js";
46
- import { NotionService } from "./services/notion.js";
47
- import { GitHubService } from "./services/github.js";
48
40
  import { createServiceRegistry } from "./services/service-registry.js";
49
41
  import { SignalDetector } from "./core/signal-detector.js";
50
42
  import { EventDispatcher } from "./core/dispatcher.js";
@@ -69,7 +61,7 @@ import { NotificationManager } from "./adapters/notification-manager.js";
69
61
  import { recordProactiveForwardDeliveries } from "./core/channel-timeline.js";
70
62
  import { continueDashboardSession as continueDashboardSessionFromHistory, endDashboardSession as endDashboardSessionFromChannel, markContextChanged, } from "./core/dashboard-session-controls.js";
71
63
  import { AuditLogger } from "./safety/audit.js";
72
- import { bootstrapManagementMd, migrateLegacyManagementMd, startManagementMdWatcher, } from "./core/management-md.js";
64
+ import { bootstrapManagementMd, startManagementMdWatcher, } from "./core/management-md.js";
73
65
  import { bootstrapManagementRegistry, startManagementRegistryWatcher, } from "./core/management-registry.js";
74
66
  import { bootstrapManagedTaskSeq } from "./db/managed-tasks-store.js";
75
67
  import { startDocsIndexer, } from "./core/docs/indexer.js";
@@ -79,11 +71,10 @@ import { DocsQAAdapter } from "./adapters/docs-qa-adapter.js";
79
71
  import { CompositeDashboardStream } from "./adapters/composite-dashboard-stream.js";
80
72
  import { createApp } from "./api/server.js";
81
73
  import { EventBroadcaster } from "./api/routes/sse.js";
82
- import { APP_NAME, createEvent, EventPriority, formatSqliteDatetime, getAgentDayBoundsUtc, getAgentDayDateStr, getAgentDayProgressMinutes, getBackendIds, nowInTimezone, parseSqliteUtcMs, } from "@aitne/shared";
74
+ import { APP_NAME, EventPriority, getBackendIds, } from "@aitne/shared";
83
75
  import { getOwnerChannel } from "./messaging/owner-channels.js";
84
76
  import { SUPPORTED_MESSAGING_PLATFORMS, } from "./messaging/constants.js";
85
77
  import { AgentWriteTracker } from "./safety/agent-write-tracker.js";
86
- import { discardStalePendingSchedules, hasActionInWindow, recoverOrphanedRunningSchedules, } from "./core/schedule-maintenance.js";
87
78
  import { ContextWriteGate, InMemoryTodayWriteLockManager, MigrationLock, getTodayWriteLockTimeoutMs, } from "./core/today-write-lock.js";
88
79
  import { applyPromptContextStaleness, } from "./core/context-staleness.js";
89
80
  import { InMemoryRoadmapWriteLockManager, getRoadmapWriteLockTimeoutMs, } from "./core/roadmap-write-lock.js";
@@ -103,13 +94,15 @@ import { parseImapAccountSecret } from "./services/mail/imap/app-password.js";
103
94
  import { ICloudImapProvider } from "./services/mail/imap/icloud-provider.js";
104
95
  import { YahooImapProvider } from "./services/mail/imap/yahoo-provider.js";
105
96
  import { GmailProvider } from "./services/mail/gmail/gmail-provider.js";
106
- import { ensureLegacyGmailRow } from "./services/mail/gmail/legacy-row.js";
107
- import { syncLegacyGmailAccountState } from "./services/mail/gmail/legacy-row.js";
108
97
  import { MailPoller } from "./observers/mail-poller.js";
109
98
  import { MailReconciliationJob } from "./observers/mail-reconciliation.js";
110
99
  import { SecretBroker } from "./secrets/secret-broker.js";
111
100
  import { captureOriginalShellEnv, syncBackendApiKeyToEnv, } from "./secrets/backend-api-key-env.js";
112
101
  import { createLogger, toSafeErrorMessage } from "./logging.js";
102
+ import { runCatchup, runPostMessagingCatchup, } from "./bootstrap/catchup.js";
103
+ import { hasFreshAgentDayTodayMd, readSkillCurationCadence, } from "./bootstrap/schedule-helpers.js";
104
+ import { createAdapterReloaders, whatsappQrResponseFromAdapter, } from "./bootstrap/adapters.js";
105
+ import { createInitialSecretState, createServiceReloaders, } from "./bootstrap/services.js";
113
106
  const logger = createLogger("daemon", {
114
107
  transport: {
115
108
  target: "pino-pretty",
@@ -227,7 +220,6 @@ async function startup() {
227
220
  let managementMdWatcher = null;
228
221
  let managementRegistryWatcher = null;
229
222
  try {
230
- migrateLegacyManagementMd(config.dataDir);
231
223
  await bootstrapManagementMd(config.dataDir, db, config.workspaceDir, {
232
224
  externalObsidianVaultPath: config.externalObsidianVaultPath,
233
225
  externalObsidianWatch: config.externalObsidianWatch,
@@ -407,185 +399,34 @@ async function startup() {
407
399
  logger.warn({ err, platform }, "Failed to deliver welcome DM after auto-pairing");
408
400
  }
409
401
  }
410
- let discordAdapter = null;
411
- let slackAdapter = null;
412
- let telegramAdapter = null;
413
- let whatsappAdapter = null;
414
- async function reloadDiscordAdapter(startNow) {
415
- const botToken = await secretBroker.getDiscordBotToken();
416
- const configured = !!botToken;
417
- messageHub.setPlatformConfigured("discord", configured);
418
- if (discordAdapter) {
419
- try {
420
- await discordAdapter.stop();
421
- }
422
- catch (err) {
423
- logger.warn({ err }, "Failed to stop Discord adapter during reload");
424
- }
425
- messageHub.unregister("discord");
426
- discordAdapter = null;
427
- }
428
- if (!botToken) {
429
- return;
430
- }
431
- discordAdapter = new DiscordAdapter({
432
- botToken,
433
- ownerUserId: config.discordOwnerUserId,
434
- onMessage: (event) => void eventBus.put(event),
435
- onOwnerDetected: (userId) => recordDetectedOwner("discord", userId),
436
- attachmentStore,
437
- });
438
- messageHub.register(discordAdapter);
439
- if (startNow) {
440
- // Mark as "connecting" while the websocket handshake is in flight so
441
- // /health doesn't briefly report "ok" before the adapter actually
442
- // comes up (register() defaults to "ok" to keep the notification
443
- // pipeline usable in unit tests; this override is the real state).
444
- messageHub.setPlatformRuntimeStatus("discord", { runtimeState: "connecting", error: null });
445
- try {
446
- await discordAdapter.start();
447
- messageHub.setPlatformRuntimeStatus("discord", { runtimeState: "ok", error: null });
448
- }
449
- catch (err) {
450
- const message = toSafeErrorMessage(err, "Discord adapter failed to start");
451
- messageHub.setPlatformRuntimeStatus("discord", { runtimeState: "error", error: message });
452
- logger.error({ err }, "Failed to start Discord adapter during reload");
453
- }
454
- }
455
- }
456
- async function reloadSlackAdapter(startNow) {
457
- const [botToken, appToken] = await Promise.all([
458
- secretBroker.getSlackBotToken(),
459
- secretBroker.getSlackAppToken(),
460
- ]);
461
- const configured = !!(botToken && appToken);
462
- messageHub.setPlatformConfigured("slack", configured);
463
- if (slackAdapter) {
464
- try {
465
- await slackAdapter.stop();
466
- }
467
- catch (err) {
468
- logger.warn({ err }, "Failed to stop Slack adapter during reload");
469
- }
470
- messageHub.unregister("slack");
471
- slackAdapter = null;
472
- }
473
- if (!botToken || !appToken) {
474
- return;
475
- }
476
- slackAdapter = new SlackAdapter({
477
- botToken,
478
- appToken,
479
- ownerUserId: config.slackOwnerUserId,
480
- onMessage: (event) => void eventBus.put(event),
481
- onOwnerDetected: (userId) => recordDetectedOwner("slack", userId),
482
- attachmentStore,
483
- });
484
- messageHub.register(slackAdapter);
485
- if (startNow) {
486
- messageHub.setPlatformRuntimeStatus("slack", { runtimeState: "connecting", error: null });
487
- try {
488
- await slackAdapter.start();
489
- messageHub.setPlatformRuntimeStatus("slack", { runtimeState: "ok", error: null });
490
- }
491
- catch (err) {
492
- const message = toSafeErrorMessage(err, "Slack adapter failed to start");
493
- messageHub.setPlatformRuntimeStatus("slack", { runtimeState: "error", error: message });
494
- logger.error({ err }, "Failed to start Slack adapter during reload");
495
- }
496
- }
497
- }
498
- async function reloadTelegramAdapter(startNow) {
499
- const botToken = await secretBroker.getTelegramBotToken();
500
- const configured = !!botToken;
501
- messageHub.setPlatformConfigured("telegram", configured);
502
- if (telegramAdapter) {
503
- try {
504
- await telegramAdapter.stop();
505
- }
506
- catch (err) {
507
- logger.warn({ err }, "Failed to stop Telegram adapter during reload");
508
- }
509
- messageHub.unregister("telegram");
510
- telegramAdapter = null;
511
- }
512
- if (!botToken) {
513
- return;
514
- }
515
- telegramAdapter = new TelegramAdapter({
516
- botToken,
517
- ownerChatId: config.telegramOwnerChatId,
518
- onMessage: (event) => void eventBus.put(event),
519
- onOwnerDetected: (chatId) => recordDetectedOwner("telegram", chatId),
520
- attachmentStore,
521
- });
522
- messageHub.register(telegramAdapter);
523
- if (startNow) {
524
- messageHub.setPlatformRuntimeStatus("telegram", { runtimeState: "connecting", error: null });
402
+ const adapterState = {
403
+ discord: null,
404
+ slack: null,
405
+ telegram: null,
406
+ whatsapp: null,
407
+ };
408
+ const { reloadDiscordAdapter, reloadSlackAdapter, reloadTelegramAdapter, buildWhatsAppAdapter, teardownWhatsAppAdapter, enableWhatsAppAdapter, } = createAdapterReloaders({
409
+ config,
410
+ secretBroker,
411
+ messageHub,
412
+ eventBus,
413
+ attachmentStore,
414
+ recordDetectedOwner,
415
+ onWhatsAppLoggedOut: async () => {
525
416
  try {
526
- await telegramAdapter.start();
527
- messageHub.setPlatformRuntimeStatus("telegram", { runtimeState: "ok", error: null });
417
+ await messageHub.sendToUser("WhatsApp session logged out — re-run foreground pairing");
528
418
  }
529
419
  catch (err) {
530
- const message = toSafeErrorMessage(err, "Telegram adapter failed to start");
531
- messageHub.setPlatformRuntimeStatus("telegram", { runtimeState: "error", error: message });
532
- logger.error({ err }, "Failed to start Telegram adapter during reload");
420
+ logger.error({ err }, "Failed to deliver WhatsApp logout notification via fallback channel");
533
421
  }
534
- }
535
- }
422
+ },
423
+ state: adapterState,
424
+ });
536
425
  await Promise.all([
537
426
  reloadDiscordAdapter(false),
538
427
  reloadSlackAdapter(false),
539
428
  reloadTelegramAdapter(false),
540
429
  ]);
541
- /**
542
- * Build (or rebuild) the WhatsApp adapter from current config and register
543
- * it with the MessageHub. Idempotent: returns the existing adapter if config
544
- * is unchanged. Throws if whatsappOwnerPhone is missing.
545
- */
546
- function buildWhatsAppAdapter() {
547
- if (!config.whatsappOwnerPhone) {
548
- throw new Error("Cannot enable WhatsApp: PA_WHATSAPP_OWNER_PHONE is not set");
549
- }
550
- const existing = messageHub.getAdapter("whatsapp");
551
- if (existing && whatsappAdapter && existing === whatsappAdapter) {
552
- return whatsappAdapter;
553
- }
554
- const adapter = new WhatsAppAdapter({
555
- ownerPhone: config.whatsappOwnerPhone,
556
- authDir: config.whatsappAuthDir ?? join(config.dataDir, "whatsapp", "auth"),
557
- onMessage: (event) => void eventBus.put(event),
558
- attachmentStore,
559
- onLoggedOut: async () => {
560
- try {
561
- await messageHub.sendToUser("WhatsApp session logged out — re-run foreground pairing");
562
- }
563
- catch (err) {
564
- logger.error({ err }, "Failed to deliver WhatsApp logout notification via fallback channel");
565
- }
566
- },
567
- });
568
- messageHub.register(adapter);
569
- whatsappAdapter = adapter;
570
- return adapter;
571
- }
572
- /**
573
- * Tear down the WhatsApp adapter completely. Used by the dashboard
574
- * `whatsappEnabled=false` toggle so we don't keep a stale Baileys socket
575
- * around. Logs but does not throw on socket close errors.
576
- */
577
- async function teardownWhatsAppAdapter() {
578
- if (!whatsappAdapter)
579
- return;
580
- try {
581
- await whatsappAdapter.stop();
582
- }
583
- catch (err) {
584
- logger.warn({ err }, "Error stopping WhatsApp adapter during teardown");
585
- }
586
- messageHub.unregister("whatsapp");
587
- whatsappAdapter = null;
588
- }
589
430
  if (config.whatsappEnabled) {
590
431
  if (!config.whatsappOwnerPhone) {
591
432
  throw new Error("PA_WHATSAPP_ENABLED=true but PA_WHATSAPP_OWNER_PHONE is not set");
@@ -637,7 +478,7 @@ async function startup() {
637
478
  },
638
479
  startPairing: async (ttlMs = 5 * 60_000) => {
639
480
  assertAdapterReady("telegram");
640
- const adapter = telegramAdapter;
481
+ const adapter = adapterState.telegram;
641
482
  if (!adapter) {
642
483
  throw new Error("Telegram adapter is not initialized. Save the token and retry.");
643
484
  }
@@ -697,12 +538,12 @@ async function startup() {
697
538
  };
698
539
  },
699
540
  getPairingStatus: () => ({
700
- paired: telegramAdapter?.getOwnerChatId() !== null,
701
- ownerChatId: telegramAdapter?.getOwnerChatId() ?? null,
702
- pairingActive: telegramAdapter?.isPairingActive() ?? false,
541
+ paired: adapterState.telegram?.getOwnerChatId() !== null,
542
+ ownerChatId: adapterState.telegram?.getOwnerChatId() ?? null,
543
+ pairingActive: adapterState.telegram?.isPairingActive() ?? false,
703
544
  }),
704
545
  cancelPairing: () => {
705
- telegramAdapter?.cancelPairing();
546
+ adapterState.telegram?.cancelPairing();
706
547
  },
707
548
  };
708
549
  }
@@ -724,7 +565,7 @@ async function startup() {
724
565
  },
725
566
  startPairing: async (ttlMs = 5 * 60_000) => {
726
567
  assertAdapterReady("slack");
727
- const adapter = slackAdapter;
568
+ const adapter = adapterState.slack;
728
569
  if (!adapter) {
729
570
  throw new Error("Slack adapter is not initialized. Save the tokens and retry.");
730
571
  }
@@ -743,12 +584,12 @@ async function startup() {
743
584
  return { phrase, expiresAt };
744
585
  },
745
586
  cancelPairing: () => {
746
- slackAdapter?.cancelPairing();
587
+ adapterState.slack?.cancelPairing();
747
588
  },
748
589
  getPairingStatus: () => ({
749
- paired: slackAdapter?.getOwnerUserId() !== null,
750
- ownerUserId: slackAdapter?.getOwnerUserId() ?? null,
751
- pairingActive: slackAdapter?.isPairingActive() ?? false,
590
+ paired: adapterState.slack?.getOwnerUserId() !== null,
591
+ ownerUserId: adapterState.slack?.getOwnerUserId() ?? null,
592
+ pairingActive: adapterState.slack?.isPairingActive() ?? false,
752
593
  }),
753
594
  };
754
595
  }
@@ -770,7 +611,7 @@ async function startup() {
770
611
  },
771
612
  startPairing: async (ttlMs = 5 * 60_000) => {
772
613
  assertAdapterReady("discord");
773
- const adapter = discordAdapter;
614
+ const adapter = adapterState.discord;
774
615
  if (!adapter) {
775
616
  throw new Error("Discord adapter is not initialized. Save the token and retry.");
776
617
  }
@@ -784,38 +625,15 @@ async function startup() {
784
625
  return { phrase, expiresAt };
785
626
  },
786
627
  cancelPairing: () => {
787
- discordAdapter?.cancelPairing();
628
+ adapterState.discord?.cancelPairing();
788
629
  },
789
630
  getPairingStatus: () => ({
790
- paired: discordAdapter?.getOwnerUserId() !== null,
791
- ownerUserId: discordAdapter?.getOwnerUserId() ?? null,
792
- pairingActive: discordAdapter?.isPairingActive() ?? false,
631
+ paired: adapterState.discord?.getOwnerUserId() !== null,
632
+ ownerUserId: adapterState.discord?.getOwnerUserId() ?? null,
633
+ pairingActive: adapterState.discord?.isPairingActive() ?? false,
793
634
  }),
794
635
  };
795
636
  }
796
- function whatsappQrResponseFromAdapter(adapter, snapshotOverride) {
797
- if (!adapter) {
798
- return {
799
- dataUrl: null,
800
- payload: null,
801
- generatedAt: null,
802
- expiresAt: null,
803
- state: "not_initialized",
804
- error: "WhatsApp adapter not enabled",
805
- };
806
- }
807
- const snapshot = snapshotOverride !== undefined
808
- ? snapshotOverride
809
- : adapter.getQrSnapshot();
810
- return {
811
- dataUrl: snapshot?.dataUrl ?? null,
812
- payload: snapshot?.payload ?? null,
813
- generatedAt: snapshot?.generatedAt ?? null,
814
- expiresAt: snapshot?.expiresAt ?? null,
815
- state: adapter.getStatus(),
816
- error: adapter.getStatusError(),
817
- };
818
- }
819
637
  // Dashboard adapter — always registered (activates on SSE connect)
820
638
  const dashboardAdapter = new DashboardAdapter((event) => void eventBus.put(event));
821
639
  messageHub.register(dashboardAdapter);
@@ -882,96 +700,14 @@ async function startup() {
882
700
  },
883
701
  },
884
702
  });
885
- const secretState = {
886
- googleCredentialsConfigured: false,
887
- googleTokenConfigured: false,
888
- googleCredentialType: null,
889
- notionConfigured: false,
890
- githubConfigured: false,
891
- githubWebhookConfigured: false,
892
- };
893
- async function refreshGoogleSecretState() {
894
- const [credentialsRaw, tokenRaw] = await Promise.all([
895
- secretBroker.getGoogleCredentialsJson(),
896
- secretBroker.getGoogleTokenJson(),
897
- ]);
898
- secretState.googleCredentialsConfigured = !!credentialsRaw;
899
- secretState.googleTokenConfigured = !!tokenRaw;
900
- secretState.googleCredentialType = detectGoogleCredentialType(credentialsRaw);
901
- }
902
- async function reloadGoogleServices() {
903
- await refreshGoogleSecretState();
904
- services.calendar = null;
905
- services.gmail = null;
906
- delete services.errors.googleCalendar;
907
- delete services.errors.gmail;
908
- if (!secretState.googleCredentialsConfigured) {
909
- if (services.mail) {
910
- syncLegacyGmailAccountState(db, services.mail, {
911
- available: false,
912
- error: "Google credentials are not configured.",
913
- });
914
- }
915
- return;
916
- }
917
- // OAuth2 pre-auth: credentials uploaded but the user has not completed the
918
- // browser flow yet (no token in the keychain). Initializing the services
919
- // would fail with a "missing token" error that the dashboard would then
920
- // render as red "Error" under the Google card — but this is the expected
921
- // mid-setup state, not a failure. Skip init and leave services.errors
922
- // unset so /health reports error: null until the user finishes OAuth or a
923
- // real init error occurs.
924
- const oauth2PreAuth = secretState.googleCredentialType === "oauth2"
925
- && !secretState.googleTokenConfigured;
926
- if (oauth2PreAuth) {
927
- if (services.mail) {
928
- syncLegacyGmailAccountState(db, services.mail, {
929
- available: false,
930
- error: "Awaiting Google OAuth authorization.",
931
- });
932
- }
933
- return;
934
- }
935
- const calendarService = new CalendarService(config, secretBroker);
936
- try {
937
- await calendarService.init();
938
- services.calendar = calendarService;
939
- }
940
- catch (err) {
941
- const msg = err.message;
942
- logger.error({ error: msg }, "Calendar service init failed, continuing without it");
943
- services.errors.googleCalendar = msg;
944
- }
945
- const gmailService = new GmailService(secretBroker);
946
- try {
947
- await gmailService.init();
948
- services.gmail = gmailService;
949
- }
950
- catch (err) {
951
- const msg = err.message;
952
- logger.error({ error: msg }, "Gmail service init failed, continuing without it");
953
- services.errors.gmail = msg;
954
- }
955
- // Ensure the shared-Google-OAuth Gmail identity exists as a unified
956
- // mail account (idempotent; returns `exists` on subsequent boots).
957
- if (services.gmail?.available) {
958
- try {
959
- await ensureLegacyGmailRow(db, services.gmail);
960
- }
961
- catch (err) {
962
- logger.error({ err }, "Failed to ensure shared-Google-OAuth Gmail mail_accounts row");
963
- }
964
- if (services.mail) {
965
- syncLegacyGmailAccountState(db, services.mail, { available: true });
966
- }
967
- }
968
- else if (services.mail) {
969
- syncLegacyGmailAccountState(db, services.mail, {
970
- available: false,
971
- error: services.errors.gmail ?? "Gmail is not configured.",
972
- });
973
- }
974
- }
703
+ const secretState = createInitialSecretState();
704
+ const { reloadGoogleServices, reloadAppleCalendarService, reloadNotionService, reloadGitHubService, } = createServiceReloaders({
705
+ db,
706
+ config,
707
+ secretBroker,
708
+ services,
709
+ secretState,
710
+ });
975
711
  // Google Maps (F-08: commute optimization)
976
712
  {
977
713
  const { GoogleMapsService } = await import("./services/google-maps.js");
@@ -1007,78 +743,6 @@ async function startup() {
1007
743
  else if (config.externalObsidianVaultPath && !config.externalObsidianVaultName) {
1008
744
  services.errors.obsidian = "externalObsidianVaultName is required for the Obsidian CLI service (externalObsidianVaultPath alone enables file watching only)";
1009
745
  }
1010
- async function reloadAppleCalendarService() {
1011
- const raw = await secretBroker.getAppleCalendarCredentialsJson();
1012
- services.appleCalendar = null;
1013
- delete services.errors.appleCalendar;
1014
- if (!raw) {
1015
- return;
1016
- }
1017
- const service = new AppleCalendarService(secretBroker);
1018
- try {
1019
- await service.init();
1020
- if (service.available) {
1021
- services.appleCalendar = service;
1022
- }
1023
- else {
1024
- // Surface the underlying iCloud error verbatim — the dashboard
1025
- // shows it on the Connections card so the user can act
1026
- // (`401 Unauthorized` → regenerate password; network error →
1027
- // retry; etc.). Falls back to a generic placeholder only if
1028
- // init() failed without recording a message.
1029
- services.errors.appleCalendar =
1030
- service.initError
1031
- ?? "Apple Calendar credentials present but iCloud discovery did not return a usable calendar — verify the app-specific password.";
1032
- }
1033
- }
1034
- catch (err) {
1035
- const msg = err.message;
1036
- logger.error({ error: msg }, "Apple Calendar service init failed, continuing without it");
1037
- services.errors.appleCalendar = msg;
1038
- }
1039
- }
1040
- async function reloadNotionService() {
1041
- const apiKey = await secretBroker.getNotionApiKey();
1042
- secretState.notionConfigured = !!apiKey;
1043
- services.notion = null;
1044
- delete services.errors.notion;
1045
- if (!apiKey) {
1046
- return;
1047
- }
1048
- const notionService = new NotionService(config, secretBroker);
1049
- try {
1050
- await notionService.init();
1051
- services.notion = notionService;
1052
- }
1053
- catch (err) {
1054
- const msg = err.message;
1055
- logger.error({ error: msg }, "Notion service init failed, continuing without it");
1056
- services.errors.notion = msg;
1057
- }
1058
- }
1059
- async function reloadGitHubService() {
1060
- const [token, webhookSecret] = await Promise.all([
1061
- secretBroker.getGitHubToken(),
1062
- secretBroker.getGitHubWebhookSecret(),
1063
- ]);
1064
- secretState.githubConfigured = !!token;
1065
- secretState.githubWebhookConfigured = !!webhookSecret;
1066
- services.github = null;
1067
- delete services.errors.github;
1068
- if (!token) {
1069
- return;
1070
- }
1071
- const githubService = new GitHubService(token, webhookSecret);
1072
- try {
1073
- await githubService.init();
1074
- services.github = githubService;
1075
- }
1076
- catch (err) {
1077
- const msg = err.message;
1078
- logger.error({ error: msg }, "GitHub service init failed, continuing without it");
1079
- services.errors.github = msg;
1080
- }
1081
- }
1082
746
  await Promise.all([
1083
747
  reloadGoogleServices(),
1084
748
  reloadAppleCalendarService(),
@@ -1088,7 +752,7 @@ async function startup() {
1088
752
  /** Build integration status snapshot for /api/health */
1089
753
  const getIntegrationStatus = () => {
1090
754
  const whatsappState = config.whatsappEnabled
1091
- ? (whatsappAdapter?.getStatus() ?? "disabled")
755
+ ? (adapterState.whatsapp?.getStatus() ?? "disabled")
1092
756
  : "not_configured";
1093
757
  return {
1094
758
  google: {
@@ -1720,7 +1384,7 @@ async function startup() {
1720
1384
  }
1721
1385
  return delegatedSyncWorker;
1722
1386
  };
1723
- if (hasActiveDelegatedSyncIntegration(db)) {
1387
+ if (hasActiveCadenceSyncIntegration(db)) {
1724
1388
  observerManager.register(buildDelegatedSyncWorker());
1725
1389
  }
1726
1390
  // ── Delegated probe observer (DELEGATED-MODE-V2 §7.1) ──
@@ -2071,17 +1735,7 @@ async function startup() {
2071
1735
  // (same logic as runCatchup, ensures schedule generation after first auth)
2072
1736
  const contextDir = getContextDir(config);
2073
1737
  const todayMdPath = join(contextDir, "today.md");
2074
- let needsMorning = false;
2075
- if (existsSync(todayMdPath)) {
2076
- const firstLine = readFileSync(todayMdPath, "utf-8").split("\n")[0];
2077
- const today = getAgentDayDateStr(config.timezone || undefined, config.dayBoundaryHour);
2078
- if (!firstLine.includes(today)) {
2079
- needsMorning = true;
2080
- }
2081
- }
2082
- else {
2083
- needsMorning = true;
2084
- }
1738
+ const needsMorning = !hasFreshAgentDayTodayMd(todayMdPath, config.timezone || undefined, config.dayBoundaryHour);
2085
1739
  if (needsMorning) {
2086
1740
  // Morning routine's post-completion hook will also check roadmap staleness.
2087
1741
  logger.info("Google services ready — today.md stale, queueing morning_routine wake");
@@ -2433,7 +2087,7 @@ async function startup() {
2433
2087
  auditLogger,
2434
2088
  validateAttachmentTurnToken: (token) => dispatcher.validateAttachmentTurnToken(token),
2435
2089
  whatsappControls: {
2436
- isInitialized: () => whatsappAdapter !== null,
2090
+ isInitialized: () => adapterState.whatsapp !== null,
2437
2091
  enable: async () => {
2438
2092
  const adapter = buildWhatsAppAdapter();
2439
2093
  if (adapter.getStatus() === "disabled") {
@@ -2444,19 +2098,19 @@ async function startup() {
2444
2098
  await teardownWhatsAppAdapter();
2445
2099
  },
2446
2100
  requestQr: async () => {
2447
- if (!whatsappAdapter) {
2448
- await whatsappControls_enable();
2101
+ if (!adapterState.whatsapp) {
2102
+ await enableWhatsAppAdapter();
2449
2103
  }
2450
- await whatsappAdapter.requestQR();
2104
+ await adapterState.whatsapp.requestQR();
2451
2105
  },
2452
2106
  waitForQr: async (timeoutMs = 10_000) => {
2453
- if (!whatsappAdapter) {
2454
- await whatsappControls_enable();
2107
+ if (!adapterState.whatsapp) {
2108
+ await enableWhatsAppAdapter();
2455
2109
  }
2456
- const snapshot = await whatsappAdapter.waitForQr(timeoutMs);
2457
- return whatsappQrResponseFromAdapter(whatsappAdapter, snapshot);
2110
+ const snapshot = await adapterState.whatsapp.waitForQr(timeoutMs);
2111
+ return whatsappQrResponseFromAdapter(adapterState.whatsapp, snapshot);
2458
2112
  },
2459
- getQrResponse: () => whatsappQrResponseFromAdapter(whatsappAdapter),
2113
+ getQrResponse: () => whatsappQrResponseFromAdapter(adapterState.whatsapp),
2460
2114
  },
2461
2115
  messagingControls: {
2462
2116
  telegram: buildTelegramControls(),
@@ -2464,13 +2118,6 @@ async function startup() {
2464
2118
  discord: buildDiscordControls(),
2465
2119
  },
2466
2120
  });
2467
- // Local helper avoiding circular reference inside the object literal above.
2468
- async function whatsappControls_enable() {
2469
- const adapter = buildWhatsAppAdapter();
2470
- if (adapter.getStatus() === "disabled") {
2471
- await adapter.start();
2472
- }
2473
- }
2474
2121
  // Mount /api/docs/* (DOCS_QA_DESIGN.md §10.4 + DOCS_QA_B7_DESIGN.md
2475
2122
  // §S5–S6) after createApp so the indexer handle and the QA SSE
2476
2123
  // adapter can be threaded in without extending ApiDependencies. The
@@ -2486,6 +2133,22 @@ async function startup() {
2486
2133
  fetch: app.fetch,
2487
2134
  hostname: "127.0.0.1",
2488
2135
  port: config.apiPort,
2136
+ // @hono/node-server's getRequestListener defaults to replacing
2137
+ // `globalThis.Request` / `globalThis.Response` with its own lazy
2138
+ // wrapper classes (named `_Request` / `_Response`) for response-
2139
+ // body materialization performance. The wrappers are prototype-
2140
+ // chained to the native classes — fine for Hono's own response
2141
+ // path — but they break `instanceof Response` checks for objects
2142
+ // returned by native `fetch()`, because a native Response's
2143
+ // prototype chain doesn't include `_Response.prototype`. This
2144
+ // makes `@huggingface/transformers`'s `toCacheResponse` check
2145
+ // (`response instanceof Response && response.status === 200`)
2146
+ // evaluate to false on fresh fetches, which silently skips
2147
+ // `cache.put` and then throws "Unable to get model file path or
2148
+ // buffer." Disabling the override keeps the native globals
2149
+ // intact and costs us nothing — we don't construct Hono's
2150
+ // `Response` instances directly anywhere in the daemon.
2151
+ overrideGlobalObjects: false,
2489
2152
  });
2490
2153
  logger.info({ port: config.apiPort }, "API server listening");
2491
2154
  void dispatcher.run(); // Start consuming dashboard events as soon as the API is live
@@ -2706,202 +2369,12 @@ async function startup() {
2706
2369
  }
2707
2370
  logger.info(`${APP_NAME} Daemon ready`);
2708
2371
  }
2709
- async function runCatchup(db, dispatcher, config) {
2710
- // Setup gate — on first boot (before rules/management.md exists) we must
2711
- // NOT run the morning routine. Without user/profile.md / rules/management.md
2712
- // the generated today.md is meaningless AND it fires
2713
- // onPromptContextChanged markActiveDmSessionsStale, which destroys the
2714
- // dashboard setup conversation on the user's next turn. Autonomous
2715
- // catchup will run on the first normal boot *after* setup completes.
2716
- const gateReason = dispatcher.isAutonomousAllowed();
2717
- if (gateReason !== null) {
2718
- logger.info({ reason: gateReason }, "Skipping startup catchup — autonomous work paused for setup");
2719
- return {
2720
- postMessagingRoadmapRefresh: false,
2721
- postMessagingRoutines: [],
2722
- postMessagingHourlyCheck: false,
2723
- };
2724
- }
2725
- const now = new Date();
2726
- const tz = config.timezone || undefined;
2727
- const contextDir = getContextDir(config);
2728
- const todayMdPath = join(contextDir, "today.md");
2729
- const { start: agentDayStartUtc, end: agentDayEndUtc } = getAgentDayBoundsUtc(tz, config.dayBoundaryHour, now);
2730
- const skippedPending = discardStalePendingSchedules(db, agentDayStartUtc);
2731
- if (skippedPending > 0) {
2732
- logger.warn({ count: skippedPending }, "Discarded stale pending schedules at startup");
2733
- }
2734
- const runningRecovery = recoverOrphanedRunningSchedules(db, agentDayStartUtc);
2735
- if (runningRecovery.skipped > 0 || runningRecovery.failed > 0) {
2736
- logger.warn(runningRecovery, "Recovered orphaned running schedules without replay");
2737
- }
2738
- // Check if morning routine is needed
2739
- let needsMorning = false;
2740
- if (existsSync(todayMdPath)) {
2741
- const firstLine = readFileSync(todayMdPath, "utf-8").split("\n")[0];
2742
- const today = getAgentDayDateStr(tz, config.dayBoundaryHour, now);
2743
- if (!firstLine.includes(today)) {
2744
- needsMorning = true;
2745
- }
2746
- }
2747
- else {
2748
- needsMorning = true;
2749
- }
2750
- const dueCatchupRoutines = getDueCatchupRoutines(db, config, agentDayStartUtc, agentDayEndUtc, now);
2751
- const needsHourlyCheckCatchup = shouldCatchUpHourlyCheck(db, config, now);
2752
- let ranMorningCatchup = false;
2753
- if (needsMorning) {
2754
- try {
2755
- await dispatcher.summarizeDmSessions();
2756
- }
2757
- catch (err) {
2758
- logger.error({ err }, "DM summarization catchup failed before morning routine");
2759
- }
2760
- logger.info("Stale today.md detected, running morning_routine catchup inline");
2761
- await dispatcher.processInline({
2762
- ...createEvent({
2763
- type: "routine.morning_routine",
2764
- source: "catchup",
2765
- priority: EventPriority.HIGH,
2766
- data: {
2767
- postCatchupRoutines: dueCatchupRoutines,
2768
- postCatchupHourlyCheck: needsHourlyCheckCatchup,
2769
- deferPostMorningCatchupsUntilStartupReady: true,
2770
- },
2771
- }),
2772
- routine: "morning_routine",
2773
- });
2774
- ranMorningCatchup = true;
2775
- if (!hasFreshAgentDayTodayMd(todayMdPath, tz, config.dayBoundaryHour)) {
2776
- logger.warn("Startup morning catchup did not produce a fresh today.md — deferring remaining catchup work");
2777
- return {
2778
- postMessagingRoadmapRefresh: false,
2779
- postMessagingRoutines: [],
2780
- postMessagingHourlyCheck: false,
2781
- };
2782
- }
2783
- return {
2784
- postMessagingRoadmapRefresh: isRoadmapStale(contextDir),
2785
- postMessagingRoutines: dueCatchupRoutines,
2786
- postMessagingHourlyCheck: needsHourlyCheckCatchup,
2787
- };
2788
- }
2789
- if (!ranMorningCatchup && isRoadmapStale(contextDir)) {
2790
- logger.info("Roadmap stale at startup, running roadmap_refresh catchup inline");
2791
- await processRoutineCatchup(dispatcher, "roadmap_refresh");
2792
- }
2793
- return {
2794
- postMessagingRoadmapRefresh: false,
2795
- postMessagingRoutines: dueCatchupRoutines,
2796
- postMessagingHourlyCheck: needsHourlyCheckCatchup,
2797
- };
2798
- }
2799
- async function runPostMessagingCatchup(dispatcher, catchup) {
2800
- if (catchup.postMessagingRoadmapRefresh) {
2801
- logger.info("Running roadmap_refresh catchup after messaging startup");
2802
- await processRoutineCatchup(dispatcher, "roadmap_refresh");
2803
- }
2804
- for (const routine of catchup.postMessagingRoutines) {
2805
- logger.info({ routine }, "Running same-day routine catchup after messaging startup");
2806
- await processRoutineCatchup(dispatcher, routine);
2807
- }
2808
- if (catchup.postMessagingHourlyCheck) {
2809
- logger.info("Triggering hourly_check catchup after messaging startup");
2810
- await dispatcher.triggerHourlyCheck("catchup_startup", { force: false });
2811
- }
2812
- }
2813
- async function processRoutineCatchup(dispatcher, routine) {
2814
- await dispatcher.processInline({
2815
- ...createEvent({
2816
- type: `routine.${routine}`,
2817
- source: "catchup",
2818
- priority: routine === "hourly_check" ? EventPriority.NORMAL : EventPriority.HIGH,
2819
- }),
2820
- routine,
2821
- });
2822
- }
2823
- function getDueCatchupRoutines(db, config, agentDayStartUtc, agentDayEndUtc, now) {
2824
- const tz = config.timezone || undefined;
2825
- const progressMinutes = getAgentDayProgressMinutes(tz, config.dayBoundaryHour, now);
2826
- const dueAt18 = getProgressMinutesForHour(18, config.dayBoundaryHour);
2827
- if (progressMinutes < dueAt18) {
2828
- return [];
2829
- }
2830
- const routines = [];
2831
- const agentDayStartMs = parseSqliteUtcMs(agentDayStartUtc);
2832
- const agentDayLocal = nowInTimezone(tz, new Date(agentDayStartMs));
2833
- const tomorrowLocal = nowInTimezone(tz, new Date(agentDayStartMs + 24 * 60 * 60 * 1000));
2834
- if (!hasActionInWindow(db, "routine.evening_review", agentDayStartUtc, agentDayEndUtc)) {
2835
- routines.push("evening_review");
2836
- }
2837
- if (agentDayLocal.dayOfWeek === 5 &&
2838
- !hasActionInWindow(db, "routine.weekly_review", agentDayStartUtc, agentDayEndUtc)) {
2839
- routines.push("weekly_review");
2840
- }
2841
- if (tomorrowLocal.day === 1 &&
2842
- !hasActionInWindow(db, "routine.monthly_review", agentDayStartUtc, agentDayEndUtc)) {
2843
- routines.push("monthly_review");
2844
- }
2845
- return routines;
2846
- }
2847
- function shouldCatchUpHourlyCheck(db, config, now) {
2848
- if (!config.hourlyCheckEnabled) {
2849
- return false;
2850
- }
2851
- const tz = config.timezone || undefined;
2852
- const local = nowInTimezone(tz, now);
2853
- if (local.hours < config.hourlyCheckActiveStartHour ||
2854
- local.hours >= config.hourlyCheckActiveEndHour ||
2855
- local.hours === config.dayBoundaryHour) {
2856
- return false;
2857
- }
2858
- // Slot anchors to `activeStartHour`, mirroring shouldFireHourlyTickAt
2859
- // in scheduler.ts so the catch-up function picks the same slot the
2860
- // cron callback would have fired at. The earlier branch already
2861
- // returned false when local.hours < activeStartHour, so the offset is
2862
- // always non-negative here.
2863
- const anchorMinutes = config.hourlyCheckActiveStartHour * 60;
2864
- const offsetFromAnchor = local.hours * 60 + local.minutes - anchorMinutes;
2865
- const slotOffsetFromAnchor = Math.floor(offsetFromAnchor / config.hourlyCheckIntervalMinutes) *
2866
- config.hourlyCheckIntervalMinutes;
2867
- const slotMinutesSinceMidnight = anchorMinutes + slotOffsetFromAnchor;
2868
- const dayStartUtc = getAgentDayBoundsUtc(tz, 0, now).start;
2869
- const slotStartMs = parseSqliteUtcMs(dayStartUtc) + slotMinutesSinceMidnight * 60 * 1000;
2870
- const slotStartUtc = formatSqliteDatetime(new Date(slotStartMs));
2871
- return !hasActionInWindow(db, "routine.hourly_check", slotStartUtc, formatSqliteDatetime(now));
2872
- }
2873
- function getProgressMinutesForHour(hour, dayBoundaryHour) {
2874
- const scheduledMinutes = hour * 60;
2875
- const boundaryMinutes = dayBoundaryHour * 60;
2876
- return scheduledMinutes >= boundaryMinutes
2877
- ? scheduledMinutes - boundaryMinutes
2878
- : 24 * 60 - boundaryMinutes + scheduledMinutes;
2879
- }
2880
- function hasFreshAgentDayTodayMd(todayMdPath, timezone, dayBoundaryHour) {
2881
- if (!existsSync(todayMdPath)) {
2882
- return false;
2883
- }
2884
- const firstLine = readFileSync(todayMdPath, "utf-8").split("\n")[0] ?? "";
2885
- const today = getAgentDayDateStr(timezone, dayBoundaryHour);
2886
- return firstLine.includes(today);
2887
- }
2888
- // P22 — read the operator's chosen cadence for skill curation runs.
2889
- // Mirrors the helper in `core/scheduler.ts` so the dispatcher hook here can
2890
- // resolve cadence at runtime without crossing module boundaries.
2891
- function readSkillCurationCadence(db) {
2892
- const row = db
2893
- .prepare(`SELECT value_json FROM runtime_state WHERE key = 'skill_curation.config'`)
2894
- .get();
2895
- if (!row)
2896
- return "weekly";
2897
- try {
2898
- const v = JSON.parse(row.value_json);
2899
- return v.cadence ?? "weekly";
2900
- }
2901
- catch {
2902
- return "weekly";
2903
- }
2904
- }
2372
+ // Catchup (`runCatchup` / `runPostMessagingCatchup`) and the pure schedule
2373
+ // predicates (`getDueCatchupRoutines`, `shouldCatchUpHourlyCheck`,
2374
+ // `getProgressMinutesForHour`, `hasFreshAgentDayTodayMd`,
2375
+ // `readSkillCurationCadence`) live in `./bootstrap/` see
2376
+ // `docs/design/appendices/file-split-plan.md` §10. Imports are at the top
2377
+ // of this file.
2905
2378
  // ── Global safety net ──
2906
2379
  // Catch unhandled rejections from fire-and-forget patterns (void async calls)
2907
2380
  // so they are logged before Node.js 22+ terminates the process.