@runcore-sh/runcore 0.4.0 → 0.5.1

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 (351) hide show
  1. package/dictionary.json +2 -2
  2. package/dist/activity/log.js +2 -2
  3. package/dist/activity/log.js.map +1 -1
  4. package/dist/agents/governed-spawn.d.ts.map +1 -1
  5. package/dist/cli.js +101 -11
  6. package/dist/cli.js.map +1 -1
  7. package/dist/extensions/cache.d.ts +57 -0
  8. package/dist/extensions/cache.d.ts.map +1 -0
  9. package/dist/extensions/cache.js +173 -0
  10. package/dist/extensions/cache.js.map +1 -0
  11. package/dist/extensions/client.d.ts +55 -0
  12. package/dist/extensions/client.d.ts.map +1 -0
  13. package/dist/extensions/client.js +120 -0
  14. package/dist/extensions/client.js.map +1 -0
  15. package/dist/extensions/index.d.ts +13 -0
  16. package/dist/extensions/index.d.ts.map +1 -0
  17. package/dist/extensions/index.js +12 -0
  18. package/dist/extensions/index.js.map +1 -0
  19. package/dist/extensions/loader.d.ts +50 -0
  20. package/dist/extensions/loader.d.ts.map +1 -0
  21. package/dist/extensions/loader.js +166 -0
  22. package/dist/extensions/loader.js.map +1 -0
  23. package/dist/extensions/manifest.d.ts +38 -0
  24. package/dist/extensions/manifest.d.ts.map +1 -0
  25. package/dist/extensions/manifest.js +17 -0
  26. package/dist/extensions/manifest.js.map +1 -0
  27. package/dist/extensions/stubs.d.ts +27 -0
  28. package/dist/extensions/stubs.d.ts.map +1 -0
  29. package/dist/extensions/stubs.js +45 -0
  30. package/dist/extensions/stubs.js.map +1 -0
  31. package/dist/lib/audit.js +2 -2
  32. package/dist/lib/audit.js.map +1 -1
  33. package/dist/lib/brain-migrate.d.ts +21 -0
  34. package/dist/lib/brain-migrate.d.ts.map +1 -0
  35. package/dist/lib/brain-migrate.js +137 -0
  36. package/dist/lib/brain-migrate.js.map +1 -0
  37. package/dist/lib/paths.d.ts +27 -0
  38. package/dist/lib/paths.d.ts.map +1 -1
  39. package/dist/lib/paths.js +65 -0
  40. package/dist/lib/paths.js.map +1 -1
  41. package/dist/llm/call-log.d.ts +40 -0
  42. package/dist/llm/call-log.d.ts.map +1 -0
  43. package/dist/llm/call-log.js +35 -0
  44. package/dist/llm/call-log.js.map +1 -0
  45. package/dist/llm/complete.d.ts +6 -0
  46. package/dist/llm/complete.d.ts.map +1 -1
  47. package/dist/llm/complete.js +27 -0
  48. package/dist/llm/complete.js.map +1 -1
  49. package/dist/mcp-server.js +118 -2
  50. package/dist/mcp-server.js.map +1 -1
  51. package/dist/memory/file-backed.d.ts +4 -0
  52. package/dist/memory/file-backed.d.ts.map +1 -1
  53. package/dist/memory/file-backed.js +4 -0
  54. package/dist/memory/file-backed.js.map +1 -1
  55. package/dist/memory/vector-index.d.ts +4 -12
  56. package/dist/memory/vector-index.d.ts.map +1 -1
  57. package/dist/memory/vector-index.js +11 -93
  58. package/dist/memory/vector-index.js.map +1 -1
  59. package/dist/search/brain-docs.d.ts +17 -7
  60. package/dist/search/brain-docs.d.ts.map +1 -1
  61. package/dist/search/brain-docs.js +170 -52
  62. package/dist/search/brain-docs.js.map +1 -1
  63. package/dist/search/brain-rag.d.ts +45 -0
  64. package/dist/search/brain-rag.d.ts.map +1 -0
  65. package/dist/search/brain-rag.js +275 -0
  66. package/dist/search/brain-rag.js.map +1 -0
  67. package/dist/search/chunker.d.ts +24 -0
  68. package/dist/search/chunker.d.ts.map +1 -0
  69. package/dist/search/chunker.js +95 -0
  70. package/dist/search/chunker.js.map +1 -0
  71. package/dist/search/embedder.d.ts +16 -0
  72. package/dist/search/embedder.d.ts.map +1 -0
  73. package/dist/search/embedder.js +108 -0
  74. package/dist/search/embedder.js.map +1 -0
  75. package/dist/search/file-watcher.d.ts +11 -0
  76. package/dist/search/file-watcher.d.ts.map +1 -0
  77. package/dist/search/file-watcher.js +86 -0
  78. package/dist/search/file-watcher.js.map +1 -0
  79. package/dist/server.d.ts.map +1 -1
  80. package/dist/server.js +814 -472
  81. package/dist/server.js.map +1 -1
  82. package/dist/sessions/store.d.ts +9 -0
  83. package/dist/sessions/store.d.ts.map +1 -1
  84. package/dist/sessions/store.js.map +1 -1
  85. package/dist/settings.d.ts +26 -0
  86. package/dist/settings.d.ts.map +1 -1
  87. package/dist/settings.js +78 -2
  88. package/dist/settings.js.map +1 -1
  89. package/dist/tracing/init.d.ts +1 -1
  90. package/dist/tracing/init.d.ts.map +1 -1
  91. package/dist/utils/logger.js +2 -2
  92. package/dist/utils/logger.js.map +1 -1
  93. package/module-tiers.json +164 -0
  94. package/package.json +9 -13
  95. package/public/avatar/cache/1184385ec5522b57.mp4 +0 -0
  96. package/public/avatar/cache/1f15f6a1ebd7e439.mp4 +0 -0
  97. package/public/avatar/cache/2c7e47ff0bdeb8d1.mp4 +0 -0
  98. package/public/avatar/cache/5f308566f7abb8f2.mp4 +0 -0
  99. package/public/avatar/cache/62f9cfba848d724e.mp4 +0 -0
  100. package/public/avatar/cache/6d64e657e6bf2aab.mp4 +0 -0
  101. package/public/avatar/cache/763ad0349e0b6f26.mp4 +0 -0
  102. package/public/avatar/cache/81a516cfd461b2b9.mp4 +0 -0
  103. package/public/avatar/cache/9366de15fd6910ca.mp4 +0 -0
  104. package/public/avatar/cache/ade41a846b283895.mp4 +0 -0
  105. package/public/avatar/cache/b6066e5c65383eec.mp4 +0 -0
  106. package/public/avatar/cache/edadb75d37891fc7.mp4 +0 -0
  107. package/public/avatar/cache/f0ae159640621dd9.mp4 +0 -0
  108. package/public/avatar/cache/fc2e5419adf29d96.mp4 +0 -0
  109. package/public/index.html +379 -59
  110. package/dist/agents/autonomous.js +0 -749
  111. package/dist/agents/autonomous.js.map +0 -1
  112. package/dist/agents/commit.js +0 -113
  113. package/dist/agents/commit.js.map +0 -1
  114. package/dist/agents/continue.js +0 -158
  115. package/dist/agents/continue.js.map +0 -1
  116. package/dist/agents/cooldown.js +0 -397
  117. package/dist/agents/cooldown.js.map +0 -1
  118. package/dist/agents/dedup-guard.js +0 -131
  119. package/dist/agents/dedup-guard.js.map +0 -1
  120. package/dist/agents/feed.js +0 -176
  121. package/dist/agents/feed.js.map +0 -1
  122. package/dist/agents/governance.js +0 -292
  123. package/dist/agents/governance.js.map +0 -1
  124. package/dist/agents/governed-spawn.js +0 -192
  125. package/dist/agents/governed-spawn.js.map +0 -1
  126. package/dist/agents/heartbeat.js +0 -324
  127. package/dist/agents/heartbeat.js.map +0 -1
  128. package/dist/agents/instance-manager.js +0 -850
  129. package/dist/agents/instance-manager.js.map +0 -1
  130. package/dist/agents/issue-reporter.js +0 -123
  131. package/dist/agents/issue-reporter.js.map +0 -1
  132. package/dist/agents/issues.js +0 -141
  133. package/dist/agents/issues.js.map +0 -1
  134. package/dist/agents/locks.js +0 -234
  135. package/dist/agents/locks.js.map +0 -1
  136. package/dist/agents/memory.js +0 -93
  137. package/dist/agents/memory.js.map +0 -1
  138. package/dist/agents/monitor.js +0 -235
  139. package/dist/agents/monitor.js.map +0 -1
  140. package/dist/agents/orchestration.js +0 -715
  141. package/dist/agents/orchestration.js.map +0 -1
  142. package/dist/agents/recover.js +0 -166
  143. package/dist/agents/recover.js.map +0 -1
  144. package/dist/agents/reflection.js +0 -199
  145. package/dist/agents/reflection.js.map +0 -1
  146. package/dist/agents/runtime/bus.js +0 -174
  147. package/dist/agents/runtime/bus.js.map +0 -1
  148. package/dist/agents/runtime/config.js +0 -101
  149. package/dist/agents/runtime/config.js.map +0 -1
  150. package/dist/agents/runtime/driver.js +0 -214
  151. package/dist/agents/runtime/driver.js.map +0 -1
  152. package/dist/agents/runtime/errors.js +0 -40
  153. package/dist/agents/runtime/errors.js.map +0 -1
  154. package/dist/agents/runtime/index.js +0 -54
  155. package/dist/agents/runtime/index.js.map +0 -1
  156. package/dist/agents/runtime/lifecycle.js +0 -116
  157. package/dist/agents/runtime/lifecycle.js.map +0 -1
  158. package/dist/agents/runtime/manager.js +0 -948
  159. package/dist/agents/runtime/manager.js.map +0 -1
  160. package/dist/agents/runtime/registry.js +0 -195
  161. package/dist/agents/runtime/registry.js.map +0 -1
  162. package/dist/agents/runtime/resources.js +0 -146
  163. package/dist/agents/runtime/resources.js.map +0 -1
  164. package/dist/agents/runtime/types.js +0 -24
  165. package/dist/agents/runtime/types.js.map +0 -1
  166. package/dist/agents/spawn-policy.js +0 -202
  167. package/dist/agents/spawn-policy.js.map +0 -1
  168. package/dist/agents/spawn.js +0 -970
  169. package/dist/agents/spawn.js.map +0 -1
  170. package/dist/agents/triage.js +0 -81
  171. package/dist/agents/triage.js.map +0 -1
  172. package/dist/agents/workflow.js +0 -543
  173. package/dist/agents/workflow.js.map +0 -1
  174. package/dist/avatar/client.js +0 -172
  175. package/dist/avatar/client.js.map +0 -1
  176. package/dist/avatar/sidecar.js +0 -125
  177. package/dist/avatar/sidecar.js.map +0 -1
  178. package/dist/browser/sessions.js +0 -122
  179. package/dist/browser/sessions.js.map +0 -1
  180. package/dist/capabilities/definitions/browser.js +0 -242
  181. package/dist/capabilities/definitions/browser.js.map +0 -1
  182. package/dist/channels/whatsapp.js +0 -200
  183. package/dist/channels/whatsapp.js.map +0 -1
  184. package/dist/credentials/store.js +0 -189
  185. package/dist/credentials/store.js.map +0 -1
  186. package/dist/files/deep-index.js +0 -337
  187. package/dist/files/deep-index.js.map +0 -1
  188. package/dist/files/extract.js +0 -33
  189. package/dist/files/extract.js.map +0 -1
  190. package/dist/files/gdrive.js +0 -246
  191. package/dist/files/gdrive.js.map +0 -1
  192. package/dist/github/client.js +0 -408
  193. package/dist/github/client.js.map +0 -1
  194. package/dist/github/commit-analysis.js +0 -276
  195. package/dist/github/commit-analysis.js.map +0 -1
  196. package/dist/github/contributor-stats.js +0 -119
  197. package/dist/github/contributor-stats.js.map +0 -1
  198. package/dist/github/issue-sla.js +0 -220
  199. package/dist/github/issue-sla.js.map +0 -1
  200. package/dist/github/issue-triage.js +0 -286
  201. package/dist/github/issue-triage.js.map +0 -1
  202. package/dist/github/pr-readiness.js +0 -197
  203. package/dist/github/pr-readiness.js.map +0 -1
  204. package/dist/github/pr-review.js +0 -410
  205. package/dist/github/pr-review.js.map +0 -1
  206. package/dist/github/release-notes.js +0 -227
  207. package/dist/github/release-notes.js.map +0 -1
  208. package/dist/github/repo-health.js +0 -303
  209. package/dist/github/repo-health.js.map +0 -1
  210. package/dist/github/retry.js +0 -117
  211. package/dist/github/retry.js.map +0 -1
  212. package/dist/github/types.js +0 -8
  213. package/dist/github/types.js.map +0 -1
  214. package/dist/github/webhooks.js +0 -153
  215. package/dist/github/webhooks.js.map +0 -1
  216. package/dist/google/auth.js +0 -325
  217. package/dist/google/auth.js.map +0 -1
  218. package/dist/google/calendar-timer.js +0 -91
  219. package/dist/google/calendar-timer.js.map +0 -1
  220. package/dist/google/calendar.js +0 -270
  221. package/dist/google/calendar.js.map +0 -1
  222. package/dist/google/docs.js +0 -309
  223. package/dist/google/docs.js.map +0 -1
  224. package/dist/google/gmail-send.js +0 -219
  225. package/dist/google/gmail-send.js.map +0 -1
  226. package/dist/google/gmail-timer.js +0 -223
  227. package/dist/google/gmail-timer.js.map +0 -1
  228. package/dist/google/gmail.js +0 -470
  229. package/dist/google/gmail.js.map +0 -1
  230. package/dist/google/plugin.js +0 -169
  231. package/dist/google/plugin.js.map +0 -1
  232. package/dist/google/tasks-timer.js +0 -107
  233. package/dist/google/tasks-timer.js.map +0 -1
  234. package/dist/google/tasks.js +0 -331
  235. package/dist/google/tasks.js.map +0 -1
  236. package/dist/google/temporal.js +0 -176
  237. package/dist/google/temporal.js.map +0 -1
  238. package/dist/integrations/gate.js +0 -100
  239. package/dist/integrations/gate.js.map +0 -1
  240. package/dist/integrations/github.js +0 -331
  241. package/dist/integrations/github.js.map +0 -1
  242. package/dist/integrations/google-tasks.js +0 -432
  243. package/dist/integrations/google-tasks.js.map +0 -1
  244. package/dist/mdns.js +0 -110
  245. package/dist/mdns.js.map +0 -1
  246. package/dist/notifications/channel.js +0 -83
  247. package/dist/notifications/channel.js.map +0 -1
  248. package/dist/notifications/channels/adapter.js +0 -55
  249. package/dist/notifications/channels/adapter.js.map +0 -1
  250. package/dist/notifications/channels/index.js +0 -6
  251. package/dist/notifications/channels/index.js.map +0 -1
  252. package/dist/notifications/channels/log.js +0 -29
  253. package/dist/notifications/channels/log.js.map +0 -1
  254. package/dist/notifications/email.js +0 -72
  255. package/dist/notifications/email.js.map +0 -1
  256. package/dist/notifications/engine.js +0 -198
  257. package/dist/notifications/engine.js.map +0 -1
  258. package/dist/notifications/index.js +0 -24
  259. package/dist/notifications/index.js.map +0 -1
  260. package/dist/notifications/phone.js +0 -48
  261. package/dist/notifications/phone.js.map +0 -1
  262. package/dist/notifications/sms.js +0 -65
  263. package/dist/notifications/sms.js.map +0 -1
  264. package/dist/notifications/types.js +0 -14
  265. package/dist/notifications/types.js.map +0 -1
  266. package/dist/notifications/webhook.js +0 -65
  267. package/dist/notifications/webhook.js.map +0 -1
  268. package/dist/resend/inbox.js +0 -199
  269. package/dist/resend/inbox.js.map +0 -1
  270. package/dist/resend/webhooks.js +0 -244
  271. package/dist/resend/webhooks.js.map +0 -1
  272. package/dist/search/browse.js +0 -225
  273. package/dist/search/browse.js.map +0 -1
  274. package/dist/search/perplexity.js +0 -41
  275. package/dist/search/perplexity.js.map +0 -1
  276. package/dist/slack/channels.js +0 -277
  277. package/dist/slack/channels.js.map +0 -1
  278. package/dist/slack/client.js +0 -468
  279. package/dist/slack/client.js.map +0 -1
  280. package/dist/slack/retry.js +0 -100
  281. package/dist/slack/retry.js.map +0 -1
  282. package/dist/slack/types.js +0 -52
  283. package/dist/slack/types.js.map +0 -1
  284. package/dist/slack/webhooks.js +0 -285
  285. package/dist/slack/webhooks.js.map +0 -1
  286. package/dist/stt/client.js +0 -66
  287. package/dist/stt/client.js.map +0 -1
  288. package/dist/stt/sidecar.js +0 -115
  289. package/dist/stt/sidecar.js.map +0 -1
  290. package/dist/tracing/bridge.js +0 -70
  291. package/dist/tracing/bridge.js.map +0 -1
  292. package/dist/tracing/correlation.js +0 -49
  293. package/dist/tracing/correlation.js.map +0 -1
  294. package/dist/tracing/index.js +0 -18
  295. package/dist/tracing/index.js.map +0 -1
  296. package/dist/tracing/init.js +0 -81
  297. package/dist/tracing/init.js.map +0 -1
  298. package/dist/tracing/instrument.js +0 -145
  299. package/dist/tracing/instrument.js.map +0 -1
  300. package/dist/tracing/middleware.js +0 -69
  301. package/dist/tracing/middleware.js.map +0 -1
  302. package/dist/tracing/tracer.js +0 -327
  303. package/dist/tracing/tracer.js.map +0 -1
  304. package/dist/tts/client.js +0 -48
  305. package/dist/tts/client.js.map +0 -1
  306. package/dist/tts/sidecar.js +0 -148
  307. package/dist/tts/sidecar.js.map +0 -1
  308. package/dist/twilio/call.js +0 -79
  309. package/dist/twilio/call.js.map +0 -1
  310. package/dist/vault/matcher.js +0 -197
  311. package/dist/vault/matcher.js.map +0 -1
  312. package/dist/vault/personal.js +0 -163
  313. package/dist/vault/personal.js.map +0 -1
  314. package/dist/vault/policy.js +0 -159
  315. package/dist/vault/policy.js.map +0 -1
  316. package/dist/vault/store.js +0 -122
  317. package/dist/vault/store.js.map +0 -1
  318. package/dist/vault/transfer.js +0 -188
  319. package/dist/vault/transfer.js.map +0 -1
  320. package/dist/volumes/index.js +0 -2
  321. package/dist/volumes/index.js.map +0 -1
  322. package/dist/volumes/manager.js +0 -462
  323. package/dist/volumes/manager.js.map +0 -1
  324. package/dist/volumes/types.js +0 -8
  325. package/dist/volumes/types.js.map +0 -1
  326. package/dist/webhooks/config.js +0 -214
  327. package/dist/webhooks/config.js.map +0 -1
  328. package/dist/webhooks/event-log.js +0 -132
  329. package/dist/webhooks/event-log.js.map +0 -1
  330. package/dist/webhooks/handler.js +0 -103
  331. package/dist/webhooks/handler.js.map +0 -1
  332. package/dist/webhooks/handlers.js +0 -231
  333. package/dist/webhooks/handlers.js.map +0 -1
  334. package/dist/webhooks/index.js +0 -33
  335. package/dist/webhooks/index.js.map +0 -1
  336. package/dist/webhooks/mount.js +0 -400
  337. package/dist/webhooks/mount.js.map +0 -1
  338. package/dist/webhooks/registry.js +0 -143
  339. package/dist/webhooks/registry.js.map +0 -1
  340. package/dist/webhooks/relay.js +0 -53
  341. package/dist/webhooks/relay.js.map +0 -1
  342. package/dist/webhooks/retry.js +0 -270
  343. package/dist/webhooks/retry.js.map +0 -1
  344. package/dist/webhooks/router.js +0 -290
  345. package/dist/webhooks/router.js.map +0 -1
  346. package/dist/webhooks/twilio.js +0 -129
  347. package/dist/webhooks/twilio.js.map +0 -1
  348. package/dist/webhooks/types.js +0 -8
  349. package/dist/webhooks/types.js.map +0 -1
  350. package/dist/webhooks/verify.js +0 -154
  351. package/dist/webhooks/verify.js.map +0 -1
package/dist/server.js CHANGED
@@ -37,38 +37,47 @@ import { installFetchGuard } from "./llm/fetch-guard.js";
37
37
  import { SensitiveRegistry } from "./llm/sensitive-registry.js";
38
38
  import { PrivacyMembrane } from "./llm/membrane.js";
39
39
  import { setActiveMembrane, getActiveMembrane, rehydrateResponse } from "./llm/redact.js";
40
- import { VolumeManager } from "./volumes/index.js";
40
+ import { logLlmCall } from "./llm/call-log.js";
41
+ // VolumeManager — dynamically imported in start() (EXT-BYOK)
42
+ let _volumes = null;
41
43
  import { loadSettings, getSettings, updateSettings, resolveProvider, resolveChatModel, resolveUtilityModel, getPulseSettings, getMeshConfig, } from "./settings.js";
42
- import { startMdns, stopMdns } from "./mdns.js";
44
+ // mdns dynamically imported in start() (EXT-BYOK)
45
+ let _mdns = null;
43
46
  import { ingestDirectory } from "./files/ingest.js";
44
47
  import { processIngestFolder } from "./files/ingest-folder.js";
45
48
  import { saveSession, loadSession } from "./sessions/store.js";
46
49
  import { extractAndLearn } from "./learning/extractor.js";
47
50
  import { startSidecar, stopSidecar, isSidecarAvailable } from "./search/sidecar.js";
48
51
  import { classifySearchNeed } from "./search/classify.js";
49
- import { findBrainDocument } from "./search/brain-docs.js";
52
+ import { findBrainDocument, setBrainRAG } from "./search/brain-docs.js";
53
+ import { BrainRAG } from "./search/brain-rag.js";
54
+ import { watchBrain } from "./search/file-watcher.js";
55
+ let stopFileWatcher = () => { };
50
56
  import { isSearchAvailable, search } from "./search/client.js";
51
- import { browseUrl, detectUrl } from "./search/browse.js";
52
- import { startTtsSidecar, stopTtsSidecar } from "./tts/sidecar.js";
53
- import { isTtsAvailable, synthesize } from "./tts/client.js";
54
- import { startSttSidecar, stopSttSidecar } from "./stt/sidecar.js";
55
- import { isSttAvailable, transcribe } from "./stt/client.js";
56
- import { startAvatarSidecar, stopAvatarSidecar, isAvatarAvailable } from "./avatar/sidecar.js";
57
- import { preparePhoto, generateVideo, getCachedVideo, cacheVideo, clearVideoCache } from "./avatar/client.js";
58
- import { getTtsConfig, getSttConfig, getAvatarConfig } from "./settings.js";
59
- import { loadVault, listVaultKeys, setVaultKey, deleteVaultKey, getDashReadableVault, getVaultEntries, hydrateEnv as rehydrateVaultEnv } from "./vault/store.js";
60
- import { exportVault, importVault, verifyExport } from "./vault/transfer.js";
61
- import { getIntegrationStatus, isIntegrationEnabled } from "./integrations/gate.js";
62
- import { makeCall } from "./twilio/call.js";
63
- import { isGoogleConfigured, isGoogleAuthenticated, getAuthUrl, exchangeCode, } from "./google/auth.js";
64
- import { isCalendarAvailable, getTodaySchedule, getUpcomingEvents, listEvents, searchEvents, getFreeBusy, createEvent, updateEvent, deleteEvent } from "./google/calendar.js";
65
- import { validateCalendarEntry, getDayOfWeek } from "./google/temporal.js";
66
- import { startCalendarTimer, stopCalendarTimer } from "./google/calendar-timer.js";
67
- import { isGmailAvailable, categorizeMessages, prioritizeInbox, getInboxSummary, markAsRead, markAsUnread, batchMarkAsRead, batchMarkAsUnread } from "./google/gmail.js";
68
- import { startGmailTimer, stopGmailTimer, onDashEmail } from "./google/gmail-timer.js";
69
- import { startTasksTimer, stopTasksTimer } from "./google/tasks-timer.js";
70
- import { sendEmail } from "./google/gmail-send.js";
71
- import { isTasksAvailable, listTaskLists, createTaskList, updateTaskList, deleteTaskList, listTasks, getTask as getGoogleTask, createTask, updateTask, completeTask, uncompleteTask, deleteTask, createRecurringWeeklyTasks, } from "./google/tasks.js";
57
+ // browse, tts, stt, avatar, vault, integrations, twilio — dynamically imported in start() (EXT-BYOK)
58
+ let _browse = null;
59
+ let _ttsSidecar = null;
60
+ let _ttsClient = null;
61
+ let _sttSidecar = null;
62
+ let _sttClient = null;
63
+ let _avatarSidecar = null;
64
+ let _avatarClient = null;
65
+ let _settingsVoice = null;
66
+ let _vaultStore = null;
67
+ let _vaultTransfer = null;
68
+ let _integrationsGate = null;
69
+ let _twilioCall = null;
70
+ // Google workspace dynamically imported in start() (EXT-BYOK)
71
+ let _googleAuth = null;
72
+ let _googleCalendar = null;
73
+ let _googleTemporal = null;
74
+ let _googleCalendarTimer = null;
75
+ let _googleGmail = null;
76
+ let _googleGmailTimer = null;
77
+ let _googleTasksTimer = null;
78
+ let _googleGmailSend = null;
79
+ let _googleDocs = null;
80
+ let _googleTasks = null;
72
81
  import { runGoalCheck } from "./goals/loop.js";
73
82
  import { startGoalTimer, stopGoalTimer } from "./goals/timer.js";
74
83
  import { drainNotifications, pushNotification, initNotifications } from "./goals/notifications.js";
@@ -76,9 +85,10 @@ import { logActivity, getActivities, getActivitiesByIds, generateTraceId } from
76
85
  import { compactHistory } from "./context/compaction.js";
77
86
  import { rateLimit } from "./rate-limit.js";
78
87
  import { initAgents, recoverAndStartMonitor, shutdownAgents, submitTask, getTask, listTasks as listAgentTasks, cancelTask, getTaskOutput, setOnBatchComplete, setAgentPool } from "./agents/index.js";
79
- import { rememberTaskCompletion } from "./agents/memory.js";
80
- import { acquireLocks, releaseLocks, releaseFileLock, forceReleaseLock, listLocks, checkLocks, pruneAllStaleLocks } from "./agents/locks.js";
81
- import { continueAfterBatch, startAutonomousTimer, stopAutonomousTimer, resetContinuation, getAutonomousStatus, triggerPulse } from "./agents/autonomous.js";
88
+ // Agent memory, locks, autonomous — dynamically imported in start() (EXT-SPAWN)
89
+ let _agentMemory = null;
90
+ let _agentLocks = null;
91
+ let _agentAutonomous = null;
82
92
  import { initPressureIntegrator, getPressureIntegrator } from "./pulse/pressure.js";
83
93
  import { startBacklogReviewTimer, stopBacklogReviewTimer, triggerBacklogReview, getLastBacklogReview, isBacklogReviewRunning } from "./services/backlogReview.js";
84
94
  import { startBriefingTimer, stopBriefingTimer, triggerBriefing, getLastBriefing, getLastDeliveryResult, updateBriefingConfig } from "./services/morningBriefing.js";
@@ -86,43 +96,49 @@ import { initTraining, getTrainingProgress } from "./services/training.js";
86
96
  import { startInsightsTimer, stopInsightsTimer, getInsights, getLastInsightRun, triggerInsightAnalysis } from "./services/traceInsights.js";
87
97
  import { startCreditMonitor, stopCreditMonitor, getCreditStatus, triggerCreditCheck } from "./services/credit-monitor.js";
88
98
  import { loadLoops, loadLoopsByState, loadTriads, transitionLoop, startOpenLoopScanner, stopOpenLoopScanner, getResonances, getLastScanRun, triggerOpenLoopScan, triggerResolutionScan, getResolutions, getLastResolutionScanRun, foldBack, } from "./openloop/index.js";
89
- import { createRuntime, getRuntime, shutdownRuntime } from "./agents/runtime/index.js";
90
- import { AgentInstanceManager } from "./agents/instance-manager.js";
91
- import { AgentPool } from "./agents/runtime.js";
92
- import { WorkflowEngine } from "./agents/workflow.js";
99
+ // Agent runtime, instance manager, pool, workflow dynamically imported in start() (EXT-SPAWN)
100
+ let _agentRuntime = null;
101
+ let _agentInstanceManager = null;
102
+ let _agentPoolMod = null;
103
+ let _agentWorkflow = null;
93
104
  import { getBoardProvider, setBoardProvider, isBoardAvailable } from "./board/provider.js";
94
105
  import { QueueBoardProvider } from "./queue/provider.js";
95
106
  import { QUEUE_STATES } from "./queue/types.js";
96
- import { Tracer } from "./tracing/tracer.js";
97
- import { initTracing, shutdownTracing } from "./tracing/init.js";
98
- import { tracingMiddleware } from "./tracing/middleware.js";
99
- import { attachOTelToBus } from "./tracing/bridge.js";
107
+ // Tracing dynamically imported in start() (EXT-HOSTED)
108
+ let _tracingTracer = null;
109
+ let _tracingInit = null;
110
+ let _tracingMiddleware = null;
111
+ let _tracingBridge = null;
100
112
  import { HealthChecker, memoryCheck, eventLoopCheck, availabilityCheck, cpuCheck, diskUsageCheck, diskCheck, queueStoreCheck, agentCapacityCheck, agentHealthCheck, boardCheck, RecoveryManager, sidecarRecovery, AlertManager, defaultAlertConfig, } from "./health/index.js";
101
- import { NotificationDispatcher, EmailChannel, PhoneChannel } from "./notifications/index.js";
113
+ // Notifications dynamically imported in start() (EXT-BYOK)
114
+ let _notifications = null;
102
115
  import { skillRegistry as _skillRegistry } from "./skills/index.js";
103
116
  import { createModuleRegistry, getModuleRegistry } from "./modules/index.js";
104
- import { createCapabilityRegistry, getCapabilityRegistry, calendarCapability, emailCapability, docsCapability, boardCapability, browserCapability, closeBrowser, taskDoneCapability, calendarContextProvider, emailContextProvider, createWebSearchContextProvider, vaultContextProvider } from "./capabilities/index.js";
117
+ import { createCapabilityRegistry, getCapabilityRegistry, calendarCapability, emailCapability, docsCapability, boardCapability, taskDoneCapability, calendarContextProvider, emailContextProvider, createWebSearchContextProvider, vaultContextProvider } from "./capabilities/index.js";
118
+ // browserCapability, closeBrowser — dynamically imported in start() (EXT-HOSTED)
119
+ let _browser = null;
105
120
  import { MetricsStore, startCollector, stopCollector, registerDefaultThresholds, evaluateAlerts, buildDashboard, metricsMiddleware, collectPrometheus, generatePeriodStats, generateComparisonReport, } from "./metrics/index.js";
106
121
  import { startGroomingTimer, stopGroomingTimer } from "./queue/grooming.js";
107
122
  import { createSchedulingStore, getSchedulingStore } from "./scheduling/store.js";
108
123
  import { startSchedulingTimer, stopSchedulingTimer } from "./scheduling/timer.js";
109
124
  import { createContactStore, getContactStore } from "./contacts/store.js";
110
- import { createCredentialStore, getCredentialStore, maskValue } from "./credentials/store.js";
111
- import { verifyWebhookSignature, githubProvider } from "./github/webhooks.js";
112
- import { initGitHub, shutdownGitHub, getGitHubStatus, reviewPullRequest, reviewAndCommentPR, triageGitHubIssue, triageAndLabelIssue, batchTriageIssues, analyzeGitHubCommit, analyzeRecentGitHubCommits, getGitHubRepoHealth, processWebhook as processGitHubWebhook, formatHealthReport, } from "./integrations/github.js";
113
- import { isSlackConfigured, getOAuthUrl as getSlackOAuthUrl, exchangeOAuthCode as exchangeSlackCode, getClient as getSlackClient, } from "./slack/client.js";
114
- import { slackEventsProvider, slackCommandsProvider, slackInteractionsProvider } from "./slack/webhooks.js";
115
- import { listChannels, getChannelInfo, joinChannel, getChannelHistory } from "./slack/channels.js";
116
- // Slack types no longer needed — providers handle their own type mapping
117
- import { getClient as getWhatsAppClient, isWhatsAppConfigured } from "./channels/whatsapp.js";
118
- import { parseFormBody, processIncomingMessage, emptyTwimlResponse, replyTwiml, twilioProvider } from "./webhooks/twilio.js";
119
- import { resendProvider } from "./resend/webhooks.js";
120
- import { handleWhatsAppMessage } from "./services/whatsapp.js";
125
+ // Credentials, GitHub, Slack, WhatsApp, Webhooks dynamically imported in start() (EXT-BYOK)
126
+ let _credentialStore = null;
127
+ let _githubWebhooks = null;
128
+ let _integrationsGithub = null;
129
+ let _slackClient = null;
130
+ let _slackWebhooks = null;
131
+ let _slackChannels = null;
132
+ let _channelsWhatsapp = null;
133
+ let _webhooksTwilio = null;
134
+ let _resendWebhooks = null;
135
+ let _servicesWhatsapp = null;
121
136
  import { initLLMCache, shutdownLLMCache, getCacheDiagnostics } from "./cache/llm-cache.js";
122
- import { mountWebhookAdmin, createWebhookRoute, verifyWebhookRequest } from "./webhooks/mount.js";
123
- import { setProviderConfigs } from "./webhooks/config.js";
124
- import { registerProviders } from "./webhooks/registry.js";
125
- import { verifyRelaySignature } from "./webhooks/relay.js";
137
+ import { createWebhookRoute, verifyWebhookRequest } from "./webhooks/mount.js";
138
+ let _webhooksMount = null;
139
+ let _webhooksConfig = null;
140
+ let _webhooksRegistry = null;
141
+ let _webhooksRelay = null;
126
142
  import { setEncryptionKey } from "./lib/key-store.js";
127
143
  import { FileManager } from "./files/manager.js";
128
144
  import { createLibraryStore } from "./library/store.js";
@@ -159,15 +175,14 @@ health.register("cpu", cpuCheck());
159
175
  health.register("disk", diskCheck(BRAIN_DIR));
160
176
  health.register("disk_usage", diskUsageCheck(BRAIN_DIR));
161
177
  const recovery = new RecoveryManager(health);
162
- // --- Alerting ---
163
- const alertDispatcher = new NotificationDispatcher();
164
- const alertManager = new AlertManager(health, defaultAlertConfig(), alertDispatcher);
178
+ // --- Alerting (initialized in start() after dynamic import) ---
179
+ let alertDispatcher = null;
180
+ let alertManager = null;
165
181
  // --- Metrics ---
166
182
  const metricsStore = new MetricsStore(BRAIN_DIR);
167
183
  registerDefaultThresholds();
168
- // --- Volume manager ---
169
- const volumeManager = new VolumeManager(BRAIN_DIR);
170
- volumeManager.init().catch((err) => log.warn("Volume manager init failed — single-volume mode", { error: String(err) }));
184
+ // --- Volume manager (initialized in start() after dynamic import) ---
185
+ let volumeManager = null;
171
186
  const chatSessions = new Map();
172
187
  const sessionKeys = new Map();
173
188
  let goalTimerStarted = false;
@@ -184,6 +199,41 @@ function getThreadsForSession(sessionId) {
184
199
  function generateThreadId() {
185
200
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
186
201
  }
202
+ /**
203
+ * Switch cs to point at the given thread's history (or back to main).
204
+ * Saves/restores main history transparently.
205
+ */
206
+ function switchSessionThread(cs, threadId, sessionId) {
207
+ const currentThread = cs.activeThreadId;
208
+ if (currentThread === threadId)
209
+ return; // already there
210
+ // Save current history back to wherever it belongs
211
+ if (currentThread) {
212
+ // Currently on a thread — save back to that thread
213
+ const threads = getThreadsForSession(sessionId);
214
+ const t = threads.get(currentThread);
215
+ if (t) {
216
+ t.history = cs.history;
217
+ t.historySummary = cs.historySummary;
218
+ }
219
+ // Restore main
220
+ cs.history = cs.mainHistory;
221
+ cs.historySummary = cs.mainHistorySummary;
222
+ }
223
+ if (threadId) {
224
+ // Switching to a thread — save main, load thread
225
+ const threads = getThreadsForSession(sessionId);
226
+ const t = threads.get(threadId);
227
+ if (t) {
228
+ cs.mainHistory = cs.history;
229
+ cs.mainHistorySummary = cs.historySummary;
230
+ cs.history = t.history;
231
+ cs.historySummary = t.historySummary;
232
+ t.updatedAt = new Date().toISOString();
233
+ }
234
+ }
235
+ cs.activeThreadId = threadId;
236
+ }
187
237
  /** One-time startup token for zero-friction local auth. */
188
238
  let startupToken = null;
189
239
  export function getStartupToken() {
@@ -191,7 +241,7 @@ export function getStartupToken() {
191
241
  }
192
242
  /** Autonomous timer started flag. */
193
243
  let autonomousStarted = false;
194
- const tracer = new Tracer();
244
+ let tracer = null;
195
245
  let instanceManager = null;
196
246
  let agentPool = null;
197
247
  let workflowEngine = null;
@@ -314,20 +364,20 @@ async function getOrCreateChatSession(sessionId, name) {
314
364
  : `You do NOT have web search capability. If ${name} asks you to search or asks about current events, be honest that you can't look things up right now.`,
315
365
  ``,
316
366
  // Google Workspace status (gated on auth, instructions come from registry)
317
- ...(isGoogleAuthenticated()
367
+ ...((_googleAuth?.isGoogleAuthenticated() ?? false)
318
368
  ? [
319
369
  `You have Google Workspace integration. Your available actions are listed below.`,
320
370
  `If Google data appears in your context, use it. It is real, live data from ${name}'s account.`,
321
371
  `You do NOT need to build or implement Google integration — it is already working.`,
322
372
  ``,
323
373
  ]
324
- : isGoogleConfigured()
374
+ : (_googleAuth?.isGoogleConfigured() ?? false)
325
375
  ? [`Google Workspace credentials are configured but not yet authorized. Tell ${name} to click "Connect Google" in settings to complete the setup.`, ``]
326
376
  : [`Google Workspace is not connected. ${name} can add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in vault settings to enable Calendar, Gmail, and Drive access.`, ``]),
327
377
  // Non-Google capability instructions (always injected)
328
378
  ...(getCapabilityRegistry()?.getPromptInstructions({ origin: "chat", name, exclude: ["calendar", "email", "docs"] }) ?? "").split("\n"),
329
379
  // Google capability instructions (only when authenticated)
330
- ...(isGoogleAuthenticated()
380
+ ...((_googleAuth?.isGoogleAuthenticated() ?? false)
331
381
  ? (getCapabilityRegistry()?.getPromptInstructions({ origin: "chat", name, filter: ["calendar", "email", "docs"] }) ?? "").split("\n")
332
382
  : []),
333
383
  ``,
@@ -377,7 +427,7 @@ async function getOrCreateChatSession(sessionId, name) {
377
427
  ] : []), // end spawning gate
378
428
  // Inject instance-readable vault values (CORE_*/DASH_* prefixed only — never secrets)
379
429
  ...(() => {
380
- const readable = getDashReadableVault();
430
+ const readable = (_vaultStore ? _vaultStore.getDashReadableVault() : []);
381
431
  if (readable.length === 0)
382
432
  return [];
383
433
  const lines = readable.map((r) => `- ${r.name}: ${r.value}`);
@@ -430,12 +480,26 @@ async function getOrCreateChatSession(sessionId, name) {
430
480
  if (key) {
431
481
  const restored = await loadSession(sessionId, key);
432
482
  if (restored) {
433
- cs = { history: restored.history, historySummary: restored.historySummary ?? "", brain, fileContext: restored.fileContext, learnedPaths: restored.learnedPaths, ingestedContext, turnCount: 0, lastExtractionTurn: 0, foldedBack: false };
483
+ cs = { history: restored.history, historySummary: restored.historySummary ?? "", brain, fileContext: restored.fileContext, learnedPaths: restored.learnedPaths, ingestedContext, turnCount: 0, lastExtractionTurn: 0, foldedBack: false, activeThreadId: null, mainHistory: [], mainHistorySummary: "" };
434
484
  chatSessions.set(sessionId, cs);
485
+ // Restore threads
486
+ if (restored.threads && restored.threads.length > 0) {
487
+ const threads = getThreadsForSession(sessionId);
488
+ for (const t of restored.threads) {
489
+ threads.set(t.id, {
490
+ id: t.id,
491
+ title: t.title,
492
+ history: t.history || [],
493
+ historySummary: t.historySummary || "",
494
+ createdAt: t.createdAt,
495
+ updatedAt: t.updatedAt,
496
+ });
497
+ }
498
+ }
435
499
  return cs;
436
500
  }
437
501
  }
438
- cs = { history: [], historySummary: "", brain, fileContext: "", learnedPaths: [], ingestedContext, turnCount: 0, lastExtractionTurn: 0, foldedBack: false };
502
+ cs = { history: [], historySummary: "", brain, fileContext: "", learnedPaths: [], ingestedContext, turnCount: 0, lastExtractionTurn: 0, foldedBack: false, activeThreadId: null, mainHistory: [], mainHistorySummary: "" };
439
503
  chatSessions.set(sessionId, cs);
440
504
  return cs;
441
505
  }
@@ -496,8 +560,13 @@ app.use("/api/*", async (c, next) => {
496
560
  const caller = `http:${c.req.method} ${c.req.path}`;
497
561
  return runWithAuditContext({ caller, channel: "http" }, () => next());
498
562
  });
499
- // --- Tracing middleware ---
500
- app.use("/api/*", tracingMiddleware());
563
+ // --- Tracing middleware (lazy — only active when hosted tier loads tracing) ---
564
+ app.use("/api/*", async (c, next) => {
565
+ if (_tracingMiddleware) {
566
+ return _tracingMiddleware.tracingMiddleware()(c, next);
567
+ }
568
+ return next();
569
+ });
501
570
  // --- Metrics middleware ---
502
571
  app.use("/api/*", metricsMiddleware());
503
572
  // --- Rate limiting ---
@@ -526,35 +595,48 @@ app.use("/api/*", postureTracker());
526
595
  app.use("/api/*", postureHeader());
527
596
  // --- Webhook initialization (batch registration + config + admin routes) ---
528
597
  const webhookInitStart = performance.now();
529
- // Phase 1: Batch-register all webhook providers (deferred from module imports to avoid
530
- // 5 individual logActivity calls during startup now a single batch call).
531
- const registerStart = performance.now();
532
- registerProviders([githubProvider, slackEventsProvider, slackCommandsProvider, slackInteractionsProvider, twilioProvider, resendProvider]);
533
- const registerMs = performance.now() - registerStart;
534
- // Phase 2: Configure webhook providers (secrets resolved from env vars)
535
- const configStart = performance.now();
536
- setProviderConfigs([
537
- { name: "slack-events", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/events" },
538
- { name: "slack-commands", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/commands" },
539
- { name: "slack-interactions", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/interactions" },
540
- { name: "twilio", secret: "TWILIO_AUTH_TOKEN", signatureHeader: "x-twilio-signature", algorithm: "twilio", path: "/api/twilio/whatsapp" },
541
- { name: "github", secret: "GITHUB_WEBHOOK_SECRET", signatureHeader: "x-hub-signature-256", algorithm: "hmac-sha256-hex", path: "/api/github/webhooks" },
542
- { name: "resend", secret: "RESEND_WEBHOOK_SECRET", signatureHeader: "svix-signature", algorithm: "custom", path: "/api/resend/webhooks" },
543
- ]);
544
- const configMs = performance.now() - configStart;
545
- // Phase 3: Mount admin routes
546
- const mountStart = performance.now();
547
- mountWebhookAdmin(app);
548
- const mountMs = performance.now() - mountStart;
549
- const webhookInitMs = performance.now() - webhookInitStart;
550
- log.debug(`Webhook init complete: ${webhookInitMs.toFixed(1)}ms — register:${registerMs.toFixed(1)}ms, config:${configMs.toFixed(1)}ms, mount:${mountMs.toFixed(1)}ms`, { durationMs: Math.round(webhookInitMs), registerMs: Math.round(registerMs), configMs: Math.round(configMs), mountMs: Math.round(mountMs) });
551
- if (webhookInitMs > 100) {
552
- logActivity({
553
- source: "system",
554
- summary: `[perf] Webhook init slow: ${webhookInitMs.toFixed(1)}ms register:${registerMs.toFixed(1)}ms, config:${configMs.toFixed(1)}ms, mount:${mountMs.toFixed(1)}ms`,
555
- });
598
+ // Phase 1-3: Webhook provider registration is deferred to start() where modules are loaded.
599
+ // initWebhookProviders() is called in start() after dynamic imports complete.
600
+ function initWebhookProviders() {
601
+ if (!_webhooksRegistry || !_webhooksConfig || !_webhooksMount || !_githubWebhooks || !_slackWebhooks || !_webhooksTwilio || !_resendWebhooks) {
602
+ log.debug("Webhook init skipped BYOK modules not loaded");
603
+ return;
604
+ }
605
+ const registerStart = performance.now();
606
+ _webhooksRegistry.registerProviders([_githubWebhooks.githubProvider, _slackWebhooks.slackEventsProvider, _slackWebhooks.slackCommandsProvider, _slackWebhooks.slackInteractionsProvider, _webhooksTwilio.twilioProvider, _resendWebhooks.resendProvider]);
607
+ const registerMs = performance.now() - registerStart;
608
+ const configStart = performance.now();
609
+ _webhooksConfig.setProviderConfigs([
610
+ { name: "slack-events", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/events" },
611
+ { name: "slack-commands", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/commands" },
612
+ { name: "slack-interactions", secret: "SLACK_SIGNING_SECRET", signatureHeader: "x-slack-signature", algorithm: "slack-v0", path: "/api/slack/interactions" },
613
+ { name: "twilio", secret: "TWILIO_AUTH_TOKEN", signatureHeader: "x-twilio-signature", algorithm: "twilio", path: "/api/twilio/whatsapp" },
614
+ { name: "github", secret: "GITHUB_WEBHOOK_SECRET", signatureHeader: "x-hub-signature-256", algorithm: "hmac-sha256-hex", path: "/api/github/webhooks" },
615
+ { name: "resend", secret: "RESEND_WEBHOOK_SECRET", signatureHeader: "svix-signature", algorithm: "custom", path: "/api/resend/webhooks" },
616
+ ]);
617
+ const configMs = performance.now() - configStart;
618
+ const mountStart = performance.now();
619
+ _webhooksMount.mountWebhookAdmin(app);
620
+ const mountMs = performance.now() - mountStart;
621
+ const webhookInitMs = performance.now() - webhookInitStart;
622
+ log.debug(`Webhook init complete: ${webhookInitMs.toFixed(1)}ms — register:${registerMs.toFixed(1)}ms, config:${configMs.toFixed(1)}ms, mount:${mountMs.toFixed(1)}ms`, { durationMs: Math.round(webhookInitMs), registerMs: Math.round(registerMs), configMs: Math.round(configMs), mountMs: Math.round(mountMs) });
623
+ if (webhookInitMs > 100) {
624
+ logActivity({
625
+ source: "system",
626
+ summary: `[perf] Webhook init slow: ${webhookInitMs.toFixed(1)}ms — register:${registerMs.toFixed(1)}ms, config:${configMs.toFixed(1)}ms, mount:${mountMs.toFixed(1)}ms`,
627
+ });
628
+ }
556
629
  }
557
630
  // --- API routes ---
631
+ // UI version check (stub — UI polls this to detect hot-reload)
632
+ app.get("/api/ui-version", (c) => c.json({ version: "0.4.0" }));
633
+ // Return current seal values so client can blur on page load
634
+ app.get("/api/sensitive/seals", (c) => {
635
+ const membrane = getActiveMembrane();
636
+ return c.json({ values: membrane ? membrane.knownValues.map(v => v.value) : [] });
637
+ });
638
+ // Pending questions (stub — proactive question chips)
639
+ app.get("/api/pending-questions", (c) => c.json({ questions: [] }));
558
640
  // Status: what screen should the UI show?
559
641
  app.get("/api/status", async (c) => {
560
642
  const status = await getStatus();
@@ -566,9 +648,9 @@ app.get("/api/status", async (c) => {
566
648
  privateMode: settings.privateMode,
567
649
  authMode: settings.safeWordMode,
568
650
  search: isSearchAvailable(),
569
- tts: isTtsAvailable(),
570
- stt: isSttAvailable(),
571
- avatar: isAvatarAvailable(),
651
+ tts: (_ttsClient?.isTtsAvailable() ?? false),
652
+ stt: (_sttClient?.isSttAvailable() ?? false),
653
+ avatar: (_avatarSidecar?.isAvatarAvailable() ?? false),
572
654
  agentName: getInstanceName(),
573
655
  });
574
656
  });
@@ -601,7 +683,8 @@ app.post("/api/pair", async (c) => {
601
683
  sessionKeys.set(result.session.id, result.sessionKey);
602
684
  setEncryptionKey(result.sessionKey);
603
685
  cacheSessionKey(result.sessionKey);
604
- await loadVault(result.sessionKey);
686
+ if (_vaultStore)
687
+ await _vaultStore.loadVault(result.sessionKey);
605
688
  // Save agent name to settings + update in-memory name
606
689
  if (agentName) {
607
690
  setInstanceName(agentName);
@@ -633,7 +716,8 @@ app.post("/api/auth", async (c) => {
633
716
  sessionKeys.set(result.session.id, result.sessionKey);
634
717
  setEncryptionKey(result.sessionKey);
635
718
  cacheSessionKey(result.sessionKey);
636
- await loadVault(result.sessionKey);
719
+ if (_vaultStore)
720
+ await _vaultStore.loadVault(result.sessionKey);
637
721
  return c.json({ sessionId: result.session.id, name: result.name });
638
722
  });
639
723
  // Validate an existing session (for "restart" auth mode)
@@ -692,7 +776,8 @@ app.post("/api/recover", async (c) => {
692
776
  sessionKeys.set(result.session.id, result.sessionKey);
693
777
  setEncryptionKey(result.sessionKey);
694
778
  cacheSessionKey(result.sessionKey);
695
- await loadVault(result.sessionKey);
779
+ if (_vaultStore)
780
+ await _vaultStore.loadVault(result.sessionKey);
696
781
  return c.json({ sessionId: result.session.id, name: result.name });
697
782
  });
698
783
  const deviceVouchers = new Map();
@@ -1076,7 +1161,7 @@ app.get("/api/vault", async (c) => {
1076
1161
  const session = validateSession(sessionId);
1077
1162
  if (!session)
1078
1163
  return c.json({ error: "Invalid or expired session" }, 401);
1079
- return c.json({ keys: listVaultKeys() });
1164
+ return c.json({ keys: (_vaultStore ? _vaultStore.listVaultKeys() : []) });
1080
1165
  });
1081
1166
  // Add or update a vault key
1082
1167
  app.put("/api/vault/:name", async (c) => {
@@ -1094,7 +1179,7 @@ app.put("/api/vault/:name", async (c) => {
1094
1179
  const { value, label } = body;
1095
1180
  if (!value)
1096
1181
  return c.json({ error: "value required" }, 400);
1097
- await setVaultKey(name, value, key, label);
1182
+ await _vaultStore.setVaultKey(name, value, key, label);
1098
1183
  return c.json({ ok: true });
1099
1184
  });
1100
1185
  // Delete a vault key
@@ -1109,7 +1194,7 @@ app.delete("/api/vault/:name", async (c) => {
1109
1194
  if (!key)
1110
1195
  return c.json({ error: "Session key not found" }, 401);
1111
1196
  const name = c.req.param("name");
1112
- await deleteVaultKey(name, key);
1197
+ await _vaultStore.deleteVaultKey(name, key);
1113
1198
  return c.json({ ok: true });
1114
1199
  });
1115
1200
  // Export vault to portable passphrase-encrypted file
@@ -1125,7 +1210,7 @@ app.post("/api/vault/export", async (c) => {
1125
1210
  return c.json({ error: "Passphrase required (min 8 characters)" }, 400);
1126
1211
  }
1127
1212
  try {
1128
- const result = await exportVault(body.passphrase);
1213
+ const result = await _vaultTransfer.exportVault(body.passphrase);
1129
1214
  return c.json({ ok: true, filePath: result.filePath, stats: result.stats });
1130
1215
  }
1131
1216
  catch (e) {
@@ -1149,7 +1234,7 @@ app.post("/api/vault/import", async (c) => {
1149
1234
  }
1150
1235
  const strategy = body.strategy ?? "skip";
1151
1236
  try {
1152
- const result = await importVault(body.filePath, body.passphrase, strategy, key);
1237
+ const result = await _vaultTransfer.importVault(body.filePath, body.passphrase, strategy, key);
1153
1238
  return c.json({ ok: true, stats: result.stats });
1154
1239
  }
1155
1240
  catch (e) {
@@ -1169,7 +1254,7 @@ app.post("/api/vault/verify-export", async (c) => {
1169
1254
  return c.json({ error: "filePath and passphrase required" }, 400);
1170
1255
  }
1171
1256
  try {
1172
- const result = await verifyExport(body.filePath, body.passphrase);
1257
+ const result = await _vaultTransfer.verifyExport(body.filePath, body.passphrase);
1173
1258
  return c.json({ ok: true, message: result.message, stats: result.stats });
1174
1259
  }
1175
1260
  catch (e) {
@@ -1181,7 +1266,7 @@ app.post("/api/vault/verify-export", async (c) => {
1181
1266
  // No session required: this just redirects to Google, no sensitive data returned
1182
1267
  app.get("/api/google/auth", async (c) => {
1183
1268
  const redirectUri = `http://localhost:${PORT}/api/google/callback`;
1184
- const result = getAuthUrl(redirectUri);
1269
+ const result = _googleAuth.getAuthUrl(redirectUri);
1185
1270
  if (!result.ok) {
1186
1271
  return c.html(`<html><body><h2>Google OAuth not configured</h2><p>${result.message}</p></body></html>`);
1187
1272
  }
@@ -1198,7 +1283,7 @@ app.get("/api/google/callback", async (c) => {
1198
1283
  return c.html(`<html><body><h2>Missing authorization code</h2><p>No code received from Google.</p></body></html>`);
1199
1284
  }
1200
1285
  const redirectUri = `http://localhost:${PORT}/api/google/callback`;
1201
- const result = await exchangeCode(code, redirectUri);
1286
+ const result = await _googleAuth.exchangeCode(code, redirectUri);
1202
1287
  if (!result.ok) {
1203
1288
  return c.html(`<html><body><h2>Token exchange failed</h2><p>${result.message}</p></body></html>`);
1204
1289
  }
@@ -1214,26 +1299,27 @@ app.get("/api/google/callback", async (c) => {
1214
1299
  vaultKey = restored.sessionKey;
1215
1300
  sessionKeys.set(restored.session.id, restored.sessionKey);
1216
1301
  setEncryptionKey(restored.sessionKey);
1217
- await loadVault(restored.sessionKey);
1302
+ if (_vaultStore)
1303
+ await _vaultStore.loadVault(restored.sessionKey);
1218
1304
  }
1219
1305
  }
1220
1306
  if (!vaultKey) {
1221
1307
  return c.html(`<html><body><h2>Session not found</h2><p>Could not find a session key to store credentials. Please log in first, then try connecting Google again.</p></body></html>`);
1222
1308
  }
1223
- await setVaultKey("GOOGLE_REFRESH_TOKEN", result.refreshToken, vaultKey, "Google OAuth refresh token");
1309
+ await _vaultStore.setVaultKey("GOOGLE_REFRESH_TOKEN", result.refreshToken, vaultKey, "Google OAuth refresh token");
1224
1310
  logActivity({ source: "google", summary: "Google OAuth connected — refresh token stored in vault", actionLabel: "PROMPTED", reason: "user connected Google OAuth" });
1225
1311
  // Start Google polling timers now that Google is connected
1226
- startCalendarTimer();
1227
- startGmailTimer();
1228
- startTasksTimer();
1312
+ _googleCalendarTimer?.startCalendarTimer();
1313
+ _googleGmailTimer?.startGmailTimer();
1314
+ _googleTasksTimer?.startTasksTimer();
1229
1315
  return c.html(`<html><body><h2>Google connected!</h2><p>${getInstanceName()} now has access to Calendar, Gmail, and Drive.</p><p>This tab will close automatically.</p><script>if(window.opener){window.opener.postMessage("google-connected","*")}setTimeout(()=>window.close(),1500)</script></body></html>`);
1230
1316
  });
1231
1317
  // Check Google auth state (public — only returns booleans, no sensitive data)
1232
1318
  app.get("/api/google/status", async (c) => {
1233
1319
  return c.json({
1234
- configured: isGoogleConfigured(),
1235
- authenticated: isGoogleAuthenticated(),
1236
- scopes: isGoogleAuthenticated() ? ["gmail.modify", "calendar.events", "drive.file", "tasks"] : [],
1320
+ configured: (_googleAuth?.isGoogleConfigured() ?? false),
1321
+ authenticated: (_googleAuth?.isGoogleAuthenticated() ?? false),
1322
+ scopes: (_googleAuth?.isGoogleAuthenticated() ?? false) ? ["gmail.modify", "calendar.events", "drive.file", "tasks"] : [],
1237
1323
  });
1238
1324
  });
1239
1325
  // Send email via Gmail
@@ -1244,14 +1330,14 @@ app.post("/api/google/send-email", async (c) => {
1244
1330
  const session = validateSession(sessionId);
1245
1331
  if (!session)
1246
1332
  return c.json({ error: "Invalid or expired session" }, 401);
1247
- if (!isGoogleAuthenticated()) {
1333
+ if (!(_googleAuth?.isGoogleAuthenticated() ?? false)) {
1248
1334
  return c.json({ error: "Google not authenticated" }, 400);
1249
1335
  }
1250
1336
  const body = await c.req.json();
1251
1337
  if (!body.to || !body.subject || !body.body) {
1252
1338
  return c.json({ error: "to, subject, and body are required" }, 400);
1253
1339
  }
1254
- const result = await sendEmail(body);
1340
+ const result = await _googleGmailSend.sendEmail(body);
1255
1341
  if (!result.ok)
1256
1342
  return c.json({ error: result.message }, 500);
1257
1343
  logActivity({ source: "gmail", summary: `Email sent to ${Array.isArray(body.to) ? body.to.join(", ") : body.to}: "${body.subject}"`, actionLabel: "PROMPTED", reason: "user sent email" });
@@ -1266,10 +1352,10 @@ app.get("/api/google/gmail/inbox-summary", async (c) => {
1266
1352
  const session = validateSession(sessionId);
1267
1353
  if (!session)
1268
1354
  return c.json({ error: "Invalid or expired session" }, 401);
1269
- if (!isGmailAvailable())
1355
+ if (!(_googleGmail?.isGmailAvailable() ?? false))
1270
1356
  return c.json({ error: "Google not authenticated" }, 400);
1271
1357
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1272
- const result = await getInboxSummary(hours);
1358
+ const result = await _googleGmail.getInboxSummary(hours);
1273
1359
  if (!result.ok)
1274
1360
  return c.json({ error: result.message }, 500);
1275
1361
  return c.json(result);
@@ -1282,10 +1368,10 @@ app.get("/api/google/gmail/categorize", async (c) => {
1282
1368
  const session = validateSession(sessionId);
1283
1369
  if (!session)
1284
1370
  return c.json({ error: "Invalid or expired session" }, 401);
1285
- if (!isGmailAvailable())
1371
+ if (!(_googleGmail?.isGmailAvailable() ?? false))
1286
1372
  return c.json({ error: "Google not authenticated" }, 400);
1287
1373
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1288
- const result = await categorizeMessages(hours);
1374
+ const result = await _googleGmail.categorizeMessages(hours);
1289
1375
  if (!result.ok)
1290
1376
  return c.json({ error: result.message }, 500);
1291
1377
  return c.json(result);
@@ -1298,10 +1384,10 @@ app.get("/api/google/gmail/prioritize", async (c) => {
1298
1384
  const session = validateSession(sessionId);
1299
1385
  if (!session)
1300
1386
  return c.json({ error: "Invalid or expired session" }, 401);
1301
- if (!isGmailAvailable())
1387
+ if (!(_googleGmail?.isGmailAvailable() ?? false))
1302
1388
  return c.json({ error: "Google not authenticated" }, 400);
1303
1389
  const hours = parseInt(c.req.query("hours") ?? "24", 10);
1304
- const result = await prioritizeInbox(hours);
1390
+ const result = await _googleGmail.prioritizeInbox(hours);
1305
1391
  if (!result.ok)
1306
1392
  return c.json({ error: result.message }, 500);
1307
1393
  return c.json(result);
@@ -1314,18 +1400,18 @@ app.post("/api/google/gmail/mark-read", async (c) => {
1314
1400
  const session = validateSession(sessionId);
1315
1401
  if (!session)
1316
1402
  return c.json({ error: "Invalid or expired session" }, 401);
1317
- if (!isGmailAvailable())
1403
+ if (!(_googleGmail?.isGmailAvailable() ?? false))
1318
1404
  return c.json({ error: "Google not authenticated" }, 400);
1319
1405
  const body = await c.req.json();
1320
1406
  if (body.messageIds && body.messageIds.length > 0) {
1321
- const result = await batchMarkAsRead(body.messageIds);
1407
+ const result = await _googleGmail.batchMarkAsRead(body.messageIds);
1322
1408
  if (!result.ok)
1323
1409
  return c.json({ error: result.message }, 500);
1324
1410
  return c.json(result);
1325
1411
  }
1326
1412
  if (!body.messageId)
1327
1413
  return c.json({ error: "messageId or messageIds required" }, 400);
1328
- const result = await markAsRead(body.messageId);
1414
+ const result = await _googleGmail.markAsRead(body.messageId);
1329
1415
  if (!result.ok)
1330
1416
  return c.json({ error: result.message }, 500);
1331
1417
  return c.json(result);
@@ -1338,18 +1424,18 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
1338
1424
  const session = validateSession(sessionId);
1339
1425
  if (!session)
1340
1426
  return c.json({ error: "Invalid or expired session" }, 401);
1341
- if (!isGmailAvailable())
1427
+ if (!(_googleGmail?.isGmailAvailable() ?? false))
1342
1428
  return c.json({ error: "Google not authenticated" }, 400);
1343
1429
  const body = await c.req.json();
1344
1430
  if (body.messageIds && body.messageIds.length > 0) {
1345
- const result = await batchMarkAsUnread(body.messageIds);
1431
+ const result = await _googleGmail.batchMarkAsUnread(body.messageIds);
1346
1432
  if (!result.ok)
1347
1433
  return c.json({ error: result.message }, 500);
1348
1434
  return c.json(result);
1349
1435
  }
1350
1436
  if (!body.messageId)
1351
1437
  return c.json({ error: "messageId or messageIds required" }, 400);
1352
- const result = await markAsUnread(body.messageId);
1438
+ const result = await _googleGmail.markAsUnread(body.messageId);
1353
1439
  if (!result.ok)
1354
1440
  return c.json({ error: result.message }, 500);
1355
1441
  return c.json(result);
@@ -1357,53 +1443,53 @@ app.post("/api/google/gmail/mark-unread", async (c) => {
1357
1443
  // --- Google Calendar routes ---
1358
1444
  // Get today's schedule
1359
1445
  app.get("/api/google/calendar/today", async (c) => {
1360
- if (!isCalendarAvailable())
1446
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1361
1447
  return c.json({ error: "Google not authenticated" }, 400);
1362
- const result = await getTodaySchedule();
1448
+ const result = await _googleCalendar.getTodaySchedule();
1363
1449
  if (!result.ok)
1364
1450
  return c.json({ error: result.message }, 500);
1365
1451
  return c.json(result);
1366
1452
  });
1367
1453
  // Get upcoming events
1368
1454
  app.get("/api/google/calendar/upcoming", async (c) => {
1369
- if (!isCalendarAvailable())
1455
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1370
1456
  return c.json({ error: "Google not authenticated" }, 400);
1371
1457
  const hours = parseInt(c.req.query("hours") ?? "4", 10);
1372
- const result = await getUpcomingEvents(hours);
1458
+ const result = await _googleCalendar.getUpcomingEvents(hours);
1373
1459
  if (!result.ok)
1374
1460
  return c.json({ error: result.message }, 500);
1375
1461
  return c.json(result);
1376
1462
  });
1377
1463
  // Get free/busy
1378
1464
  app.post("/api/google/calendar/freebusy", async (c) => {
1379
- if (!isCalendarAvailable())
1465
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1380
1466
  return c.json({ error: "Google not authenticated" }, 400);
1381
1467
  const body = await c.req.json();
1382
1468
  if (!body.start || !body.end)
1383
1469
  return c.json({ error: "start and end are required" }, 400);
1384
- const result = await getFreeBusy(body.start, body.end);
1470
+ const result = await _googleCalendar.getFreeBusy(body.start, body.end);
1385
1471
  if (!result.ok)
1386
1472
  return c.json({ error: result.message }, 500);
1387
1473
  return c.json(result);
1388
1474
  });
1389
1475
  // Create calendar event
1390
1476
  app.post("/api/google/calendar/events", async (c) => {
1391
- if (!isCalendarAvailable())
1477
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1392
1478
  return c.json({ error: "Google not authenticated" }, 400);
1393
1479
  const body = await c.req.json();
1394
1480
  if (!body.title || !body.start || !body.end) {
1395
1481
  return c.json({ error: "title, start, and end are required" }, 400);
1396
1482
  }
1397
1483
  // Temporal validation: catch day-of-week mismatches before creating events (ts_temporal_mismatch_01)
1398
- const temporalCheck = validateCalendarEntry(body.start, body.expectedDayOfWeek);
1484
+ const temporalCheck = _googleTemporal.validateCalendarEntry(body.start, body.expectedDayOfWeek);
1399
1485
  if (!temporalCheck.ok) {
1400
1486
  return c.json({
1401
1487
  error: temporalCheck.message,
1402
1488
  suggestion: temporalCheck.suggestion,
1403
- actualDayOfWeek: getDayOfWeek(body.start),
1489
+ actualDayOfWeek: _googleTemporal.getDayOfWeek(body.start),
1404
1490
  }, 400);
1405
1491
  }
1406
- const result = await createEvent(body.title, body.start, body.end, {
1492
+ const result = await _googleCalendar.createEvent(body.title, body.start, body.end, {
1407
1493
  description: body.description,
1408
1494
  location: body.location,
1409
1495
  attendees: body.attendees,
@@ -1413,13 +1499,13 @@ app.post("/api/google/calendar/events", async (c) => {
1413
1499
  if (!result.ok)
1414
1500
  return c.json({ error: result.message }, 500);
1415
1501
  logActivity({ source: "calendar", summary: `Event created: ${body.title}`, actionLabel: "PROMPTED", reason: "user created calendar event" });
1416
- return c.json({ ...result, actualDayOfWeek: getDayOfWeek(body.start) });
1502
+ return c.json({ ...result, actualDayOfWeek: _googleTemporal.getDayOfWeek(body.start) });
1417
1503
  });
1418
1504
  // List events with flexible filtering
1419
1505
  app.get("/api/google/calendar/events", async (c) => {
1420
- if (!isCalendarAvailable())
1506
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1421
1507
  return c.json({ error: "Google not authenticated" }, 400);
1422
- const result = await listEvents({
1508
+ const result = await _googleCalendar.listEvents({
1423
1509
  timeMin: c.req.query("timeMin"),
1424
1510
  timeMax: c.req.query("timeMax"),
1425
1511
  query: c.req.query("q"),
@@ -1432,34 +1518,34 @@ app.get("/api/google/calendar/events", async (c) => {
1432
1518
  });
1433
1519
  // Update calendar event
1434
1520
  app.patch("/api/google/calendar/events/:eventId", async (c) => {
1435
- if (!isCalendarAvailable())
1521
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1436
1522
  return c.json({ error: "Google not authenticated" }, 400);
1437
1523
  const eventId = c.req.param("eventId");
1438
1524
  const body = await c.req.json();
1439
1525
  // Temporal validation on updated start date (ts_temporal_mismatch_01)
1440
1526
  if (body.start) {
1441
- const temporalCheck = validateCalendarEntry(body.start, body.expectedDayOfWeek);
1527
+ const temporalCheck = _googleTemporal.validateCalendarEntry(body.start, body.expectedDayOfWeek);
1442
1528
  if (!temporalCheck.ok) {
1443
1529
  return c.json({
1444
1530
  error: temporalCheck.message,
1445
1531
  suggestion: temporalCheck.suggestion,
1446
- actualDayOfWeek: getDayOfWeek(body.start),
1532
+ actualDayOfWeek: _googleTemporal.getDayOfWeek(body.start),
1447
1533
  }, 400);
1448
1534
  }
1449
1535
  }
1450
- const result = await updateEvent(eventId, body);
1536
+ const result = await _googleCalendar.updateEvent(eventId, body);
1451
1537
  if (!result.ok)
1452
1538
  return c.json({ error: result.message }, 500);
1453
1539
  logActivity({ source: "calendar", summary: `Event updated: ${eventId}`, actionLabel: "PROMPTED", reason: "user updated calendar event" });
1454
- return c.json(body.start ? { ...result, actualDayOfWeek: getDayOfWeek(body.start) } : result);
1540
+ return c.json(body.start ? { ...result, actualDayOfWeek: _googleTemporal.getDayOfWeek(body.start) } : result);
1455
1541
  });
1456
1542
  // Delete calendar event
1457
1543
  app.delete("/api/google/calendar/events/:eventId", async (c) => {
1458
- if (!isCalendarAvailable())
1544
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1459
1545
  return c.json({ error: "Google not authenticated" }, 400);
1460
1546
  const eventId = c.req.param("eventId");
1461
1547
  const sendUpdates = c.req.query("sendUpdates");
1462
- const result = await deleteEvent(eventId, { sendUpdates });
1548
+ const result = await _googleCalendar.deleteEvent(eventId, { sendUpdates });
1463
1549
  if (!result.ok)
1464
1550
  return c.json({ error: result.message }, 500);
1465
1551
  logActivity({ source: "calendar", summary: `Event deleted: ${eventId}`, actionLabel: "PROMPTED", reason: "user deleted calendar event" });
@@ -1467,12 +1553,12 @@ app.delete("/api/google/calendar/events/:eventId", async (c) => {
1467
1553
  });
1468
1554
  // Search calendar events by text
1469
1555
  app.get("/api/google/calendar/search", async (c) => {
1470
- if (!isCalendarAvailable())
1556
+ if (!(_googleCalendar?.isCalendarAvailable() ?? false))
1471
1557
  return c.json({ error: "Google not authenticated" }, 400);
1472
1558
  const query = c.req.query("q");
1473
1559
  if (!query)
1474
1560
  return c.json({ error: "q (search query) is required" }, 400);
1475
- const result = await searchEvents(query, {
1561
+ const result = await _googleCalendar.searchEvents(query, {
1476
1562
  timeMin: c.req.query("timeMin"),
1477
1563
  timeMax: c.req.query("timeMax"),
1478
1564
  maxResults: c.req.query("maxResults") ? parseInt(c.req.query("maxResults"), 10) : undefined,
@@ -1490,9 +1576,9 @@ app.get("/api/google/tasks/lists", async (c) => {
1490
1576
  const session = validateSession(sessionId);
1491
1577
  if (!session)
1492
1578
  return c.json({ error: "Invalid or expired session" }, 401);
1493
- if (!isTasksAvailable())
1579
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1494
1580
  return c.json({ error: "Google not authenticated" }, 400);
1495
- const result = await listTaskLists();
1581
+ const result = await _googleTasks.listTaskLists();
1496
1582
  if (!result.ok)
1497
1583
  return c.json({ error: result.message }, 500);
1498
1584
  return c.json(result);
@@ -1505,12 +1591,12 @@ app.post("/api/google/tasks/lists", async (c) => {
1505
1591
  const session = validateSession(sessionId);
1506
1592
  if (!session)
1507
1593
  return c.json({ error: "Invalid or expired session" }, 401);
1508
- if (!isTasksAvailable())
1594
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1509
1595
  return c.json({ error: "Google not authenticated" }, 400);
1510
1596
  const body = await c.req.json();
1511
1597
  if (!body.title)
1512
1598
  return c.json({ error: "title is required" }, 400);
1513
- const result = await createTaskList(body.title);
1599
+ const result = await _googleTasks.createTaskList(body.title);
1514
1600
  if (!result.ok)
1515
1601
  return c.json({ error: result.message }, 500);
1516
1602
  logActivity({ source: "tasks", summary: `Task list created: ${body.title}`, actionLabel: "PROMPTED", reason: "user created task list" });
@@ -1524,13 +1610,13 @@ app.patch("/api/google/tasks/lists/:listId", async (c) => {
1524
1610
  const session = validateSession(sessionId);
1525
1611
  if (!session)
1526
1612
  return c.json({ error: "Invalid or expired session" }, 401);
1527
- if (!isTasksAvailable())
1613
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1528
1614
  return c.json({ error: "Google not authenticated" }, 400);
1529
1615
  const listId = c.req.param("listId");
1530
1616
  const body = await c.req.json();
1531
1617
  if (!body.title)
1532
1618
  return c.json({ error: "title is required" }, 400);
1533
- const result = await updateTaskList(listId, body.title);
1619
+ const result = await _googleTasks.updateTaskList(listId, body.title);
1534
1620
  if (!result.ok)
1535
1621
  return c.json({ error: result.message }, 500);
1536
1622
  return c.json(result);
@@ -1543,10 +1629,10 @@ app.delete("/api/google/tasks/lists/:listId", async (c) => {
1543
1629
  const session = validateSession(sessionId);
1544
1630
  if (!session)
1545
1631
  return c.json({ error: "Invalid or expired session" }, 401);
1546
- if (!isTasksAvailable())
1632
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1547
1633
  return c.json({ error: "Google not authenticated" }, 400);
1548
1634
  const listId = c.req.param("listId");
1549
- const result = await deleteTaskList(listId);
1635
+ const result = await _googleTasks.deleteTaskList(listId);
1550
1636
  if (!result.ok)
1551
1637
  return c.json({ error: result.message }, 500);
1552
1638
  logActivity({ source: "tasks", summary: `Task list deleted: ${listId}`, actionLabel: "PROMPTED", reason: "user deleted task list" });
@@ -1560,7 +1646,7 @@ app.post("/api/google/tasks/recurring", async (c) => {
1560
1646
  const session = validateSession(sessionId);
1561
1647
  if (!session)
1562
1648
  return c.json({ error: "Invalid or expired session" }, 401);
1563
- if (!isTasksAvailable())
1649
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1564
1650
  return c.json({ error: "Google not authenticated" }, 400);
1565
1651
  const body = await c.req.json();
1566
1652
  if (!body.title)
@@ -1584,7 +1670,7 @@ app.post("/api/google/tasks/recurring", async (c) => {
1584
1670
  }, 400);
1585
1671
  }
1586
1672
  }
1587
- const result = await createRecurringWeeklyTasks(body);
1673
+ const result = await _googleTasks.createRecurringWeeklyTasks(body);
1588
1674
  if (!result.ok)
1589
1675
  return c.json({ error: result.message }, 500);
1590
1676
  logActivity({ source: "tasks", summary: result.message, actionLabel: "PROMPTED", reason: "user created recurring tasks" });
@@ -1598,13 +1684,13 @@ app.get("/api/google/tasks/:listId", async (c) => {
1598
1684
  const session = validateSession(sessionId);
1599
1685
  if (!session)
1600
1686
  return c.json({ error: "Invalid or expired session" }, 401);
1601
- if (!isTasksAvailable())
1687
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1602
1688
  return c.json({ error: "Google not authenticated" }, 400);
1603
1689
  const listId = c.req.param("listId");
1604
1690
  const showCompleted = c.req.query("showCompleted") === "true";
1605
1691
  const dueMin = c.req.query("dueMin");
1606
1692
  const dueMax = c.req.query("dueMax");
1607
- const result = await listTasks(listId, {
1693
+ const result = await _googleTasks.listTasks(listId, {
1608
1694
  showCompleted,
1609
1695
  dueMin: dueMin || undefined,
1610
1696
  dueMax: dueMax || undefined,
@@ -1621,11 +1707,11 @@ app.get("/api/google/tasks/:listId/:taskId", async (c) => {
1621
1707
  const session = validateSession(sessionId);
1622
1708
  if (!session)
1623
1709
  return c.json({ error: "Invalid or expired session" }, 401);
1624
- if (!isTasksAvailable())
1710
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1625
1711
  return c.json({ error: "Google not authenticated" }, 400);
1626
1712
  const listId = c.req.param("listId");
1627
1713
  const taskId = c.req.param("taskId");
1628
- const result = await getGoogleTask(listId, taskId);
1714
+ const result = await _googleTasks.getTask(listId, taskId);
1629
1715
  if (!result.ok)
1630
1716
  return c.json({ error: result.message }, 500);
1631
1717
  return c.json(result);
@@ -1638,13 +1724,13 @@ app.post("/api/google/tasks/:listId", async (c) => {
1638
1724
  const session = validateSession(sessionId);
1639
1725
  if (!session)
1640
1726
  return c.json({ error: "Invalid or expired session" }, 401);
1641
- if (!isTasksAvailable())
1727
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1642
1728
  return c.json({ error: "Google not authenticated" }, 400);
1643
1729
  const listId = c.req.param("listId");
1644
1730
  const body = await c.req.json();
1645
1731
  if (!body.title)
1646
1732
  return c.json({ error: "title is required" }, 400);
1647
- const result = await createTask(listId, body);
1733
+ const result = await _googleTasks.createTask(listId, body);
1648
1734
  if (!result.ok)
1649
1735
  return c.json({ error: result.message }, 500);
1650
1736
  logActivity({ source: "tasks", summary: `Task created: ${body.title}`, actionLabel: "PROMPTED", reason: "user created task" });
@@ -1658,12 +1744,12 @@ app.patch("/api/google/tasks/:listId/:taskId", async (c) => {
1658
1744
  const session = validateSession(sessionId);
1659
1745
  if (!session)
1660
1746
  return c.json({ error: "Invalid or expired session" }, 401);
1661
- if (!isTasksAvailable())
1747
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1662
1748
  return c.json({ error: "Google not authenticated" }, 400);
1663
1749
  const listId = c.req.param("listId");
1664
1750
  const taskId = c.req.param("taskId");
1665
1751
  const body = await c.req.json();
1666
- const result = await updateTask(listId, taskId, body);
1752
+ const result = await _googleTasks.updateTask(listId, taskId, body);
1667
1753
  if (!result.ok)
1668
1754
  return c.json({ error: result.message }, 500);
1669
1755
  logActivity({ source: "tasks", summary: `Task updated: ${taskId}`, actionLabel: "PROMPTED", reason: "user updated task" });
@@ -1677,11 +1763,11 @@ app.post("/api/google/tasks/:listId/:taskId/complete", async (c) => {
1677
1763
  const session = validateSession(sessionId);
1678
1764
  if (!session)
1679
1765
  return c.json({ error: "Invalid or expired session" }, 401);
1680
- if (!isTasksAvailable())
1766
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1681
1767
  return c.json({ error: "Google not authenticated" }, 400);
1682
1768
  const listId = c.req.param("listId");
1683
1769
  const taskId = c.req.param("taskId");
1684
- const result = await completeTask(listId, taskId);
1770
+ const result = await _googleTasks.completeTask(listId, taskId);
1685
1771
  if (!result.ok)
1686
1772
  return c.json({ error: result.message }, 500);
1687
1773
  logActivity({ source: "tasks", summary: `Task completed: ${taskId}`, actionLabel: "PROMPTED", reason: "user completed task" });
@@ -1695,11 +1781,11 @@ app.post("/api/google/tasks/:listId/:taskId/uncomplete", async (c) => {
1695
1781
  const session = validateSession(sessionId);
1696
1782
  if (!session)
1697
1783
  return c.json({ error: "Invalid or expired session" }, 401);
1698
- if (!isTasksAvailable())
1784
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1699
1785
  return c.json({ error: "Google not authenticated" }, 400);
1700
1786
  const listId = c.req.param("listId");
1701
1787
  const taskId = c.req.param("taskId");
1702
- const result = await uncompleteTask(listId, taskId);
1788
+ const result = await _googleTasks.uncompleteTask(listId, taskId);
1703
1789
  if (!result.ok)
1704
1790
  return c.json({ error: result.message }, 500);
1705
1791
  return c.json(result);
@@ -1712,11 +1798,11 @@ app.delete("/api/google/tasks/:listId/:taskId", async (c) => {
1712
1798
  const session = validateSession(sessionId);
1713
1799
  if (!session)
1714
1800
  return c.json({ error: "Invalid or expired session" }, 401);
1715
- if (!isTasksAvailable())
1801
+ if (!(_googleTasks?.isTasksAvailable() ?? false))
1716
1802
  return c.json({ error: "Google not authenticated" }, 400);
1717
1803
  const listId = c.req.param("listId");
1718
1804
  const taskId = c.req.param("taskId");
1719
- const result = await deleteTask(listId, taskId);
1805
+ const result = await _googleTasks.deleteTask(listId, taskId);
1720
1806
  if (!result.ok)
1721
1807
  return c.json({ error: result.message }, 500);
1722
1808
  logActivity({ source: "tasks", summary: `Task deleted: ${taskId}`, actionLabel: "PROMPTED", reason: "user deleted task" });
@@ -1857,7 +1943,7 @@ app.put("/api/settings", async (c) => {
1857
1943
  app.get("/api/admin/integrations", async (c) => {
1858
1944
  const settings = getSettings();
1859
1945
  const integrations = settings.integrations ?? { enabled: true };
1860
- const status = getIntegrationStatus();
1946
+ const status = (_integrationsGate ? _integrationsGate.getIntegrationStatus() : []);
1861
1947
  return c.json({
1862
1948
  enabled: integrations.enabled ?? true,
1863
1949
  services: status,
@@ -1876,9 +1962,9 @@ app.post("/api/admin/integrations", async (c) => {
1876
1962
  }
1877
1963
  const updated = await updateSettings(patch);
1878
1964
  // Re-hydrate env with new gates — clear integration vars first, then re-hydrate
1879
- const status = getIntegrationStatus();
1880
- rehydrateVaultEnv();
1881
- const credStore = getCredentialStore();
1965
+ const status = (_integrationsGate ? _integrationsGate.getIntegrationStatus() : []);
1966
+ _vaultStore?.hydrateEnv();
1967
+ const credStore = (_credentialStore ? _credentialStore.getCredentialStore() : null);
1882
1968
  if (credStore)
1883
1969
  await credStore.hydrate();
1884
1970
  return c.json({
@@ -1886,23 +1972,23 @@ app.post("/api/admin/integrations", async (c) => {
1886
1972
  services: status.map((s) => ({
1887
1973
  ...s,
1888
1974
  // Re-check after settings update
1889
- enabled: isIntegrationEnabled(s.service),
1975
+ enabled: (_integrationsGate?.isIntegrationEnabled(s.service) ?? false),
1890
1976
  })),
1891
1977
  });
1892
1978
  });
1893
1979
  // --- Voice routes ---
1894
1980
  // Voice status: which voice features are available?
1895
1981
  app.get("/api/voice-status", async (c) => {
1896
- return c.json({ tts: isTtsAvailable(), stt: isSttAvailable() });
1982
+ return c.json({ tts: (_ttsClient?.isTtsAvailable() ?? false), stt: (_sttClient?.isSttAvailable() ?? false) });
1897
1983
  });
1898
1984
  // TTS: synthesize text to WAV audio via Piper
1899
1985
  app.get("/api/tts", async (c) => {
1900
1986
  const text = c.req.query("text");
1901
1987
  if (!text)
1902
1988
  return c.json({ error: "text query param required" }, 400);
1903
- if (!isTtsAvailable())
1989
+ if (!(_ttsClient?.isTtsAvailable() ?? false))
1904
1990
  return c.json({ error: "TTS not available" }, 503);
1905
- const wav = await synthesize(text);
1991
+ const wav = await _ttsClient.synthesize(text);
1906
1992
  if (!wav)
1907
1993
  return c.json({ error: "Synthesis failed" }, 502);
1908
1994
  return new Response(wav, {
@@ -1914,12 +2000,12 @@ app.get("/api/tts", async (c) => {
1914
2000
  });
1915
2001
  // STT: transcribe audio via whisper-server
1916
2002
  app.post("/api/stt", async (c) => {
1917
- if (!isSttAvailable())
2003
+ if (!(_sttClient?.isSttAvailable() ?? false))
1918
2004
  return c.json({ error: "STT not available" }, 503);
1919
2005
  const body = await c.req.arrayBuffer();
1920
2006
  if (!body || body.byteLength === 0)
1921
2007
  return c.json({ error: "Audio body required" }, 400);
1922
- const text = await transcribe(Buffer.from(body));
2008
+ const text = await _sttClient.transcribe(Buffer.from(body));
1923
2009
  if (!text)
1924
2010
  return c.json({ error: "Transcription failed" }, 502);
1925
2011
  return c.json({ text });
@@ -1934,7 +2020,7 @@ function pushPendingVideo(filename) {
1934
2020
  }
1935
2021
  // Avatar status: is MuseTalk sidecar running?
1936
2022
  app.get("/api/avatar/status", async (c) => {
1937
- return c.json({ available: isAvatarAvailable() });
2023
+ return c.json({ available: (_avatarSidecar?.isAvatarAvailable() ?? false) });
1938
2024
  });
1939
2025
  // Poll for new avatar videos since a given timestamp
1940
2026
  app.get("/api/avatar/latest", async (c) => {
@@ -1976,18 +2062,18 @@ app.post("/api/avatar/photo", async (c) => {
1976
2062
  const session = validateSession(sessionId);
1977
2063
  if (!session)
1978
2064
  return c.json({ error: "Invalid or expired session" }, 401);
1979
- if (!isAvatarAvailable())
2065
+ if (!(_avatarSidecar?.isAvatarAvailable() ?? false))
1980
2066
  return c.json({ error: "Avatar not available" }, 503);
1981
2067
  const body = await c.req.arrayBuffer();
1982
2068
  if (!body || body.byteLength === 0)
1983
2069
  return c.json({ error: "Photo body required" }, 400);
1984
- const avatarConfig = getAvatarConfig();
2070
+ const avatarConfig = _settingsVoice ? _settingsVoice.getAvatarConfig() : { photoPath: "public/avatar/photo.png", port: 0, enabled: false };
1985
2071
  const photoPath = join(process.cwd(), avatarConfig.photoPath);
1986
2072
  await mkdir(join(UI_DIR, "avatar"), { recursive: true });
1987
2073
  await writeFile(photoPath, Buffer.from(body));
1988
- const ok = await preparePhoto(photoPath);
2074
+ const ok = await _avatarClient.preparePhoto(photoPath);
1989
2075
  if (ok) {
1990
- await clearVideoCache();
2076
+ await _avatarClient.clearVideoCache();
1991
2077
  latestAvatarVideo = null;
1992
2078
  }
1993
2079
  return c.json({ ok });
@@ -2036,8 +2122,9 @@ app.get("/api/history", async (c) => {
2036
2122
  if (!session)
2037
2123
  return c.json({ error: "Invalid or expired session" }, 401);
2038
2124
  const cs = await getOrCreateChatSession(sessionId, session.name);
2039
- // Return only user and assistant messages (not system)
2040
- const messages = cs.history
2125
+ // Always return main history (not active thread's)
2126
+ const historySource = cs.activeThreadId ? cs.mainHistory : cs.history;
2127
+ const messages = historySource
2041
2128
  .filter((m) => m.role === "user" || m.role === "assistant")
2042
2129
  .map((m) => ({ role: m.role, content: m.content }));
2043
2130
  return c.json({ messages });
@@ -2084,20 +2171,25 @@ app.post("/api/threads", async (c) => {
2084
2171
  const now = new Date().toISOString();
2085
2172
  const thread = {
2086
2173
  id: generateThreadId(),
2087
- title: body.title || "New thread",
2174
+ title: body.title || "New chat",
2088
2175
  history: [],
2089
2176
  historySummary: "",
2090
2177
  createdAt: now,
2091
2178
  updatedAt: now,
2092
2179
  };
2093
2180
  threads.set(thread.id, thread);
2094
- // Save current main history as the previous thread, then clear for fresh conversation
2181
+ // New thread starts blank. Main chat keeps its history.
2182
+ // If currently on a thread, save it back first.
2095
2183
  const cs = chatSessions.get(sessionId);
2096
- if (cs) {
2097
- cs.history = [];
2098
- cs.historySummary = "";
2099
- cs.foldedBack = false;
2100
- cs.turnCount = 0;
2184
+ if (cs && cs.activeThreadId) {
2185
+ const prevThread = threads.get(cs.activeThreadId);
2186
+ if (prevThread) {
2187
+ prevThread.history = cs.history;
2188
+ prevThread.historySummary = cs.historySummary;
2189
+ }
2190
+ cs.history = cs.mainHistory;
2191
+ cs.historySummary = cs.mainHistorySummary;
2192
+ cs.activeThreadId = null;
2101
2193
  }
2102
2194
  return c.json({ thread: { id: thread.id, title: thread.title, createdAt: thread.createdAt, updatedAt: thread.updatedAt } });
2103
2195
  });
@@ -2108,11 +2200,15 @@ app.get("/api/threads/:id/history", async (c) => {
2108
2200
  const session = validateSession(sessionId);
2109
2201
  if (!session)
2110
2202
  return c.json({ error: "Invalid session" }, 401);
2203
+ const threadId = c.req.param("id");
2111
2204
  const threads = getThreadsForSession(sessionId);
2112
- const thread = threads.get(c.req.param("id"));
2205
+ const thread = threads.get(threadId);
2113
2206
  if (!thread)
2114
2207
  return c.json({ error: "Thread not found" }, 404);
2115
- const messages = thread.history
2208
+ // If this thread is currently active on cs, its live history is in cs.history
2209
+ const cs = chatSessions.get(sessionId);
2210
+ const historySource = (cs && cs.activeThreadId === threadId) ? cs.history : thread.history;
2211
+ const messages = historySource
2116
2212
  .filter((m) => m.role === "user" || m.role === "assistant")
2117
2213
  .map((m) => ({ role: m.role, content: m.content }));
2118
2214
  return c.json({ messages });
@@ -2141,8 +2237,14 @@ app.delete("/api/threads/:id", async (c) => {
2141
2237
  const session = validateSession(sessionId);
2142
2238
  if (!session)
2143
2239
  return c.json({ error: "Invalid session" }, 401);
2240
+ const threadId = c.req.param("id");
2144
2241
  const threads = getThreadsForSession(sessionId);
2145
- threads.delete(c.req.param("id"));
2242
+ // If deleting the active thread, switch back to main first
2243
+ const cs = chatSessions.get(sessionId);
2244
+ if (cs && cs.activeThreadId === threadId) {
2245
+ switchSessionThread(cs, null, sessionId);
2246
+ }
2247
+ threads.delete(threadId);
2146
2248
  return c.json({ ok: true });
2147
2249
  });
2148
2250
  // Activity log: poll for background actions
@@ -2273,13 +2375,17 @@ app.post("/api/branch", async (c) => {
2273
2375
  reqSignal?.addEventListener("abort", onAbort, { once: true });
2274
2376
  // Token buffer for split-placeholder rehydration
2275
2377
  let tokenBuf = "";
2378
+ let totalOutputChars = 0;
2379
+ const streamStartMs = performance.now();
2276
2380
  const flushBuf = () => {
2277
2381
  if (!tokenBuf)
2278
2382
  return;
2279
2383
  const rehydrated = rehydrateResponse(tokenBuf);
2384
+ totalOutputChars += rehydrated.length;
2280
2385
  tokenBuf = "";
2281
2386
  stream.writeSSE({ data: JSON.stringify({ token: rehydrated }) }).catch(() => { });
2282
2387
  };
2388
+ const streamModel = activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4");
2283
2389
  stream_fn({
2284
2390
  messages: redactedBranchMessages,
2285
2391
  model: activeChatModel,
@@ -2296,6 +2402,12 @@ app.post("/api/branch", async (c) => {
2296
2402
  flushBuf(); // flush remainder
2297
2403
  reqSignal?.removeEventListener("abort", onAbort);
2298
2404
  stream.writeSSE({ data: JSON.stringify({ done: true }) }).catch(() => { });
2405
+ const durationMs = Math.round(performance.now() - streamStartMs);
2406
+ logLlmCall({
2407
+ ts: new Date().toISOString(), mode: "stream",
2408
+ provider: activeProvider, model: streamModel,
2409
+ durationMs, outputTokens: Math.ceil(totalOutputChars / 4), ok: true,
2410
+ });
2299
2411
  resolve();
2300
2412
  },
2301
2413
  onError: async (err) => {
@@ -2307,6 +2419,12 @@ app.post("/api/branch", async (c) => {
2307
2419
  if (!health.ok)
2308
2420
  errorMsg += " — Check that Ollama is running. " + health.message;
2309
2421
  }
2422
+ const durationMs = Math.round(performance.now() - streamStartMs);
2423
+ logLlmCall({
2424
+ ts: new Date().toISOString(), mode: "stream",
2425
+ provider: activeProvider, model: streamModel,
2426
+ durationMs, ok: false, error: errorMsg,
2427
+ });
2310
2428
  stream.writeSSE({ data: JSON.stringify({ error: errorMsg }) }).catch(() => { });
2311
2429
  resolve();
2312
2430
  },
@@ -2357,7 +2475,7 @@ app.post("/api/agents/tasks/:id/cancel", async (c) => {
2357
2475
  });
2358
2476
  // --- File lock routes ---
2359
2477
  app.get("/api/agents/locks", async (c) => {
2360
- const locks = await listLocks();
2478
+ const locks = await _agentLocks.listLocks();
2361
2479
  return c.json({ locks });
2362
2480
  });
2363
2481
  app.post("/api/agents/locks/acquire", async (c) => {
@@ -2366,7 +2484,7 @@ app.post("/api/agents/locks/acquire", async (c) => {
2366
2484
  if (!agentId || !filePaths || !Array.isArray(filePaths)) {
2367
2485
  return c.json({ error: "agentId and filePaths[] required" }, 400);
2368
2486
  }
2369
- const result = await acquireLocks(agentId, agentLabel || agentId, filePaths, timeoutMs);
2487
+ const result = await _agentLocks.acquireLocks(agentId, agentLabel || agentId, filePaths, timeoutMs);
2370
2488
  return c.json(result, result.acquired ? 200 : 409);
2371
2489
  });
2372
2490
  app.post("/api/agents/locks/release", async (c) => {
@@ -2375,10 +2493,10 @@ app.post("/api/agents/locks/release", async (c) => {
2375
2493
  if (!agentId)
2376
2494
  return c.json({ error: "agentId required" }, 400);
2377
2495
  if (filePath) {
2378
- const ok = await releaseFileLock(agentId, filePath);
2496
+ const ok = await _agentLocks.releaseFileLock(agentId, filePath);
2379
2497
  return c.json({ released: ok ? 1 : 0 });
2380
2498
  }
2381
- const count = await releaseLocks(agentId);
2499
+ const count = await _agentLocks.releaseLocks(agentId);
2382
2500
  return c.json({ released: count });
2383
2501
  });
2384
2502
  app.post("/api/agents/locks/force-release", async (c) => {
@@ -2386,7 +2504,7 @@ app.post("/api/agents/locks/force-release", async (c) => {
2386
2504
  const { filePath } = body;
2387
2505
  if (!filePath)
2388
2506
  return c.json({ error: "filePath required" }, 400);
2389
- const ok = await forceReleaseLock(filePath);
2507
+ const ok = await _agentLocks.forceReleaseLock(filePath);
2390
2508
  return c.json({ released: ok });
2391
2509
  });
2392
2510
  app.post("/api/agents/locks/check", async (c) => {
@@ -2395,11 +2513,11 @@ app.post("/api/agents/locks/check", async (c) => {
2395
2513
  if (!filePaths || !Array.isArray(filePaths)) {
2396
2514
  return c.json({ error: "filePaths[] required" }, 400);
2397
2515
  }
2398
- const conflicts = await checkLocks(filePaths);
2516
+ const conflicts = await _agentLocks.checkLocks(filePaths);
2399
2517
  return c.json({ locked: conflicts.length > 0, conflicts });
2400
2518
  });
2401
2519
  app.post("/api/agents/locks/prune", async (_c) => {
2402
- const pruned = await pruneAllStaleLocks();
2520
+ const pruned = await _agentLocks.pruneAllStaleLocks();
2403
2521
  return _c.json({ pruned });
2404
2522
  });
2405
2523
  // --- Self-reported issues (autonomous agent findings) ---
@@ -2410,7 +2528,7 @@ app.get("/api/agents/issues", async (c) => {
2410
2528
  });
2411
2529
  // --- Agent runtime routes ---
2412
2530
  app.get("/api/runtime/status", async (c) => {
2413
- const rt = getRuntime();
2531
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2414
2532
  if (!rt)
2415
2533
  return c.json({ available: false });
2416
2534
  return c.json({
@@ -2420,14 +2538,14 @@ app.get("/api/runtime/status", async (c) => {
2420
2538
  });
2421
2539
  });
2422
2540
  app.get("/api/runtime/instances", async (c) => {
2423
- const rt = getRuntime();
2541
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2424
2542
  if (!rt)
2425
2543
  return c.json({ instances: [] });
2426
2544
  const states = c.req.query("states")?.split(",");
2427
2545
  return c.json({ instances: rt.listInstances(states ? { states } : undefined) });
2428
2546
  });
2429
2547
  app.get("/api/runtime/instances/:id", async (c) => {
2430
- const rt = getRuntime();
2548
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2431
2549
  if (!rt)
2432
2550
  return c.json({ error: "Runtime not initialized" }, 503);
2433
2551
  const inst = rt.getInstance(c.req.param("id"));
@@ -2436,7 +2554,7 @@ app.get("/api/runtime/instances/:id", async (c) => {
2436
2554
  return c.json(inst);
2437
2555
  });
2438
2556
  app.post("/api/runtime/spawn", async (c) => {
2439
- const rt = getRuntime();
2557
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2440
2558
  if (!rt)
2441
2559
  return c.json({ error: "Runtime not initialized" }, 503);
2442
2560
  const body = await c.req.json();
@@ -2481,7 +2599,7 @@ app.post("/api/runtime/spawn", async (c) => {
2481
2599
  }
2482
2600
  });
2483
2601
  app.post("/api/runtime/instances/:id/pause", async (c) => {
2484
- const rt = getRuntime();
2602
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2485
2603
  if (!rt)
2486
2604
  return c.json({ error: "Runtime not initialized" }, 503);
2487
2605
  try {
@@ -2493,7 +2611,7 @@ app.post("/api/runtime/instances/:id/pause", async (c) => {
2493
2611
  }
2494
2612
  });
2495
2613
  app.post("/api/runtime/instances/:id/resume", async (c) => {
2496
- const rt = getRuntime();
2614
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2497
2615
  if (!rt)
2498
2616
  return c.json({ error: "Runtime not initialized" }, 503);
2499
2617
  try {
@@ -2505,7 +2623,7 @@ app.post("/api/runtime/instances/:id/resume", async (c) => {
2505
2623
  }
2506
2624
  });
2507
2625
  app.post("/api/runtime/instances/:id/terminate", async (c) => {
2508
- const rt = getRuntime();
2626
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2509
2627
  if (!rt)
2510
2628
  return c.json({ error: "Runtime not initialized" }, 503);
2511
2629
  try {
@@ -2517,7 +2635,7 @@ app.post("/api/runtime/instances/:id/terminate", async (c) => {
2517
2635
  }
2518
2636
  });
2519
2637
  app.post("/api/runtime/instances/:id/message", async (c) => {
2520
- const rt = getRuntime();
2638
+ const rt = (_agentRuntime?.getRuntime() ?? null);
2521
2639
  if (!rt)
2522
2640
  return c.json({ error: "Runtime not initialized" }, 503);
2523
2641
  const body = await c.req.json();
@@ -2666,7 +2784,7 @@ app.post("/api/github/webhooks", async (c) => {
2666
2784
  const rawBody = await c.req.text();
2667
2785
  const secret = process.env.GITHUB_WEBHOOK_SECRET;
2668
2786
  if (secret && signature) {
2669
- if (!verifyWebhookSignature(rawBody, signature, secret)) {
2787
+ if (!_githubWebhooks.verifyWebhookSignature(rawBody, signature, secret)) {
2670
2788
  return c.json({ error: "Invalid signature" }, 401);
2671
2789
  }
2672
2790
  }
@@ -2677,12 +2795,12 @@ app.post("/api/github/webhooks", async (c) => {
2677
2795
  catch {
2678
2796
  return c.json({ error: "Invalid JSON" }, 400);
2679
2797
  }
2680
- const result = await processGitHubWebhook(eventType, payload);
2798
+ const result = await _integrationsGithub.processWebhook(eventType, payload);
2681
2799
  return c.json(result);
2682
2800
  });
2683
2801
  // GitHub status
2684
2802
  app.get("/api/github/status", async (c) => {
2685
- const status = await getGitHubStatus();
2803
+ const status = await _integrationsGithub.getGitHubStatus();
2686
2804
  return c.json(status);
2687
2805
  });
2688
2806
  // GitHub PR review
@@ -2698,8 +2816,8 @@ app.post("/api/github/pr/review", async (c) => {
2698
2816
  if (!prNumber)
2699
2817
  return c.json({ error: "prNumber required" }, 400);
2700
2818
  const result = postComment
2701
- ? await reviewAndCommentPR(prNumber, repo)
2702
- : await reviewPullRequest(prNumber, repo);
2819
+ ? await _integrationsGithub.reviewAndCommentPR(prNumber, repo)
2820
+ : await _integrationsGithub.reviewPullRequest(prNumber, repo);
2703
2821
  if (!result)
2704
2822
  return c.json({ error: "Failed to review PR" }, 502);
2705
2823
  return c.json(result);
@@ -2717,8 +2835,8 @@ app.post("/api/github/issues/triage", async (c) => {
2717
2835
  if (!issueNumber)
2718
2836
  return c.json({ error: "issueNumber required" }, 400);
2719
2837
  const result = apply
2720
- ? await triageAndLabelIssue(issueNumber, repo)
2721
- : await triageGitHubIssue(issueNumber, repo);
2838
+ ? await _integrationsGithub.triageAndLabelIssue(issueNumber, repo)
2839
+ : await _integrationsGithub.triageGitHubIssue(issueNumber, repo);
2722
2840
  if (!result)
2723
2841
  return c.json({ error: "Failed to triage issue" }, 502);
2724
2842
  return c.json(result);
@@ -2733,7 +2851,7 @@ app.post("/api/github/issues/triage/batch", async (c) => {
2733
2851
  return c.json({ error: "Invalid or expired session" }, 401);
2734
2852
  const body = await c.req.json();
2735
2853
  const { repo, apply } = body;
2736
- const results = await batchTriageIssues(repo, { apply });
2854
+ const results = await _integrationsGithub.batchTriageIssues(repo, { apply });
2737
2855
  return c.json({ count: results.length, results });
2738
2856
  });
2739
2857
  // GitHub commit analysis
@@ -2747,34 +2865,34 @@ app.post("/api/github/commits/analyze", async (c) => {
2747
2865
  const body = await c.req.json();
2748
2866
  const { sha, repo, count, since } = body;
2749
2867
  if (sha) {
2750
- const result = await analyzeGitHubCommit(sha, repo);
2868
+ const result = await _integrationsGithub.analyzeGitHubCommit(sha, repo);
2751
2869
  if (!result)
2752
2870
  return c.json({ error: "Failed to analyze commit" }, 502);
2753
2871
  return c.json(result);
2754
2872
  }
2755
- const results = await analyzeRecentGitHubCommits(repo, { count, since });
2873
+ const results = await _integrationsGithub.analyzeRecentGitHubCommits(repo, { count, since });
2756
2874
  return c.json({ count: results.length, results });
2757
2875
  });
2758
2876
  // GitHub repo health
2759
2877
  app.get("/api/github/health/:owner/:repo", async (c) => {
2760
2878
  const { owner, repo } = c.req.param();
2761
- const report = await getGitHubRepoHealth(`${owner}/${repo}`);
2879
+ const report = await _integrationsGithub.getGitHubRepoHealth(`${owner}/${repo}`);
2762
2880
  if (!report)
2763
2881
  return c.json({ error: "Failed to generate health report" }, 502);
2764
2882
  return c.json(report);
2765
2883
  });
2766
2884
  app.get("/api/github/health/:owner/:repo/markdown", async (c) => {
2767
2885
  const { owner, repo } = c.req.param();
2768
- const report = await getGitHubRepoHealth(`${owner}/${repo}`);
2886
+ const report = await _integrationsGithub.getGitHubRepoHealth(`${owner}/${repo}`);
2769
2887
  if (!report)
2770
2888
  return c.json({ error: "Failed to generate health report" }, 502);
2771
- return c.text(formatHealthReport(report));
2889
+ return c.text(_integrationsGithub.formatHealthReport(report));
2772
2890
  });
2773
2891
  // --- Slack routes ---
2774
2892
  // Slack OAuth: initiate "Add to Slack" flow
2775
2893
  app.get("/api/slack/auth", async (c) => {
2776
2894
  const redirectUri = `http://localhost:${PORT}/api/slack/callback`;
2777
- const result = getSlackOAuthUrl(redirectUri);
2895
+ const result = _slackClient.getOAuthUrl(redirectUri);
2778
2896
  if (!result.ok) {
2779
2897
  return c.html(`<html><body><h2>Slack not configured</h2><p>${result.message}</p></body></html>`);
2780
2898
  }
@@ -2791,7 +2909,7 @@ app.get("/api/slack/callback", async (c) => {
2791
2909
  return c.html(`<html><body><h2>Missing authorization code</h2></body></html>`);
2792
2910
  }
2793
2911
  const redirectUri = `http://localhost:${PORT}/api/slack/callback`;
2794
- const result = await exchangeSlackCode(code, redirectUri);
2912
+ const result = await _slackClient.exchangeOAuthCode(code, redirectUri);
2795
2913
  if (!result.ok) {
2796
2914
  return c.html(`<html><body><h2>Token exchange failed</h2><p>${result.message}</p></body></html>`);
2797
2915
  }
@@ -2803,32 +2921,33 @@ app.get("/api/slack/callback", async (c) => {
2803
2921
  vaultKey = restored.sessionKey;
2804
2922
  sessionKeys.set(restored.session.id, restored.sessionKey);
2805
2923
  setEncryptionKey(restored.sessionKey);
2806
- await loadVault(restored.sessionKey);
2924
+ if (_vaultStore)
2925
+ await _vaultStore.loadVault(restored.sessionKey);
2807
2926
  }
2808
2927
  }
2809
2928
  if (!vaultKey) {
2810
2929
  return c.html(`<html><body><h2>Session not found</h2><p>Log in first, then try connecting Slack again.</p></body></html>`);
2811
2930
  }
2812
- await setVaultKey("SLACK_BOT_TOKEN", result.botToken, vaultKey, "Slack Bot Token");
2931
+ await _vaultStore.setVaultKey("SLACK_BOT_TOKEN", result.botToken, vaultKey, "Slack Bot Token");
2813
2932
  if (result.teamId) {
2814
- await setVaultKey("SLACK_TEAM_ID", result.teamId, vaultKey, "Slack Team ID");
2933
+ await _vaultStore.setVaultKey("SLACK_TEAM_ID", result.teamId, vaultKey, "Slack Team ID");
2815
2934
  }
2816
2935
  logActivity({ source: "slack", summary: `Slack OAuth connected — team: ${result.teamName ?? result.teamId}`, actionLabel: "PROMPTED", reason: "user connected Slack OAuth" });
2817
2936
  return c.html(`<html><body><h2>Slack connected!</h2><p>Team: ${result.teamName ?? "connected"}</p><p>This tab will close automatically.</p><script>if(window.opener){window.opener.postMessage("slack-connected","*")}setTimeout(()=>window.close(),1500)</script></body></html>`);
2818
2937
  });
2819
2938
  // Slack status: check connection state
2820
2939
  app.get("/api/slack/status", async (c) => {
2821
- const client = getSlackClient();
2940
+ const client = (_slackClient?.getClient() ?? null);
2822
2941
  if (!client) {
2823
2942
  return c.json({
2824
- configured: isSlackConfigured(),
2943
+ configured: (_slackClient?.isSlackConfigured() ?? false),
2825
2944
  authenticated: false,
2826
2945
  message: "SLACK_BOT_TOKEN not set",
2827
2946
  });
2828
2947
  }
2829
2948
  const auth = await client.testAuth();
2830
2949
  return c.json({
2831
- configured: isSlackConfigured(),
2950
+ configured: (_slackClient?.isSlackConfigured() ?? false),
2832
2951
  authenticated: auth.ok,
2833
2952
  userId: auth.userId,
2834
2953
  teamId: auth.teamId,
@@ -2844,7 +2963,7 @@ app.post("/api/slack/send", async (c) => {
2844
2963
  const session = validateSession(sessionId);
2845
2964
  if (!session)
2846
2965
  return c.json({ error: "Invalid or expired session" }, 401);
2847
- const client = getSlackClient();
2966
+ const client = (_slackClient?.getClient() ?? null);
2848
2967
  if (!client)
2849
2968
  return c.json({ error: "Slack not available" }, 503);
2850
2969
  const body = await c.req.json();
@@ -2867,7 +2986,7 @@ app.post("/api/slack/dm", async (c) => {
2867
2986
  const session = validateSession(sessionId);
2868
2987
  if (!session)
2869
2988
  return c.json({ error: "Invalid or expired session" }, 401);
2870
- const client = getSlackClient();
2989
+ const client = (_slackClient?.getClient() ?? null);
2871
2990
  if (!client)
2872
2991
  return c.json({ error: "Slack not available" }, 503);
2873
2992
  const body = await c.req.json();
@@ -2888,7 +3007,7 @@ app.get("/api/slack/channels", async (c) => {
2888
3007
  if (!session)
2889
3008
  return c.json({ error: "Invalid or expired session" }, 401);
2890
3009
  const types = c.req.query("types") || undefined;
2891
- const result = await listChannels({ types });
3010
+ const result = await _slackChannels.listChannels({ types });
2892
3011
  if (!result.ok)
2893
3012
  return c.json({ error: result.message }, 502);
2894
3013
  return c.json({ channels: result.channels });
@@ -2901,7 +3020,7 @@ app.get("/api/slack/channels/:id", async (c) => {
2901
3020
  const session = validateSession(sessionId);
2902
3021
  if (!session)
2903
3022
  return c.json({ error: "Invalid or expired session" }, 401);
2904
- const result = await getChannelInfo(c.req.param("id"));
3023
+ const result = await _slackChannels.getChannelInfo(c.req.param("id"));
2905
3024
  if (!result.ok)
2906
3025
  return c.json({ error: result.message }, 502);
2907
3026
  return c.json(result.channel);
@@ -2914,7 +3033,7 @@ app.post("/api/slack/channels/:id/join", async (c) => {
2914
3033
  const session = validateSession(sessionId);
2915
3034
  if (!session)
2916
3035
  return c.json({ error: "Invalid or expired session" }, 401);
2917
- const result = await joinChannel(c.req.param("id"));
3036
+ const result = await _slackChannels.joinChannel(c.req.param("id"));
2918
3037
  if (!result.ok)
2919
3038
  return c.json({ error: result.message }, 502);
2920
3039
  return c.json({ ok: true, message: result.message });
@@ -2928,7 +3047,7 @@ app.get("/api/slack/channels/:id/history", async (c) => {
2928
3047
  if (!session)
2929
3048
  return c.json({ error: "Invalid or expired session" }, 401);
2930
3049
  const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : undefined;
2931
- const result = await getChannelHistory(c.req.param("id"), { limit });
3050
+ const result = await _slackChannels.getChannelHistory(c.req.param("id"), { limit });
2932
3051
  if (!result.ok)
2933
3052
  return c.json({ error: result.message }, 502);
2934
3053
  return c.json({ messages: result.messages });
@@ -2941,7 +3060,7 @@ app.get("/api/slack/users/:id", async (c) => {
2941
3060
  const session = validateSession(sessionId);
2942
3061
  if (!session)
2943
3062
  return c.json({ error: "Invalid or expired session" }, 401);
2944
- const client = getSlackClient();
3063
+ const client = (_slackClient?.getClient() ?? null);
2945
3064
  if (!client)
2946
3065
  return c.json({ error: "Slack not available" }, 503);
2947
3066
  const result = await client.getUser(c.req.param("id"));
@@ -2986,14 +3105,14 @@ app.post("/api/resend/check-inbox", async (c) => {
2986
3105
  // --- WhatsApp routes (Twilio-backed) ---
2987
3106
  // WhatsApp status: check if configured
2988
3107
  app.get("/api/whatsapp/status", (c) => {
2989
- return c.json({ available: isWhatsAppConfigured() });
3108
+ return c.json({ available: (_channelsWhatsapp?.isWhatsAppConfigured() ?? false) });
2990
3109
  });
2991
3110
  // WhatsApp webhook: receive incoming messages from Twilio
2992
3111
  // Uses centralized verification via verifyWebhookRequest from the webhook registry.
2993
3112
  // Custom TwiML response handling prevents use of generic createWebhookRoute.
2994
3113
  app.post("/api/twilio/whatsapp", async (c) => {
2995
3114
  const rawBody = await c.req.text();
2996
- const params = parseFormBody(rawBody);
3115
+ const params = _webhooksTwilio.parseFormBody(rawBody);
2997
3116
  // Verify via centralized webhook system (handles secret resolution from config)
2998
3117
  const headers = {};
2999
3118
  c.req.raw.headers.forEach((value, key) => {
@@ -3008,24 +3127,24 @@ app.post("/api/twilio/whatsapp", async (c) => {
3008
3127
  }
3009
3128
  const payload = params;
3010
3129
  // Store the inbound message in history + update contacts
3011
- await processIncomingMessage(payload);
3130
+ await _webhooksTwilio.processIncomingMessage(payload);
3012
3131
  // Extract sender info
3013
3132
  const from = payload.From?.replace(/^whatsapp:/, "") ?? "";
3014
3133
  const body = payload.Body?.trim() ?? "";
3015
3134
  if (!body) {
3016
3135
  c.header("Content-Type", "text/xml");
3017
- return c.body(emptyTwimlResponse());
3136
+ return c.body(_webhooksTwilio.emptyTwimlResponse());
3018
3137
  }
3019
3138
  // Process through chat pipeline (async — Twilio allows up to 15s for response).
3020
3139
  // The service sends the reply via Twilio API, so we return empty TwiML to avoid
3021
3140
  // duplicate messages. If chat processing fails, we reply inline via TwiML as fallback.
3022
- const result = await handleWhatsAppMessage(from, body, payload.ProfileName);
3141
+ const result = await _servicesWhatsapp.handleWhatsAppMessage(from, body, payload.ProfileName);
3023
3142
  c.header("Content-Type", "text/xml");
3024
3143
  if (!result.ok && !result.reply) {
3025
3144
  // Chat failed and no reply was sent — respond inline so user isn't left hanging
3026
- return c.body(replyTwiml("Sorry, I couldn't process that right now. Please try again."));
3145
+ return c.body(_webhooksTwilio.replyTwiml("Sorry, I couldn't process that right now. Please try again."));
3027
3146
  }
3028
- return c.body(emptyTwimlResponse());
3147
+ return c.body(_webhooksTwilio.emptyTwimlResponse());
3029
3148
  });
3030
3149
  // WhatsApp relay: receive pre-verified messages from the Cloudflare Worker.
3031
3150
  // The Worker has already verified Twilio's signature — this endpoint verifies
@@ -3037,21 +3156,21 @@ app.post("/api/relay/whatsapp", async (c) => {
3037
3156
  headers[key.toLowerCase()] = value;
3038
3157
  });
3039
3158
  const relaySecret = process.env.RELAY_SECRET ?? "";
3040
- const verification = verifyRelaySignature(rawBody, headers, relaySecret);
3159
+ const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
3041
3160
  if (!verification.valid) {
3042
3161
  return c.json({ error: verification.error ?? "Invalid relay signature" }, 401);
3043
3162
  }
3044
- const params = parseFormBody(rawBody);
3163
+ const params = _webhooksTwilio.parseFormBody(rawBody);
3045
3164
  const payload = params;
3046
3165
  // Store inbound message + update contacts (same as direct webhook path)
3047
- await processIncomingMessage(payload);
3166
+ await _webhooksTwilio.processIncomingMessage(payload);
3048
3167
  const from = payload.From?.replace(/^whatsapp:/, "") ?? "";
3049
3168
  const body = payload.Body?.trim() ?? "";
3050
3169
  if (!body) {
3051
3170
  return c.json({ ok: true, message: "Empty message, skipped" });
3052
3171
  }
3053
3172
  // Process through chat pipeline — reply sent via Twilio API
3054
- const result = await handleWhatsAppMessage(from, body, payload.ProfileName);
3173
+ const result = await _servicesWhatsapp.handleWhatsAppMessage(from, body, payload.ProfileName);
3055
3174
  return c.json({ ok: result.ok, reply: result.reply, error: result.error });
3056
3175
  });
3057
3176
  // Resend relay: receive pre-verified inbound emails from the Cloudflare Worker.
@@ -3064,7 +3183,7 @@ app.post("/api/relay/resend", async (c) => {
3064
3183
  headers[key.toLowerCase()] = value;
3065
3184
  });
3066
3185
  const relaySecret = process.env.RELAY_SECRET ?? "";
3067
- const verification = verifyRelaySignature(rawBody, headers, relaySecret);
3186
+ const verification = _webhooksRelay.verifyRelaySignature(rawBody, headers, relaySecret);
3068
3187
  if (!verification.valid) {
3069
3188
  return c.json({ error: verification.error ?? "Invalid relay signature" }, 401);
3070
3189
  }
@@ -3115,7 +3234,7 @@ app.post("/api/whatsapp/send", async (c) => {
3115
3234
  const session = validateSession(sessionId);
3116
3235
  if (!session)
3117
3236
  return c.json({ error: "Invalid or expired session" }, 401);
3118
- const client = getWhatsAppClient();
3237
+ const client = (_channelsWhatsapp?.getClient() ?? null);
3119
3238
  if (!client)
3120
3239
  return c.json({ error: "WhatsApp not configured. Add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER to vault." }, 503);
3121
3240
  const body = await c.req.json();
@@ -3134,7 +3253,7 @@ app.get("/api/whatsapp/contacts", async (c) => {
3134
3253
  const session = validateSession(sessionId);
3135
3254
  if (!session)
3136
3255
  return c.json({ error: "Invalid or expired session" }, 401);
3137
- const client = getWhatsAppClient();
3256
+ const client = (_channelsWhatsapp?.getClient() ?? null);
3138
3257
  if (!client)
3139
3258
  return c.json({ error: "WhatsApp not configured" }, 503);
3140
3259
  const contacts = await client.listContacts();
@@ -3148,7 +3267,7 @@ app.get("/api/whatsapp/history", async (c) => {
3148
3267
  const session = validateSession(sessionId);
3149
3268
  if (!session)
3150
3269
  return c.json({ error: "Invalid or expired session" }, 401);
3151
- const client = getWhatsAppClient();
3270
+ const client = (_channelsWhatsapp?.getClient() ?? null);
3152
3271
  if (!client)
3153
3272
  return c.json({ error: "WhatsApp not configured" }, 503);
3154
3273
  const phone = c.req.query("phone") || undefined;
@@ -3802,18 +3921,18 @@ app.get("/api/contacts/graph/:id", async (c) => {
3802
3921
  // --- Credentials endpoints ---
3803
3922
  // List credentials (values masked).
3804
3923
  app.get("/api/credentials", async (c) => {
3805
- const store = getCredentialStore();
3924
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3806
3925
  if (!store)
3807
3926
  return c.json({ error: "Credentials not initialized" }, 503);
3808
3927
  const type = (c.req.query("type") || undefined);
3809
3928
  const search = c.req.query("search") || undefined;
3810
3929
  const creds = await store.list({ type, search });
3811
- const masked = creds.map((cr) => ({ ...cr, value: maskValue(cr.value) }));
3930
+ const masked = creds.map((cr) => ({ ...cr, value: _credentialStore.maskValue(cr.value) }));
3812
3931
  return c.json({ credentials: masked, count: masked.length });
3813
3932
  });
3814
3933
  // Get single credential (full value for reveal/copy).
3815
3934
  app.get("/api/credentials/:id", async (c) => {
3816
- const store = getCredentialStore();
3935
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3817
3936
  if (!store)
3818
3937
  return c.json({ error: "Credentials not initialized" }, 503);
3819
3938
  const cred = await store.get(c.req.param("id"));
@@ -3823,7 +3942,7 @@ app.get("/api/credentials/:id", async (c) => {
3823
3942
  });
3824
3943
  // Create credential.
3825
3944
  app.post("/api/credentials", async (c) => {
3826
- const store = getCredentialStore();
3945
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3827
3946
  if (!store)
3828
3947
  return c.json({ error: "Credentials not initialized" }, 503);
3829
3948
  const body = await c.req.json();
@@ -3839,7 +3958,7 @@ app.post("/api/credentials", async (c) => {
3839
3958
  });
3840
3959
  // Update credential.
3841
3960
  app.patch("/api/credentials/:id", async (c) => {
3842
- const store = getCredentialStore();
3961
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3843
3962
  if (!store)
3844
3963
  return c.json({ error: "Credentials not initialized" }, 503);
3845
3964
  const id = c.req.param("id");
@@ -3855,7 +3974,7 @@ app.patch("/api/credentials/:id", async (c) => {
3855
3974
  });
3856
3975
  // Archive credential (soft-delete).
3857
3976
  app.delete("/api/credentials/:id", async (c) => {
3858
- const store = getCredentialStore();
3977
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3859
3978
  if (!store)
3860
3979
  return c.json({ error: "Credentials not initialized" }, 503);
3861
3980
  const id = c.req.param("id");
@@ -3870,10 +3989,10 @@ app.delete("/api/credentials/:id", async (c) => {
3870
3989
  });
3871
3990
  // Migrate vault keys → credential store.
3872
3991
  app.post("/api/credentials/migrate-vault", async (c) => {
3873
- const store = getCredentialStore();
3992
+ const store = (_credentialStore ? _credentialStore.getCredentialStore() : null);
3874
3993
  if (!store)
3875
3994
  return c.json({ error: "Credentials not initialized" }, 503);
3876
- const vaultEntries = getVaultEntries();
3995
+ const vaultEntries = (_vaultStore ? _vaultStore.getVaultEntries() : []);
3877
3996
  if (vaultEntries.length === 0) {
3878
3997
  return c.json({ migrated: 0, message: "No vault keys to migrate" });
3879
3998
  }
@@ -4159,14 +4278,14 @@ app.get("/api/help/context", async (c) => {
4159
4278
  model: resolveChatModel() ?? "auto",
4160
4279
  sidecars: {
4161
4280
  search: isSearchAvailable(),
4162
- tts: isTtsAvailable(),
4163
- stt: isSttAvailable(),
4164
- avatar: isAvatarAvailable(),
4281
+ tts: (_ttsClient?.isTtsAvailable() ?? false),
4282
+ stt: (_sttClient?.isSttAvailable() ?? false),
4283
+ avatar: (_avatarSidecar?.isAvatarAvailable() ?? false),
4165
4284
  },
4166
4285
  integrations: {
4167
- google: isGoogleAuthenticated(),
4168
- slack: isSlackConfigured(),
4169
- whatsapp: isWhatsAppConfigured(),
4286
+ google: (_googleAuth?.isGoogleAuthenticated() ?? false),
4287
+ slack: (_slackClient?.isSlackConfigured() ?? false),
4288
+ whatsapp: (_channelsWhatsapp?.isWhatsAppConfigured() ?? false),
4170
4289
  },
4171
4290
  recentActivity: last20.map((e) => ({
4172
4291
  timestamp: e.timestamp,
@@ -4303,7 +4422,7 @@ app.get("/api/browse", async (c) => {
4303
4422
  catch {
4304
4423
  return c.json({ error: "Invalid URL" }, 400);
4305
4424
  }
4306
- const result = await browseUrl(url);
4425
+ const result = await _browse.browseUrl(url);
4307
4426
  if (!result) {
4308
4427
  return c.json({ error: "Failed to fetch URL — timeout, non-HTML content, or network error" }, 502);
4309
4428
  }
@@ -4409,9 +4528,9 @@ app.get("/api/ops/health", async (c) => {
4409
4528
  models: settings.models,
4410
4529
  sidecars: {
4411
4530
  search: isSidecarAvailable(),
4412
- tts: isTtsAvailable(),
4413
- stt: isSttAvailable(),
4414
- avatar: isAvatarAvailable(),
4531
+ tts: (_ttsClient?.isTtsAvailable() ?? false),
4532
+ stt: (_sttClient?.isSttAvailable() ?? false),
4533
+ avatar: (_avatarSidecar?.isAvatarAvailable() ?? false),
4415
4534
  },
4416
4535
  agents: {
4417
4536
  total: agents.length,
@@ -4434,20 +4553,20 @@ app.get("/api/ops/sidecars", async (c) => {
4434
4553
  port: parseInt(resolveEnv("SEARCH_PORT") ?? "3578", 10),
4435
4554
  },
4436
4555
  tts: {
4437
- available: isTtsAvailable(),
4556
+ available: (_ttsClient?.isTtsAvailable() ?? false),
4438
4557
  enabled: settings.tts.enabled,
4439
4558
  port: settings.tts.port,
4440
4559
  voice: settings.tts.voice,
4441
4560
  autoPlay: settings.tts.autoPlay,
4442
4561
  },
4443
4562
  stt: {
4444
- available: isSttAvailable(),
4563
+ available: (_sttClient?.isSttAvailable() ?? false),
4445
4564
  enabled: settings.stt.enabled,
4446
4565
  port: settings.stt.port,
4447
4566
  model: settings.stt.model,
4448
4567
  },
4449
4568
  avatar: {
4450
- available: isAvatarAvailable(),
4569
+ available: (_avatarSidecar?.isAvatarAvailable() ?? false),
4451
4570
  enabled: settings.avatar.enabled,
4452
4571
  port: settings.avatar.port,
4453
4572
  },
@@ -4463,16 +4582,16 @@ app.post("/api/ops/sidecars/:name/restart", async (c) => {
4463
4582
  ok = await startSidecar();
4464
4583
  break;
4465
4584
  case "tts":
4466
- stopTtsSidecar();
4467
- ok = await startTtsSidecar();
4585
+ _ttsSidecar?.stopTtsSidecar();
4586
+ ok = await _ttsSidecar?.startTtsSidecar() ?? false;
4468
4587
  break;
4469
4588
  case "stt":
4470
- stopSttSidecar();
4471
- ok = await startSttSidecar();
4589
+ _sttSidecar?.stopSttSidecar();
4590
+ ok = await _sttSidecar?.startSttSidecar() ?? false;
4472
4591
  break;
4473
4592
  case "avatar":
4474
- stopAvatarSidecar();
4475
- ok = await startAvatarSidecar();
4593
+ _avatarSidecar?.stopAvatarSidecar();
4594
+ ok = await _avatarSidecar?.startAvatarSidecar() ?? false;
4476
4595
  break;
4477
4596
  default:
4478
4597
  return c.json({ error: `Unknown sidecar: ${name}` }, 400);
@@ -4775,8 +4894,11 @@ app.post("/api/chat", async (c) => {
4775
4894
  return c.json({ error: "Invalid or expired session" }, 401);
4776
4895
  }
4777
4896
  const cs = await getOrCreateChatSession(sessionId, session.name);
4897
+ // Route to thread history if threadId is provided
4898
+ const requestThreadId = body.threadId;
4899
+ switchSessionThread(cs, requestThreadId || null, sessionId);
4778
4900
  // Reset continuation rounds when user sends a real message
4779
- resetContinuation(sessionId);
4901
+ _agentAutonomous?.resetContinuation(sessionId);
4780
4902
  // Inject user-chat tension into the nervous system
4781
4903
  getPressureIntegrator()?.addUserChatTension();
4782
4904
  // Start goal timer on first chat call
@@ -4784,25 +4906,40 @@ app.post("/api/chat", async (c) => {
4784
4906
  goalTimerStarted = true;
4785
4907
  startGoalTimer({ brain: cs.brain, humanName: session.name });
4786
4908
  // Start Google polling timers if already authenticated
4787
- if (isCalendarAvailable()) {
4788
- startCalendarTimer();
4909
+ if ((_googleCalendar?.isCalendarAvailable() ?? false)) {
4910
+ _googleCalendarTimer?.startCalendarTimer();
4789
4911
  }
4790
- if (isGmailAvailable()) {
4791
- startGmailTimer();
4912
+ if ((_googleGmail?.isGmailAvailable() ?? false)) {
4913
+ _googleGmailTimer?.startGmailTimer();
4792
4914
  }
4793
- if (isTasksAvailable()) {
4794
- startTasksTimer();
4915
+ if ((_googleTasks?.isTasksAvailable() ?? false)) {
4916
+ _googleTasksTimer?.startTasksTimer();
4795
4917
  }
4796
4918
  }
4797
4919
  // Helper: persist session to disk (fire-and-forget)
4798
4920
  const persistSession = () => {
4799
4921
  const key = sessionKeys.get(sessionId);
4800
4922
  if (key) {
4923
+ // Collect threads for persistence
4924
+ const threads = getThreadsForSession(sessionId);
4925
+ const threadList = [...threads.values()].map((t) => ({
4926
+ id: t.id,
4927
+ title: t.title,
4928
+ // If this thread is active, its live history is in cs.history
4929
+ history: (cs.activeThreadId === t.id) ? cs.history : t.history,
4930
+ historySummary: (cs.activeThreadId === t.id) ? cs.historySummary : t.historySummary,
4931
+ createdAt: t.createdAt,
4932
+ updatedAt: t.updatedAt,
4933
+ }));
4934
+ // Persist main history (from mainHistory if a thread is active, else cs.history)
4935
+ const mainHistory = cs.activeThreadId ? cs.mainHistory : cs.history;
4936
+ const mainSummary = cs.activeThreadId ? cs.mainHistorySummary : cs.historySummary;
4801
4937
  saveSession(sessionId, {
4802
- history: cs.history,
4938
+ history: mainHistory,
4803
4939
  fileContext: cs.fileContext,
4804
4940
  learnedPaths: cs.learnedPaths,
4805
- historySummary: cs.historySummary,
4941
+ historySummary: mainSummary,
4942
+ threads: threadList.length > 0 ? threadList : undefined,
4806
4943
  }, key).catch(() => { });
4807
4944
  }
4808
4945
  // Auto fold-back: fire once per session when user messages reach threshold
@@ -4841,14 +4978,14 @@ app.post("/api/chat", async (c) => {
4841
4978
  if (callMatch) {
4842
4979
  const to = callMatch[1] || undefined;
4843
4980
  const msg = callMatch[2]?.trim() || undefined;
4844
- const result = await makeCall({ to, message: msg });
4981
+ const result = await _twilioCall.makeCall({ to, message: msg });
4845
4982
  return c.json({ system: true, content: result.message });
4846
4983
  }
4847
4984
  // --- Handle "email <address> subject: <subject> body: <body>" command ---
4848
4985
  const emailMatch = message.match(/^(?:email|send email|mail)\s+(\S+@\S+)\s+subject:\s*(.+?)\s+body:\s*([\s\S]+)$/i);
4849
4986
  if (emailMatch) {
4850
4987
  const [, to, subject, body] = emailMatch;
4851
- const result = await sendEmail({ to: to.trim(), subject: subject.trim(), body: body.trim() });
4988
+ const result = await _googleGmailSend.sendEmail({ to: to.trim(), subject: subject.trim(), body: body.trim() });
4852
4989
  logActivity({ source: "gmail", summary: `Email sent to ${to.trim()}: "${subject.trim()}"`, actionLabel: "PROMPTED", reason: "user sent email via chat" });
4853
4990
  return c.json({ system: true, content: result.message });
4854
4991
  }
@@ -4922,7 +5059,7 @@ app.post("/api/chat", async (c) => {
4922
5059
  // --- Handle "auto" / "autonomous" command ---
4923
5060
  const autoMatch = message.match(/^(?:auto|autonomous)\s*$/i);
4924
5061
  if (autoMatch) {
4925
- const status = getAutonomousStatus();
5062
+ const status = (_agentAutonomous?.getAutonomousStatus() ?? null);
4926
5063
  const lines = [
4927
5064
  `**Autonomous Work Loop**`,
4928
5065
  ``,
@@ -5151,10 +5288,10 @@ app.post("/api/chat", async (c) => {
5151
5288
  });
5152
5289
  }
5153
5290
  // --- URL browse injection ---
5154
- const detectedUrl = detectUrl(chatMessage);
5291
+ const detectedUrl = _browse?.detectUrl(chatMessage);
5155
5292
  if (detectedUrl) {
5156
5293
  logActivity({ source: "browse", summary: `Auto-browsing URL in message: ${detectedUrl}`, actionLabel: "PROMPTED", reason: "user message contained URL" });
5157
- const browseResult = await browseUrl(detectedUrl);
5294
+ const browseResult = await _browse.browseUrl(detectedUrl);
5158
5295
  if (browseResult) {
5159
5296
  const browseMsg = {
5160
5297
  role: "system",
@@ -5175,21 +5312,25 @@ app.post("/api/chat", async (c) => {
5175
5312
  const doc = await findBrainDocument(chatMessage);
5176
5313
  if (doc) {
5177
5314
  brainDocFound = true;
5315
+ const siblingNote = doc.siblings && doc.siblings.length > 0
5316
+ ? `\nAlso in the same directory: ${doc.siblings.join(", ")}\nYou can ask to see any of these files.`
5317
+ : "";
5178
5318
  const docMsg = {
5179
5319
  role: "system",
5180
5320
  content: [
5181
5321
  `--- Brain document: ${doc.filename} ---`,
5182
5322
  doc.content,
5183
5323
  `--- End ${doc.filename} ---`,
5184
- `This document was found in your brain files. Use it to answer the user's question.`,
5324
+ `This document was found in your brain files. Use it to answer the user's question.${siblingNote}`,
5185
5325
  ].join("\n"),
5186
5326
  };
5187
5327
  ctx.messages.splice(1, 0, docMsg);
5188
5328
  logActivity({ source: "system", summary: `Auto-loaded brain document: ${doc.filename}`, actionLabel: "PROMPTED", reason: "user message referenced a brain document" });
5189
5329
  }
5190
5330
  }
5191
- catch {
5331
+ catch (err) {
5192
5332
  // Non-critical — fall through to web search
5333
+ console.error("[brain-docs] findBrainDocument error:", err instanceof Error ? err.message : String(err));
5193
5334
  }
5194
5335
  // --- Context provider injection (web search, calendar, email) ---
5195
5336
  {
@@ -5544,6 +5685,8 @@ app.post("/api/chat", async (c) => {
5544
5685
  reqSignal?.addEventListener("abort", onAbort, { once: true });
5545
5686
  // Token buffer for split-placeholder rehydration
5546
5687
  let tokenBuf2 = "";
5688
+ const streamStartMs2 = performance.now();
5689
+ const streamModel2 = activeChatModel ?? (activeProvider === "ollama" ? "llama3.2:3b" : "claude-sonnet-4");
5547
5690
  const flushBuf2 = () => {
5548
5691
  if (!tokenBuf2)
5549
5692
  return;
@@ -5568,6 +5711,12 @@ app.post("/api/chat", async (c) => {
5568
5711
  onDone: async () => {
5569
5712
  flushBuf2(); // flush remainder
5570
5713
  reqSignal?.removeEventListener("abort", onAbort);
5714
+ logLlmCall({
5715
+ ts: new Date().toISOString(), mode: "stream",
5716
+ provider: activeProvider, model: streamModel2,
5717
+ durationMs: Math.round(performance.now() - streamStartMs2),
5718
+ outputTokens: Math.ceil(fullResponse.length / 4), ok: true,
5719
+ });
5571
5720
  // Process action blocks BEFORE sending done — ensures SSE events reach client before stream closes.
5572
5721
  // ALWAYS strip [AGENT_REQUEST] blocks from response, even if they can't be parsed.
5573
5722
  // Capture raw block content first for logging/parsing.
@@ -5740,20 +5889,20 @@ app.post("/api/chat", async (c) => {
5740
5889
  }
5741
5890
  }).catch(() => { });
5742
5891
  // Fire-and-forget: generate avatar video (TTS → MuseTalk → MP4)
5743
- if (isAvatarAvailable() && isTtsAvailable() && fullResponse) {
5892
+ if ((_avatarSidecar?.isAvatarAvailable() ?? false) && (_ttsClient?.isTtsAvailable() ?? false) && fullResponse) {
5744
5893
  const trimmedText = fullResponse.slice(0, 2000);
5745
- synthesize(trimmedText).then(async (wavBuffer) => {
5894
+ _ttsClient.synthesize(trimmedText).then(async (wavBuffer) => {
5746
5895
  if (!wavBuffer)
5747
5896
  return;
5748
- const cached = await getCachedVideo(wavBuffer);
5897
+ const cached = await _avatarClient.getCachedVideo(wavBuffer);
5749
5898
  if (cached) {
5750
5899
  pushPendingVideo(cached);
5751
5900
  return;
5752
5901
  }
5753
- const mp4 = await generateVideo(wavBuffer);
5902
+ const mp4 = await _avatarClient.generateVideo(wavBuffer);
5754
5903
  if (!mp4)
5755
5904
  return;
5756
- const filename = await cacheVideo(mp4, wavBuffer);
5905
+ const filename = await _avatarClient.cacheVideo(mp4, wavBuffer);
5757
5906
  pushPendingVideo(filename);
5758
5907
  logActivity({ source: "avatar", summary: "Generated avatar video", actionLabel: "PROMPTED", reason: "user conversation triggered avatar" });
5759
5908
  }).catch((err) => {
@@ -5796,6 +5945,12 @@ app.post("/api/chat", async (c) => {
5796
5945
  // If this error is from an abort, save partial and exit quietly
5797
5946
  if (reqSignal?.aborted) {
5798
5947
  savePartial();
5948
+ logLlmCall({
5949
+ ts: new Date().toISOString(), mode: "stream",
5950
+ provider: activeProvider, model: streamModel2,
5951
+ durationMs: Math.round(performance.now() - streamStartMs2),
5952
+ outputTokens: Math.ceil(fullResponse.length / 4), ok: true, error: "client_abort",
5953
+ });
5799
5954
  }
5800
5955
  else {
5801
5956
  let errorMsg = err instanceof LLMError ? err.userMessage : (err.message || "Stream error");
@@ -5808,6 +5963,12 @@ app.post("/api/chat", async (c) => {
5808
5963
  if (!health.ok)
5809
5964
  errorMsg += " — Check that Ollama is running. " + health.message;
5810
5965
  }
5966
+ logLlmCall({
5967
+ ts: new Date().toISOString(), mode: "stream",
5968
+ provider: activeProvider, model: streamModel2,
5969
+ durationMs: Math.round(performance.now() - streamStartMs2),
5970
+ ok: false, error: errorMsg,
5971
+ });
5811
5972
  stream.writeSSE({ data: JSON.stringify({ error: errorMsg, errorDetail: rawDetail }) }).catch(() => { });
5812
5973
  }
5813
5974
  resolve(); // Still resolve so stream closes
@@ -5924,12 +6085,111 @@ async function start(opts) {
5924
6085
  const tierGate = await import("./tier/gate.js");
5925
6086
  // Initialize instance name before anything else
5926
6087
  initInstanceName();
5927
- // Initialize OpenTelemetry tracing (must be early, before instrumented code)
5928
- initTracing({
5929
- serviceName: `${getInstanceNameLower()}-brain`,
5930
- serviceVersion: "0.1.0",
5931
- consoleExport: process.env.OTEL_CONSOLE_EXPORT !== "false",
5932
- });
6088
+ // --- Dynamic imports: load gated modules based on tier ---
6089
+ // EXT-BYOK: vault, voice, integrations, notifications, etc.
6090
+ if (tierGate.meetsMinimum(tier, "byok")) {
6091
+ [
6092
+ _browse, _ttsSidecar, _ttsClient, _sttSidecar, _sttClient,
6093
+ _avatarSidecar, _avatarClient, _settingsVoice,
6094
+ _vaultStore, _vaultTransfer, _integrationsGate, _twilioCall,
6095
+ _googleAuth, _googleCalendar, _googleTemporal, _googleCalendarTimer,
6096
+ _googleGmail, _googleGmailTimer, _googleTasksTimer, _googleGmailSend,
6097
+ _googleDocs, _googleTasks,
6098
+ _notifications, _credentialStore, _githubWebhooks, _integrationsGithub,
6099
+ _slackClient, _slackWebhooks, _slackChannels,
6100
+ _channelsWhatsapp, _webhooksTwilio, _resendWebhooks, _servicesWhatsapp,
6101
+ _webhooksMount, _webhooksConfig, _webhooksRegistry, _webhooksRelay,
6102
+ _volumes, _mdns,
6103
+ ] = await Promise.all([
6104
+ import("./search/browse.js"),
6105
+ import("./tts/sidecar.js"),
6106
+ import("./tts/client.js"),
6107
+ import("./stt/sidecar.js"),
6108
+ import("./stt/client.js"),
6109
+ import("./avatar/sidecar.js"),
6110
+ import("./avatar/client.js"),
6111
+ import("./settings.js"),
6112
+ import("./vault/store.js"),
6113
+ import("./vault/transfer.js"),
6114
+ import("./integrations/gate.js"),
6115
+ import("./twilio/call.js"),
6116
+ import("./google/auth.js"),
6117
+ import("./google/calendar.js"),
6118
+ import("./google/temporal.js"),
6119
+ import("./google/calendar-timer.js"),
6120
+ import("./google/gmail.js"),
6121
+ import("./google/gmail-timer.js"),
6122
+ import("./google/tasks-timer.js"),
6123
+ import("./google/gmail-send.js"),
6124
+ import("./google/docs.js"),
6125
+ import("./google/tasks.js"),
6126
+ import("./notifications/index.js"),
6127
+ import("./credentials/store.js"),
6128
+ import("./github/webhooks.js"),
6129
+ import("./integrations/github.js"),
6130
+ import("./slack/client.js"),
6131
+ import("./slack/webhooks.js"),
6132
+ import("./slack/channels.js"),
6133
+ import("./channels/whatsapp.js"),
6134
+ import("./webhooks/twilio.js"),
6135
+ import("./resend/webhooks.js"),
6136
+ import("./services/whatsapp.js"),
6137
+ import("./webhooks/mount.js"),
6138
+ import("./webhooks/config.js"),
6139
+ import("./webhooks/registry.js"),
6140
+ import("./webhooks/relay.js"),
6141
+ import("./volumes/index.js"),
6142
+ import("./mdns.js"),
6143
+ ]);
6144
+ // Initialize alerting (requires notifications module)
6145
+ alertDispatcher = new _notifications.NotificationDispatcher();
6146
+ alertManager = new AlertManager(health, defaultAlertConfig(), alertDispatcher);
6147
+ // Initialize volume manager
6148
+ volumeManager = new _volumes.VolumeManager(BRAIN_DIR);
6149
+ volumeManager.init().catch((err) => log.warn("Volume manager init failed — single-volume mode", { error: String(err) }));
6150
+ // Initialize webhook providers (now that modules are loaded)
6151
+ initWebhookProviders();
6152
+ log.info(`BYOK-tier modules loaded (tier: ${tier})`);
6153
+ }
6154
+ else {
6155
+ // Local tier: alerting with no-op dispatcher
6156
+ alertDispatcher = null;
6157
+ alertManager = new AlertManager(health, defaultAlertConfig(), { dispatch: async () => { } });
6158
+ log.info(`Local-tier — BYOK modules skipped (tier: ${tier})`);
6159
+ }
6160
+ // EXT-SPAWN: agent runtime, instance manager, pool, workflow
6161
+ if (tierGate.canSpawn(tier)) {
6162
+ [_agentMemory, _agentLocks, _agentAutonomous, _agentRuntime, _agentInstanceManager, _agentPoolMod, _agentWorkflow] = await Promise.all([
6163
+ import("./agents/memory.js"),
6164
+ import("./agents/locks.js"),
6165
+ import("./agents/autonomous.js"),
6166
+ import("./agents/runtime/index.js"),
6167
+ import("./agents/instance-manager.js"),
6168
+ import("./agents/runtime.js"),
6169
+ import("./agents/workflow.js"),
6170
+ ]);
6171
+ log.info(`Spawn-tier modules loaded (tier: ${tier})`);
6172
+ }
6173
+ // EXT-HOSTED: tracing, browser
6174
+ if (tierGate.meetsMinimum(tier, "hosted")) {
6175
+ [_tracingTracer, _tracingInit, _tracingMiddleware, _tracingBridge, _browser] = await Promise.all([
6176
+ import("./tracing/tracer.js"),
6177
+ import("./tracing/init.js"),
6178
+ import("./tracing/middleware.js"),
6179
+ import("./tracing/bridge.js"),
6180
+ import("./capabilities/definitions/browser.js"),
6181
+ ]);
6182
+ tracer = new _tracingTracer.Tracer();
6183
+ log.info(`Hosted-tier modules loaded (tier: ${tier})`);
6184
+ }
6185
+ // Initialize OpenTelemetry tracing (must be early, before instrumented code) — hosted tier only
6186
+ if (_tracingInit) {
6187
+ _tracingInit.initTracing({
6188
+ serviceName: `${getInstanceNameLower()}-brain`,
6189
+ serviceVersion: "0.1.0",
6190
+ consoleExport: process.env.OTEL_CONSOLE === "1",
6191
+ });
6192
+ }
5933
6193
  // Load settings (airplane mode, model selection)
5934
6194
  const settings = await loadSettings();
5935
6195
  // Initialize posture system (UI surface assembly)
@@ -5953,13 +6213,18 @@ async function start(opts) {
5953
6213
  if (restored) {
5954
6214
  sessionKeys.set(restored.session.id, restored.sessionKey);
5955
6215
  setEncryptionKey(restored.sessionKey);
5956
- await loadVault(restored.sessionKey);
6216
+ if (_vaultStore)
6217
+ await _vaultStore.loadVault(restored.sessionKey);
5957
6218
  log.info("Session restored (no re-auth needed)");
5958
6219
  }
5959
6220
  return code;
5960
6221
  })(),
5961
- // Start all sidecars in parallel
5962
- Promise.all([startSidecar(), startTtsSidecar(), startSttSidecar()]),
6222
+ // Start all sidecars in parallel (search always, tts/stt if BYOK)
6223
+ Promise.all([
6224
+ startSidecar(),
6225
+ _ttsSidecar ? _ttsSidecar.startTtsSidecar() : Promise.resolve(false),
6226
+ _sttSidecar ? _sttSidecar.startSttSidecar() : Promise.resolve(false),
6227
+ ]),
5963
6228
  ]);
5964
6229
  const code = pairingCode;
5965
6230
  const [searchAvailable, ttsAvailable, sttAvailable] = sidecarResults;
@@ -5996,15 +6261,29 @@ async function start(opts) {
5996
6261
  log.info(`Loaded ${notifCount} pending notification(s) from disk`);
5997
6262
  // Register sidecar health checks (optional — degraded, not unhealthy)
5998
6263
  health.register("search", availabilityCheck(isSidecarAvailable, "search"), { critical: false });
5999
- health.register("tts", availabilityCheck(isTtsAvailable, "tts"), { critical: false });
6000
- health.register("stt", availabilityCheck(isSttAvailable, "stt"), { critical: false });
6264
+ if (_ttsClient)
6265
+ health.register("tts", availabilityCheck(_ttsClient.isTtsAvailable, "tts"), { critical: false });
6266
+ if (_sttClient)
6267
+ health.register("stt", availabilityCheck(_sttClient.isSttAvailable, "stt"), { critical: false });
6001
6268
  // Google Workspace health checks (optional — degraded if not connected)
6002
- health.register("google_calendar", availabilityCheck(isCalendarAvailable, "Google Calendar"), { critical: false });
6269
+ if (_googleCalendar)
6270
+ health.register("google_calendar", availabilityCheck(_googleCalendar.isCalendarAvailable, "Google Calendar"), { critical: false });
6003
6271
  // Initialize FileManager (file registry + storage for uploads, visual memory, etc.)
6004
6272
  await FileManager.init(BRAIN_DIR, join(BRAIN_DIR, "files", "storage"));
6005
6273
  // Initialize Library store (virtual folders over file registry)
6006
6274
  createLibraryStore(BRAIN_DIR);
6007
6275
  initBrainShadow(BRAIN_DIR);
6276
+ // Initialize Brain RAG (semantic file search)
6277
+ const rag = new BrainRAG();
6278
+ await rag.load();
6279
+ setBrainRAG(rag);
6280
+ stopFileWatcher = watchBrain(rag);
6281
+ // Background index — never block startup
6282
+ rag.indexAll().then((r) => {
6283
+ log.info(`Brain RAG index: ${r.indexed} indexed, ${r.skipped} skipped, ${r.errors} errors`);
6284
+ }).catch((err) => {
6285
+ log.error("Brain RAG indexAll failed", { error: err instanceof Error ? err.message : String(err) });
6286
+ });
6008
6287
  // Register board provider (local queue, always available)
6009
6288
  const queueProvider = new QueueBoardProvider(BRAIN_DIR);
6010
6289
  queueProvider.setOnProjectlessTask((identifier, title) => {
@@ -6014,7 +6293,8 @@ async function start(opts) {
6014
6293
  queueProvider.getStore().setOnStateTransition(async (task, from, to) => {
6015
6294
  if (to === "done" || to === "cancelled") {
6016
6295
  try {
6017
- await rememberTaskCompletion(task, from);
6296
+ if (_agentMemory)
6297
+ await _agentMemory.rememberTaskCompletion(task, from);
6018
6298
  }
6019
6299
  catch (err) {
6020
6300
  log.warn("Failed to record task completion memory", {
@@ -6029,10 +6309,13 @@ async function start(opts) {
6029
6309
  startSchedulingTimer(schedulingStore);
6030
6310
  createContactStore(BRAIN_DIR);
6031
6311
  createCalendarStore(BRAIN_DIR);
6032
- const credStore = createCredentialStore(BRAIN_DIR);
6033
- await credStore.hydrate();
6312
+ if (_credentialStore) {
6313
+ const credStore = _credentialStore.createCredentialStore(BRAIN_DIR);
6314
+ await credStore.hydrate();
6315
+ }
6034
6316
  initTraining();
6035
- initGitHub();
6317
+ if (_integrationsGithub)
6318
+ _integrationsGithub.initGitHub();
6036
6319
  // Start metrics collector (system, HTTP, agent metrics at 30s interval)
6037
6320
  // brainDir enables tiered aggregation (hourly/daily rollups)
6038
6321
  startCollector(metricsStore, undefined, BRAIN_DIR);
@@ -6041,26 +6324,33 @@ async function start(opts) {
6041
6324
  // of skills/module init, so running them concurrently improves startup time (DASH-60).
6042
6325
  // Note: initAgents() only creates directories — recovery runs after the runtime
6043
6326
  // is ready so the monitor can skip runtime-managed tasks (DASH-82 fix).
6044
- const [, moduleRegistry, , runtime] = await Promise.all([
6327
+ const initPromises = [
6045
6328
  _skillRegistry.refresh(),
6046
6329
  Promise.resolve(createModuleRegistry(BRAIN_DIR)),
6047
6330
  initAgents(),
6048
- createRuntime(),
6049
- ]);
6331
+ ];
6332
+ if (_agentRuntime)
6333
+ initPromises.push(_agentRuntime.createRuntime());
6334
+ else
6335
+ initPromises.push(Promise.resolve(null));
6336
+ const [, moduleRegistry, , runtime] = await Promise.all(initPromises);
6050
6337
  const skillRegistry = _skillRegistry;
6051
6338
  // Recover tasks from previous session AFTER runtime is initialized.
6052
6339
  // The monitor checks the runtime registry to skip tasks that RuntimeManager
6053
6340
  // already handles, preventing the double-recovery race (DASH-82).
6054
6341
  await recoverAndStartMonitor();
6055
- tracer.attachToBus(runtime.bus);
6056
- attachOTelToBus(runtime.bus);
6342
+ if (tracer && runtime?.bus)
6343
+ tracer.attachToBus(runtime.bus);
6344
+ if (_tracingBridge && runtime?.bus)
6345
+ _tracingBridge.attachOTelToBus(runtime.bus);
6057
6346
  // Initialize capability registry (action blocks + context providers)
6058
6347
  const capRegistry = createCapabilityRegistry();
6059
6348
  capRegistry.register(calendarCapability);
6060
6349
  capRegistry.register(emailCapability);
6061
6350
  capRegistry.register(docsCapability);
6062
6351
  capRegistry.register(boardCapability);
6063
- capRegistry.register(browserCapability);
6352
+ if (_browser)
6353
+ capRegistry.register(_browser.browserCapability);
6064
6354
  // Meta capabilities
6065
6355
  capRegistry.register(taskDoneCapability);
6066
6356
  // Context providers — replace hardcoded injection logic
@@ -6073,11 +6363,12 @@ async function start(opts) {
6073
6363
  }));
6074
6364
  capRegistry.register(vaultContextProvider);
6075
6365
  // Start autonomous work timer (60-min coma failsafe — primary trigger is now tension-based)
6076
- startAutonomousTimer();
6366
+ if (_agentAutonomous)
6367
+ _agentAutonomous.startAutonomousTimer();
6077
6368
  // Initialize metabolic pulse (tension-based heartbeat)
6078
6369
  const pulseSettings = getPulseSettings();
6079
- if (pulseSettings.mode !== "timer") {
6080
- const pulse = initPressureIntegrator(triggerPulse, { threshold: pulseSettings.threshold });
6370
+ if (pulseSettings.mode !== "timer" && _agentAutonomous) {
6371
+ const pulse = initPressureIntegrator(_agentAutonomous.triggerPulse, { threshold: pulseSettings.threshold });
6081
6372
  log.info(`Metabolic pulse initialized: Θ=${pulseSettings.threshold}mV, mode=${pulseSettings.mode}`);
6082
6373
  // Boot scan: if todos already exist, inject tension so Core starts working immediately
6083
6374
  const todoCount = (await queueProvider.getStore().list()).filter((t) => t.state === "todo").length;
@@ -6111,17 +6402,17 @@ async function start(opts) {
6111
6402
  // Start open loop scanner (ambient resonance matching)
6112
6403
  startOpenLoopScanner();
6113
6404
  // Start Google polling timers at boot if already authenticated (don't wait for first chat)
6114
- if (isGmailAvailable())
6115
- startGmailTimer();
6116
- if (isCalendarAvailable()) {
6117
- startCalendarTimer();
6405
+ if (_googleGmail?.isGmailAvailable())
6406
+ _googleGmailTimer.startGmailTimer();
6407
+ if (_googleCalendar?.isCalendarAvailable()) {
6408
+ _googleCalendarTimer.startCalendarTimer();
6118
6409
  // Initial sync: pull Google events into local calendar store
6119
6410
  getGoogleCalendarAdapter().sync().catch((err) => {
6120
6411
  log.warn("Initial Google Calendar sync failed", { error: err instanceof Error ? err.message : String(err) });
6121
6412
  });
6122
6413
  }
6123
- if (isTasksAvailable())
6124
- startTasksTimer();
6414
+ if (_googleTasks?.isTasksAvailable())
6415
+ _googleTasksTimer.startTasksTimer();
6125
6416
  // Initialize plugin registry (authenticate + start all registered plugins)
6126
6417
  await initPlugins().catch((err) => {
6127
6418
  log.warn("Plugin init failed", { error: err instanceof Error ? err.message : String(err) });
@@ -6130,7 +6421,8 @@ async function start(opts) {
6130
6421
  setOnBatchComplete(async (sessionId, results) => {
6131
6422
  try {
6132
6423
  logActivity({ source: "agent", summary: `Batch complete: session=${sessionId}, ${results.length} result(s)` });
6133
- await continueAfterBatch(sessionId, results);
6424
+ if (_agentAutonomous)
6425
+ await _agentAutonomous.continueAfterBatch(sessionId, results);
6134
6426
  }
6135
6427
  catch (err) {
6136
6428
  const msg = err instanceof Error ? err.message : String(err);
@@ -6138,15 +6430,15 @@ async function start(opts) {
6138
6430
  }
6139
6431
  });
6140
6432
  // Initialize agent spawning — tier >= spawn only
6141
- if (tierGate.canSpawn(tier)) {
6433
+ if (tierGate.canSpawn(tier) && _agentInstanceManager && _agentPoolMod && _agentWorkflow && runtime) {
6142
6434
  // Initialize instance manager (GC, health checks, load balancing)
6143
- instanceManager = new AgentInstanceManager(runtime);
6435
+ instanceManager = new _agentInstanceManager.AgentInstanceManager(runtime);
6144
6436
  await instanceManager.init();
6145
6437
  // Initialize agent pool (circuit breakers, isolation, resource management)
6146
- agentPool = AgentPool.fromExisting(runtime, instanceManager);
6438
+ agentPool = _agentPoolMod.AgentPool.fromExisting(runtime, instanceManager);
6147
6439
  setAgentPool(agentPool);
6148
6440
  // Initialize workflow engine for multi-agent coordination
6149
- workflowEngine = new WorkflowEngine(agentPool);
6441
+ workflowEngine = new _agentWorkflow.WorkflowEngine(agentPool);
6150
6442
  await workflowEngine.loadAllDefinitions().catch((err) => {
6151
6443
  log.warn("Failed to load workflow definitions", { error: err instanceof Error ? err.message : String(err) });
6152
6444
  });
@@ -6162,7 +6454,7 @@ async function start(opts) {
6162
6454
  health.register("board", boardCheck(() => getBoardProvider()), { critical: false });
6163
6455
  // Agent runtime capacity: resource utilization
6164
6456
  health.register("agent_capacity", agentCapacityCheck(() => {
6165
- const rt = getRuntime();
6457
+ const rt = _agentRuntime ? _agentRuntime.getRuntime() : null;
6166
6458
  return rt ? rt.getResourceSnapshot() : null;
6167
6459
  }), { critical: false });
6168
6460
  // Agent instance health: aggregate health scores
@@ -6172,17 +6464,20 @@ async function start(opts) {
6172
6464
  // --- Register auto-recovery actions ---
6173
6465
  // Sidecar recovery: restart if unavailable for 3+ checks
6174
6466
  recovery.register(sidecarRecovery("search", "search", stopSidecar, startSidecar));
6175
- recovery.register(sidecarRecovery("tts", "tts", stopTtsSidecar, startTtsSidecar));
6176
- recovery.register(sidecarRecovery("stt", "stt", stopSttSidecar, startSttSidecar));
6467
+ if (_ttsSidecar)
6468
+ recovery.register(sidecarRecovery("tts", "tts", _ttsSidecar.stopTtsSidecar, _ttsSidecar.startTtsSidecar));
6469
+ if (_sttSidecar)
6470
+ recovery.register(sidecarRecovery("stt", "stt", _sttSidecar.stopSttSidecar, _sttSidecar.startSttSidecar));
6177
6471
  // Start recovery loop (evaluates every 30s)
6178
6472
  recovery.start();
6179
6473
  // Start alert evaluation loop (evaluates every 30s)
6180
- alertManager.start();
6474
+ if (alertManager)
6475
+ alertManager.start();
6181
6476
  // Wire notification channels — tier >= byok only (requires BYOK API keys)
6182
- if (tierGate.canAlert(tier)) {
6477
+ if (tierGate.canAlert(tier) && _notifications && alertDispatcher) {
6183
6478
  const resendKey = process.env.RESEND_API_KEY;
6184
6479
  if (resendKey) {
6185
- alertDispatcher.add(new EmailChannel({
6480
+ alertDispatcher.add(new _notifications.EmailChannel({
6186
6481
  endpoint: "https://api.resend.com/emails",
6187
6482
  apiKey: resendKey,
6188
6483
  from: `${getInstanceName()} <${getAlertEmailFrom()}>`,
@@ -6191,7 +6486,7 @@ async function start(opts) {
6191
6486
  log.info("Alert channel: email (Resend)");
6192
6487
  }
6193
6488
  if (process.env.TWILIO_ACCOUNT_SID) {
6194
- alertDispatcher.add(new PhoneChannel());
6489
+ alertDispatcher.add(new _notifications.PhoneChannel());
6195
6490
  log.info("Alert channel: phone (Twilio voice)");
6196
6491
  }
6197
6492
  alertManager.updateNotifications([
@@ -6202,21 +6497,25 @@ async function start(opts) {
6202
6497
  // Start credit monitoring (checks every 5 min, configurable via CORE_CREDIT_CHECK_INTERVAL_MS)
6203
6498
  startCreditMonitor(health, alertManager);
6204
6499
  // Avatar sidecar launches in background — slow (model loading) but non-blocking
6205
- const avatarConfig = getAvatarConfig();
6500
+ const avatarConfig = _settingsVoice ? _settingsVoice.getAvatarConfig() : { enabled: false, port: 0, photoPath: "" };
6206
6501
  let avatarAvailable = false;
6207
- (async () => {
6208
- avatarAvailable = await startAvatarSidecar();
6209
- if (avatarAvailable) {
6210
- const photoPath = join(process.cwd(), avatarConfig.photoPath);
6211
- const prepared = await preparePhoto(photoPath).catch(() => false);
6212
- if (!prepared) {
6213
- log.warn("Photo preparation failed place a photo at " + avatarConfig.photoPath, { namespace: "avatar" });
6214
- }
6215
- else {
6216
- log.info("Avatar readyMuseTalk sidecar (port " + avatarConfig.port + ")", { namespace: "avatar" });
6502
+ if (_avatarSidecar && _avatarClient) {
6503
+ const avatarSidecar = _avatarSidecar;
6504
+ const avatarClient = _avatarClient;
6505
+ (async () => {
6506
+ avatarAvailable = await avatarSidecar.startAvatarSidecar();
6507
+ if (avatarAvailable) {
6508
+ const photoPath = join(process.cwd(), avatarConfig.photoPath);
6509
+ const prepared = await avatarClient.preparePhoto(photoPath).catch(() => false);
6510
+ if (!prepared) {
6511
+ log.warn("Photo preparation failed place a photo at " + avatarConfig.photoPath, { namespace: "avatar" });
6512
+ }
6513
+ else {
6514
+ log.info("Avatar ready — MuseTalk sidecar (port " + avatarConfig.port + ")", { namespace: "avatar" });
6515
+ }
6217
6516
  }
6218
- }
6219
- })();
6517
+ })();
6518
+ }
6220
6519
  log.info(`${getInstanceName()} — Local Chat starting`);
6221
6520
  // Show LLM provider based on settings
6222
6521
  const provider = resolveProvider();
@@ -6252,8 +6551,8 @@ async function start(opts) {
6252
6551
  log.info("Search: awaiting auth (Perplexity via vault, or: cd sidecar/search && pip install -r requirements.txt)");
6253
6552
  }
6254
6553
  // Show voice status
6255
- const ttsConfig = getTtsConfig();
6256
- const sttConfig = getSttConfig();
6554
+ const ttsConfig = _settingsVoice ? _settingsVoice.getTtsConfig() : { enabled: false, port: 0 };
6555
+ const sttConfig = _settingsVoice ? _settingsVoice.getSttConfig() : { enabled: false, port: 0 };
6257
6556
  if (ttsAvailable) {
6258
6557
  log.info(`TTS: Piper sidecar (port ${ttsConfig.port})`);
6259
6558
  }
@@ -6279,16 +6578,16 @@ async function start(opts) {
6279
6578
  log.info("Avatar: disabled");
6280
6579
  }
6281
6580
  // Show runtime status
6282
- const rt = getRuntime();
6581
+ const rt = (_agentRuntime?.getRuntime() ?? null);
6283
6582
  if (rt) {
6284
6583
  const snap = rt.getResourceSnapshot();
6285
6584
  log.info(`Runtime: active (${snap.activeAgents}/${snap.maxAgents} agents, ${snap.totalMemoryMB}/${snap.maxMemoryMB}MB)`);
6286
6585
  }
6287
6586
  // Show Google integration status
6288
- if (isGoogleAuthenticated()) {
6587
+ if ((_googleAuth?.isGoogleAuthenticated() ?? false)) {
6289
6588
  log.info("Google: connected (Calendar, Gmail, Drive)");
6290
6589
  }
6291
- else if (isGoogleConfigured()) {
6590
+ else if ((_googleAuth?.isGoogleConfigured() ?? false)) {
6292
6591
  log.info("Google: configured — visit /api/google/auth to connect");
6293
6592
  }
6294
6593
  else {
@@ -6335,84 +6634,86 @@ async function start(opts) {
6335
6634
  }
6336
6635
  }
6337
6636
  // Register email handler — emails with instance name in subject are processed as chat
6338
- onDashEmail(async (message) => {
6339
- const human = await readHuman();
6340
- const name = human?.name ?? "Human";
6341
- const emailBody = message.body?.trim();
6342
- if (!emailBody)
6343
- return null;
6344
- // Build a lightweight Brain for email context
6345
- const ltm = new FileSystemLongTermMemory(MEMORY_DIR);
6346
- await ltm.init();
6347
- const emailBrain = new Brain({
6348
- systemPrompt: [
6349
- `You are ${getInstanceName()}, a personal AI agent paired with ${name}. You run locally on ${name}'s machine.`,
6350
- `Today is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}.`,
6351
- `You are responding to an email that was sent to you. This is a working email channel you received this email and your reply will be sent back automatically via Gmail.`,
6352
- ``,
6353
- `Your capabilities — USE THEM when the email requests action:`,
6354
- `- Google Calendar: CREATE, UPDATE, DELETE events using [CALENDAR_ACTION] blocks.`,
6355
- `- Gmail: SEND emails and REPLY to threads using [EMAIL_ACTION] blocks.`,
6356
- `- Google Docs & Sheets: CREATE documents using [DOC_ACTION] blocks.`,
6357
- `- Task board for tracking work.`,
6358
- `- Long-term memory of past conversations and learned facts.`,
6359
- ``,
6360
- ...(getCapabilityRegistry()?.getPromptInstructions({ origin: "email", name }) ?? "").split("\n"),
6361
- ``,
6362
- `Rules for email replies:`,
6363
- `- Write a natural, helpful reply as plain text (no markdown).`,
6364
- `- Be warm and direct you have personality, you're not a corporate assistant.`,
6365
- `- Keep it concise and appropriate for email.`,
6366
- `- Do not include a subject line. Just write the body of the reply.`,
6367
- `- NEVER claim you can't do something you clearly just did (you ARE sending this email).`,
6368
- `- When someone asks you to DO something (schedule, create, send), DO IT with the appropriate action block — don't just acknowledge it.`,
6369
- `- Action blocks will be stripped from the email reply automatically. The recipient only sees your text.`,
6370
- `- Sign off as "— ${getInstanceName()}"`,
6371
- ].join("\n"),
6372
- }, ltm);
6373
- const senderName = message.from.split("<")[0].trim() || "someone";
6374
- const ctx = await emailBrain.getContextForTurn({
6375
- userInput: emailBody,
6376
- conversationHistory: [],
6377
- });
6378
- // Inject email metadata so the agent knows the context
6379
- ctx.messages.splice(1, 0, {
6380
- role: "system",
6381
- content: [
6382
- `--- Incoming email ---`,
6383
- `From: ${message.from}`,
6384
- `Subject: ${message.subject}`,
6385
- `Date: ${message.date}`,
6386
- `--- End email metadata ---`,
6387
- ].join("\n"),
6388
- });
6389
- // Add the email body as the "user" message
6390
- ctx.messages.push({ role: "user", content: emailBody });
6391
- const provider = getProvider(resolveProvider());
6392
- const model = resolveChatModel();
6393
- let reply = await provider.completeChat(ctx.messages, model ?? undefined);
6394
- if (!reply?.trim())
6395
- return null;
6396
- // Process action blocks from the AI response via capability registry
6397
- {
6398
- const capReg = getCapabilityRegistry();
6399
- if (capReg) {
6400
- const { cleaned } = await capReg.processResponse(reply, { origin: "email", name });
6401
- reply = cleaned;
6637
+ if (_googleGmailTimer)
6638
+ _googleGmailTimer.onDashEmail(async (message) => {
6639
+ const human = await readHuman();
6640
+ const name = human?.name ?? "Human";
6641
+ const emailBody = message.body?.trim();
6642
+ if (!emailBody)
6643
+ return null;
6644
+ // Build a lightweight Brain for email context
6645
+ const ltm = new FileSystemLongTermMemory(MEMORY_DIR);
6646
+ await ltm.init();
6647
+ const emailBrain = new Brain({
6648
+ systemPrompt: [
6649
+ `You are ${getInstanceName()}, a personal AI agent paired with ${name}. You run locally on ${name}'s machine.`,
6650
+ `Today is ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}.`,
6651
+ `You are responding to an email that was sent to you. This is a working email channel — you received this email and your reply will be sent back automatically via Gmail.`,
6652
+ ``,
6653
+ `Your capabilities USE THEM when the email requests action:`,
6654
+ `- Google Calendar: CREATE, UPDATE, DELETE events using [CALENDAR_ACTION] blocks.`,
6655
+ `- Gmail: SEND emails and REPLY to threads using [EMAIL_ACTION] blocks.`,
6656
+ `- Google Docs & Sheets: CREATE documents using [DOC_ACTION] blocks.`,
6657
+ `- Task board for tracking work.`,
6658
+ `- Long-term memory of past conversations and learned facts.`,
6659
+ ``,
6660
+ ...(getCapabilityRegistry()?.getPromptInstructions({ origin: "email", name }) ?? "").split("\n"),
6661
+ ``,
6662
+ `Rules for email replies:`,
6663
+ `- Write a natural, helpful reply as plain text (no markdown).`,
6664
+ `- Be warm and direct you have personality, you're not a corporate assistant.`,
6665
+ `- Keep it concise and appropriate for email.`,
6666
+ `- Do not include a subject line. Just write the body of the reply.`,
6667
+ `- NEVER claim you can't do something you clearly just did (you ARE sending this email).`,
6668
+ `- When someone asks you to DO something (schedule, create, send), DO IT with the appropriate action block don't just acknowledge it.`,
6669
+ `- Action blocks will be stripped from the email reply automatically. The recipient only sees your text.`,
6670
+ `- Sign off as "— ${getInstanceName()}"`,
6671
+ ].join("\n"),
6672
+ }, ltm);
6673
+ const senderName = message.from.split("<")[0].trim() || "someone";
6674
+ const ctx = await emailBrain.getContextForTurn({
6675
+ userInput: emailBody,
6676
+ conversationHistory: [],
6677
+ });
6678
+ // Inject email metadata so the agent knows the context
6679
+ ctx.messages.splice(1, 0, {
6680
+ role: "system",
6681
+ content: [
6682
+ `--- Incoming email ---`,
6683
+ `From: ${message.from}`,
6684
+ `Subject: ${message.subject}`,
6685
+ `Date: ${message.date}`,
6686
+ `--- End email metadata ---`,
6687
+ ].join("\n"),
6688
+ });
6689
+ // Add the email body as the "user" message
6690
+ ctx.messages.push({ role: "user", content: emailBody });
6691
+ const provider = getProvider(resolveProvider());
6692
+ const model = resolveChatModel();
6693
+ let reply = await provider.completeChat(ctx.messages, model ?? undefined);
6694
+ if (!reply?.trim())
6695
+ return null;
6696
+ // Process action blocks from the AI response via capability registry
6697
+ {
6698
+ const capReg = getCapabilityRegistry();
6699
+ if (capReg) {
6700
+ const { cleaned } = await capReg.processResponse(reply, { origin: "email", name });
6701
+ reply = cleaned;
6702
+ }
6402
6703
  }
6403
- }
6404
- // Guard: if stripping action blocks left only a signature,
6405
- // the reply has no real content — don't send an empty email.
6406
- const signaturePattern = new RegExp(`[\\s\\n]*[—\\-]+\\s*${getInstanceName()}[\\s.!]*$`, "i");
6407
- const withoutSignature = reply.replace(signaturePattern, "").trim();
6408
- if (!withoutSignature) {
6409
- log.warn("Email reply was empty after stripping action blocks — not sending", { to: message.from, subject: message.subject });
6410
- return null;
6411
- }
6412
- log.info("Email reply generated", { to: message.from, subject: message.subject, replyLength: reply.length });
6413
- return reply.trim();
6414
- });
6415
- log.info(`${getInstanceName()} email handler registered — emails with '${getInstanceName()}' in subject will be auto-replied`);
6704
+ // Guard: if stripping action blocks left only a signature,
6705
+ // the reply has no real content don't send an empty email.
6706
+ const signaturePattern = new RegExp(`[\\s\\n]*[—\\-]+\\s*${getInstanceName()}[\\s.!]*$`, "i");
6707
+ const withoutSignature = reply.replace(signaturePattern, "").trim();
6708
+ if (!withoutSignature) {
6709
+ log.warn("Email reply was empty after stripping action blocks — not sending", { to: message.from, subject: message.subject });
6710
+ return null;
6711
+ }
6712
+ log.info("Email reply generated", { to: message.from, subject: message.subject, replyLength: reply.length });
6713
+ return reply.trim();
6714
+ });
6715
+ if (_googleGmailTimer)
6716
+ log.info(`${getInstanceName()} email handler registered — emails with '${getInstanceName()}' in subject will be auto-replied`);
6416
6717
  await new Promise((resolve) => {
6417
6718
  function onListening(server) {
6418
6719
  const addr = server.address();
@@ -6440,7 +6741,8 @@ async function start(opts) {
6440
6741
  // Announce on LAN if mesh.lanAnnounce is enabled AND tier >= byok
6441
6742
  if (tierGate.canMesh(tier) && getMeshConfig().lanAnnounce) {
6442
6743
  try {
6443
- startMdns(actualPort);
6744
+ if (_mdns)
6745
+ _mdns.startMdns(actualPort);
6444
6746
  }
6445
6747
  catch (err) {
6446
6748
  log.warn("mDNS announcement failed — discovery disabled", {
@@ -6571,46 +6873,86 @@ function parseRoadmapYaml(raw) {
6571
6873
  // then sidecars, timers, and monitors.
6572
6874
  async function gracefulShutdown(signal) {
6573
6875
  // Agent pool handles coordinated shutdown: drain → terminate → cleanup
6574
- if (workflowEngine) {
6575
- await workflowEngine.shutdown().catch(() => { });
6576
- workflowEngine = null;
6577
- }
6578
- if (agentPool) {
6579
- await agentPool.shutdown(signal).catch(() => { });
6580
- setAgentPool(null);
6581
- agentPool = null;
6582
- }
6583
- else {
6584
- // Fallback if pool wasn't initialized
6585
- await instanceManager?.shutdown().catch(() => { });
6586
- await shutdownRuntime(signal).catch(() => { });
6876
+ try {
6877
+ if (workflowEngine) {
6878
+ await workflowEngine.shutdown().catch(() => { });
6879
+ workflowEngine = null;
6880
+ }
6881
+ if (agentPool) {
6882
+ await agentPool.shutdown(signal).catch(() => { });
6883
+ setAgentPool(null);
6884
+ agentPool = null;
6885
+ }
6886
+ else if (instanceManager || _agentRuntime) {
6887
+ // Fallback if pool wasn't initialized
6888
+ await instanceManager?.shutdown().catch(() => { });
6889
+ if (_agentRuntime)
6890
+ await _agentRuntime.shutdownRuntime(signal).catch(() => { });
6891
+ }
6587
6892
  }
6893
+ catch { }
6588
6894
  recovery.stop();
6589
6895
  stopCollector();
6590
- stopGmailTimer();
6591
- stopCalendarTimer();
6592
- stopTasksTimer();
6896
+ try {
6897
+ _googleGmailTimer?.stopGmailTimer();
6898
+ }
6899
+ catch { }
6900
+ try {
6901
+ _googleCalendarTimer?.stopCalendarTimer();
6902
+ }
6903
+ catch { }
6904
+ try {
6905
+ _googleTasksTimer?.stopTasksTimer();
6906
+ }
6907
+ catch { }
6593
6908
  stopGroomingTimer();
6594
6909
  stopSchedulingTimer();
6595
- shutdownGitHub();
6910
+ try {
6911
+ _integrationsGithub?.shutdownGitHub();
6912
+ }
6913
+ catch { }
6596
6914
  stopGoalTimer();
6597
- stopAutonomousTimer();
6915
+ try {
6916
+ _agentAutonomous?.stopAutonomousTimer();
6917
+ }
6918
+ catch { }
6598
6919
  stopBacklogReviewTimer();
6599
6920
  stopBriefingTimer();
6600
6921
  stopInsightsTimer();
6601
6922
  stopOpenLoopScanner();
6602
6923
  stopCreditMonitor();
6603
6924
  stopPushMonitor();
6604
- stopMdns();
6605
- stopAvatarSidecar();
6606
- stopTtsSidecar();
6607
- stopSttSidecar();
6925
+ try {
6926
+ _mdns?.stopMdns();
6927
+ }
6928
+ catch { }
6929
+ stopFileWatcher();
6930
+ try {
6931
+ _avatarSidecar?.stopAvatarSidecar();
6932
+ }
6933
+ catch { }
6934
+ try {
6935
+ _ttsSidecar?.stopTtsSidecar();
6936
+ }
6937
+ catch { }
6938
+ try {
6939
+ _sttSidecar?.stopSttSidecar();
6940
+ }
6941
+ catch { }
6608
6942
  stopSidecar();
6609
- await closeBrowser();
6943
+ try {
6944
+ if (_browser)
6945
+ await _browser.closeBrowser();
6946
+ }
6947
+ catch { }
6610
6948
  shutdownAgents();
6611
6949
  await shutdownPlugins().catch(() => { });
6612
6950
  await shutdownLLMCache();
6613
- await shutdownTracing();
6951
+ try {
6952
+ if (_tracingInit)
6953
+ await _tracingInit.shutdownTracing();
6954
+ }
6955
+ catch { }
6614
6956
  releaseLock();
6615
6957
  process.exit(0);
6616
6958
  }