@ouro.bot/cli 0.1.0-alpha.484 → 0.1.0-alpha.486
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/changelog.json +17 -0
- package/dist/heart/active-work.js +89 -3
- package/dist/heart/background-operations.js +26 -3
- package/dist/heart/daemon/cli-exec.js +171 -9
- package/dist/heart/mail-import-discovery.js +37 -2
- package/dist/heart/providers/azure.js +1 -1
- package/dist/heart/providers/github-copilot.js +1 -1
- package/dist/heart/providers/openai-codex.js +1 -1
- package/dist/heart/session-events.js +40 -3
- package/dist/heart/streaming.js +13 -2
- package/dist/mailroom/blob-store.js +16 -10
- package/dist/mailroom/core.js +1 -1
- package/dist/mailroom/file-store.js +35 -9
- package/dist/mailroom/mbox-import.js +41 -0
- package/dist/mailroom/reader.js +22 -0
- package/dist/mailroom/search-cache.js +182 -0
- package/dist/mailroom/search-relevance.js +319 -0
- package/dist/mind/context.js +36 -4
- package/dist/mind/friends/resolver.js +16 -1
- package/dist/nerves/coverage/file-completeness.js +4 -0
- package/dist/repertoire/tools-mail.js +453 -68
- package/dist/senses/bluebubbles/inbound-log.js +13 -0
- package/dist/senses/bluebubbles/index.js +394 -236
- package/dist/senses/bluebubbles/processed-log.js +111 -0
- package/dist/senses/inner-dialog-worker.js +38 -2
- package/dist/senses/mail.js +19 -3
- package/dist/senses/trust-gate.js +96 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ exports.enrichReactionText = enrichReactionText;
|
|
|
37
37
|
exports.createStatusBatcher = createStatusBatcher;
|
|
38
38
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
39
39
|
exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
|
|
40
|
+
exports.recoverCapturedBlueBubblesInboundMessages = recoverCapturedBlueBubblesInboundMessages;
|
|
40
41
|
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
41
42
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
42
43
|
exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
|
|
@@ -66,6 +67,7 @@ const model_1 = require("./model");
|
|
|
66
67
|
const client_1 = require("./client");
|
|
67
68
|
const inbound_log_1 = require("./inbound-log");
|
|
68
69
|
const mutation_log_1 = require("./mutation-log");
|
|
70
|
+
const processed_log_1 = require("./processed-log");
|
|
69
71
|
const runtime_state_1 = require("./runtime-state");
|
|
70
72
|
const session_cleanup_1 = require("./session-cleanup");
|
|
71
73
|
const tool_activity_callbacks_1 = require("../../heart/tool-activity-callbacks");
|
|
@@ -73,6 +75,7 @@ const commands_1 = require("../commands");
|
|
|
73
75
|
const trust_gate_1 = require("../trust-gate");
|
|
74
76
|
const pipeline_1 = require("../pipeline");
|
|
75
77
|
const bbFailoverStates = new Map();
|
|
78
|
+
const bbInFlightMessageGuids = new Set();
|
|
76
79
|
// Enrich reaction text with the original message content for context.
|
|
77
80
|
// If originalText is provided and non-empty, format as: baseText to: "truncated"
|
|
78
81
|
// Otherwise return baseText unchanged.
|
|
@@ -122,6 +125,26 @@ function createStatusBatcher(send, delayMs) {
|
|
|
122
125
|
},
|
|
123
126
|
};
|
|
124
127
|
}
|
|
128
|
+
function blueBubblesMessageKey(sessionKey, messageGuid) {
|
|
129
|
+
return `${sessionKey}:${messageGuid.trim()}`;
|
|
130
|
+
}
|
|
131
|
+
function isBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
132
|
+
if (!messageGuid.trim())
|
|
133
|
+
return false;
|
|
134
|
+
return bbInFlightMessageGuids.has(blueBubblesMessageKey(sessionKey, messageGuid));
|
|
135
|
+
}
|
|
136
|
+
function beginBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
137
|
+
if (!messageGuid.trim())
|
|
138
|
+
return true;
|
|
139
|
+
const key = blueBubblesMessageKey(sessionKey, messageGuid);
|
|
140
|
+
if (bbInFlightMessageGuids.has(key))
|
|
141
|
+
return false;
|
|
142
|
+
bbInFlightMessageGuids.add(key);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
function endBlueBubblesMessageInFlight(sessionKey, messageGuid) {
|
|
146
|
+
bbInFlightMessageGuids.delete(blueBubblesMessageKey(sessionKey, messageGuid));
|
|
147
|
+
}
|
|
125
148
|
const defaultDeps = {
|
|
126
149
|
getAgentName: identity_1.getAgentName,
|
|
127
150
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -575,6 +598,7 @@ function isWebhookPasswordValid(url, expectedPassword) {
|
|
|
575
598
|
}
|
|
576
599
|
async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
577
600
|
const client = resolvedDeps.createClient();
|
|
601
|
+
const agentName = resolvedDeps.getAgentName();
|
|
578
602
|
if (event.fromMe) {
|
|
579
603
|
(0, runtime_1.emitNervesEvent)({
|
|
580
604
|
component: "senses",
|
|
@@ -617,252 +641,284 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
|
617
641
|
});
|
|
618
642
|
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
619
643
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
level: "warn",
|
|
634
|
-
component: "senses",
|
|
635
|
-
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
636
|
-
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
637
|
-
meta: {
|
|
638
|
-
sessionPath: sessPath,
|
|
639
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
return (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
644
|
-
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
645
|
-
const existing = resolvedDeps.loadSession(sessPath);
|
|
646
|
-
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
647
|
-
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
648
|
-
? existing.messages
|
|
649
|
-
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
|
|
650
|
-
if (event.kind === "message") {
|
|
651
|
-
const agentName = resolvedDeps.getAgentName();
|
|
652
|
-
if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
653
|
-
(0, runtime_1.emitNervesEvent)({
|
|
654
|
-
component: "senses",
|
|
655
|
-
event: "senses.bluebubbles_recovery_skip",
|
|
656
|
-
message: "skipped bluebubbles message already recorded as handled",
|
|
657
|
-
meta: {
|
|
658
|
-
messageGuid: event.messageGuid,
|
|
659
|
-
sessionKey: event.chat.sessionKey,
|
|
660
|
-
source,
|
|
661
|
-
},
|
|
662
|
-
});
|
|
663
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
664
|
-
}
|
|
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") {
|
|
672
|
-
(0, runtime_1.emitNervesEvent)({
|
|
673
|
-
component: "senses",
|
|
674
|
-
event: "senses.bluebubbles_recovery_skip",
|
|
675
|
-
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
676
|
-
meta: {
|
|
677
|
-
messageGuid: event.messageGuid,
|
|
678
|
-
sessionKey: event.chat.sessionKey,
|
|
679
|
-
source,
|
|
680
|
-
},
|
|
681
|
-
});
|
|
682
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
683
|
-
}
|
|
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),
|
|
644
|
+
let ownsInFlightMessage = false;
|
|
645
|
+
if (event.kind === "message") {
|
|
646
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
647
|
+
(0, runtime_1.emitNervesEvent)({
|
|
648
|
+
component: "senses",
|
|
649
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
650
|
+
message: "skipped bluebubbles message already marked as handled",
|
|
651
|
+
meta: {
|
|
652
|
+
messageGuid: event.messageGuid,
|
|
653
|
+
sessionKey: event.chat.sessionKey,
|
|
654
|
+
source,
|
|
655
|
+
dedupeReason: "processed",
|
|
656
|
+
},
|
|
693
657
|
});
|
|
658
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
694
659
|
}
|
|
695
|
-
|
|
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);
|
|
660
|
+
if (!beginBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
|
|
700
661
|
(0, runtime_1.emitNervesEvent)({
|
|
701
662
|
component: "senses",
|
|
702
|
-
event: "senses.
|
|
703
|
-
message:
|
|
704
|
-
meta: {
|
|
663
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
664
|
+
message: "skipped bluebubbles message already in flight",
|
|
665
|
+
meta: {
|
|
666
|
+
messageGuid: event.messageGuid,
|
|
667
|
+
sessionKey: event.chat.sessionKey,
|
|
668
|
+
source,
|
|
669
|
+
dedupeReason: "in_flight",
|
|
670
|
+
},
|
|
705
671
|
});
|
|
672
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
706
673
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
role: "user",
|
|
719
|
-
content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
|
|
720
|
-
};
|
|
721
|
-
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
|
|
722
|
-
const controller = new AbortController();
|
|
723
|
-
// BB-specific tool context wrappers
|
|
724
|
-
const summarize = (0, core_1.createSummarize)("human");
|
|
725
|
-
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
726
|
-
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
727
|
-
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
728
|
-
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
729
|
-
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
730
|
-
? false
|
|
731
|
-
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
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 */
|
|
674
|
+
ownsInFlightMessage = true;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
// ── Adapter setup: friend, session, content, callbacks ──────────
|
|
678
|
+
const store = resolvedDeps.createFriendStore();
|
|
679
|
+
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
680
|
+
const baseContext = await resolver.resolve();
|
|
681
|
+
const context = { ...baseContext, isGroupChat: event.chat.isGroup };
|
|
682
|
+
const replyTarget = createReplyTargetController(event);
|
|
683
|
+
const friendId = context.friend.id;
|
|
684
|
+
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
749
685
|
try {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
state: existing?.state,
|
|
762
|
-
events: existing?.events,
|
|
763
|
-
}),
|
|
686
|
+
(0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
(0, runtime_1.emitNervesEvent)({
|
|
690
|
+
level: "warn",
|
|
691
|
+
component: "senses",
|
|
692
|
+
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
693
|
+
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
694
|
+
meta: {
|
|
695
|
+
sessionPath: sessPath,
|
|
696
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
764
697
|
},
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return await (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
701
|
+
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
702
|
+
const existing = resolvedDeps.loadSession(sessPath);
|
|
703
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
704
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
705
|
+
? existing.messages
|
|
706
|
+
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
|
|
707
|
+
if (event.kind === "message") {
|
|
708
|
+
// Record EARLY for audit and crash recovery. This is capture truth, not
|
|
709
|
+
// a claim that the agent turn completed successfully.
|
|
710
|
+
const inboundSource = source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
|
|
711
|
+
? "recovery-bootstrap"
|
|
712
|
+
: source;
|
|
713
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
|
|
714
|
+
if (inboundSource === "recovery-bootstrap") {
|
|
715
|
+
(0, runtime_1.emitNervesEvent)({
|
|
716
|
+
component: "senses",
|
|
717
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
718
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
719
|
+
meta: {
|
|
720
|
+
messageGuid: event.messageGuid,
|
|
721
|
+
sessionKey: event.chat.sessionKey,
|
|
722
|
+
source,
|
|
784
723
|
},
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
724
|
+
});
|
|
725
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
|
|
726
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (event.kind === "message" && event.chat.isGroup) {
|
|
730
|
+
await (0, group_context_1.upsertGroupContextParticipants)({
|
|
731
|
+
store,
|
|
732
|
+
participants: (event.chat.participantHandles ?? []).map((externalId) => ({
|
|
733
|
+
provider: "imessage-handle",
|
|
734
|
+
externalId,
|
|
735
|
+
})),
|
|
736
|
+
groupExternalId: resolveGroupExternalId(event),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
// Fetch the text of the message being replied to (if this is a threaded reply)
|
|
740
|
+
const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
|
|
741
|
+
let repliedToText = null;
|
|
742
|
+
if (threadGuid) {
|
|
743
|
+
repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
|
|
744
|
+
(0, runtime_1.emitNervesEvent)({
|
|
745
|
+
component: "senses",
|
|
746
|
+
event: "senses.bluebubbles_reply_context",
|
|
747
|
+
message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
|
|
748
|
+
meta: { threadGuid, hasText: !!repliedToText },
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
// Enrich reaction mutations with the original message text for context
|
|
752
|
+
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
753
|
+
if (isReaction && event.targetMessageGuid) {
|
|
754
|
+
/* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
|
|
755
|
+
const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
|
|
756
|
+
if (originalText)
|
|
757
|
+
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
758
|
+
/* v8 ignore stop */
|
|
759
|
+
}
|
|
760
|
+
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
761
|
+
const userMessage = {
|
|
762
|
+
role: "user",
|
|
763
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
|
|
764
|
+
};
|
|
765
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
|
|
766
|
+
const controller = new AbortController();
|
|
767
|
+
// BB-specific tool context wrappers
|
|
768
|
+
const summarize = (0, core_1.createSummarize)("human");
|
|
769
|
+
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
770
|
+
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
771
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
772
|
+
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
773
|
+
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
774
|
+
? false
|
|
775
|
+
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
776
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
777
|
+
// Buffer terminal errors so failover can suppress them.
|
|
778
|
+
// If failover produces a message, the buffered error is skipped.
|
|
779
|
+
// If failover doesn't fire, the buffered error is replayed.
|
|
780
|
+
let bufferedTerminalError = null;
|
|
781
|
+
/* v8 ignore start -- failover-aware error buffering @preserve */
|
|
782
|
+
const failoverAwareCallbacks = {
|
|
783
|
+
...callbacks,
|
|
784
|
+
onError(error, severity) {
|
|
785
|
+
if (severity === "terminal") {
|
|
786
|
+
bufferedTerminalError = error;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
callbacks.onError(error, severity);
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
/* v8 ignore stop */
|
|
793
|
+
try {
|
|
794
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
795
|
+
channel: "bluebubbles",
|
|
796
|
+
sessionKey: event.chat.sessionKey,
|
|
797
|
+
capabilities: bbCapabilities,
|
|
798
|
+
messages: [userMessage],
|
|
799
|
+
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
800
|
+
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
801
|
+
sessionLoader: {
|
|
802
|
+
loadOrCreate: () => Promise.resolve({
|
|
803
|
+
messages: sessionMessages,
|
|
804
|
+
sessionPath: sessPath,
|
|
805
|
+
state: existing?.state,
|
|
806
|
+
events: existing?.events,
|
|
807
|
+
}),
|
|
808
|
+
},
|
|
809
|
+
pendingDir,
|
|
810
|
+
friendStore: store,
|
|
811
|
+
provider: "imessage-handle",
|
|
812
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
813
|
+
isGroupChat: event.chat.isGroup,
|
|
814
|
+
groupHasFamilyMember,
|
|
815
|
+
hasExistingGroupWithFamily,
|
|
816
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
817
|
+
drainPending: pending_1.drainPending,
|
|
818
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
|
|
819
|
+
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
820
|
+
...opts,
|
|
821
|
+
toolContext: {
|
|
822
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
823
|
+
signin: async () => undefined,
|
|
824
|
+
...opts?.toolContext,
|
|
825
|
+
summarize,
|
|
826
|
+
bluebubblesReplyTarget: {
|
|
827
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
828
|
+
},
|
|
829
|
+
codingFeedback: {
|
|
830
|
+
send: async (message) => {
|
|
831
|
+
await client.sendText({
|
|
832
|
+
chat: event.chat,
|
|
833
|
+
text: message,
|
|
834
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
835
|
+
});
|
|
836
|
+
},
|
|
792
837
|
},
|
|
793
838
|
},
|
|
839
|
+
}),
|
|
840
|
+
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
841
|
+
const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
|
|
842
|
+
resolvedDeps.deferPostTurnPersist(sessionPathArg, prepared, usage, state);
|
|
794
843
|
},
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
844
|
+
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
845
|
+
signal: controller.signal,
|
|
846
|
+
runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
|
|
847
|
+
callbacks: failoverAwareCallbacks,
|
|
848
|
+
failoverState: (() => {
|
|
849
|
+
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
|
850
|
+
bbFailoverStates.set(event.chat.sessionKey, { pending: null });
|
|
851
|
+
}
|
|
852
|
+
return bbFailoverStates.get(event.chat.sessionKey);
|
|
853
|
+
})(),
|
|
854
|
+
});
|
|
855
|
+
/* v8 ignore start -- failover display + error replay @preserve */
|
|
856
|
+
if (result.failoverMessage) {
|
|
857
|
+
// Failover handled it — show the failover message, skip the buffered error
|
|
858
|
+
await client.sendText({ chat: event.chat, text: result.failoverMessage });
|
|
859
|
+
}
|
|
860
|
+
else if (bufferedTerminalError) {
|
|
861
|
+
// No failover — replay the buffered terminal error
|
|
862
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
863
|
+
}
|
|
864
|
+
/* v8 ignore stop */
|
|
865
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
866
|
+
if (!result.gateResult.allowed) {
|
|
867
|
+
// Send auto-reply via BB API if the gate provides one
|
|
868
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
869
|
+
await client.sendText({
|
|
870
|
+
chat: event.chat,
|
|
871
|
+
text: result.gateResult.autoReply,
|
|
872
|
+
});
|
|
807
873
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
callbacks.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// ── Handle gate result ────────────────────────────────────────
|
|
822
|
-
if (!result.gateResult.allowed) {
|
|
823
|
-
// Send auto-reply via BB API if the gate provides one
|
|
824
|
-
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
825
|
-
await client.sendText({
|
|
826
|
-
chat: event.chat,
|
|
827
|
-
text: result.gateResult.autoReply,
|
|
828
|
-
});
|
|
874
|
+
if (event.kind === "message") {
|
|
875
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, source, "trust-gated");
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
handled: true,
|
|
879
|
+
notifiedAgent: false,
|
|
880
|
+
kind: event.kind,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
// Gate allowed — flush the agent's reply
|
|
884
|
+
await callbacks.flush();
|
|
885
|
+
if (event.kind === "message") {
|
|
886
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, source, "turn-complete");
|
|
829
887
|
}
|
|
888
|
+
(0, runtime_1.emitNervesEvent)({
|
|
889
|
+
component: "senses",
|
|
890
|
+
event: "senses.bluebubbles_turn_end",
|
|
891
|
+
message: "bluebubbles event handled",
|
|
892
|
+
meta: {
|
|
893
|
+
messageGuid: event.messageGuid,
|
|
894
|
+
kind: event.kind,
|
|
895
|
+
sessionKey: event.chat.sessionKey,
|
|
896
|
+
},
|
|
897
|
+
});
|
|
830
898
|
return {
|
|
831
899
|
handled: true,
|
|
832
|
-
notifiedAgent:
|
|
900
|
+
notifiedAgent: true,
|
|
833
901
|
kind: event.kind,
|
|
834
902
|
};
|
|
835
903
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
},
|
|
847
|
-
});
|
|
848
|
-
return {
|
|
849
|
-
handled: true,
|
|
850
|
-
notifiedAgent: true,
|
|
851
|
-
kind: event.kind,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
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;
|
|
904
|
+
finally {
|
|
905
|
+
// If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
|
|
906
|
+
// replay it now so the user still sees the error.
|
|
907
|
+
/* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
|
|
908
|
+
if (bufferedTerminalError) {
|
|
909
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
910
|
+
bufferedTerminalError = null;
|
|
911
|
+
}
|
|
912
|
+
/* v8 ignore stop */
|
|
913
|
+
await callbacks.finish();
|
|
861
914
|
}
|
|
862
|
-
|
|
863
|
-
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
finally {
|
|
918
|
+
if (ownsInFlightMessage && event.kind === "message") {
|
|
919
|
+
endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
|
|
864
920
|
}
|
|
865
|
-
}
|
|
921
|
+
}
|
|
866
922
|
}
|
|
867
923
|
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
868
924
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
@@ -908,8 +964,15 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
908
964
|
// the downstream `already_processed` path fires its observability events
|
|
909
965
|
// and the caller sees a consistent return shape.
|
|
910
966
|
const agentName = resolvedDeps.getAgentName();
|
|
967
|
+
// normalizeBlueBubblesEvent rejects guidless payloads, so duplicate handling
|
|
968
|
+
// only needs to discriminate between known processed, in-flight, or new.
|
|
969
|
+
const duplicateReason = (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, normalized.chat.sessionKey, normalized.messageGuid)
|
|
970
|
+
? "processed"
|
|
971
|
+
: isBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid)
|
|
972
|
+
? "in_flight"
|
|
973
|
+
: null;
|
|
911
974
|
if (normalized.messageGuid
|
|
912
|
-
&&
|
|
975
|
+
&& duplicateReason) {
|
|
913
976
|
(0, runtime_1.emitNervesEvent)({
|
|
914
977
|
level: "warn",
|
|
915
978
|
component: "senses",
|
|
@@ -920,6 +983,7 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
920
983
|
sessionKey: normalized.chat.sessionKey,
|
|
921
984
|
eventType: normalized.eventType,
|
|
922
985
|
normalizedKind: normalized.kind,
|
|
986
|
+
dedupeReason: duplicateReason,
|
|
923
987
|
},
|
|
924
988
|
});
|
|
925
989
|
return handleBlueBubblesNormalizedEvent(normalized, resolvedDeps, "webhook");
|
|
@@ -929,7 +993,7 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
929
993
|
}
|
|
930
994
|
function countPendingRecoveryCandidates(agentName) {
|
|
931
995
|
return (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
|
|
932
|
-
.filter((entry) => !(0,
|
|
996
|
+
.filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid))
|
|
933
997
|
.length;
|
|
934
998
|
}
|
|
935
999
|
function parseTimestampMs(value) {
|
|
@@ -959,16 +1023,23 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
959
1023
|
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
960
1024
|
try {
|
|
961
1025
|
await client.checkHealth();
|
|
1026
|
+
const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps);
|
|
962
1027
|
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
963
1028
|
const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState);
|
|
964
|
-
const failed = recovery.failed + catchUp.failed;
|
|
965
|
-
const recovered = recovery.recovered + catchUp.recovered;
|
|
1029
|
+
const failed = captured.failed + recovery.failed + catchUp.failed;
|
|
1030
|
+
const recovered = captured.recovered + recovery.recovered + catchUp.recovered;
|
|
1031
|
+
// upstreamStatus reflects whether BlueBubbles itself is healthy and we
|
|
1032
|
+
// have unprocessed work (pendingRecoveryCount). Per-cycle recovery
|
|
1033
|
+
// failures are noted in `detail` for transparency but do NOT flip the
|
|
1034
|
+
// status to error: a single permanently-unrecoverable message would
|
|
1035
|
+
// otherwise stick the sense in "error" forever, contradicting `ouro
|
|
1036
|
+
// doctor` which only checks upstream reachability.
|
|
966
1037
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
967
|
-
upstreamStatus: recovery.pending > 0
|
|
968
|
-
detail:
|
|
969
|
-
? `recovery
|
|
970
|
-
:
|
|
971
|
-
?
|
|
1038
|
+
upstreamStatus: recovery.pending > 0 ? "error" : "ok",
|
|
1039
|
+
detail: recovery.pending > 0
|
|
1040
|
+
? `pending recovery: ${recovery.pending}`
|
|
1041
|
+
: failed > 0
|
|
1042
|
+
? `${failed} message(s) unrecoverable this cycle; upstream ok`
|
|
972
1043
|
: catchUp.recovered > 0
|
|
973
1044
|
? formatRecoveredCount(catchUp.recovered)
|
|
974
1045
|
: "upstream reachable",
|
|
@@ -1066,7 +1137,8 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
|
|
|
1066
1137
|
result.inspected++;
|
|
1067
1138
|
if (event.fromMe
|
|
1068
1139
|
|| event.timestamp < catchUpSince
|
|
1069
|
-
|| (0,
|
|
1140
|
+
|| (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
|
|
1141
|
+
|| isBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
|
|
1070
1142
|
result.skipped++;
|
|
1071
1143
|
continue;
|
|
1072
1144
|
}
|
|
@@ -1109,13 +1181,99 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
|
|
|
1109
1181
|
}
|
|
1110
1182
|
return result;
|
|
1111
1183
|
}
|
|
1184
|
+
function inboundEntryToRecoveryEvent(entry) {
|
|
1185
|
+
const chatIdentifier = entry.chatIdentifier ?? extractChatIdentifierFromSessionKey(entry.sessionKey) ?? "unknown";
|
|
1186
|
+
const normalizedSessionKey = normalizeBlueBubblesSessionKey(entry.sessionKey);
|
|
1187
|
+
const chatGuid = entry.chatGuid ?? (normalizedSessionKey.startsWith("chat:") ? normalizedSessionKey.slice("chat:".length) : undefined);
|
|
1188
|
+
const isGroup = normalizedSessionKey.includes("+;");
|
|
1189
|
+
return {
|
|
1190
|
+
kind: "message",
|
|
1191
|
+
eventType: "new-message",
|
|
1192
|
+
messageGuid: entry.messageGuid,
|
|
1193
|
+
timestamp: parseTimestampMs(entry.recordedAt) ?? Date.now(),
|
|
1194
|
+
fromMe: false,
|
|
1195
|
+
sender: {
|
|
1196
|
+
provider: "imessage-handle",
|
|
1197
|
+
externalId: chatIdentifier,
|
|
1198
|
+
rawId: chatIdentifier,
|
|
1199
|
+
displayName: chatIdentifier,
|
|
1200
|
+
},
|
|
1201
|
+
chat: {
|
|
1202
|
+
chatGuid,
|
|
1203
|
+
chatIdentifier,
|
|
1204
|
+
isGroup,
|
|
1205
|
+
sessionKey: normalizedSessionKey,
|
|
1206
|
+
sendTarget: chatGuid
|
|
1207
|
+
? { kind: "chat_guid", value: chatGuid }
|
|
1208
|
+
: { kind: "chat_identifier", value: chatIdentifier },
|
|
1209
|
+
participantHandles: [],
|
|
1210
|
+
},
|
|
1211
|
+
text: entry.textForAgent,
|
|
1212
|
+
textForAgent: entry.textForAgent,
|
|
1213
|
+
attachments: [],
|
|
1214
|
+
hasPayloadData: false,
|
|
1215
|
+
requiresRepair: true,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
|
|
1219
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1220
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1221
|
+
const client = resolvedDeps.createClient();
|
|
1222
|
+
const result = { recovered: 0, skipped: 0, failed: 0 };
|
|
1223
|
+
const seenMessageGuids = new Set();
|
|
1224
|
+
const candidates = (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
|
|
1225
|
+
.filter((entry) => {
|
|
1226
|
+
if (seenMessageGuids.has(entry.messageGuid))
|
|
1227
|
+
return false;
|
|
1228
|
+
seenMessageGuids.add(entry.messageGuid);
|
|
1229
|
+
return true;
|
|
1230
|
+
})
|
|
1231
|
+
.sort((left, right) => (parseTimestampMs(left.recordedAt) ?? 0) - (parseTimestampMs(right.recordedAt) ?? 0));
|
|
1232
|
+
for (const entry of candidates) {
|
|
1233
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid)
|
|
1234
|
+
|| isBlueBubblesMessageInFlight(entry.sessionKey, entry.messageGuid)) {
|
|
1235
|
+
result.skipped++;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
const repaired = await client.repairEvent(inboundEntryToRecoveryEvent(entry));
|
|
1240
|
+
if (repaired.kind !== "message") {
|
|
1241
|
+
result.skipped++;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source);
|
|
1245
|
+
if (handled.reason === "already_processed") {
|
|
1246
|
+
result.skipped++;
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
result.recovered++;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
result.failed++;
|
|
1254
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1255
|
+
level: "warn",
|
|
1256
|
+
component: "senses",
|
|
1257
|
+
event: "senses.bluebubbles_capture_recovery_error",
|
|
1258
|
+
message: "captured bluebubbles message recovery failed",
|
|
1259
|
+
meta: {
|
|
1260
|
+
messageGuid: entry.messageGuid,
|
|
1261
|
+
sessionKey: entry.sessionKey,
|
|
1262
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return result;
|
|
1268
|
+
}
|
|
1112
1269
|
async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
1113
1270
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1114
1271
|
const agentName = resolvedDeps.getAgentName();
|
|
1115
1272
|
const client = resolvedDeps.createClient();
|
|
1116
1273
|
const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
|
|
1117
1274
|
for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
|
|
1118
|
-
if ((0,
|
|
1275
|
+
if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, candidate.sessionKey, candidate.messageGuid)
|
|
1276
|
+
|| isBlueBubblesMessageInFlight(candidate.sessionKey, candidate.messageGuid)) {
|
|
1119
1277
|
result.skipped++;
|
|
1120
1278
|
continue;
|
|
1121
1279
|
}
|