@ouro.bot/cli 0.1.0-alpha.48 → 0.1.0-alpha.481

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 (358) hide show
  1. package/README.md +132 -19
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
  4. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
  5. package/changelog.json +3069 -0
  6. package/dist/arc/attention-types.js +8 -0
  7. package/dist/arc/cares.js +140 -0
  8. package/dist/arc/episodes.js +117 -0
  9. package/dist/arc/intentions.js +133 -0
  10. package/dist/arc/json-store.js +117 -0
  11. package/dist/arc/obligations.js +237 -0
  12. package/dist/arc/packets.js +193 -0
  13. package/dist/arc/presence.js +185 -0
  14. package/dist/arc/task-lifecycle.js +65 -0
  15. package/dist/heart/active-work.js +857 -0
  16. package/dist/heart/agent-entry.js +58 -3
  17. package/dist/heart/attachments/image-normalize.js +194 -0
  18. package/dist/heart/attachments/materialize.js +97 -0
  19. package/dist/heart/attachments/originals.js +88 -0
  20. package/dist/heart/attachments/render.js +29 -0
  21. package/dist/heart/attachments/sources/adapter.js +2 -0
  22. package/dist/heart/attachments/sources/bluebubbles.js +156 -0
  23. package/dist/heart/attachments/sources/cli-local-file.js +78 -0
  24. package/dist/heart/attachments/sources/index.js +16 -0
  25. package/dist/heart/attachments/store.js +103 -0
  26. package/dist/heart/attachments/types.js +93 -0
  27. package/dist/heart/auth/auth-flow.js +426 -0
  28. package/dist/heart/background-operations.js +234 -0
  29. package/dist/heart/bridges/manager.js +358 -0
  30. package/dist/heart/bridges/state-machine.js +135 -0
  31. package/dist/heart/bridges/store.js +123 -0
  32. package/dist/heart/bundle-state.js +168 -0
  33. package/dist/heart/commitments.js +111 -0
  34. package/dist/heart/config-registry.js +304 -0
  35. package/dist/heart/config.js +110 -128
  36. package/dist/heart/core.js +745 -227
  37. package/dist/heart/cross-chat-delivery.js +131 -0
  38. package/dist/heart/daemon/agent-config-check.js +490 -0
  39. package/dist/heart/daemon/agent-discovery.js +79 -3
  40. package/dist/heart/daemon/agent-service.js +360 -0
  41. package/dist/heart/daemon/agentic-repair.js +216 -0
  42. package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
  43. package/dist/heart/daemon/cadence.js +70 -0
  44. package/dist/heart/daemon/cli-defaults.js +640 -0
  45. package/dist/heart/daemon/cli-exec.js +6933 -0
  46. package/dist/heart/daemon/cli-help.js +487 -0
  47. package/dist/heart/daemon/cli-parse.js +1527 -0
  48. package/dist/heart/daemon/cli-render-doctor.js +57 -0
  49. package/dist/heart/daemon/cli-render.js +561 -0
  50. package/dist/heart/daemon/cli-types.js +8 -0
  51. package/dist/heart/daemon/connect-bay.js +323 -0
  52. package/dist/heart/daemon/daemon-cli.js +29 -1616
  53. package/dist/heart/daemon/daemon-entry.js +345 -3
  54. package/dist/heart/daemon/daemon-health.js +141 -0
  55. package/dist/heart/daemon/daemon-runtime-sync.js +190 -12
  56. package/dist/heart/daemon/daemon-tombstone.js +236 -0
  57. package/dist/heart/daemon/daemon.js +677 -58
  58. package/dist/heart/daemon/dns-workflow.js +394 -0
  59. package/dist/heart/daemon/doctor-types.js +8 -0
  60. package/dist/heart/daemon/doctor.js +486 -0
  61. package/dist/heart/daemon/health-monitor.js +92 -1
  62. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  63. package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
  64. package/dist/heart/daemon/http-health-probe.js +80 -0
  65. package/dist/heart/daemon/human-command-screens.js +234 -0
  66. package/dist/heart/daemon/human-readiness.js +114 -0
  67. package/dist/heart/daemon/inner-status.js +89 -0
  68. package/dist/heart/daemon/interactive-repair.js +394 -0
  69. package/dist/heart/daemon/launchd.js +25 -5
  70. package/dist/heart/daemon/log-tailer.js +82 -12
  71. package/dist/heart/daemon/logs-prune.js +110 -0
  72. package/dist/heart/daemon/message-router.js +2 -2
  73. package/dist/heart/daemon/os-cron-deps.js +134 -0
  74. package/dist/heart/daemon/ouro-bot-entry.js +4 -2
  75. package/dist/heart/daemon/ouro-entry.js +3 -1
  76. package/dist/heart/daemon/process-manager.js +214 -0
  77. package/dist/heart/daemon/provider-discovery.js +137 -0
  78. package/dist/heart/daemon/provider-ping-progress.js +83 -0
  79. package/dist/heart/daemon/pulse.js +475 -0
  80. package/dist/heart/daemon/readiness-repair.js +365 -0
  81. package/dist/heart/daemon/run-hooks.js +2 -0
  82. package/dist/heart/daemon/runtime-logging.js +67 -16
  83. package/dist/heart/daemon/runtime-metadata.js +73 -0
  84. package/dist/heart/daemon/runtime-mode.js +67 -0
  85. package/dist/heart/daemon/safe-mode.js +161 -0
  86. package/dist/heart/daemon/sense-manager.js +178 -37
  87. package/dist/heart/daemon/session-id-resolver.js +131 -0
  88. package/dist/heart/daemon/skill-management-installer.js +94 -0
  89. package/dist/heart/daemon/socket-client.js +109 -4
  90. package/dist/heart/daemon/stale-bundle-prune.js +96 -0
  91. package/dist/heart/daemon/startup-tui.js +264 -0
  92. package/dist/heart/daemon/task-scheduler.js +3 -25
  93. package/dist/heart/daemon/terminal-ui.js +499 -0
  94. package/dist/heart/daemon/thoughts.js +149 -10
  95. package/dist/heart/daemon/up-progress.js +366 -0
  96. package/dist/heart/daemon/vault-items.js +56 -0
  97. package/dist/heart/delegation.js +62 -0
  98. package/dist/heart/habits/habit-migration.js +189 -0
  99. package/dist/heart/habits/habit-parser.js +140 -0
  100. package/dist/heart/habits/habit-runtime-state.js +100 -0
  101. package/dist/heart/habits/habit-scheduler.js +372 -0
  102. package/dist/heart/{daemon → hatch}/hatch-flow.js +52 -117
  103. package/dist/heart/{daemon → hatch}/hatch-specialist.js +3 -3
  104. package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
  105. package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
  106. package/dist/heart/identity.js +201 -66
  107. package/dist/heart/kept-notes.js +357 -0
  108. package/dist/heart/kicks.js +1 -1
  109. package/dist/heart/machine-identity.js +161 -0
  110. package/dist/heart/mcp/mcp-server.js +653 -0
  111. package/dist/heart/migrate-config.js +100 -0
  112. package/dist/heart/model-capabilities.js +59 -0
  113. package/dist/heart/outlook/outlook-http-hooks.js +66 -0
  114. package/dist/heart/outlook/outlook-http-response.js +7 -0
  115. package/dist/heart/outlook/outlook-http-routes.js +244 -0
  116. package/dist/heart/outlook/outlook-http-static.js +99 -0
  117. package/dist/heart/outlook/outlook-http-transport.js +116 -0
  118. package/dist/heart/outlook/outlook-http.js +99 -0
  119. package/dist/heart/outlook/outlook-read.js +31 -0
  120. package/dist/heart/outlook/outlook-types.js +27 -0
  121. package/dist/heart/outlook/outlook-view.js +195 -0
  122. package/dist/heart/outlook/readers/agent-machine.js +359 -0
  123. package/dist/heart/outlook/readers/continuity-readers.js +332 -0
  124. package/dist/heart/outlook/readers/mail.js +362 -0
  125. package/dist/heart/outlook/readers/runtime-readers.js +644 -0
  126. package/dist/heart/outlook/readers/sessions.js +232 -0
  127. package/dist/heart/outlook/readers/shared.js +111 -0
  128. package/dist/heart/platform.js +81 -0
  129. package/dist/heart/progress-story.js +42 -0
  130. package/dist/heart/provider-attempt.js +134 -0
  131. package/dist/heart/provider-binding-resolver.js +255 -0
  132. package/dist/heart/provider-credentials.js +424 -0
  133. package/dist/heart/provider-failover.js +266 -0
  134. package/dist/heart/provider-models.js +81 -0
  135. package/dist/heart/provider-ping.js +262 -0
  136. package/dist/heart/provider-state.js +216 -0
  137. package/dist/heart/provider-visibility.js +188 -0
  138. package/dist/heart/providers/anthropic-token.js +131 -0
  139. package/dist/heart/providers/anthropic.js +193 -55
  140. package/dist/heart/providers/azure.js +103 -12
  141. package/dist/heart/providers/error-classification.js +63 -0
  142. package/dist/heart/providers/github-copilot.js +145 -0
  143. package/dist/heart/providers/minimax-vlm.js +189 -0
  144. package/dist/heart/providers/minimax.js +29 -7
  145. package/dist/heart/providers/openai-codex.js +62 -38
  146. package/dist/heart/runtime-capability-check.js +170 -0
  147. package/dist/heart/runtime-credentials.js +260 -0
  148. package/dist/heart/sense-truth.js +11 -4
  149. package/dist/heart/session-activity.js +190 -0
  150. package/dist/heart/session-events.js +855 -0
  151. package/dist/heart/session-transcript.js +167 -0
  152. package/dist/heart/start-of-turn-packet.js +345 -0
  153. package/dist/heart/streaming.js +36 -27
  154. package/dist/heart/sync.js +332 -0
  155. package/dist/heart/target-resolution.js +127 -0
  156. package/dist/heart/tempo.js +93 -0
  157. package/dist/heart/temporal-view.js +41 -0
  158. package/dist/heart/tool-activity-callbacks.js +36 -0
  159. package/dist/heart/tool-description.js +135 -0
  160. package/dist/heart/tool-friction.js +55 -0
  161. package/dist/heart/tool-loop.js +200 -0
  162. package/dist/heart/turn-context.js +361 -0
  163. package/dist/heart/turn-coordinator.js +24 -1
  164. package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +1 -1
  165. package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
  166. package/dist/heart/versioning/ouro-path-installer.js +425 -0
  167. package/dist/heart/versioning/ouro-version-manager.js +295 -0
  168. package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
  169. package/dist/heart/{daemon → versioning}/update-checker.js +5 -1
  170. package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
  171. package/dist/mailroom/attention.js +167 -0
  172. package/dist/mailroom/autonomy.js +209 -0
  173. package/dist/mailroom/blob-store.js +573 -0
  174. package/dist/mailroom/core.js +658 -0
  175. package/dist/mailroom/entry.js +160 -0
  176. package/dist/mailroom/file-store.js +400 -0
  177. package/dist/mailroom/mbox-import.js +341 -0
  178. package/dist/mailroom/outbound.js +380 -0
  179. package/dist/mailroom/policy.js +263 -0
  180. package/dist/mailroom/reader.js +197 -0
  181. package/dist/mailroom/smtp-ingress.js +176 -0
  182. package/dist/mailroom/source-state.js +176 -0
  183. package/dist/mailroom/travel-extract.js +89 -0
  184. package/dist/mind/bundle-manifest.js +7 -1
  185. package/dist/mind/context.js +132 -93
  186. package/dist/mind/diary-integrity.js +60 -0
  187. package/dist/mind/{memory.js → diary.js} +74 -93
  188. package/dist/mind/embedding-provider.js +60 -0
  189. package/dist/mind/file-state.js +179 -0
  190. package/dist/mind/friends/channel.js +30 -0
  191. package/dist/mind/friends/group-context.js +144 -0
  192. package/dist/mind/friends/resolver.js +38 -1
  193. package/dist/mind/friends/store-file.js +39 -3
  194. package/dist/mind/friends/trust-explanation.js +74 -0
  195. package/dist/mind/friends/types.js +2 -2
  196. package/dist/mind/journal-index.js +161 -0
  197. package/dist/mind/note-search.js +268 -0
  198. package/dist/mind/obligation-steering.js +221 -0
  199. package/dist/mind/pending.js +66 -7
  200. package/dist/mind/prompt-refresh.js +3 -2
  201. package/dist/mind/prompt.js +978 -169
  202. package/dist/mind/provenance-trust.js +26 -0
  203. package/dist/mind/scrutiny.js +173 -0
  204. package/dist/nerves/cli-logging.js +7 -1
  205. package/dist/nerves/coverage/audit-rules.js +15 -6
  206. package/dist/nerves/coverage/audit.js +28 -2
  207. package/dist/nerves/coverage/cli.js +1 -1
  208. package/dist/nerves/coverage/contract.js +5 -5
  209. package/dist/nerves/coverage/file-completeness.js +84 -5
  210. package/dist/nerves/coverage/run-artifacts.js +1 -1
  211. package/dist/nerves/event-buffer.js +111 -0
  212. package/dist/nerves/index.js +224 -4
  213. package/dist/nerves/observation.js +20 -0
  214. package/dist/nerves/redact.js +79 -0
  215. package/dist/nerves/runtime.js +5 -1
  216. package/dist/outlook-ui/assets/index-BPr5vNuM.css +1 -0
  217. package/dist/outlook-ui/assets/index-CPfhbn13.js +61 -0
  218. package/dist/outlook-ui/index.html +15 -0
  219. package/dist/repertoire/ado-client.js +15 -56
  220. package/dist/repertoire/ado-semantic.js +11 -10
  221. package/dist/repertoire/api-client.js +97 -0
  222. package/dist/repertoire/bitwarden-store.js +774 -0
  223. package/dist/repertoire/bundle-templates.js +72 -0
  224. package/dist/repertoire/bw-installer.js +180 -0
  225. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  226. package/dist/repertoire/coding/context-pack.js +330 -0
  227. package/dist/repertoire/coding/feedback.js +197 -30
  228. package/dist/repertoire/coding/manager.js +158 -9
  229. package/dist/repertoire/coding/spawner.js +55 -9
  230. package/dist/repertoire/coding/tools.js +170 -7
  231. package/dist/repertoire/commerce-errors.js +109 -0
  232. package/dist/repertoire/commerce-self-test.js +156 -0
  233. package/dist/repertoire/credential-access.js +111 -0
  234. package/dist/repertoire/duffel-client.js +185 -0
  235. package/dist/repertoire/github-client.js +14 -55
  236. package/dist/repertoire/graph-client.js +11 -52
  237. package/dist/repertoire/guardrails.js +396 -0
  238. package/dist/repertoire/mcp-client.js +255 -0
  239. package/dist/repertoire/mcp-manager.js +305 -0
  240. package/dist/repertoire/mcp-tools.js +63 -0
  241. package/dist/repertoire/shell-sessions.js +133 -0
  242. package/dist/repertoire/skills.js +15 -24
  243. package/dist/repertoire/stripe-client.js +131 -0
  244. package/dist/repertoire/tasks/board.js +43 -5
  245. package/dist/repertoire/tasks/fix.js +182 -0
  246. package/dist/repertoire/tasks/index.js +37 -4
  247. package/dist/repertoire/tasks/lifecycle.js +2 -2
  248. package/dist/repertoire/tasks/parser.js +3 -2
  249. package/dist/repertoire/tasks/scanner.js +194 -37
  250. package/dist/repertoire/tasks/transitions.js +16 -78
  251. package/dist/repertoire/tool-results.js +29 -0
  252. package/dist/repertoire/tools-attachments.js +317 -0
  253. package/dist/repertoire/tools-base.js +44 -740
  254. package/dist/repertoire/tools-bluebubbles.js +1 -0
  255. package/dist/repertoire/tools-bridge.js +141 -0
  256. package/dist/repertoire/tools-bundle.js +984 -0
  257. package/dist/repertoire/tools-config.js +185 -0
  258. package/dist/repertoire/tools-continuity.js +248 -0
  259. package/dist/repertoire/tools-credential.js +381 -0
  260. package/dist/repertoire/tools-files.js +342 -0
  261. package/dist/repertoire/tools-flight.js +224 -0
  262. package/dist/repertoire/tools-flow.js +105 -0
  263. package/dist/repertoire/tools-github.js +1 -7
  264. package/dist/repertoire/tools-mail.js +896 -0
  265. package/dist/repertoire/tools-notes.js +376 -0
  266. package/dist/repertoire/tools-session.js +746 -0
  267. package/dist/repertoire/tools-shell.js +120 -0
  268. package/dist/repertoire/tools-stripe.js +180 -0
  269. package/dist/repertoire/tools-surface.js +243 -0
  270. package/dist/repertoire/tools-teams.js +9 -39
  271. package/dist/repertoire/tools-travel.js +125 -0
  272. package/dist/repertoire/tools-user-profile.js +144 -0
  273. package/dist/repertoire/tools-vault.js +40 -0
  274. package/dist/repertoire/tools.js +144 -113
  275. package/dist/repertoire/travel-api-client.js +360 -0
  276. package/dist/repertoire/user-profile.js +131 -0
  277. package/dist/repertoire/vault-setup.js +246 -0
  278. package/dist/repertoire/vault-unlock.js +561 -0
  279. package/dist/scripts/claude-code-hook.js +41 -0
  280. package/dist/scripts/claude-code-stop-hook.js +47 -0
  281. package/dist/senses/attention-queue.js +116 -0
  282. package/dist/senses/bluebubbles/attachment-cache.js +53 -0
  283. package/dist/senses/bluebubbles/attachment-download.js +137 -0
  284. package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +219 -18
  285. package/dist/senses/bluebubbles/entry.js +73 -0
  286. package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +7 -3
  287. package/dist/senses/{bluebubbles.js → bluebubbles/index.js} +705 -116
  288. package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
  289. package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
  290. package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +3 -3
  291. package/dist/senses/bluebubbles/replay.js +129 -0
  292. package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +2 -2
  293. package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
  294. package/dist/senses/cli/bracketed-paste.js +82 -0
  295. package/dist/senses/cli/image-paste.js +287 -0
  296. package/dist/senses/cli/image-ref-navigation.js +75 -0
  297. package/dist/senses/cli/ink-app.js +156 -0
  298. package/dist/senses/cli/inline-diff.js +64 -0
  299. package/dist/senses/cli/input-keys.js +174 -0
  300. package/dist/senses/cli/kill-ring.js +86 -0
  301. package/dist/senses/cli/message-list.js +51 -0
  302. package/dist/senses/cli/ouro-tui.js +605 -0
  303. package/dist/senses/cli/spinner-imperative.js +135 -0
  304. package/dist/senses/cli/spinner.js +101 -0
  305. package/dist/senses/cli/status-line.js +60 -0
  306. package/dist/senses/cli/streaming-markdown.js +526 -0
  307. package/dist/senses/cli/tool-display.js +83 -0
  308. package/dist/senses/cli/tool-render.js +85 -0
  309. package/dist/senses/cli/tui-store.js +240 -0
  310. package/dist/senses/cli/virtual-list.js +35 -0
  311. package/dist/senses/cli-entry.js +60 -8
  312. package/dist/senses/cli-layout.js +187 -0
  313. package/dist/senses/cli.js +516 -211
  314. package/dist/senses/commands.js +66 -3
  315. package/dist/senses/habit-turn-message.js +108 -0
  316. package/dist/senses/inner-dialog-worker.js +97 -17
  317. package/dist/senses/inner-dialog.js +404 -14
  318. package/dist/senses/mail-entry.js +66 -0
  319. package/dist/senses/mail.js +232 -0
  320. package/dist/senses/pipeline.js +533 -72
  321. package/dist/senses/proactive-content-guard.js +51 -0
  322. package/dist/senses/shared-turn.js +205 -0
  323. package/dist/senses/surface-tool.js +68 -0
  324. package/dist/senses/teams-entry.js +60 -8
  325. package/dist/senses/teams.js +413 -163
  326. package/dist/senses/trust-gate.js +5 -5
  327. package/package.json +37 -7
  328. package/skills/agent-commerce.md +106 -0
  329. package/skills/browser-navigation.md +117 -0
  330. package/skills/commerce-setup-guide.md +116 -0
  331. package/skills/commerce-setup.md +84 -0
  332. package/skills/configure-dev-tools.md +101 -0
  333. package/skills/travel-planning.md +138 -0
  334. package/dist/heart/daemon/ouro-path-installer.js +0 -178
  335. package/dist/heart/daemon/subagent-installer.js +0 -166
  336. package/dist/mind/associative-recall.js +0 -209
  337. package/dist/senses/bluebubbles-entry.js +0 -13
  338. package/dist/senses/debug-activity.js +0 -127
  339. package/subagents/README.md +0 -86
  340. package/subagents/work-doer.md +0 -237
  341. package/subagents/work-merger.md +0 -618
  342. package/subagents/work-planner.md +0 -390
  343. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  344. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  345. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  346. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  347. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  348. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  349. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  350. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  351. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  352. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  353. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  354. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
  355. /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
  356. /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
  357. /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
  358. /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
@@ -33,52 +33,116 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.enrichReactionText = enrichReactionText;
37
+ exports.createStatusBatcher = createStatusBatcher;
36
38
  exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
39
+ exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
37
40
  exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
38
41
  exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
42
+ exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
39
43
  exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
40
44
  exports.startBlueBubblesApp = startBlueBubblesApp;
41
45
  const fs = __importStar(require("node:fs"));
42
46
  const http = __importStar(require("node:http"));
43
47
  const path = __importStar(require("node:path"));
44
- const core_1 = require("../heart/core");
45
- const config_1 = require("../heart/config");
46
- const identity_1 = require("../heart/identity");
47
- const turn_coordinator_1 = require("../heart/turn-coordinator");
48
- const context_1 = require("../mind/context");
49
- const tokens_1 = require("../mind/friends/tokens");
50
- const resolver_1 = require("../mind/friends/resolver");
51
- const store_file_1 = require("../mind/friends/store-file");
52
- const types_1 = require("../mind/friends/types");
53
- const channel_1 = require("../mind/friends/channel");
54
- const pending_1 = require("../mind/pending");
55
- const prompt_1 = require("../mind/prompt");
56
- const phrases_1 = require("../mind/phrases");
57
- const runtime_1 = require("../nerves/runtime");
58
- const bluebubbles_model_1 = require("./bluebubbles-model");
59
- const bluebubbles_client_1 = require("./bluebubbles-client");
60
- const bluebubbles_inbound_log_1 = require("./bluebubbles-inbound-log");
61
- const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
62
- const bluebubbles_runtime_state_1 = require("./bluebubbles-runtime-state");
63
- const bluebubbles_session_cleanup_1 = require("./bluebubbles-session-cleanup");
64
- const debug_activity_1 = require("./debug-activity");
65
- const trust_gate_1 = require("./trust-gate");
66
- const pipeline_1 = require("./pipeline");
48
+ const core_1 = require("../../heart/core");
49
+ const config_1 = require("../../heart/config");
50
+ const identity_1 = require("../../heart/identity");
51
+ const turn_coordinator_1 = require("../../heart/turn-coordinator");
52
+ const context_1 = require("../../mind/context");
53
+ const tokens_1 = require("../../mind/friends/tokens");
54
+ const group_context_1 = require("../../mind/friends/group-context");
55
+ const resolver_1 = require("../../mind/friends/resolver");
56
+ const store_file_1 = require("../../mind/friends/store-file");
57
+ const types_1 = require("../../mind/friends/types");
58
+ const channel_1 = require("../../mind/friends/channel");
59
+ const pending_1 = require("../../mind/pending");
60
+ const prompt_1 = require("../../mind/prompt");
61
+ const mcp_manager_1 = require("../../repertoire/mcp-manager");
62
+ // getPhrases removed — no longer needed after debug-activity cleanup
63
+ const runtime_1 = require("../../nerves/runtime");
64
+ const proactive_content_guard_1 = require("../proactive-content-guard");
65
+ const model_1 = require("./model");
66
+ const client_1 = require("./client");
67
+ const inbound_log_1 = require("./inbound-log");
68
+ const mutation_log_1 = require("./mutation-log");
69
+ const runtime_state_1 = require("./runtime-state");
70
+ const session_cleanup_1 = require("./session-cleanup");
71
+ const tool_activity_callbacks_1 = require("../../heart/tool-activity-callbacks");
72
+ const commands_1 = require("../commands");
73
+ const trust_gate_1 = require("../trust-gate");
74
+ const pipeline_1 = require("../pipeline");
75
+ const bbFailoverStates = new Map();
76
+ // Enrich reaction text with the original message content for context.
77
+ // If originalText is provided and non-empty, format as: baseText to: "truncated"
78
+ // Otherwise return baseText unchanged.
79
+ function enrichReactionText(baseText, originalText, maxLen) {
80
+ if (!originalText)
81
+ return baseText;
82
+ const truncated = originalText.length > maxLen
83
+ ? originalText.slice(0, maxLen - 3) + "..."
84
+ : originalText;
85
+ return `${baseText} to: "${truncated}"`;
86
+ }
87
+ /**
88
+ * Accumulates status descriptions and debounces them.
89
+ * If multiple descriptions arrive within `delayMs`, they are joined with ` · `
90
+ * and sent as a single message. Flush sends immediately and clears the timer.
91
+ */
92
+ function createStatusBatcher(send, delayMs) {
93
+ (0, runtime_1.emitNervesEvent)({
94
+ component: "senses",
95
+ event: "senses.bluebubbles_status_batcher_created",
96
+ message: "status batcher initialized",
97
+ meta: { delayMs },
98
+ });
99
+ let pending = [];
100
+ let timer = null;
101
+ function fire() {
102
+ if (pending.length === 0)
103
+ return;
104
+ const combined = pending.join(" \u00b7 ");
105
+ pending = [];
106
+ timer = null;
107
+ send(combined);
108
+ }
109
+ return {
110
+ add(text) {
111
+ pending.push(text);
112
+ if (timer !== null)
113
+ clearTimeout(timer);
114
+ timer = setTimeout(fire, delayMs);
115
+ },
116
+ flush() {
117
+ if (timer !== null) {
118
+ clearTimeout(timer);
119
+ timer = null;
120
+ }
121
+ fire();
122
+ },
123
+ };
124
+ }
67
125
  const defaultDeps = {
68
126
  getAgentName: identity_1.getAgentName,
69
127
  buildSystem: prompt_1.buildSystem,
70
128
  runAgent: core_1.runAgent,
71
129
  loadSession: context_1.loadSession,
72
- postTurn: context_1.postTurn,
130
+ postTurnTrim: context_1.postTurnTrim,
131
+ deferPostTurnPersist: context_1.deferPostTurnPersist,
73
132
  sessionPath: config_1.sessionPath,
74
133
  accumulateFriendTokens: tokens_1.accumulateFriendTokens,
75
- createClient: () => (0, bluebubbles_client_1.createBlueBubblesClient)(),
76
- recordMutation: bluebubbles_mutation_log_1.recordBlueBubblesMutation,
134
+ createClient: () => (0, client_1.createBlueBubblesClient)(),
135
+ recordMutation: mutation_log_1.recordBlueBubblesMutation,
77
136
  createFriendStore: () => new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends")),
78
137
  createFriendResolver: (store, params) => new resolver_1.FriendResolver(store, params),
79
138
  createServer: http.createServer,
80
139
  };
81
140
  const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
141
+ const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
142
+ const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
143
+ const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
144
+ const BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS = 24 * 60 * 60 * 1000;
145
+ const BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS = 10 * 60 * 1000;
82
146
  function resolveFriendParams(event) {
83
147
  if (event.chat.isGroup) {
84
148
  const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
@@ -96,6 +160,10 @@ function resolveFriendParams(event) {
96
160
  channel: "bluebubbles",
97
161
  };
98
162
  }
163
+ function resolveGroupExternalId(event) {
164
+ const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
165
+ return `group:${groupKey}`;
166
+ }
99
167
  /**
100
168
  * Check if any participant in a group chat is a known family member.
101
169
  * Looks up each participant handle in the friend store.
@@ -192,7 +260,7 @@ function extractHistoricalLaneSummary(messages) {
192
260
  }
193
261
  return summaries;
194
262
  }
195
- function buildConversationScopePrefix(event, existingMessages) {
263
+ function buildConversationScopePrefix(event, existingMessages, repliedToText) {
196
264
  if (event.kind !== "message") {
197
265
  return "";
198
266
  }
@@ -200,6 +268,10 @@ function buildConversationScopePrefix(event, existingMessages) {
200
268
  const lines = [];
201
269
  if (event.threadOriginatorGuid?.trim()) {
202
270
  lines.push(`[conversation scope: existing chat trunk | current inbound lane: thread | current thread id: ${event.threadOriginatorGuid.trim()} | default outbound target for this turn: current_lane]`);
271
+ if (repliedToText) {
272
+ lines.push(`[replying to: "${repliedToText}"]`);
273
+ }
274
+ lines.push(`[if you need more context about what was being discussed, use query_session to search your session history, or search_notes to search diary/journal notes.]`);
203
275
  }
204
276
  else {
205
277
  lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target for this turn: top_level]");
@@ -215,8 +287,8 @@ function buildConversationScopePrefix(event, existingMessages) {
215
287
  }
216
288
  return lines.join("\n");
217
289
  }
218
- function buildInboundText(event, existingMessages) {
219
- const metadataPrefix = buildConversationScopePrefix(event, existingMessages);
290
+ function buildInboundText(event, existingMessages, repliedToText) {
291
+ const metadataPrefix = buildConversationScopePrefix(event, existingMessages, repliedToText);
220
292
  const baseText = event.repairNotice?.trim()
221
293
  ? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
222
294
  : event.textForAgent;
@@ -229,8 +301,8 @@ function buildInboundText(event, existingMessages) {
229
301
  }
230
302
  return `${event.sender.displayName}: ${scopedText}`;
231
303
  }
232
- function buildInboundContent(event, existingMessages) {
233
- const text = buildInboundText(event, existingMessages);
304
+ function buildInboundContent(event, existingMessages, repliedToText) {
305
+ const text = buildInboundText(event, existingMessages, repliedToText);
234
306
  if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
235
307
  return text;
236
308
  }
@@ -336,64 +408,63 @@ function emitBlueBubblesMarkReadWarning(chat, error) {
336
408
  },
337
409
  });
338
410
  }
339
- function createBlueBubblesCallbacks(client, chat, replyTarget) {
411
+ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat) {
340
412
  let textBuffer = "";
341
- const phrases = (0, phrases_1.getPhrases)();
342
- const activity = (0, debug_activity_1.createDebugActivityController)({
343
- thinkingPhrases: phrases.thinking,
344
- followupPhrases: phrases.followup,
345
- startTypingOnModelStart: true,
346
- suppressInitialModelStatus: true,
347
- suppressFollowupPhraseStatus: true,
348
- transport: {
349
- sendStatus: async (text) => {
350
- const sent = await client.sendText({
351
- chat,
352
- text,
353
- replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
354
- });
355
- return sent.messageGuid;
356
- },
357
- editStatus: async (_messageGuid, text) => {
358
- await client.sendText({
359
- chat,
360
- text,
361
- replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
362
- });
363
- },
364
- setTyping: async (active) => {
365
- if (!active) {
366
- await client.setTyping(chat, false);
367
- return;
368
- }
369
- const [markReadResult, typingResult] = await Promise.allSettled([
370
- client.markChatRead(chat),
371
- client.setTyping(chat, true),
372
- ]);
373
- if (markReadResult.status === "rejected") {
374
- emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
375
- }
376
- if (typingResult.status === "rejected") {
377
- throw typingResult.reason;
378
- }
379
- },
380
- },
381
- onTransportError: (operation, error) => {
413
+ let typingActive = false;
414
+ let queue = Promise.resolve();
415
+ function enqueue(operation, task) {
416
+ queue = queue.then(task).catch((error) => {
382
417
  (0, runtime_1.emitNervesEvent)({
383
418
  level: "warn",
384
419
  component: "senses",
385
420
  event: "senses.bluebubbles_activity_error",
386
421
  message: "bluebubbles activity transport failed",
387
- meta: {
388
- operation,
389
- reason: error instanceof Error ? error.message : String(error),
390
- },
422
+ meta: { operation, reason: error instanceof Error ? error.message : String(error) },
391
423
  });
392
- },
424
+ });
425
+ }
426
+ function startTypingNow() {
427
+ /* v8 ignore next -- defensive guard: callers already check typingActive @preserve */
428
+ if (typingActive)
429
+ return;
430
+ typingActive = true;
431
+ enqueue("typing_start", async () => {
432
+ const [markReadResult, typingResult] = await Promise.allSettled([
433
+ client.markChatRead(chat),
434
+ client.setTyping(chat, true),
435
+ ]);
436
+ if (markReadResult.status === "rejected") {
437
+ emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
438
+ }
439
+ if (typingResult.status === "rejected") {
440
+ throw typingResult.reason;
441
+ }
442
+ });
443
+ }
444
+ function sendStatus(text) {
445
+ enqueue("send_status", async () => {
446
+ await client.sendText({
447
+ chat,
448
+ text,
449
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
450
+ });
451
+ // Re-enable typing indicator — sending a message clears the typing bubble
452
+ await client.setTyping(chat, true);
453
+ });
454
+ }
455
+ const statusBatcher = createStatusBatcher((text) => sendStatus(text), 500);
456
+ const toolCallbacks = (0, tool_activity_callbacks_1.createToolActivityCallbacks)({
457
+ onDescription: (text) => statusBatcher.add(text),
458
+ /* v8 ignore next -- onResult only called in debug mode; tested via tool-activity-callbacks.test.ts @preserve */
459
+ onResult: (text) => { statusBatcher.flush(); sendStatus(text); },
460
+ /* v8 ignore next -- onFailure only called on tool failure; tested via tool-activity-callbacks.test.ts @preserve */
461
+ onFailure: (text) => { statusBatcher.flush(); sendStatus(text); },
462
+ isDebug: commands_1.getDebugMode,
393
463
  });
394
464
  return {
395
465
  onModelStart() {
396
- activity.onModelStart();
466
+ if (!isGroupChat)
467
+ startTypingNow();
397
468
  (0, runtime_1.emitNervesEvent)({
398
469
  component: "senses",
399
470
  event: "senses.bluebubbles_turn_start",
@@ -410,12 +481,16 @@ function createBlueBubblesCallbacks(client, chat, replyTarget) {
410
481
  });
411
482
  },
412
483
  onTextChunk(text) {
413
- activity.onTextChunk(text);
484
+ if (isGroupChat && !typingActive)
485
+ startTypingNow();
414
486
  textBuffer += text;
415
487
  },
416
488
  onReasoningChunk(_text) { },
417
489
  onToolStart(name, _args) {
418
- activity.onToolStart(name, _args);
490
+ // Tool activity is a reply commitment — start typing if not already
491
+ if (!typingActive)
492
+ startTypingNow();
493
+ toolCallbacks.onToolStart(name, _args);
419
494
  (0, runtime_1.emitNervesEvent)({
420
495
  component: "senses",
421
496
  event: "senses.bluebubbles_tool_start",
@@ -424,7 +499,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget) {
424
499
  });
425
500
  },
426
501
  onToolEnd(name, summary, success) {
427
- activity.onToolEnd(name, summary, success);
502
+ toolCallbacks.onToolEnd(name, summary, success);
428
503
  (0, runtime_1.emitNervesEvent)({
429
504
  component: "senses",
430
505
  event: "senses.bluebubbles_tool_end",
@@ -433,7 +508,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget) {
433
508
  });
434
509
  },
435
510
  onError(error, severity) {
436
- activity.onError(error);
511
+ sendStatus(`\u2717 ${error.message}`);
437
512
  (0, runtime_1.emitNervesEvent)({
438
513
  level: severity === "terminal" ? "error" : "warn",
439
514
  component: "senses",
@@ -446,14 +521,24 @@ function createBlueBubblesCallbacks(client, chat, replyTarget) {
446
521
  textBuffer = "";
447
522
  },
448
523
  async flush() {
449
- await activity.drain();
524
+ statusBatcher.flush();
525
+ await queue;
450
526
  const trimmed = textBuffer.trim();
451
527
  if (!trimmed) {
452
- await activity.finish();
528
+ if (typingActive) {
529
+ typingActive = false;
530
+ enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
531
+ await queue;
532
+ }
453
533
  return;
454
534
  }
455
535
  textBuffer = "";
456
- await activity.finish();
536
+ /* v8 ignore next 4 -- branch: typing may already be stopped before flush @preserve */
537
+ if (typingActive) {
538
+ typingActive = false;
539
+ enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
540
+ await queue;
541
+ }
457
542
  await client.sendText({
458
543
  chat,
459
544
  text: trimmed,
@@ -461,7 +546,14 @@ function createBlueBubblesCallbacks(client, chat, replyTarget) {
461
546
  });
462
547
  },
463
548
  async finish() {
464
- await activity.finish();
549
+ statusBatcher.flush();
550
+ if (!typingActive) {
551
+ await queue;
552
+ return;
553
+ }
554
+ typingActive = false;
555
+ enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
556
+ await queue;
465
557
  },
466
558
  };
467
559
  }
@@ -534,7 +626,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
534
626
  const friendId = context.friend.id;
535
627
  const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
536
628
  try {
537
- (0, bluebubbles_session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
629
+ (0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
538
630
  }
539
631
  catch (error) {
540
632
  (0, runtime_1.emitNervesEvent)({
@@ -551,12 +643,13 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
551
643
  return (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
552
644
  // Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
553
645
  const existing = resolvedDeps.loadSession(sessPath);
646
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
554
647
  const sessionMessages = existing?.messages && existing.messages.length > 0
555
648
  ? existing.messages
556
- : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
649
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
557
650
  if (event.kind === "message") {
558
651
  const agentName = resolvedDeps.getAgentName();
559
- if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
652
+ if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
560
653
  (0, runtime_1.emitNervesEvent)({
561
654
  component: "senses",
562
655
  event: "senses.bluebubbles_recovery_skip",
@@ -569,8 +662,13 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
569
662
  });
570
663
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
571
664
  }
572
- if (source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)) {
573
- (0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, "recovery-bootstrap");
665
+ // Record EARLY to prevent duplicate processing. BB webhooks can retry
666
+ // before the first turn completes — recording after the turn is too late.
667
+ const inboundSource = source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
668
+ ? "recovery-bootstrap"
669
+ : source;
670
+ (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
671
+ if (inboundSource === "recovery-bootstrap") {
574
672
  (0, runtime_1.emitNervesEvent)({
575
673
  component: "senses",
576
674
  event: "senses.bluebubbles_recovery_skip",
@@ -584,15 +682,46 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
584
682
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
585
683
  }
586
684
  }
685
+ if (event.kind === "message" && event.chat.isGroup) {
686
+ await (0, group_context_1.upsertGroupContextParticipants)({
687
+ store,
688
+ participants: (event.chat.participantHandles ?? []).map((externalId) => ({
689
+ provider: "imessage-handle",
690
+ externalId,
691
+ })),
692
+ groupExternalId: resolveGroupExternalId(event),
693
+ });
694
+ }
695
+ // Fetch the text of the message being replied to (if this is a threaded reply)
696
+ const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
697
+ let repliedToText = null;
698
+ if (threadGuid) {
699
+ repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
700
+ (0, runtime_1.emitNervesEvent)({
701
+ component: "senses",
702
+ event: "senses.bluebubbles_reply_context",
703
+ message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
704
+ meta: { threadGuid, hasText: !!repliedToText },
705
+ });
706
+ }
707
+ // Enrich reaction mutations with the original message text for context
708
+ const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
709
+ if (isReaction && event.targetMessageGuid) {
710
+ /* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
711
+ const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
712
+ if (originalText)
713
+ event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
714
+ /* v8 ignore stop */
715
+ }
587
716
  // Build inbound user message (adapter concern: BB-specific content formatting)
588
717
  const userMessage = {
589
718
  role: "user",
590
- content: buildInboundContent(event, existing?.messages ?? sessionMessages),
719
+ content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
591
720
  };
592
- const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
721
+ const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
593
722
  const controller = new AbortController();
594
723
  // BB-specific tool context wrappers
595
- const summarize = (0, core_1.createSummarize)();
724
+ const summarize = (0, core_1.createSummarize)("human");
596
725
  const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
597
726
  const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
598
727
  // ── Compute trust gate context for group/acquaintance rules ─────
@@ -601,15 +730,38 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
601
730
  ? false
602
731
  : await checkHasExistingGroupWithFamily(store, context.friend);
603
732
  // ── Call shared pipeline ──────────────────────────────────────────
733
+ // Buffer terminal errors so failover can suppress them.
734
+ // If failover produces a message, the buffered error is skipped.
735
+ // If failover doesn't fire, the buffered error is replayed.
736
+ let bufferedTerminalError = null;
737
+ /* v8 ignore start -- failover-aware error buffering @preserve */
738
+ const failoverAwareCallbacks = {
739
+ ...callbacks,
740
+ onError(error, severity) {
741
+ if (severity === "terminal") {
742
+ bufferedTerminalError = error;
743
+ return;
744
+ }
745
+ callbacks.onError(error, severity);
746
+ },
747
+ };
748
+ /* v8 ignore stop */
604
749
  try {
605
750
  const result = await (0, pipeline_1.handleInboundTurn)({
606
751
  channel: "bluebubbles",
752
+ sessionKey: event.chat.sessionKey,
607
753
  capabilities: bbCapabilities,
608
754
  messages: [userMessage],
609
755
  continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
610
- callbacks,
611
756
  friendResolver: { resolve: () => Promise.resolve(context) },
612
- sessionLoader: { loadOrCreate: () => Promise.resolve({ messages: sessionMessages, sessionPath: sessPath, state: existing?.state }) },
757
+ sessionLoader: {
758
+ loadOrCreate: () => Promise.resolve({
759
+ messages: sessionMessages,
760
+ sessionPath: sessPath,
761
+ state: existing?.state,
762
+ events: existing?.events,
763
+ }),
764
+ },
613
765
  pendingDir,
614
766
  friendStore: store,
615
767
  provider: "imessage-handle",
@@ -619,6 +771,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
619
771
  hasExistingGroupWithFamily,
620
772
  enforceTrustGate: trust_gate_1.enforceTrustGate,
621
773
  drainPending: pending_1.drainPending,
774
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
622
775
  runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
623
776
  ...opts,
624
777
  toolContext: {
@@ -640,10 +793,31 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
640
793
  },
641
794
  },
642
795
  }),
643
- postTurn: resolvedDeps.postTurn,
796
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
797
+ const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
798
+ resolvedDeps.deferPostTurnPersist(sessionPathArg, prepared, usage, state);
799
+ },
644
800
  accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
645
801
  signal: controller.signal,
802
+ runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
803
+ callbacks: failoverAwareCallbacks,
804
+ failoverState: (() => {
805
+ if (!bbFailoverStates.has(event.chat.sessionKey)) {
806
+ bbFailoverStates.set(event.chat.sessionKey, { pending: null });
807
+ }
808
+ return bbFailoverStates.get(event.chat.sessionKey);
809
+ })(),
646
810
  });
811
+ /* v8 ignore start -- failover display + error replay @preserve */
812
+ if (result.failoverMessage) {
813
+ // Failover handled it — show the failover message, skip the buffered error
814
+ await client.sendText({ chat: event.chat, text: result.failoverMessage });
815
+ }
816
+ else if (bufferedTerminalError) {
817
+ // No failover — replay the buffered terminal error
818
+ callbacks.onError(bufferedTerminalError, "terminal");
819
+ }
820
+ /* v8 ignore stop */
647
821
  // ── Handle gate result ────────────────────────────────────────
648
822
  if (!result.gateResult.allowed) {
649
823
  // Send auto-reply via BB API if the gate provides one
@@ -653,9 +827,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
653
827
  text: result.gateResult.autoReply,
654
828
  });
655
829
  }
656
- if (event.kind === "message") {
657
- (0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
658
- }
659
830
  return {
660
831
  handled: true,
661
832
  notifiedAgent: false,
@@ -664,9 +835,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
664
835
  }
665
836
  // Gate allowed — flush the agent's reply
666
837
  await callbacks.flush();
667
- if (event.kind === "message") {
668
- (0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
669
- }
670
838
  (0, runtime_1.emitNervesEvent)({
671
839
  component: "senses",
672
840
  event: "senses.bluebubbles_turn_end",
@@ -684,6 +852,14 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
684
852
  };
685
853
  }
686
854
  finally {
855
+ // If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
856
+ // replay it now so the user still sees the error.
857
+ /* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
858
+ if (bufferedTerminalError) {
859
+ callbacks.onError(bufferedTerminalError, "terminal");
860
+ bufferedTerminalError = null;
861
+ }
862
+ /* v8 ignore stop */
687
863
  await callbacks.finish();
688
864
  }
689
865
  });
@@ -691,36 +867,119 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
691
867
  async function handleBlueBubblesEvent(payload, deps = {}) {
692
868
  const resolvedDeps = { ...defaultDeps, ...deps };
693
869
  const client = resolvedDeps.createClient();
694
- const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
870
+ let normalized;
871
+ try {
872
+ normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
873
+ }
874
+ catch (error) {
875
+ if (error instanceof model_1.BlueBubblesIgnoredEventError) {
876
+ (0, runtime_1.emitNervesEvent)({
877
+ component: "senses",
878
+ event: "senses.bluebubbles_event_skipped",
879
+ message: "skipped ignorable bluebubbles event",
880
+ meta: {
881
+ eventType: error.eventType,
882
+ },
883
+ });
884
+ return {
885
+ handled: true,
886
+ notifiedAgent: false,
887
+ reason: "ignored",
888
+ };
889
+ }
890
+ throw error;
891
+ }
892
+ // Pre-repair dedup: if we've already processed this messageGuid, skip the
893
+ // repair+hydrate path entirely. Applies to BOTH `kind: "message"` AND
894
+ // `kind: "mutation"` events — BlueBubbles often sends a `new-message`
895
+ // webhook for a fresh message AND one or more follow-up `updated-message`
896
+ // webhooks for delivery/read status. The mutation path (inside repairEvent)
897
+ // can promote an updated-message back to a message if it has recoverable
898
+ // content, which then re-runs the full VLM-describe pipeline on the same
899
+ // attachment.
900
+ //
901
+ // Without this early check, we paid DOUBLE latency and double tokens on
902
+ // every image-bearing message. Verified live on 2026-04-08T00:58Z: two
903
+ // sequential VLM describes for attachment guid 317E37EB-..., 13.7s +
904
+ // 14.0s each, for the exact same 291KB JPEG — triggered by a sequence of
905
+ // `new-message` followed ~3s later by `updated-message` for the same guid.
906
+ //
907
+ // We still route the skip through `handleBlueBubblesNormalizedEvent` so
908
+ // the downstream `already_processed` path fires its observability events
909
+ // and the caller sees a consistent return shape.
910
+ const agentName = resolvedDeps.getAgentName();
911
+ if (normalized.messageGuid
912
+ && (0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, normalized.chat.sessionKey, normalized.messageGuid)) {
913
+ (0, runtime_1.emitNervesEvent)({
914
+ level: "warn",
915
+ component: "senses",
916
+ event: "senses.bluebubbles_repair_skipped_duplicate",
917
+ message: "skipped repair+hydrate for already-processed bluebubbles messageGuid",
918
+ meta: {
919
+ messageGuid: normalized.messageGuid,
920
+ sessionKey: normalized.chat.sessionKey,
921
+ eventType: normalized.eventType,
922
+ normalizedKind: normalized.kind,
923
+ },
924
+ });
925
+ return handleBlueBubblesNormalizedEvent(normalized, resolvedDeps, "webhook");
926
+ }
927
+ const event = await client.repairEvent(normalized);
695
928
  return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
696
929
  }
697
930
  function countPendingRecoveryCandidates(agentName) {
698
- return (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
699
- .filter((entry) => !(0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, entry.sessionKey, entry.messageGuid))
931
+ return (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
932
+ .filter((entry) => !(0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, entry.sessionKey, entry.messageGuid))
700
933
  .length;
701
934
  }
935
+ function parseTimestampMs(value) {
936
+ if (!value)
937
+ return null;
938
+ const parsed = Date.parse(value);
939
+ return Number.isFinite(parsed) ? parsed : null;
940
+ }
941
+ function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
942
+ if (previousState.upstreamStatus === "error") {
943
+ return nowMs - BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS;
944
+ }
945
+ const lastCheckedAt = parseTimestampMs(previousState.lastCheckedAt);
946
+ if (lastCheckedAt !== null) {
947
+ return Math.max(0, lastCheckedAt - BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS);
948
+ }
949
+ return nowMs - BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS;
950
+ }
951
+ function formatRecoveredCount(count) {
952
+ return `caught up ${count} missed message(s)`;
953
+ }
702
954
  async function syncBlueBubblesRuntime(deps = {}) {
703
955
  const resolvedDeps = { ...defaultDeps, ...deps };
704
956
  const agentName = resolvedDeps.getAgentName();
705
957
  const client = resolvedDeps.createClient();
706
958
  const checkedAt = new Date().toISOString();
959
+ const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
707
960
  try {
708
961
  await client.checkHealth();
709
962
  const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
710
- (0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
711
- upstreamStatus: recovery.pending > 0 || recovery.failed > 0 ? "error" : "ok",
712
- detail: recovery.failed > 0
713
- ? `recovery failures: ${recovery.failed}`
963
+ const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState);
964
+ const failed = recovery.failed + catchUp.failed;
965
+ const recovered = recovery.recovered + catchUp.recovered;
966
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
967
+ upstreamStatus: recovery.pending > 0 || failed > 0 ? "error" : "ok",
968
+ detail: failed > 0
969
+ ? `recovery failures: ${failed}`
714
970
  : recovery.pending > 0
715
971
  ? `pending recovery: ${recovery.pending}`
716
- : "upstream reachable",
972
+ : catchUp.recovered > 0
973
+ ? formatRecoveredCount(catchUp.recovered)
974
+ : "upstream reachable",
717
975
  lastCheckedAt: checkedAt,
718
976
  pendingRecoveryCount: recovery.pending,
719
- lastRecoveredAt: recovery.recovered > 0 ? checkedAt : undefined,
977
+ lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
978
+ lastRecoveredMessageGuid: catchUp.lastRecoveredMessageGuid ?? previousState.lastRecoveredMessageGuid,
720
979
  });
721
980
  }
722
981
  catch (error) {
723
- (0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
982
+ (0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
724
983
  upstreamStatus: "error",
725
984
  detail: error instanceof Error ? error.message : String(error),
726
985
  lastCheckedAt: checkedAt,
@@ -728,13 +987,135 @@ async function syncBlueBubblesRuntime(deps = {}) {
728
987
  });
729
988
  }
730
989
  }
990
+ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
991
+ const resolvedDeps = { ...defaultDeps, ...deps };
992
+ const agentName = resolvedDeps.getAgentName();
993
+ const client = resolvedDeps.createClient();
994
+ const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
995
+ const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
996
+ const catchUpSince = resolveBlueBubblesCatchUpSince(state);
997
+ /* v8 ignore next -- older injected test doubles may omit the catch-up query method */
998
+ if (!client.listRecentMessages)
999
+ return result;
1000
+ (0, runtime_1.emitNervesEvent)({
1001
+ component: "senses",
1002
+ event: "senses.bluebubbles_catchup_start",
1003
+ message: "bluebubbles upstream catch-up pass started",
1004
+ meta: {
1005
+ since: new Date(catchUpSince).toISOString(),
1006
+ pageSize: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
1007
+ maxPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
1008
+ },
1009
+ });
1010
+ const recentEvents = [];
1011
+ for (let page = 0; page < BLUEBUBBLES_CATCHUP_MAX_PAGES; page++) {
1012
+ let pageEvents;
1013
+ try {
1014
+ pageEvents = await client.listRecentMessages({
1015
+ limit: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
1016
+ offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
1017
+ });
1018
+ }
1019
+ catch (error) {
1020
+ result.failed++;
1021
+ (0, runtime_1.emitNervesEvent)({
1022
+ level: "warn",
1023
+ component: "senses",
1024
+ event: "senses.bluebubbles_catchup_error",
1025
+ message: "bluebubbles upstream catch-up query failed",
1026
+ meta: {
1027
+ offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
1028
+ reason: error instanceof Error ? error.message : String(error),
1029
+ },
1030
+ });
1031
+ break;
1032
+ }
1033
+ recentEvents.push(...pageEvents);
1034
+ if (pageEvents.length < BLUEBUBBLES_CATCHUP_PAGE_SIZE)
1035
+ break;
1036
+ const oldestMessageTimestamp = pageEvents
1037
+ .filter((event) => event.kind === "message")
1038
+ .reduce((oldest, event) => Math.min(oldest, event.timestamp), Number.POSITIVE_INFINITY);
1039
+ if (oldestMessageTimestamp <= catchUpSince)
1040
+ break;
1041
+ if (page === BLUEBUBBLES_CATCHUP_MAX_PAGES - 1) {
1042
+ result.failed++;
1043
+ (0, runtime_1.emitNervesEvent)({
1044
+ level: "warn",
1045
+ component: "senses",
1046
+ event: "senses.bluebubbles_catchup_error",
1047
+ message: "bluebubbles upstream catch-up reached the bounded page limit",
1048
+ meta: {
1049
+ inspectedPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
1050
+ reason: "catch-up page limit reached before the outage window cutoff",
1051
+ },
1052
+ });
1053
+ }
1054
+ }
1055
+ const seenMessageGuids = new Set();
1056
+ const candidates = recentEvents
1057
+ .filter((event) => event.kind === "message")
1058
+ .filter((event) => {
1059
+ if (seenMessageGuids.has(event.messageGuid))
1060
+ return false;
1061
+ seenMessageGuids.add(event.messageGuid);
1062
+ return true;
1063
+ })
1064
+ .sort((left, right) => left.timestamp - right.timestamp);
1065
+ for (const event of candidates) {
1066
+ result.inspected++;
1067
+ if (event.fromMe
1068
+ || event.timestamp < catchUpSince
1069
+ || (0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
1070
+ result.skipped++;
1071
+ continue;
1072
+ }
1073
+ try {
1074
+ const repaired = await client.repairEvent(event);
1075
+ if (repaired.kind !== "message") {
1076
+ result.skipped++;
1077
+ continue;
1078
+ }
1079
+ const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup");
1080
+ if (handled.reason === "already_processed") {
1081
+ result.skipped++;
1082
+ }
1083
+ else {
1084
+ result.recovered++;
1085
+ result.lastRecoveredMessageGuid = repaired.messageGuid;
1086
+ }
1087
+ }
1088
+ catch (error) {
1089
+ result.failed++;
1090
+ (0, runtime_1.emitNervesEvent)({
1091
+ level: "warn",
1092
+ component: "senses",
1093
+ event: "senses.bluebubbles_catchup_error",
1094
+ message: "bluebubbles upstream catch-up message failed",
1095
+ meta: {
1096
+ messageGuid: event.messageGuid,
1097
+ reason: error instanceof Error ? error.message : String(error),
1098
+ },
1099
+ });
1100
+ }
1101
+ }
1102
+ if (result.inspected > 0 || result.recovered > 0 || result.skipped > 0 || result.failed > 0) {
1103
+ (0, runtime_1.emitNervesEvent)({
1104
+ component: "senses",
1105
+ event: "senses.bluebubbles_catchup_complete",
1106
+ message: "bluebubbles upstream catch-up pass completed",
1107
+ meta: { ...result },
1108
+ });
1109
+ }
1110
+ return result;
1111
+ }
731
1112
  async function recoverMissedBlueBubblesMessages(deps = {}) {
732
1113
  const resolvedDeps = { ...defaultDeps, ...deps };
733
1114
  const agentName = resolvedDeps.getAgentName();
734
1115
  const client = resolvedDeps.createClient();
735
1116
  const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
736
- for (const candidate of (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
737
- if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, candidate.sessionKey, candidate.messageGuid)) {
1117
+ for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
1118
+ if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, candidate.sessionKey, candidate.messageGuid)) {
738
1119
  result.skipped++;
739
1120
  continue;
740
1121
  }
@@ -779,6 +1160,14 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
779
1160
  function createBlueBubblesWebhookHandler(deps = {}) {
780
1161
  return async (req, res) => {
781
1162
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
1163
+ if (url.pathname === "/health") {
1164
+ if (req.method === "GET" || req.method === "HEAD") {
1165
+ writeJson(res, 200, { status: "ok", uptime: process.uptime() });
1166
+ return;
1167
+ }
1168
+ writeJson(res, 405, { error: "Method not allowed" });
1169
+ return;
1170
+ }
782
1171
  const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
783
1172
  const runtimeConfig = (0, config_1.getBlueBubblesConfig)();
784
1173
  if (url.pathname !== channelConfig.webhookPath) {
@@ -839,6 +1228,192 @@ function findImessageHandle(friend) {
839
1228
  }
840
1229
  return undefined;
841
1230
  }
1231
+ function normalizeBlueBubblesSessionKey(sessionKey) {
1232
+ const trimmed = sessionKey.trim();
1233
+ if (trimmed.startsWith("chat_identifier_")) {
1234
+ return `chat_identifier:${trimmed.slice("chat_identifier_".length)}`;
1235
+ }
1236
+ if (trimmed.startsWith("chat_")) {
1237
+ return `chat:${trimmed.slice("chat_".length)}`;
1238
+ }
1239
+ return trimmed;
1240
+ }
1241
+ function extractChatIdentifierFromSessionKey(sessionKey) {
1242
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
1243
+ if (normalizedKey.startsWith("chat:")) {
1244
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
1245
+ const parts = chatGuid.split(";");
1246
+ return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
1247
+ }
1248
+ if (normalizedKey.startsWith("chat_identifier:")) {
1249
+ const identifier = normalizedKey.slice("chat_identifier:".length).trim();
1250
+ return identifier || undefined;
1251
+ }
1252
+ return undefined;
1253
+ }
1254
+ function buildChatRefForSessionKey(friend, sessionKey) {
1255
+ const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
1256
+ if (normalizedKey.startsWith("chat:")) {
1257
+ const chatGuid = normalizedKey.slice("chat:".length).trim();
1258
+ if (!chatGuid)
1259
+ return null;
1260
+ return {
1261
+ chatGuid,
1262
+ chatIdentifier: extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend),
1263
+ isGroup: chatGuid.includes(";+;"),
1264
+ sessionKey,
1265
+ sendTarget: { kind: "chat_guid", value: chatGuid },
1266
+ participantHandles: [],
1267
+ };
1268
+ }
1269
+ const chatIdentifier = extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend);
1270
+ if (!chatIdentifier)
1271
+ return null;
1272
+ return {
1273
+ chatIdentifier,
1274
+ isGroup: false,
1275
+ sessionKey,
1276
+ sendTarget: { kind: "chat_identifier", value: chatIdentifier },
1277
+ participantHandles: [],
1278
+ };
1279
+ }
1280
+ async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
1281
+ const resolvedDeps = { ...defaultDeps, ...deps };
1282
+ const client = resolvedDeps.createClient();
1283
+ const store = resolvedDeps.createFriendStore();
1284
+ let friend;
1285
+ try {
1286
+ friend = await store.get(params.friendId);
1287
+ }
1288
+ catch {
1289
+ friend = null;
1290
+ }
1291
+ // Direct filesystem fallback — store.get() with name resolution wasn't working in production
1292
+ // despite correct compiled code. Bypass the entire store abstraction.
1293
+ /* v8 ignore start -- direct filesystem name resolution @preserve */
1294
+ if (!friend) {
1295
+ try {
1296
+ const friendsDir = path.join((0, identity_1.getAgentRoot)(), "friends");
1297
+ const files = fs.readdirSync(friendsDir).filter((f) => f.endsWith(".json"));
1298
+ for (const file of files) {
1299
+ const raw = JSON.parse(fs.readFileSync(path.join(friendsDir, file), "utf-8"));
1300
+ if (raw.name?.toLowerCase() === params.friendId.toLowerCase()) {
1301
+ friend = raw;
1302
+ (0, runtime_1.emitNervesEvent)({
1303
+ component: "senses",
1304
+ event: "senses.bluebubbles_proactive_name_resolved",
1305
+ message: "resolved friend by name via direct filesystem scan",
1306
+ meta: { friendId: params.friendId, resolvedId: raw.id, name: raw.name },
1307
+ });
1308
+ break;
1309
+ }
1310
+ }
1311
+ }
1312
+ catch (err) {
1313
+ (0, runtime_1.emitNervesEvent)({
1314
+ level: "warn",
1315
+ component: "senses",
1316
+ event: "senses.bluebubbles_proactive_name_resolve_error",
1317
+ message: "direct filesystem name resolution failed",
1318
+ meta: { friendId: params.friendId, error: err instanceof Error ? err.message : String(err) },
1319
+ });
1320
+ }
1321
+ }
1322
+ /* v8 ignore stop */
1323
+ if (!friend) {
1324
+ (0, runtime_1.emitNervesEvent)({
1325
+ level: "warn",
1326
+ component: "senses",
1327
+ event: "senses.bluebubbles_proactive_no_friend",
1328
+ message: "proactive send skipped: friend not found",
1329
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1330
+ });
1331
+ return { delivered: false, reason: "friend_not_found" };
1332
+ }
1333
+ const explicitCrossChatAuthorized = params.intent === "explicit_cross_chat"
1334
+ && types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
1335
+ if (!explicitCrossChatAuthorized && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
1336
+ (0, runtime_1.emitNervesEvent)({
1337
+ component: "senses",
1338
+ event: "senses.bluebubbles_proactive_trust_skip",
1339
+ message: "proactive send skipped: trust level not allowed",
1340
+ meta: {
1341
+ friendId: params.friendId,
1342
+ sessionKey: params.sessionKey,
1343
+ trustLevel: friend.trustLevel ?? "unknown",
1344
+ intent: params.intent ?? "generic_outreach",
1345
+ authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
1346
+ },
1347
+ });
1348
+ return { delivered: false, reason: "trust_skip" };
1349
+ }
1350
+ const chat = buildChatRefForSessionKey(friend, params.sessionKey);
1351
+ if (!chat) {
1352
+ (0, runtime_1.emitNervesEvent)({
1353
+ level: "warn",
1354
+ component: "senses",
1355
+ event: "senses.bluebubbles_proactive_no_handle",
1356
+ message: "proactive send skipped: no iMessage handle found",
1357
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey },
1358
+ });
1359
+ return { delivered: false, reason: "missing_target" };
1360
+ }
1361
+ // Proactive outreach to individuals must go to DMs, never group chats.
1362
+ // Explicit cross-chat responses (bridge completions, delegation returns) ARE allowed to groups
1363
+ // because the request originated from that group.
1364
+ /* v8 ignore start -- group gate: only fires when proactive send targets a group session @preserve */
1365
+ if (chat.isGroup && params.intent !== "explicit_cross_chat") {
1366
+ (0, runtime_1.emitNervesEvent)({
1367
+ level: "warn",
1368
+ component: "senses",
1369
+ event: "senses.bluebubbles_proactive_group_blocked",
1370
+ message: "proactive send blocked: would route to group chat",
1371
+ meta: { friendId: params.friendId, sessionKey: params.sessionKey, chatGuid: chat.chatGuid ?? null, intent: params.intent ?? null },
1372
+ });
1373
+ return { delivered: false, reason: "group_blocked" };
1374
+ }
1375
+ /* v8 ignore stop */
1376
+ const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
1377
+ if (internalContentBlockReason) {
1378
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1379
+ friendId: params.friendId,
1380
+ sessionKey: params.sessionKey,
1381
+ reason: internalContentBlockReason,
1382
+ source: "session_send",
1383
+ intent: params.intent ?? "generic_outreach",
1384
+ });
1385
+ return { delivered: false, reason: "internal_content_blocked" };
1386
+ }
1387
+ try {
1388
+ await client.sendText({ chat, text: params.text });
1389
+ (0, runtime_1.emitNervesEvent)({
1390
+ component: "senses",
1391
+ event: "senses.bluebubbles_proactive_sent",
1392
+ message: "proactive bluebubbles message sent",
1393
+ meta: {
1394
+ friendId: params.friendId,
1395
+ sessionKey: params.sessionKey,
1396
+ chatGuid: chat.chatGuid ?? null,
1397
+ chatIdentifier: chat.chatIdentifier ?? null,
1398
+ },
1399
+ });
1400
+ return { delivered: true };
1401
+ }
1402
+ catch (error) {
1403
+ (0, runtime_1.emitNervesEvent)({
1404
+ level: "error",
1405
+ component: "senses",
1406
+ event: "senses.bluebubbles_proactive_send_error",
1407
+ message: "proactive bluebubbles send failed",
1408
+ meta: {
1409
+ friendId: params.friendId,
1410
+ sessionKey: params.sessionKey,
1411
+ reason: error instanceof Error ? error.message : String(error),
1412
+ },
1413
+ });
1414
+ return { delivered: false, reason: "send_error" };
1415
+ }
1416
+ }
842
1417
  function scanPendingBlueBubblesFiles(pendingRoot) {
843
1418
  const results = [];
844
1419
  let friendIds;
@@ -909,6 +1484,20 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
909
1484
  catch { /* ignore */ }
910
1485
  continue;
911
1486
  }
1487
+ const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
1488
+ if (internalBlockReason) {
1489
+ result.skipped++;
1490
+ try {
1491
+ fs.unlinkSync(filePath);
1492
+ }
1493
+ catch { /* ignore */ }
1494
+ (0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
1495
+ friendId,
1496
+ reason: internalBlockReason,
1497
+ source: "pending_drain",
1498
+ });
1499
+ continue;
1500
+ }
912
1501
  let friend;
913
1502
  try {
914
1503
  friend = await store.get(friendId);