@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.151
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/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +185 -191
- package/changelog.json +991 -0
- package/dist/heart/active-work.js +633 -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/commitments.js +105 -0
- package/dist/heart/config.js +66 -21
- package/dist/heart/core.js +645 -112
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/agent-service.js +360 -0
- package/dist/heart/daemon/auth-flow.js +457 -0
- package/dist/heart/daemon/cadence.js +70 -0
- package/dist/heart/daemon/daemon-cli.js +2301 -205
- package/dist/heart/daemon/daemon-entry.js +131 -4
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon-tombstone.js +106 -0
- package/dist/heart/daemon/daemon.js +372 -3
- package/dist/heart/daemon/habit-migration.js +181 -0
- package/dist/heart/daemon/habit-parser.js +119 -0
- package/dist/heart/daemon/habit-scheduler.js +252 -0
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +28 -111
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/inner-status.js +89 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/mcp-server.js +591 -0
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/os-cron-deps.js +134 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-path-installer.js +78 -34
- package/dist/heart/daemon/ouro-version-manager.js +171 -0
- package/dist/heart/daemon/process-manager.js +75 -0
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +50 -2
- 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 +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +2 -2
- package/dist/heart/daemon/specialist-prompt.js +10 -7
- package/dist/heart/daemon/specialist-tools.js +53 -4
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/task-scheduler.js +3 -25
- package/dist/heart/daemon/thoughts.js +507 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +64 -21
- package/dist/heart/kicks.js +2 -20
- package/dist/heart/model-capabilities.js +48 -0
- package/dist/heart/obligations.js +197 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/provider-failover.js +88 -0
- package/dist/heart/provider-ping.js +159 -0
- package/dist/heart/providers/anthropic-token.js +163 -0
- package/dist/heart/providers/anthropic.js +195 -34
- package/dist/heart/providers/azure.js +115 -9
- package/dist/heart/providers/github-copilot.js +157 -0
- package/dist/heart/providers/minimax.js +33 -3
- package/dist/heart/providers/openai-codex.js +49 -14
- package/dist/heart/session-activity.js +173 -0
- package/dist/heart/session-recall.js +216 -0
- package/dist/heart/streaming.js +117 -33
- package/dist/heart/target-resolution.js +127 -0
- package/dist/heart/tool-loop.js +200 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +122 -32
- package/dist/mind/bundle-manifest.js +14 -0
- package/dist/mind/context.js +125 -14
- package/dist/mind/{memory.js → diary.js} +70 -56
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +44 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/journal-index.js +161 -0
- package/dist/mind/obligation-steering.js +221 -0
- package/dist/mind/obligations.js +134 -0
- package/dist/mind/pending.js +76 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +612 -88
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/nerves/index.js +59 -2
- package/dist/nerves/runtime.js +5 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/ado-semantic.js +6 -0
- package/dist/repertoire/coding/codex-jsonl.js +64 -0
- package/dist/repertoire/coding/context-pack.js +254 -0
- package/dist/repertoire/coding/feedback.js +301 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +210 -4
- package/dist/repertoire/coding/spawner.js +58 -12
- package/dist/repertoire/coding/tools.js +176 -4
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/guardrails.js +290 -0
- package/dist/repertoire/mcp-client.js +254 -0
- package/dist/repertoire/mcp-manager.js +198 -0
- package/dist/repertoire/skills.js +14 -23
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/parser.js +0 -2
- package/dist/repertoire/tasks/transitions.js +1 -20
- package/dist/repertoire/tools-base.js +990 -273
- package/dist/repertoire/tools-bluebubbles.js +94 -0
- package/dist/repertoire/tools-github.js +1 -6
- package/dist/repertoire/tools-teams.js +65 -59
- package/dist/repertoire/tools.js +281 -91
- 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 +97 -0
- package/dist/senses/bluebubbles-client.js +258 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +981 -45
- package/dist/senses/cli-entry.js +1 -1
- package/dist/senses/cli-layout.js +187 -0
- package/dist/senses/cli.js +374 -131
- package/dist/senses/contextual-heartbeat.js +106 -0
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +154 -0
- package/dist/senses/inner-dialog-worker.js +112 -19
- package/dist/senses/inner-dialog.js +621 -84
- package/dist/senses/pipeline.js +463 -0
- package/dist/senses/shared-turn.js +196 -0
- package/dist/senses/surface-tool.js +95 -0
- package/dist/senses/teams.js +796 -141
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +12 -4
- package/skills/configure-dev-tools.md +81 -0
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/subagent-installer.js +0 -134
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -33,23 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.enrichReactionText = enrichReactionText;
|
|
36
37
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
38
|
+
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
37
39
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
40
|
+
exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
|
|
41
|
+
exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
|
|
38
42
|
exports.startBlueBubblesApp = startBlueBubblesApp;
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
39
44
|
const http = __importStar(require("node:http"));
|
|
40
45
|
const path = __importStar(require("node:path"));
|
|
41
46
|
const core_1 = require("../heart/core");
|
|
42
47
|
const config_1 = require("../heart/config");
|
|
43
48
|
const identity_1 = require("../heart/identity");
|
|
49
|
+
const turn_coordinator_1 = require("../heart/turn-coordinator");
|
|
44
50
|
const context_1 = require("../mind/context");
|
|
45
51
|
const tokens_1 = require("../mind/friends/tokens");
|
|
52
|
+
const group_context_1 = require("../mind/friends/group-context");
|
|
46
53
|
const resolver_1 = require("../mind/friends/resolver");
|
|
47
54
|
const store_file_1 = require("../mind/friends/store-file");
|
|
55
|
+
const types_1 = require("../mind/friends/types");
|
|
56
|
+
const channel_1 = require("../mind/friends/channel");
|
|
57
|
+
const pending_1 = require("../mind/pending");
|
|
48
58
|
const prompt_1 = require("../mind/prompt");
|
|
59
|
+
const mcp_manager_1 = require("../repertoire/mcp-manager");
|
|
60
|
+
const phrases_1 = require("../mind/phrases");
|
|
49
61
|
const runtime_1 = require("../nerves/runtime");
|
|
50
62
|
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
51
63
|
const bluebubbles_client_1 = require("./bluebubbles-client");
|
|
64
|
+
const bluebubbles_inbound_log_1 = require("./bluebubbles-inbound-log");
|
|
52
65
|
const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
|
|
66
|
+
const bluebubbles_runtime_state_1 = require("./bluebubbles-runtime-state");
|
|
67
|
+
const bluebubbles_session_cleanup_1 = require("./bluebubbles-session-cleanup");
|
|
68
|
+
const debug_activity_1 = require("./debug-activity");
|
|
69
|
+
const trust_gate_1 = require("./trust-gate");
|
|
70
|
+
const pipeline_1 = require("./pipeline");
|
|
71
|
+
const bbFailoverStates = new Map();
|
|
72
|
+
// Enrich reaction text with the original message content for context.
|
|
73
|
+
// If originalText is provided and non-empty, format as: baseText to: "truncated"
|
|
74
|
+
// Otherwise return baseText unchanged.
|
|
75
|
+
function enrichReactionText(baseText, originalText, maxLen) {
|
|
76
|
+
if (!originalText)
|
|
77
|
+
return baseText;
|
|
78
|
+
const truncated = originalText.length > maxLen
|
|
79
|
+
? originalText.slice(0, maxLen - 3) + "..."
|
|
80
|
+
: originalText;
|
|
81
|
+
return `${baseText} to: "${truncated}"`;
|
|
82
|
+
}
|
|
53
83
|
const defaultDeps = {
|
|
54
84
|
getAgentName: identity_1.getAgentName,
|
|
55
85
|
buildSystem: prompt_1.buildSystem,
|
|
@@ -64,6 +94,7 @@ const defaultDeps = {
|
|
|
64
94
|
createFriendResolver: (store, params) => new resolver_1.FriendResolver(store, params),
|
|
65
95
|
createServer: http.createServer,
|
|
66
96
|
};
|
|
97
|
+
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
67
98
|
function resolveFriendParams(event) {
|
|
68
99
|
if (event.chat.isGroup) {
|
|
69
100
|
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
@@ -81,21 +112,313 @@ function resolveFriendParams(event) {
|
|
|
81
112
|
channel: "bluebubbles",
|
|
82
113
|
};
|
|
83
114
|
}
|
|
84
|
-
function
|
|
115
|
+
function resolveGroupExternalId(event) {
|
|
116
|
+
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
117
|
+
return `group:${groupKey}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if any participant in a group chat is a known family member.
|
|
121
|
+
* Looks up each participant handle in the friend store.
|
|
122
|
+
*/
|
|
123
|
+
async function checkGroupHasFamilyMember(store, event) {
|
|
124
|
+
if (!event.chat.isGroup)
|
|
125
|
+
return false;
|
|
126
|
+
for (const handle of event.chat.participantHandles ?? []) {
|
|
127
|
+
const friend = await store.findByExternalId("imessage-handle", handle);
|
|
128
|
+
if (friend?.trustLevel === "family")
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if an acquaintance shares any group chat with a family member.
|
|
135
|
+
* Compares group-prefixed externalIds between the acquaintance and all family members.
|
|
136
|
+
*/
|
|
137
|
+
async function checkHasExistingGroupWithFamily(store, senderFriend) {
|
|
138
|
+
const trustLevel = senderFriend.trustLevel ?? "friend";
|
|
139
|
+
if (trustLevel !== "acquaintance")
|
|
140
|
+
return false;
|
|
141
|
+
const acquaintanceGroups = new Set((senderFriend.externalIds ?? [])
|
|
142
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
143
|
+
.map((eid) => eid.externalId));
|
|
144
|
+
if (acquaintanceGroups.size === 0)
|
|
145
|
+
return false;
|
|
146
|
+
const allFriends = await (store.listAll?.() ?? Promise.resolve([]));
|
|
147
|
+
for (const friend of allFriends) {
|
|
148
|
+
if (friend.trustLevel !== "family")
|
|
149
|
+
continue;
|
|
150
|
+
const friendGroups = (friend.externalIds ?? [])
|
|
151
|
+
.filter((eid) => eid.externalId.startsWith("group:"))
|
|
152
|
+
.map((eid) => eid.externalId);
|
|
153
|
+
for (const group of friendGroups) {
|
|
154
|
+
if (acquaintanceGroups.has(group))
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function extractMessageText(content) {
|
|
161
|
+
if (typeof content === "string")
|
|
162
|
+
return content;
|
|
163
|
+
if (!Array.isArray(content))
|
|
164
|
+
return "";
|
|
165
|
+
return content
|
|
166
|
+
.map((part) => {
|
|
167
|
+
if (part && typeof part === "object" && "type" in part && part.type === "text" && typeof part.text === "string") {
|
|
168
|
+
return part.text;
|
|
169
|
+
}
|
|
170
|
+
return "";
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join("\n");
|
|
174
|
+
}
|
|
175
|
+
function isHistoricalLaneMetadataLine(line) {
|
|
176
|
+
return /^\[(conversation scope|recent active lanes|routing control):?/i.test(line)
|
|
177
|
+
|| /^- (top_level|thread:[^:]+):/i.test(line);
|
|
178
|
+
}
|
|
179
|
+
function extractHistoricalLaneSummary(messages) {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const summaries = [];
|
|
182
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
183
|
+
const message = messages[index];
|
|
184
|
+
if (message.role !== "user")
|
|
185
|
+
continue;
|
|
186
|
+
const text = extractMessageText(message.content);
|
|
187
|
+
if (!text)
|
|
188
|
+
continue;
|
|
189
|
+
const firstLine = text.split("\n")[0].trim();
|
|
190
|
+
const threadMatch = firstLine.match(/thread id: ([^\]|]+)/i);
|
|
191
|
+
const laneKey = threadMatch
|
|
192
|
+
? `thread:${threadMatch[1].trim()}`
|
|
193
|
+
: /top[-_]level/i.test(firstLine)
|
|
194
|
+
? "top_level"
|
|
195
|
+
: null;
|
|
196
|
+
if (!laneKey || seen.has(laneKey))
|
|
197
|
+
continue;
|
|
198
|
+
seen.add(laneKey);
|
|
199
|
+
const snippet = text
|
|
200
|
+
.split("\n")
|
|
201
|
+
.slice(1)
|
|
202
|
+
.map((line) => line.trim())
|
|
203
|
+
.find((line) => line.length > 0 && !isHistoricalLaneMetadataLine(line))
|
|
204
|
+
?.slice(0, 80) ?? "(no recent text)";
|
|
205
|
+
summaries.push({
|
|
206
|
+
key: laneKey,
|
|
207
|
+
label: laneKey === "top_level" ? "top_level" : laneKey,
|
|
208
|
+
snippet,
|
|
209
|
+
});
|
|
210
|
+
if (summaries.length >= 5)
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
return summaries;
|
|
214
|
+
}
|
|
215
|
+
function buildConversationScopePrefix(event, existingMessages, repliedToText) {
|
|
216
|
+
if (event.kind !== "message") {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
const summaries = extractHistoricalLaneSummary(existingMessages);
|
|
220
|
+
const lines = [];
|
|
221
|
+
if (event.threadOriginatorGuid?.trim()) {
|
|
222
|
+
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]`);
|
|
223
|
+
if (repliedToText) {
|
|
224
|
+
lines.push(`[replying to: "${repliedToText}"]`);
|
|
225
|
+
}
|
|
226
|
+
lines.push(`[if you need more context about what was being discussed, use query_session to search your session history, or recall to check your memory.]`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target for this turn: top_level]");
|
|
230
|
+
}
|
|
231
|
+
if (summaries.length > 0) {
|
|
232
|
+
lines.push("[recent active lanes]");
|
|
233
|
+
for (const summary of summaries) {
|
|
234
|
+
lines.push(`- ${summary.label}: ${summary.snippet}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (event.threadOriginatorGuid?.trim() || summaries.some((summary) => summary.key.startsWith("thread:"))) {
|
|
238
|
+
lines.push("[routing control: use bluebubbles_set_reply_target with target=top_level to widen back out, or target=thread plus a listed thread id to route into a specific active thread]");
|
|
239
|
+
}
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
function buildInboundText(event, existingMessages, repliedToText) {
|
|
243
|
+
const metadataPrefix = buildConversationScopePrefix(event, existingMessages, repliedToText);
|
|
85
244
|
const baseText = event.repairNotice?.trim()
|
|
86
245
|
? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
|
|
87
246
|
: event.textForAgent;
|
|
88
|
-
if (!event.chat.isGroup)
|
|
89
|
-
return baseText;
|
|
247
|
+
if (!event.chat.isGroup) {
|
|
248
|
+
return metadataPrefix ? `${metadataPrefix}\n${baseText}` : baseText;
|
|
249
|
+
}
|
|
250
|
+
const scopedText = metadataPrefix ? `${metadataPrefix}\n${baseText}` : baseText;
|
|
90
251
|
if (event.kind === "mutation") {
|
|
91
|
-
return `${event.sender.displayName} ${
|
|
252
|
+
return `${event.sender.displayName} ${scopedText}`;
|
|
253
|
+
}
|
|
254
|
+
return `${event.sender.displayName}: ${scopedText}`;
|
|
255
|
+
}
|
|
256
|
+
function buildInboundContent(event, existingMessages, repliedToText) {
|
|
257
|
+
const text = buildInboundText(event, existingMessages, repliedToText);
|
|
258
|
+
if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
|
|
259
|
+
return text;
|
|
92
260
|
}
|
|
93
|
-
return
|
|
261
|
+
return [
|
|
262
|
+
{ type: "text", text },
|
|
263
|
+
...event.inputPartsForAgent,
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
function sessionLikelyContainsMessage(event, existingMessages) {
|
|
267
|
+
const fragment = event.textForAgent.trim();
|
|
268
|
+
if (!fragment)
|
|
269
|
+
return false;
|
|
270
|
+
return existingMessages.some((message) => {
|
|
271
|
+
if (message.role !== "user")
|
|
272
|
+
return false;
|
|
273
|
+
return extractMessageText(message.content).includes(fragment);
|
|
274
|
+
});
|
|
94
275
|
}
|
|
95
|
-
function
|
|
276
|
+
function mutationEntryToEvent(entry) {
|
|
277
|
+
return {
|
|
278
|
+
kind: "mutation",
|
|
279
|
+
eventType: entry.eventType,
|
|
280
|
+
mutationType: entry.mutationType,
|
|
281
|
+
messageGuid: entry.messageGuid,
|
|
282
|
+
targetMessageGuid: entry.targetMessageGuid ?? undefined,
|
|
283
|
+
timestamp: Date.parse(entry.recordedAt) || Date.now(),
|
|
284
|
+
fromMe: entry.fromMe,
|
|
285
|
+
sender: {
|
|
286
|
+
provider: "imessage-handle",
|
|
287
|
+
externalId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
288
|
+
rawId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
289
|
+
displayName: entry.chatIdentifier ?? entry.chatGuid ?? "Unknown",
|
|
290
|
+
},
|
|
291
|
+
chat: {
|
|
292
|
+
chatGuid: entry.chatGuid ?? undefined,
|
|
293
|
+
chatIdentifier: entry.chatIdentifier ?? undefined,
|
|
294
|
+
displayName: undefined,
|
|
295
|
+
isGroup: Boolean(entry.chatGuid?.includes(";+;")),
|
|
296
|
+
sessionKey: entry.sessionKey,
|
|
297
|
+
sendTarget: entry.chatGuid
|
|
298
|
+
? { kind: "chat_guid", value: entry.chatGuid }
|
|
299
|
+
: { kind: "chat_identifier", value: entry.chatIdentifier ?? "unknown" },
|
|
300
|
+
participantHandles: [],
|
|
301
|
+
},
|
|
302
|
+
shouldNotifyAgent: entry.shouldNotifyAgent,
|
|
303
|
+
textForAgent: entry.textForAgent,
|
|
304
|
+
requiresRepair: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function getBlueBubblesContinuityIngressTexts(event) {
|
|
308
|
+
if (event.kind !== "message")
|
|
309
|
+
return [];
|
|
310
|
+
const text = event.textForAgent.trim();
|
|
311
|
+
if (text.length > 0)
|
|
312
|
+
return [text];
|
|
313
|
+
const fallbackText = (event.inputPartsForAgent ?? [])
|
|
314
|
+
.map((part) => {
|
|
315
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
316
|
+
return part.text.trim();
|
|
317
|
+
}
|
|
318
|
+
return "";
|
|
319
|
+
})
|
|
320
|
+
.filter(Boolean)
|
|
321
|
+
.join("\n");
|
|
322
|
+
return fallbackText ? [fallbackText] : [];
|
|
323
|
+
}
|
|
324
|
+
function createReplyTargetController(event) {
|
|
325
|
+
const defaultTargetLabel = event.kind === "message" && event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
|
|
326
|
+
let selection = event.kind === "message" && event.threadOriginatorGuid?.trim()
|
|
327
|
+
? { target: "current_lane" }
|
|
328
|
+
: { target: "top_level" };
|
|
329
|
+
return {
|
|
330
|
+
getReplyToMessageGuid() {
|
|
331
|
+
if (event.kind !== "message")
|
|
332
|
+
return undefined;
|
|
333
|
+
if (selection.target === "top_level")
|
|
334
|
+
return undefined;
|
|
335
|
+
if (selection.target === "thread")
|
|
336
|
+
return selection.threadOriginatorGuid.trim();
|
|
337
|
+
return event.threadOriginatorGuid?.trim() ? event.messageGuid : undefined;
|
|
338
|
+
},
|
|
339
|
+
setSelection(next) {
|
|
340
|
+
selection = next;
|
|
341
|
+
if (next.target === "top_level") {
|
|
342
|
+
return "bluebubbles reply target override: top_level";
|
|
343
|
+
}
|
|
344
|
+
if (next.target === "thread") {
|
|
345
|
+
return `bluebubbles reply target override: thread:${next.threadOriginatorGuid}`;
|
|
346
|
+
}
|
|
347
|
+
return `bluebubbles reply target: using default for this turn (${defaultTargetLabel})`;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function emitBlueBubblesMarkReadWarning(chat, error) {
|
|
352
|
+
(0, runtime_1.emitNervesEvent)({
|
|
353
|
+
level: "warn",
|
|
354
|
+
component: "senses",
|
|
355
|
+
event: "senses.bluebubbles_mark_read_error",
|
|
356
|
+
message: "failed to mark bluebubbles chat as read",
|
|
357
|
+
meta: {
|
|
358
|
+
chatGuid: chat.chatGuid ?? null,
|
|
359
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat) {
|
|
96
364
|
let textBuffer = "";
|
|
365
|
+
const phrases = (0, phrases_1.getPhrases)();
|
|
366
|
+
const activity = (0, debug_activity_1.createDebugActivityController)({
|
|
367
|
+
thinkingPhrases: phrases.thinking,
|
|
368
|
+
followupPhrases: phrases.followup,
|
|
369
|
+
startTypingOnModelStart: !isGroupChat,
|
|
370
|
+
startTypingOnFirstTextChunk: isGroupChat,
|
|
371
|
+
suppressInitialModelStatus: true,
|
|
372
|
+
suppressFollowupPhraseStatus: true,
|
|
373
|
+
transport: {
|
|
374
|
+
sendStatus: async (text) => {
|
|
375
|
+
const sent = await client.sendText({
|
|
376
|
+
chat,
|
|
377
|
+
text,
|
|
378
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
379
|
+
});
|
|
380
|
+
return sent.messageGuid;
|
|
381
|
+
},
|
|
382
|
+
editStatus: async (_messageGuid, text) => {
|
|
383
|
+
await client.sendText({
|
|
384
|
+
chat,
|
|
385
|
+
text,
|
|
386
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
setTyping: async (active) => {
|
|
390
|
+
if (!active) {
|
|
391
|
+
await client.setTyping(chat, false);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const [markReadResult, typingResult] = await Promise.allSettled([
|
|
395
|
+
client.markChatRead(chat),
|
|
396
|
+
client.setTyping(chat, true),
|
|
397
|
+
]);
|
|
398
|
+
if (markReadResult.status === "rejected") {
|
|
399
|
+
emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
|
|
400
|
+
}
|
|
401
|
+
if (typingResult.status === "rejected") {
|
|
402
|
+
throw typingResult.reason;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
onTransportError: (operation, error) => {
|
|
407
|
+
(0, runtime_1.emitNervesEvent)({
|
|
408
|
+
level: "warn",
|
|
409
|
+
component: "senses",
|
|
410
|
+
event: "senses.bluebubbles_activity_error",
|
|
411
|
+
message: "bluebubbles activity transport failed",
|
|
412
|
+
meta: {
|
|
413
|
+
operation,
|
|
414
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
},
|
|
418
|
+
});
|
|
97
419
|
return {
|
|
98
420
|
onModelStart() {
|
|
421
|
+
activity.onModelStart();
|
|
99
422
|
(0, runtime_1.emitNervesEvent)({
|
|
100
423
|
component: "senses",
|
|
101
424
|
event: "senses.bluebubbles_turn_start",
|
|
@@ -112,10 +435,12 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
112
435
|
});
|
|
113
436
|
},
|
|
114
437
|
onTextChunk(text) {
|
|
438
|
+
activity.onTextChunk(text);
|
|
115
439
|
textBuffer += text;
|
|
116
440
|
},
|
|
117
441
|
onReasoningChunk(_text) { },
|
|
118
442
|
onToolStart(name, _args) {
|
|
443
|
+
activity.onToolStart(name, _args);
|
|
119
444
|
(0, runtime_1.emitNervesEvent)({
|
|
120
445
|
component: "senses",
|
|
121
446
|
event: "senses.bluebubbles_tool_start",
|
|
@@ -124,6 +449,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
124
449
|
});
|
|
125
450
|
},
|
|
126
451
|
onToolEnd(name, summary, success) {
|
|
452
|
+
activity.onToolEnd(name, summary, success);
|
|
127
453
|
(0, runtime_1.emitNervesEvent)({
|
|
128
454
|
component: "senses",
|
|
129
455
|
event: "senses.bluebubbles_tool_end",
|
|
@@ -132,6 +458,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
132
458
|
});
|
|
133
459
|
},
|
|
134
460
|
onError(error, severity) {
|
|
461
|
+
activity.onError(error);
|
|
135
462
|
(0, runtime_1.emitNervesEvent)({
|
|
136
463
|
level: severity === "terminal" ? "error" : "warn",
|
|
137
464
|
component: "senses",
|
|
@@ -144,17 +471,23 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
144
471
|
textBuffer = "";
|
|
145
472
|
},
|
|
146
473
|
async flush() {
|
|
474
|
+
await activity.drain();
|
|
147
475
|
const trimmed = textBuffer.trim();
|
|
148
476
|
if (!trimmed) {
|
|
477
|
+
await activity.finish();
|
|
149
478
|
return;
|
|
150
479
|
}
|
|
151
480
|
textBuffer = "";
|
|
481
|
+
await activity.finish();
|
|
152
482
|
await client.sendText({
|
|
153
483
|
chat,
|
|
154
484
|
text: trimmed,
|
|
155
|
-
replyToMessageGuid,
|
|
485
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
156
486
|
});
|
|
157
487
|
},
|
|
488
|
+
async finish() {
|
|
489
|
+
await activity.finish();
|
|
490
|
+
},
|
|
158
491
|
};
|
|
159
492
|
}
|
|
160
493
|
async function readRequestBody(req) {
|
|
@@ -173,10 +506,8 @@ function isWebhookPasswordValid(url, expectedPassword) {
|
|
|
173
506
|
const provided = url.searchParams.get("password");
|
|
174
507
|
return !provided || provided === expectedPassword;
|
|
175
508
|
}
|
|
176
|
-
async function
|
|
177
|
-
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
509
|
+
async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
178
510
|
const client = resolvedDeps.createClient();
|
|
179
|
-
const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
|
|
180
511
|
if (event.fromMe) {
|
|
181
512
|
(0, runtime_1.emitNervesEvent)({
|
|
182
513
|
component: "senses",
|
|
@@ -219,46 +550,340 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
219
550
|
});
|
|
220
551
|
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
221
552
|
}
|
|
553
|
+
// ── Adapter setup: friend, session, content, callbacks ──────────
|
|
222
554
|
const store = resolvedDeps.createFriendStore();
|
|
223
555
|
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
friendStore: store,
|
|
228
|
-
summarize: (0, core_1.createSummarize)(),
|
|
229
|
-
context,
|
|
230
|
-
};
|
|
556
|
+
const baseContext = await resolver.resolve();
|
|
557
|
+
const context = { ...baseContext, isGroupChat: event.chat.isGroup };
|
|
558
|
+
const replyTarget = createReplyTargetController(event);
|
|
231
559
|
const friendId = context.friend.id;
|
|
232
560
|
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
561
|
+
try {
|
|
562
|
+
(0, bluebubbles_session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
(0, runtime_1.emitNervesEvent)({
|
|
566
|
+
level: "warn",
|
|
567
|
+
component: "senses",
|
|
568
|
+
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
569
|
+
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
570
|
+
meta: {
|
|
571
|
+
sessionPath: sessPath,
|
|
572
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
577
|
+
// Pre-load session inside the turn lock so same-chat deliveries cannot race on stale trunk state.
|
|
578
|
+
const existing = resolvedDeps.loadSession(sessPath);
|
|
579
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
580
|
+
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
581
|
+
? existing.messages
|
|
582
|
+
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", { mcpManager }, context) }];
|
|
583
|
+
if (event.kind === "message") {
|
|
584
|
+
const agentName = resolvedDeps.getAgentName();
|
|
585
|
+
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
586
|
+
(0, runtime_1.emitNervesEvent)({
|
|
587
|
+
component: "senses",
|
|
588
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
589
|
+
message: "skipped bluebubbles message already recorded as handled",
|
|
590
|
+
meta: {
|
|
591
|
+
messageGuid: event.messageGuid,
|
|
592
|
+
sessionKey: event.chat.sessionKey,
|
|
593
|
+
source,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
597
|
+
}
|
|
598
|
+
// Record EARLY to prevent duplicate processing. BB webhooks can retry
|
|
599
|
+
// before the first turn completes — recording after the turn is too late.
|
|
600
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, source);
|
|
601
|
+
if (source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)) {
|
|
602
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, "recovery-bootstrap");
|
|
603
|
+
(0, runtime_1.emitNervesEvent)({
|
|
604
|
+
component: "senses",
|
|
605
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
606
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
607
|
+
meta: {
|
|
608
|
+
messageGuid: event.messageGuid,
|
|
609
|
+
sessionKey: event.chat.sessionKey,
|
|
610
|
+
source,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (event.kind === "message" && event.chat.isGroup) {
|
|
617
|
+
await (0, group_context_1.upsertGroupContextParticipants)({
|
|
618
|
+
store,
|
|
619
|
+
participants: (event.chat.participantHandles ?? []).map((externalId) => ({
|
|
620
|
+
provider: "imessage-handle",
|
|
621
|
+
externalId,
|
|
622
|
+
})),
|
|
623
|
+
groupExternalId: resolveGroupExternalId(event),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// Fetch the text of the message being replied to (if this is a threaded reply)
|
|
627
|
+
const threadGuid = event.kind === "message" ? event.threadOriginatorGuid?.trim() : undefined;
|
|
628
|
+
let repliedToText = null;
|
|
629
|
+
if (threadGuid) {
|
|
630
|
+
repliedToText = await client.getMessageText(threadGuid).catch(/* v8 ignore next */ () => null);
|
|
631
|
+
(0, runtime_1.emitNervesEvent)({
|
|
632
|
+
component: "senses",
|
|
633
|
+
event: "senses.bluebubbles_reply_context",
|
|
634
|
+
message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
|
|
635
|
+
meta: { threadGuid, hasText: !!repliedToText },
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
// Enrich reaction mutations with the original message text for context
|
|
639
|
+
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
640
|
+
if (isReaction && event.targetMessageGuid) {
|
|
641
|
+
/* v8 ignore start -- best-effort lookup; enrichReactionText covered by unit tests @preserve */
|
|
642
|
+
const originalText = await client.getMessageText(event.targetMessageGuid).catch(() => null);
|
|
643
|
+
if (originalText)
|
|
644
|
+
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
645
|
+
/* v8 ignore stop */
|
|
646
|
+
}
|
|
647
|
+
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
648
|
+
const userMessage = {
|
|
649
|
+
role: "user",
|
|
650
|
+
content: buildInboundContent(event, existing?.messages ?? sessionMessages, repliedToText),
|
|
651
|
+
};
|
|
652
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup);
|
|
653
|
+
const controller = new AbortController();
|
|
654
|
+
// BB-specific tool context wrappers
|
|
655
|
+
const summarize = (0, core_1.createSummarize)();
|
|
656
|
+
const bbCapabilities = (0, channel_1.getChannelCapabilities)("bluebubbles");
|
|
657
|
+
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
658
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
659
|
+
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
660
|
+
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
661
|
+
? false
|
|
662
|
+
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
663
|
+
// ── Call shared pipeline ──────────────────────────────────────────
|
|
664
|
+
// Buffer terminal errors so failover can suppress them.
|
|
665
|
+
// If failover produces a message, the buffered error is skipped.
|
|
666
|
+
// If failover doesn't fire, the buffered error is replayed.
|
|
667
|
+
let bufferedTerminalError = null;
|
|
668
|
+
/* v8 ignore start -- failover-aware error buffering @preserve */
|
|
669
|
+
const failoverAwareCallbacks = {
|
|
670
|
+
...callbacks,
|
|
671
|
+
onError(error, severity) {
|
|
672
|
+
if (severity === "terminal") {
|
|
673
|
+
bufferedTerminalError = error;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
callbacks.onError(error, severity);
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
/* v8 ignore stop */
|
|
680
|
+
try {
|
|
681
|
+
const result = await (0, pipeline_1.handleInboundTurn)({
|
|
682
|
+
channel: "bluebubbles",
|
|
683
|
+
sessionKey: event.chat.sessionKey,
|
|
684
|
+
capabilities: bbCapabilities,
|
|
685
|
+
messages: [userMessage],
|
|
686
|
+
continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
|
|
687
|
+
friendResolver: { resolve: () => Promise.resolve(context) },
|
|
688
|
+
sessionLoader: {
|
|
689
|
+
loadOrCreate: () => Promise.resolve({
|
|
690
|
+
messages: sessionMessages,
|
|
691
|
+
sessionPath: sessPath,
|
|
692
|
+
state: existing?.state,
|
|
693
|
+
}),
|
|
694
|
+
},
|
|
695
|
+
pendingDir,
|
|
696
|
+
friendStore: store,
|
|
697
|
+
provider: "imessage-handle",
|
|
698
|
+
externalId: event.sender.externalId || event.sender.rawId,
|
|
699
|
+
isGroupChat: event.chat.isGroup,
|
|
700
|
+
groupHasFamilyMember,
|
|
701
|
+
hasExistingGroupWithFamily,
|
|
702
|
+
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
703
|
+
drainPending: pending_1.drainPending,
|
|
704
|
+
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
|
|
705
|
+
runAgent: (msgs, cb, channel, sig, opts) => resolvedDeps.runAgent(msgs, cb, channel, sig, {
|
|
706
|
+
...opts,
|
|
707
|
+
toolContext: {
|
|
708
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
709
|
+
signin: async () => undefined,
|
|
710
|
+
...opts?.toolContext,
|
|
711
|
+
summarize,
|
|
712
|
+
bluebubblesReplyTarget: {
|
|
713
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
714
|
+
},
|
|
715
|
+
codingFeedback: {
|
|
716
|
+
send: async (message) => {
|
|
717
|
+
await client.sendText({
|
|
718
|
+
chat: event.chat,
|
|
719
|
+
text: message,
|
|
720
|
+
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
721
|
+
});
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
}),
|
|
726
|
+
postTurn: resolvedDeps.postTurn,
|
|
727
|
+
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
728
|
+
signal: controller.signal,
|
|
729
|
+
runAgentOptions: { mcpManager, ...(isReaction ? { isReactionSignal: true } : {}) },
|
|
730
|
+
callbacks: failoverAwareCallbacks,
|
|
731
|
+
failoverState: (() => {
|
|
732
|
+
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
|
733
|
+
bbFailoverStates.set(event.chat.sessionKey, { pending: null });
|
|
734
|
+
}
|
|
735
|
+
return bbFailoverStates.get(event.chat.sessionKey);
|
|
736
|
+
})(),
|
|
737
|
+
});
|
|
738
|
+
/* v8 ignore start -- failover display + error replay @preserve */
|
|
739
|
+
if (result.failoverMessage) {
|
|
740
|
+
// Failover handled it — show the failover message, skip the buffered error
|
|
741
|
+
await client.sendText({ chat: event.chat, text: result.failoverMessage });
|
|
742
|
+
}
|
|
743
|
+
else if (bufferedTerminalError) {
|
|
744
|
+
// No failover — replay the buffered terminal error
|
|
745
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
746
|
+
}
|
|
747
|
+
/* v8 ignore stop */
|
|
748
|
+
// ── Handle gate result ────────────────────────────────────────
|
|
749
|
+
if (!result.gateResult.allowed) {
|
|
750
|
+
// Send auto-reply via BB API if the gate provides one
|
|
751
|
+
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
752
|
+
await client.sendText({
|
|
753
|
+
chat: event.chat,
|
|
754
|
+
text: result.gateResult.autoReply,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
if (event.kind === "message") {
|
|
758
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
handled: true,
|
|
762
|
+
notifiedAgent: false,
|
|
763
|
+
kind: event.kind,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// Gate allowed — flush the agent's reply
|
|
767
|
+
await callbacks.flush();
|
|
768
|
+
if (event.kind === "message") {
|
|
769
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
770
|
+
}
|
|
771
|
+
(0, runtime_1.emitNervesEvent)({
|
|
772
|
+
component: "senses",
|
|
773
|
+
event: "senses.bluebubbles_turn_end",
|
|
774
|
+
message: "bluebubbles event handled",
|
|
775
|
+
meta: {
|
|
776
|
+
messageGuid: event.messageGuid,
|
|
777
|
+
kind: event.kind,
|
|
778
|
+
sessionKey: event.chat.sessionKey,
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
handled: true,
|
|
783
|
+
notifiedAgent: true,
|
|
784
|
+
kind: event.kind,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
finally {
|
|
788
|
+
// If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
|
|
789
|
+
// replay it now so the user still sees the error.
|
|
790
|
+
/* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
|
|
791
|
+
if (bufferedTerminalError) {
|
|
792
|
+
callbacks.onError(bufferedTerminalError, "terminal");
|
|
793
|
+
bufferedTerminalError = null;
|
|
794
|
+
}
|
|
795
|
+
/* v8 ignore stop */
|
|
796
|
+
await callbacks.finish();
|
|
797
|
+
}
|
|
256
798
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
799
|
+
}
|
|
800
|
+
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
801
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
802
|
+
const client = resolvedDeps.createClient();
|
|
803
|
+
const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
|
|
804
|
+
return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
|
|
805
|
+
}
|
|
806
|
+
function countPendingRecoveryCandidates(agentName) {
|
|
807
|
+
return (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
|
|
808
|
+
.filter((entry) => !(0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, entry.sessionKey, entry.messageGuid))
|
|
809
|
+
.length;
|
|
810
|
+
}
|
|
811
|
+
async function syncBlueBubblesRuntime(deps = {}) {
|
|
812
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
813
|
+
const agentName = resolvedDeps.getAgentName();
|
|
814
|
+
const client = resolvedDeps.createClient();
|
|
815
|
+
const checkedAt = new Date().toISOString();
|
|
816
|
+
try {
|
|
817
|
+
await client.checkHealth();
|
|
818
|
+
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
819
|
+
(0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
820
|
+
upstreamStatus: recovery.pending > 0 || recovery.failed > 0 ? "error" : "ok",
|
|
821
|
+
detail: recovery.failed > 0
|
|
822
|
+
? `recovery failures: ${recovery.failed}`
|
|
823
|
+
: recovery.pending > 0
|
|
824
|
+
? `pending recovery: ${recovery.pending}`
|
|
825
|
+
: "upstream reachable",
|
|
826
|
+
lastCheckedAt: checkedAt,
|
|
827
|
+
pendingRecoveryCount: recovery.pending,
|
|
828
|
+
lastRecoveredAt: recovery.recovered > 0 ? checkedAt : undefined,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
(0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
833
|
+
upstreamStatus: "error",
|
|
834
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
835
|
+
lastCheckedAt: checkedAt,
|
|
836
|
+
pendingRecoveryCount: countPendingRecoveryCandidates(agentName),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
841
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
842
|
+
const agentName = resolvedDeps.getAgentName();
|
|
843
|
+
const client = resolvedDeps.createClient();
|
|
844
|
+
const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
|
|
845
|
+
for (const candidate of (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
|
|
846
|
+
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, candidate.sessionKey, candidate.messageGuid)) {
|
|
847
|
+
result.skipped++;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
try {
|
|
851
|
+
const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
|
|
852
|
+
if (repaired.kind !== "message") {
|
|
853
|
+
result.pending++;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery");
|
|
857
|
+
if (handled.reason === "already_processed") {
|
|
858
|
+
result.skipped++;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
result.recovered++;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
result.failed++;
|
|
866
|
+
(0, runtime_1.emitNervesEvent)({
|
|
867
|
+
level: "warn",
|
|
868
|
+
component: "senses",
|
|
869
|
+
event: "senses.bluebubbles_recovery_error",
|
|
870
|
+
message: "bluebubbles backlog recovery failed",
|
|
871
|
+
meta: {
|
|
872
|
+
messageGuid: candidate.messageGuid,
|
|
873
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (result.recovered > 0 || result.skipped > 0 || result.pending > 0 || result.failed > 0) {
|
|
879
|
+
(0, runtime_1.emitNervesEvent)({
|
|
880
|
+
component: "senses",
|
|
881
|
+
event: "senses.bluebubbles_recovery_complete",
|
|
882
|
+
message: "bluebubbles backlog recovery pass completed",
|
|
883
|
+
meta: { ...result },
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return result;
|
|
262
887
|
}
|
|
263
888
|
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
264
889
|
return async (req, res) => {
|
|
@@ -315,11 +940,321 @@ function createBlueBubblesWebhookHandler(deps = {}) {
|
|
|
315
940
|
}
|
|
316
941
|
};
|
|
317
942
|
}
|
|
943
|
+
function findImessageHandle(friend) {
|
|
944
|
+
for (const ext of friend.externalIds) {
|
|
945
|
+
if (ext.provider === "imessage-handle" && !ext.externalId.startsWith("group:")) {
|
|
946
|
+
return ext.externalId;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
function normalizeBlueBubblesSessionKey(sessionKey) {
|
|
952
|
+
const trimmed = sessionKey.trim();
|
|
953
|
+
if (trimmed.startsWith("chat_identifier_")) {
|
|
954
|
+
return `chat_identifier:${trimmed.slice("chat_identifier_".length)}`;
|
|
955
|
+
}
|
|
956
|
+
if (trimmed.startsWith("chat_")) {
|
|
957
|
+
return `chat:${trimmed.slice("chat_".length)}`;
|
|
958
|
+
}
|
|
959
|
+
return trimmed;
|
|
960
|
+
}
|
|
961
|
+
function extractChatIdentifierFromSessionKey(sessionKey) {
|
|
962
|
+
const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
|
|
963
|
+
if (normalizedKey.startsWith("chat:")) {
|
|
964
|
+
const chatGuid = normalizedKey.slice("chat:".length).trim();
|
|
965
|
+
const parts = chatGuid.split(";");
|
|
966
|
+
return parts.length >= 3 ? parts[2]?.trim() || undefined : undefined;
|
|
967
|
+
}
|
|
968
|
+
if (normalizedKey.startsWith("chat_identifier:")) {
|
|
969
|
+
const identifier = normalizedKey.slice("chat_identifier:".length).trim();
|
|
970
|
+
return identifier || undefined;
|
|
971
|
+
}
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
974
|
+
function buildChatRefForSessionKey(friend, sessionKey) {
|
|
975
|
+
const normalizedKey = normalizeBlueBubblesSessionKey(sessionKey);
|
|
976
|
+
if (normalizedKey.startsWith("chat:")) {
|
|
977
|
+
const chatGuid = normalizedKey.slice("chat:".length).trim();
|
|
978
|
+
if (!chatGuid)
|
|
979
|
+
return null;
|
|
980
|
+
return {
|
|
981
|
+
chatGuid,
|
|
982
|
+
chatIdentifier: extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend),
|
|
983
|
+
isGroup: chatGuid.includes(";+;"),
|
|
984
|
+
sessionKey,
|
|
985
|
+
sendTarget: { kind: "chat_guid", value: chatGuid },
|
|
986
|
+
participantHandles: [],
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const chatIdentifier = extractChatIdentifierFromSessionKey(sessionKey) ?? findImessageHandle(friend);
|
|
990
|
+
if (!chatIdentifier)
|
|
991
|
+
return null;
|
|
992
|
+
return {
|
|
993
|
+
chatIdentifier,
|
|
994
|
+
isGroup: false,
|
|
995
|
+
sessionKey,
|
|
996
|
+
sendTarget: { kind: "chat_identifier", value: chatIdentifier },
|
|
997
|
+
participantHandles: [],
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
|
|
1001
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1002
|
+
const client = resolvedDeps.createClient();
|
|
1003
|
+
const store = resolvedDeps.createFriendStore();
|
|
1004
|
+
let friend;
|
|
1005
|
+
try {
|
|
1006
|
+
friend = await store.get(params.friendId);
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
friend = null;
|
|
1010
|
+
}
|
|
1011
|
+
if (!friend) {
|
|
1012
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1013
|
+
level: "warn",
|
|
1014
|
+
component: "senses",
|
|
1015
|
+
event: "senses.bluebubbles_proactive_no_friend",
|
|
1016
|
+
message: "proactive send skipped: friend not found",
|
|
1017
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1018
|
+
});
|
|
1019
|
+
return { delivered: false, reason: "friend_not_found" };
|
|
1020
|
+
}
|
|
1021
|
+
const explicitCrossChatAuthorized = params.intent === "explicit_cross_chat"
|
|
1022
|
+
&& types_1.TRUSTED_LEVELS.has(params.authorizingSession?.trustLevel ?? "stranger");
|
|
1023
|
+
if (!explicitCrossChatAuthorized && !types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
1024
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1025
|
+
component: "senses",
|
|
1026
|
+
event: "senses.bluebubbles_proactive_trust_skip",
|
|
1027
|
+
message: "proactive send skipped: trust level not allowed",
|
|
1028
|
+
meta: {
|
|
1029
|
+
friendId: params.friendId,
|
|
1030
|
+
sessionKey: params.sessionKey,
|
|
1031
|
+
trustLevel: friend.trustLevel ?? "unknown",
|
|
1032
|
+
intent: params.intent ?? "generic_outreach",
|
|
1033
|
+
authorizingTrustLevel: params.authorizingSession?.trustLevel ?? null,
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
return { delivered: false, reason: "trust_skip" };
|
|
1037
|
+
}
|
|
1038
|
+
const chat = buildChatRefForSessionKey(friend, params.sessionKey);
|
|
1039
|
+
if (!chat) {
|
|
1040
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1041
|
+
level: "warn",
|
|
1042
|
+
component: "senses",
|
|
1043
|
+
event: "senses.bluebubbles_proactive_no_handle",
|
|
1044
|
+
message: "proactive send skipped: no iMessage handle found",
|
|
1045
|
+
meta: { friendId: params.friendId, sessionKey: params.sessionKey },
|
|
1046
|
+
});
|
|
1047
|
+
return { delivered: false, reason: "missing_target" };
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
await client.sendText({ chat, text: params.text });
|
|
1051
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1052
|
+
component: "senses",
|
|
1053
|
+
event: "senses.bluebubbles_proactive_sent",
|
|
1054
|
+
message: "proactive bluebubbles message sent",
|
|
1055
|
+
meta: {
|
|
1056
|
+
friendId: params.friendId,
|
|
1057
|
+
sessionKey: params.sessionKey,
|
|
1058
|
+
chatGuid: chat.chatGuid ?? null,
|
|
1059
|
+
chatIdentifier: chat.chatIdentifier ?? null,
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
return { delivered: true };
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1066
|
+
level: "error",
|
|
1067
|
+
component: "senses",
|
|
1068
|
+
event: "senses.bluebubbles_proactive_send_error",
|
|
1069
|
+
message: "proactive bluebubbles send failed",
|
|
1070
|
+
meta: {
|
|
1071
|
+
friendId: params.friendId,
|
|
1072
|
+
sessionKey: params.sessionKey,
|
|
1073
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
return { delivered: false, reason: "send_error" };
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function scanPendingBlueBubblesFiles(pendingRoot) {
|
|
1080
|
+
const results = [];
|
|
1081
|
+
let friendIds;
|
|
1082
|
+
try {
|
|
1083
|
+
friendIds = fs.readdirSync(pendingRoot);
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return results;
|
|
1087
|
+
}
|
|
1088
|
+
for (const friendId of friendIds) {
|
|
1089
|
+
const bbDir = path.join(pendingRoot, friendId, "bluebubbles");
|
|
1090
|
+
let keys;
|
|
1091
|
+
try {
|
|
1092
|
+
keys = fs.readdirSync(bbDir);
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
for (const key of keys) {
|
|
1098
|
+
const keyDir = path.join(bbDir, key);
|
|
1099
|
+
let files;
|
|
1100
|
+
try {
|
|
1101
|
+
files = fs.readdirSync(keyDir);
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
for (const file of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
1107
|
+
const filePath = path.join(keyDir, file);
|
|
1108
|
+
try {
|
|
1109
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1110
|
+
results.push({ friendId, key, filePath, content });
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
// skip unreadable files
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return results;
|
|
1119
|
+
}
|
|
1120
|
+
async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
|
|
1121
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1122
|
+
const root = pendingRoot ?? path.join((0, identity_1.getAgentRoot)(), "state", "pending");
|
|
1123
|
+
const client = resolvedDeps.createClient();
|
|
1124
|
+
const store = resolvedDeps.createFriendStore();
|
|
1125
|
+
const pendingFiles = scanPendingBlueBubblesFiles(root);
|
|
1126
|
+
const result = { sent: 0, skipped: 0, failed: 0 };
|
|
1127
|
+
for (const { friendId, filePath, content } of pendingFiles) {
|
|
1128
|
+
let parsed;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = JSON.parse(content);
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
result.failed++;
|
|
1134
|
+
try {
|
|
1135
|
+
fs.unlinkSync(filePath);
|
|
1136
|
+
}
|
|
1137
|
+
catch { /* ignore */ }
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
const messageText = typeof parsed.content === "string" ? parsed.content : "";
|
|
1141
|
+
if (!messageText.trim()) {
|
|
1142
|
+
result.skipped++;
|
|
1143
|
+
try {
|
|
1144
|
+
fs.unlinkSync(filePath);
|
|
1145
|
+
}
|
|
1146
|
+
catch { /* ignore */ }
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
let friend;
|
|
1150
|
+
try {
|
|
1151
|
+
friend = await store.get(friendId);
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
friend = null;
|
|
1155
|
+
}
|
|
1156
|
+
if (!friend) {
|
|
1157
|
+
result.skipped++;
|
|
1158
|
+
try {
|
|
1159
|
+
fs.unlinkSync(filePath);
|
|
1160
|
+
}
|
|
1161
|
+
catch { /* ignore */ }
|
|
1162
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1163
|
+
level: "warn",
|
|
1164
|
+
component: "senses",
|
|
1165
|
+
event: "senses.bluebubbles_proactive_no_friend",
|
|
1166
|
+
message: "proactive send skipped: friend not found",
|
|
1167
|
+
meta: { friendId },
|
|
1168
|
+
});
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (!types_1.TRUSTED_LEVELS.has(friend.trustLevel ?? "stranger")) {
|
|
1172
|
+
result.skipped++;
|
|
1173
|
+
try {
|
|
1174
|
+
fs.unlinkSync(filePath);
|
|
1175
|
+
}
|
|
1176
|
+
catch { /* ignore */ }
|
|
1177
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1178
|
+
component: "senses",
|
|
1179
|
+
event: "senses.bluebubbles_proactive_trust_skip",
|
|
1180
|
+
message: "proactive send skipped: trust level not allowed",
|
|
1181
|
+
meta: { friendId, trustLevel: friend.trustLevel ?? "unknown" },
|
|
1182
|
+
});
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
const handle = findImessageHandle(friend);
|
|
1186
|
+
if (!handle) {
|
|
1187
|
+
result.skipped++;
|
|
1188
|
+
try {
|
|
1189
|
+
fs.unlinkSync(filePath);
|
|
1190
|
+
}
|
|
1191
|
+
catch { /* ignore */ }
|
|
1192
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1193
|
+
level: "warn",
|
|
1194
|
+
component: "senses",
|
|
1195
|
+
event: "senses.bluebubbles_proactive_no_handle",
|
|
1196
|
+
message: "proactive send skipped: no iMessage handle found",
|
|
1197
|
+
meta: { friendId },
|
|
1198
|
+
});
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const chat = {
|
|
1202
|
+
chatIdentifier: handle,
|
|
1203
|
+
isGroup: false,
|
|
1204
|
+
sessionKey: friendId,
|
|
1205
|
+
sendTarget: { kind: "chat_identifier", value: handle },
|
|
1206
|
+
participantHandles: [],
|
|
1207
|
+
};
|
|
1208
|
+
try {
|
|
1209
|
+
await client.sendText({ chat, text: messageText });
|
|
1210
|
+
result.sent++;
|
|
1211
|
+
try {
|
|
1212
|
+
fs.unlinkSync(filePath);
|
|
1213
|
+
}
|
|
1214
|
+
catch { /* ignore */ }
|
|
1215
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1216
|
+
component: "senses",
|
|
1217
|
+
event: "senses.bluebubbles_proactive_sent",
|
|
1218
|
+
message: "proactive bluebubbles message sent",
|
|
1219
|
+
meta: { friendId, handle },
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
result.failed++;
|
|
1224
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1225
|
+
level: "error",
|
|
1226
|
+
component: "senses",
|
|
1227
|
+
event: "senses.bluebubbles_proactive_send_error",
|
|
1228
|
+
message: "proactive bluebubbles send failed",
|
|
1229
|
+
meta: {
|
|
1230
|
+
friendId,
|
|
1231
|
+
handle,
|
|
1232
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (result.sent > 0 || result.skipped > 0 || result.failed > 0) {
|
|
1238
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1239
|
+
component: "senses",
|
|
1240
|
+
event: "senses.bluebubbles_proactive_drain_complete",
|
|
1241
|
+
message: "bluebubbles proactive drain complete",
|
|
1242
|
+
meta: { sent: result.sent, skipped: result.skipped, failed: result.failed },
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
318
1247
|
function startBlueBubblesApp(deps = {}) {
|
|
319
1248
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
320
1249
|
resolvedDeps.createClient();
|
|
321
1250
|
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
322
1251
|
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps));
|
|
1252
|
+
const runtimeTimer = setInterval(() => {
|
|
1253
|
+
void syncBlueBubblesRuntime(resolvedDeps);
|
|
1254
|
+
}, BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS);
|
|
1255
|
+
server.on?.("close", () => {
|
|
1256
|
+
clearInterval(runtimeTimer);
|
|
1257
|
+
});
|
|
323
1258
|
server.listen(channelConfig.port, () => {
|
|
324
1259
|
(0, runtime_1.emitNervesEvent)({
|
|
325
1260
|
component: "channels",
|
|
@@ -328,5 +1263,6 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
328
1263
|
meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
|
|
329
1264
|
});
|
|
330
1265
|
});
|
|
1266
|
+
void syncBlueBubblesRuntime(resolvedDeps);
|
|
331
1267
|
return server;
|
|
332
1268
|
}
|