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