@ouro.bot/cli 0.1.0-alpha.56 → 0.1.0-alpha.560
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.
- package/README.md +127 -23
- package/RepairGuide.ouro/agent.json +5 -0
- package/RepairGuide.ouro/psyche/IDENTITY.md +19 -0
- package/RepairGuide.ouro/psyche/SOUL.md +55 -0
- package/RepairGuide.ouro/skills/diagnose-broken-remote.md +63 -0
- package/RepairGuide.ouro/skills/diagnose-stacked-typed-issues.md +35 -0
- package/RepairGuide.ouro/skills/diagnose-sync-blocked.md +54 -0
- package/RepairGuide.ouro/skills/diagnose-vault-expired.md +60 -0
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +4 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +2 -2
- package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +1 -1
- package/changelog.json +3596 -0
- package/dist/arc/attention-types.js +8 -0
- package/dist/arc/cares.js +140 -0
- package/dist/arc/episodes.js +117 -0
- package/dist/arc/intentions.js +133 -0
- package/dist/arc/json-store.js +117 -0
- package/dist/arc/obligations.js +237 -0
- package/dist/arc/packets.js +193 -0
- package/dist/arc/presence.js +185 -0
- package/dist/arc/task-lifecycle.js +65 -0
- package/dist/heart/active-work.js +837 -26
- package/dist/heart/agent-entry.js +58 -3
- package/dist/heart/attachments/image-normalize.js +194 -0
- package/dist/heart/attachments/materialize.js +97 -0
- package/dist/heart/attachments/originals.js +88 -0
- package/dist/heart/attachments/render.js +29 -0
- package/dist/heart/attachments/sources/adapter.js +2 -0
- package/dist/heart/attachments/sources/bluebubbles.js +156 -0
- package/dist/heart/attachments/sources/cli-local-file.js +78 -0
- package/dist/heart/attachments/sources/index.js +16 -0
- package/dist/heart/attachments/store.js +103 -0
- package/dist/heart/attachments/types.js +93 -0
- package/dist/heart/auth/auth-flow.js +479 -0
- package/dist/heart/background-operations.js +281 -0
- package/dist/heart/bundle-state.js +168 -0
- package/dist/heart/commitments.js +111 -0
- package/dist/heart/config-registry.js +322 -0
- package/dist/heart/config.js +114 -118
- package/dist/heart/core.js +913 -246
- package/dist/heart/cross-chat-delivery.js +3 -18
- package/dist/heart/daemon/agent-config-check.js +419 -0
- package/dist/heart/daemon/agent-discovery.js +102 -3
- package/dist/heart/daemon/agent-service.js +522 -0
- package/dist/heart/daemon/agentic-repair.js +547 -0
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +122 -0
- package/dist/heart/daemon/boot-sync-probe.js +197 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/cli-defaults.js +776 -0
- package/dist/heart/daemon/cli-exec.js +7457 -0
- package/dist/heart/daemon/cli-help.js +498 -0
- package/dist/heart/daemon/cli-parse.js +1592 -0
- package/dist/heart/daemon/cli-render-doctor.js +57 -0
- package/dist/heart/daemon/cli-render.js +763 -0
- package/dist/heart/daemon/cli-types.js +8 -0
- package/dist/heart/daemon/connect-bay.js +323 -0
- package/dist/heart/daemon/daemon-cli.js +29 -1698
- package/dist/heart/daemon/daemon-entry.js +387 -2
- package/dist/heart/daemon/daemon-health.js +176 -0
- package/dist/heart/daemon/daemon-rollup.js +57 -0
- package/dist/heart/daemon/daemon-runtime-sync.js +88 -13
- package/dist/heart/daemon/daemon-tombstone.js +236 -0
- package/dist/heart/daemon/daemon.js +796 -71
- package/dist/heart/daemon/dns-workflow.js +394 -0
- package/dist/heart/daemon/doctor-types.js +8 -0
- package/dist/heart/daemon/doctor.js +826 -0
- package/dist/heart/daemon/health-monitor.js +122 -1
- package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
- package/dist/heart/daemon/hooks/bundle-meta.js +115 -1
- package/dist/heart/daemon/http-health-probe.js +80 -0
- package/dist/heart/daemon/human-command-screens.js +234 -0
- package/dist/heart/daemon/human-readiness.js +114 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/interactive-repair.js +394 -0
- package/dist/heart/daemon/launchd.js +37 -8
- package/dist/heart/daemon/log-tailer.js +82 -12
- package/dist/heart/daemon/logs-prune.js +110 -0
- package/dist/heart/daemon/mcp-canary.js +297 -0
- package/dist/heart/daemon/message-router.js +2 -2
- package/dist/heart/daemon/os-cron-deps.js +135 -0
- package/dist/heart/daemon/os-cron.js +14 -12
- package/dist/heart/daemon/ouro-bot-entry.js +4 -2
- package/dist/heart/daemon/ouro-entry.js +3 -1
- package/dist/heart/daemon/process-manager.js +375 -33
- package/dist/heart/daemon/provider-discovery.js +137 -0
- package/dist/heart/daemon/provider-ping-progress.js +83 -0
- package/dist/heart/daemon/pulse.js +475 -0
- package/dist/heart/daemon/readiness-repair.js +365 -0
- package/dist/heart/daemon/run-hooks.js +2 -0
- package/dist/heart/daemon/runtime-logging.js +67 -16
- package/dist/heart/daemon/runtime-metadata.js +3 -31
- package/dist/heart/daemon/safe-mode.js +161 -0
- package/dist/heart/daemon/sense-manager.js +389 -38
- package/dist/heart/daemon/session-id-resolver.js +131 -0
- package/dist/heart/daemon/skill-management-installer.js +94 -0
- package/dist/heart/daemon/socket-client.js +158 -11
- package/dist/heart/daemon/stale-bundle-prune.js +96 -0
- package/dist/heart/daemon/startup-tui.js +330 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/terminal-ui.js +499 -0
- package/dist/heart/daemon/thoughts.js +162 -17
- package/dist/heart/daemon/up-progress.js +366 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/delegation.js +1 -1
- package/dist/heart/habits/habit-migration.js +189 -0
- package/dist/heart/habits/habit-parser.js +140 -0
- package/dist/heart/habits/habit-runtime-state.js +100 -0
- package/dist/heart/habits/habit-scheduler.js +372 -0
- package/dist/heart/{daemon → hatch}/hatch-flow.js +32 -56
- package/dist/heart/{daemon → hatch}/hatch-specialist.js +6 -8
- package/dist/heart/{daemon → hatch}/specialist-prompt.js +12 -9
- package/dist/heart/{daemon → hatch}/specialist-tools.js +35 -12
- package/dist/heart/identity.js +203 -57
- package/dist/heart/kept-notes.js +357 -0
- package/dist/heart/kicks.js +1 -1
- package/dist/heart/machine-identity.js +161 -0
- package/dist/heart/mail-import-discovery.js +353 -0
- package/dist/heart/mailbox/mailbox-http-hooks.js +66 -0
- package/dist/heart/mailbox/mailbox-http-response.js +7 -0
- package/dist/heart/mailbox/mailbox-http-routes.js +246 -0
- package/dist/heart/mailbox/mailbox-http-static.js +103 -0
- package/dist/heart/mailbox/mailbox-http-transport.js +116 -0
- package/dist/heart/mailbox/mailbox-http.js +99 -0
- package/dist/heart/mailbox/mailbox-read.js +31 -0
- package/dist/heart/mailbox/mailbox-types.js +27 -0
- package/dist/heart/mailbox/mailbox-view.js +195 -0
- package/dist/heart/mailbox/readers/agent-machine.js +382 -0
- package/dist/heart/mailbox/readers/continuity-readers.js +338 -0
- package/dist/heart/mailbox/readers/mail.js +362 -0
- package/dist/heart/mailbox/readers/runtime-readers.js +651 -0
- package/dist/heart/mailbox/readers/sessions.js +232 -0
- package/dist/heart/mailbox/readers/shared.js +111 -0
- package/dist/heart/mcp/mcp-server.js +683 -0
- package/dist/heart/migrate-config.js +100 -0
- package/dist/heart/model-capabilities.js +19 -0
- package/dist/heart/platform.js +81 -0
- package/dist/heart/provider-attempt.js +134 -0
- package/dist/heart/provider-binding-resolver.js +267 -0
- package/dist/heart/provider-credentials.js +425 -0
- package/dist/heart/provider-failover.js +301 -0
- package/dist/heart/provider-models.js +81 -0
- package/dist/heart/provider-ping.js +262 -0
- package/dist/heart/provider-readiness-cache.js +40 -0
- package/dist/heart/provider-visibility.js +188 -0
- package/dist/heart/providers/anthropic-token.js +131 -0
- package/dist/heart/providers/anthropic.js +139 -52
- package/dist/heart/providers/azure.js +97 -13
- package/dist/heart/providers/error-classification.js +127 -0
- package/dist/heart/providers/github-copilot.js +145 -0
- package/dist/heart/providers/minimax-vlm.js +189 -0
- package/dist/heart/providers/minimax.js +26 -8
- package/dist/heart/providers/openai-codex.js +55 -40
- package/dist/heart/runtime-capability-check.js +170 -0
- package/dist/heart/runtime-credentials.js +367 -0
- package/dist/heart/runtime-cwd.js +87 -0
- package/dist/heart/sense-truth.js +13 -4
- package/dist/heart/session-activity.js +43 -22
- package/dist/heart/session-events.js +1149 -0
- package/dist/heart/session-playback-cli-main.js +5 -0
- package/dist/heart/session-playback-cli.js +36 -0
- package/dist/heart/session-playback.js +231 -0
- package/dist/heart/session-stats-cli-main.js +5 -0
- package/dist/heart/session-stats.js +182 -0
- package/dist/heart/session-transcript.js +243 -0
- package/dist/heart/start-of-turn-packet.js +345 -0
- package/dist/heart/streaming.js +44 -27
- package/dist/heart/sync-classification.js +176 -0
- package/dist/heart/sync.js +449 -0
- package/dist/heart/target-resolution.js +9 -5
- package/dist/heart/tempo.js +93 -0
- package/dist/heart/temporal-view.js +41 -0
- package/dist/heart/timeouts.js +101 -0
- package/dist/heart/tool-activity-callbacks.js +59 -0
- package/dist/heart/tool-description.js +139 -0
- package/dist/heart/tool-friction.js +55 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-context.js +389 -0
- package/dist/heart/{daemon → versioning}/ouro-bot-global-installer.js +6 -5
- package/dist/heart/{daemon → versioning}/ouro-bot-wrapper.js +1 -1
- package/dist/heart/versioning/ouro-path-installer.js +426 -0
- package/dist/heart/versioning/ouro-version-manager.js +295 -0
- package/dist/heart/{daemon → versioning}/staged-restart.js +40 -8
- package/dist/heart/{daemon → versioning}/update-checker.js +6 -1
- package/dist/heart/{daemon → versioning}/update-hooks.js +63 -59
- package/dist/mailbox-ui/assets/index-B-461hes.js +61 -0
- package/dist/mailbox-ui/assets/index-BPr5vNuM.css +1 -0
- package/dist/mailbox-ui/index.html +15 -0
- package/dist/mailroom/attention.js +167 -0
- package/dist/mailroom/autonomy.js +209 -0
- package/dist/mailroom/blob-store.js +674 -0
- package/dist/mailroom/body-cache.js +61 -0
- package/dist/mailroom/core.js +720 -0
- package/dist/mailroom/entry.js +160 -0
- package/dist/mailroom/file-store.js +430 -0
- package/dist/mailroom/mbox-import.js +383 -0
- package/dist/mailroom/outbound.js +380 -0
- package/dist/mailroom/policy.js +263 -0
- package/dist/mailroom/reader.js +233 -0
- package/dist/mailroom/search-cache.js +256 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mailroom/smtp-ingress.js +176 -0
- package/dist/mailroom/source-state.js +176 -0
- package/dist/mailroom/thread.js +109 -0
- package/dist/mailroom/travel-extract.js +89 -0
- package/dist/mind/bundle-manifest.js +7 -1
- package/dist/mind/context.js +165 -101
- package/dist/mind/diary-integrity.js +60 -0
- package/dist/mind/{memory.js → diary.js} +62 -75
- package/dist/mind/embedding-provider.js +60 -0
- package/dist/mind/file-state.js +179 -0
- package/dist/mind/friends/channel.js +39 -0
- package/dist/mind/friends/resolver.js +54 -2
- package/dist/mind/friends/store-file.js +39 -3
- package/dist/mind/friends/types.js +2 -2
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/note-search.js +268 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/pending.js +4 -0
- package/dist/mind/prompt-refresh.js +3 -2
- package/dist/mind/prompt.js +1011 -123
- package/dist/mind/provenance-trust.js +26 -0
- package/dist/mind/scrutiny.js +173 -0
- package/dist/nerves/cli-logging.js +7 -1
- package/dist/nerves/coverage/audit-rules.js +15 -6
- package/dist/nerves/coverage/audit.js +28 -2
- package/dist/nerves/coverage/cli.js +1 -1
- package/dist/nerves/coverage/contract.js +5 -5
- package/dist/nerves/coverage/file-completeness.js +129 -5
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/event-buffer.js +111 -0
- package/dist/nerves/index.js +224 -4
- package/dist/nerves/observation.js +20 -0
- package/dist/nerves/redact.js +79 -0
- package/dist/nerves/review/cli-main.js +5 -0
- package/dist/nerves/review/cli.js +156 -0
- package/dist/nerves/review/core.js +152 -0
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +15 -56
- package/dist/repertoire/ado-semantic.js +11 -10
- package/dist/repertoire/api-client.js +97 -0
- package/dist/repertoire/bitwarden-store.js +963 -0
- package/dist/repertoire/bundle-templates.js +72 -0
- package/dist/repertoire/bw-installer.js +180 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +330 -0
- package/dist/repertoire/coding/feedback.js +197 -30
- package/dist/repertoire/coding/manager.js +158 -9
- package/dist/repertoire/coding/spawner.js +55 -9
- package/dist/repertoire/coding/tools.js +170 -7
- package/dist/repertoire/commerce-errors.js +109 -0
- package/dist/repertoire/commerce-self-test.js +156 -0
- package/dist/repertoire/credential-access.js +178 -0
- package/dist/repertoire/duffel-client.js +185 -0
- package/dist/repertoire/github-client.js +14 -55
- package/dist/repertoire/graph-client.js +11 -52
- package/dist/repertoire/guardrails.js +396 -0
- package/dist/repertoire/mcp-client.js +295 -0
- package/dist/repertoire/mcp-manager.js +362 -0
- package/dist/repertoire/mcp-tools.js +63 -0
- package/dist/repertoire/shell-sessions.js +133 -0
- package/dist/repertoire/skills.js +15 -24
- package/dist/repertoire/stripe-client.js +131 -0
- package/dist/repertoire/tasks/board.js +31 -5
- package/dist/repertoire/tasks/fix.js +182 -0
- package/dist/repertoire/tasks/index.js +16 -4
- package/dist/repertoire/tasks/lifecycle.js +2 -2
- package/dist/repertoire/tasks/parser.js +3 -2
- package/dist/repertoire/tasks/scanner.js +194 -37
- package/dist/repertoire/tasks/transitions.js +16 -78
- package/dist/repertoire/tool-results.js +29 -0
- package/dist/repertoire/tools-attachments.js +317 -0
- package/dist/repertoire/tools-base.js +47 -1075
- package/dist/repertoire/tools-bluebubbles.js +1 -0
- package/dist/repertoire/tools-bridge.js +142 -0
- package/dist/repertoire/tools-bundle.js +984 -0
- package/dist/repertoire/tools-config.js +185 -0
- package/dist/repertoire/tools-continuity.js +248 -0
- package/dist/repertoire/tools-credential.js +381 -0
- package/dist/repertoire/tools-files.js +342 -0
- package/dist/repertoire/tools-flight.js +224 -0
- package/dist/repertoire/tools-flow.js +119 -0
- package/dist/repertoire/tools-github.js +1 -7
- package/dist/repertoire/tools-mail.js +1857 -0
- package/dist/repertoire/tools-notes.js +421 -0
- package/dist/repertoire/tools-session.js +750 -0
- package/dist/repertoire/tools-shell.js +120 -0
- package/dist/repertoire/tools-stripe.js +180 -0
- package/dist/repertoire/tools-surface.js +243 -0
- package/dist/repertoire/tools-teams.js +9 -39
- package/dist/repertoire/tools-travel.js +125 -0
- package/dist/repertoire/tools-trip.js +604 -0
- package/dist/repertoire/tools-user-profile.js +144 -0
- package/dist/repertoire/tools-vault.js +40 -0
- package/dist/repertoire/tools.js +108 -100
- package/dist/repertoire/travel-api-client.js +360 -0
- package/dist/repertoire/user-profile.js +131 -0
- package/dist/repertoire/vault-setup.js +246 -0
- package/dist/repertoire/vault-unlock.js +594 -0
- package/dist/scripts/claude-code-hook.js +41 -0
- package/dist/scripts/claude-code-stop-hook.js +47 -0
- package/dist/senses/attention-queue.js +116 -0
- package/dist/senses/bluebubbles/active-turns.js +216 -0
- package/dist/senses/bluebubbles/attachment-cache.js +53 -0
- package/dist/senses/bluebubbles/attachment-download.js +137 -0
- package/dist/senses/{bluebubbles-client.js → bluebubbles/client.js} +219 -18
- package/dist/senses/bluebubbles/entry.js +77 -0
- package/dist/senses/{bluebubbles-inbound-log.js → bluebubbles/inbound-log.js} +20 -3
- package/dist/senses/bluebubbles/index.js +2305 -0
- package/dist/senses/{bluebubbles-media.js → bluebubbles/media.js} +121 -70
- package/dist/senses/{bluebubbles-model.js → bluebubbles/model.js} +33 -12
- package/dist/senses/{bluebubbles-mutation-log.js → bluebubbles/mutation-log.js} +3 -3
- package/dist/senses/bluebubbles/processed-log.js +133 -0
- package/dist/senses/bluebubbles/replay.js +137 -0
- package/dist/senses/{bluebubbles-runtime-state.js → bluebubbles/runtime-state.js} +30 -2
- package/dist/senses/{bluebubbles-session-cleanup.js → bluebubbles/session-cleanup.js} +1 -1
- package/dist/senses/cli/bracketed-paste.js +82 -0
- package/dist/senses/cli/image-paste.js +287 -0
- package/dist/senses/cli/image-ref-navigation.js +75 -0
- package/dist/senses/cli/ink-app.js +156 -0
- package/dist/senses/cli/inline-diff.js +64 -0
- package/dist/senses/cli/input-keys.js +174 -0
- package/dist/senses/cli/kill-ring.js +86 -0
- package/dist/senses/cli/message-list.js +51 -0
- package/dist/senses/cli/ouro-tui.js +607 -0
- package/dist/senses/cli/spinner-imperative.js +135 -0
- package/dist/senses/cli/spinner.js +101 -0
- package/dist/senses/cli/status-line.js +60 -0
- package/dist/senses/cli/streaming-markdown.js +526 -0
- package/dist/senses/cli/tool-display.js +85 -0
- package/dist/senses/cli/tool-render.js +85 -0
- package/dist/senses/cli/tui-store.js +240 -0
- package/dist/senses/cli/virtual-list.js +35 -0
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +520 -209
- package/dist/senses/commands.js +66 -3
- package/dist/senses/habit-turn-message.js +108 -0
- package/dist/senses/inner-dialog-worker.js +175 -21
- package/dist/senses/inner-dialog.js +330 -27
- package/dist/senses/mail-entry.js +66 -0
- package/dist/senses/mail.js +379 -0
- package/dist/senses/pipeline.js +549 -181
- package/dist/senses/proactive-content-guard.js +51 -0
- package/dist/senses/shared-turn.js +248 -0
- package/dist/senses/surface-tool.js +68 -0
- package/dist/senses/teams-entry.js +60 -8
- package/dist/senses/teams.js +387 -98
- package/dist/senses/trust-gate.js +100 -5
- package/dist/senses/voice/elevenlabs.js +125 -0
- package/dist/senses/voice/index.js +22 -0
- package/dist/senses/voice/transcript.js +70 -0
- package/dist/senses/voice/turn.js +85 -0
- package/dist/senses/voice/types.js +2 -0
- package/dist/senses/voice/whisper.js +133 -0
- package/dist/senses/voice-entry.js +80 -0
- package/dist/trips/core.js +138 -0
- package/dist/trips/store.js +146 -0
- package/package.json +38 -7
- package/skills/agent-commerce.md +106 -0
- package/skills/browser-navigation.md +117 -0
- package/skills/commerce-setup-guide.md +116 -0
- package/skills/commerce-setup.md +84 -0
- package/skills/configure-dev-tools.md +101 -0
- package/skills/travel-planning.md +138 -0
- package/dist/heart/daemon/auth-flow.js +0 -351
- package/dist/heart/daemon/ouro-path-installer.js +0 -178
- package/dist/heart/daemon/subagent-installer.js +0 -166
- package/dist/heart/session-recall.js +0 -116
- package/dist/mind/associative-recall.js +0 -209
- package/dist/senses/bluebubbles-entry.js +0 -13
- package/dist/senses/bluebubbles.js +0 -1177
- package/dist/senses/debug-activity.js +0 -148
- package/subagents/README.md +0 -86
- package/subagents/work-doer.md +0 -237
- package/subagents/work-merger.md +0 -618
- package/subagents/work-planner.md +0 -390
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
- /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
- /package/dist/heart/{daemon → hatch}/hatch-animation.js +0 -0
- /package/dist/heart/{daemon → hatch}/specialist-orchestrator.js +0 -0
- /package/dist/heart/{daemon → versioning}/ouro-uti.js +0 -0
- /package/dist/heart/{daemon → versioning}/wrapper-publish-guard.js +0 -0
|
@@ -0,0 +1,2305 @@
|
|
|
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.enrichReactionText = enrichReactionText;
|
|
37
|
+
exports.createStatusBatcher = createStatusBatcher;
|
|
38
|
+
exports.createBlueBubblesCallbacks = createBlueBubblesCallbacks;
|
|
39
|
+
exports.isAgentSelfHandle = isAgentSelfHandle;
|
|
40
|
+
exports.getDiscoveredOwnHandles = getDiscoveredOwnHandles;
|
|
41
|
+
exports.clearDiscoveredOwnHandles = clearDiscoveredOwnHandles;
|
|
42
|
+
exports.recordDiscoveredOwnHandle = recordDiscoveredOwnHandle;
|
|
43
|
+
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
44
|
+
exports.recoverQueuedBlueBubblesMessages = recoverQueuedBlueBubblesMessages;
|
|
45
|
+
exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
|
|
46
|
+
exports.recoverCapturedBlueBubblesInboundMessages = recoverCapturedBlueBubblesInboundMessages;
|
|
47
|
+
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
48
|
+
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
49
|
+
exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
|
|
50
|
+
exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
|
|
51
|
+
exports.startBlueBubblesApp = startBlueBubblesApp;
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const http = __importStar(require("node:http"));
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
const core_1 = require("../../heart/core");
|
|
56
|
+
const config_1 = require("../../heart/config");
|
|
57
|
+
const identity_1 = require("../../heart/identity");
|
|
58
|
+
const runtime_cwd_1 = require("../../heart/runtime-cwd");
|
|
59
|
+
const turn_coordinator_1 = require("../../heart/turn-coordinator");
|
|
60
|
+
const context_1 = require("../../mind/context");
|
|
61
|
+
const tokens_1 = require("../../mind/friends/tokens");
|
|
62
|
+
const group_context_1 = require("../../mind/friends/group-context");
|
|
63
|
+
const resolver_1 = require("../../mind/friends/resolver");
|
|
64
|
+
const store_file_1 = require("../../mind/friends/store-file");
|
|
65
|
+
const types_1 = require("../../mind/friends/types");
|
|
66
|
+
const channel_1 = require("../../mind/friends/channel");
|
|
67
|
+
const pending_1 = require("../../mind/pending");
|
|
68
|
+
const prompt_1 = require("../../mind/prompt");
|
|
69
|
+
const mcp_manager_1 = require("../../repertoire/mcp-manager");
|
|
70
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
71
|
+
const proactive_content_guard_1 = require("../proactive-content-guard");
|
|
72
|
+
const model_1 = require("./model");
|
|
73
|
+
const client_1 = require("./client");
|
|
74
|
+
const inbound_log_1 = require("./inbound-log");
|
|
75
|
+
const mutation_log_1 = require("./mutation-log");
|
|
76
|
+
const processed_log_1 = require("./processed-log");
|
|
77
|
+
const runtime_state_1 = require("./runtime-state");
|
|
78
|
+
const session_cleanup_1 = require("./session-cleanup");
|
|
79
|
+
const active_turns_1 = require("./active-turns");
|
|
80
|
+
const tool_activity_callbacks_1 = require("../../heart/tool-activity-callbacks");
|
|
81
|
+
const commands_1 = require("../commands");
|
|
82
|
+
const trust_gate_1 = require("../trust-gate");
|
|
83
|
+
const pipeline_1 = require("../pipeline");
|
|
84
|
+
const bbFailoverStates = new Map();
|
|
85
|
+
const bbInFlightMessageGuids = new Set();
|
|
86
|
+
// Enrich reaction text with the original message content for context.
|
|
87
|
+
// If originalText is provided and non-empty, format as: baseText to: "truncated"
|
|
88
|
+
// Otherwise return baseText unchanged.
|
|
89
|
+
function enrichReactionText(baseText, originalText, maxLen) {
|
|
90
|
+
if (!originalText)
|
|
91
|
+
return baseText;
|
|
92
|
+
const truncated = originalText.length > maxLen
|
|
93
|
+
? originalText.slice(0, maxLen - 3) + "..."
|
|
94
|
+
: originalText;
|
|
95
|
+
return `${baseText} to: "${truncated}"`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Accumulates status descriptions and debounces them.
|
|
99
|
+
* If multiple descriptions arrive within `delayMs`, they are joined with ` · `
|
|
100
|
+
* and sent as a single message. Flush sends immediately and clears the timer.
|
|
101
|
+
*/
|
|
102
|
+
function createStatusBatcher(send, delayMs) {
|
|
103
|
+
(0, runtime_1.emitNervesEvent)({
|
|
104
|
+
component: "senses",
|
|
105
|
+
event: "senses.bluebubbles_status_batcher_created",
|
|
106
|
+
message: "status batcher initialized",
|
|
107
|
+
meta: { delayMs },
|
|
108
|
+
});
|
|
109
|
+
let pending = [];
|
|
110
|
+
let timer = null;
|
|
111
|
+
function fire() {
|
|
112
|
+
if (pending.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
const combined = pending.join(" \u00b7 ");
|
|
115
|
+
pending = [];
|
|
116
|
+
timer = null;
|
|
117
|
+
send(combined);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
add(text) {
|
|
121
|
+
pending.push(text);
|
|
122
|
+
if (timer !== null)
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
timer = setTimeout(fire, delayMs);
|
|
125
|
+
},
|
|
126
|
+
flush() {
|
|
127
|
+
if (timer !== null) {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
timer = null;
|
|
130
|
+
}
|
|
131
|
+
fire();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function blueBubblesMessageKey(sessionKey, messageGuid) {
|
|
136
|
+
return `${sessionKey}:${messageGuid.trim()}`;
|
|
137
|
+
}
|
|
138
|
+
function isBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
139
|
+
if (!messageGuid.trim())
|
|
140
|
+
return false;
|
|
141
|
+
return bbInFlightMessageGuids.has(blueBubblesMessageKey(sessionKey, messageGuid));
|
|
142
|
+
}
|
|
143
|
+
function beginBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
144
|
+
if (!messageGuid.trim())
|
|
145
|
+
return true;
|
|
146
|
+
const key = blueBubblesMessageKey(sessionKey, messageGuid);
|
|
147
|
+
if (bbInFlightMessageGuids.has(key))
|
|
148
|
+
return false;
|
|
149
|
+
bbInFlightMessageGuids.add(key);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
function endBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
153
|
+
bbInFlightMessageGuids.delete(blueBubblesMessageKey(sessionKey, messageGuid));
|
|
154
|
+
}
|
|
155
|
+
const defaultDeps = {
|
|
156
|
+
getAgentName: identity_1.getAgentName,
|
|
157
|
+
buildSystem: prompt_1.buildSystem,
|
|
158
|
+
runAgent: core_1.runAgent,
|
|
159
|
+
loadSession: context_1.loadSession,
|
|
160
|
+
postTurnTrim: context_1.postTurnTrim,
|
|
161
|
+
deferPostTurnPersist: context_1.deferPostTurnPersist,
|
|
162
|
+
sessionPath: config_1.sessionPath,
|
|
163
|
+
accumulateFriendTokens: tokens_1.accumulateFriendTokens,
|
|
164
|
+
createClient: () => (0, client_1.createBlueBubblesClient)(),
|
|
165
|
+
recordMutation: mutation_log_1.recordBlueBubblesMutation,
|
|
166
|
+
createFriendStore: () => new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentRoot)(), "friends")),
|
|
167
|
+
createFriendResolver: (store, params) => new resolver_1.FriendResolver(store, params),
|
|
168
|
+
createServer: http.createServer,
|
|
169
|
+
getOwnHandles: () => [...(0, config_1.getBlueBubblesConfig)().ownHandles, ...discoveredOwnHandles],
|
|
170
|
+
};
|
|
171
|
+
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
172
|
+
const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
|
|
173
|
+
const BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS = 30_000;
|
|
174
|
+
const BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS = 2 * 60_000;
|
|
175
|
+
const BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS = 10 * 60_000;
|
|
176
|
+
const BLUEBUBBLES_LIVE_TURN_STALLED_MS = 90_000;
|
|
177
|
+
const BLUEBUBBLES_SILENCE_WATCHDOG_MS = 75_000;
|
|
178
|
+
const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
|
|
179
|
+
const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
|
|
180
|
+
const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
|
|
181
|
+
const BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS = 24 * 60 * 60 * 1000;
|
|
182
|
+
const BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS = 10 * 60 * 1000;
|
|
183
|
+
class BlueBubblesRecoveryTurnTimeoutError extends Error {
|
|
184
|
+
constructor(timeoutMs) {
|
|
185
|
+
super(`bluebubbles recovery turn timed out after ${timeoutMs}ms`);
|
|
186
|
+
this.name = "BlueBubblesRecoveryTurnTimeoutError";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function resolveFriendParams(event) {
|
|
190
|
+
if (event.chat.isGroup) {
|
|
191
|
+
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
192
|
+
return {
|
|
193
|
+
provider: "imessage-handle",
|
|
194
|
+
externalId: `group:${groupKey}`,
|
|
195
|
+
displayName: event.chat.displayName ?? "Unknown Group",
|
|
196
|
+
channel: "bluebubbles",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
provider: "imessage-handle",
|
|
201
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
202
|
+
displayName: event.sender.displayName || "Unknown",
|
|
203
|
+
channel: "bluebubbles",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function resolveGroupExternalId(event) {
|
|
207
|
+
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
208
|
+
return `group:${groupKey}`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if any participant in a group chat is a known family member.
|
|
212
|
+
* Looks up each participant handle in the friend store.
|
|
213
|
+
*/
|
|
214
|
+
async function checkGroupHasFamilyMember(store, event) {
|
|
215
|
+
if (!event.chat.isGroup)
|
|
216
|
+
return false;
|
|
217
|
+
for (const handle of event.chat.participantHandles ?? []) {
|
|
218
|
+
const friend = await store.findByExternalId("imessage-handle", handle);
|
|
219
|
+
if (friend?.trustLevel === "family")
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Check if an acquaintance shares any group chat with a family member.
|
|
226
|
+
* Compares group-prefixed externalIds between the acquaintance and all family members.
|
|
227
|
+
*/
|
|
228
|
+
async function checkHasExistingGroupWithFamily(store, senderFriend) {
|
|
229
|
+
const trustLevel = senderFriend.trustLevel ?? "friend";
|
|
230
|
+
if (trustLevel !== "acquaintance")
|
|
231
|
+
return false;
|
|
232
|
+
const acquaintanceGroups = new Set((senderFriend.externalIds ?? [])
|
|
233
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
234
|
+
.map((eid) => eid.externalId));
|
|
235
|
+
if (acquaintanceGroups.size === 0)
|
|
236
|
+
return false;
|
|
237
|
+
const allFriends = await (store.listAll?.() ?? Promise.resolve([]));
|
|
238
|
+
for (const friend of allFriends) {
|
|
239
|
+
if (friend.trustLevel !== "family")
|
|
240
|
+
continue;
|
|
241
|
+
const friendGroups = (friend.externalIds ?? [])
|
|
242
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
243
|
+
.map((eid) => eid.externalId);
|
|
244
|
+
for (const group of friendGroups) {
|
|
245
|
+
if (acquaintanceGroups.has(group))
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
function extractMessageText(content) {
|
|
252
|
+
if (typeof content === "string")
|
|
253
|
+
return content;
|
|
254
|
+
if (!Array.isArray(content))
|
|
255
|
+
return "";
|
|
256
|
+
return content
|
|
257
|
+
.map((part) => {
|
|
258
|
+
if (part && typeof part === "object" && "type" in part && part.type === "text" && typeof part.text === "string") {
|
|
259
|
+
return part.text;
|
|
260
|
+
}
|
|
261
|
+
return "";
|
|
262
|
+
})
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.join("\n");
|
|
265
|
+
}
|
|
266
|
+
function isHistoricalLaneMetadataLine(line) {
|
|
267
|
+
return /^\[(conversation scope|recent active lanes|routing control):?/i.test(line)
|
|
268
|
+
|| /^- (top_level|thread:[^:]+):/i.test(line);
|
|
269
|
+
}
|
|
270
|
+
function extractHistoricalLaneSummary(messages) {
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
const summaries = [];
|
|
273
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
274
|
+
const message = messages[index];
|
|
275
|
+
if (message.role !== "user")
|
|
276
|
+
continue;
|
|
277
|
+
const text = extractMessageText(message.content);
|
|
278
|
+
if (!text)
|
|
279
|
+
continue;
|
|
280
|
+
const firstLine = text.split("\n")[0].trim();
|
|
281
|
+
const threadMatch = firstLine.match(/thread id: ([^\]|]+)/i);
|
|
282
|
+
const laneKey = threadMatch
|
|
283
|
+
? `thread:${threadMatch[1].trim()}`
|
|
284
|
+
: /top[-_]level/i.test(firstLine)
|
|
285
|
+
? "top_level"
|
|
286
|
+
: null;
|
|
287
|
+
if (!laneKey || seen.has(laneKey))
|
|
288
|
+
continue;
|
|
289
|
+
seen.add(laneKey);
|
|
290
|
+
const snippet = text
|
|
291
|
+
.split("\n")
|
|
292
|
+
.slice(1)
|
|
293
|
+
.map((line) => line.trim())
|
|
294
|
+
.find((line) => line.length > 0 && !isHistoricalLaneMetadataLine(line))
|
|
295
|
+
?.slice(0, 80) ?? "(no recent text)";
|
|
296
|
+
summaries.push({
|
|
297
|
+
key: laneKey,
|
|
298
|
+
label: laneKey === "top_level" ? "top_level" : laneKey,
|
|
299
|
+
snippet,
|
|
300
|
+
});
|
|
301
|
+
if (summaries.length >= 5)
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
return summaries;
|
|
305
|
+
}
|
|
306
|
+
function buildConversationScopePrefix(event, existingMessages, repliedToText) {
|
|
307
|
+
if (event.kind !== "message") {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
const summaries = extractHistoricalLaneSummary(existingMessages);
|
|
311
|
+
const lines = [];
|
|
312
|
+
if (event.threadOriginatorGuid?.trim()) {
|
|
313
|
+
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]`);
|
|
314
|
+
if (repliedToText) {
|
|
315
|
+
lines.push(`[replying to: "${repliedToText}"]`);
|
|
316
|
+
}
|
|
317
|
+
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.]`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target for this turn: top_level]");
|
|
321
|
+
}
|
|
322
|
+
if (summaries.length > 0) {
|
|
323
|
+
lines.push("[recent active lanes]");
|
|
324
|
+
for (const summary of summaries) {
|
|
325
|
+
lines.push(`- ${summary.label}: ${summary.snippet}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (event.threadOriginatorGuid?.trim() || summaries.some((summary) => summary.key.startsWith("thread:"))) {
|
|
329
|
+
lines.push("[routing control: use bluebubbles_set_reply_target with target=top_level to widen back out, or target=thread plus a listed thread id to route into a specific active thread]");
|
|
330
|
+
}
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
function buildInboundText(event, existingMessages, repliedToText) {
|
|
334
|
+
const metadataPrefix = buildConversationScopePrefix(event, existingMessages, repliedToText);
|
|
335
|
+
const baseText = event.repairNotice?.trim()
|
|
336
|
+
? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
|
|
337
|
+
: event.textForAgent;
|
|
338
|
+
if (!event.chat.isGroup) {
|
|
339
|
+
return metadataPrefix ? `${metadataPrefix}\n${baseText}` : baseText;
|
|
340
|
+
}
|
|
341
|
+
const scopedText = metadataPrefix ? `${metadataPrefix}\n${baseText}` : baseText;
|
|
342
|
+
if (event.kind === "mutation") {
|
|
343
|
+
return `${event.sender.displayName} ${scopedText}`;
|
|
344
|
+
}
|
|
345
|
+
return `${event.sender.displayName}: ${scopedText}`;
|
|
346
|
+
}
|
|
347
|
+
function buildInboundContent(event, existingMessages, repliedToText) {
|
|
348
|
+
const text = buildInboundText(event, existingMessages, repliedToText);
|
|
349
|
+
if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
|
|
350
|
+
return text;
|
|
351
|
+
}
|
|
352
|
+
return [
|
|
353
|
+
{ type: "text", text },
|
|
354
|
+
...event.inputPartsForAgent,
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
function sessionLikelyContainsMessage(event, existingMessages) {
|
|
358
|
+
const fragment = event.textForAgent.trim();
|
|
359
|
+
if (!fragment)
|
|
360
|
+
return false;
|
|
361
|
+
return existingMessages.some((message) => {
|
|
362
|
+
if (message.role !== "user")
|
|
363
|
+
return false;
|
|
364
|
+
return extractMessageText(message.content).includes(fragment);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function mutationEntryToEvent(entry) {
|
|
368
|
+
return {
|
|
369
|
+
kind: "mutation",
|
|
370
|
+
eventType: entry.eventType,
|
|
371
|
+
mutationType: entry.mutationType,
|
|
372
|
+
messageGuid: entry.messageGuid,
|
|
373
|
+
targetMessageGuid: entry.targetMessageGuid ?? undefined,
|
|
374
|
+
timestamp: Date.parse(entry.recordedAt) || Date.now(),
|
|
375
|
+
fromMe: entry.fromMe,
|
|
376
|
+
sender: {
|
|
377
|
+
provider: "imessage-handle",
|
|
378
|
+
externalId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
379
|
+
rawId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
380
|
+
displayName: entry.chatIdentifier ?? entry.chatGuid ?? "Unknown",
|
|
381
|
+
},
|
|
382
|
+
chat: {
|
|
383
|
+
chatGuid: entry.chatGuid ?? undefined,
|
|
384
|
+
chatIdentifier: entry.chatIdentifier ?? undefined,
|
|
385
|
+
displayName: undefined,
|
|
386
|
+
isGroup: Boolean(entry.chatGuid?.includes(";+;")),
|
|
387
|
+
sessionKey: entry.sessionKey,
|
|
388
|
+
sendTarget: entry.chatGuid
|
|
389
|
+
? { kind: "chat_guid", value: entry.chatGuid }
|
|
390
|
+
: { kind: "chat_identifier", value: entry.chatIdentifier ?? "unknown" },
|
|
391
|
+
participantHandles: [],
|
|
392
|
+
},
|
|
393
|
+
shouldNotifyAgent: entry.shouldNotifyAgent,
|
|
394
|
+
textForAgent: entry.textForAgent,
|
|
395
|
+
requiresRepair: true,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function getBlueBubblesContinuityIngressTexts(event) {
|
|
399
|
+
if (event.kind !== "message")
|
|
400
|
+
return [];
|
|
401
|
+
const text = event.textForAgent.trim();
|
|
402
|
+
if (text.length > 0)
|
|
403
|
+
return [text];
|
|
404
|
+
const fallbackText = (event.inputPartsForAgent ?? [])
|
|
405
|
+
.map((part) => {
|
|
406
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
407
|
+
return part.text.trim();
|
|
408
|
+
}
|
|
409
|
+
return "";
|
|
410
|
+
})
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.join("\n");
|
|
413
|
+
return fallbackText ? [fallbackText] : [];
|
|
414
|
+
}
|
|
415
|
+
function createReplyTargetController(event) {
|
|
416
|
+
const defaultTargetLabel = event.kind === "message" && event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
|
|
417
|
+
let selection = event.kind === "message" && event.threadOriginatorGuid?.trim()
|
|
418
|
+
? { target: "current_lane" }
|
|
419
|
+
: { target: "top_level" };
|
|
420
|
+
return {
|
|
421
|
+
getReplyToMessageGuid() {
|
|
422
|
+
if (event.kind !== "message")
|
|
423
|
+
return undefined;
|
|
424
|
+
if (selection.target === "top_level")
|
|
425
|
+
return undefined;
|
|
426
|
+
if (selection.target === "thread")
|
|
427
|
+
return selection.threadOriginatorGuid.trim();
|
|
428
|
+
return event.threadOriginatorGuid?.trim() ? event.messageGuid : undefined;
|
|
429
|
+
},
|
|
430
|
+
setSelection(next) {
|
|
431
|
+
selection = next;
|
|
432
|
+
if (next.target === "top_level") {
|
|
433
|
+
return "bluebubbles reply target override: top_level";
|
|
434
|
+
}
|
|
435
|
+
if (next.target === "thread") {
|
|
436
|
+
return `bluebubbles reply target override: thread:${next.threadOriginatorGuid}`;
|
|
437
|
+
}
|
|
438
|
+
return `bluebubbles reply target: using default for this turn (${defaultTargetLabel})`;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function emitBlueBubblesMarkReadWarning(chat, error) {
|
|
443
|
+
(0, runtime_1.emitNervesEvent)({
|
|
444
|
+
level: "warn",
|
|
445
|
+
component: "senses",
|
|
446
|
+
event: "senses.bluebubbles_mark_read_error",
|
|
447
|
+
message: "failed to mark bluebubbles chat as read",
|
|
448
|
+
meta: {
|
|
449
|
+
chatGuid: chat.chatGuid ?? null,
|
|
450
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVisibleActivity) {
|
|
455
|
+
let textBuffer = "";
|
|
456
|
+
let typingActive = false;
|
|
457
|
+
let queue = Promise.resolve();
|
|
458
|
+
let lastVisibleActivityMs = Date.now();
|
|
459
|
+
let silenceWatchdog = null;
|
|
460
|
+
function enqueue(operation, task) {
|
|
461
|
+
queue = queue.then(task).catch((error) => {
|
|
462
|
+
(0, runtime_1.emitNervesEvent)({
|
|
463
|
+
level: "warn",
|
|
464
|
+
component: "senses",
|
|
465
|
+
event: "senses.bluebubbles_activity_error",
|
|
466
|
+
message: "bluebubbles activity transport failed",
|
|
467
|
+
meta: { operation, reason: error instanceof Error ? error.message : String(error) },
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function startTypingNow() {
|
|
472
|
+
/* v8 ignore next -- defensive guard: callers already check typingActive @preserve */
|
|
473
|
+
if (typingActive)
|
|
474
|
+
return;
|
|
475
|
+
typingActive = true;
|
|
476
|
+
enqueue("typing_start", async () => {
|
|
477
|
+
const [markReadResult, typingResult] = await Promise.allSettled([
|
|
478
|
+
client.markChatRead(chat),
|
|
479
|
+
client.setTyping(chat, true),
|
|
480
|
+
]);
|
|
481
|
+
if (markReadResult.status === "rejected") {
|
|
482
|
+
emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
|
|
483
|
+
}
|
|
484
|
+
if (typingResult.status === "rejected") {
|
|
485
|
+
throw typingResult.reason;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function recordVisibleActivity() {
|
|
490
|
+
lastVisibleActivityMs = Date.now();
|
|
491
|
+
onVisibleActivity?.();
|
|
492
|
+
}
|
|
493
|
+
function stopSilenceWatchdog() {
|
|
494
|
+
if (silenceWatchdog === null)
|
|
495
|
+
return;
|
|
496
|
+
clearInterval(silenceWatchdog);
|
|
497
|
+
silenceWatchdog = null;
|
|
498
|
+
}
|
|
499
|
+
function startSilenceWatchdog() {
|
|
500
|
+
if (silenceWatchdog !== null)
|
|
501
|
+
return;
|
|
502
|
+
silenceWatchdog = setInterval(() => {
|
|
503
|
+
if (Date.now() - lastVisibleActivityMs < BLUEBUBBLES_SILENCE_WATCHDOG_MS)
|
|
504
|
+
return;
|
|
505
|
+
sendStatus("still working on this...");
|
|
506
|
+
}, BLUEBUBBLES_SILENCE_WATCHDOG_MS);
|
|
507
|
+
/* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
|
|
508
|
+
if (typeof silenceWatchdog.unref === "function") {
|
|
509
|
+
silenceWatchdog.unref();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function sendStatus(text) {
|
|
513
|
+
enqueue("send_status", async () => {
|
|
514
|
+
await client.sendText({
|
|
515
|
+
chat,
|
|
516
|
+
text,
|
|
517
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
518
|
+
});
|
|
519
|
+
recordVisibleActivity();
|
|
520
|
+
// Re-enable typing indicator — sending a message clears the typing bubble
|
|
521
|
+
await client.setTyping(chat, true);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const statusBatcher = createStatusBatcher((text) => sendStatus(text), 500);
|
|
525
|
+
const toolCallbacks = (0, tool_activity_callbacks_1.createToolActivityCallbacks)({
|
|
526
|
+
onDescription: (text) => statusBatcher.add(text),
|
|
527
|
+
/* v8 ignore next -- onResult only called in debug mode; tested via tool-activity-callbacks.test.ts @preserve */
|
|
528
|
+
onResult: (text) => { statusBatcher.flush(); sendStatus(text); },
|
|
529
|
+
/* v8 ignore next -- onFailure only called on tool failure; tested via tool-activity-callbacks.test.ts @preserve */
|
|
530
|
+
onFailure: (text) => { statusBatcher.flush(); sendStatus(text); },
|
|
531
|
+
isDebug: commands_1.getDebugMode,
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
onModelStart() {
|
|
535
|
+
startSilenceWatchdog();
|
|
536
|
+
if (!isGroupChat)
|
|
537
|
+
startTypingNow();
|
|
538
|
+
(0, runtime_1.emitNervesEvent)({
|
|
539
|
+
component: "senses",
|
|
540
|
+
event: "senses.bluebubbles_turn_start",
|
|
541
|
+
message: "bluebubbles turn started",
|
|
542
|
+
meta: { chatGuid: chat.chatGuid ?? null },
|
|
543
|
+
});
|
|
544
|
+
},
|
|
545
|
+
onModelStreamStart() {
|
|
546
|
+
(0, runtime_1.emitNervesEvent)({
|
|
547
|
+
component: "senses",
|
|
548
|
+
event: "senses.bluebubbles_stream_start",
|
|
549
|
+
message: "bluebubbles non-streaming response started",
|
|
550
|
+
meta: {},
|
|
551
|
+
});
|
|
552
|
+
},
|
|
553
|
+
onTextChunk(text) {
|
|
554
|
+
if (isGroupChat && !typingActive)
|
|
555
|
+
startTypingNow();
|
|
556
|
+
textBuffer += text;
|
|
557
|
+
},
|
|
558
|
+
onReasoningChunk(_text) { },
|
|
559
|
+
onToolStart(name, _args) {
|
|
560
|
+
// observe + speak are flow-control: their visible output (or lack of it) is
|
|
561
|
+
// handled outside the tool-activity callbacks. speak in particular delivers
|
|
562
|
+
// its message via onTextChunk/flushNow — we MUST NOT enqueue a "speaking..."
|
|
563
|
+
// status sendText here, which would arrive as a separate iMessage right
|
|
564
|
+
// before the actual speak content.
|
|
565
|
+
if (name === "observe" || name === "speak") {
|
|
566
|
+
(0, runtime_1.emitNervesEvent)({
|
|
567
|
+
component: "senses",
|
|
568
|
+
event: "senses.bluebubbles_tool_start",
|
|
569
|
+
message: "bluebubbles tool execution started",
|
|
570
|
+
meta: { name },
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Tool activity is a reply commitment — start typing if not already
|
|
575
|
+
if (!typingActive)
|
|
576
|
+
startTypingNow();
|
|
577
|
+
toolCallbacks.onToolStart(name, _args);
|
|
578
|
+
(0, runtime_1.emitNervesEvent)({
|
|
579
|
+
component: "senses",
|
|
580
|
+
event: "senses.bluebubbles_tool_start",
|
|
581
|
+
message: "bluebubbles tool execution started",
|
|
582
|
+
meta: { name },
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
onToolEnd(name, summary, success) {
|
|
586
|
+
// observe + speak skip the tool-activity end callback (no ✓/✗ status sent).
|
|
587
|
+
if (name !== "observe" && name !== "speak") {
|
|
588
|
+
toolCallbacks.onToolEnd(name, summary, success);
|
|
589
|
+
}
|
|
590
|
+
(0, runtime_1.emitNervesEvent)({
|
|
591
|
+
component: "senses",
|
|
592
|
+
event: "senses.bluebubbles_tool_end",
|
|
593
|
+
message: "bluebubbles tool execution completed",
|
|
594
|
+
meta: { name, success, summary },
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
onError(error, severity) {
|
|
598
|
+
sendStatus(`\u2717 ${error.message}`);
|
|
599
|
+
(0, runtime_1.emitNervesEvent)({
|
|
600
|
+
level: severity === "terminal" ? "error" : "warn",
|
|
601
|
+
component: "senses",
|
|
602
|
+
event: "senses.bluebubbles_turn_error",
|
|
603
|
+
message: "bluebubbles turn callback error",
|
|
604
|
+
meta: { severity, reason: error.message },
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
onClearText() {
|
|
608
|
+
textBuffer = "";
|
|
609
|
+
},
|
|
610
|
+
async flushNow() {
|
|
611
|
+
// Contract: throws if delivery fails. We deliberately let `client.sendText`
|
|
612
|
+
// rejections propagate so the engine's speak interception can mark the
|
|
613
|
+
// tool call as failed and tell the agent the message did not reach the
|
|
614
|
+
// friend (rather than silently logging and pretending success).
|
|
615
|
+
const trimmed = textBuffer.trim();
|
|
616
|
+
if (!trimmed)
|
|
617
|
+
return;
|
|
618
|
+
textBuffer = "";
|
|
619
|
+
await client.sendText({
|
|
620
|
+
chat,
|
|
621
|
+
text: trimmed,
|
|
622
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
623
|
+
});
|
|
624
|
+
recordVisibleActivity();
|
|
625
|
+
// Note: do NOT call client.setTyping(chat, false) here — the agent is
|
|
626
|
+
// still mid-turn, so the typing indicator stays ACTIVE.
|
|
627
|
+
(0, runtime_1.emitNervesEvent)({
|
|
628
|
+
component: "senses",
|
|
629
|
+
event: "bluebubbles.speak_flush",
|
|
630
|
+
message: "bluebubbles flushed mid-turn speak",
|
|
631
|
+
meta: { messageLength: trimmed.length },
|
|
632
|
+
});
|
|
633
|
+
},
|
|
634
|
+
async flush() {
|
|
635
|
+
statusBatcher.flush();
|
|
636
|
+
await queue;
|
|
637
|
+
const trimmed = textBuffer.trim();
|
|
638
|
+
if (!trimmed) {
|
|
639
|
+
if (typingActive) {
|
|
640
|
+
typingActive = false;
|
|
641
|
+
enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
|
|
642
|
+
await queue;
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
textBuffer = "";
|
|
647
|
+
/* v8 ignore next 4 -- branch: typing may already be stopped before flush @preserve */
|
|
648
|
+
if (typingActive) {
|
|
649
|
+
typingActive = false;
|
|
650
|
+
enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
|
|
651
|
+
await queue;
|
|
652
|
+
}
|
|
653
|
+
await client.sendText({
|
|
654
|
+
chat,
|
|
655
|
+
text: trimmed,
|
|
656
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
657
|
+
});
|
|
658
|
+
recordVisibleActivity();
|
|
659
|
+
},
|
|
660
|
+
async finish() {
|
|
661
|
+
stopSilenceWatchdog();
|
|
662
|
+
statusBatcher.flush();
|
|
663
|
+
if (!typingActive) {
|
|
664
|
+
await queue;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
typingActive = false;
|
|
668
|
+
enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
|
|
669
|
+
await queue;
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
async function readRequestBody(req) {
|
|
674
|
+
let body = "";
|
|
675
|
+
for await (const chunk of req) {
|
|
676
|
+
body += chunk.toString();
|
|
677
|
+
}
|
|
678
|
+
return body;
|
|
679
|
+
}
|
|
680
|
+
function writeJson(res, statusCode, payload) {
|
|
681
|
+
res.statusCode = statusCode;
|
|
682
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
683
|
+
res.end(JSON.stringify(payload));
|
|
684
|
+
}
|
|
685
|
+
function isWebhookPasswordValid(url, expectedPassword) {
|
|
686
|
+
const provided = url.searchParams.get("password");
|
|
687
|
+
return !provided || provided === expectedPassword;
|
|
688
|
+
}
|
|
689
|
+
function normalizeHandleForSelfMatch(handle) {
|
|
690
|
+
const trimmed = handle.trim().toLowerCase();
|
|
691
|
+
if (!trimmed)
|
|
692
|
+
return "";
|
|
693
|
+
// Phone-shaped: strip everything but digits so +1 (415) 555-... matches 14155550000.
|
|
694
|
+
if (/[+\d]/.test(trimmed) && !trimmed.includes("@")) {
|
|
695
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
696
|
+
if (digits.length >= 7)
|
|
697
|
+
return digits;
|
|
698
|
+
}
|
|
699
|
+
return trimmed;
|
|
700
|
+
}
|
|
701
|
+
function isAgentSelfHandle(senderExternalId, ownHandles) {
|
|
702
|
+
if (!senderExternalId || !senderExternalId.trim())
|
|
703
|
+
return false;
|
|
704
|
+
const target = normalizeHandleForSelfMatch(senderExternalId);
|
|
705
|
+
/* v8 ignore start -- target is non-empty by construction since senderExternalId was just verified non-whitespace */
|
|
706
|
+
if (!target)
|
|
707
|
+
return false;
|
|
708
|
+
/* v8 ignore stop */
|
|
709
|
+
for (const own of ownHandles) {
|
|
710
|
+
if (normalizeHandleForSelfMatch(own) === target)
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* In-memory store of agent iMessage handles auto-discovered from group-chat
|
|
717
|
+
* `event.fromMe === true` events. Bluebubbles can attribute the peer's handle
|
|
718
|
+
* as `event.sender.externalId` on 1:1 outbound messages, so discovery is
|
|
719
|
+
* intentionally limited to the group echo bug that motivated
|
|
720
|
+
* `bluebubbles.ownHandles` originally.
|
|
721
|
+
*
|
|
722
|
+
* Per-process. A daemon restart re-learns from the next outbound. The
|
|
723
|
+
* accompanying nerves event (`senses.bluebubbles_own_handle_discovered`)
|
|
724
|
+
* tells the operator what to add to `bluebubbles.ownHandles` for cross-
|
|
725
|
+
* restart durability.
|
|
726
|
+
*/
|
|
727
|
+
const discoveredOwnHandles = new Set();
|
|
728
|
+
function getDiscoveredOwnHandles() {
|
|
729
|
+
return [...discoveredOwnHandles];
|
|
730
|
+
}
|
|
731
|
+
function clearDiscoveredOwnHandles() {
|
|
732
|
+
discoveredOwnHandles.clear();
|
|
733
|
+
}
|
|
734
|
+
function recordDiscoveredOwnHandle(senderExternalId) {
|
|
735
|
+
if (!senderExternalId || !senderExternalId.trim())
|
|
736
|
+
return false;
|
|
737
|
+
const trimmed = senderExternalId.trim();
|
|
738
|
+
const normalized = normalizeHandleForSelfMatch(trimmed);
|
|
739
|
+
/* v8 ignore next -- defensive: normalizeHandleForSelfMatch only returns falsy for empty input, already guarded above @preserve */
|
|
740
|
+
if (!normalized)
|
|
741
|
+
return false;
|
|
742
|
+
for (const existing of discoveredOwnHandles) {
|
|
743
|
+
if (normalizeHandleForSelfMatch(existing) === normalized)
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
discoveredOwnHandles.add(trimmed);
|
|
747
|
+
(0, runtime_1.emitNervesEvent)({
|
|
748
|
+
level: "info",
|
|
749
|
+
component: "senses",
|
|
750
|
+
event: "senses.bluebubbles_own_handle_discovered",
|
|
751
|
+
message: "captured a new agent-owned bluebubbles handle from an isFromMe outbound — add to bluebubbles.ownHandles for cross-restart durability",
|
|
752
|
+
meta: { handle: trimmed, totalDiscovered: discoveredOwnHandles.size },
|
|
753
|
+
});
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function isSelfFriendRecord(friend, agentName) {
|
|
757
|
+
if (!friend || friend.kind !== "agent")
|
|
758
|
+
return false;
|
|
759
|
+
const normalizedAgent = agentName.trim().toLowerCase();
|
|
760
|
+
const name = friend.name?.trim().toLowerCase();
|
|
761
|
+
const bundleName = friend.agentMeta?.bundleName?.trim().toLowerCase();
|
|
762
|
+
return name === normalizedAgent || bundleName === normalizedAgent;
|
|
763
|
+
}
|
|
764
|
+
async function shouldFilterAgentSelfHandle(event, resolvedDeps) {
|
|
765
|
+
if (!event.chat.isGroup)
|
|
766
|
+
return false;
|
|
767
|
+
if (!isAgentSelfHandle(event.sender.externalId, resolvedDeps.getOwnHandles()))
|
|
768
|
+
return false;
|
|
769
|
+
const store = resolvedDeps.createFriendStore();
|
|
770
|
+
const knownFriend = await store
|
|
771
|
+
.findByExternalId("imessage-handle", event.sender.externalId)
|
|
772
|
+
.catch(() => null);
|
|
773
|
+
if (knownFriend && !isSelfFriendRecord(knownFriend, resolvedDeps.getAgentName())) {
|
|
774
|
+
(0, runtime_1.emitNervesEvent)({
|
|
775
|
+
level: "warn",
|
|
776
|
+
component: "senses",
|
|
777
|
+
event: "senses.bluebubbles_self_handle_bypassed_known_friend",
|
|
778
|
+
message: "did not filter bluebubbles sender even though it matched ownHandles because it resolves to a known non-self friend",
|
|
779
|
+
meta: {
|
|
780
|
+
messageGuid: event.messageGuid,
|
|
781
|
+
kind: event.kind,
|
|
782
|
+
senderExternalId: event.sender.externalId,
|
|
783
|
+
friendId: knownFriend.id,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, options = {}) {
|
|
791
|
+
const client = resolvedDeps.createClient();
|
|
792
|
+
const agentName = resolvedDeps.getAgentName();
|
|
793
|
+
(0, runtime_cwd_1.recoverRuntimeCwd)();
|
|
794
|
+
if (event.fromMe) {
|
|
795
|
+
(0, runtime_1.emitNervesEvent)({
|
|
796
|
+
component: "senses",
|
|
797
|
+
event: "senses.bluebubbles_from_me_ignored",
|
|
798
|
+
message: "ignored from-me bluebubbles event",
|
|
799
|
+
meta: {
|
|
800
|
+
messageGuid: event.messageGuid,
|
|
801
|
+
kind: event.kind,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
if (event.chat.isGroup)
|
|
805
|
+
recordDiscoveredOwnHandle(event.sender.externalId);
|
|
806
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
|
|
807
|
+
}
|
|
808
|
+
// Fallback self-detection: BlueBubbles sometimes broadcasts a group-chat
|
|
809
|
+
// outbound message back through the webhook with `isFromMe` missing/false.
|
|
810
|
+
// Without this guard the agent ingests its own message and replies to it
|
|
811
|
+
// ("Slugger talking to himself"). Compare the sender's externalId against
|
|
812
|
+
// the agent's known iMessage handles. Keep this group-only: 1:1 outbound
|
|
813
|
+
// echoes can be attributed to the peer handle, and stale ownHandles entries
|
|
814
|
+
// must not make real DMs disappear.
|
|
815
|
+
if (await shouldFilterAgentSelfHandle(event, resolvedDeps)) {
|
|
816
|
+
(0, runtime_1.emitNervesEvent)({
|
|
817
|
+
level: "warn",
|
|
818
|
+
component: "senses",
|
|
819
|
+
event: "senses.bluebubbles_self_handle_filtered",
|
|
820
|
+
message: "filtered bluebubbles event whose sender matched an agent-owned handle (isFromMe was missing/false)",
|
|
821
|
+
meta: {
|
|
822
|
+
messageGuid: event.messageGuid,
|
|
823
|
+
kind: event.kind,
|
|
824
|
+
senderExternalId: event.sender.externalId,
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
|
|
828
|
+
}
|
|
829
|
+
if (event.kind === "mutation") {
|
|
830
|
+
try {
|
|
831
|
+
resolvedDeps.recordMutation(resolvedDeps.getAgentName(), event);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
(0, runtime_1.emitNervesEvent)({
|
|
835
|
+
level: "error",
|
|
836
|
+
component: "senses",
|
|
837
|
+
event: "senses.bluebubbles_mutation_log_error",
|
|
838
|
+
message: "failed recording bluebubbles mutation sidecar",
|
|
839
|
+
meta: {
|
|
840
|
+
messageGuid: event.messageGuid,
|
|
841
|
+
mutationType: event.mutationType,
|
|
842
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (event.kind === "mutation" && !event.shouldNotifyAgent) {
|
|
848
|
+
(0, runtime_1.emitNervesEvent)({
|
|
849
|
+
component: "senses",
|
|
850
|
+
event: "senses.bluebubbles_state_mutation_recorded",
|
|
851
|
+
message: "recorded non-notify bluebubbles mutation",
|
|
852
|
+
meta: {
|
|
853
|
+
messageGuid: event.messageGuid,
|
|
854
|
+
mutationType: event.mutationType,
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
858
|
+
}
|
|
859
|
+
let ownsInFlightMessage = false;
|
|
860
|
+
let releaseInFlightAfterTurnSettles = false;
|
|
861
|
+
let activeTurnId = null;
|
|
862
|
+
if (event.kind === "message") {
|
|
863
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
|
|
864
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)) {
|
|
865
|
+
(0, runtime_1.emitNervesEvent)({
|
|
866
|
+
component: "senses",
|
|
867
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
868
|
+
message: "skipped bluebubbles message already marked as handled",
|
|
869
|
+
meta: {
|
|
870
|
+
messageGuid: event.messageGuid,
|
|
871
|
+
sessionKey: event.chat.sessionKey,
|
|
872
|
+
source,
|
|
873
|
+
dedupeReason: "processed",
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
877
|
+
}
|
|
878
|
+
if (!beginBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
|
|
879
|
+
(0, runtime_1.emitNervesEvent)({
|
|
880
|
+
component: "senses",
|
|
881
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
882
|
+
message: "skipped bluebubbles message already in flight",
|
|
883
|
+
meta: {
|
|
884
|
+
messageGuid: event.messageGuid,
|
|
885
|
+
sessionKey: event.chat.sessionKey,
|
|
886
|
+
source,
|
|
887
|
+
dedupeReason: "in_flight",
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
891
|
+
}
|
|
892
|
+
ownsInFlightMessage = true;
|
|
893
|
+
activeTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
// ── Adapter setup: friend, session, content, callbacks ──────────
|
|
897
|
+
const store = resolvedDeps.createFriendStore();
|
|
898
|
+
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
899
|
+
const baseContext = await resolver.resolve();
|
|
900
|
+
const context = { ...baseContext, isGroupChat: event.chat.isGroup };
|
|
901
|
+
const replyTarget = createReplyTargetController(event);
|
|
902
|
+
const friendId = context.friend.id;
|
|
903
|
+
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
904
|
+
try {
|
|
905
|
+
(0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
(0, runtime_1.emitNervesEvent)({
|
|
909
|
+
level: "warn",
|
|
910
|
+
component: "senses",
|
|
911
|
+
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
912
|
+
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
913
|
+
meta: {
|
|
914
|
+
sessionPath: sessPath,
|
|
915
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
return await (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
920
|
+
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
921
|
+
const existing = resolvedDeps.loadSession(sessPath);
|
|
922
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
923
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
924
|
+
? existing.messages
|
|
925
|
+
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
|
|
926
|
+
if (event.kind === "message") {
|
|
927
|
+
// Record EARLY for audit and crash recovery. This is capture truth, not
|
|
928
|
+
// a claim that the agent turn completed successfully.
|
|
929
|
+
const inboundSource = source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
|
|
930
|
+
? "recovery-bootstrap"
|
|
931
|
+
: source;
|
|
932
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
|
|
933
|
+
if (inboundSource === "recovery-bootstrap") {
|
|
934
|
+
(0, runtime_1.emitNervesEvent)({
|
|
935
|
+
component: "senses",
|
|
936
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
937
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
938
|
+
meta: {
|
|
939
|
+
messageGuid: event.messageGuid,
|
|
940
|
+
sessionKey: event.chat.sessionKey,
|
|
941
|
+
source,
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
|
|
945
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (event.kind === "message" && event.chat.isGroup) {
|
|
949
|
+
await (0, group_context_1.upsertGroupContextParticipants)({
|
|
950
|
+
store,
|
|
951
|
+
participants: (event.chat.participantHandles ?? []).map((externalId) => ({
|
|
952
|
+
provider: "imessage-handle",
|
|
953
|
+
externalId,
|
|
954
|
+
})),
|
|
955
|
+
groupExternalId: resolveGroupExternalId(event),
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
// Fetch the text of the message being replied to (if this is a threaded reply)
|
|
959
|
+
const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
|
|
960
|
+
let repliedToText = null;
|
|
961
|
+
if (threadGuid) {
|
|
962
|
+
repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
|
|
963
|
+
(0, runtime_1.emitNervesEvent)({
|
|
964
|
+
component: "senses",
|
|
965
|
+
event: "senses.bluebubbles_reply_context",
|
|
966
|
+
message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
|
|
967
|
+
meta: { threadGuid, hasText: !!repliedToText },
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
// Enrich reaction mutations with the original message text for context
|
|
971
|
+
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
972
|
+
if (isReaction && event.targetMessageGuid) {
|
|
973
|
+
/* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
|
|
974
|
+
const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
|
|
975
|
+
if (originalText)
|
|
976
|
+
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
977
|
+
/* v8 ignore stop */
|
|
978
|
+
}
|
|
979
|
+
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
980
|
+
const userMessage = {
|
|
981
|
+
role: "user",
|
|
982
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
|
|
983
|
+
};
|
|
984
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, activeTurnId
|
|
985
|
+
? () => (0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, activeTurnId)
|
|
986
|
+
: undefined);
|
|
987
|
+
const controller = new AbortController();
|
|
988
|
+
let timeoutTimer;
|
|
989
|
+
let timeoutPromise;
|
|
990
|
+
let timeoutReject;
|
|
991
|
+
let recoveryTimedOut = false;
|
|
992
|
+
// BB-specific tool context wrappers
|
|
993
|
+
const summarize = (0, core_1.createSummarize)("human");
|
|
994
|
+
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
995
|
+
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
996
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
997
|
+
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
998
|
+
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
999
|
+
? false
|
|
1000
|
+
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
1001
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
1002
|
+
// Buffer terminal errors so failover can suppress them.
|
|
1003
|
+
// If failover produces a message, the buffered error is skipped.
|
|
1004
|
+
// If failover doesn't fire, the buffered error is replayed.
|
|
1005
|
+
let bufferedTerminalError = null;
|
|
1006
|
+
/* v8 ignore start -- failover-aware error buffering @preserve */
|
|
1007
|
+
const failoverAwareCallbacks = {
|
|
1008
|
+
...callbacks,
|
|
1009
|
+
onError(error, severity) {
|
|
1010
|
+
if (severity === "terminal") {
|
|
1011
|
+
bufferedTerminalError = error;
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
callbacks.onError(error, severity);
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
/* v8 ignore stop */
|
|
1018
|
+
try {
|
|
1019
|
+
const liveWebhookTimeout = source === "webhook" && options.timeoutMs === undefined;
|
|
1020
|
+
const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
|
|
1021
|
+
timeoutPromise = new Promise((_, reject) => {
|
|
1022
|
+
timeoutReject = reject;
|
|
1023
|
+
});
|
|
1024
|
+
timeoutTimer = setTimeout(() => {
|
|
1025
|
+
const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
|
|
1026
|
+
recoveryTimedOut = true;
|
|
1027
|
+
if (liveWebhookTimeout && ownsInFlightMessage && event.kind === "message") {
|
|
1028
|
+
endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
|
|
1029
|
+
ownsInFlightMessage = false;
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
releaseInFlightAfterTurnSettles = true;
|
|
1033
|
+
}
|
|
1034
|
+
controller.abort(reason);
|
|
1035
|
+
timeoutReject?.(reason);
|
|
1036
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1037
|
+
level: "warn",
|
|
1038
|
+
component: "senses",
|
|
1039
|
+
event: "senses.bluebubbles_turn_timeout",
|
|
1040
|
+
message: "bluebubbles turn timed out",
|
|
1041
|
+
meta: {
|
|
1042
|
+
messageGuid: event.messageGuid,
|
|
1043
|
+
sessionKey: event.chat.sessionKey,
|
|
1044
|
+
source,
|
|
1045
|
+
timeoutMs,
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
}, timeoutMs);
|
|
1049
|
+
/* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
|
|
1050
|
+
if (typeof timeoutTimer.unref === "function") {
|
|
1051
|
+
timeoutTimer.unref();
|
|
1052
|
+
}
|
|
1053
|
+
const turnPromise = (0, pipeline_1.handleInboundTurn)({
|
|
1054
|
+
channel: "bluebubbles",
|
|
1055
|
+
sessionKey: event.chat.sessionKey,
|
|
1056
|
+
capabilities: bbCapabilities,
|
|
1057
|
+
messages: [userMessage],
|
|
1058
|
+
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
1059
|
+
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
1060
|
+
sessionLoader: {
|
|
1061
|
+
loadOrCreate: () => Promise.resolve({
|
|
1062
|
+
messages: sessionMessages,
|
|
1063
|
+
sessionPath: sessPath,
|
|
1064
|
+
state: existing?.state,
|
|
1065
|
+
events: existing?.events,
|
|
1066
|
+
}),
|
|
1067
|
+
},
|
|
1068
|
+
pendingDir,
|
|
1069
|
+
friendStore: store,
|
|
1070
|
+
provider: "imessage-handle",
|
|
1071
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
1072
|
+
isGroupChat: event.chat.isGroup,
|
|
1073
|
+
groupHasFamilyMember,
|
|
1074
|
+
hasExistingGroupWithFamily,
|
|
1075
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
1076
|
+
drainPending: pending_1.drainPending,
|
|
1077
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
|
|
1078
|
+
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
1079
|
+
...opts,
|
|
1080
|
+
toolContext: {
|
|
1081
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
1082
|
+
signin: async () => undefined,
|
|
1083
|
+
...opts?.toolContext,
|
|
1084
|
+
summarize,
|
|
1085
|
+
bluebubblesReplyTarget: {
|
|
1086
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
1087
|
+
},
|
|
1088
|
+
codingFeedback: {
|
|
1089
|
+
send: async (message) => {
|
|
1090
|
+
await client.sendText({
|
|
1091
|
+
chat: event.chat,
|
|
1092
|
+
text: message,
|
|
1093
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
1094
|
+
});
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
}),
|
|
1099
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
1100
|
+
const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
|
|
1101
|
+
resolvedDeps.deferPostTurnPersist(sessionPathArg, prepared, usage, state);
|
|
1102
|
+
},
|
|
1103
|
+
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
1104
|
+
signal: controller.signal,
|
|
1105
|
+
runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
|
|
1106
|
+
callbacks: failoverAwareCallbacks,
|
|
1107
|
+
failoverState: (() => {
|
|
1108
|
+
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
|
1109
|
+
bbFailoverStates.set(event.chat.sessionKey, { pending: null });
|
|
1110
|
+
}
|
|
1111
|
+
return bbFailoverStates.get(event.chat.sessionKey);
|
|
1112
|
+
})(),
|
|
1113
|
+
});
|
|
1114
|
+
/* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
|
|
1115
|
+
void turnPromise
|
|
1116
|
+
.catch((error) => {
|
|
1117
|
+
if (!recoveryTimedOut)
|
|
1118
|
+
return;
|
|
1119
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1120
|
+
level: "warn",
|
|
1121
|
+
component: "senses",
|
|
1122
|
+
event: "senses.bluebubbles_recovery_error",
|
|
1123
|
+
message: "bluebubbles recovery turn rejected after timeout",
|
|
1124
|
+
meta: {
|
|
1125
|
+
messageGuid: event.messageGuid,
|
|
1126
|
+
sessionKey: event.chat.sessionKey,
|
|
1127
|
+
source,
|
|
1128
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1129
|
+
},
|
|
1130
|
+
});
|
|
1131
|
+
})
|
|
1132
|
+
.finally(() => {
|
|
1133
|
+
if (releaseInFlightAfterTurnSettles && ownsInFlightMessage && event.kind === "message") {
|
|
1134
|
+
endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
/* v8 ignore stop */
|
|
1138
|
+
const result = await (async () => {
|
|
1139
|
+
try {
|
|
1140
|
+
return await Promise.race([turnPromise, timeoutPromise]);
|
|
1141
|
+
}
|
|
1142
|
+
catch (error) {
|
|
1143
|
+
if (error instanceof BlueBubblesRecoveryTurnTimeoutError) {
|
|
1144
|
+
callbacks.onError(new Error("live iMessage turn timed out; I captured it for recovery instead of silently hanging"), "terminal");
|
|
1145
|
+
}
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
})();
|
|
1149
|
+
/* v8 ignore start -- failover display + error replay @preserve */
|
|
1150
|
+
if (result.failoverMessage) {
|
|
1151
|
+
// Failover handled it — show the failover message, skip the buffered error
|
|
1152
|
+
await client.sendText({ chat: event.chat, text: result.failoverMessage });
|
|
1153
|
+
}
|
|
1154
|
+
else if (bufferedTerminalError) {
|
|
1155
|
+
// No failover — replay the buffered terminal error
|
|
1156
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
1157
|
+
}
|
|
1158
|
+
/* v8 ignore stop */
|
|
1159
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
1160
|
+
if (!result.gateResult.allowed) {
|
|
1161
|
+
// Send auto-reply via BB API if the gate provides one
|
|
1162
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
1163
|
+
await client.sendText({
|
|
1164
|
+
chat: event.chat,
|
|
1165
|
+
text: result.gateResult.autoReply,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (event.kind === "message") {
|
|
1169
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, source, "trust-gated");
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
handled: true,
|
|
1173
|
+
notifiedAgent: false,
|
|
1174
|
+
kind: event.kind,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
// Gate allowed — flush the agent's reply
|
|
1178
|
+
await callbacks.flush();
|
|
1179
|
+
if (event.kind === "message") {
|
|
1180
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, source, "turn-complete");
|
|
1181
|
+
}
|
|
1182
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1183
|
+
component: "senses",
|
|
1184
|
+
event: "senses.bluebubbles_turn_end",
|
|
1185
|
+
message: "bluebubbles event handled",
|
|
1186
|
+
meta: {
|
|
1187
|
+
messageGuid: event.messageGuid,
|
|
1188
|
+
kind: event.kind,
|
|
1189
|
+
sessionKey: event.chat.sessionKey,
|
|
1190
|
+
},
|
|
1191
|
+
});
|
|
1192
|
+
return {
|
|
1193
|
+
handled: true,
|
|
1194
|
+
notifiedAgent: true,
|
|
1195
|
+
kind: event.kind,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
finally {
|
|
1199
|
+
// If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
|
|
1200
|
+
// replay it now so the user still sees the error.
|
|
1201
|
+
/* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
|
|
1202
|
+
if (bufferedTerminalError) {
|
|
1203
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
1204
|
+
bufferedTerminalError = null;
|
|
1205
|
+
}
|
|
1206
|
+
/* v8 ignore stop */
|
|
1207
|
+
clearTimeout(timeoutTimer);
|
|
1208
|
+
await callbacks.finish();
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
1213
|
+
if (activeTurnId)
|
|
1214
|
+
(0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, activeTurnId);
|
|
1215
|
+
if (ownsInFlightMessage && event.kind === "message" && !releaseInFlightAfterTurnSettles) {
|
|
1216
|
+
endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
1221
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1222
|
+
const client = resolvedDeps.createClient();
|
|
1223
|
+
let normalized;
|
|
1224
|
+
try {
|
|
1225
|
+
normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
if (error instanceof model_1.BlueBubblesIgnoredEventError) {
|
|
1229
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1230
|
+
component: "senses",
|
|
1231
|
+
event: "senses.bluebubbles_event_skipped",
|
|
1232
|
+
message: "skipped ignorable bluebubbles event",
|
|
1233
|
+
meta: {
|
|
1234
|
+
eventType: error.eventType,
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
handled: true,
|
|
1239
|
+
notifiedAgent: false,
|
|
1240
|
+
reason: "ignored",
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
throw error;
|
|
1244
|
+
}
|
|
1245
|
+
// Pre-repair dedup: if we've already processed this messageGuid, skip the
|
|
1246
|
+
// repair+hydrate path entirely. Applies to BOTH `kind: "message"` AND
|
|
1247
|
+
// `kind: "mutation"` events — BlueBubbles often sends a `new-message`
|
|
1248
|
+
// webhook for a fresh message AND one or more follow-up `updated-message`
|
|
1249
|
+
// webhooks for delivery/read status. The mutation path (inside repairEvent)
|
|
1250
|
+
// can promote an updated-message back to a message if it has recoverable
|
|
1251
|
+
// content, which then re-runs the full VLM-describe pipeline on the same
|
|
1252
|
+
// attachment.
|
|
1253
|
+
//
|
|
1254
|
+
// Without this early check, we paid DOUBLE latency and double tokens on
|
|
1255
|
+
// every image-bearing message. Verified live on 2026-04-08T00:58Z: two
|
|
1256
|
+
// sequential VLM describes for attachment guid 317E37EB-..., 13.7s +
|
|
1257
|
+
// 14.0s each, for the exact same 291KB JPEG — triggered by a sequence of
|
|
1258
|
+
// `new-message` followed ~3s later by `updated-message` for the same guid.
|
|
1259
|
+
//
|
|
1260
|
+
// We still route the skip through `handleBlueBubblesNormalizedEvent` so
|
|
1261
|
+
// the downstream `already_processed` path fires its observability events
|
|
1262
|
+
// and the caller sees a consistent return shape.
|
|
1263
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1264
|
+
// normalizeBlueBubblesEvent rejects guidless payloads, so duplicate handling
|
|
1265
|
+
// only needs to discriminate between known processed, in-flight, or new.
|
|
1266
|
+
const duplicateReason = (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, normalized.chat.sessionKey, normalized.messageGuid)
|
|
1267
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, normalized.messageGuid)
|
|
1268
|
+
? "processed"
|
|
1269
|
+
: isBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid)
|
|
1270
|
+
? "in_flight"
|
|
1271
|
+
: null;
|
|
1272
|
+
if (normalized.messageGuid
|
|
1273
|
+
&& duplicateReason) {
|
|
1274
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1275
|
+
level: "warn",
|
|
1276
|
+
component: "senses",
|
|
1277
|
+
event: "senses.bluebubbles_repair_skipped_duplicate",
|
|
1278
|
+
message: "skipped repair+hydrate for already-processed bluebubbles messageGuid",
|
|
1279
|
+
meta: {
|
|
1280
|
+
messageGuid: normalized.messageGuid,
|
|
1281
|
+
sessionKey: normalized.chat.sessionKey,
|
|
1282
|
+
eventType: normalized.eventType,
|
|
1283
|
+
normalizedKind: normalized.kind,
|
|
1284
|
+
dedupeReason: duplicateReason,
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
return handleBlueBubblesNormalizedEvent(normalized, resolvedDeps, "webhook");
|
|
1288
|
+
}
|
|
1289
|
+
const event = await client.repairEvent(normalized);
|
|
1290
|
+
return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
|
|
1291
|
+
}
|
|
1292
|
+
function listPendingCapturedInboundMessages(agentName) {
|
|
1293
|
+
const seenMessageGuids = new Set();
|
|
1294
|
+
return (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
|
|
1295
|
+
.filter((entry) => {
|
|
1296
|
+
if (seenMessageGuids.has(entry.messageGuid))
|
|
1297
|
+
return false;
|
|
1298
|
+
seenMessageGuids.add(entry.messageGuid);
|
|
1299
|
+
return true;
|
|
1300
|
+
})
|
|
1301
|
+
.filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid));
|
|
1302
|
+
}
|
|
1303
|
+
function listPendingRecoveryEntries(agentName) {
|
|
1304
|
+
const pendingByGuid = new Map();
|
|
1305
|
+
const add = (messageGuid, recordedAt) => {
|
|
1306
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, messageGuid))
|
|
1307
|
+
return;
|
|
1308
|
+
const previous = pendingByGuid.get(messageGuid);
|
|
1309
|
+
if (!previous) {
|
|
1310
|
+
pendingByGuid.set(messageGuid, recordedAt);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const previousMs = Date.parse(previous);
|
|
1314
|
+
const nextMs = Date.parse(recordedAt);
|
|
1315
|
+
if (Number.isFinite(nextMs) && (!Number.isFinite(previousMs) || nextMs < previousMs)) {
|
|
1316
|
+
pendingByGuid.set(messageGuid, recordedAt);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
for (const entry of listPendingCapturedInboundMessages(agentName)) {
|
|
1320
|
+
add(entry.messageGuid, entry.recordedAt);
|
|
1321
|
+
}
|
|
1322
|
+
for (const entry of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
|
|
1323
|
+
add(entry.messageGuid, entry.recordedAt);
|
|
1324
|
+
}
|
|
1325
|
+
return [...pendingByGuid].map(([messageGuid, recordedAt]) => ({ messageGuid, recordedAt }));
|
|
1326
|
+
}
|
|
1327
|
+
function parseTimestampMs(value) {
|
|
1328
|
+
if (!value)
|
|
1329
|
+
return null;
|
|
1330
|
+
const parsed = Date.parse(value);
|
|
1331
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1332
|
+
}
|
|
1333
|
+
function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
|
|
1334
|
+
if (previousState.upstreamStatus === "error") {
|
|
1335
|
+
return nowMs - BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS;
|
|
1336
|
+
}
|
|
1337
|
+
const lastCheckedAt = parseTimestampMs(previousState.lastCheckedAt);
|
|
1338
|
+
if (lastCheckedAt !== null) {
|
|
1339
|
+
return Math.max(0, lastCheckedAt - BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS);
|
|
1340
|
+
}
|
|
1341
|
+
return nowMs - BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS;
|
|
1342
|
+
}
|
|
1343
|
+
function formatBlueBubblesRuntimeDetail(queued, failed, active) {
|
|
1344
|
+
if (active.stalledTurnCount > 0) {
|
|
1345
|
+
return `iMessage live turn appears stalled; ${active.stalledTurnCount} active turn(s) older than ${BLUEBUBBLES_LIVE_TURN_STALLED_MS}ms`;
|
|
1346
|
+
}
|
|
1347
|
+
if (active.activeTurnCount > 0)
|
|
1348
|
+
return `upstream reachable; ${active.activeTurnCount} live turn(s) active`;
|
|
1349
|
+
if (queued > 0)
|
|
1350
|
+
return `upstream reachable but iMessage is not caught up; ${queued} recovery item(s) queued`;
|
|
1351
|
+
if (failed > 0)
|
|
1352
|
+
return `${failed} message(s) unrecoverable this cycle; upstream ok`;
|
|
1353
|
+
return "upstream reachable";
|
|
1354
|
+
}
|
|
1355
|
+
function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
|
|
1356
|
+
const pendingEntries = listPendingRecoveryEntries(agentName);
|
|
1357
|
+
const pendingRecordedAt = pendingEntries
|
|
1358
|
+
.map((entry) => entry.recordedAt)
|
|
1359
|
+
.map((value) => ({ value, ms: Date.parse(value) }))
|
|
1360
|
+
.filter((entry) => Number.isFinite(entry.ms))
|
|
1361
|
+
.sort((left, right) => left.ms - right.ms);
|
|
1362
|
+
const oldest = pendingRecordedAt[0];
|
|
1363
|
+
return {
|
|
1364
|
+
pendingRecoveryCount: pendingEntries.length,
|
|
1365
|
+
oldestPendingRecoveryAt: oldest?.value,
|
|
1366
|
+
oldestPendingRecoveryAgeMs: oldest ? Math.max(0, nowMs - oldest.ms) : undefined,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async function syncBlueBubblesRuntime(deps = {}) {
|
|
1370
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1371
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1372
|
+
const client = resolvedDeps.createClient();
|
|
1373
|
+
const checkedAt = new Date().toISOString();
|
|
1374
|
+
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
1375
|
+
try {
|
|
1376
|
+
await client.checkHealth();
|
|
1377
|
+
const pendingBeforeCatchup = blueBubblesPendingRecoverySnapshot(agentName);
|
|
1378
|
+
const activeBeforeCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
1379
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1380
|
+
upstreamStatus: "ok",
|
|
1381
|
+
detail: "upstream reachable; recovery pass running",
|
|
1382
|
+
lastCheckedAt: checkedAt,
|
|
1383
|
+
proofMethod: "bluebubbles.checkHealth",
|
|
1384
|
+
...pendingBeforeCatchup,
|
|
1385
|
+
...activeBeforeCatchup,
|
|
1386
|
+
lastRecoveredAt: previousState.lastRecoveredAt,
|
|
1387
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1388
|
+
});
|
|
1389
|
+
const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState, {
|
|
1390
|
+
processTurns: false,
|
|
1391
|
+
});
|
|
1392
|
+
const failed = catchUp.failed;
|
|
1393
|
+
const pendingAfterCatchup = blueBubblesPendingRecoverySnapshot(agentName);
|
|
1394
|
+
const activeAfterCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
1395
|
+
const queued = pendingAfterCatchup.pendingRecoveryCount;
|
|
1396
|
+
// upstreamStatus reflects whether BlueBubbles itself and the local bridge
|
|
1397
|
+
// can answer webhook traffic. The daemon status layer treats
|
|
1398
|
+
// pendingRecoveryCount as unhealthy for user-facing iMessage reachability,
|
|
1399
|
+
// while this field stays scoped to upstream transport reachability.
|
|
1400
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1401
|
+
upstreamStatus: "ok",
|
|
1402
|
+
detail: formatBlueBubblesRuntimeDetail(queued, failed, activeAfterCatchup),
|
|
1403
|
+
lastCheckedAt: checkedAt,
|
|
1404
|
+
proofMethod: "bluebubbles.checkHealth",
|
|
1405
|
+
...pendingAfterCatchup,
|
|
1406
|
+
...activeAfterCatchup,
|
|
1407
|
+
pendingRecoveryCount: queued,
|
|
1408
|
+
failedRecoveryCount: failed,
|
|
1409
|
+
lastRecoveredAt: previousState.lastRecoveredAt,
|
|
1410
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1415
|
+
upstreamStatus: "error",
|
|
1416
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1417
|
+
lastCheckedAt: checkedAt,
|
|
1418
|
+
proofMethod: "bluebubbles.checkHealth",
|
|
1419
|
+
...blueBubblesPendingRecoverySnapshot(agentName),
|
|
1420
|
+
...(0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS),
|
|
1421
|
+
failedRecoveryCount: 0,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
1426
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1427
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1428
|
+
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
1429
|
+
const initialPending = blueBubblesPendingRecoverySnapshot(agentName).pendingRecoveryCount;
|
|
1430
|
+
if (initialPending === 0) {
|
|
1431
|
+
return { recovered: 0, skipped: 0, failed: 0, pendingRecoveryCount: 0 };
|
|
1432
|
+
}
|
|
1433
|
+
const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps);
|
|
1434
|
+
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
1435
|
+
const pendingSnapshot = blueBubblesPendingRecoverySnapshot(agentName);
|
|
1436
|
+
const pendingRecoveryCount = pendingSnapshot.pendingRecoveryCount;
|
|
1437
|
+
const failed = captured.failed + recovery.failed;
|
|
1438
|
+
const recovered = captured.recovered + recovery.recovered;
|
|
1439
|
+
const skipped = captured.skipped + recovery.skipped;
|
|
1440
|
+
const checkedAt = new Date().toISOString();
|
|
1441
|
+
try {
|
|
1442
|
+
await resolvedDeps.createClient().checkHealth();
|
|
1443
|
+
const activeSnapshot = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
1444
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1445
|
+
upstreamStatus: "ok",
|
|
1446
|
+
detail: formatBlueBubblesRuntimeDetail(pendingRecoveryCount, failed, activeSnapshot),
|
|
1447
|
+
lastCheckedAt: checkedAt,
|
|
1448
|
+
proofMethod: "bluebubbles.checkHealth",
|
|
1449
|
+
...pendingSnapshot,
|
|
1450
|
+
...activeSnapshot,
|
|
1451
|
+
failedRecoveryCount: failed,
|
|
1452
|
+
lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
|
|
1453
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1458
|
+
upstreamStatus: "error",
|
|
1459
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1460
|
+
lastCheckedAt: checkedAt,
|
|
1461
|
+
proofMethod: "bluebubbles.checkHealth",
|
|
1462
|
+
...pendingSnapshot,
|
|
1463
|
+
...(0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS),
|
|
1464
|
+
failedRecoveryCount: failed,
|
|
1465
|
+
lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
|
|
1466
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return { recovered, skipped, failed, pendingRecoveryCount };
|
|
1470
|
+
}
|
|
1471
|
+
async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, options = {}) {
|
|
1472
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1473
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1474
|
+
const client = resolvedDeps.createClient();
|
|
1475
|
+
const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
|
|
1476
|
+
const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
1477
|
+
const catchUpSince = resolveBlueBubblesCatchUpSince(state);
|
|
1478
|
+
const processTurns = options.processTurns !== false;
|
|
1479
|
+
/* v8 ignore next -- older injected test doubles may omit the catch-up query method */
|
|
1480
|
+
if (!client.listRecentMessages)
|
|
1481
|
+
return result;
|
|
1482
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1483
|
+
component: "senses",
|
|
1484
|
+
event: "senses.bluebubbles_catchup_start",
|
|
1485
|
+
message: "bluebubbles upstream catch-up pass started",
|
|
1486
|
+
meta: {
|
|
1487
|
+
since: new Date(catchUpSince).toISOString(),
|
|
1488
|
+
pageSize: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1489
|
+
maxPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
|
|
1490
|
+
},
|
|
1491
|
+
});
|
|
1492
|
+
const recentEvents = [];
|
|
1493
|
+
for (let page = 0; page < BLUEBUBBLES_CATCHUP_MAX_PAGES; page++) {
|
|
1494
|
+
let pageEvents;
|
|
1495
|
+
try {
|
|
1496
|
+
pageEvents = await client.listRecentMessages({
|
|
1497
|
+
limit: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1498
|
+
offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
result.failed++;
|
|
1503
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1504
|
+
level: "warn",
|
|
1505
|
+
component: "senses",
|
|
1506
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1507
|
+
message: "bluebubbles upstream catch-up query failed",
|
|
1508
|
+
meta: {
|
|
1509
|
+
offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1510
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1511
|
+
},
|
|
1512
|
+
});
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
recentEvents.push(...pageEvents);
|
|
1516
|
+
if (pageEvents.length < BLUEBUBBLES_CATCHUP_PAGE_SIZE)
|
|
1517
|
+
break;
|
|
1518
|
+
const oldestMessageTimestamp = pageEvents
|
|
1519
|
+
.filter((event) => event.kind === "message")
|
|
1520
|
+
.reduce((oldest, event) => Math.min(oldest, event.timestamp), Number.POSITIVE_INFINITY);
|
|
1521
|
+
if (oldestMessageTimestamp <= catchUpSince)
|
|
1522
|
+
break;
|
|
1523
|
+
if (page === BLUEBUBBLES_CATCHUP_MAX_PAGES - 1) {
|
|
1524
|
+
result.failed++;
|
|
1525
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1526
|
+
level: "warn",
|
|
1527
|
+
component: "senses",
|
|
1528
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1529
|
+
message: "bluebubbles upstream catch-up reached the bounded page limit",
|
|
1530
|
+
meta: {
|
|
1531
|
+
inspectedPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
|
|
1532
|
+
reason: "catch-up page limit reached before the outage window cutoff",
|
|
1533
|
+
},
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const seenMessageGuids = new Set();
|
|
1538
|
+
const candidates = recentEvents
|
|
1539
|
+
.filter((event) => event.kind === "message")
|
|
1540
|
+
.filter((event) => {
|
|
1541
|
+
if (seenMessageGuids.has(event.messageGuid))
|
|
1542
|
+
return false;
|
|
1543
|
+
seenMessageGuids.add(event.messageGuid);
|
|
1544
|
+
return true;
|
|
1545
|
+
})
|
|
1546
|
+
.sort((left, right) => left.timestamp - right.timestamp);
|
|
1547
|
+
for (const event of candidates) {
|
|
1548
|
+
result.inspected++;
|
|
1549
|
+
if (event.fromMe
|
|
1550
|
+
|| event.timestamp < catchUpSince
|
|
1551
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)
|
|
1552
|
+
|| isBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
|
|
1553
|
+
result.skipped++;
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
if (!processTurns) {
|
|
1557
|
+
if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
1558
|
+
result.skipped++;
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, "upstream-catchup");
|
|
1562
|
+
result.queued = (result.queued ?? 0) + 1;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const repaired = await client.repairEvent(event);
|
|
1567
|
+
if (repaired.kind !== "message") {
|
|
1568
|
+
result.skipped++;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup", {
|
|
1572
|
+
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
1573
|
+
});
|
|
1574
|
+
if (handled.reason === "already_processed") {
|
|
1575
|
+
result.skipped++;
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
result.recovered++;
|
|
1579
|
+
result.lastRecoveredMessageGuid = repaired.messageGuid;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
catch (error) {
|
|
1583
|
+
result.failed++;
|
|
1584
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1585
|
+
level: "warn",
|
|
1586
|
+
component: "senses",
|
|
1587
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1588
|
+
message: "bluebubbles upstream catch-up message failed",
|
|
1589
|
+
meta: {
|
|
1590
|
+
messageGuid: event.messageGuid,
|
|
1591
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (result.inspected > 0 || result.recovered > 0 || result.skipped > 0 || result.failed > 0) {
|
|
1597
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1598
|
+
component: "senses",
|
|
1599
|
+
event: "senses.bluebubbles_catchup_complete",
|
|
1600
|
+
message: "bluebubbles upstream catch-up pass completed",
|
|
1601
|
+
meta: { ...result },
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
return result;
|
|
1605
|
+
}
|
|
1606
|
+
function inboundEntryToRecoveryEvent(entry) {
|
|
1607
|
+
const chatIdentifier = entry.chatIdentifier ?? extractChatIdentifierFromSessionKey(entry.sessionKey) ?? "unknown";
|
|
1608
|
+
const normalizedSessionKey = normalizeBlueBubblesSessionKey(entry.sessionKey);
|
|
1609
|
+
const chatGuid = entry.chatGuid ?? (normalizedSessionKey.startsWith("chat:") ? normalizedSessionKey.slice("chat:".length) : undefined);
|
|
1610
|
+
const isGroup = normalizedSessionKey.includes("+;");
|
|
1611
|
+
return {
|
|
1612
|
+
kind: "message",
|
|
1613
|
+
eventType: "new-message",
|
|
1614
|
+
messageGuid: entry.messageGuid,
|
|
1615
|
+
timestamp: parseTimestampMs(entry.recordedAt) ?? Date.now(),
|
|
1616
|
+
fromMe: false,
|
|
1617
|
+
sender: {
|
|
1618
|
+
provider: "imessage-handle",
|
|
1619
|
+
externalId: chatIdentifier,
|
|
1620
|
+
rawId: chatIdentifier,
|
|
1621
|
+
displayName: chatIdentifier,
|
|
1622
|
+
},
|
|
1623
|
+
chat: {
|
|
1624
|
+
chatGuid,
|
|
1625
|
+
chatIdentifier,
|
|
1626
|
+
isGroup,
|
|
1627
|
+
sessionKey: normalizedSessionKey,
|
|
1628
|
+
sendTarget: chatGuid
|
|
1629
|
+
? { kind: "chat_guid", value: chatGuid }
|
|
1630
|
+
: { kind: "chat_identifier", value: chatIdentifier },
|
|
1631
|
+
participantHandles: [],
|
|
1632
|
+
},
|
|
1633
|
+
text: entry.textForAgent,
|
|
1634
|
+
textForAgent: entry.textForAgent,
|
|
1635
|
+
attachments: [],
|
|
1636
|
+
hasPayloadData: false,
|
|
1637
|
+
requiresRepair: true,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
|
|
1641
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1642
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1643
|
+
const client = resolvedDeps.createClient();
|
|
1644
|
+
const result = { recovered: 0, skipped: 0, failed: 0 };
|
|
1645
|
+
const seenMessageGuids = new Set();
|
|
1646
|
+
const candidates = (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
|
|
1647
|
+
.filter((entry) => {
|
|
1648
|
+
if (seenMessageGuids.has(entry.messageGuid))
|
|
1649
|
+
return false;
|
|
1650
|
+
seenMessageGuids.add(entry.messageGuid);
|
|
1651
|
+
return true;
|
|
1652
|
+
})
|
|
1653
|
+
.sort((left, right) => (parseTimestampMs(left.recordedAt) ?? 0) - (parseTimestampMs(right.recordedAt) ?? 0));
|
|
1654
|
+
for (const entry of candidates) {
|
|
1655
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid)
|
|
1656
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid)
|
|
1657
|
+
|| isBlueBubblesMessageInFlight(entry.sessionKey, entry.messageGuid)) {
|
|
1658
|
+
result.skipped++;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const repaired = await client.repairEvent(inboundEntryToRecoveryEvent(entry));
|
|
1663
|
+
if (repaired.kind !== "message") {
|
|
1664
|
+
result.skipped++;
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source, {
|
|
1668
|
+
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
1669
|
+
});
|
|
1670
|
+
if (handled.reason === "already_processed") {
|
|
1671
|
+
result.skipped++;
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
result.recovered++;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
catch (error) {
|
|
1678
|
+
result.failed++;
|
|
1679
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1680
|
+
level: "warn",
|
|
1681
|
+
component: "senses",
|
|
1682
|
+
event: "senses.bluebubbles_capture_recovery_error",
|
|
1683
|
+
message: "captured bluebubbles message recovery failed",
|
|
1684
|
+
meta: {
|
|
1685
|
+
messageGuid: entry.messageGuid,
|
|
1686
|
+
sessionKey: entry.sessionKey,
|
|
1687
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return result;
|
|
1693
|
+
}
|
|
1694
|
+
async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
1695
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1696
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1697
|
+
const client = resolvedDeps.createClient();
|
|
1698
|
+
const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
|
|
1699
|
+
for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
|
|
1700
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, candidate.sessionKey, candidate.messageGuid)
|
|
1701
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, candidate.messageGuid)
|
|
1702
|
+
|| isBlueBubblesMessageInFlight(candidate.sessionKey, candidate.messageGuid)) {
|
|
1703
|
+
result.skipped++;
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
try {
|
|
1707
|
+
const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
|
|
1708
|
+
if (repaired.kind !== "message") {
|
|
1709
|
+
result.pending++;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery", {
|
|
1713
|
+
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
1714
|
+
});
|
|
1715
|
+
if (handled.reason === "already_processed") {
|
|
1716
|
+
result.skipped++;
|
|
1717
|
+
}
|
|
1718
|
+
else {
|
|
1719
|
+
result.recovered++;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
catch (error) {
|
|
1723
|
+
result.failed++;
|
|
1724
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1725
|
+
level: "warn",
|
|
1726
|
+
component: "senses",
|
|
1727
|
+
event: "senses.bluebubbles_recovery_error",
|
|
1728
|
+
message: "bluebubbles backlog recovery failed",
|
|
1729
|
+
meta: {
|
|
1730
|
+
messageGuid: candidate.messageGuid,
|
|
1731
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1732
|
+
},
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
if (result.recovered > 0 || result.skipped > 0 || result.pending > 0 || result.failed > 0) {
|
|
1737
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1738
|
+
component: "senses",
|
|
1739
|
+
event: "senses.bluebubbles_recovery_complete",
|
|
1740
|
+
message: "bluebubbles backlog recovery pass completed",
|
|
1741
|
+
meta: { ...result },
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
1747
|
+
return async (req, res) => {
|
|
1748
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1749
|
+
if (url.pathname === "/health") {
|
|
1750
|
+
if (req.method === "GET" || req.method === "HEAD") {
|
|
1751
|
+
writeJson(res, 200, { status: "ok", uptime: process.uptime() });
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
writeJson(res, 405, { error: "Method not allowed" });
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
1758
|
+
const runtimeConfig = (0, config_1.getBlueBubblesConfig)();
|
|
1759
|
+
if (url.pathname !== channelConfig.webhookPath) {
|
|
1760
|
+
writeJson(res, 404, { error: "Not found" });
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (req.method !== "POST") {
|
|
1764
|
+
writeJson(res, 405, { error: "Method not allowed" });
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (!isWebhookPasswordValid(url, runtimeConfig.password)) {
|
|
1768
|
+
writeJson(res, 401, { error: "Unauthorized" });
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
let payload;
|
|
1772
|
+
try {
|
|
1773
|
+
const rawBody = await readRequestBody(req);
|
|
1774
|
+
payload = JSON.parse(rawBody);
|
|
1775
|
+
}
|
|
1776
|
+
catch (error) {
|
|
1777
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1778
|
+
level: "warn",
|
|
1779
|
+
component: "senses",
|
|
1780
|
+
event: "senses.bluebubbles_webhook_bad_json",
|
|
1781
|
+
message: "failed to parse bluebubbles webhook body",
|
|
1782
|
+
meta: {
|
|
1783
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1784
|
+
},
|
|
1785
|
+
});
|
|
1786
|
+
writeJson(res, 400, { error: "Invalid JSON body" });
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
let normalized;
|
|
1790
|
+
try {
|
|
1791
|
+
normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
|
|
1792
|
+
}
|
|
1793
|
+
catch (error) {
|
|
1794
|
+
if (error instanceof model_1.BlueBubblesIgnoredEventError) {
|
|
1795
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1796
|
+
component: "senses",
|
|
1797
|
+
event: "senses.bluebubbles_event_skipped",
|
|
1798
|
+
message: "skipped ignorable bluebubbles event",
|
|
1799
|
+
meta: {
|
|
1800
|
+
eventType: error.eventType,
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1803
|
+
writeJson(res, 200, {
|
|
1804
|
+
handled: true,
|
|
1805
|
+
notifiedAgent: false,
|
|
1806
|
+
reason: "ignored",
|
|
1807
|
+
});
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1811
|
+
level: "error",
|
|
1812
|
+
component: "senses",
|
|
1813
|
+
event: "senses.bluebubbles_webhook_error",
|
|
1814
|
+
message: "bluebubbles webhook handling failed",
|
|
1815
|
+
meta: {
|
|
1816
|
+
/* v8 ignore next -- normalizeBlueBubblesEvent throws Error subclasses; String fallback is defensive @preserve */
|
|
1817
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1818
|
+
},
|
|
1819
|
+
});
|
|
1820
|
+
writeJson(res, 500, {
|
|
1821
|
+
/* v8 ignore next -- normalizeBlueBubblesEvent throws Error subclasses; String fallback is defensive @preserve */
|
|
1822
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1823
|
+
});
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1827
|
+
if (normalized.kind === "message" && !normalized.fromMe) {
|
|
1828
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), normalized, "webhook");
|
|
1829
|
+
}
|
|
1830
|
+
else if (normalized.kind === "mutation") {
|
|
1831
|
+
try {
|
|
1832
|
+
resolvedDeps.recordMutation(resolvedDeps.getAgentName(), normalized);
|
|
1833
|
+
}
|
|
1834
|
+
catch (error) {
|
|
1835
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1836
|
+
level: "error",
|
|
1837
|
+
component: "senses",
|
|
1838
|
+
event: "senses.bluebubbles_mutation_log_error",
|
|
1839
|
+
message: "failed recording bluebubbles mutation sidecar",
|
|
1840
|
+
meta: {
|
|
1841
|
+
messageGuid: normalized.messageGuid,
|
|
1842
|
+
mutationType: normalized.mutationType,
|
|
1843
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
writeJson(res, 200, {
|
|
1849
|
+
handled: true,
|
|
1850
|
+
notifiedAgent: false,
|
|
1851
|
+
kind: normalized.kind,
|
|
1852
|
+
queued: true,
|
|
1853
|
+
reason: "queued",
|
|
1854
|
+
});
|
|
1855
|
+
setTimeout(() => {
|
|
1856
|
+
void handleBlueBubblesEvent(payload, deps).catch((error) => {
|
|
1857
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1858
|
+
level: "error",
|
|
1859
|
+
component: "senses",
|
|
1860
|
+
event: "senses.bluebubbles_webhook_async_error",
|
|
1861
|
+
message: "bluebubbles webhook async handling failed after durable capture",
|
|
1862
|
+
meta: {
|
|
1863
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1864
|
+
},
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
}, 0);
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function findImessageHandle(friend) {
|
|
1871
|
+
for (const ext of friend.externalIds) {
|
|
1872
|
+
if (ext.provider === "imessage-handle" && !ext.externalId.startsWith("group:")) {
|
|
1873
|
+
return ext.externalId;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return undefined;
|
|
1877
|
+
}
|
|
1878
|
+
function normalizeBlueBubblesSessionKey(sessionKey) {
|
|
1879
|
+
const trimmed = sessionKey.trim();
|
|
1880
|
+
if (trimmed.startsWith("chat_identifier_")) {
|
|
1881
|
+
return `chat_identifier:${trimmed.slice("chat_identifier_".length)}`;
|
|
1882
|
+
}
|
|
1883
|
+
if (trimmed.startsWith("chat_")) {
|
|
1884
|
+
return `chat:${trimmed.slice("chat_".length)}`;
|
|
1885
|
+
}
|
|
1886
|
+
return trimmed;
|
|
1887
|
+
}
|
|
1888
|
+
function extractChatIdentifierFromSessionKey(sessionKey) {
|
|
1889
|
+
const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
|
|
1890
|
+
if (normalizedKey.startsWith("chat:")) {
|
|
1891
|
+
const chatGuid = normalizedKey.slice("chat:".length).trim();
|
|
1892
|
+
const parts = chatGuid.split(";");
|
|
1893
|
+
return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
|
|
1894
|
+
}
|
|
1895
|
+
if (normalizedKey.startsWith("chat_identifier:")) {
|
|
1896
|
+
const identifier = normalizedKey.slice("chat_identifier:".length).trim();
|
|
1897
|
+
return identifier || undefined;
|
|
1898
|
+
}
|
|
1899
|
+
return undefined;
|
|
1900
|
+
}
|
|
1901
|
+
function buildChatRefForSessionKey(friend, sessionKey) {
|
|
1902
|
+
const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
|
|
1903
|
+
if (normalizedKey.startsWith("chat:")) {
|
|
1904
|
+
const chatGuid = normalizedKey.slice("chat:".length).trim();
|
|
1905
|
+
if (!chatGuid)
|
|
1906
|
+
return null;
|
|
1907
|
+
return {
|
|
1908
|
+
chatGuid,
|
|
1909
|
+
chatIdentifier: extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend),
|
|
1910
|
+
isGroup: chatGuid.includes(";+;"),
|
|
1911
|
+
sessionKey,
|
|
1912
|
+
sendTarget: { kind: "chat_guid", value: chatGuid },
|
|
1913
|
+
participantHandles: [],
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
const chatIdentifier = extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend);
|
|
1917
|
+
if (!chatIdentifier)
|
|
1918
|
+
return null;
|
|
1919
|
+
return {
|
|
1920
|
+
chatIdentifier,
|
|
1921
|
+
isGroup: false,
|
|
1922
|
+
sessionKey,
|
|
1923
|
+
sendTarget: { kind: "chat_identifier", value: chatIdentifier },
|
|
1924
|
+
participantHandles: [],
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
|
|
1928
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1929
|
+
const client = resolvedDeps.createClient();
|
|
1930
|
+
const store = resolvedDeps.createFriendStore();
|
|
1931
|
+
let friend;
|
|
1932
|
+
try {
|
|
1933
|
+
friend = await store.get(params.friendId);
|
|
1934
|
+
}
|
|
1935
|
+
catch {
|
|
1936
|
+
friend = null;
|
|
1937
|
+
}
|
|
1938
|
+
// Direct filesystem fallback — store.get() with name resolution wasn't working in production
|
|
1939
|
+
// despite correct compiled code. Bypass the entire store abstraction.
|
|
1940
|
+
/* v8 ignore start -- direct filesystem name resolution @preserve */
|
|
1941
|
+
if (!friend) {
|
|
1942
|
+
try {
|
|
1943
|
+
const friendsDir = path.join((0, identity_1.getAgentRoot)(), "friends");
|
|
1944
|
+
const files = fs.readdirSync(friendsDir).filter((f) => f.endsWith(".json"));
|
|
1945
|
+
for (const file of files) {
|
|
1946
|
+
const raw = JSON.parse(fs.readFileSync(path.join(friendsDir, file), "utf-8"));
|
|
1947
|
+
if (raw.name?.toLowerCase() === params.friendId.toLowerCase()) {
|
|
1948
|
+
friend = raw;
|
|
1949
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1950
|
+
component: "senses",
|
|
1951
|
+
event: "senses.bluebubbles_proactive_name_resolved",
|
|
1952
|
+
message: "resolved friend by name via direct filesystem scan",
|
|
1953
|
+
meta: { friendId: params.friendId, resolvedId: raw.id, name: raw.name },
|
|
1954
|
+
});
|
|
1955
|
+
break;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
catch (err) {
|
|
1960
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1961
|
+
level: "warn",
|
|
1962
|
+
component: "senses",
|
|
1963
|
+
event: "senses.bluebubbles_proactive_name_resolve_error",
|
|
1964
|
+
message: "direct filesystem name resolution failed",
|
|
1965
|
+
meta: { friendId: params.friendId, error: err instanceof Error ? err.message : String(err) },
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
/* v8 ignore stop */
|
|
1970
|
+
if (!friend) {
|
|
1971
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1972
|
+
level: "warn",
|
|
1973
|
+
component: "senses",
|
|
1974
|
+
event: "senses.bluebubbles_proactive_no_friend",
|
|
1975
|
+
message: "proactive send skipped: friend not found",
|
|
1976
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1977
|
+
});
|
|
1978
|
+
return { delivered: false, reason: "friend_not_found" };
|
|
1979
|
+
}
|
|
1980
|
+
const explicitCrossChatAuthorized = params.intent === "explicit_cross_chat"
|
|
1981
|
+
&& types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
|
|
1982
|
+
if (!explicitCrossChatAuthorized && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
1983
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1984
|
+
component: "senses",
|
|
1985
|
+
event: "senses.bluebubbles_proactive_trust_skip",
|
|
1986
|
+
message: "proactive send skipped: trust level not allowed",
|
|
1987
|
+
meta: {
|
|
1988
|
+
friendId: params.friendId,
|
|
1989
|
+
sessionKey: params.sessionKey,
|
|
1990
|
+
trustLevel: friend.trustLevel ?? "unknown",
|
|
1991
|
+
intent: params.intent ?? "generic_outreach",
|
|
1992
|
+
authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
|
|
1993
|
+
},
|
|
1994
|
+
});
|
|
1995
|
+
return { delivered: false, reason: "trust_skip" };
|
|
1996
|
+
}
|
|
1997
|
+
const chat = buildChatRefForSessionKey(friend, params.sessionKey);
|
|
1998
|
+
if (!chat) {
|
|
1999
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2000
|
+
level: "warn",
|
|
2001
|
+
component: "senses",
|
|
2002
|
+
event: "senses.bluebubbles_proactive_no_handle",
|
|
2003
|
+
message: "proactive send skipped: no iMessage handle found",
|
|
2004
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
2005
|
+
});
|
|
2006
|
+
return { delivered: false, reason: "missing_target" };
|
|
2007
|
+
}
|
|
2008
|
+
// Proactive outreach to individuals must go to DMs, never group chats.
|
|
2009
|
+
// Explicit cross-chat responses (bridge completions, delegation returns) ARE allowed to groups
|
|
2010
|
+
// because the request originated from that group.
|
|
2011
|
+
/* v8 ignore start -- group gate: only fires when proactive send targets a group session @preserve */
|
|
2012
|
+
if (chat.isGroup && params.intent !== "explicit_cross_chat") {
|
|
2013
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2014
|
+
level: "warn",
|
|
2015
|
+
component: "senses",
|
|
2016
|
+
event: "senses.bluebubbles_proactive_group_blocked",
|
|
2017
|
+
message: "proactive send blocked: would route to group chat",
|
|
2018
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey, chatGuid: chat.chatGuid ?? null, intent: params.intent ?? null },
|
|
2019
|
+
});
|
|
2020
|
+
return { delivered: false, reason: "group_blocked" };
|
|
2021
|
+
}
|
|
2022
|
+
/* v8 ignore stop */
|
|
2023
|
+
const internalContentBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(params.text);
|
|
2024
|
+
if (internalContentBlockReason) {
|
|
2025
|
+
(0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
|
|
2026
|
+
friendId: params.friendId,
|
|
2027
|
+
sessionKey: params.sessionKey,
|
|
2028
|
+
reason: internalContentBlockReason,
|
|
2029
|
+
source: "session_send",
|
|
2030
|
+
intent: params.intent ?? "generic_outreach",
|
|
2031
|
+
});
|
|
2032
|
+
return { delivered: false, reason: "internal_content_blocked" };
|
|
2033
|
+
}
|
|
2034
|
+
try {
|
|
2035
|
+
await client.sendText({ chat, text: params.text });
|
|
2036
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2037
|
+
component: "senses",
|
|
2038
|
+
event: "senses.bluebubbles_proactive_sent",
|
|
2039
|
+
message: "proactive bluebubbles message sent",
|
|
2040
|
+
meta: {
|
|
2041
|
+
friendId: params.friendId,
|
|
2042
|
+
sessionKey: params.sessionKey,
|
|
2043
|
+
chatGuid: chat.chatGuid ?? null,
|
|
2044
|
+
chatIdentifier: chat.chatIdentifier ?? null,
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
return { delivered: true };
|
|
2048
|
+
}
|
|
2049
|
+
catch (error) {
|
|
2050
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2051
|
+
level: "error",
|
|
2052
|
+
component: "senses",
|
|
2053
|
+
event: "senses.bluebubbles_proactive_send_error",
|
|
2054
|
+
message: "proactive bluebubbles send failed",
|
|
2055
|
+
meta: {
|
|
2056
|
+
friendId: params.friendId,
|
|
2057
|
+
sessionKey: params.sessionKey,
|
|
2058
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2059
|
+
},
|
|
2060
|
+
});
|
|
2061
|
+
return { delivered: false, reason: "send_error" };
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
function scanPendingBlueBubblesFiles(pendingRoot) {
|
|
2065
|
+
const results = [];
|
|
2066
|
+
let friendIds;
|
|
2067
|
+
try {
|
|
2068
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
2069
|
+
}
|
|
2070
|
+
catch {
|
|
2071
|
+
return results;
|
|
2072
|
+
}
|
|
2073
|
+
for (const friendId of friendIds) {
|
|
2074
|
+
const bbDir = path.join(pendingRoot, friendId, "bluebubbles");
|
|
2075
|
+
let keys;
|
|
2076
|
+
try {
|
|
2077
|
+
keys = fs.readdirSync(bbDir);
|
|
2078
|
+
}
|
|
2079
|
+
catch {
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
for (const key of keys) {
|
|
2083
|
+
const keyDir = path.join(bbDir, key);
|
|
2084
|
+
let files;
|
|
2085
|
+
try {
|
|
2086
|
+
files = fs.readdirSync(keyDir);
|
|
2087
|
+
}
|
|
2088
|
+
catch {
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
2092
|
+
const filePath = path.join(keyDir, file);
|
|
2093
|
+
try {
|
|
2094
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
2095
|
+
results.push({ friendId, key, filePath, content });
|
|
2096
|
+
}
|
|
2097
|
+
catch {
|
|
2098
|
+
// skip unreadable files
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return results;
|
|
2104
|
+
}
|
|
2105
|
+
async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
|
|
2106
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2107
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
2108
|
+
const client = resolvedDeps.createClient();
|
|
2109
|
+
const store = resolvedDeps.createFriendStore();
|
|
2110
|
+
const pendingFiles = scanPendingBlueBubblesFiles(root);
|
|
2111
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
2112
|
+
for (const { friendId, filePath, content } of pendingFiles) {
|
|
2113
|
+
let parsed;
|
|
2114
|
+
try {
|
|
2115
|
+
parsed = JSON.parse(content);
|
|
2116
|
+
}
|
|
2117
|
+
catch {
|
|
2118
|
+
result.failed++;
|
|
2119
|
+
try {
|
|
2120
|
+
fs.unlinkSync(filePath);
|
|
2121
|
+
}
|
|
2122
|
+
catch { /* ignore */ }
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
2126
|
+
if (!messageText.trim()) {
|
|
2127
|
+
result.skipped++;
|
|
2128
|
+
try {
|
|
2129
|
+
fs.unlinkSync(filePath);
|
|
2130
|
+
}
|
|
2131
|
+
catch { /* ignore */ }
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
|
|
2135
|
+
if (internalBlockReason) {
|
|
2136
|
+
result.skipped++;
|
|
2137
|
+
try {
|
|
2138
|
+
fs.unlinkSync(filePath);
|
|
2139
|
+
}
|
|
2140
|
+
catch { /* ignore */ }
|
|
2141
|
+
(0, proactive_content_guard_1.emitProactiveInternalContentBlocked)({
|
|
2142
|
+
friendId,
|
|
2143
|
+
reason: internalBlockReason,
|
|
2144
|
+
source: "pending_drain",
|
|
2145
|
+
});
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
let friend;
|
|
2149
|
+
try {
|
|
2150
|
+
friend = await store.get(friendId);
|
|
2151
|
+
}
|
|
2152
|
+
catch {
|
|
2153
|
+
friend = null;
|
|
2154
|
+
}
|
|
2155
|
+
if (!friend) {
|
|
2156
|
+
result.skipped++;
|
|
2157
|
+
try {
|
|
2158
|
+
fs.unlinkSync(filePath);
|
|
2159
|
+
}
|
|
2160
|
+
catch { /* ignore */ }
|
|
2161
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2162
|
+
level: "warn",
|
|
2163
|
+
component: "senses",
|
|
2164
|
+
event: "senses.bluebubbles_proactive_no_friend",
|
|
2165
|
+
message: "proactive send skipped: friend not found",
|
|
2166
|
+
meta: { friendId },
|
|
2167
|
+
});
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
2171
|
+
result.skipped++;
|
|
2172
|
+
try {
|
|
2173
|
+
fs.unlinkSync(filePath);
|
|
2174
|
+
}
|
|
2175
|
+
catch { /* ignore */ }
|
|
2176
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2177
|
+
component: "senses",
|
|
2178
|
+
event: "senses.bluebubbles_proactive_trust_skip",
|
|
2179
|
+
message: "proactive send skipped: trust level not allowed",
|
|
2180
|
+
meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
|
|
2181
|
+
});
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
const handle = findImessageHandle(friend);
|
|
2185
|
+
if (!handle) {
|
|
2186
|
+
result.skipped++;
|
|
2187
|
+
try {
|
|
2188
|
+
fs.unlinkSync(filePath);
|
|
2189
|
+
}
|
|
2190
|
+
catch { /* ignore */ }
|
|
2191
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2192
|
+
level: "warn",
|
|
2193
|
+
component: "senses",
|
|
2194
|
+
event: "senses.bluebubbles_proactive_no_handle",
|
|
2195
|
+
message: "proactive send skipped: no iMessage handle found",
|
|
2196
|
+
meta: { friendId },
|
|
2197
|
+
});
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
const chat = {
|
|
2201
|
+
chatIdentifier: handle,
|
|
2202
|
+
isGroup: false,
|
|
2203
|
+
sessionKey: friendId,
|
|
2204
|
+
sendTarget: { kind: "chat_identifier", value: handle },
|
|
2205
|
+
participantHandles: [],
|
|
2206
|
+
};
|
|
2207
|
+
try {
|
|
2208
|
+
await client.sendText({ chat, text: messageText });
|
|
2209
|
+
result.sent++;
|
|
2210
|
+
try {
|
|
2211
|
+
fs.unlinkSync(filePath);
|
|
2212
|
+
}
|
|
2213
|
+
catch { /* ignore */ }
|
|
2214
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2215
|
+
component: "senses",
|
|
2216
|
+
event: "senses.bluebubbles_proactive_sent",
|
|
2217
|
+
message: "proactive bluebubbles message sent",
|
|
2218
|
+
meta: { friendId, handle },
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
catch (error) {
|
|
2222
|
+
result.failed++;
|
|
2223
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2224
|
+
level: "error",
|
|
2225
|
+
component: "senses",
|
|
2226
|
+
event: "senses.bluebubbles_proactive_send_error",
|
|
2227
|
+
message: "proactive bluebubbles send failed",
|
|
2228
|
+
meta: {
|
|
2229
|
+
friendId,
|
|
2230
|
+
handle,
|
|
2231
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2232
|
+
},
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
2237
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2238
|
+
component: "senses",
|
|
2239
|
+
event: "senses.bluebubbles_proactive_drain_complete",
|
|
2240
|
+
message: "bluebubbles proactive drain complete",
|
|
2241
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
function startBlueBubblesApp(deps = {}) {
|
|
2247
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2248
|
+
resolvedDeps.createClient();
|
|
2249
|
+
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
2250
|
+
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps));
|
|
2251
|
+
let recoveryPassRunning = false;
|
|
2252
|
+
let recoveryDelayTimer = null;
|
|
2253
|
+
function triggerRecoveryPass() {
|
|
2254
|
+
/* v8 ignore next -- re-entrant timer guard; difficult to force deterministically without timing the turn lock @preserve */
|
|
2255
|
+
if (recoveryPassRunning)
|
|
2256
|
+
return;
|
|
2257
|
+
recoveryPassRunning = true;
|
|
2258
|
+
void recoverQueuedBlueBubblesMessages(resolvedDeps)
|
|
2259
|
+
/* v8 ignore start -- defensive wrapper; expected per-message failures are handled inside recovery helpers @preserve */
|
|
2260
|
+
.catch((error) => {
|
|
2261
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2262
|
+
level: "warn",
|
|
2263
|
+
component: "senses",
|
|
2264
|
+
event: "senses.bluebubbles_recovery_error",
|
|
2265
|
+
message: "bluebubbles queued recovery pass failed",
|
|
2266
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
2267
|
+
});
|
|
2268
|
+
})
|
|
2269
|
+
/* v8 ignore stop */
|
|
2270
|
+
.finally(() => {
|
|
2271
|
+
recoveryPassRunning = false;
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
function scheduleRecoveryPass() {
|
|
2275
|
+
/* v8 ignore next -- duplicate scheduling guard for overlapping health sync completions @preserve */
|
|
2276
|
+
if (recoveryDelayTimer !== null)
|
|
2277
|
+
return;
|
|
2278
|
+
recoveryDelayTimer = setTimeout(() => {
|
|
2279
|
+
recoveryDelayTimer = null;
|
|
2280
|
+
triggerRecoveryPass();
|
|
2281
|
+
}, BLUEBUBBLES_RECOVERY_PASS_DELAY_MS);
|
|
2282
|
+
}
|
|
2283
|
+
const runtimeTimer = setInterval(() => {
|
|
2284
|
+
void syncBlueBubblesRuntime(resolvedDeps).then(scheduleRecoveryPass);
|
|
2285
|
+
}, BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS);
|
|
2286
|
+
const recoveryTimer = setInterval(triggerRecoveryPass, BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS);
|
|
2287
|
+
server.on?.("close", () => {
|
|
2288
|
+
clearInterval(runtimeTimer);
|
|
2289
|
+
clearInterval(recoveryTimer);
|
|
2290
|
+
if (recoveryDelayTimer !== null) {
|
|
2291
|
+
clearTimeout(recoveryDelayTimer);
|
|
2292
|
+
recoveryDelayTimer = null;
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
server.listen(channelConfig.port, () => {
|
|
2296
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2297
|
+
component: "channels",
|
|
2298
|
+
event: "channel.app_started",
|
|
2299
|
+
message: "BlueBubbles sense started",
|
|
2300
|
+
meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
void syncBlueBubblesRuntime(resolvedDeps).then(scheduleRecoveryPass);
|
|
2304
|
+
return server;
|
|
2305
|
+
}
|