@ouro.bot/cli 0.1.0-alpha.83 → 0.1.0-alpha.831

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 (647) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -23
  3. package/RepairGuide.ouro/agent.json +5 -0
  4. package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
  5. package/RepairGuide.ouro/psyche/SOUL.md +55 -0
  6. package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
  7. package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
  8. package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
  9. package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
  10. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
  11. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  12. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  13. package/assets/bluebubbles-host +264 -0
  14. package/assets/sanctuary-host-launcher.sh +16 -0
  15. package/changelog.json +5308 -16
  16. package/deploy/unraid/Dockerfile +54 -0
  17. package/deploy/unraid/README.txt +2983 -0
  18. package/deploy/unraid/audit-container-spec.sh +4 -0
  19. package/deploy/unraid/container-runtime.json +1 -0
  20. package/deploy/unraid/docker-man-template-transaction.mjs +711 -0
  21. package/deploy/unraid/docker-man-template-xml.cjs +105 -0
  22. package/deploy/unraid/migrate-sanctuary-bundle.mjs +60 -0
  23. package/deploy/unraid/ouro-events/bootstrap-spool.sh +92 -0
  24. package/deploy/unraid/ouro-events/emit-event.mjs +310 -0
  25. package/deploy/unraid/ouro-events/emit-usenet-event.sh +54 -0
  26. package/deploy/unraid/ouro-events/install-usenet-guard.sh +435 -0
  27. package/deploy/unraid/ouro-events/usenet-health.sh +176 -0
  28. package/deploy/unraid/sanctuary-acceptance-adapter.sh +43 -0
  29. package/deploy/unraid/sanctuary-acceptance-contract.json +198 -0
  30. package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
  31. package/deploy/unraid/sanctuary-authority-installation.json +36 -0
  32. package/deploy/unraid/sanctuary-authority-service.sh +30 -0
  33. package/deploy/unraid/sanctuary-deployment-target.mjs +710 -0
  34. package/deploy/unraid/sanctuary-safe-rename.py +24 -0
  35. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +2084 -0
  36. package/deploy/unraid/sanctuary-unit16-run.sh +560 -0
  37. package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
  38. package/deploy/unraid/sanctuary.ouro/agent.json +66 -0
  39. package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
  40. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
  41. package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
  42. package/deploy/unraid/sanctuary.ouro/mcp/media-mcp.mjs +795 -0
  43. package/deploy/unraid/sanctuary.ouro/provider-readiness.json +11 -0
  44. package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +5 -0
  45. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +5 -0
  46. package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +15 -0
  47. package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +25 -0
  48. package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +15 -0
  49. package/deploy/unraid/sanctuary.ouro/state/policy/steward.json +7 -0
  50. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +135 -0
  51. package/deploy/unraid/sanctuary.xml +28 -0
  52. package/dist/a2a/card.js +57 -0
  53. package/dist/a2a/client.js +180 -0
  54. package/dist/a2a/config.js +50 -0
  55. package/dist/a2a/delegation-stores.js +56 -0
  56. package/dist/a2a/did-resolution.js +93 -0
  57. package/dist/a2a/identity.js +170 -0
  58. package/dist/a2a/inbound-share.js +107 -0
  59. package/dist/a2a/mission-result-wire.js +196 -0
  60. package/dist/a2a/onboarding.js +115 -0
  61. package/dist/a2a/pin-store.js +132 -0
  62. package/dist/a2a/seen-ledger.js +120 -0
  63. package/dist/a2a/server.js +604 -0
  64. package/dist/a2a/task-store.js +69 -0
  65. package/dist/a2a/transport.js +45 -0
  66. package/dist/a2a/types.js +3 -0
  67. package/dist/arc/attention-types.js +8 -0
  68. package/dist/arc/cares.js +360 -0
  69. package/dist/arc/episodes.js +118 -0
  70. package/dist/arc/evolution.js +490 -0
  71. package/dist/arc/flight-recorder.js +1034 -0
  72. package/dist/arc/intentions.js +134 -0
  73. package/dist/arc/json-store.js +117 -0
  74. package/dist/arc/obligations.js +386 -0
  75. package/dist/arc/packets.js +336 -0
  76. package/dist/arc/presence.js +185 -0
  77. package/dist/arc/task-lifecycle.js +57 -0
  78. package/dist/commerce/store.js +755 -0
  79. package/dist/commerce/types.js +3 -0
  80. package/dist/heart/active-work.js +798 -40
  81. package/dist/heart/agent-entry.js +252 -3
  82. package/dist/heart/approval-files.js +113 -0
  83. package/dist/heart/approval-store.js +960 -0
  84. package/dist/heart/attachments/image-normalize.js +194 -0
  85. package/dist/heart/attachments/materialize.js +97 -0
  86. package/dist/heart/attachments/originals.js +88 -0
  87. package/dist/heart/attachments/render.js +29 -0
  88. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  89. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  90. package/dist/heart/attachments/sources/index.js +18 -0
  91. package/dist/heart/attachments/sources/telegram.js +71 -0
  92. package/dist/heart/attachments/store.js +132 -0
  93. package/dist/heart/attachments/types.js +93 -0
  94. package/dist/heart/auth/auth-flow.js +495 -0
  95. package/dist/heart/autonomy-budget.js +478 -0
  96. package/dist/heart/awaiting/await-alert.js +148 -0
  97. package/dist/heart/awaiting/await-expiry.js +143 -0
  98. package/dist/heart/awaiting/await-loader.js +91 -0
  99. package/dist/heart/awaiting/await-parser.js +149 -0
  100. package/dist/heart/awaiting/await-runtime-state.js +111 -0
  101. package/dist/heart/awaiting/await-scheduler.js +382 -0
  102. package/dist/heart/background-operations.js +281 -0
  103. package/dist/heart/bridges/manager.js +137 -17
  104. package/dist/heart/bridges/store.js +14 -2
  105. package/dist/heart/bundle-state.js +168 -0
  106. package/dist/heart/commitments.js +44 -18
  107. package/dist/heart/config-registry.js +331 -0
  108. package/dist/heart/config.js +174 -131
  109. package/dist/heart/context-loss-gauntlet.js +387 -0
  110. package/dist/heart/context-loss-sentinel.js +1157 -0
  111. package/dist/heart/core.js +2169 -581
  112. package/dist/heart/cross-chat-delivery.js +9 -22
  113. package/dist/heart/daemon/agent-config-check.js +451 -0
  114. package/dist/heart/daemon/agent-discovery.js +185 -3
  115. package/dist/heart/daemon/agent-service.js +542 -0
  116. package/dist/heart/daemon/agentic-repair.js +547 -0
  117. package/dist/heart/daemon/await-private-wake.js +59 -0
  118. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  119. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  120. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  121. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  122. package/dist/heart/daemon/boot-sync-probe.js +197 -0
  123. package/dist/heart/daemon/cadence.js +180 -0
  124. package/dist/heart/daemon/cli-defaults.js +854 -0
  125. package/dist/heart/daemon/cli-desk.js +322 -0
  126. package/dist/heart/daemon/cli-exec.js +8822 -0
  127. package/dist/heart/daemon/cli-help.js +716 -0
  128. package/dist/heart/daemon/cli-parse.js +2771 -0
  129. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  130. package/dist/heart/daemon/cli-render.js +777 -0
  131. package/dist/heart/daemon/cli-types.js +8 -0
  132. package/dist/heart/daemon/connect-bay.js +323 -0
  133. package/dist/heart/daemon/container-credential-bootstrap.js +273 -0
  134. package/dist/heart/daemon/container-healthcheck.js +112 -0
  135. package/dist/heart/daemon/container-runtime.js +97 -0
  136. package/dist/heart/daemon/container-spec-auditor-main.js +171 -0
  137. package/dist/heart/daemon/container-spec-auditor.js +229 -0
  138. package/dist/heart/daemon/daemon-bootstrap-startup.js +115 -0
  139. package/dist/heart/daemon/daemon-cli.js +29 -2349
  140. package/dist/heart/daemon/daemon-entry.js +930 -11
  141. package/dist/heart/daemon/daemon-health.js +186 -0
  142. package/dist/heart/daemon/daemon-rollup.js +57 -0
  143. package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
  144. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  145. package/dist/heart/daemon/daemon.js +2084 -105
  146. package/dist/heart/daemon/dns-workflow.js +394 -0
  147. package/dist/heart/daemon/doctor-types.js +8 -0
  148. package/dist/heart/daemon/doctor.js +1755 -0
  149. package/dist/heart/daemon/freshness.js +345 -0
  150. package/dist/heart/daemon/habit-private-wake.js +61 -0
  151. package/dist/heart/daemon/health-monitor.js +142 -11
  152. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  153. package/dist/heart/daemon/hooks/bundle-meta.js +135 -1
  154. package/dist/heart/daemon/http-health-probe.js +80 -0
  155. package/dist/heart/daemon/human-command-screens.js +234 -0
  156. package/dist/heart/daemon/human-readiness.js +114 -0
  157. package/dist/heart/daemon/inner-status.js +78 -0
  158. package/dist/heart/daemon/interactive-repair.js +394 -0
  159. package/dist/heart/daemon/launchd.js +37 -8
  160. package/dist/heart/daemon/log-tailer.js +79 -10
  161. package/dist/heart/daemon/logs-prune.js +132 -0
  162. package/dist/heart/daemon/mcp-canary.js +524 -0
  163. package/dist/heart/daemon/message-router.js +65 -5
  164. package/dist/heart/daemon/migrate-to-desk.js +848 -0
  165. package/dist/heart/daemon/os-cron-deps.js +135 -0
  166. package/dist/heart/daemon/os-cron.js +22 -14
  167. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  168. package/dist/heart/daemon/ouro-entry.js +3 -1
  169. package/dist/heart/daemon/plugin-cli.js +432 -0
  170. package/dist/heart/daemon/process-manager.js +733 -61
  171. package/dist/heart/daemon/provider-discovery.js +137 -0
  172. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  173. package/dist/heart/daemon/prunable-bundle.js +144 -0
  174. package/dist/heart/daemon/pulse.js +511 -0
  175. package/dist/heart/daemon/readiness-repair.js +365 -0
  176. package/dist/heart/daemon/run-hooks.js +2 -0
  177. package/dist/heart/daemon/runtime-logging.js +47 -14
  178. package/dist/heart/daemon/runtime-metadata.js +2 -30
  179. package/dist/heart/daemon/runtime-mode.js +13 -2
  180. package/dist/heart/daemon/safe-mode.js +161 -0
  181. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2654 -0
  182. package/dist/heart/daemon/sanctuary-acceptance-harness.js +1830 -0
  183. package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
  184. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +955 -0
  185. package/dist/heart/daemon/sanctuary-authority-codec.js +86 -0
  186. package/dist/heart/daemon/sanctuary-authority-epoch.js +256 -0
  187. package/dist/heart/daemon/sanctuary-authority-installation.js +140 -0
  188. package/dist/heart/daemon/sanctuary-authority-ledger.js +241 -0
  189. package/dist/heart/daemon/sanctuary-authority-root-lifecycle.js +819 -0
  190. package/dist/heart/daemon/sanctuary-authority-vault-migration.js +79 -0
  191. package/dist/heart/daemon/sanctuary-bundle-migration.js +729 -0
  192. package/dist/heart/daemon/sanctuary-host-authority.js +1008 -0
  193. package/dist/heart/daemon/sanctuary-host-detached-supervisor.js +494 -0
  194. package/dist/heart/daemon/sanctuary-host-executor.js +670 -0
  195. package/dist/heart/daemon/sanctuary-host-linux-kernel.js +334 -0
  196. package/dist/heart/daemon/sanctuary-host-supervisor-entry.js +207 -0
  197. package/dist/heart/daemon/sanctuary-host-supervisor.js +95 -0
  198. package/dist/heart/daemon/sanctuary-package-management.js +103 -0
  199. package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
  200. package/dist/heart/daemon/sanctuary-scheduler-origin.js +132 -0
  201. package/dist/heart/daemon/sanctuary-telegram-authority-entry.js +445 -0
  202. package/dist/heart/daemon/sanctuary-telegram-authority-gateway.js +585 -0
  203. package/dist/heart/daemon/sanctuary-telegram-authority-service.js +630 -0
  204. package/dist/heart/daemon/sense-manager.js +699 -48
  205. package/dist/heart/daemon/session-id-resolver.js +131 -0
  206. package/dist/heart/daemon/skill-management-installer.js +1 -1
  207. package/dist/heart/daemon/socket-client.js +180 -12
  208. package/dist/heart/daemon/stale-bundle-prune.js +113 -0
  209. package/dist/heart/daemon/startup-tui.js +339 -0
  210. package/dist/heart/daemon/supercronic-supervisor.js +282 -0
  211. package/dist/heart/daemon/task-scheduler.js +117 -39
  212. package/dist/heart/daemon/terminal-ui.js +499 -0
  213. package/dist/heart/daemon/thoughts.js +138 -54
  214. package/dist/heart/daemon/up-progress.js +366 -0
  215. package/dist/heart/daemon/vault-items.js +56 -0
  216. package/dist/heart/delegation.js +1 -4
  217. package/dist/heart/external-events/router.js +1430 -0
  218. package/dist/heart/frontend-approval-runtime.js +506 -0
  219. package/dist/heart/frontend-journal.js +215 -0
  220. package/dist/heart/frontend-session-service.js +237 -0
  221. package/dist/heart/frontend-socket-client.js +164 -0
  222. package/dist/heart/frontend-socket.js +400 -0
  223. package/dist/heart/habits/habit-cancel.js +810 -0
  224. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  225. package/dist/heart/habits/habit-migration.js +194 -0
  226. package/dist/heart/habits/habit-parser.js +278 -0
  227. package/dist/heart/habits/habit-runtime-state.js +93 -0
  228. package/dist/heart/habits/habit-scheduler.js +653 -0
  229. package/dist/heart/habits/habit-session-summary.js +318 -0
  230. package/dist/heart/habits/habit-session.js +636 -0
  231. package/dist/heart/{daemon → hatch}/hatch-flow.js +55 -62
  232. package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
  233. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  234. package/dist/heart/{daemon → hatch}/specialist-tools.js +53 -18
  235. package/dist/heart/identity.js +196 -59
  236. package/dist/heart/kept-notes.js +289 -0
  237. package/dist/heart/kicks.js +1 -1
  238. package/dist/heart/machine-identity.js +161 -0
  239. package/dist/heart/mail-import-discovery.js +356 -0
  240. package/dist/heart/mailbox/mailbox-http-hooks.js +98 -0
  241. package/dist/heart/mailbox/mailbox-http-response.js +7 -0
  242. package/dist/heart/mailbox/mailbox-http-routes.js +398 -0
  243. package/dist/heart/mailbox/mailbox-http-static.js +103 -0
  244. package/dist/heart/mailbox/mailbox-http-transport.js +129 -0
  245. package/dist/heart/mailbox/mailbox-http.js +99 -0
  246. package/dist/heart/mailbox/mailbox-read.js +39 -0
  247. package/dist/heart/mailbox/mailbox-types.js +27 -0
  248. package/dist/heart/mailbox/mailbox-view.js +197 -0
  249. package/dist/heart/mailbox/readers/agent-machine.js +428 -0
  250. package/dist/heart/mailbox/readers/continuity-readers.js +329 -0
  251. package/dist/heart/mailbox/readers/mail.js +375 -0
  252. package/dist/heart/mailbox/readers/runtime-readers.js +823 -0
  253. package/dist/heart/mailbox/readers/sessions.js +231 -0
  254. package/dist/heart/mailbox/readers/shared.js +111 -0
  255. package/dist/heart/mcp/mcp-server.js +696 -0
  256. package/dist/heart/migrate-config.js +100 -0
  257. package/dist/heart/model-capabilities.js +11 -0
  258. package/dist/heart/orientation-frame.js +345 -0
  259. package/dist/heart/platform.js +81 -0
  260. package/dist/heart/primitives.js +1 -1
  261. package/dist/heart/private-runtime/decision-renderer.js +158 -0
  262. package/dist/heart/private-runtime/index.js +18 -0
  263. package/dist/heart/private-runtime/ledger.js +221 -0
  264. package/dist/heart/private-runtime/policy.js +252 -0
  265. package/dist/heart/private-runtime/types.js +2 -0
  266. package/dist/heart/progress-story.js +1 -1
  267. package/dist/heart/provider-attempt.js +137 -0
  268. package/dist/heart/provider-binding-resolver.js +273 -0
  269. package/dist/heart/provider-credentials.js +449 -0
  270. package/dist/heart/provider-failover.js +311 -0
  271. package/dist/heart/provider-models.js +96 -0
  272. package/dist/heart/provider-ping.js +265 -0
  273. package/dist/heart/provider-readiness-cache.js +119 -0
  274. package/dist/heart/provider-visibility.js +188 -0
  275. package/dist/heart/providers/anthropic-token.js +131 -0
  276. package/dist/heart/providers/anthropic.js +173 -60
  277. package/dist/heart/providers/azure.js +23 -11
  278. package/dist/heart/providers/error-classification.js +127 -0
  279. package/dist/heart/providers/github-copilot.js +34 -38
  280. package/dist/heart/providers/minimax-vlm.js +189 -0
  281. package/dist/heart/providers/minimax.js +26 -8
  282. package/dist/heart/providers/openai-codex-token.js +349 -0
  283. package/dist/heart/providers/openai-codex.js +55 -40
  284. package/dist/heart/providers/openai-compatible.js +177 -0
  285. package/dist/heart/run-ledger.js +247 -0
  286. package/dist/heart/runtime-capability-check.js +170 -0
  287. package/dist/heart/runtime-credentials.js +607 -0
  288. package/dist/heart/runtime-cwd.js +87 -0
  289. package/dist/heart/sense-truth.js +22 -4
  290. package/dist/heart/session-activity.js +92 -24
  291. package/dist/heart/session-events.js +1504 -0
  292. package/dist/heart/session-playback-cli-main.js +5 -0
  293. package/dist/heart/session-playback-cli.js +36 -0
  294. package/dist/heart/session-playback.js +231 -0
  295. package/dist/heart/session-redaction-repair-cli-main.js +31 -0
  296. package/dist/heart/session-redaction-repair.js +552 -0
  297. package/dist/heart/session-stats-cli-main.js +5 -0
  298. package/dist/heart/session-stats.js +183 -0
  299. package/dist/heart/session-transcript.js +133 -0
  300. package/dist/heart/start-of-turn-packet.js +372 -0
  301. package/dist/heart/steward-policy.js +618 -0
  302. package/dist/heart/streaming.js +604 -207
  303. package/dist/heart/structured-output.js +196 -0
  304. package/dist/heart/sync-classification.js +176 -0
  305. package/dist/heart/sync.js +449 -0
  306. package/dist/heart/target-resolution.js +11 -7
  307. package/dist/heart/tempo.js +93 -0
  308. package/dist/heart/temporal-view.js +41 -0
  309. package/dist/heart/timeouts.js +101 -0
  310. package/dist/heart/tool-activity-callbacks.js +59 -0
  311. package/dist/heart/tool-approval.js +382 -0
  312. package/dist/heart/tool-description.js +155 -0
  313. package/dist/heart/tool-friction.js +55 -0
  314. package/dist/heart/tool-loop.js +200 -0
  315. package/dist/heart/turn-context.js +482 -0
  316. package/dist/heart/turn-execution-lease.js +30 -0
  317. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
  318. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  319. package/dist/heart/versioning/ouro-path-installer.js +432 -0
  320. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  321. package/dist/heart/versioning/ouro-version-manager.js +409 -0
  322. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  323. package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
  324. package/dist/heart/versioning/update-hooks.js +154 -0
  325. package/dist/heart/versioning/version-intent.js +114 -0
  326. package/dist/heart/work-card.js +380 -0
  327. package/dist/mailbox-ui/assets/index-Cyv4mw3Y.js +1 -0
  328. package/dist/mailbox-ui/assets/index-Du_9G9WO.css +1 -0
  329. package/dist/mailbox-ui/assets/vendor-CcN1XpQ9.js +61 -0
  330. package/dist/mailbox-ui/index.html +16 -0
  331. package/dist/mailroom/attention.js +167 -0
  332. package/dist/mailroom/autonomy.js +209 -0
  333. package/dist/mailroom/blob-store.js +889 -0
  334. package/dist/mailroom/body-cache.js +61 -0
  335. package/dist/mailroom/cache-sync-cli.js +58 -0
  336. package/dist/mailroom/core.js +803 -0
  337. package/dist/mailroom/entry.js +160 -0
  338. package/dist/mailroom/file-store.js +568 -0
  339. package/dist/mailroom/hosted-cache-sync.js +384 -0
  340. package/dist/mailroom/mbox-import.js +393 -0
  341. package/dist/mailroom/migration.js +164 -0
  342. package/dist/mailroom/outbound.js +380 -0
  343. package/dist/mailroom/policy.js +263 -0
  344. package/dist/mailroom/reader.js +292 -0
  345. package/dist/mailroom/search-cache.js +513 -0
  346. package/dist/mailroom/search-relevance.js +319 -0
  347. package/dist/mailroom/smtp-ingress.js +176 -0
  348. package/dist/mailroom/source-state.js +176 -0
  349. package/dist/mailroom/thread.js +109 -0
  350. package/dist/mailroom/travel-extract.js +89 -0
  351. package/dist/mind/bundle-manifest.js +24 -2
  352. package/dist/mind/context.js +309 -111
  353. package/dist/mind/desk-section.js +362 -0
  354. package/dist/mind/diary-integrity.js +60 -0
  355. package/dist/mind/{memory.js → diary.js} +68 -77
  356. package/dist/mind/embedding-provider.js +60 -0
  357. package/dist/mind/file-state.js +179 -0
  358. package/dist/mind/{associative-recall.js → note-search.js} +47 -58
  359. package/dist/mind/obligation-steering.js +199 -3
  360. package/dist/mind/pending.js +36 -10
  361. package/dist/mind/prompt-budget.js +479 -0
  362. package/dist/mind/prompt-refresh.js +3 -2
  363. package/dist/mind/prompt.js +1050 -189
  364. package/dist/mind/provenance-trust.js +26 -0
  365. package/dist/mind/record-paths.js +312 -0
  366. package/dist/mind/scrutiny.js +173 -0
  367. package/dist/mind/session-transaction.js +564 -0
  368. package/dist/nerves/cli-logging.js +7 -1
  369. package/dist/nerves/coverage/audit-rules.js +15 -6
  370. package/dist/nerves/coverage/audit.js +28 -2
  371. package/dist/nerves/coverage/cli.js +1 -1
  372. package/dist/nerves/coverage/contract.js +5 -5
  373. package/dist/nerves/coverage/file-completeness.js +139 -5
  374. package/dist/nerves/coverage/run-artifacts.js +55 -35
  375. package/dist/nerves/coverage/source-scanner.js +4 -4
  376. package/dist/nerves/event-buffer.js +111 -0
  377. package/dist/nerves/index.js +293 -9
  378. package/dist/nerves/observation.js +20 -0
  379. package/dist/nerves/redact.js +79 -0
  380. package/dist/nerves/review/cli-main.js +5 -0
  381. package/dist/nerves/review/cli.js +156 -0
  382. package/dist/nerves/review/core.js +159 -0
  383. package/dist/nerves/runtime.js +17 -1
  384. package/dist/repertoire/ado-client.js +15 -56
  385. package/dist/repertoire/ado-semantic.js +16 -10
  386. package/dist/repertoire/api-client.js +97 -0
  387. package/dist/repertoire/bitwarden-store.js +1045 -0
  388. package/dist/repertoire/bundle-templates.js +71 -0
  389. package/dist/repertoire/bw-installer.js +180 -0
  390. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  391. package/dist/repertoire/coding/context-pack.js +331 -0
  392. package/dist/repertoire/coding/feedback.js +174 -32
  393. package/dist/repertoire/coding/index.js +15 -3
  394. package/dist/repertoire/coding/manager.js +159 -10
  395. package/dist/repertoire/coding/spawner.js +58 -11
  396. package/dist/repertoire/coding/tools.js +221 -15
  397. package/dist/repertoire/coding/workbench-client.js +272 -0
  398. package/dist/repertoire/coding/workbench-manager.js +346 -0
  399. package/dist/repertoire/commerce-errors.js +109 -0
  400. package/dist/repertoire/commerce-self-test.js +156 -0
  401. package/dist/repertoire/credential-access.js +178 -0
  402. package/dist/repertoire/desk/classifier.js +362 -0
  403. package/dist/repertoire/duffel-client.js +185 -0
  404. package/dist/repertoire/github-client.js +14 -55
  405. package/dist/repertoire/graph-client.js +11 -52
  406. package/dist/repertoire/guardrails.js +811 -18
  407. package/dist/repertoire/mcp-client.js +44 -3
  408. package/dist/repertoire/mcp-manager.js +427 -139
  409. package/dist/repertoire/mcp-tools.js +124 -0
  410. package/dist/repertoire/plugin-mcp.js +175 -0
  411. package/dist/repertoire/plugins.js +253 -0
  412. package/dist/repertoire/relationship-authorization.js +322 -0
  413. package/dist/repertoire/shell-sessions.js +134 -0
  414. package/dist/repertoire/skills.js +48 -4
  415. package/dist/repertoire/stripe-client.js +131 -0
  416. package/dist/repertoire/tool-arguments.js +170 -0
  417. package/dist/repertoire/tool-results.js +29 -0
  418. package/dist/repertoire/tools-a2a.js +699 -0
  419. package/dist/repertoire/tools-attachments.js +322 -0
  420. package/dist/repertoire/tools-awaiting.js +575 -0
  421. package/dist/repertoire/tools-base.js +77 -1138
  422. package/dist/repertoire/tools-bluebubbles.js +2 -0
  423. package/dist/repertoire/tools-bridge.js +144 -0
  424. package/dist/repertoire/tools-bundle.js +993 -0
  425. package/dist/repertoire/tools-commerce.js +253 -0
  426. package/dist/repertoire/tools-config.js +186 -0
  427. package/dist/repertoire/tools-continuity.js +575 -0
  428. package/dist/repertoire/tools-credential.js +383 -0
  429. package/dist/repertoire/tools-evolution.js +527 -0
  430. package/dist/repertoire/tools-files.js +344 -0
  431. package/dist/repertoire/tools-flight.js +290 -0
  432. package/dist/repertoire/tools-flow.js +119 -0
  433. package/dist/repertoire/tools-github.js +3 -8
  434. package/dist/repertoire/tools-habits.js +103 -0
  435. package/dist/repertoire/tools-mail.js +1828 -0
  436. package/dist/repertoire/tools-notes.js +485 -0
  437. package/dist/repertoire/tools-obligations.js +182 -0
  438. package/dist/repertoire/tools-orientation.js +31 -0
  439. package/dist/repertoire/tools-record.js +482 -0
  440. package/dist/repertoire/tools-rsvp.js +139 -0
  441. package/dist/repertoire/tools-runtime.js +150 -0
  442. package/dist/repertoire/tools-sanctuary-host.js +202 -0
  443. package/dist/repertoire/tools-session.js +1133 -0
  444. package/dist/repertoire/tools-shell.js +149 -0
  445. package/dist/repertoire/tools-steward-policy.js +119 -0
  446. package/dist/repertoire/tools-stripe.js +224 -0
  447. package/dist/repertoire/tools-surface.js +369 -0
  448. package/dist/repertoire/tools-teams.js +12 -39
  449. package/dist/repertoire/tools-telegram-contacts.js +37 -0
  450. package/dist/repertoire/tools-travel.js +125 -0
  451. package/dist/repertoire/tools-trip.js +982 -0
  452. package/dist/repertoire/tools-unraid.js +531 -0
  453. package/dist/repertoire/tools-user-profile.js +146 -0
  454. package/dist/repertoire/tools-vault.js +40 -0
  455. package/dist/repertoire/tools-voice.js +145 -0
  456. package/dist/repertoire/tools.js +494 -148
  457. package/dist/repertoire/travel-api-client.js +360 -0
  458. package/dist/repertoire/unraid-client.js +155 -0
  459. package/dist/repertoire/unraid-restart.js +339 -0
  460. package/dist/repertoire/user-profile.js +131 -0
  461. package/dist/repertoire/vault-setup.js +246 -0
  462. package/dist/repertoire/vault-unlock.js +628 -0
  463. package/dist/rsvp/aisleplanner-client.js +354 -0
  464. package/dist/rsvp/cli.js +926 -0
  465. package/dist/rsvp/config.js +327 -0
  466. package/dist/rsvp/cutover.js +433 -0
  467. package/dist/rsvp/diagnostics.js +232 -0
  468. package/dist/rsvp/diff-renderer.js +176 -0
  469. package/dist/rsvp/habit-policy.js +110 -0
  470. package/dist/rsvp/habit-stage.js +123 -0
  471. package/dist/rsvp/incident-bundle.js +97 -0
  472. package/dist/rsvp/migration.js +204 -0
  473. package/dist/rsvp/native-habit-runner.js +630 -0
  474. package/dist/rsvp/outbound-state.js +628 -0
  475. package/dist/rsvp/query.js +118 -0
  476. package/dist/rsvp/replay.js +233 -0
  477. package/dist/rsvp/snapshot.js +232 -0
  478. package/dist/rsvp/spend-ledger.js +175 -0
  479. package/dist/scripts/claude-code-hook.js +41 -0
  480. package/dist/scripts/claude-code-stop-hook.js +47 -0
  481. package/dist/senses/a2a-entry.js +115 -0
  482. package/dist/senses/acp-server.js +386 -0
  483. package/dist/senses/attention-queue.js +186 -0
  484. package/dist/senses/await-turn-message.js +58 -0
  485. package/dist/senses/bluebubbles/active-turns.js +216 -0
  486. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  487. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  488. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +355 -40
  489. package/dist/senses/bluebubbles/context-packet.js +207 -0
  490. package/dist/senses/bluebubbles/context-smoke.js +144 -0
  491. package/dist/senses/bluebubbles/entry.js +90 -0
  492. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +21 -3
  493. package/dist/senses/bluebubbles/index.js +3841 -0
  494. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  495. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -71
  496. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +159 -28
  497. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +24 -13
  498. package/dist/senses/bluebubbles/outbound-state.js +308 -0
  499. package/dist/senses/bluebubbles/processed-log.js +133 -0
  500. package/dist/senses/bluebubbles/reaction-policy.js +47 -0
  501. package/dist/senses/bluebubbles/replay.js +284 -0
  502. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
  503. package/dist/senses/bluebubbles/semantic-receipts.js +1986 -0
  504. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  505. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  506. package/dist/senses/bluebubbles-meta-guard.js +39 -0
  507. package/dist/senses/cli/bracketed-paste.js +82 -0
  508. package/dist/senses/cli/image-paste.js +287 -0
  509. package/dist/senses/cli/image-ref-navigation.js +75 -0
  510. package/dist/senses/cli/ink-app.js +156 -0
  511. package/dist/senses/cli/inline-diff.js +64 -0
  512. package/dist/senses/cli/input-keys.js +174 -0
  513. package/dist/senses/cli/kill-ring.js +86 -0
  514. package/dist/senses/cli/message-list.js +51 -0
  515. package/dist/senses/cli/ouro-tui.js +607 -0
  516. package/dist/senses/cli/spinner-imperative.js +135 -0
  517. package/dist/senses/cli/spinner.js +101 -0
  518. package/dist/senses/cli/status-line.js +60 -0
  519. package/dist/senses/cli/streaming-markdown.js +526 -0
  520. package/dist/senses/cli/tool-display.js +85 -0
  521. package/dist/senses/cli/tool-render.js +85 -0
  522. package/dist/senses/cli/tui-store.js +241 -0
  523. package/dist/senses/cli/virtual-list.js +35 -0
  524. package/dist/senses/cli-entry.js +60 -8
  525. package/dist/senses/cli.js +604 -320
  526. package/dist/senses/commands.js +66 -3
  527. package/dist/senses/context-packet-ledger.js +371 -0
  528. package/dist/senses/context-packets.js +154 -0
  529. package/dist/senses/habit-lifecycle-guard.js +24 -0
  530. package/dist/senses/habit-turn-message.js +160 -0
  531. package/dist/senses/ingress-evidence.js +2 -0
  532. package/dist/senses/mail-entry.js +66 -0
  533. package/dist/senses/mail.js +524 -0
  534. package/dist/senses/pipeline.js +1115 -204
  535. package/dist/senses/private-runtime-worker.js +859 -0
  536. package/dist/senses/private-runtime.js +1517 -0
  537. package/dist/senses/proactive-content-guard.js +51 -0
  538. package/dist/senses/root-host-approval-port.js +345 -0
  539. package/dist/senses/root-host-approval-runtime.js +393 -0
  540. package/dist/senses/sanctuary-authority-resident.js +102 -0
  541. package/dist/senses/sanctuary-download-credit-presentation.js +31 -0
  542. package/dist/senses/sanctuary-full-visibility-contract.js +302 -0
  543. package/dist/senses/sanctuary-grounding.js +118 -0
  544. package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
  545. package/dist/senses/sanctuary-health-acceptance-probe.js +774 -0
  546. package/dist/senses/sanctuary-health-runner.js +100 -0
  547. package/dist/senses/sanctuary-health.js +408 -0
  548. package/dist/senses/sanctuary-install-state-contract.js +126 -0
  549. package/dist/senses/sanctuary-interactive-control.js +410 -0
  550. package/dist/senses/sanctuary-media-catalog-contract.js +297 -0
  551. package/dist/senses/sanctuary-media-optimization.js +514 -0
  552. package/dist/senses/sanctuary-runtime.js +340 -0
  553. package/dist/senses/sanctuary-sab.js +85 -0
  554. package/dist/senses/sanctuary-storage-optimization-contract.js +32 -0
  555. package/dist/senses/shared-turn.js +772 -0
  556. package/dist/senses/surface-tool.js +109 -0
  557. package/dist/senses/teams-entry.js +60 -8
  558. package/dist/senses/teams.js +519 -218
  559. package/dist/senses/telegram-admission.js +718 -0
  560. package/dist/senses/telegram-approval-runtime.js +793 -0
  561. package/dist/senses/telegram-attachments.js +186 -0
  562. package/dist/senses/telegram-audit-ledger.js +220 -0
  563. package/dist/senses/telegram-authority-transport.js +231 -0
  564. package/dist/senses/telegram-client.js +1749 -0
  565. package/dist/senses/telegram-effect-adapter.js +896 -0
  566. package/dist/senses/telegram-entry.js +83 -0
  567. package/dist/senses/telegram.js +2324 -0
  568. package/dist/senses/trust-gate.js +104 -9
  569. package/dist/senses/voice/audio-playback.js +237 -0
  570. package/dist/senses/voice/audio-routing.js +119 -0
  571. package/dist/senses/voice/elevenlabs.js +202 -0
  572. package/dist/senses/voice/floor-control.js +431 -0
  573. package/dist/senses/voice/floor-controller.js +115 -0
  574. package/dist/senses/voice/golden-path.js +116 -0
  575. package/dist/senses/voice/index.js +29 -0
  576. package/dist/senses/voice/meeting.js +113 -0
  577. package/dist/senses/voice/outbound.js +190 -0
  578. package/dist/senses/voice/phone.js +33 -0
  579. package/dist/senses/voice/playback.js +139 -0
  580. package/dist/senses/voice/realtime-eval.js +496 -0
  581. package/dist/senses/voice/realtime-trace.js +531 -0
  582. package/dist/senses/voice/transcript.js +70 -0
  583. package/dist/senses/voice/turn.js +191 -0
  584. package/dist/senses/voice/twilio-phone-runtime.js +807 -0
  585. package/dist/senses/voice/twilio-phone.js +5031 -0
  586. package/dist/senses/voice/types.js +2 -0
  587. package/dist/senses/voice/whisper.js +161 -0
  588. package/dist/senses/voice-entry.js +81 -0
  589. package/dist/senses/voice-realtime-eval-command.js +99 -0
  590. package/dist/senses/voice-realtime-eval-entry.js +21 -0
  591. package/dist/senses/voice-twilio-entry.js +87 -0
  592. package/dist/trips/core.js +138 -0
  593. package/dist/trips/store.js +265 -0
  594. package/dist/util/frontmatter.js +69 -0
  595. package/npm-shrinkwrap.json +8138 -0
  596. package/package.json +72 -13
  597. package/skills/agent-commerce.md +113 -0
  598. package/skills/browser-navigation.md +117 -0
  599. package/skills/commerce-setup-guide.md +116 -0
  600. package/skills/commerce-setup.md +84 -0
  601. package/skills/configure-dev-tools.md +99 -0
  602. package/skills/travel-planning.md +138 -0
  603. package/dist/heart/daemon/auth-flow.js +0 -430
  604. package/dist/heart/daemon/ouro-path-installer.js +0 -260
  605. package/dist/heart/daemon/ouro-version-manager.js +0 -171
  606. package/dist/heart/daemon/update-hooks.js +0 -138
  607. package/dist/heart/obligations.js +0 -191
  608. package/dist/heart/safe-workspace.js +0 -228
  609. package/dist/heart/session-recall.js +0 -116
  610. package/dist/mind/friends/channel.js +0 -84
  611. package/dist/mind/friends/group-context.js +0 -144
  612. package/dist/mind/friends/resolver.js +0 -84
  613. package/dist/mind/friends/store-file.js +0 -190
  614. package/dist/mind/friends/store.js +0 -4
  615. package/dist/mind/friends/tokens.js +0 -26
  616. package/dist/mind/friends/trust-explanation.js +0 -74
  617. package/dist/mind/friends/types.js +0 -29
  618. package/dist/repertoire/tasks/board.js +0 -134
  619. package/dist/repertoire/tasks/index.js +0 -224
  620. package/dist/repertoire/tasks/lifecycle.js +0 -80
  621. package/dist/repertoire/tasks/middleware.js +0 -65
  622. package/dist/repertoire/tasks/parser.js +0 -173
  623. package/dist/repertoire/tasks/scanner.js +0 -132
  624. package/dist/repertoire/tasks/transitions.js +0 -144
  625. package/dist/senses/bluebubbles-entry.js +0 -13
  626. package/dist/senses/bluebubbles.js +0 -1181
  627. package/dist/senses/debug-activity.js +0 -154
  628. package/dist/senses/inner-dialog-worker.js +0 -90
  629. package/dist/senses/inner-dialog.js +0 -525
  630. package/subagents/README.md +0 -7
  631. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  632. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  633. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  634. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  635. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  636. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  637. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  638. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  639. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  640. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  641. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  642. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  643. /package/dist/{repertoire/tasks/types.js → heart/attachments/sources/adapter.js} +0 -0
  644. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  645. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  646. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  647. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -0,0 +1,3841 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BB_IN_FLIGHT_MAX_AGE_MS = exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS = void 0;
37
+ exports.pruneBlueBubblesSemanticObservationGenerations = pruneBlueBubblesSemanticObservationGenerations;
38
+ exports.tokenizeForDedupe = tokenizeForDedupe;
39
+ exports.jaccardSimilarity = jaccardSimilarity;
40
+ exports.isBlueBubblesMessageInFlight = isBlueBubblesMessageInFlight;
41
+ exports.beginBlueBubblesMessageInFlight = beginBlueBubblesMessageInFlight;
42
+ exports.endBlueBubblesMessageInFlight = endBlueBubblesMessageInFlight;
43
+ exports.__resetBlueBubblesInFlightForTests = __resetBlueBubblesInFlightForTests;
44
+ exports.createBlueBubblesCallbacks = createBlueBubblesCallbacks;
45
+ exports.isAgentSelfHandle = isAgentSelfHandle;
46
+ exports.getDiscoveredOwnHandles = getDiscoveredOwnHandles;
47
+ exports.clearDiscoveredOwnHandles = clearDiscoveredOwnHandles;
48
+ exports.recordDiscoveredOwnHandle = recordDiscoveredOwnHandle;
49
+ exports.seedBlueBubblesStartupSemanticObservations = seedBlueBubblesStartupSemanticObservations;
50
+ exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
51
+ exports.recoverQueuedBlueBubblesMessages = recoverQueuedBlueBubblesMessages;
52
+ exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
53
+ exports.recoverCapturedBlueBubblesInboundMessages = recoverCapturedBlueBubblesInboundMessages;
54
+ exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
55
+ exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
56
+ exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
57
+ exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
58
+ exports.startBlueBubblesApp = startBlueBubblesApp;
59
+ const fs = __importStar(require("node:fs"));
60
+ const http = __importStar(require("node:http"));
61
+ const path = __importStar(require("node:path"));
62
+ const node_crypto_1 = require("node:crypto");
63
+ const core_1 = require("../../heart/core");
64
+ const config_1 = require("../../heart/config");
65
+ const identity_1 = require("../../heart/identity");
66
+ const machine_identity_1 = require("../../heart/machine-identity");
67
+ const runtime_cwd_1 = require("../../heart/runtime-cwd");
68
+ const turn_coordinator_1 = require("../../heart/turn-coordinator");
69
+ const context_1 = require("../../mind/context");
70
+ const friends_1 = require("@ouro.bot/friends");
71
+ const pending_1 = require("../../mind/pending");
72
+ const prompt_1 = require("../../mind/prompt");
73
+ const mcp_manager_1 = require("../../repertoire/mcp-manager");
74
+ const runtime_1 = require("../../nerves/runtime");
75
+ const session_transaction_1 = require("../../mind/session-transaction");
76
+ const orientation_frame_1 = require("../../heart/orientation-frame");
77
+ const proactive_content_guard_1 = require("../proactive-content-guard");
78
+ const bluebubbles_meta_guard_1 = require("../bluebubbles-meta-guard");
79
+ const model_1 = require("./model");
80
+ const client_1 = require("./client");
81
+ const webhook_registration_1 = require("./webhook-registration");
82
+ const inbound_log_1 = require("./inbound-log");
83
+ const mutation_log_1 = require("./mutation-log");
84
+ const processed_log_1 = require("./processed-log");
85
+ const semantic_receipts_1 = require("./semantic-receipts");
86
+ const runtime_state_1 = require("./runtime-state");
87
+ const session_cleanup_1 = require("./session-cleanup");
88
+ const reaction_policy_1 = require("./reaction-policy");
89
+ const latest_turn_1 = require("./latest-turn");
90
+ const active_turns_1 = require("./active-turns");
91
+ const trust_gate_1 = require("../trust-gate");
92
+ const pipeline_1 = require("../pipeline");
93
+ const context_packet_ledger_1 = require("../context-packet-ledger");
94
+ const context_packet_1 = require("./context-packet");
95
+ const autonomy_budget_1 = require("../../heart/autonomy-budget");
96
+ const outbound_state_1 = require("./outbound-state");
97
+ const bbFailoverStates = new Map();
98
+ const activeSemanticHandlingSlots = new Map();
99
+ const semanticObservationGenerations = new Map();
100
+ const BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES = new Set([
101
+ "accepted",
102
+ "enqueued",
103
+ "local-visible",
104
+ "delivered",
105
+ ]);
106
+ exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS = 15 * 60_000;
107
+ function pruneBlueBubblesSemanticObservationGenerations(nowMs = Date.now()) {
108
+ let pruned = 0;
109
+ for (const [keyHash, generation] of semanticObservationGenerations) {
110
+ if (generation.retainedAtMs === null
111
+ || nowMs - generation.retainedAtMs < exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS)
112
+ continue;
113
+ (0, latest_turn_1.clearPending)(generation.reservation);
114
+ semanticObservationGenerations.delete(keyHash);
115
+ pruned += 1;
116
+ }
117
+ return pruned;
118
+ }
119
+ function adoptBlueBubblesSemanticObservation(keyHash, observedReservation, activate) {
120
+ pruneBlueBubblesSemanticObservationGenerations();
121
+ const existing = semanticObservationGenerations.get(keyHash);
122
+ if (!existing) {
123
+ const generation = { reservation: observedReservation, retry: false, retainedAtMs: null };
124
+ semanticObservationGenerations.set(keyHash, generation);
125
+ return generation;
126
+ }
127
+ (0, latest_turn_1.mergeObservationReservations)(existing.reservation, observedReservation);
128
+ if (activate) {
129
+ existing.retainedAtMs = null;
130
+ (0, latest_turn_1.reactivateObservation)(existing.reservation);
131
+ }
132
+ return existing;
133
+ }
134
+ function suspendBlueBubblesSemanticObservation(generation) {
135
+ generation.retry = true;
136
+ generation.retainedAtMs = Date.now();
137
+ (0, latest_turn_1.reactivateObservation)(generation.reservation);
138
+ }
139
+ function completeBlueBubblesSemanticObservation(keyHash, generation) {
140
+ (0, latest_turn_1.clearPending)(generation.reservation);
141
+ semanticObservationGenerations.delete(keyHash);
142
+ }
143
+ async function acquireBlueBubblesSemanticHandlingSlot(keyHash, generation) {
144
+ const active = activeSemanticHandlingSlots.get(keyHash);
145
+ if (!active) {
146
+ const slot = {
147
+ generation,
148
+ reservation: generation.reservation,
149
+ waiters: [],
150
+ };
151
+ activeSemanticHandlingSlots.set(keyHash, slot);
152
+ return {
153
+ status: "owner",
154
+ slot,
155
+ reservation: generation.reservation,
156
+ allowSameGenerationRetry: generation.retry,
157
+ };
158
+ }
159
+ return await new Promise((resolve) => {
160
+ active.waiters.push(resolve);
161
+ });
162
+ }
163
+ function releaseBlueBubblesSemanticHandlingSlot(keyHash, slot, outcome) {
164
+ if (outcome === "retryable") {
165
+ slot.generation.retry = true;
166
+ const next = slot.waiters.shift();
167
+ if (next) {
168
+ (0, latest_turn_1.reactivateObservation)(slot.reservation);
169
+ next({
170
+ status: "owner",
171
+ slot,
172
+ reservation: slot.reservation,
173
+ allowSameGenerationRetry: true,
174
+ });
175
+ return;
176
+ }
177
+ }
178
+ activeSemanticHandlingSlots.delete(keyHash);
179
+ if (outcome === "terminal") {
180
+ completeBlueBubblesSemanticObservation(keyHash, slot.generation);
181
+ }
182
+ else {
183
+ suspendBlueBubblesSemanticObservation(slot.generation);
184
+ }
185
+ for (const resolve of slot.waiters.splice(0)) {
186
+ resolve({ status: "handled_by_owner" });
187
+ }
188
+ }
189
+ /**
190
+ * In-flight message tracker.
191
+ *
192
+ * Stores the timestamp (ms) when each (sessionKey, messageGuid) was claimed.
193
+ * Stale entries (older than BB_IN_FLIGHT_MAX_AGE_MS) are treated as expired:
194
+ * `isBlueBubblesMessageInFlight` returns false, and `beginBlueBubblesMessageInFlight`
195
+ * is allowed to replace the stale marker.
196
+ *
197
+ * Rationale: `handleBlueBubblesNormalizedEvent` has many exit paths and a leak
198
+ * on any one of them strands the marker forever (until BB sense process restart),
199
+ * which silently halts forward progress on the recovery queue. A real
200
+ * agent lost BlueBubbles inbound for 12+ hours on 2026-05-11 because of
201
+ * exactly this.
202
+ *
203
+ * The TTL bounds the worst case: a leaked marker self-clears after
204
+ * BB_IN_FLIGHT_MAX_AGE_MS, and the next recovery pass can retry. This is
205
+ * defense-in-depth alongside auditing the explicit `endBlueBubblesMessageInFlight`
206
+ * exit-path calls — the audit is still worthwhile, but the TTL guarantees the
207
+ * class of bug can't wedge the queue indefinitely.
208
+ */
209
+ const bbInFlightMessageClaims = new Map();
210
+ /**
211
+ * In-flight markers expire 50% beyond the longest expected turn (recovery turn
212
+ * timeout = 10 min). 15 min gives normal turns full headroom while preventing
213
+ * a leaked marker from blocking forward progress for hours.
214
+ */
215
+ exports.BB_IN_FLIGHT_MAX_AGE_MS = 15 * 60_000;
216
+ // ── Near-duplicate outward-text detection ────────────────────────
217
+ // Used by createBlueBubblesCallbacks to collapse mid-turn rephrasings of the
218
+ // same answer/status into a single delivery. Exposed for direct unit testing
219
+ // of the similarity behavior without spinning up the full callbacks closure.
220
+ const NEAR_DUPLICATE_JACCARD_THRESHOLD = 0.7;
221
+ const NEAR_DUPLICATE_MIN_TOKENS = 5;
222
+ function tokenizeForDedupe(text) {
223
+ const matches = text.toLowerCase().match(/[a-z0-9']+/g);
224
+ if (!matches)
225
+ return new Set();
226
+ return new Set(matches);
227
+ }
228
+ function jaccardSimilarity(a, b) {
229
+ if (a.size === 0 || b.size === 0)
230
+ return 0;
231
+ let intersection = 0;
232
+ for (const token of a) {
233
+ if (b.has(token))
234
+ intersection += 1;
235
+ }
236
+ const union = a.size + b.size - intersection;
237
+ return intersection / union;
238
+ }
239
+ function blueBubblesMessageKey(sessionKey, messageGuid) {
240
+ return `${sessionKey}:${messageGuid.trim()}`;
241
+ }
242
+ function isClaimStale(claimedAtMs, nowMs = Date.now()) {
243
+ return nowMs - claimedAtMs >= exports.BB_IN_FLIGHT_MAX_AGE_MS;
244
+ }
245
+ /** Internal — exported only so unit tests can exercise the TTL semantics. */
246
+ function isBlueBubblesMessageInFlight(sessionKey, messageGuid) {
247
+ if (!messageGuid.trim())
248
+ return false;
249
+ const claimedAtMs = bbInFlightMessageClaims.get(blueBubblesMessageKey(sessionKey, messageGuid));
250
+ if (claimedAtMs === undefined)
251
+ return false;
252
+ return !isClaimStale(claimedAtMs);
253
+ }
254
+ /** Internal — exported only so unit tests can exercise the TTL semantics. */
255
+ function beginBlueBubblesMessageInFlight(sessionKey, messageGuid) {
256
+ if (!messageGuid.trim())
257
+ return true;
258
+ const key = blueBubblesMessageKey(sessionKey, messageGuid);
259
+ const existing = bbInFlightMessageClaims.get(key);
260
+ if (existing !== undefined && !isClaimStale(existing))
261
+ return false;
262
+ if (existing !== undefined && isClaimStale(existing)) {
263
+ (0, runtime_1.emitNervesEvent)({
264
+ level: "warn",
265
+ component: "senses",
266
+ event: "senses.bluebubbles_in_flight_marker_expired",
267
+ message: "in-flight marker expired by TTL; allowing re-claim",
268
+ meta: {
269
+ sessionKey,
270
+ messageGuid,
271
+ claimAgeMs: Date.now() - existing,
272
+ ttlMs: exports.BB_IN_FLIGHT_MAX_AGE_MS,
273
+ },
274
+ });
275
+ }
276
+ bbInFlightMessageClaims.set(key, Date.now());
277
+ return true;
278
+ }
279
+ /** Internal — exported only so unit tests can exercise the TTL semantics. */
280
+ function endBlueBubblesMessageInFlight(sessionKey, messageGuid) {
281
+ bbInFlightMessageClaims.delete(blueBubblesMessageKey(sessionKey, messageGuid));
282
+ }
283
+ /**
284
+ * Test-only: clear all in-flight markers. Production code must use begin/end pairs.
285
+ * Exported so unit tests can reset cross-test state without resetting modules.
286
+ */
287
+ function __resetBlueBubblesInFlightForTests() {
288
+ bbInFlightMessageClaims.clear();
289
+ }
290
+ const defaultDeps = {
291
+ getAgentName: identity_1.getAgentName,
292
+ buildSystem: prompt_1.buildSystem,
293
+ runAgent: core_1.runAgent,
294
+ loadSession: context_1.loadSession,
295
+ saveSession: context_1.saveSession,
296
+ postTurnTrim: context_1.postTurnTrim,
297
+ postTurnPersist: context_1.postTurnPersist,
298
+ sessionPath: config_1.sessionPath,
299
+ accumulateFriendTokens: friends_1.accumulateFriendTokens,
300
+ createClient: () => (0, client_1.createBlueBubblesClient)(),
301
+ recordMutation: mutation_log_1.recordBlueBubblesMutation,
302
+ createFriendStore: () => new friends_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends")),
303
+ createFriendResolver: (store, params) => new friends_1.FriendResolver(store, params),
304
+ createServer: http.createServer,
305
+ getOwnHandles: () => [...(0, config_1.getBlueBubblesConfig)().ownHandles, ...discoveredOwnHandles],
306
+ promoteFailoverState: (sessionKey, state) => {
307
+ bbFailoverStates.set(sessionKey, structuredClone(state));
308
+ },
309
+ recordProcessed: processed_log_1.recordProcessedBlueBubblesMessage,
310
+ withSessionTurnLease: session_transaction_1.withSessionTurnLease,
311
+ };
312
+ const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
313
+ const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
314
+ const BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS = 30_000;
315
+ const BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS = 2 * 60_000;
316
+ const BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS = 10 * 60_000;
317
+ const BLUEBUBBLES_LIVE_TURN_STALLED_MS = 90_000;
318
+ const BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS = 2_000;
319
+ const BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS = 20_000;
320
+ const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
321
+ const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
322
+ const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
323
+ const BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS = 24 * 60 * 60 * 1000;
324
+ const BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS = 10 * 60 * 1000;
325
+ class BlueBubblesRecoveryTurnTimeoutError extends Error {
326
+ constructor(timeoutMs) {
327
+ super(`bluebubbles recovery turn timed out after ${timeoutMs}ms`);
328
+ this.name = "BlueBubblesRecoveryTurnTimeoutError";
329
+ }
330
+ }
331
+ class BlueBubblesActivityTimeoutError extends Error {
332
+ constructor(operation, timeoutMs) {
333
+ super(`bluebubbles ${operation} activity timed out after ${timeoutMs}ms`);
334
+ this.name = "BlueBubblesActivityTimeoutError";
335
+ }
336
+ }
337
+ function withBlueBubblesActivityTimeout(operation, timeoutMs, controller, task) {
338
+ let timer = null;
339
+ controller.signal.throwIfAborted();
340
+ let removeAbortListener;
341
+ const aborted = new Promise((_, reject) => {
342
+ const rejectFromAbort = () => {
343
+ reject(controller.signal.reason);
344
+ };
345
+ controller.signal.addEventListener("abort", rejectFromAbort, { once: true });
346
+ removeAbortListener = () => controller.signal.removeEventListener("abort", rejectFromAbort);
347
+ });
348
+ return Promise.race([
349
+ Promise.resolve().then(() => task(controller.signal)),
350
+ aborted,
351
+ new Promise((_, reject) => {
352
+ timer = setTimeout(() => {
353
+ const error = new BlueBubblesActivityTimeoutError(operation, timeoutMs);
354
+ controller.abort(error);
355
+ reject(error);
356
+ }, timeoutMs);
357
+ /* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
358
+ if (typeof timer.unref === "function") {
359
+ timer.unref();
360
+ }
361
+ }),
362
+ ]).finally(() => {
363
+ removeAbortListener();
364
+ /* v8 ignore next -- timer is assigned unless a callback task throws synchronously; callback tasks are async @preserve */
365
+ if (timer !== null)
366
+ clearTimeout(timer);
367
+ });
368
+ }
369
+ async function waitForBlueBubblesCallbackCleanup(queue, timeoutMs, meta) {
370
+ let timer = null;
371
+ let timedOut = false;
372
+ await Promise.race([
373
+ queue,
374
+ new Promise((resolve) => {
375
+ timer = setTimeout(() => {
376
+ timedOut = true;
377
+ resolve();
378
+ }, timeoutMs);
379
+ /* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
380
+ if (typeof timer.unref === "function") {
381
+ timer.unref();
382
+ }
383
+ }),
384
+ ]).finally(() => {
385
+ /* v8 ignore next -- timer is assigned for the cleanup race; null only protects unusual synchronous construction failure @preserve */
386
+ if (timer !== null)
387
+ clearTimeout(timer);
388
+ });
389
+ if (!timedOut)
390
+ return true;
391
+ (0, runtime_1.emitNervesEvent)({
392
+ level: "warn",
393
+ component: "senses",
394
+ event: "senses.bluebubbles_activity_cleanup_timeout",
395
+ message: "bluebubbles callback cleanup timed out; releasing live turn lane",
396
+ meta: { ...meta, timeoutMs },
397
+ });
398
+ return false;
399
+ }
400
+ function resolveFriendParams(event) {
401
+ if (event.chat.isGroup) {
402
+ return {
403
+ provider: "imessage-handle",
404
+ externalId: `group:${event.chat.chatGuid}`,
405
+ displayName: event.chat.displayName ?? "Unknown Group",
406
+ channel: "bluebubbles",
407
+ };
408
+ }
409
+ return {
410
+ provider: "imessage-handle",
411
+ externalId: event.sender.externalId,
412
+ displayName: event.sender.displayName,
413
+ channel: "bluebubbles",
414
+ };
415
+ }
416
+ function resolveGroupExternalId(event) {
417
+ return `group:${event.chat.chatGuid}`;
418
+ }
419
+ /**
420
+ * Check if any participant in a group chat is a known family member.
421
+ * Looks up each participant handle in the friend store.
422
+ */
423
+ async function checkGroupHasFamilyMember(store, event) {
424
+ if (!event.chat.isGroup)
425
+ return false;
426
+ for (const handle of event.chat.participantHandles) {
427
+ const friend = await store.findByExternalId("imessage-handle", handle);
428
+ if (friend?.trustLevel === "family")
429
+ return true;
430
+ }
431
+ return false;
432
+ }
433
+ /**
434
+ * Check if an acquaintance shares any group chat with a family member.
435
+ * Compares group-prefixed externalIds between the acquaintance and all family members.
436
+ */
437
+ async function checkHasExistingGroupWithFamily(store, senderFriend) {
438
+ const trustLevel = senderFriend.trustLevel ?? "friend";
439
+ if (trustLevel !== "acquaintance")
440
+ return false;
441
+ const acquaintanceGroups = new Set((senderFriend.externalIds ?? [])
442
+ .filter((eid) => eid.externalId.startsWith("group:"))
443
+ .map((eid) => eid.externalId));
444
+ if (acquaintanceGroups.size === 0)
445
+ return false;
446
+ const allFriends = await (store.listAll?.() ?? Promise.resolve([]));
447
+ for (const friend of allFriends) {
448
+ if (friend.trustLevel !== "family")
449
+ continue;
450
+ const friendGroups = (friend.externalIds ?? [])
451
+ .filter((eid) => eid.externalId.startsWith("group:"))
452
+ .map((eid) => eid.externalId);
453
+ for (const group of friendGroups) {
454
+ if (acquaintanceGroups.has(group))
455
+ return true;
456
+ }
457
+ }
458
+ return false;
459
+ }
460
+ function extractMessageText(content) {
461
+ if (typeof content === "string")
462
+ return content;
463
+ if (!Array.isArray(content))
464
+ return "";
465
+ return content
466
+ .map((part) => {
467
+ if (part && typeof part === "object" && "type" in part && part.type === "text" && typeof part.text === "string") {
468
+ return part.text;
469
+ }
470
+ return "";
471
+ })
472
+ .filter(Boolean)
473
+ .join("\n");
474
+ }
475
+ function isHistoricalLaneMetadataLine(line) {
476
+ return /^\[(conversation scope|recent active lanes|routing control):?/i.test(line)
477
+ || /^- (top_level|thread:[^:]+):/i.test(line);
478
+ }
479
+ function extractHistoricalLaneSummary(messages) {
480
+ const seen = new Set();
481
+ const summaries = [];
482
+ for (let index = messages.length - 1; index >= 0; index--) {
483
+ const message = messages[index];
484
+ if (message.role !== "user")
485
+ continue;
486
+ const text = extractMessageText(message.content);
487
+ if (!text)
488
+ continue;
489
+ const firstLine = text.split("\n")[0].trim();
490
+ const threadMatch = firstLine.match(/thread id: ([^\]|]+)/i);
491
+ const laneKey = threadMatch
492
+ ? `thread:${threadMatch[1].trim()}`
493
+ : /top[-_]level/i.test(firstLine)
494
+ ? "top_level"
495
+ : null;
496
+ if (!laneKey || seen.has(laneKey))
497
+ continue;
498
+ seen.add(laneKey);
499
+ const snippet = text
500
+ .split("\n")
501
+ .slice(1)
502
+ .map((line) => line.trim())
503
+ .find((line) => line.length > 0 && !isHistoricalLaneMetadataLine(line))
504
+ ?.slice(0, 80) ?? "(no recent text)";
505
+ summaries.push({
506
+ key: laneKey,
507
+ label: laneKey === "top_level" ? "top_level" : laneKey,
508
+ snippet,
509
+ });
510
+ if (summaries.length >= 5)
511
+ break;
512
+ }
513
+ return summaries;
514
+ }
515
+ function buildBlueBubblesOrientationSource(event, existingMessages, evidence, conversationKind, repliedToText) {
516
+ const repairNotice = event.repairNotice?.trim();
517
+ const actor = evidence.actor;
518
+ const participants = evidence.participants
519
+ .filter((participant) => !(participant.provider === actor.provider
520
+ && participant.externalId === actor.externalId))
521
+ .map((participant) => ({
522
+ role: "group_participant_only",
523
+ provider: participant.provider,
524
+ externalId: participant.externalId,
525
+ displayName: participant.displayName,
526
+ }));
527
+ const presentation = {
528
+ authority: "presentation_only",
529
+ conversationKind,
530
+ event: {
531
+ provider: evidence.provider,
532
+ kind: evidence.kind,
533
+ sourceEventType: evidence.sourceEventType,
534
+ fromMe: evidence.fromMe,
535
+ },
536
+ actor: {
537
+ role: "observed_actor",
538
+ provider: actor.provider,
539
+ externalId: actor.externalId,
540
+ displayName: actor.displayName,
541
+ },
542
+ participants,
543
+ };
544
+ const summaries = extractHistoricalLaneSummary(existingMessages);
545
+ const threadId = event.threadOriginatorGuid?.trim() || undefined;
546
+ const hasThreadRoute = !!threadId || summaries.some((summary) => summary.key.startsWith("thread:"));
547
+ return {
548
+ kind: "bluebubbles",
549
+ ...presentation,
550
+ lane: threadId ? "thread" : "top_level",
551
+ defaultReplyTarget: threadId ? "current_lane" : "top_level",
552
+ ...(threadId ? { threadId } : {}),
553
+ ...(repliedToText ? { replyingToText: repliedToText } : {}),
554
+ ...(repairNotice ? { repairNotice } : {}),
555
+ ...(summaries.length > 0 ? { recentLanes: summaries } : {}),
556
+ ...(hasThreadRoute
557
+ ? { routingHint: "use bluebubbles_set_reply_target to choose current_lane, top_level, or a listed thread before sending BlueBubbles-specific replies." }
558
+ : {}),
559
+ };
560
+ }
561
+ function buildInboundText(event) {
562
+ const baseText = event.textForAgent;
563
+ if (!event.chat.isGroup) {
564
+ return baseText;
565
+ }
566
+ return `${event.sender.displayName}: ${baseText}`;
567
+ }
568
+ function buildInboundContent(event) {
569
+ const text = buildInboundText(event);
570
+ if (!event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
571
+ return text;
572
+ }
573
+ return [
574
+ { type: "text", text },
575
+ ...event.inputPartsForAgent,
576
+ ];
577
+ }
578
+ function sessionLikelyContainsMessage(event, existingMessages) {
579
+ const fragment = event.textForAgent.trim();
580
+ if (!fragment)
581
+ return false;
582
+ return existingMessages.some((message) => {
583
+ if (message.role !== "user")
584
+ return false;
585
+ return extractMessageText(message.content).includes(fragment);
586
+ });
587
+ }
588
+ function getBlueBubblesContinuityIngressTexts(event) {
589
+ const text = event.textForAgent.trim();
590
+ if (text.length > 0)
591
+ return [text];
592
+ const fallbackText = (event.inputPartsForAgent ?? [])
593
+ .map((part) => {
594
+ if (part.type === "text" && typeof part.text === "string") {
595
+ return part.text.trim();
596
+ }
597
+ return "";
598
+ })
599
+ .filter(Boolean)
600
+ .join("\n");
601
+ return fallbackText ? [fallbackText] : [];
602
+ }
603
+ function sha256Hex(input) {
604
+ return (0, node_crypto_1.createHash)("sha256").update(input).digest("hex");
605
+ }
606
+ function timestampIsoForAutonomy(event) {
607
+ return Number.isFinite(event.timestamp)
608
+ ? new Date(event.timestamp).toISOString()
609
+ : new Date().toISOString();
610
+ }
611
+ function blueBubblesAutonomyTarget(event) {
612
+ return {
613
+ messageGuid: event.messageGuid,
614
+ sessionKeyHash: sha256Hex(event.chat.sessionKey),
615
+ chatGuidHash: event.chat.chatGuid ? sha256Hex(event.chat.chatGuid) : undefined,
616
+ chatIdentifierHash: event.chat.chatIdentifier ? sha256Hex(event.chat.chatIdentifier) : undefined,
617
+ };
618
+ }
619
+ function blueBubblesRecoveryStormInput(agentName, event, source) {
620
+ return {
621
+ agent: agentName,
622
+ triggerType: "recovery",
623
+ sourceKind: "sense",
624
+ senseOrHabit: "bluebubbles",
625
+ provider: "bluebubbles-recovery",
626
+ target: blueBubblesAutonomyTarget(event),
627
+ normalizedErrorName: "BlueBubblesRecoveryError",
628
+ normalizedErrorCode: "RECOVERY_FAILED",
629
+ codeLocation: `senses/bluebubbles/${source}`,
630
+ idempotencyBucket: `bluebubbles-recovery:${source}`,
631
+ };
632
+ }
633
+ function shouldUseBlueBubblesRecoveryBudget(source, options) {
634
+ return options.autonomyBudgetTrigger === "recovery"
635
+ || source === "mutation-recovery"
636
+ || source === "upstream-catchup"
637
+ || source === "recovery-bootstrap";
638
+ }
639
+ function shouldCountBlueBubblesRecoveryResultAsSkipped(reason) {
640
+ return reason === "already_processed"
641
+ || reason === "autonomy_budget_blocked"
642
+ || reason === "mutation_state_only"
643
+ || reason === "from_me";
644
+ }
645
+ function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
646
+ try {
647
+ (0, autonomy_budget_1.recordAutonomyFailure)((0, identity_1.getAgentRoot)(agentName), {
648
+ ...blueBubblesRecoveryStormInput(agentName, event, source),
649
+ occurredAt: timestampIsoForAutonomy(event),
650
+ });
651
+ }
652
+ catch {
653
+ // Autonomy failure accounting must not turn a recovery error into a broader sense outage.
654
+ }
655
+ }
656
+ function insertEphemeralContextMessages(messages, contextMessages, currentUserMessage) {
657
+ if (contextMessages.length === 0)
658
+ return messages;
659
+ const requiredCurrentIndex = currentUserMessage ? messages.indexOf(currentUserMessage) : -1;
660
+ const insertionIndex = requiredCurrentIndex >= 0 ? requiredCurrentIndex : Math.max(0, messages.length - 1);
661
+ return [
662
+ ...messages.slice(0, insertionIndex),
663
+ ...contextMessages,
664
+ ...messages.slice(insertionIndex),
665
+ ];
666
+ }
667
+ function knownProviderMessageTexts(messages) {
668
+ return messages
669
+ .map((message) => extractMessageText(message.content))
670
+ .map((text) => text.trim())
671
+ .filter((text) => text.length > 0);
672
+ }
673
+ async function buildBlueBubblesContextMessages(input) {
674
+ const event = input.event;
675
+ if (!Number.isFinite(event.timestamp))
676
+ return { messages: [], contextPacketIds: [] };
677
+ let result;
678
+ try {
679
+ result = await (0, context_packet_1.buildBlueBubblesContextPacket)({
680
+ agentName: input.agentName,
681
+ client: input.client,
682
+ event,
683
+ knownMessageTexts: knownProviderMessageTexts(input.knownMessages),
684
+ });
685
+ }
686
+ catch (error) {
687
+ (0, runtime_1.emitNervesEvent)({
688
+ level: "warn",
689
+ component: "senses",
690
+ event: "senses.bluebubbles_context_packet_error",
691
+ message: "failed to build live bluebubbles context packet",
692
+ meta: {
693
+ messageGuid: event.messageGuid,
694
+ reason: error instanceof Error ? error.message : String(error),
695
+ },
696
+ });
697
+ return { messages: [], contextPacketIds: [] };
698
+ }
699
+ if (!result)
700
+ return { messages: [], contextPacketIds: [] };
701
+ const { packet, optionalRendered, verifiedPredecessorMessage, historyCount } = result;
702
+ const prepared = {
703
+ messages: optionalRendered
704
+ ? [{ role: "system", content: optionalRendered.text }]
705
+ : [],
706
+ verifiedPredecessorMessage,
707
+ };
708
+ try {
709
+ (0, context_packet_ledger_1.writeSenseContextPacket)(input.agentRoot, packet);
710
+ (0, runtime_1.emitNervesEvent)({
711
+ component: "senses",
712
+ event: "senses.bluebubbles_context_packet_injected",
713
+ message: "injected bluebubbles same-chat context packet",
714
+ meta: {
715
+ packetId: packet.packetId,
716
+ messageGuid: event.messageGuid,
717
+ contextMessages: historyCount,
718
+ },
719
+ });
720
+ return {
721
+ ...prepared,
722
+ contextPacketIds: [packet.packetId],
723
+ };
724
+ }
725
+ catch (error) {
726
+ (0, runtime_1.emitNervesEvent)({
727
+ level: "warn",
728
+ component: "senses",
729
+ event: "senses.bluebubbles_context_packet_error",
730
+ message: "failed to build live bluebubbles context packet",
731
+ meta: {
732
+ messageGuid: event.messageGuid,
733
+ reason: error instanceof Error ? error.message : String(error),
734
+ },
735
+ });
736
+ return { ...prepared, contextPacketIds: [] };
737
+ }
738
+ }
739
+ function createReplyTargetController(event) {
740
+ const defaultTargetLabel = event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
741
+ let selection = event.threadOriginatorGuid?.trim()
742
+ ? { target: "current_lane" }
743
+ : { target: "top_level" };
744
+ return {
745
+ getReplyToMessageGuid() {
746
+ if (selection.target === "top_level")
747
+ return undefined;
748
+ if (selection.target === "thread")
749
+ return selection.threadOriginatorGuid.trim();
750
+ return event.threadOriginatorGuid?.trim() ? event.messageGuid : undefined;
751
+ },
752
+ setSelection(next) {
753
+ selection = next;
754
+ if (next.target === "top_level") {
755
+ return "bluebubbles reply target override: top_level";
756
+ }
757
+ if (next.target === "thread") {
758
+ return `bluebubbles reply target override: thread:${next.threadOriginatorGuid}`;
759
+ }
760
+ return `bluebubbles reply target: using default for this turn (${defaultTargetLabel})`;
761
+ },
762
+ };
763
+ }
764
+ function emitBlueBubblesMarkReadWarning(chat, error) {
765
+ (0, runtime_1.emitNervesEvent)({
766
+ level: "warn",
767
+ component: "senses",
768
+ event: "senses.bluebubbles_mark_read_error",
769
+ message: "failed to mark bluebubbles chat as read",
770
+ meta: {
771
+ chatGuid: chat.chatGuid ?? null,
772
+ reason: error instanceof Error ? error.message : String(error),
773
+ },
774
+ });
775
+ }
776
+ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVisibleActivity, options = {}) {
777
+ let textBuffer = "";
778
+ let typingActive = false;
779
+ let typingCouldBeActive = false;
780
+ let queue = Promise.resolve();
781
+ let outboundClosed = false;
782
+ let nextCleanupToken = 0;
783
+ const activeCleanupTokens = new Set();
784
+ const activeActivityControllers = new Set();
785
+ // Per-turn outward-send dedupe. A single createBlueBubblesCallbacks lifetime
786
+ // serves one inbound turn, so collapsing identical outward bodies inside
787
+ // this closure is scoped tightly: each fresh inbound turn starts with an
788
+ // empty set. Mid-turn settle/proof retry loops historically caused the
789
+ // agent to re-emit the same answer through multiple speak calls and a
790
+ // final flush, surfacing as 4 near-identical iMessages from one ask
791
+ // (2026-05-08 06:18 incident). The guard lets the engine retry harmlessly
792
+ // without duplicating outward delivery to the friend.
793
+ //
794
+ // PR #699 used exact whitespace+case-normalized match. Post-#699 evidence
795
+ // (2026-05-09 05:25 UTC, evt-001814 + evt-001818) showed two answers that
796
+ // start "yep — I looked it up… Assuming you mean AMC's The Audacity…" with
797
+ // substantially the same content but slight rephrasing — the LLM rewrites
798
+ // the same answer on a retry/recovery loop and bypasses an exact-match
799
+ // guard. We now also keep the original token sets so a fuzzy (Jaccard)
800
+ // check catches near-duplicates from the same turn.
801
+ const sentOutwardTextNorms = new Set();
802
+ const sentOutwardTokenSets = [];
803
+ async function canRunOutbound() {
804
+ if (outboundClosed)
805
+ return false;
806
+ if (options.isOutboundCurrent && !options.isOutboundCurrent())
807
+ return false;
808
+ if (options.admitOutbound && !await options.admitOutbound())
809
+ return false;
810
+ if (outboundClosed)
811
+ return false;
812
+ if (options.isOutboundCurrent && !options.isOutboundCurrent())
813
+ return false;
814
+ return true;
815
+ }
816
+ function enqueue(operation, task, enqueueOptions = {}) {
817
+ const cleanupToken = enqueueOptions.cleanupToken;
818
+ const controller = new AbortController();
819
+ activeActivityControllers.add(controller);
820
+ queue = queue
821
+ .then(async () => {
822
+ if (cleanupToken === undefined) {
823
+ if (!await canRunOutbound())
824
+ return;
825
+ }
826
+ else if (!activeCleanupTokens.has(cleanupToken)) {
827
+ return;
828
+ }
829
+ await withBlueBubblesActivityTimeout(operation, BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS, controller, task);
830
+ })
831
+ .catch((error) => {
832
+ if (controller.signal.aborted && !(error instanceof BlueBubblesActivityTimeoutError))
833
+ return;
834
+ (0, runtime_1.emitNervesEvent)({
835
+ level: "warn",
836
+ component: "senses",
837
+ event: "senses.bluebubbles_activity_error",
838
+ message: "bluebubbles activity transport failed",
839
+ meta: { operation, reason: error instanceof Error ? error.message : String(error) },
840
+ });
841
+ })
842
+ .finally(() => {
843
+ activeActivityControllers.delete(controller);
844
+ });
845
+ }
846
+ function abortActiveActivity(reason) {
847
+ for (const controller of activeActivityControllers) {
848
+ controller.abort(new Error(reason));
849
+ }
850
+ }
851
+ function enqueueTypingStop() {
852
+ typingActive = false;
853
+ const cleanupToken = ++nextCleanupToken;
854
+ activeCleanupTokens.add(cleanupToken);
855
+ enqueue("typing_stop", async (signal) => {
856
+ try {
857
+ await client.setTyping(chat, false, signal);
858
+ typingCouldBeActive = false;
859
+ }
860
+ finally {
861
+ activeCleanupTokens.delete(cleanupToken);
862
+ }
863
+ }, { cleanupToken });
864
+ }
865
+ function startTypingNow() {
866
+ /* v8 ignore next -- defensive guard: callers already check typingActive @preserve */
867
+ if (typingActive)
868
+ return;
869
+ /* v8 ignore next -- defensive guard: public callback entrypoints already drop after outbound close @preserve */
870
+ if (outboundClosed)
871
+ return;
872
+ typingActive = true;
873
+ typingCouldBeActive = true;
874
+ enqueue("typing_start", async (signal) => {
875
+ const [markReadResult, typingResult] = await Promise.allSettled([
876
+ client.markChatRead(chat, signal),
877
+ client.setTyping(chat, true, signal),
878
+ ]);
879
+ if (markReadResult.status === "rejected") {
880
+ emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
881
+ }
882
+ if (typingResult.status === "rejected") {
883
+ throw typingResult.reason;
884
+ }
885
+ });
886
+ }
887
+ function recordVisibleActivity() {
888
+ onVisibleActivity?.();
889
+ }
890
+ function isDuplicateOutwardText(trimmed) {
891
+ const norm = trimmed.replace(/\s+/g, " ").trim().toLowerCase();
892
+ if (sentOutwardTextNorms.has(norm))
893
+ return true;
894
+ // Fuzzy near-duplicate: when the same answer is rephrased between speak
895
+ // and settle (or across two speak calls in a recovery loop), the exact
896
+ // norm differs but token overlap is very high. We compare against every
897
+ // already-sent body's token set; if any has Jaccard overlap >= the
898
+ // threshold, treat as a duplicate. We require a minimum token count on
899
+ // the new body so single-word replies ("yes", "ok") don't get suppressed
900
+ // against any previous short one with shared tokens.
901
+ const newTokens = tokenizeForDedupe(trimmed);
902
+ if (newTokens.size >= NEAR_DUPLICATE_MIN_TOKENS) {
903
+ for (const prevTokens of sentOutwardTokenSets) {
904
+ if (prevTokens.size < NEAR_DUPLICATE_MIN_TOKENS)
905
+ continue;
906
+ if (jaccardSimilarity(newTokens, prevTokens) >= NEAR_DUPLICATE_JACCARD_THRESHOLD) {
907
+ return true;
908
+ }
909
+ }
910
+ }
911
+ sentOutwardTextNorms.add(norm);
912
+ sentOutwardTokenSets.push(newTokens);
913
+ return false;
914
+ }
915
+ function emitDuplicateOutwardSuppressed(site, messageLength) {
916
+ (0, runtime_1.emitNervesEvent)({
917
+ level: "warn",
918
+ component: "senses",
919
+ event: "bluebubbles.duplicate_outward_suppressed",
920
+ message: "suppressed near-identical outward send within single inbound turn",
921
+ meta: {
922
+ site,
923
+ chatGuid: chat.chatGuid ?? null,
924
+ messageLength,
925
+ },
926
+ });
927
+ }
928
+ return {
929
+ settleOutputMode: "retractable_buffer",
930
+ onModelStart() {
931
+ if (outboundClosed)
932
+ return;
933
+ if (options.enableActivitySignals !== false && !isGroupChat)
934
+ startTypingNow();
935
+ (0, runtime_1.emitNervesEvent)({
936
+ component: "senses",
937
+ event: "senses.bluebubbles_turn_start",
938
+ message: "bluebubbles turn started",
939
+ meta: { chatGuid: chat.chatGuid ?? null },
940
+ });
941
+ },
942
+ onModelStreamStart() {
943
+ (0, runtime_1.emitNervesEvent)({
944
+ component: "senses",
945
+ event: "senses.bluebubbles_stream_start",
946
+ message: "bluebubbles non-streaming response started",
947
+ meta: {},
948
+ });
949
+ },
950
+ onTextChunk(text) {
951
+ if (outboundClosed)
952
+ return;
953
+ if (options.enableActivitySignals !== false && isGroupChat && !typingActive)
954
+ startTypingNow();
955
+ textBuffer += text;
956
+ },
957
+ onReasoningChunk(_text) { },
958
+ onToolStart(name, _args) {
959
+ if (outboundClosed)
960
+ return;
961
+ // Tool activity is a reply commitment, but iMessage is not a tool-progress
962
+ // console. Keep the human-facing thread quiet until the agent has real
963
+ // text, while preserving native read/typing signals and nerves telemetry
964
+ // for debugging.
965
+ if (options.enableActivitySignals !== false && !typingActive && name !== "observe" && name !== "speak")
966
+ startTypingNow();
967
+ (0, runtime_1.emitNervesEvent)({
968
+ component: "senses",
969
+ event: "senses.bluebubbles_tool_start",
970
+ message: "bluebubbles tool execution started",
971
+ meta: { name },
972
+ });
973
+ },
974
+ onToolEnd(name, summary, success) {
975
+ (0, runtime_1.emitNervesEvent)({
976
+ component: "senses",
977
+ event: "senses.bluebubbles_tool_end",
978
+ message: "bluebubbles tool execution completed",
979
+ meta: { name, success, summary },
980
+ });
981
+ },
982
+ onError(error, severity) {
983
+ (0, runtime_1.emitNervesEvent)({
984
+ level: severity === "terminal" ? "error" : "warn",
985
+ component: "senses",
986
+ event: "senses.bluebubbles_turn_error",
987
+ message: "bluebubbles turn callback error",
988
+ meta: { severity, reason: error.message },
989
+ });
990
+ },
991
+ onClearText() {
992
+ textBuffer = "";
993
+ },
994
+ async flushNow() {
995
+ if (outboundClosed) {
996
+ textBuffer = "";
997
+ return;
998
+ }
999
+ // Contract: throws if delivery fails. We deliberately let `client.sendText`
1000
+ // rejections propagate so the engine's speak interception can mark the
1001
+ // tool call as failed and tell the agent the message did not reach the
1002
+ // friend (rather than silently logging and pretending success).
1003
+ const trimmed = textBuffer.trim();
1004
+ if (!trimmed)
1005
+ return;
1006
+ textBuffer = "";
1007
+ if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(trimmed)) {
1008
+ (0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
1009
+ site: "flushNow",
1010
+ message: "bluebubbles speak text blocked: internal meta markers",
1011
+ meta: {
1012
+ chatGuid: chat.chatGuid ?? null,
1013
+ messageLength: trimmed.length,
1014
+ },
1015
+ });
1016
+ return;
1017
+ }
1018
+ if (isDuplicateOutwardText(trimmed)) {
1019
+ emitDuplicateOutwardSuppressed("flushNow", trimmed.length);
1020
+ return;
1021
+ }
1022
+ if (!await canRunOutbound())
1023
+ return;
1024
+ await client.sendText({
1025
+ chat,
1026
+ text: trimmed,
1027
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
1028
+ });
1029
+ recordVisibleActivity();
1030
+ // Note: do NOT call client.setTyping(chat, false) here — the agent is
1031
+ // still mid-turn, so the typing indicator stays ACTIVE.
1032
+ (0, runtime_1.emitNervesEvent)({
1033
+ component: "senses",
1034
+ event: "bluebubbles.speak_flush",
1035
+ message: "bluebubbles flushed mid-turn speak",
1036
+ meta: { messageLength: trimmed.length },
1037
+ });
1038
+ },
1039
+ async flush(flushOptions = {}) {
1040
+ if (outboundClosed) {
1041
+ textBuffer = "";
1042
+ await waitForBlueBubblesCallbackCleanup(queue, BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS, {
1043
+ operation: "flush_after_close",
1044
+ chatGuid: chat.chatGuid ?? null,
1045
+ });
1046
+ return { status: "not_invoked", reason: "closed" };
1047
+ }
1048
+ await queue;
1049
+ const trimmed = textBuffer.trim();
1050
+ if (!trimmed) {
1051
+ if (typingActive) {
1052
+ enqueueTypingStop();
1053
+ await queue;
1054
+ }
1055
+ return { status: "not_invoked", reason: "empty" };
1056
+ }
1057
+ textBuffer = "";
1058
+ /* v8 ignore next 4 -- branch: typing may already be stopped before flush @preserve */
1059
+ if (typingActive) {
1060
+ enqueueTypingStop();
1061
+ await queue;
1062
+ }
1063
+ if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(trimmed)) {
1064
+ (0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
1065
+ site: "flush",
1066
+ message: "bluebubbles outbound text blocked: internal meta markers",
1067
+ meta: {
1068
+ chatGuid: chat.chatGuid ?? null,
1069
+ messageLength: trimmed.length,
1070
+ },
1071
+ });
1072
+ return { status: "not_invoked", reason: "blocked_meta" };
1073
+ }
1074
+ if (isDuplicateOutwardText(trimmed)) {
1075
+ emitDuplicateOutwardSuppressed("flush", trimmed.length);
1076
+ return { status: "not_invoked", reason: "duplicate" };
1077
+ }
1078
+ if (!await canRunOutbound())
1079
+ return { status: "not_invoked", reason: "not_current" };
1080
+ const replyToMessageGuid = replyTarget.getReplyToMessageGuid();
1081
+ const durableOutbound = options.durableOutbound;
1082
+ const durableTempGuid = durableOutbound
1083
+ ? `ouro-inbound-${(0, node_crypto_1.createHash)("sha256")
1084
+ .update(durableOutbound.idempotencyKey)
1085
+ .digest("hex")
1086
+ .slice(0, 32)}`
1087
+ : null;
1088
+ const boundaryState = { durableRecord: null, denialReason: "not_current" };
1089
+ const admitFinalTransport = () => {
1090
+ if (outboundClosed
1091
+ || (options.isOutboundCurrent && !options.isOutboundCurrent())
1092
+ || (options.isOutboundAdmittedNow && !options.isOutboundAdmittedNow())) {
1093
+ boundaryState.denialReason = "not_current";
1094
+ return false;
1095
+ }
1096
+ if (!durableOutbound || boundaryState.durableRecord)
1097
+ return true;
1098
+ const reservation = (0, outbound_state_1.reserveBlueBubblesOutbound)({
1099
+ agentRoot: durableOutbound.agentRoot,
1100
+ idempotencyKey: durableOutbound.idempotencyKey,
1101
+ chat,
1102
+ attachment: durableOutbound.attachment,
1103
+ text: trimmed,
1104
+ tempGuid: durableTempGuid,
1105
+ ...(replyToMessageGuid ? { replyToMessageGuid } : {}),
1106
+ });
1107
+ if (reservation.status !== "reserved") {
1108
+ boundaryState.denialReason = "durable_duplicate";
1109
+ return false;
1110
+ }
1111
+ boundaryState.durableRecord = reservation.record;
1112
+ return true;
1113
+ };
1114
+ const markDurableFailure = (error) => {
1115
+ const durableRecord = boundaryState.durableRecord;
1116
+ if (!durableRecord || !durableOutbound)
1117
+ return;
1118
+ try {
1119
+ (0, outbound_state_1.markBlueBubblesOutboundFailed)({
1120
+ agentRoot: durableOutbound.agentRoot,
1121
+ recordId: durableRecord.recordId,
1122
+ failedAt: new Date().toISOString(),
1123
+ reason: error instanceof Error ? error.message : String(error),
1124
+ });
1125
+ }
1126
+ catch (stateError) {
1127
+ (0, runtime_1.emitNervesEvent)({
1128
+ level: "error",
1129
+ component: "senses",
1130
+ event: "senses.bluebubbles_outbound_state_error",
1131
+ message: "failed to record bluebubbles outbound transport failure",
1132
+ meta: {
1133
+ recordId: durableRecord.recordId,
1134
+ reason: String(stateError),
1135
+ },
1136
+ });
1137
+ }
1138
+ };
1139
+ let response;
1140
+ while (true) {
1141
+ try {
1142
+ flushOptions.signal?.throwIfAborted();
1143
+ response = await client.sendText({
1144
+ chat,
1145
+ text: trimmed,
1146
+ replyToMessageGuid,
1147
+ ...(durableTempGuid ? { tempGuid: durableTempGuid } : {}),
1148
+ ...(flushOptions.signal ? { signal: flushOptions.signal } : {}),
1149
+ beforeTransportInvocation: admitFinalTransport,
1150
+ onTransportInvocation: options.onFinalTransportInvoked,
1151
+ });
1152
+ break;
1153
+ }
1154
+ catch (error) {
1155
+ if (error instanceof client_1.BlueBubblesSendError
1156
+ && error.transportInvoked === false
1157
+ && error.errorCode === "admission_denied") {
1158
+ if (boundaryState.denialReason === "durable_duplicate") {
1159
+ return { status: "not_invoked", reason: "durable_duplicate" };
1160
+ }
1161
+ // A newer observation may be only a temporary fence (duplicate,
1162
+ // audit-only, or otherwise non-promoting). Re-await admission and
1163
+ // retry the same durable reservation if this turn remains current.
1164
+ if (!await canRunOutbound()) {
1165
+ markDurableFailure(error);
1166
+ return { status: "not_invoked", reason: "not_current" };
1167
+ }
1168
+ continue;
1169
+ }
1170
+ markDurableFailure(error);
1171
+ throw error;
1172
+ }
1173
+ }
1174
+ const durableRecord = boundaryState.durableRecord;
1175
+ if (durableRecord && durableOutbound) {
1176
+ try {
1177
+ (0, outbound_state_1.markBlueBubblesOutboundAccepted)({
1178
+ agentRoot: durableOutbound.agentRoot,
1179
+ recordId: durableRecord.recordId,
1180
+ acceptedAt: new Date().toISOString(),
1181
+ ...(response.messageGuid ? { messageGuid: response.messageGuid } : {}),
1182
+ });
1183
+ }
1184
+ catch (stateError) {
1185
+ (0, runtime_1.emitNervesEvent)({
1186
+ level: "error",
1187
+ component: "senses",
1188
+ event: "senses.bluebubbles_outbound_state_error",
1189
+ message: "failed to record accepted bluebubbles outbound transport",
1190
+ meta: {
1191
+ recordId: durableRecord.recordId,
1192
+ reason: String(stateError),
1193
+ },
1194
+ });
1195
+ }
1196
+ }
1197
+ options.onFinalTransportAccepted?.();
1198
+ recordVisibleActivity();
1199
+ return { status: "accepted" };
1200
+ },
1201
+ async finish(options = {}) {
1202
+ const cleanupTimeoutMs = options.timeoutMs ?? BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS;
1203
+ const needsTypingStop = typingActive;
1204
+ if (needsTypingStop) {
1205
+ enqueueTypingStop();
1206
+ }
1207
+ const drained = await waitForBlueBubblesCallbackCleanup(queue, cleanupTimeoutMs, {
1208
+ operation: "finish",
1209
+ chatGuid: chat.chatGuid ?? null,
1210
+ });
1211
+ if (!drained) {
1212
+ // The queued start/read request may outlive this turn's bounded lane.
1213
+ // Invalidate its deferred stop, then issue one stop request now while A
1214
+ // still owns the canonical lock so B cannot start typing ahead of it.
1215
+ activeCleanupTokens.clear();
1216
+ abortActiveActivity("bluebubbles activity superseded by bounded cleanup");
1217
+ await waitForBlueBubblesCallbackCleanup(queue, cleanupTimeoutMs, {
1218
+ operation: "finish_after_activity_abort",
1219
+ chatGuid: chat.chatGuid ?? null,
1220
+ });
1221
+ }
1222
+ if (typingCouldBeActive) {
1223
+ const fallbackController = new AbortController();
1224
+ try {
1225
+ await withBlueBubblesActivityTimeout("typing_stop_fallback", cleanupTimeoutMs, fallbackController, (signal) => client.setTyping(chat, false, signal));
1226
+ typingCouldBeActive = false;
1227
+ }
1228
+ catch (error) {
1229
+ (0, runtime_1.emitNervesEvent)({
1230
+ level: "warn",
1231
+ component: "senses",
1232
+ event: "senses.bluebubbles_activity_error",
1233
+ message: "bluebubbles activity transport failed",
1234
+ meta: {
1235
+ operation: "typing_stop_fallback",
1236
+ reason: error instanceof Error ? error.message : String(error),
1237
+ },
1238
+ });
1239
+ }
1240
+ }
1241
+ activeCleanupTokens.clear();
1242
+ },
1243
+ cancelOutbound(reason) {
1244
+ outboundClosed = true;
1245
+ textBuffer = "";
1246
+ abortActiveActivity(`bluebubbles outbound ${reason}`);
1247
+ (0, runtime_1.emitNervesEvent)({
1248
+ level: "warn",
1249
+ component: "senses",
1250
+ event: "senses.bluebubbles_outbound_closed",
1251
+ message: "bluebubbles outbound callbacks closed for timed-out turn",
1252
+ meta: { chatGuid: chat.chatGuid ?? null, reason },
1253
+ });
1254
+ },
1255
+ };
1256
+ }
1257
+ async function readRequestBody(req) {
1258
+ let body = "";
1259
+ for await (const chunk of req) {
1260
+ body += chunk.toString();
1261
+ }
1262
+ return body;
1263
+ }
1264
+ function writeJson(res, statusCode, payload) {
1265
+ res.statusCode = statusCode;
1266
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1267
+ res.end(JSON.stringify(payload));
1268
+ }
1269
+ function writeSemanticCaptureFailure(res) {
1270
+ res.statusCode = 503;
1271
+ res.setHeader("Content-Type", "application/json");
1272
+ res.end('{"ok":false,"error":"semantic_capture_failed"}');
1273
+ }
1274
+ function isWebhookPasswordValid(url, expectedPassword) {
1275
+ const provided = url.searchParams.get("password");
1276
+ return !provided || provided === expectedPassword;
1277
+ }
1278
+ function normalizeHandleForSelfMatch(handle) {
1279
+ const trimmed = handle.trim().toLowerCase();
1280
+ if (!trimmed)
1281
+ return "";
1282
+ // Phone-shaped: strip everything but digits so +1 (415) 555-... matches 14155550000.
1283
+ if (/[+\d]/.test(trimmed) && !trimmed.includes("@")) {
1284
+ const digits = trimmed.replace(/\D/g, "");
1285
+ if (digits.length >= 7)
1286
+ return digits;
1287
+ }
1288
+ return trimmed;
1289
+ }
1290
+ function isAgentSelfHandle(senderExternalId, ownHandles) {
1291
+ if (!senderExternalId || !senderExternalId.trim())
1292
+ return false;
1293
+ const target = normalizeHandleForSelfMatch(senderExternalId);
1294
+ /* v8 ignore start -- target is non-empty by construction since senderExternalId was just verified non-whitespace */
1295
+ if (!target)
1296
+ return false;
1297
+ /* v8 ignore stop */
1298
+ for (const own of ownHandles) {
1299
+ if (normalizeHandleForSelfMatch(own) === target)
1300
+ return true;
1301
+ }
1302
+ return false;
1303
+ }
1304
+ /**
1305
+ * In-memory store of agent iMessage handles auto-discovered from group-chat
1306
+ * `event.fromMe === true` events. Bluebubbles can attribute the peer's handle
1307
+ * as `event.sender.externalId` on 1:1 outbound messages, so discovery is
1308
+ * intentionally limited to the group echo bug that motivated
1309
+ * `bluebubbles.ownHandles` originally.
1310
+ *
1311
+ * Per-process. A daemon restart re-learns from the next outbound. The
1312
+ * accompanying nerves event (`senses.bluebubbles_own_handle_discovered`)
1313
+ * tells the operator what to add to `bluebubbles.ownHandles` for cross-
1314
+ * restart durability.
1315
+ */
1316
+ const discoveredOwnHandles = new Set();
1317
+ function getDiscoveredOwnHandles() {
1318
+ return [...discoveredOwnHandles];
1319
+ }
1320
+ function clearDiscoveredOwnHandles() {
1321
+ discoveredOwnHandles.clear();
1322
+ }
1323
+ function recordDiscoveredOwnHandle(senderExternalId) {
1324
+ if (!senderExternalId || !senderExternalId.trim())
1325
+ return false;
1326
+ const trimmed = senderExternalId.trim();
1327
+ const normalized = normalizeHandleForSelfMatch(trimmed);
1328
+ /* v8 ignore next -- defensive: normalizeHandleForSelfMatch only returns falsy for empty input, already guarded above @preserve */
1329
+ if (!normalized)
1330
+ return false;
1331
+ for (const existing of discoveredOwnHandles) {
1332
+ if (normalizeHandleForSelfMatch(existing) === normalized)
1333
+ return false;
1334
+ }
1335
+ discoveredOwnHandles.add(trimmed);
1336
+ (0, runtime_1.emitNervesEvent)({
1337
+ level: "info",
1338
+ component: "senses",
1339
+ event: "senses.bluebubbles_own_handle_discovered",
1340
+ message: "captured a new agent-owned bluebubbles handle from an isFromMe outbound — add to bluebubbles.ownHandles for cross-restart durability",
1341
+ meta: { handle: trimmed, totalDiscovered: discoveredOwnHandles.size },
1342
+ });
1343
+ return true;
1344
+ }
1345
+ function isSelfFriendRecord(friend, agentName) {
1346
+ if (!friend || friend.kind !== "agent")
1347
+ return false;
1348
+ const normalizedAgent = agentName.trim().toLowerCase();
1349
+ const name = friend.name?.trim().toLowerCase();
1350
+ const bundleName = friend.agentMeta?.bundleName?.trim().toLowerCase();
1351
+ return name === normalizedAgent || bundleName === normalizedAgent;
1352
+ }
1353
+ async function shouldFilterAgentSelfHandle(event, resolvedDeps) {
1354
+ if (!event.chat.isGroup)
1355
+ return false;
1356
+ if (!isAgentSelfHandle(event.sender.externalId, resolvedDeps.getOwnHandles()))
1357
+ return false;
1358
+ const store = resolvedDeps.createFriendStore();
1359
+ const knownFriend = await store
1360
+ .findByExternalId("imessage-handle", event.sender.externalId)
1361
+ .catch(() => null);
1362
+ if (knownFriend && !isSelfFriendRecord(knownFriend, resolvedDeps.getAgentName())) {
1363
+ (0, runtime_1.emitNervesEvent)({
1364
+ level: "warn",
1365
+ component: "senses",
1366
+ event: "senses.bluebubbles_self_handle_bypassed_known_friend",
1367
+ message: "did not filter bluebubbles sender even though it matched ownHandles because it resolves to a known non-self friend",
1368
+ meta: {
1369
+ messageGuid: event.messageGuid,
1370
+ kind: event.kind,
1371
+ senderExternalId: event.sender.externalId,
1372
+ friendId: knownFriend.id,
1373
+ },
1374
+ });
1375
+ return false;
1376
+ }
1377
+ return true;
1378
+ }
1379
+ function createBlueBubblesTurnStage(sessionKey) {
1380
+ return {
1381
+ failoverState: structuredClone(bbFailoverStates.get(sessionKey) ?? { pending: null }),
1382
+ };
1383
+ }
1384
+ function persistAcceptedBlueBubblesProjection(input) {
1385
+ const { stage, deps, event } = input;
1386
+ if (stage.postTurn) {
1387
+ try {
1388
+ deps.postTurnPersist(stage.postTurn.sessionPath, stage.postTurn.prepared, stage.postTurn.usage, stage.postTurn.state);
1389
+ }
1390
+ catch (error) {
1391
+ (0, runtime_1.emitNervesEvent)({
1392
+ level: "error",
1393
+ component: "senses",
1394
+ event: "senses.bluebubbles_accepted_projection_failed",
1395
+ message: "accepted bluebubbles reply could not be projected into canonical session history",
1396
+ meta: {
1397
+ messageGuid: event.messageGuid,
1398
+ sessionKey: event.chat.sessionKey,
1399
+ reason: error instanceof Error ? error.message : String(error),
1400
+ },
1401
+ });
1402
+ }
1403
+ }
1404
+ }
1405
+ function promoteAcceptedBlueBubblesAncillaryState(input) {
1406
+ const { stage, deps, agentName, event, source } = input;
1407
+ try {
1408
+ deps.promoteFailoverState(event.chat.sessionKey, stage.failoverState);
1409
+ }
1410
+ catch (error) {
1411
+ (0, runtime_1.emitNervesEvent)({
1412
+ level: "warn",
1413
+ component: "senses",
1414
+ event: "senses.bluebubbles_failover_state_promotion_failed",
1415
+ message: "accepted bluebubbles reply could not promote staged failover state",
1416
+ meta: {
1417
+ messageGuid: event.messageGuid,
1418
+ sessionKey: event.chat.sessionKey,
1419
+ reason: error instanceof Error ? error.message : String(error),
1420
+ },
1421
+ });
1422
+ }
1423
+ if (stage.tokens) {
1424
+ const emitTokenPromotionFailure = (error) => {
1425
+ (0, runtime_1.emitNervesEvent)({
1426
+ level: "warn",
1427
+ component: "senses",
1428
+ event: "senses.bluebubbles_token_promotion_failed",
1429
+ message: "accepted bluebubbles reply could not promote staged token usage",
1430
+ meta: {
1431
+ messageGuid: event.messageGuid,
1432
+ sessionKey: event.chat.sessionKey,
1433
+ reason: error instanceof Error ? error.message : String(error),
1434
+ },
1435
+ });
1436
+ };
1437
+ try {
1438
+ void deps.accumulateFriendTokens(stage.tokens.store, stage.tokens.friendId, stage.tokens.usage).catch(emitTokenPromotionFailure);
1439
+ }
1440
+ catch (error) {
1441
+ emitTokenPromotionFailure(error);
1442
+ }
1443
+ }
1444
+ try {
1445
+ deps.recordProcessed(agentName, event, source, input.processedOutcome);
1446
+ }
1447
+ catch (error) {
1448
+ (0, runtime_1.emitNervesEvent)({
1449
+ level: "warn",
1450
+ component: "senses",
1451
+ event: "senses.bluebubbles_processed_promotion_failed",
1452
+ message: "accepted bluebubbles reply could not promote processed sidecar state",
1453
+ meta: {
1454
+ messageGuid: event.messageGuid,
1455
+ sessionKey: event.chat.sessionKey,
1456
+ reason: error instanceof Error ? error.message : String(error),
1457
+ },
1458
+ });
1459
+ }
1460
+ }
1461
+ function hasObservedBlueBubblesDirection(event) {
1462
+ return typeof event.fromMe === "boolean";
1463
+ }
1464
+ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, options) {
1465
+ options.lifecycleSignal?.throwIfAborted();
1466
+ const client = resolvedDeps.createClient();
1467
+ const agentName = resolvedDeps.getAgentName();
1468
+ (0, runtime_cwd_1.recoverRuntimeCwd)();
1469
+ let activeTurnId = null;
1470
+ if (shouldUseBlueBubblesRecoveryBudget(source, options)) {
1471
+ const decision = (0, autonomy_budget_1.reserveAutonomyBudget)((0, identity_1.getAgentRoot)(agentName), {
1472
+ agent: agentName,
1473
+ triggerType: "recovery",
1474
+ sourceKind: "sense",
1475
+ senseOrHabit: "bluebubbles",
1476
+ target: blueBubblesAutonomyTarget(event),
1477
+ idempotencyKey: `bluebubbles-recovery:${source}:${event.messageGuid}`,
1478
+ now: timestampIsoForAutonomy(event),
1479
+ storm: blueBubblesRecoveryStormInput(agentName, event, source),
1480
+ });
1481
+ if (!decision.allowed) {
1482
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "autonomy_budget_blocked" };
1483
+ }
1484
+ }
1485
+ try {
1486
+ // Reserve the canonical chat lane before any asynchronous adapter setup.
1487
+ // Every admitted inbound is projected in observation order even when a
1488
+ // newer turn supersedes its inference before provider work begins.
1489
+ return await (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", `${agentName}:${event.chat.sessionKey}`, async () => {
1490
+ options.lifecycleSignal?.throwIfAborted();
1491
+ // Fallback self-detection: BlueBubbles sometimes broadcasts a group-chat
1492
+ // outbound message back through the webhook with `isFromMe` missing/false.
1493
+ // Keep this group-only: stale ownHandles entries must not hide real DMs.
1494
+ if (await shouldFilterAgentSelfHandle(event, resolvedDeps)) {
1495
+ (0, runtime_1.emitNervesEvent)({
1496
+ level: "warn",
1497
+ component: "senses",
1498
+ event: "senses.bluebubbles_self_handle_filtered",
1499
+ message: "filtered bluebubbles event whose sender matched an agent-owned handle (isFromMe was missing/false)",
1500
+ meta: {
1501
+ messageGuid: event.messageGuid,
1502
+ kind: event.kind,
1503
+ senderExternalId: event.sender.externalId,
1504
+ },
1505
+ });
1506
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
1507
+ }
1508
+ options.lifecycleSignal?.throwIfAborted();
1509
+ // ── Adapter setup: friend, session, content, callbacks ────────
1510
+ const store = resolvedDeps.createFriendStore();
1511
+ const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
1512
+ const baseContext = await resolver.resolve();
1513
+ options.lifecycleSignal?.throwIfAborted();
1514
+ const context = { ...baseContext, isGroupChat: event.chat.isGroup };
1515
+ const replyTarget = createReplyTargetController(event);
1516
+ const friendId = context.friend.id;
1517
+ const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
1518
+ return await resolvedDeps.withSessionTurnLease(sessPath, async (sessionTurnLease) => {
1519
+ try {
1520
+ (0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
1521
+ }
1522
+ catch (error) {
1523
+ (0, runtime_1.emitNervesEvent)({
1524
+ level: "warn",
1525
+ component: "senses",
1526
+ event: "senses.bluebubbles_thread_lane_cleanup_error",
1527
+ message: "failed to inspect obsolete bluebubbles thread-lane sessions",
1528
+ meta: {
1529
+ sessionPath: sessPath,
1530
+ reason: error instanceof Error ? error.message : String(error),
1531
+ },
1532
+ });
1533
+ }
1534
+ // Pre-load session inside the canonical turn lock so same-chat
1535
+ // deliveries cannot race on stale trunk state.
1536
+ const existing = resolvedDeps.loadSession(sessPath);
1537
+ const sessionMessages = existing?.messages && existing.messages.length > 0
1538
+ ? structuredClone(existing.messages)
1539
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
1540
+ // Record EARLY for audit and crash recovery. This is capture truth, not
1541
+ // a claim that the agent turn completed successfully.
1542
+ const inboundSource = (options.semanticRecovery || source !== "webhook")
1543
+ && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
1544
+ ? "recovery-bootstrap"
1545
+ : source;
1546
+ (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
1547
+ if (inboundSource === "recovery-bootstrap") {
1548
+ (0, runtime_1.emitNervesEvent)({
1549
+ component: "senses",
1550
+ event: "senses.bluebubbles_recovery_skip",
1551
+ message: "skipped bluebubbles recovery because the session already contains the message text",
1552
+ meta: {
1553
+ messageGuid: event.messageGuid,
1554
+ sessionKey: event.chat.sessionKey,
1555
+ source,
1556
+ },
1557
+ });
1558
+ (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
1559
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
1560
+ }
1561
+ // The inbound event is canonical truth as soon as its canonical lane is
1562
+ // reserved. Persist it before any supersedable orientation/provider work.
1563
+ const priorMessages = sessionMessages;
1564
+ const userMessage = {
1565
+ role: "user",
1566
+ content: buildInboundContent(event),
1567
+ };
1568
+ resolvedDeps.saveSession(sessPath, [...structuredClone(sessionMessages), structuredClone(userMessage)], existing?.lastUsage, existing?.state);
1569
+ if (!await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1570
+ || !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1571
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1572
+ }
1573
+ options.lifecycleSignal?.throwIfAborted();
1574
+ const mcpOwner = { agentName, agentRoot: (0, identity_1.getAgentRoot)(agentName) };
1575
+ const liveTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
1576
+ activeTurnId = liveTurnId;
1577
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)(mcpOwner) ?? undefined;
1578
+ if (event.chat.isGroup) {
1579
+ await (0, friends_1.upsertGroupContextParticipants)({
1580
+ store,
1581
+ participants: event.chat.participantHandles.map((externalId) => ({
1582
+ provider: "imessage-handle",
1583
+ externalId,
1584
+ })),
1585
+ groupExternalId: resolveGroupExternalId(event),
1586
+ });
1587
+ }
1588
+ // Fetch the text of the message being replied to (if this is a threaded reply)
1589
+ const threadGuid = event.threadOriginatorGuid?.trim();
1590
+ let repliedToText = null;
1591
+ if (threadGuid) {
1592
+ repliedToText = await client.getMessageText(threadGuid).catch(() => null);
1593
+ (0, runtime_1.emitNervesEvent)({
1594
+ component: "senses",
1595
+ event: "senses.bluebubbles_reply_context",
1596
+ message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
1597
+ meta: { threadGuid, hasText: !!repliedToText },
1598
+ });
1599
+ }
1600
+ // Shutdown can land during MCP/group/reply-context awaits above. Recheck
1601
+ // before the remaining synchronous setup installs its lifecycle listener.
1602
+ options.lifecycleSignal?.throwIfAborted();
1603
+ const orientationFrame = (0, orientation_frame_1.buildOrientationFrame)({
1604
+ channel: "bluebubbles",
1605
+ messages: [...priorMessages, userMessage],
1606
+ currentUserMessages: [userMessage],
1607
+ structuredOutputs: existing?.structuredOutputs ?? [],
1608
+ source: buildBlueBubblesOrientationSource(event, priorMessages, options.orientationEvidence, options.orientationConversationKind, repliedToText),
1609
+ });
1610
+ if (!(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1611
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1612
+ }
1613
+ const turnStage = createBlueBubblesTurnStage(event.chat.sessionKey);
1614
+ let finalTransportInvoked = false;
1615
+ let finalTransportAccepted = false;
1616
+ const outboundRuntimeConfig = (0, config_1.getBlueBubblesConfig)();
1617
+ const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () => (0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, liveTurnId), {
1618
+ admitOutbound: async () => (!options.lifecycleSignal?.aborted
1619
+ && await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1620
+ && !options.lifecycleSignal?.aborted),
1621
+ isOutboundCurrent: () => (!options.lifecycleSignal?.aborted
1622
+ && (0, latest_turn_1.isCurrent)(options.latestTurnCapability)),
1623
+ isOutboundAdmittedNow: () => (!options.lifecycleSignal?.aborted
1624
+ && (0, latest_turn_1.isDeliveryAdmittedNow)(options.latestTurnCapability)),
1625
+ onFinalTransportInvoked: () => {
1626
+ finalTransportInvoked = true;
1627
+ },
1628
+ onFinalTransportAccepted: () => {
1629
+ finalTransportAccepted = true;
1630
+ },
1631
+ durableOutbound: {
1632
+ agentRoot: (0, identity_1.getAgentRoot)(),
1633
+ idempotencyKey: blueBubblesInboundReplyIdempotencyKey(options.currentIngressEvidence.captureKeyHash),
1634
+ attachment: {
1635
+ serverUrl: outboundRuntimeConfig.serverUrl,
1636
+ accountId: outboundRuntimeConfig.accountId,
1637
+ },
1638
+ },
1639
+ });
1640
+ const controller = new AbortController();
1641
+ const lifecycleSignal = options.lifecycleSignal;
1642
+ let rejectLifecycle;
1643
+ const lifecyclePromise = new Promise((_resolve, reject) => {
1644
+ rejectLifecycle = reject;
1645
+ });
1646
+ let resolveSupersession;
1647
+ const supersessionPromise = new Promise((resolve) => {
1648
+ resolveSupersession = resolve;
1649
+ });
1650
+ const cancelForSupersession = () => {
1651
+ callbacks.cancelOutbound("superseded");
1652
+ if (!finalTransportInvoked) {
1653
+ controller.abort(options.latestTurnCapability.signal.reason);
1654
+ resolveSupersession({
1655
+ handled: true,
1656
+ notifiedAgent: false,
1657
+ kind: event.kind,
1658
+ reason: "superseded",
1659
+ });
1660
+ }
1661
+ };
1662
+ options.latestTurnCapability.signal.addEventListener("abort", cancelForSupersession, { once: true });
1663
+ const cancelForShutdown = () => {
1664
+ callbacks.cancelOutbound("shutdown");
1665
+ if (!finalTransportInvoked) {
1666
+ const reason = lifecycleSignal.reason;
1667
+ controller.abort(reason);
1668
+ rejectLifecycle(reason);
1669
+ }
1670
+ };
1671
+ lifecycleSignal?.addEventListener("abort", cancelForShutdown, { once: true });
1672
+ let timeoutTimer;
1673
+ let timeoutPromise;
1674
+ let resolveTimeout;
1675
+ let recoveryTimedOut = false;
1676
+ let activeRunTurn = null;
1677
+ // BB-specific tool context wrappers
1678
+ const summarize = (0, core_1.createSummarize)("human");
1679
+ const bbCapabilities = (0, friends_1.getChannelCapabilities)("bluebubbles");
1680
+ const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
1681
+ // Buffer terminal errors so failover can suppress them.
1682
+ // If failover produces a message, the buffered error is skipped.
1683
+ // If failover doesn't fire, the buffered error is replayed.
1684
+ let bufferedTerminalError = null;
1685
+ /* v8 ignore start -- failover-aware error buffering @preserve */
1686
+ const failoverAwareCallbacks = {
1687
+ ...callbacks,
1688
+ onError(error, severity) {
1689
+ if (severity === "terminal") {
1690
+ bufferedTerminalError = error;
1691
+ return;
1692
+ }
1693
+ callbacks.onError(error, severity);
1694
+ },
1695
+ };
1696
+ /* v8 ignore stop */
1697
+ try {
1698
+ // ── Compute trust gate context for group/acquaintance rules ─────
1699
+ // These store reads can block on filesystem I/O. Race them against the
1700
+ // runtime lifecycle before constructing the shared pipeline so a closed
1701
+ // worker cannot begin new durable or outward work after the read settles.
1702
+ const trustContext = await Promise.race([
1703
+ Promise.all([
1704
+ checkGroupHasFamilyMember(store, event),
1705
+ event.chat.isGroup
1706
+ ? Promise.resolve(false)
1707
+ : checkHasExistingGroupWithFamily(store, context.friend),
1708
+ ]),
1709
+ lifecyclePromise,
1710
+ supersessionPromise.then(() => null),
1711
+ ]);
1712
+ if (trustContext === null) {
1713
+ return {
1714
+ handled: true,
1715
+ notifiedAgent: false,
1716
+ kind: event.kind,
1717
+ reason: "superseded",
1718
+ };
1719
+ }
1720
+ const [groupHasFamilyMember, hasExistingGroupWithFamily] = trustContext;
1721
+ lifecycleSignal?.throwIfAborted();
1722
+ // ── Call shared pipeline ────────────────────────────────────────
1723
+ const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
1724
+ timeoutPromise = new Promise((resolve) => {
1725
+ resolveTimeout = resolve;
1726
+ });
1727
+ timeoutTimer = setTimeout(() => {
1728
+ const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
1729
+ recoveryTimedOut = true;
1730
+ if (!finalTransportInvoked) {
1731
+ resolveTimeout?.({
1732
+ handled: true,
1733
+ notifiedAgent: false,
1734
+ kind: event.kind,
1735
+ reason: "turn_timeout",
1736
+ });
1737
+ }
1738
+ callbacks.cancelOutbound("turn_timeout");
1739
+ (0, latest_turn_1.cancel)(options.latestTurnCapability, "turn_timeout");
1740
+ if (!finalTransportInvoked)
1741
+ controller.abort(reason);
1742
+ (0, runtime_1.emitNervesEvent)({
1743
+ level: "warn",
1744
+ component: "senses",
1745
+ event: "senses.bluebubbles_turn_timeout",
1746
+ message: "bluebubbles turn timed out",
1747
+ meta: {
1748
+ messageGuid: event.messageGuid,
1749
+ sessionKey: event.chat.sessionKey,
1750
+ source,
1751
+ timeoutMs,
1752
+ },
1753
+ });
1754
+ (0, runtime_1.emitNervesEvent)({
1755
+ level: "warn",
1756
+ component: "senses",
1757
+ event: "senses.bluebubbles_timeout_notice_suppressed",
1758
+ message: "bluebubbles timeout notice suppressed from iMessage",
1759
+ meta: {
1760
+ messageGuid: event.messageGuid,
1761
+ sessionKey: event.chat.sessionKey,
1762
+ source,
1763
+ timeoutMs,
1764
+ },
1765
+ });
1766
+ }, timeoutMs);
1767
+ /* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
1768
+ if (typeof timeoutTimer.unref === "function") {
1769
+ timeoutTimer.unref();
1770
+ }
1771
+ let preparedBlueBubblesContext = {
1772
+ messages: [],
1773
+ contextPacketIds: [],
1774
+ };
1775
+ const turnPromise = (0, pipeline_1.handleInboundTurn)({
1776
+ channel: "bluebubbles",
1777
+ sessionKey: event.chat.sessionKey,
1778
+ capabilities: bbCapabilities,
1779
+ messages: [userMessage],
1780
+ continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
1781
+ friendResolver: { resolve: () => Promise.resolve(context) },
1782
+ sessionLoader: {
1783
+ loadOrCreate: () => Promise.resolve({
1784
+ messages: sessionMessages,
1785
+ sessionPath: sessPath,
1786
+ state: existing?.state,
1787
+ events: existing?.events,
1788
+ structuredOutputs: existing?.structuredOutputs,
1789
+ }),
1790
+ },
1791
+ pendingDir,
1792
+ friendStore: store,
1793
+ provider: "imessage-handle",
1794
+ externalId: event.sender.externalId,
1795
+ isGroupChat: event.chat.isGroup,
1796
+ groupHasFamilyMember,
1797
+ hasExistingGroupWithFamily,
1798
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
1799
+ drainPending: pending_1.drainPending,
1800
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
1801
+ prepareRunAgentOptions: async ({ messages = [], currentUserMessages = [], runAgentOptions }) => {
1802
+ preparedBlueBubblesContext = await buildBlueBubblesContextMessages({
1803
+ agentName,
1804
+ agentRoot: (0, identity_1.getAgentRoot)(),
1805
+ client,
1806
+ event,
1807
+ knownMessages: messages,
1808
+ });
1809
+ const currentUserMessage = currentUserMessages.findLast((message) => message.role === "user")
1810
+ ?? messages.findLast((message) => message.role === "user");
1811
+ if (!currentUserMessage)
1812
+ return undefined;
1813
+ Object.freeze(currentUserMessage);
1814
+ const { verifiedPredecessorMessage } = preparedBlueBubblesContext;
1815
+ return {
1816
+ ...(preparedBlueBubblesContext.contextPacketIds.length > 0 ? {
1817
+ contextPacketIds: [
1818
+ ...(runAgentOptions.contextPacketIds ?? []),
1819
+ ...preparedBlueBubblesContext.contextPacketIds,
1820
+ ],
1821
+ } : {}),
1822
+ ...(preparedBlueBubblesContext.messages.length > 0 ? {
1823
+ promptOnlyEvidenceMessages: preparedBlueBubblesContext.messages,
1824
+ } : {}),
1825
+ requiredPromptEvidence: {
1826
+ currentUserMessage,
1827
+ ...(verifiedPredecessorMessage ? { verifiedPredecessorMessage } : {}),
1828
+ },
1829
+ };
1830
+ },
1831
+ runAgent: async (msgs, cb, channel, sig, opts) => {
1832
+ const { codingFeedback: _omittedCodingFeedback, ...safeToolContext } = opts?.toolContext ?? {};
1833
+ const requiredCurrent = opts?.requiredPromptEvidence?.currentUserMessage;
1834
+ const providerMessages = insertEphemeralContextMessages(msgs.map((message) => message === requiredCurrent ? message : structuredClone(message)), [
1835
+ ...preparedBlueBubblesContext.messages,
1836
+ ...(preparedBlueBubblesContext.verifiedPredecessorMessage
1837
+ ? [preparedBlueBubblesContext.verifiedPredecessorMessage]
1838
+ : []),
1839
+ ], requiredCurrent);
1840
+ let generatedMessages = [];
1841
+ const agentResult = await resolvedDeps.runAgent(providerMessages, cb, channel, sig, {
1842
+ ...opts,
1843
+ captureGeneratedMessages: (generated) => {
1844
+ opts?.captureGeneratedMessages?.(generated);
1845
+ generatedMessages = structuredClone(generated);
1846
+ },
1847
+ toolContext: {
1848
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1849
+ signin: async () => undefined,
1850
+ ...safeToolContext,
1851
+ ...mcpOwner,
1852
+ summarize,
1853
+ bluebubblesReplyTarget: {
1854
+ setSelection: (selection) => replyTarget.setSelection(selection),
1855
+ },
1856
+ },
1857
+ });
1858
+ if (generatedMessages.length > 0) {
1859
+ msgs.push(...generatedMessages);
1860
+ }
1861
+ return agentResult;
1862
+ },
1863
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
1864
+ const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
1865
+ turnStage.postTurn = {
1866
+ sessionPath: sessionPathArg,
1867
+ prepared,
1868
+ usage,
1869
+ state,
1870
+ };
1871
+ },
1872
+ accumulateFriendTokens: async (tokenStore, tokenFriendId, usage) => {
1873
+ turnStage.tokens = {
1874
+ store: tokenStore,
1875
+ friendId: tokenFriendId,
1876
+ usage,
1877
+ };
1878
+ },
1879
+ signal: controller.signal,
1880
+ runAgentOptions: {
1881
+ mcpManager,
1882
+ orientationFrame,
1883
+ toolContext: {
1884
+ signin: async () => undefined,
1885
+ ...mcpOwner,
1886
+ currentIngressEvidence: options.currentIngressEvidence,
1887
+ },
1888
+ },
1889
+ callbacks: failoverAwareCallbacks,
1890
+ sessionTurnLease,
1891
+ failoverState: turnStage.failoverState,
1892
+ });
1893
+ /* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
1894
+ void turnPromise
1895
+ .catch((error) => {
1896
+ if (!recoveryTimedOut)
1897
+ return;
1898
+ (0, runtime_1.emitNervesEvent)({
1899
+ level: "warn",
1900
+ component: "senses",
1901
+ event: "senses.bluebubbles_recovery_error",
1902
+ message: "bluebubbles recovery turn rejected after timeout",
1903
+ meta: {
1904
+ messageGuid: event.messageGuid,
1905
+ sessionKey: event.chat.sessionKey,
1906
+ source,
1907
+ reason: error instanceof Error ? error.message : String(error),
1908
+ },
1909
+ });
1910
+ });
1911
+ /* v8 ignore stop */
1912
+ const runTurn = (async () => {
1913
+ const result = await turnPromise;
1914
+ if (recoveryTimedOut && !finalTransportInvoked) {
1915
+ (0, runtime_1.emitNervesEvent)({
1916
+ level: "warn",
1917
+ component: "senses",
1918
+ event: "senses.bluebubbles_timed_out_turn_suppressed",
1919
+ message: "suppressed late bluebubbles turn result after timeout",
1920
+ meta: {
1921
+ messageGuid: event.messageGuid,
1922
+ sessionKey: event.chat.sessionKey,
1923
+ source,
1924
+ reason: "turn completed after its final-transport boundary closed",
1925
+ },
1926
+ });
1927
+ return {
1928
+ handled: true,
1929
+ notifiedAgent: false,
1930
+ kind: event.kind,
1931
+ reason: "turn_timeout",
1932
+ };
1933
+ }
1934
+ /* v8 ignore start -- failover display + error replay @preserve */
1935
+ if (result.failoverMessage) {
1936
+ callbacks.onClearText?.();
1937
+ callbacks.onTextChunk(result.failoverMessage);
1938
+ bufferedTerminalError = null;
1939
+ }
1940
+ else if (bufferedTerminalError) {
1941
+ callbacks.onError(bufferedTerminalError, "terminal");
1942
+ bufferedTerminalError = null;
1943
+ }
1944
+ /* v8 ignore stop */
1945
+ // ── Handle gate result ────────────────────────────────────────
1946
+ let processedOutcome = "turn-complete";
1947
+ if (!result.gateResult.allowed) {
1948
+ callbacks.onClearText?.();
1949
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
1950
+ callbacks.onTextChunk(result.gateResult.autoReply);
1951
+ }
1952
+ processedOutcome = "trust-gated";
1953
+ }
1954
+ let delivery;
1955
+ try {
1956
+ delivery = await callbacks.flush({ signal: controller.signal });
1957
+ }
1958
+ catch (error) {
1959
+ (0, runtime_1.emitNervesEvent)({
1960
+ level: "warn",
1961
+ component: "senses",
1962
+ event: "senses.bluebubbles_final_transport_failed",
1963
+ message: "bluebubbles final transport failed after invocation",
1964
+ meta: {
1965
+ messageGuid: event.messageGuid,
1966
+ sessionKey: event.chat.sessionKey,
1967
+ reason: error instanceof Error ? error.message : String(error),
1968
+ },
1969
+ });
1970
+ return {
1971
+ handled: true,
1972
+ notifiedAgent: false,
1973
+ kind: event.kind,
1974
+ reason: "delivery_failed",
1975
+ };
1976
+ }
1977
+ if (delivery.status !== "accepted" || !finalTransportAccepted) {
1978
+ return {
1979
+ handled: true,
1980
+ notifiedAgent: false,
1981
+ kind: event.kind,
1982
+ reason: !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)
1983
+ ? "superseded"
1984
+ : "no_visible_reply",
1985
+ };
1986
+ }
1987
+ // Acceptance is the visibility boundary. Publish the terminal dedupe
1988
+ // receipt first, then project the private generated tail while this
1989
+ // chat's canonical lock remains held. Even if receipt publication
1990
+ // fails, make the best possible canonical projection before surfacing
1991
+ // the retained-claim failure. Ancillary accounting follows both.
1992
+ let receiptFailed = false;
1993
+ let receiptFailure;
1994
+ try {
1995
+ options.publishAcceptedReceipt();
1996
+ }
1997
+ catch (error) {
1998
+ receiptFailed = true;
1999
+ receiptFailure = error;
2000
+ }
2001
+ persistAcceptedBlueBubblesProjection({
2002
+ stage: turnStage,
2003
+ deps: resolvedDeps,
2004
+ event,
2005
+ });
2006
+ if (receiptFailed)
2007
+ throw receiptFailure;
2008
+ promoteAcceptedBlueBubblesAncillaryState({
2009
+ stage: turnStage,
2010
+ deps: resolvedDeps,
2011
+ agentName,
2012
+ event,
2013
+ source,
2014
+ processedOutcome,
2015
+ });
2016
+ (0, runtime_1.emitNervesEvent)({
2017
+ component: "senses",
2018
+ event: "senses.bluebubbles_turn_end",
2019
+ message: "bluebubbles event handled",
2020
+ meta: {
2021
+ messageGuid: event.messageGuid,
2022
+ kind: event.kind,
2023
+ sessionKey: event.chat.sessionKey,
2024
+ },
2025
+ });
2026
+ return {
2027
+ handled: true,
2028
+ notifiedAgent: true,
2029
+ kind: event.kind,
2030
+ };
2031
+ })();
2032
+ activeRunTurn = runTurn;
2033
+ /* v8 ignore start -- detached post-timeout suppression telemetry @preserve */
2034
+ void runTurn.catch((error) => {
2035
+ if (!recoveryTimedOut)
2036
+ return;
2037
+ (0, runtime_1.emitNervesEvent)({
2038
+ level: "warn",
2039
+ component: "senses",
2040
+ event: "senses.bluebubbles_timed_out_turn_suppressed",
2041
+ message: "suppressed late bluebubbles turn result after timeout",
2042
+ meta: {
2043
+ messageGuid: event.messageGuid,
2044
+ sessionKey: event.chat.sessionKey,
2045
+ source,
2046
+ reason: error instanceof Error ? error.message : String(error),
2047
+ },
2048
+ });
2049
+ });
2050
+ /* v8 ignore stop */
2051
+ return await Promise.race([runTurn, timeoutPromise, supersessionPromise, lifecyclePromise]);
2052
+ }
2053
+ finally {
2054
+ // A timed-out/superseded provider may ignore abort briefly. Never release
2055
+ // the cross-process session lease until that work has actually quiesced.
2056
+ if (activeRunTurn)
2057
+ await activeRunTurn.catch(() => undefined);
2058
+ // If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
2059
+ // replay it now so the user still sees the error.
2060
+ /* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
2061
+ if (bufferedTerminalError) {
2062
+ callbacks.onError(bufferedTerminalError, "terminal");
2063
+ bufferedTerminalError = null;
2064
+ }
2065
+ /* v8 ignore stop */
2066
+ clearTimeout(timeoutTimer);
2067
+ options.latestTurnCapability.signal.removeEventListener("abort", cancelForSupersession);
2068
+ lifecycleSignal?.removeEventListener("abort", cancelForShutdown);
2069
+ (0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, liveTurnId);
2070
+ activeTurnId = null;
2071
+ await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
2072
+ }
2073
+ });
2074
+ });
2075
+ }
2076
+ finally {
2077
+ if (activeTurnId)
2078
+ (0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, activeTurnId);
2079
+ }
2080
+ }
2081
+ function recordBlueBubblesAuditSidecar(agentName, event, source, resolvedDeps) {
2082
+ if (event.kind === "message") {
2083
+ if (event.fromMe !== true)
2084
+ (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, source);
2085
+ return;
2086
+ }
2087
+ try {
2088
+ resolvedDeps.recordMutation(agentName, event);
2089
+ }
2090
+ catch (error) {
2091
+ (0, runtime_1.emitNervesEvent)({
2092
+ level: "error",
2093
+ component: "senses",
2094
+ event: "senses.bluebubbles_mutation_log_error",
2095
+ message: "failed recording bluebubbles mutation sidecar",
2096
+ meta: {
2097
+ messageGuid: event.messageGuid,
2098
+ mutationType: event.mutationType,
2099
+ reason: error instanceof Error ? error.message : String(error),
2100
+ },
2101
+ });
2102
+ }
2103
+ }
2104
+ async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source, observationReservation) {
2105
+ const agentName = resolvedDeps.getAgentName();
2106
+ const capturedAt = new Date().toISOString();
2107
+ if (!hasObservedBlueBubblesDirection(normalized)) {
2108
+ (0, runtime_1.emitNervesEvent)({
2109
+ level: "warn",
2110
+ component: "senses",
2111
+ event: "senses.bluebubbles_direction_unobserved",
2112
+ message: "kept bluebubbles event audit-only because message direction was unobserved",
2113
+ meta: {
2114
+ messageGuid: normalized.messageGuid,
2115
+ kind: normalized.kind,
2116
+ source,
2117
+ },
2118
+ });
2119
+ recordBlueBubblesAuditSidecar(agentName, normalized, source, resolvedDeps);
2120
+ return { status: "audit_only", normalized };
2121
+ }
2122
+ const observed = normalized;
2123
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2124
+ let coordinateGeneration;
2125
+ if (observed.kind === "mutation"
2126
+ && observed.mutationType === "reaction"
2127
+ && observed.reaction
2128
+ && !observed.revision
2129
+ && !Number.isFinite(observed.effectiveTimestamp)
2130
+ && observed.sender.observed === true) {
2131
+ const identity = (0, semantic_receipts_1.buildBlueBubblesSemanticIdentity)({
2132
+ providerNamespace: cutover.providerNamespace,
2133
+ kind: "reaction",
2134
+ eventGuid: observed.messageGuid,
2135
+ targetGuid: observed.targetMessageGuid,
2136
+ actorExternalId: observed.sender.externalId,
2137
+ canonicalValue: observed.reaction.canonicalValue,
2138
+ canonicalAction: observed.reaction.action,
2139
+ coordinateGeneration: 0,
2140
+ });
2141
+ if (identity?.coordinateKey && identity.coordinateHash) {
2142
+ const coordinate = await (0, semantic_receipts_1.allocateBlueBubblesReactionCoordinate)(agentName, {
2143
+ coordinateKey: identity.coordinateKey,
2144
+ coordinateHash: identity.coordinateHash,
2145
+ canonicalAction: observed.reaction.action,
2146
+ });
2147
+ coordinateGeneration = coordinate.generation;
2148
+ }
2149
+ }
2150
+ const capture = (0, semantic_receipts_1.buildBlueBubblesSemanticCapture)({
2151
+ cutover,
2152
+ capturedAt,
2153
+ observationEpoch: observationReservation.observationEpoch,
2154
+ observationOrdinal: observationReservation.ordinal,
2155
+ event: observed,
2156
+ targetAuthorship: null,
2157
+ coordinateGeneration,
2158
+ });
2159
+ if (!capture) {
2160
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(observed, cutover);
2161
+ recordBlueBubblesAuditSidecar(agentName, observed, source, resolvedDeps);
2162
+ return { status: "audit_only", normalized: observed };
2163
+ }
2164
+ const writeResult = (0, semantic_receipts_1.writeBlueBubblesSemanticCapture)(agentName, capture);
2165
+ if (writeResult === "semantic_identity_collision") {
2166
+ throw new Error("semantic_capture_failed");
2167
+ }
2168
+ recordBlueBubblesAuditSidecar(agentName, observed, source, resolvedDeps);
2169
+ return {
2170
+ status: "captured",
2171
+ capture,
2172
+ normalized: observed,
2173
+ writeResult,
2174
+ };
2175
+ }
2176
+ function semanticCaptureChat(capture) {
2177
+ const event = capture.event;
2178
+ const sessionKey = event.sessionKey?.trim();
2179
+ const chatGuid = event.chatGuid?.trim() || undefined;
2180
+ const chatIdentifier = event.chatIdentifier?.trim() || undefined;
2181
+ if (!sessionKey)
2182
+ return null;
2183
+ const fallbackSendTarget = sessionKey.startsWith("chat:")
2184
+ ? { kind: "chat_guid", value: sessionKey.slice("chat:".length).trim() }
2185
+ : sessionKey.startsWith("chat_identifier:")
2186
+ ? { kind: "chat_identifier", value: sessionKey.slice("chat_identifier:".length).trim() }
2187
+ : null;
2188
+ const sendTarget = chatGuid
2189
+ ? { kind: "chat_guid", value: chatGuid }
2190
+ : chatIdentifier
2191
+ ? { kind: "chat_identifier", value: chatIdentifier }
2192
+ : fallbackSendTarget;
2193
+ if (!sendTarget?.value)
2194
+ return null;
2195
+ return {
2196
+ chatGuid,
2197
+ chatIdentifier,
2198
+ isGroup: Boolean(chatGuid?.includes(";+;") || event.participants.length > 1),
2199
+ sessionKey,
2200
+ sendTarget,
2201
+ participantHandles: event.participants.map((participant) => participant.externalId),
2202
+ };
2203
+ }
2204
+ function semanticCaptureRequiresRoutingRepair(capture) {
2205
+ return !capture.event.chatGuid?.trim() && !capture.event.chatIdentifier?.trim();
2206
+ }
2207
+ function hasResolvedBlueBubblesRouting(event) {
2208
+ return Boolean(event.chat.chatGuid?.trim() || event.chat.chatIdentifier?.trim());
2209
+ }
2210
+ function reserveBlueBubblesObservation(event) {
2211
+ return (0, latest_turn_1.reserveObservation)({
2212
+ chatGuid: event.chat.chatGuid,
2213
+ chatIdentifier: event.chat.chatIdentifier,
2214
+ });
2215
+ }
2216
+ function groupBlueBubblesObservationsByLane(candidates) {
2217
+ let groups = [];
2218
+ for (const candidate of candidates) {
2219
+ const keys = new Set((0, latest_turn_1.observationSchedulingKeys)(candidate.observationReservation));
2220
+ const destination = { keys: new Set(keys), candidates: [candidate] };
2221
+ const independentGroups = [];
2222
+ for (const group of groups) {
2223
+ const intersects = (keys.has("unresolved:*")
2224
+ || group.keys.has("unresolved:*")
2225
+ || [...keys].some((key) => group.keys.has(key)));
2226
+ if (!intersects) {
2227
+ independentGroups.push(group);
2228
+ continue;
2229
+ }
2230
+ destination.candidates.push(...group.candidates);
2231
+ for (const key of group.keys)
2232
+ destination.keys.add(key);
2233
+ }
2234
+ groups = [...independentGroups, destination];
2235
+ }
2236
+ return groups.map((group) => group.candidates.sort((left, right) => (right.observationReservation.ordinal - left.observationReservation.ordinal)));
2237
+ }
2238
+ function canonicalizeBlueBubblesEvent(event, capability) {
2239
+ const canonical = capability.canonicalChat;
2240
+ const { chatIdentifier: _observedIdentifier, ...observedChat } = event.chat;
2241
+ return {
2242
+ ...event,
2243
+ chat: {
2244
+ ...observedChat,
2245
+ chatGuid: canonical.chatGuid,
2246
+ ...(canonical.chatIdentifier ? { chatIdentifier: canonical.chatIdentifier } : {}),
2247
+ sessionKey: canonical.sessionKey,
2248
+ sendTarget: { kind: "chat_guid", value: canonical.chatGuid },
2249
+ },
2250
+ };
2251
+ }
2252
+ function isCaptureOnlyBlueBubblesMutation(capture) {
2253
+ return capture.event.kind === "reaction"
2254
+ || capture.event.kind === "edit"
2255
+ || capture.event.kind === "unsend";
2256
+ }
2257
+ function isAuditOnlySemanticCapture(capture) {
2258
+ return capture.event.kind === "read" || capture.event.kind === "delivery";
2259
+ }
2260
+ function semanticCaptureToNormalizedEvent(capture) {
2261
+ const event = capture.event;
2262
+ const messageGuid = event.eventGuid?.trim();
2263
+ const chat = semanticCaptureChat(capture);
2264
+ if (!messageGuid || !chat)
2265
+ return null;
2266
+ const sender = {
2267
+ provider: "imessage-handle",
2268
+ externalId: event.actor.externalId,
2269
+ rawId: event.actor.externalId,
2270
+ displayName: event.actor.displayName ?? event.actor.externalId,
2271
+ observed: true,
2272
+ };
2273
+ const timestamp = Date.parse(event.effectiveAt ?? capture.capturedAt);
2274
+ if (event.kind === "message") {
2275
+ const text = event.text ?? "";
2276
+ return {
2277
+ kind: "message",
2278
+ eventType: event.sourceEventType,
2279
+ messageGuid,
2280
+ timestamp,
2281
+ fromMe: event.fromMe,
2282
+ sender,
2283
+ chat,
2284
+ text,
2285
+ textForAgent: text,
2286
+ attachments: [],
2287
+ hasPayloadData: false,
2288
+ requiresRepair: true,
2289
+ };
2290
+ }
2291
+ const shouldNotifyAgent = event.kind === "reaction" || event.kind === "edit" || event.kind === "unsend";
2292
+ const targetMessageGuid = event.targetGuid ?? undefined;
2293
+ const reaction = event.kind === "reaction" && event.canonicalAction && event.canonicalValue
2294
+ ? {
2295
+ ...(0, model_1.describeBlueBubblesReaction)(event.rawTransportValue ?? event.canonicalValue, event.rawTransportValue ?? event.canonicalValue),
2296
+ canonicalValue: event.canonicalValue,
2297
+ action: event.canonicalAction,
2298
+ }
2299
+ : undefined;
2300
+ const textForAgent = reaction
2301
+ ? (0, model_1.renderBlueBubblesReactionText)(reaction, { guid: targetMessageGuid })
2302
+ : event.kind === "edit"
2303
+ ? event.text ? `edited message: ${event.text}` : "edited a message"
2304
+ : event.kind === "unsend"
2305
+ ? "unsent a message"
2306
+ : event.kind === "read"
2307
+ ? "message marked as read"
2308
+ : "message marked as delivered";
2309
+ return {
2310
+ kind: "mutation",
2311
+ eventType: event.sourceEventType,
2312
+ mutationType: event.kind,
2313
+ messageGuid,
2314
+ targetMessageGuid,
2315
+ timestamp,
2316
+ fromMe: event.fromMe,
2317
+ sender,
2318
+ chat,
2319
+ shouldNotifyAgent,
2320
+ textForAgent,
2321
+ requiresRepair: true,
2322
+ ...(reaction ? { reaction } : {}),
2323
+ ...(event.kind === "edit" && event.text !== null ? { editedText: event.text } : {}),
2324
+ ...(event.revision ? { revision: event.revision } : {}),
2325
+ ...(event.effectiveAt ? { effectiveTimestamp: Date.parse(event.effectiveAt) } : {}),
2326
+ ...(event.kind === "unsend" && event.effectiveAt
2327
+ ? { retractionTimestamp: Date.parse(event.effectiveAt) }
2328
+ : {}),
2329
+ };
2330
+ }
2331
+ function seedBlueBubblesStartupSemanticObservations(deps = {}) {
2332
+ const resolvedDeps = { ...defaultDeps, ...deps };
2333
+ const agentName = resolvedDeps.getAgentName();
2334
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2335
+ let seeded = 0;
2336
+ for (const capture of (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName)
2337
+ .sort(semantic_receipts_1.compareBlueBubblesSemanticCaptureOrder)) {
2338
+ const disposition = (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(capture, cutover);
2339
+ if (disposition.disposition === "audit_only")
2340
+ continue;
2341
+ const normalized = semanticCaptureToNormalizedEvent(capture);
2342
+ if (!normalized)
2343
+ continue;
2344
+ const generation = adoptBlueBubblesSemanticObservation(capture.keyHash, reserveBlueBubblesObservation(normalized), false);
2345
+ // Startup seeding runs synchronously before listen. Retain each fence so
2346
+ // persisted pre-restart observations remain causally older than any live
2347
+ // webhook accepted by this process and can reuse the same generation when
2348
+ // the delayed recovery pass begins.
2349
+ suspendBlueBubblesSemanticObservation(generation);
2350
+ seeded += 1;
2351
+ }
2352
+ return seeded;
2353
+ }
2354
+ async function classifyCapturedBlueBubblesReaction(captured) {
2355
+ const event = captured.capture.event;
2356
+ if (event.kind !== "reaction" || !event.canonicalAction || !event.canonicalValue)
2357
+ return null;
2358
+ const input = {
2359
+ fromMe: event.fromMe,
2360
+ action: event.canonicalAction,
2361
+ canonicalValue: event.canonicalValue,
2362
+ };
2363
+ return { decision: (0, reaction_policy_1.classifyBlueBubblesReaction)(input) };
2364
+ }
2365
+ function captureOnlyHandledDisposition(capture, reactionDecision) {
2366
+ if (capture.event.kind !== "reaction") {
2367
+ return {
2368
+ outcome: capture.event.kind === "edit" ? "edit_capture_only" : "unsend_capture_only",
2369
+ detailCode: null,
2370
+ };
2371
+ }
2372
+ const classifiedOutcome = reactionDecision.outcome;
2373
+ if (classifiedOutcome === "capture_only_negative" || classifiedOutcome === "capture_only_question") {
2374
+ return {
2375
+ outcome: "capture_only_unknown",
2376
+ detailCode: classifiedOutcome,
2377
+ };
2378
+ }
2379
+ return { outcome: classifiedOutcome, detailCode: null };
2380
+ }
2381
+ function auditOnlyHandledOutcome(kind) {
2382
+ return kind === "read" ? "read_audit_only" : "delivery_audit_only";
2383
+ }
2384
+ function writeCapturedBlueBubblesHandled(agentName, capture, outcome, detailCode = null) {
2385
+ const handledResult = (0, semantic_receipts_1.writeBlueBubblesSemanticHandled)(agentName, {
2386
+ schemaVersion: 1,
2387
+ canonicalKey: capture.canonicalKey,
2388
+ keyHash: capture.keyHash,
2389
+ handledAt: new Date().toISOString(),
2390
+ outcome,
2391
+ detailCode,
2392
+ });
2393
+ if (handledResult === "semantic_handled_collision") {
2394
+ throw new Error("semantic_handled_collision");
2395
+ }
2396
+ }
2397
+ function blueBubblesInboundReplyIdempotencyKey(captureKeyHash) {
2398
+ return `bluebubbles-inbound-reply:${captureKeyHash}`;
2399
+ }
2400
+ function blueBubblesRecoveredOutboundOutcome(record) {
2401
+ return BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES.has(record.status)
2402
+ ? "message_completed"
2403
+ : "message_observed";
2404
+ }
2405
+ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, source, options) {
2406
+ options.lifecycleSignal?.throwIfAborted();
2407
+ const agentName = resolvedDeps.getAgentName();
2408
+ const identity = {
2409
+ canonicalKey: captured.capture.canonicalKey,
2410
+ keyHash: captured.capture.keyHash,
2411
+ };
2412
+ const generation = adoptBlueBubblesSemanticObservation(identity.keyHash, options.observationReservation, true);
2413
+ const semanticHandling = await acquireBlueBubblesSemanticHandlingSlot(identity.keyHash, generation);
2414
+ if (semanticHandling.status === "handled_by_owner") {
2415
+ return {
2416
+ handled: true,
2417
+ notifiedAgent: false,
2418
+ kind: captured.normalized.kind,
2419
+ reason: "already_processed",
2420
+ };
2421
+ }
2422
+ const reservation = semanticHandling.reservation;
2423
+ let claim;
2424
+ try {
2425
+ options.lifecycleSignal?.throwIfAborted();
2426
+ claim = await (0, semantic_receipts_1.acquireBlueBubblesSemanticClaim)(agentName, identity);
2427
+ }
2428
+ catch (error) {
2429
+ releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "retryable");
2430
+ throw error;
2431
+ }
2432
+ if (claim.status === "already_handled") {
2433
+ releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "terminal");
2434
+ return {
2435
+ handled: true,
2436
+ notifiedAgent: false,
2437
+ kind: captured.normalized.kind,
2438
+ reason: "already_processed",
2439
+ };
2440
+ }
2441
+ if (claim.status === "timeout") {
2442
+ releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "retryable");
2443
+ return {
2444
+ handled: false,
2445
+ notifiedAgent: false,
2446
+ kind: captured.normalized.kind,
2447
+ reason: "semantic_claim_timeout",
2448
+ };
2449
+ }
2450
+ let latestTurnCapability = null;
2451
+ let semanticHandlingCompleted = false;
2452
+ let retainSemanticClaim = false;
2453
+ let acceptedReceiptPublished = false;
2454
+ const publishTerminalReceipt = (outcome, detailCode = null) => {
2455
+ try {
2456
+ writeCapturedBlueBubblesHandled(agentName, captured.capture, outcome, detailCode);
2457
+ }
2458
+ catch (error) {
2459
+ // A missing terminal receipt must never reopen this semantic event. Keep
2460
+ // the durable claim live so recovery cannot resurrect a canceled turn or
2461
+ // duplicate a reply whose transport is already externally visible.
2462
+ retainSemanticClaim = true;
2463
+ semanticHandlingCompleted = true;
2464
+ (0, runtime_1.emitNervesEvent)({
2465
+ level: "error",
2466
+ component: "senses",
2467
+ event: "senses.bluebubbles_terminal_receipt_failed",
2468
+ message: "bluebubbles event could not publish its terminal semantic receipt",
2469
+ meta: {
2470
+ messageGuid: captured.normalized.messageGuid,
2471
+ keyHash: captured.capture.keyHash,
2472
+ outcome,
2473
+ reason: error instanceof Error ? error.message : String(error),
2474
+ },
2475
+ });
2476
+ throw error;
2477
+ }
2478
+ };
2479
+ const publishAcceptedReceipt = () => {
2480
+ if (acceptedReceiptPublished)
2481
+ return;
2482
+ publishTerminalReceipt("message_completed");
2483
+ acceptedReceiptPublished = true;
2484
+ };
2485
+ try {
2486
+ options.lifecycleSignal?.throwIfAborted();
2487
+ const reactionClassification = await classifyCapturedBlueBubblesReaction(captured);
2488
+ options.lifecycleSignal?.throwIfAborted();
2489
+ const reactionDecision = reactionClassification?.decision ?? null;
2490
+ if (captured.capture.event.fromMe) {
2491
+ (0, runtime_1.emitNervesEvent)({
2492
+ component: "senses",
2493
+ event: "senses.bluebubbles_from_me_ignored",
2494
+ message: "ignored from-me bluebubbles event",
2495
+ meta: {
2496
+ messageGuid: captured.normalized.messageGuid,
2497
+ kind: captured.normalized.kind,
2498
+ },
2499
+ });
2500
+ if (captured.normalized.chat.isGroup) {
2501
+ recordDiscoveredOwnHandle(captured.normalized.sender.externalId);
2502
+ }
2503
+ const result = {
2504
+ handled: true,
2505
+ notifiedAgent: false,
2506
+ kind: captured.normalized.kind,
2507
+ reason: "from_me",
2508
+ };
2509
+ publishTerminalReceipt("ignored_self");
2510
+ semanticHandlingCompleted = true;
2511
+ return result;
2512
+ }
2513
+ const durableOutbound = (0, outbound_state_1.readBlueBubblesOutboundRecordByIdempotencyKey)((0, identity_1.getAgentRoot)(), blueBubblesInboundReplyIdempotencyKey(captured.capture.keyHash));
2514
+ if (durableOutbound) {
2515
+ const outcome = blueBubblesRecoveredOutboundOutcome(durableOutbound);
2516
+ (0, runtime_1.emitNervesEvent)({
2517
+ level: "warn",
2518
+ component: "senses",
2519
+ event: "senses.bluebubbles_inbound_reply_recovery_suppressed",
2520
+ message: "suppressed bluebubbles inbound reply recovery at its durable send boundary",
2521
+ meta: {
2522
+ messageGuid: captured.normalized.messageGuid,
2523
+ recordId: durableOutbound.recordId,
2524
+ outboundStatus: durableOutbound.status,
2525
+ outcome,
2526
+ },
2527
+ });
2528
+ publishTerminalReceipt(outcome);
2529
+ semanticHandlingCompleted = true;
2530
+ return {
2531
+ handled: true,
2532
+ notifiedAgent: false,
2533
+ kind: captured.normalized.kind,
2534
+ reason: "already_processed",
2535
+ };
2536
+ }
2537
+ let handledOutcome = "message_observed";
2538
+ let result;
2539
+ if (isAuditOnlySemanticCapture(captured.capture)) {
2540
+ handledOutcome = auditOnlyHandledOutcome(captured.capture.event.kind);
2541
+ result = {
2542
+ handled: true,
2543
+ notifiedAgent: false,
2544
+ kind: captured.normalized.kind,
2545
+ reason: "mutation_state_only",
2546
+ };
2547
+ }
2548
+ else {
2549
+ const captureOnlyMutation = isCaptureOnlyBlueBubblesMutation(captured.capture);
2550
+ const repaired = captureOnlyMutation && captured.normalized.chat.chatGuid?.trim()
2551
+ ? captured.normalized
2552
+ : await resolvedDeps.createClient().repairEvent(captured.normalized);
2553
+ options.lifecycleSignal?.throwIfAborted();
2554
+ if (semanticCaptureRequiresRoutingRepair(captured.capture)
2555
+ && !hasResolvedBlueBubblesRouting(repaired)) {
2556
+ throw new Error("semantic_capture_routing_invalid");
2557
+ }
2558
+ const identityGroundedRepaired = {
2559
+ ...repaired,
2560
+ fromMe: captured.normalized.fromMe,
2561
+ sender: captured.normalized.sender,
2562
+ };
2563
+ const promotion = (0, latest_turn_1.promote)(reservation, {
2564
+ chatGuid: identityGroundedRepaired.chat.chatGuid,
2565
+ chatIdentifier: identityGroundedRepaired.chat.chatIdentifier,
2566
+ }, {
2567
+ allowSameGenerationRetry: semanticHandling.allowSameGenerationRetry,
2568
+ });
2569
+ if (promotion.status !== "promoted") {
2570
+ if (!captureOnlyMutation && promotion.status === "unresolved") {
2571
+ throw new Error("semantic_capture_routing_invalid");
2572
+ }
2573
+ const disposition = captureOnlyMutation
2574
+ ? captureOnlyHandledDisposition(captured.capture, reactionDecision)
2575
+ : { outcome: "message_observed", detailCode: null };
2576
+ publishTerminalReceipt(disposition.outcome, disposition.detailCode);
2577
+ semanticHandlingCompleted = true;
2578
+ return {
2579
+ handled: true,
2580
+ notifiedAgent: false,
2581
+ kind: captured.normalized.kind,
2582
+ reason: captureOnlyMutation ? "mutation_state_only" : "superseded",
2583
+ };
2584
+ }
2585
+ latestTurnCapability = promotion.capability;
2586
+ if (captureOnlyMutation) {
2587
+ const disposition = captureOnlyHandledDisposition(captured.capture, reactionDecision);
2588
+ publishTerminalReceipt(disposition.outcome, disposition.detailCode);
2589
+ semanticHandlingCompleted = true;
2590
+ return {
2591
+ handled: true,
2592
+ notifiedAgent: false,
2593
+ kind: captured.normalized.kind,
2594
+ reason: "mutation_state_only",
2595
+ };
2596
+ }
2597
+ if (identityGroundedRepaired.kind === "mutation") {
2598
+ const outcome = auditOnlyHandledOutcome(identityGroundedRepaired.mutationType);
2599
+ publishTerminalReceipt(outcome);
2600
+ semanticHandlingCompleted = true;
2601
+ return {
2602
+ handled: true,
2603
+ notifiedAgent: false,
2604
+ kind: captured.normalized.kind,
2605
+ reason: "mutation_state_only",
2606
+ };
2607
+ }
2608
+ const canonicalEvent = canonicalizeBlueBubblesEvent(identityGroundedRepaired, latestTurnCapability);
2609
+ const capturedOptions = {
2610
+ ...options,
2611
+ latestTurnCapability,
2612
+ currentIngressEvidence: Object.freeze({
2613
+ schemaVersion: 1,
2614
+ provider: "bluebubbles",
2615
+ captureKeyHash: captured.capture.keyHash,
2616
+ }),
2617
+ orientationEvidence: captured.capture.event,
2618
+ orientationConversationKind: canonicalEvent.chat.isGroup ? "group" : "one_to_one",
2619
+ publishAcceptedReceipt,
2620
+ };
2621
+ result = await handleBlueBubblesNormalizedEvent(canonicalEvent, resolvedDeps, source, capturedOptions);
2622
+ handledOutcome = result.notifiedAgent ? "message_completed" : "message_observed";
2623
+ }
2624
+ if (result.notifiedAgent) {
2625
+ publishAcceptedReceipt();
2626
+ }
2627
+ else {
2628
+ publishTerminalReceipt(handledOutcome);
2629
+ }
2630
+ semanticHandlingCompleted = true;
2631
+ return result;
2632
+ }
2633
+ finally {
2634
+ if (latestTurnCapability)
2635
+ (0, latest_turn_1.finish)(latestTurnCapability);
2636
+ try {
2637
+ if (!retainSemanticClaim) {
2638
+ (0, semantic_receipts_1.releaseBlueBubblesSemanticClaim)(agentName, claim);
2639
+ }
2640
+ }
2641
+ finally {
2642
+ releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, retainSemanticClaim
2643
+ ? "retained"
2644
+ : semanticHandlingCompleted ? "terminal" : "retryable");
2645
+ }
2646
+ }
2647
+ }
2648
+ async function handleBlueBubblesEvent(payload, deps = {}) {
2649
+ const resolvedDeps = { ...defaultDeps, ...deps };
2650
+ let normalized;
2651
+ try {
2652
+ normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
2653
+ }
2654
+ catch (error) {
2655
+ if (error instanceof model_1.BlueBubblesIgnoredEventError) {
2656
+ (0, runtime_1.emitNervesEvent)({
2657
+ component: "senses",
2658
+ event: "senses.bluebubbles_event_skipped",
2659
+ message: "skipped ignorable bluebubbles event",
2660
+ meta: {
2661
+ eventType: error.eventType,
2662
+ },
2663
+ });
2664
+ return {
2665
+ handled: true,
2666
+ notifiedAgent: false,
2667
+ reason: "ignored",
2668
+ };
2669
+ }
2670
+ throw error;
2671
+ }
2672
+ const observationReservation = reserveBlueBubblesObservation(normalized);
2673
+ let captured;
2674
+ try {
2675
+ captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook", observationReservation);
2676
+ }
2677
+ catch (error) {
2678
+ (0, latest_turn_1.clearPending)(observationReservation);
2679
+ throw error;
2680
+ }
2681
+ if (captured.status === "audit_only") {
2682
+ (0, latest_turn_1.clearPending)(observationReservation);
2683
+ return { handled: true, notifiedAgent: false, kind: normalized.kind, reason: "ignored" };
2684
+ }
2685
+ return handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook", {
2686
+ observationReservation,
2687
+ });
2688
+ }
2689
+ function parseTimestampMs(value) {
2690
+ if (!value)
2691
+ return null;
2692
+ const parsed = Date.parse(value);
2693
+ return Number.isFinite(parsed) ? parsed : null;
2694
+ }
2695
+ function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
2696
+ if (previousState.upstreamStatus === "error") {
2697
+ return nowMs - BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS;
2698
+ }
2699
+ const lastCheckedAt = parseTimestampMs(previousState.lastCheckedAt);
2700
+ if (lastCheckedAt !== null) {
2701
+ return Math.max(0, lastCheckedAt - BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS);
2702
+ }
2703
+ return nowMs - BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS;
2704
+ }
2705
+ function formatBlueBubblesRuntimeDetail(queued, failed, active) {
2706
+ if (active.stalledTurnCount > 0) {
2707
+ return `iMessage live turn appears stalled; ${active.stalledTurnCount} active turn(s) older than ${BLUEBUBBLES_LIVE_TURN_STALLED_MS}ms`;
2708
+ }
2709
+ if (active.activeTurnCount > 0)
2710
+ return `upstream reachable; ${active.activeTurnCount} live turn(s) active`;
2711
+ if (queued > 0)
2712
+ return `upstream reachable but iMessage is not caught up; ${queued} recovery item(s) queued`;
2713
+ if (failed > 0)
2714
+ return `${failed} message(s) unrecoverable this cycle; upstream ok`;
2715
+ return "upstream reachable";
2716
+ }
2717
+ function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
2718
+ const pendingEntries = (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName);
2719
+ const pendingRecordedAt = pendingEntries
2720
+ .map((entry) => entry.capturedAt)
2721
+ .map((value) => ({ value, ms: Date.parse(value) }))
2722
+ .filter((entry) => Number.isFinite(entry.ms))
2723
+ .sort((left, right) => left.ms - right.ms);
2724
+ const oldest = pendingRecordedAt[0];
2725
+ return {
2726
+ pendingRecoveryCount: pendingEntries.length,
2727
+ oldestPendingRecoveryAt: oldest?.value,
2728
+ oldestPendingRecoveryAgeMs: oldest ? Math.max(0, nowMs - oldest.ms) : undefined,
2729
+ };
2730
+ }
2731
+ async function syncBlueBubblesRuntime(deps = {}, options = {}) {
2732
+ options.lifecycleSignal?.throwIfAborted();
2733
+ const resolvedDeps = { ...defaultDeps, ...deps };
2734
+ const agentName = resolvedDeps.getAgentName();
2735
+ // Allocate before the first health-probe await. Any live webhook accepted
2736
+ // while that probe is pending must remain causally newer than this pass.
2737
+ const catchUpObservationBatch = options.catchUpObservationBatch ?? (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
2738
+ pruneBlueBubblesSemanticObservationGenerations();
2739
+ const client = resolvedDeps.createClient();
2740
+ const checkedAt = new Date().toISOString();
2741
+ const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
2742
+ try {
2743
+ await client.checkHealth();
2744
+ options.lifecycleSignal?.throwIfAborted();
2745
+ const pendingBeforeCatchup = blueBubblesPendingRecoverySnapshot(agentName);
2746
+ const activeBeforeCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
2747
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
2748
+ upstreamStatus: "ok",
2749
+ detail: "upstream reachable; recovery pass running",
2750
+ lastCheckedAt: checkedAt,
2751
+ proofMethod: "bluebubbles.checkHealth",
2752
+ ...pendingBeforeCatchup,
2753
+ ...activeBeforeCatchup,
2754
+ lastRecoveredAt: previousState.lastRecoveredAt,
2755
+ lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
2756
+ });
2757
+ const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState, {
2758
+ processTurns: false,
2759
+ observationBatch: catchUpObservationBatch,
2760
+ lifecycleSignal: options.lifecycleSignal,
2761
+ });
2762
+ options.lifecycleSignal?.throwIfAborted();
2763
+ const failed = catchUp.failed;
2764
+ const pendingAfterCatchup = blueBubblesPendingRecoverySnapshot(agentName);
2765
+ const activeAfterCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
2766
+ const queued = pendingAfterCatchup.pendingRecoveryCount;
2767
+ // upstreamStatus reflects whether BlueBubbles itself and the local bridge
2768
+ // can answer webhook traffic. The daemon status layer treats
2769
+ // pendingRecoveryCount as unhealthy for user-facing iMessage reachability,
2770
+ // while this field stays scoped to upstream transport reachability.
2771
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
2772
+ upstreamStatus: "ok",
2773
+ detail: formatBlueBubblesRuntimeDetail(queued, failed, activeAfterCatchup),
2774
+ lastCheckedAt: checkedAt,
2775
+ proofMethod: "bluebubbles.checkHealth",
2776
+ ...pendingAfterCatchup,
2777
+ ...activeAfterCatchup,
2778
+ pendingRecoveryCount: queued,
2779
+ failedRecoveryCount: failed,
2780
+ lastRecoveredAt: previousState.lastRecoveredAt,
2781
+ lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
2782
+ });
2783
+ }
2784
+ catch (error) {
2785
+ if (options.lifecycleSignal?.aborted)
2786
+ return;
2787
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
2788
+ upstreamStatus: "error",
2789
+ detail: error instanceof Error ? error.message : String(error),
2790
+ lastCheckedAt: checkedAt,
2791
+ proofMethod: "bluebubbles.checkHealth",
2792
+ ...blueBubblesPendingRecoverySnapshot(agentName),
2793
+ ...(0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS),
2794
+ failedRecoveryCount: 0,
2795
+ });
2796
+ }
2797
+ }
2798
+ async function recoverQueuedBlueBubblesMessages(deps = {}, options = {}) {
2799
+ options.lifecycleSignal?.throwIfAborted();
2800
+ const resolvedDeps = { ...defaultDeps, ...deps };
2801
+ const agentName = resolvedDeps.getAgentName();
2802
+ const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
2803
+ const initialPending = blueBubblesPendingRecoverySnapshot(agentName).pendingRecoveryCount;
2804
+ if (initialPending === 0) {
2805
+ return { recovered: 0, skipped: 0, failed: 0, pendingRecoveryCount: 0 };
2806
+ }
2807
+ const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps, options);
2808
+ options.lifecycleSignal?.throwIfAborted();
2809
+ const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
2810
+ options.lifecycleSignal?.throwIfAborted();
2811
+ const pendingSnapshot = blueBubblesPendingRecoverySnapshot(agentName);
2812
+ const pendingRecoveryCount = pendingSnapshot.pendingRecoveryCount;
2813
+ const failed = captured.failed + recovery.failed;
2814
+ const recovered = captured.recovered + recovery.recovered;
2815
+ const skipped = captured.skipped + recovery.skipped;
2816
+ const checkedAt = new Date().toISOString();
2817
+ try {
2818
+ await resolvedDeps.createClient().checkHealth();
2819
+ options.lifecycleSignal?.throwIfAborted();
2820
+ const activeSnapshot = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
2821
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
2822
+ upstreamStatus: "ok",
2823
+ detail: formatBlueBubblesRuntimeDetail(pendingRecoveryCount, failed, activeSnapshot),
2824
+ lastCheckedAt: checkedAt,
2825
+ proofMethod: "bluebubbles.checkHealth",
2826
+ ...pendingSnapshot,
2827
+ ...activeSnapshot,
2828
+ failedRecoveryCount: failed,
2829
+ lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
2830
+ lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
2831
+ });
2832
+ }
2833
+ catch (error) {
2834
+ if (options.lifecycleSignal?.aborted)
2835
+ throw options.lifecycleSignal.reason;
2836
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
2837
+ upstreamStatus: "error",
2838
+ detail: error instanceof Error ? error.message : String(error),
2839
+ lastCheckedAt: checkedAt,
2840
+ proofMethod: "bluebubbles.checkHealth",
2841
+ ...pendingSnapshot,
2842
+ ...(0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS),
2843
+ failedRecoveryCount: failed,
2844
+ lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
2845
+ lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
2846
+ });
2847
+ }
2848
+ return { recovered, skipped, failed, pendingRecoveryCount };
2849
+ }
2850
+ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, options = {}) {
2851
+ options.lifecycleSignal?.throwIfAborted();
2852
+ const resolvedDeps = { ...defaultDeps, ...deps };
2853
+ const agentName = resolvedDeps.getAgentName();
2854
+ const client = resolvedDeps.createClient();
2855
+ const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
2856
+ const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
2857
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2858
+ const catchUpSince = Math.max(resolveBlueBubblesCatchUpSince(state), Date.parse(cutover.effectiveAt));
2859
+ const processTurns = options.processTurns !== false;
2860
+ /* v8 ignore next -- older injected test doubles may omit the catch-up query method */
2861
+ if (!client.listRecentMessages)
2862
+ return result;
2863
+ (0, runtime_1.emitNervesEvent)({
2864
+ component: "senses",
2865
+ event: "senses.bluebubbles_catchup_start",
2866
+ message: "bluebubbles upstream catch-up pass started",
2867
+ meta: {
2868
+ since: new Date(catchUpSince).toISOString(),
2869
+ pageSize: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
2870
+ maxPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
2871
+ },
2872
+ });
2873
+ const preparedCandidates = [];
2874
+ const seenMessageGuids = new Set();
2875
+ const observationBatch = options.observationBatch ?? (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
2876
+ for (let page = 0; page < BLUEBUBBLES_CATCHUP_MAX_PAGES; page++) {
2877
+ let pageEvents;
2878
+ try {
2879
+ pageEvents = await client.listRecentMessages({
2880
+ limit: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
2881
+ offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
2882
+ beforeTimestamp: observationBatch.beforeTimestamp,
2883
+ });
2884
+ }
2885
+ catch (error) {
2886
+ if (options.lifecycleSignal?.aborted)
2887
+ throw options.lifecycleSignal.reason;
2888
+ result.failed++;
2889
+ (0, runtime_1.emitNervesEvent)({
2890
+ level: "warn",
2891
+ component: "senses",
2892
+ event: "senses.bluebubbles_catchup_error",
2893
+ message: "bluebubbles upstream catch-up query failed",
2894
+ meta: {
2895
+ offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
2896
+ reason: error instanceof Error ? error.message : String(error),
2897
+ },
2898
+ });
2899
+ break;
2900
+ }
2901
+ options.lifecycleSignal?.throwIfAborted();
2902
+ const pageMessages = pageEvents
2903
+ .filter((event) => event.kind === "message")
2904
+ .sort((left, right) => right.timestamp - left.timestamp);
2905
+ for (let pageIndex = 0; pageIndex < pageMessages.length; pageIndex++) {
2906
+ const event = pageMessages[pageIndex];
2907
+ if (seenMessageGuids.has(event.messageGuid))
2908
+ continue;
2909
+ seenMessageGuids.add(event.messageGuid);
2910
+ if (event.fromMe
2911
+ || event.timestamp < catchUpSince
2912
+ || (Number.isFinite(event.timestamp) && event.timestamp > observationBatch.beforeTimestamp)) {
2913
+ preparedCandidates.push({ status: "skip", event });
2914
+ continue;
2915
+ }
2916
+ // The bounded ordinal range and upstream snapshot were fixed before the
2917
+ // first query. Each returned page can therefore publish its fences
2918
+ // before the next await, while later ingress stays outside this pass.
2919
+ preparedCandidates.push({
2920
+ status: "ready",
2921
+ event,
2922
+ observationReservation: (0, latest_turn_1.reserveObservationFromBatch)(observationBatch, page * BLUEBUBBLES_CATCHUP_PAGE_SIZE + pageIndex, {
2923
+ chatGuid: event.chat.chatGuid,
2924
+ chatIdentifier: event.chat.chatIdentifier,
2925
+ }),
2926
+ });
2927
+ }
2928
+ if (pageEvents.length < BLUEBUBBLES_CATCHUP_PAGE_SIZE)
2929
+ break;
2930
+ const oldestMessageTimestamp = pageMessages
2931
+ .reduce((oldest, event) => Math.min(oldest, event.timestamp), Number.POSITIVE_INFINITY);
2932
+ if (oldestMessageTimestamp <= catchUpSince)
2933
+ break;
2934
+ if (page === BLUEBUBBLES_CATCHUP_MAX_PAGES - 1) {
2935
+ result.failed++;
2936
+ (0, runtime_1.emitNervesEvent)({
2937
+ level: "warn",
2938
+ component: "senses",
2939
+ event: "senses.bluebubbles_catchup_error",
2940
+ message: "bluebubbles upstream catch-up reached the bounded page limit",
2941
+ meta: {
2942
+ inspectedPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
2943
+ reason: "catch-up page limit reached before the outage window cutoff",
2944
+ },
2945
+ });
2946
+ }
2947
+ }
2948
+ for (const prepared of preparedCandidates) {
2949
+ result.inspected++;
2950
+ if (prepared.status === "skip") {
2951
+ result.skipped++;
2952
+ }
2953
+ }
2954
+ const readyCandidates = preparedCandidates.filter((prepared) => prepared.status === "ready");
2955
+ let lastRecoveredOrdinal = -1;
2956
+ await Promise.all(groupBlueBubblesObservationsByLane(readyCandidates).map(async (lane) => {
2957
+ for (const prepared of lane) {
2958
+ const { event, observationReservation } = prepared;
2959
+ let semanticGenerationOwned = false;
2960
+ try {
2961
+ options.lifecycleSignal?.throwIfAborted();
2962
+ const captured = await captureBlueBubblesSemanticEvent(event, resolvedDeps, "upstream-catchup", observationReservation);
2963
+ if (captured.status === "audit_only") {
2964
+ (0, latest_turn_1.clearPending)(observationReservation);
2965
+ result.skipped++;
2966
+ continue;
2967
+ }
2968
+ if (!processTurns) {
2969
+ const generation = adoptBlueBubblesSemanticObservation(captured.capture.keyHash, observationReservation, false);
2970
+ semanticGenerationOwned = true;
2971
+ if (captured.writeResult === "semantic_capture_duplicate") {
2972
+ if (!activeSemanticHandlingSlots.has(captured.capture.keyHash)) {
2973
+ suspendBlueBubblesSemanticObservation(generation);
2974
+ }
2975
+ result.skipped++;
2976
+ }
2977
+ else {
2978
+ // Runtime sync only captures work for the delayed recovery pass,
2979
+ // but its original generation must revoke older work now and be
2980
+ // reused by the queued recovery pass after any intervening turn.
2981
+ const promotion = (0, latest_turn_1.promote)(generation.reservation, {
2982
+ chatGuid: event.chat.chatGuid,
2983
+ chatIdentifier: event.chat.chatIdentifier,
2984
+ });
2985
+ if (promotion.status === "promoted")
2986
+ (0, latest_turn_1.finish)(promotion.capability);
2987
+ suspendBlueBubblesSemanticObservation(generation);
2988
+ result.queued = (result.queued ?? 0) + 1;
2989
+ }
2990
+ continue;
2991
+ }
2992
+ semanticGenerationOwned = true;
2993
+ const handled = await handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "upstream-catchup", {
2994
+ timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2995
+ autonomyBudgetTrigger: "recovery",
2996
+ observationReservation,
2997
+ lifecycleSignal: options.lifecycleSignal,
2998
+ });
2999
+ if (handled.reason === "semantic_claim_timeout") {
3000
+ continue;
3001
+ }
3002
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
3003
+ result.skipped++;
3004
+ }
3005
+ else {
3006
+ result.recovered++;
3007
+ if (observationReservation.ordinal > lastRecoveredOrdinal) {
3008
+ lastRecoveredOrdinal = observationReservation.ordinal;
3009
+ result.lastRecoveredMessageGuid = event.messageGuid;
3010
+ }
3011
+ }
3012
+ }
3013
+ catch (error) {
3014
+ if (!semanticGenerationOwned)
3015
+ (0, latest_turn_1.clearPending)(observationReservation);
3016
+ if (options.lifecycleSignal?.aborted)
3017
+ throw options.lifecycleSignal.reason;
3018
+ recordBlueBubblesRecoveryFailureForBudget(agentName, event, "upstream-catchup");
3019
+ result.failed++;
3020
+ (0, runtime_1.emitNervesEvent)({
3021
+ level: "warn",
3022
+ component: "senses",
3023
+ event: "senses.bluebubbles_catchup_error",
3024
+ message: "bluebubbles upstream catch-up message failed",
3025
+ meta: {
3026
+ messageGuid: event.messageGuid,
3027
+ reason: error instanceof Error ? error.message : String(error),
3028
+ },
3029
+ });
3030
+ }
3031
+ }
3032
+ }));
3033
+ if (result.inspected > 0 || result.recovered > 0 || result.skipped > 0 || result.failed > 0) {
3034
+ (0, runtime_1.emitNervesEvent)({
3035
+ component: "senses",
3036
+ event: "senses.bluebubbles_catchup_complete",
3037
+ message: "bluebubbles upstream catch-up pass completed",
3038
+ meta: { ...result },
3039
+ });
3040
+ }
3041
+ return result;
3042
+ }
3043
+ async function recoverCapturedBlueBubblesInboundMessages(deps = {}, options = {}) {
3044
+ options.lifecycleSignal?.throwIfAborted();
3045
+ const resolvedDeps = { ...defaultDeps, ...deps };
3046
+ const agentName = resolvedDeps.getAgentName();
3047
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
3048
+ const result = { recovered: 0, skipped: 0, failed: 0 };
3049
+ const seenKeyHashes = new Set();
3050
+ const candidates = (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName)
3051
+ .sort(semantic_receipts_1.compareBlueBubblesSemanticCaptureOrder);
3052
+ const preparedCandidates = [];
3053
+ for (const capture of candidates) {
3054
+ options.lifecycleSignal?.throwIfAborted();
3055
+ if (seenKeyHashes.has(capture.keyHash)) {
3056
+ preparedCandidates.push({ status: "skip", capture });
3057
+ continue;
3058
+ }
3059
+ seenKeyHashes.add(capture.keyHash);
3060
+ const disposition = (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(capture, cutover);
3061
+ if (disposition.disposition === "audit_only" && disposition.reason !== "audit_event") {
3062
+ preparedCandidates.push({ status: "skip", capture });
3063
+ continue;
3064
+ }
3065
+ const normalized = semanticCaptureToNormalizedEvent(capture);
3066
+ if (!normalized) {
3067
+ preparedCandidates.push({ status: "invalid", capture });
3068
+ continue;
3069
+ }
3070
+ preparedCandidates.push({
3071
+ status: "ready",
3072
+ capture,
3073
+ normalized,
3074
+ observationReservation: reserveBlueBubblesObservation(normalized),
3075
+ });
3076
+ }
3077
+ for (const prepared of preparedCandidates) {
3078
+ const { capture } = prepared;
3079
+ if (prepared.status === "skip") {
3080
+ result.skipped++;
3081
+ continue;
3082
+ }
3083
+ if (prepared.status === "invalid") {
3084
+ result.failed++;
3085
+ (0, runtime_1.emitNervesEvent)({
3086
+ level: "warn",
3087
+ component: "senses",
3088
+ event: "senses.bluebubbles_capture_recovery_error",
3089
+ message: "captured bluebubbles message recovery failed",
3090
+ meta: {
3091
+ messageGuid: capture.event.eventGuid,
3092
+ sessionKey: capture.event.sessionKey,
3093
+ reason: "semantic_capture_routing_invalid",
3094
+ },
3095
+ });
3096
+ }
3097
+ }
3098
+ const readyCandidates = preparedCandidates.filter((prepared) => prepared.status === "ready");
3099
+ await Promise.all(groupBlueBubblesObservationsByLane(readyCandidates).map(async (lane) => {
3100
+ for (const prepared of lane) {
3101
+ const { capture, normalized, observationReservation } = prepared;
3102
+ try {
3103
+ options.lifecycleSignal?.throwIfAborted();
3104
+ const handled = await handleCapturedBlueBubblesSemanticEvent({
3105
+ status: "captured",
3106
+ capture,
3107
+ normalized,
3108
+ writeResult: "semantic_capture_duplicate",
3109
+ }, resolvedDeps, "webhook", {
3110
+ timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
3111
+ autonomyBudgetTrigger: "recovery",
3112
+ semanticRecovery: true,
3113
+ observationReservation,
3114
+ lifecycleSignal: options.lifecycleSignal,
3115
+ });
3116
+ if (handled.reason === "semantic_claim_timeout") {
3117
+ continue;
3118
+ }
3119
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
3120
+ result.skipped++;
3121
+ }
3122
+ else {
3123
+ result.recovered++;
3124
+ }
3125
+ }
3126
+ catch (error) {
3127
+ if (options.lifecycleSignal?.aborted)
3128
+ throw options.lifecycleSignal.reason;
3129
+ recordBlueBubblesRecoveryFailureForBudget(agentName, normalized, "webhook");
3130
+ result.failed++;
3131
+ (0, runtime_1.emitNervesEvent)({
3132
+ level: "warn",
3133
+ component: "senses",
3134
+ event: "senses.bluebubbles_capture_recovery_error",
3135
+ message: "captured bluebubbles message recovery failed",
3136
+ meta: {
3137
+ messageGuid: capture.event.eventGuid,
3138
+ sessionKey: capture.event.sessionKey,
3139
+ reason: error instanceof Error ? error.message : String(error),
3140
+ },
3141
+ });
3142
+ }
3143
+ }
3144
+ }));
3145
+ const semanticEventGuids = new Set(candidates
3146
+ .map((capture) => capture.event.eventGuid)
3147
+ .filter((eventGuid) => typeof eventGuid === "string")
3148
+ .map((eventGuid) => eventGuid.trim().toLowerCase()));
3149
+ for (const legacyEntry of (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)) {
3150
+ if (semanticEventGuids.has(legacyEntry.messageGuid.trim().toLowerCase()))
3151
+ continue;
3152
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(legacyEntry, cutover);
3153
+ result.skipped++;
3154
+ }
3155
+ return result;
3156
+ }
3157
+ async function recoverMissedBlueBubblesMessages(deps = {}) {
3158
+ const resolvedDeps = { ...defaultDeps, ...deps };
3159
+ const agentName = resolvedDeps.getAgentName();
3160
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
3161
+ const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
3162
+ for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
3163
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(candidate, cutover);
3164
+ result.skipped++;
3165
+ }
3166
+ if (result.recovered > 0 || result.skipped > 0 || result.pending > 0 || result.failed > 0) {
3167
+ (0, runtime_1.emitNervesEvent)({
3168
+ component: "senses",
3169
+ event: "senses.bluebubbles_recovery_complete",
3170
+ message: "bluebubbles backlog recovery pass completed",
3171
+ meta: { ...result },
3172
+ });
3173
+ }
3174
+ return result;
3175
+ }
3176
+ function createBlueBubblesWebhookHandler(deps = {}, options = {}) {
3177
+ return async (req, res) => {
3178
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3179
+ if (url.pathname === "/health") {
3180
+ if (req.method === "GET" || req.method === "HEAD") {
3181
+ writeJson(res, 200, { status: "ok", uptime: process.uptime() });
3182
+ return;
3183
+ }
3184
+ writeJson(res, 405, { error: "Method not allowed" });
3185
+ return;
3186
+ }
3187
+ const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
3188
+ const runtimeConfig = (0, config_1.getBlueBubblesConfig)();
3189
+ if (url.pathname !== channelConfig.webhookPath) {
3190
+ writeJson(res, 404, { error: "Not found" });
3191
+ return;
3192
+ }
3193
+ if (req.method !== "POST") {
3194
+ writeJson(res, 405, { error: "Method not allowed" });
3195
+ return;
3196
+ }
3197
+ if (!isWebhookPasswordValid(url, runtimeConfig.password)) {
3198
+ writeJson(res, 401, { error: "Unauthorized" });
3199
+ return;
3200
+ }
3201
+ let payload;
3202
+ try {
3203
+ const rawBody = await readRequestBody(req);
3204
+ payload = JSON.parse(rawBody);
3205
+ }
3206
+ catch (error) {
3207
+ (0, runtime_1.emitNervesEvent)({
3208
+ level: "warn",
3209
+ component: "senses",
3210
+ event: "senses.bluebubbles_webhook_bad_json",
3211
+ message: "failed to parse bluebubbles webhook body",
3212
+ meta: {
3213
+ reason: error instanceof Error ? error.message : String(error),
3214
+ },
3215
+ });
3216
+ writeJson(res, 400, { error: "Invalid JSON body" });
3217
+ return;
3218
+ }
3219
+ let normalized;
3220
+ try {
3221
+ normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
3222
+ }
3223
+ catch (error) {
3224
+ if (error instanceof model_1.BlueBubblesIgnoredEventError) {
3225
+ (0, runtime_1.emitNervesEvent)({
3226
+ component: "senses",
3227
+ event: "senses.bluebubbles_event_skipped",
3228
+ message: "skipped ignorable bluebubbles event",
3229
+ meta: {
3230
+ eventType: error.eventType,
3231
+ },
3232
+ });
3233
+ writeJson(res, 200, {
3234
+ handled: true,
3235
+ notifiedAgent: false,
3236
+ reason: "ignored",
3237
+ });
3238
+ return;
3239
+ }
3240
+ (0, runtime_1.emitNervesEvent)({
3241
+ level: "error",
3242
+ component: "senses",
3243
+ event: "senses.bluebubbles_webhook_error",
3244
+ message: "bluebubbles webhook handling failed",
3245
+ meta: {
3246
+ /* v8 ignore next -- normalizeBlueBubblesEvent throws Error subclasses; String fallback is defensive @preserve */
3247
+ reason: error instanceof Error ? error.message : String(error),
3248
+ },
3249
+ });
3250
+ writeJson(res, 500, {
3251
+ /* v8 ignore next -- normalizeBlueBubblesEvent throws Error subclasses; String fallback is defensive @preserve */
3252
+ error: error instanceof Error ? error.message : String(error),
3253
+ });
3254
+ return;
3255
+ }
3256
+ const resolvedDeps = { ...defaultDeps, ...deps };
3257
+ const observationReservation = reserveBlueBubblesObservation(normalized);
3258
+ let captured;
3259
+ try {
3260
+ captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook", observationReservation);
3261
+ }
3262
+ catch (error) {
3263
+ (0, latest_turn_1.clearPending)(observationReservation);
3264
+ (0, runtime_1.emitNervesEvent)({
3265
+ level: "error",
3266
+ component: "senses",
3267
+ event: "senses.bluebubbles_webhook_error",
3268
+ message: "bluebubbles semantic capture failed before acknowledgement",
3269
+ meta: {
3270
+ messageGuid: normalized.messageGuid,
3271
+ reason: error instanceof Error ? error.message : String(error),
3272
+ },
3273
+ });
3274
+ writeSemanticCaptureFailure(res);
3275
+ return;
3276
+ }
3277
+ if (captured.status === "audit_only") {
3278
+ (0, latest_turn_1.clearPending)(observationReservation);
3279
+ writeJson(res, 200, {
3280
+ handled: true,
3281
+ notifiedAgent: false,
3282
+ kind: normalized.kind,
3283
+ queued: false,
3284
+ reason: "ignored",
3285
+ });
3286
+ return;
3287
+ }
3288
+ writeJson(res, 200, {
3289
+ handled: true,
3290
+ notifiedAgent: false,
3291
+ kind: normalized.kind,
3292
+ queued: true,
3293
+ reason: "queued",
3294
+ });
3295
+ setTimeout(() => {
3296
+ if (options.lifecycleSignal?.aborted) {
3297
+ (0, latest_turn_1.clearPending)(observationReservation);
3298
+ return;
3299
+ }
3300
+ void handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook", {
3301
+ observationReservation,
3302
+ lifecycleSignal: options.lifecycleSignal,
3303
+ }).catch((error) => {
3304
+ if (options.lifecycleSignal?.aborted)
3305
+ return;
3306
+ (0, runtime_1.emitNervesEvent)({
3307
+ level: "error",
3308
+ component: "senses",
3309
+ event: "senses.bluebubbles_webhook_async_error",
3310
+ message: "bluebubbles webhook async handling failed after durable capture",
3311
+ meta: {
3312
+ reason: error instanceof Error ? error.message : String(error),
3313
+ },
3314
+ });
3315
+ });
3316
+ }, 0);
3317
+ };
3318
+ }
3319
+ function findImessageHandle(friend) {
3320
+ for (const ext of friend.externalIds) {
3321
+ if (ext.provider === "imessage-handle" && !ext.externalId.startsWith("group:")) {
3322
+ return ext.externalId;
3323
+ }
3324
+ }
3325
+ return undefined;
3326
+ }
3327
+ function normalizeBlueBubblesSessionKey(sessionKey) {
3328
+ const trimmed = sessionKey.trim();
3329
+ if (trimmed.startsWith("chat_identifier_")) {
3330
+ return `chat_identifier:${trimmed.slice("chat_identifier_".length)}`;
3331
+ }
3332
+ if (trimmed.startsWith("chat_")) {
3333
+ return `chat:${trimmed.slice("chat_".length)}`;
3334
+ }
3335
+ return trimmed;
3336
+ }
3337
+ function extractChatIdentifierFromSessionKey(sessionKey) {
3338
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
3339
+ if (normalizedKey.startsWith("chat:")) {
3340
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
3341
+ const parts = chatGuid.split(";");
3342
+ return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
3343
+ }
3344
+ if (normalizedKey.startsWith("chat_identifier:")) {
3345
+ const identifier = normalizedKey.slice("chat_identifier:".length).trim();
3346
+ return identifier || undefined;
3347
+ }
3348
+ return undefined;
3349
+ }
3350
+ function buildChatRefForSessionKey(friend, sessionKey) {
3351
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
3352
+ if (normalizedKey.startsWith("chat:")) {
3353
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
3354
+ if (!chatGuid)
3355
+ return null;
3356
+ return {
3357
+ chatGuid,
3358
+ chatIdentifier: extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend),
3359
+ isGroup: chatGuid.includes(";+;"),
3360
+ sessionKey,
3361
+ sendTarget: { kind: "chat_guid", value: chatGuid },
3362
+ participantHandles: [],
3363
+ };
3364
+ }
3365
+ const chatIdentifier = extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend);
3366
+ if (!chatIdentifier)
3367
+ return null;
3368
+ return {
3369
+ chatIdentifier,
3370
+ isGroup: false,
3371
+ sessionKey,
3372
+ sendTarget: { kind: "chat_identifier", value: chatIdentifier },
3373
+ participantHandles: [],
3374
+ };
3375
+ }
3376
+ async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
3377
+ if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(params.text)) {
3378
+ (0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
3379
+ site: "proactive",
3380
+ message: "bluebubbles proactive send blocked: internal meta markers",
3381
+ meta: {
3382
+ friendId: params.friendId,
3383
+ sessionKey: params.sessionKey,
3384
+ },
3385
+ });
3386
+ return { delivered: false, reason: "blocked_meta_content" };
3387
+ }
3388
+ const resolvedDeps = { ...defaultDeps, ...deps };
3389
+ const client = resolvedDeps.createClient();
3390
+ const store = resolvedDeps.createFriendStore();
3391
+ let friend;
3392
+ try {
3393
+ friend = await store.get(params.friendId);
3394
+ }
3395
+ catch {
3396
+ friend = null;
3397
+ }
3398
+ // Direct filesystem fallback — store.get() with name resolution wasn't working in production
3399
+ // despite correct compiled code. Bypass the entire store abstraction.
3400
+ /* v8 ignore start -- direct filesystem name resolution @preserve */
3401
+ if (!friend) {
3402
+ try {
3403
+ const friendsDir = path.join((0, identity_1.getAgentRoot)(), "friends");
3404
+ const files = fs.readdirSync(friendsDir).filter((f) => f.endsWith(".json"));
3405
+ for (const file of files) {
3406
+ const raw = JSON.parse(fs.readFileSync(path.join(friendsDir, file), "utf-8"));
3407
+ if (raw.name?.toLowerCase() === params.friendId.toLowerCase()) {
3408
+ friend = raw;
3409
+ (0, runtime_1.emitNervesEvent)({
3410
+ component: "senses",
3411
+ event: "senses.bluebubbles_proactive_name_resolved",
3412
+ message: "resolved friend by name via direct filesystem scan",
3413
+ meta: { friendId: params.friendId, resolvedId: raw.id, name: raw.name },
3414
+ });
3415
+ break;
3416
+ }
3417
+ }
3418
+ }
3419
+ catch (err) {
3420
+ (0, runtime_1.emitNervesEvent)({
3421
+ level: "warn",
3422
+ component: "senses",
3423
+ event: "senses.bluebubbles_proactive_name_resolve_error",
3424
+ message: "direct filesystem name resolution failed",
3425
+ meta: { friendId: params.friendId, error: err instanceof Error ? err.message : String(err) },
3426
+ });
3427
+ }
3428
+ }
3429
+ /* v8 ignore stop */
3430
+ if (!friend) {
3431
+ (0, runtime_1.emitNervesEvent)({
3432
+ level: "warn",
3433
+ component: "senses",
3434
+ event: "senses.bluebubbles_proactive_no_friend",
3435
+ message: "proactive send skipped: friend not found",
3436
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
3437
+ });
3438
+ return { delivered: false, reason: "friend_not_found" };
3439
+ }
3440
+ const explicitCrossChatAuthorized = params.intent === "explicit_cross_chat"
3441
+ && friends_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
3442
+ if (!explicitCrossChatAuthorized && !friends_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
3443
+ (0, runtime_1.emitNervesEvent)({
3444
+ component: "senses",
3445
+ event: "senses.bluebubbles_proactive_trust_skip",
3446
+ message: "proactive send skipped: trust level not allowed",
3447
+ meta: {
3448
+ friendId: params.friendId,
3449
+ sessionKey: params.sessionKey,
3450
+ trustLevel: friend.trustLevel ?? "unknown",
3451
+ intent: params.intent ?? "generic_outreach",
3452
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
3453
+ },
3454
+ });
3455
+ return { delivered: false, reason: "trust_skip" };
3456
+ }
3457
+ const chat = buildChatRefForSessionKey(friend, params.sessionKey);
3458
+ if (!chat) {
3459
+ (0, runtime_1.emitNervesEvent)({
3460
+ level: "warn",
3461
+ component: "senses",
3462
+ event: "senses.bluebubbles_proactive_no_handle",
3463
+ message: "proactive send skipped: no iMessage handle found",
3464
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
3465
+ });
3466
+ return { delivered: false, reason: "missing_target" };
3467
+ }
3468
+ // Proactive outreach to individuals must go to DMs, never group chats.
3469
+ // Explicit cross-chat responses (bridge completions, delegation returns) ARE allowed to groups
3470
+ // because the request originated from that group.
3471
+ /* v8 ignore start -- group gate: only fires when proactive send targets a group session @preserve */
3472
+ if (chat.isGroup && params.intent !== "explicit_cross_chat") {
3473
+ (0, runtime_1.emitNervesEvent)({
3474
+ level: "warn",
3475
+ component: "senses",
3476
+ event: "senses.bluebubbles_proactive_group_blocked",
3477
+ message: "proactive send blocked: would route to group chat",
3478
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey, chatGuid: chat.chatGuid ?? null, intent: params.intent ?? null },
3479
+ });
3480
+ return { delivered: false, reason: "group_blocked" };
3481
+ }
3482
+ /* v8 ignore stop */
3483
+ const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
3484
+ if (internalContentBlockReason) {
3485
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
3486
+ friendId: params.friendId,
3487
+ sessionKey: params.sessionKey,
3488
+ reason: internalContentBlockReason,
3489
+ source: "session_send",
3490
+ intent: params.intent ?? "generic_outreach",
3491
+ });
3492
+ return { delivered: false, reason: "internal_content_blocked" };
3493
+ }
3494
+ try {
3495
+ await client.sendText({ chat, text: params.text });
3496
+ (0, runtime_1.emitNervesEvent)({
3497
+ component: "senses",
3498
+ event: "senses.bluebubbles_proactive_sent",
3499
+ message: "proactive bluebubbles message sent",
3500
+ meta: {
3501
+ friendId: params.friendId,
3502
+ sessionKey: params.sessionKey,
3503
+ chatGuid: chat.chatGuid ?? null,
3504
+ chatIdentifier: chat.chatIdentifier ?? null,
3505
+ },
3506
+ });
3507
+ return { delivered: true };
3508
+ }
3509
+ catch (error) {
3510
+ (0, runtime_1.emitNervesEvent)({
3511
+ level: "error",
3512
+ component: "senses",
3513
+ event: "senses.bluebubbles_proactive_send_error",
3514
+ message: "proactive bluebubbles send failed",
3515
+ meta: {
3516
+ friendId: params.friendId,
3517
+ sessionKey: params.sessionKey,
3518
+ reason: error instanceof Error ? error.message : String(error),
3519
+ },
3520
+ });
3521
+ return { delivered: false, reason: "send_error" };
3522
+ }
3523
+ }
3524
+ function scanPendingBlueBubblesFiles(pendingRoot) {
3525
+ const results = [];
3526
+ let friendIds;
3527
+ try {
3528
+ friendIds = fs.readdirSync(pendingRoot);
3529
+ }
3530
+ catch {
3531
+ return results;
3532
+ }
3533
+ for (const friendId of friendIds) {
3534
+ const bbDir = path.join(pendingRoot, friendId, "bluebubbles");
3535
+ let keys;
3536
+ try {
3537
+ keys = fs.readdirSync(bbDir);
3538
+ }
3539
+ catch {
3540
+ continue;
3541
+ }
3542
+ for (const key of keys) {
3543
+ const keyDir = path.join(bbDir, key);
3544
+ let files;
3545
+ try {
3546
+ files = fs.readdirSync(keyDir);
3547
+ }
3548
+ catch {
3549
+ continue;
3550
+ }
3551
+ for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
3552
+ const filePath = path.join(keyDir, file);
3553
+ try {
3554
+ const content = fs.readFileSync(filePath, "utf-8");
3555
+ results.push({ friendId, key, filePath, content });
3556
+ }
3557
+ catch {
3558
+ // skip unreadable files
3559
+ }
3560
+ }
3561
+ }
3562
+ }
3563
+ return results;
3564
+ }
3565
+ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
3566
+ const resolvedDeps = { ...defaultDeps, ...deps };
3567
+ const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
3568
+ const client = resolvedDeps.createClient();
3569
+ const store = resolvedDeps.createFriendStore();
3570
+ const pendingFiles = scanPendingBlueBubblesFiles(root);
3571
+ const result = { sent: 0, skipped: 0, failed: 0 };
3572
+ for (const { friendId, filePath, content } of pendingFiles) {
3573
+ let parsed;
3574
+ try {
3575
+ parsed = JSON.parse(content);
3576
+ }
3577
+ catch {
3578
+ result.failed++;
3579
+ try {
3580
+ fs.unlinkSync(filePath);
3581
+ }
3582
+ catch { /* ignore */ }
3583
+ continue;
3584
+ }
3585
+ const messageText = typeof parsed.content === "string" ? parsed.content : "";
3586
+ if (!messageText.trim()) {
3587
+ result.skipped++;
3588
+ try {
3589
+ fs.unlinkSync(filePath);
3590
+ }
3591
+ catch { /* ignore */ }
3592
+ continue;
3593
+ }
3594
+ if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(messageText)) {
3595
+ result.skipped++;
3596
+ try {
3597
+ fs.unlinkSync(filePath);
3598
+ }
3599
+ catch { /* ignore */ }
3600
+ (0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
3601
+ site: "drain",
3602
+ message: "bluebubbles drain blocked: internal meta markers",
3603
+ meta: {
3604
+ friendId,
3605
+ filePath,
3606
+ },
3607
+ });
3608
+ continue;
3609
+ }
3610
+ const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
3611
+ if (internalBlockReason) {
3612
+ result.skipped++;
3613
+ try {
3614
+ fs.unlinkSync(filePath);
3615
+ }
3616
+ catch { /* ignore */ }
3617
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
3618
+ friendId,
3619
+ reason: internalBlockReason,
3620
+ source: "pending_drain",
3621
+ });
3622
+ continue;
3623
+ }
3624
+ let friend;
3625
+ try {
3626
+ friend = await store.get(friendId);
3627
+ }
3628
+ catch {
3629
+ friend = null;
3630
+ }
3631
+ if (!friend) {
3632
+ result.skipped++;
3633
+ try {
3634
+ fs.unlinkSync(filePath);
3635
+ }
3636
+ catch { /* ignore */ }
3637
+ (0, runtime_1.emitNervesEvent)({
3638
+ level: "warn",
3639
+ component: "senses",
3640
+ event: "senses.bluebubbles_proactive_no_friend",
3641
+ message: "proactive send skipped: friend not found",
3642
+ meta: { friendId },
3643
+ });
3644
+ continue;
3645
+ }
3646
+ if (!friends_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
3647
+ result.skipped++;
3648
+ try {
3649
+ fs.unlinkSync(filePath);
3650
+ }
3651
+ catch { /* ignore */ }
3652
+ (0, runtime_1.emitNervesEvent)({
3653
+ component: "senses",
3654
+ event: "senses.bluebubbles_proactive_trust_skip",
3655
+ message: "proactive send skipped: trust level not allowed",
3656
+ meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
3657
+ });
3658
+ continue;
3659
+ }
3660
+ const handle = findImessageHandle(friend);
3661
+ if (!handle) {
3662
+ result.skipped++;
3663
+ try {
3664
+ fs.unlinkSync(filePath);
3665
+ }
3666
+ catch { /* ignore */ }
3667
+ (0, runtime_1.emitNervesEvent)({
3668
+ level: "warn",
3669
+ component: "senses",
3670
+ event: "senses.bluebubbles_proactive_no_handle",
3671
+ message: "proactive send skipped: no iMessage handle found",
3672
+ meta: { friendId },
3673
+ });
3674
+ continue;
3675
+ }
3676
+ const chat = {
3677
+ chatIdentifier: handle,
3678
+ isGroup: false,
3679
+ sessionKey: friendId,
3680
+ sendTarget: { kind: "chat_identifier", value: handle },
3681
+ participantHandles: [],
3682
+ };
3683
+ try {
3684
+ await client.sendText({ chat, text: messageText });
3685
+ result.sent++;
3686
+ try {
3687
+ fs.unlinkSync(filePath);
3688
+ }
3689
+ catch { /* ignore */ }
3690
+ (0, runtime_1.emitNervesEvent)({
3691
+ component: "senses",
3692
+ event: "senses.bluebubbles_proactive_sent",
3693
+ message: "proactive bluebubbles message sent",
3694
+ meta: { friendId, handle },
3695
+ });
3696
+ }
3697
+ catch (error) {
3698
+ result.failed++;
3699
+ (0, runtime_1.emitNervesEvent)({
3700
+ level: "error",
3701
+ component: "senses",
3702
+ event: "senses.bluebubbles_proactive_send_error",
3703
+ message: "proactive bluebubbles send failed",
3704
+ meta: {
3705
+ friendId,
3706
+ handle,
3707
+ reason: error instanceof Error ? error.message : String(error),
3708
+ },
3709
+ });
3710
+ }
3711
+ }
3712
+ if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
3713
+ (0, runtime_1.emitNervesEvent)({
3714
+ component: "senses",
3715
+ event: "senses.bluebubbles_proactive_drain_complete",
3716
+ message: "bluebubbles proactive drain complete",
3717
+ meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
3718
+ });
3719
+ }
3720
+ return result;
3721
+ }
3722
+ function startBlueBubblesApp(deps = {}) {
3723
+ const resolvedDeps = { ...defaultDeps, ...deps };
3724
+ const lifecycleController = new AbortController();
3725
+ (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(resolvedDeps.getAgentName());
3726
+ resolvedDeps.createClient();
3727
+ seedBlueBubblesStartupSemanticObservations(resolvedDeps);
3728
+ const startupCatchUpObservationBatch = (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
3729
+ const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
3730
+ const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps, {
3731
+ lifecycleSignal: lifecycleController.signal,
3732
+ }));
3733
+ let recoveryPassRunning = false;
3734
+ let runtimeSyncRunning = false;
3735
+ let recoveryDelayTimer = null;
3736
+ let webhookReconciler = null;
3737
+ let closed = false;
3738
+ function triggerRecoveryPass() {
3739
+ /* v8 ignore next -- close clears both recovery timers; guard covers an already-queued host callback @preserve */
3740
+ if (closed)
3741
+ return;
3742
+ /* v8 ignore next -- re-entrant timer guard; difficult to force deterministically without timing the turn lock @preserve */
3743
+ if (recoveryPassRunning)
3744
+ return;
3745
+ recoveryPassRunning = true;
3746
+ void recoverQueuedBlueBubblesMessages(resolvedDeps, {
3747
+ lifecycleSignal: lifecycleController.signal,
3748
+ })
3749
+ /* v8 ignore start -- defensive wrapper; expected per-message failures are handled inside recovery helpers @preserve */
3750
+ .catch((error) => {
3751
+ if (closed || lifecycleController.signal.aborted)
3752
+ return;
3753
+ (0, runtime_1.emitNervesEvent)({
3754
+ level: "warn",
3755
+ component: "senses",
3756
+ event: "senses.bluebubbles_recovery_error",
3757
+ message: "bluebubbles queued recovery pass failed",
3758
+ meta: { reason: error instanceof Error ? error.message : String(error) },
3759
+ });
3760
+ })
3761
+ /* v8 ignore stop */
3762
+ .finally(() => {
3763
+ recoveryPassRunning = false;
3764
+ });
3765
+ }
3766
+ function scheduleRecoveryPass() {
3767
+ if (closed)
3768
+ return;
3769
+ /* v8 ignore next -- duplicate scheduling guard for overlapping health sync completions @preserve */
3770
+ if (recoveryDelayTimer !== null)
3771
+ return;
3772
+ recoveryDelayTimer = setTimeout(() => {
3773
+ recoveryDelayTimer = null;
3774
+ triggerRecoveryPass();
3775
+ }, BLUEBUBBLES_RECOVERY_PASS_DELAY_MS);
3776
+ }
3777
+ function triggerRuntimeSync(catchUpObservationBatch) {
3778
+ if (closed || runtimeSyncRunning)
3779
+ return;
3780
+ runtimeSyncRunning = true;
3781
+ void syncBlueBubblesRuntime(resolvedDeps, {
3782
+ ...(catchUpObservationBatch ? { catchUpObservationBatch } : {}),
3783
+ lifecycleSignal: lifecycleController.signal,
3784
+ })
3785
+ .then(scheduleRecoveryPass)
3786
+ .catch((error) => {
3787
+ if (closed)
3788
+ return;
3789
+ (0, runtime_1.emitNervesEvent)({
3790
+ level: "warn",
3791
+ component: "senses",
3792
+ event: "senses.bluebubbles_recovery_error",
3793
+ message: "bluebubbles runtime sync failed",
3794
+ meta: { reason: error instanceof Error ? error.message : String(error) },
3795
+ });
3796
+ })
3797
+ .finally(() => {
3798
+ runtimeSyncRunning = false;
3799
+ });
3800
+ }
3801
+ const runtimeTimer = setInterval(() => {
3802
+ /* v8 ignore next -- close clears this interval; guard covers an already-queued host callback @preserve */
3803
+ if (closed)
3804
+ return;
3805
+ triggerRuntimeSync();
3806
+ }, BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS);
3807
+ const recoveryTimer = setInterval(triggerRecoveryPass, BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS);
3808
+ server.on?.("close", () => {
3809
+ closed = true;
3810
+ webhookReconciler?.close();
3811
+ webhookReconciler = null;
3812
+ lifecycleController.abort(new Error("bluebubbles_runtime_shutdown"));
3813
+ clearInterval(runtimeTimer);
3814
+ clearInterval(recoveryTimer);
3815
+ if (recoveryDelayTimer !== null) {
3816
+ clearTimeout(recoveryDelayTimer);
3817
+ recoveryDelayTimer = null;
3818
+ }
3819
+ });
3820
+ server.listen(channelConfig.port, () => {
3821
+ const blueBubblesConfig = (0, config_1.getBlueBubblesConfig)();
3822
+ webhookReconciler = (0, webhook_registration_1.createBlueBubblesWebhookReconciler)({
3823
+ serverUrl: blueBubblesConfig.serverUrl,
3824
+ password: blueBubblesConfig.password,
3825
+ callbackPort: channelConfig.port,
3826
+ callbackPath: channelConfig.webhookPath,
3827
+ agentName: resolvedDeps.getAgentName(),
3828
+ machineId: (0, machine_identity_1.loadOrCreateMachineIdentity)().machineId,
3829
+ requestTimeoutMs: channelConfig.requestTimeoutMs,
3830
+ listenerReady: true,
3831
+ });
3832
+ (0, runtime_1.emitNervesEvent)({
3833
+ component: "channels",
3834
+ event: "channel.app_started",
3835
+ message: "BlueBubbles sense started",
3836
+ meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
3837
+ });
3838
+ });
3839
+ triggerRuntimeSync(startupCatchUpObservationBatch);
3840
+ return server;
3841
+ }