@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.727
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 +9 -0
- package/dist/heart/core.js +181 -209
- package/dist/heart/daemon/daemon.js +79 -14
- package/dist/heart/daemon/process-manager.js +100 -18
- package/dist/heart/providers/anthropic.js +17 -12
- package/dist/heart/streaming.js +6 -6
- package/dist/mind/prompt-budget.js +104 -5
- package/dist/repertoire/tools.js +0 -10
- package/dist/senses/bluebubbles/client.js +43 -57
- package/dist/senses/bluebubbles/context-packet.js +104 -19
- package/dist/senses/bluebubbles/entry.js +14 -1
- package/dist/senses/bluebubbles/index.js +1382 -719
- package/dist/senses/bluebubbles/latest-turn.js +311 -0
- package/dist/senses/bluebubbles/outbound-state.js +4 -0
- package/dist/senses/bluebubbles/reaction-policy.js +4 -12
- package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
- package/package.json +1 -1
|
@@ -33,11 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.BB_IN_FLIGHT_MAX_AGE_MS = void 0;
|
|
37
|
-
exports.
|
|
36
|
+
exports.BB_IN_FLIGHT_MAX_AGE_MS = exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS = void 0;
|
|
37
|
+
exports.pruneBlueBubblesSemanticObservationGenerations = pruneBlueBubblesSemanticObservationGenerations;
|
|
38
38
|
exports.tokenizeForDedupe = tokenizeForDedupe;
|
|
39
39
|
exports.jaccardSimilarity = jaccardSimilarity;
|
|
40
|
-
exports.createStatusBatcher = createStatusBatcher;
|
|
41
40
|
exports.isBlueBubblesMessageInFlight = isBlueBubblesMessageInFlight;
|
|
42
41
|
exports.beginBlueBubblesMessageInFlight = beginBlueBubblesMessageInFlight;
|
|
43
42
|
exports.endBlueBubblesMessageInFlight = endBlueBubblesMessageInFlight;
|
|
@@ -47,6 +46,7 @@ exports.isAgentSelfHandle = isAgentSelfHandle;
|
|
|
47
46
|
exports.getDiscoveredOwnHandles = getDiscoveredOwnHandles;
|
|
48
47
|
exports.clearDiscoveredOwnHandles = clearDiscoveredOwnHandles;
|
|
49
48
|
exports.recordDiscoveredOwnHandle = recordDiscoveredOwnHandle;
|
|
49
|
+
exports.seedBlueBubblesStartupSemanticObservations = seedBlueBubblesStartupSemanticObservations;
|
|
50
50
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
51
51
|
exports.recoverQueuedBlueBubblesMessages = recoverQueuedBlueBubblesMessages;
|
|
52
52
|
exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
|
|
@@ -83,14 +83,106 @@ const semantic_receipts_1 = require("./semantic-receipts");
|
|
|
83
83
|
const runtime_state_1 = require("./runtime-state");
|
|
84
84
|
const session_cleanup_1 = require("./session-cleanup");
|
|
85
85
|
const reaction_policy_1 = require("./reaction-policy");
|
|
86
|
+
const latest_turn_1 = require("./latest-turn");
|
|
86
87
|
const active_turns_1 = require("./active-turns");
|
|
87
88
|
const trust_gate_1 = require("../trust-gate");
|
|
88
89
|
const pipeline_1 = require("../pipeline");
|
|
89
|
-
const context_packets_1 = require("../context-packets");
|
|
90
90
|
const context_packet_ledger_1 = require("../context-packet-ledger");
|
|
91
91
|
const context_packet_1 = require("./context-packet");
|
|
92
92
|
const autonomy_budget_1 = require("../../heart/autonomy-budget");
|
|
93
|
+
const outbound_state_1 = require("./outbound-state");
|
|
93
94
|
const bbFailoverStates = new Map();
|
|
95
|
+
const activeSemanticHandlingSlots = new Map();
|
|
96
|
+
const semanticObservationGenerations = new Map();
|
|
97
|
+
const BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES = new Set([
|
|
98
|
+
"accepted",
|
|
99
|
+
"enqueued",
|
|
100
|
+
"local-visible",
|
|
101
|
+
"delivered",
|
|
102
|
+
]);
|
|
103
|
+
exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS = 15 * 60_000;
|
|
104
|
+
function pruneBlueBubblesSemanticObservationGenerations(nowMs = Date.now()) {
|
|
105
|
+
let pruned = 0;
|
|
106
|
+
for (const [keyHash, generation] of semanticObservationGenerations) {
|
|
107
|
+
if (generation.retainedAtMs === null
|
|
108
|
+
|| nowMs - generation.retainedAtMs < exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS)
|
|
109
|
+
continue;
|
|
110
|
+
(0, latest_turn_1.clearPending)(generation.reservation);
|
|
111
|
+
semanticObservationGenerations.delete(keyHash);
|
|
112
|
+
pruned += 1;
|
|
113
|
+
}
|
|
114
|
+
return pruned;
|
|
115
|
+
}
|
|
116
|
+
function adoptBlueBubblesSemanticObservation(keyHash, observedReservation, activate) {
|
|
117
|
+
pruneBlueBubblesSemanticObservationGenerations();
|
|
118
|
+
const existing = semanticObservationGenerations.get(keyHash);
|
|
119
|
+
if (!existing) {
|
|
120
|
+
const generation = { reservation: observedReservation, retry: false, retainedAtMs: null };
|
|
121
|
+
semanticObservationGenerations.set(keyHash, generation);
|
|
122
|
+
return generation;
|
|
123
|
+
}
|
|
124
|
+
(0, latest_turn_1.mergeObservationReservations)(existing.reservation, observedReservation);
|
|
125
|
+
if (activate) {
|
|
126
|
+
existing.retainedAtMs = null;
|
|
127
|
+
(0, latest_turn_1.reactivateObservation)(existing.reservation);
|
|
128
|
+
}
|
|
129
|
+
return existing;
|
|
130
|
+
}
|
|
131
|
+
function suspendBlueBubblesSemanticObservation(generation) {
|
|
132
|
+
generation.retry = true;
|
|
133
|
+
generation.retainedAtMs = Date.now();
|
|
134
|
+
(0, latest_turn_1.reactivateObservation)(generation.reservation);
|
|
135
|
+
}
|
|
136
|
+
function completeBlueBubblesSemanticObservation(keyHash, generation) {
|
|
137
|
+
(0, latest_turn_1.clearPending)(generation.reservation);
|
|
138
|
+
semanticObservationGenerations.delete(keyHash);
|
|
139
|
+
}
|
|
140
|
+
async function acquireBlueBubblesSemanticHandlingSlot(keyHash, generation) {
|
|
141
|
+
const active = activeSemanticHandlingSlots.get(keyHash);
|
|
142
|
+
if (!active) {
|
|
143
|
+
const slot = {
|
|
144
|
+
generation,
|
|
145
|
+
reservation: generation.reservation,
|
|
146
|
+
waiters: [],
|
|
147
|
+
};
|
|
148
|
+
activeSemanticHandlingSlots.set(keyHash, slot);
|
|
149
|
+
return {
|
|
150
|
+
status: "owner",
|
|
151
|
+
slot,
|
|
152
|
+
reservation: generation.reservation,
|
|
153
|
+
allowSameGenerationRetry: generation.retry,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return await new Promise((resolve) => {
|
|
157
|
+
active.waiters.push(resolve);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function releaseBlueBubblesSemanticHandlingSlot(keyHash, slot, outcome) {
|
|
161
|
+
if (outcome === "retryable") {
|
|
162
|
+
slot.generation.retry = true;
|
|
163
|
+
const next = slot.waiters.shift();
|
|
164
|
+
if (next) {
|
|
165
|
+
(0, latest_turn_1.reactivateObservation)(slot.reservation);
|
|
166
|
+
next({
|
|
167
|
+
status: "owner",
|
|
168
|
+
slot,
|
|
169
|
+
reservation: slot.reservation,
|
|
170
|
+
allowSameGenerationRetry: true,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
activeSemanticHandlingSlots.delete(keyHash);
|
|
176
|
+
if (outcome === "terminal") {
|
|
177
|
+
completeBlueBubblesSemanticObservation(keyHash, slot.generation);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
suspendBlueBubblesSemanticObservation(slot.generation);
|
|
181
|
+
}
|
|
182
|
+
for (const resolve of slot.waiters.splice(0)) {
|
|
183
|
+
resolve({ status: "handled_by_owner" });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
94
186
|
/**
|
|
95
187
|
* In-flight message tracker.
|
|
96
188
|
*
|
|
@@ -118,20 +210,6 @@ const bbInFlightMessageClaims = new Map();
|
|
|
118
210
|
* a leaked marker from blocking forward progress for hours.
|
|
119
211
|
*/
|
|
120
212
|
exports.BB_IN_FLIGHT_MAX_AGE_MS = 15 * 60_000;
|
|
121
|
-
// Resolve the message a reaction points at, so the agent is told *what* was
|
|
122
|
-
// reacted to and *whose* message it was rather than a bare "reacted with love".
|
|
123
|
-
// Prefers the authorship-carrying lookup; falls back to text-only when the client
|
|
124
|
-
// does not provide one. Every failure path returns an explicit "unknown" rather
|
|
125
|
-
// than a value the caller could mistake for a resolved target.
|
|
126
|
-
async function resolveReactionTarget(client, targetMessageGuid) {
|
|
127
|
-
const base = { guid: targetMessageGuid };
|
|
128
|
-
if (client.getMessageDetails) {
|
|
129
|
-
const details = await client.getMessageDetails(targetMessageGuid).catch(() => null);
|
|
130
|
-
return { ...base, text: details?.text ?? null, fromMe: details?.fromMe ?? null };
|
|
131
|
-
}
|
|
132
|
-
const text = await client.getMessageText(targetMessageGuid).catch(() => null);
|
|
133
|
-
return { ...base, text, fromMe: null };
|
|
134
|
-
}
|
|
135
213
|
// ── Near-duplicate outward-text detection ────────────────────────
|
|
136
214
|
// Used by createBlueBubblesCallbacks to collapse mid-turn rephrasings of the
|
|
137
215
|
// same answer/status into a single delivery. Exposed for direct unit testing
|
|
@@ -155,44 +233,6 @@ function jaccardSimilarity(a, b) {
|
|
|
155
233
|
const union = a.size + b.size - intersection;
|
|
156
234
|
return intersection / union;
|
|
157
235
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Accumulates status descriptions and debounces them.
|
|
160
|
-
* If multiple descriptions arrive within `delayMs`, they are joined with ` · `
|
|
161
|
-
* and sent as a single message. Flush sends immediately and clears the timer.
|
|
162
|
-
*/
|
|
163
|
-
function createStatusBatcher(send, delayMs) {
|
|
164
|
-
(0, runtime_1.emitNervesEvent)({
|
|
165
|
-
component: "senses",
|
|
166
|
-
event: "senses.bluebubbles_status_batcher_created",
|
|
167
|
-
message: "status batcher initialized",
|
|
168
|
-
meta: { delayMs },
|
|
169
|
-
});
|
|
170
|
-
let pending = [];
|
|
171
|
-
let timer = null;
|
|
172
|
-
function fire() {
|
|
173
|
-
if (pending.length === 0)
|
|
174
|
-
return;
|
|
175
|
-
const combined = pending.join(" \u00b7 ");
|
|
176
|
-
pending = [];
|
|
177
|
-
timer = null;
|
|
178
|
-
send(combined);
|
|
179
|
-
}
|
|
180
|
-
return {
|
|
181
|
-
add(text) {
|
|
182
|
-
pending.push(text);
|
|
183
|
-
if (timer !== null)
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
timer = setTimeout(fire, delayMs);
|
|
186
|
-
},
|
|
187
|
-
flush() {
|
|
188
|
-
if (timer !== null) {
|
|
189
|
-
clearTimeout(timer);
|
|
190
|
-
timer = null;
|
|
191
|
-
}
|
|
192
|
-
fire();
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
236
|
function blueBubblesMessageKey(sessionKey, messageGuid) {
|
|
197
237
|
return `${sessionKey}:${messageGuid.trim()}`;
|
|
198
238
|
}
|
|
@@ -249,8 +289,9 @@ const defaultDeps = {
|
|
|
249
289
|
buildSystem: prompt_1.buildSystem,
|
|
250
290
|
runAgent: core_1.runAgent,
|
|
251
291
|
loadSession: context_1.loadSession,
|
|
292
|
+
saveSession: context_1.saveSession,
|
|
252
293
|
postTurnTrim: context_1.postTurnTrim,
|
|
253
|
-
|
|
294
|
+
postTurnPersist: context_1.postTurnPersist,
|
|
254
295
|
sessionPath: config_1.sessionPath,
|
|
255
296
|
accumulateFriendTokens: friends_1.accumulateFriendTokens,
|
|
256
297
|
createClient: () => (0, client_1.createBlueBubblesClient)(),
|
|
@@ -259,6 +300,10 @@ const defaultDeps = {
|
|
|
259
300
|
createFriendResolver: (store, params) => new friends_1.FriendResolver(store, params),
|
|
260
301
|
createServer: http.createServer,
|
|
261
302
|
getOwnHandles: () => [...(0, config_1.getBlueBubblesConfig)().ownHandles, ...discoveredOwnHandles],
|
|
303
|
+
promoteFailoverState: (sessionKey, state) => {
|
|
304
|
+
bbFailoverStates.set(sessionKey, structuredClone(state));
|
|
305
|
+
},
|
|
306
|
+
recordProcessed: processed_log_1.recordProcessedBlueBubblesMessage,
|
|
262
307
|
};
|
|
263
308
|
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
264
309
|
const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
|
|
@@ -266,7 +311,6 @@ const BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS = 30_000;
|
|
|
266
311
|
const BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS = 2 * 60_000;
|
|
267
312
|
const BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS = 10 * 60_000;
|
|
268
313
|
const BLUEBUBBLES_LIVE_TURN_STALLED_MS = 90_000;
|
|
269
|
-
const BLUEBUBBLES_SILENCE_WATCHDOG_MS = 75_000;
|
|
270
314
|
const BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS = 2_000;
|
|
271
315
|
const BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS = 20_000;
|
|
272
316
|
const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
|
|
@@ -280,32 +324,31 @@ class BlueBubblesRecoveryTurnTimeoutError extends Error {
|
|
|
280
324
|
this.name = "BlueBubblesRecoveryTurnTimeoutError";
|
|
281
325
|
}
|
|
282
326
|
}
|
|
283
|
-
function getBlueBubblesTimeoutReason(timeoutReason, timeoutMs) {
|
|
284
|
-
/* v8 ignore next 3 -- timeoutReason is set atomically with recoveryTimedOut; fallback keeps the invariant fail-closed @preserve */
|
|
285
|
-
if (!timeoutReason) {
|
|
286
|
-
return new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
|
|
287
|
-
}
|
|
288
|
-
return timeoutReason;
|
|
289
|
-
}
|
|
290
327
|
class BlueBubblesActivityTimeoutError extends Error {
|
|
291
328
|
constructor(operation, timeoutMs) {
|
|
292
329
|
super(`bluebubbles ${operation} activity timed out after ${timeoutMs}ms`);
|
|
293
330
|
this.name = "BlueBubblesActivityTimeoutError";
|
|
294
331
|
}
|
|
295
332
|
}
|
|
296
|
-
function
|
|
297
|
-
return error instanceof BlueBubblesRecoveryTurnTimeoutError
|
|
298
|
-
|| (error instanceof Error
|
|
299
|
-
&& error.name === "BlueBubblesRecoveryTurnTimeoutError"
|
|
300
|
-
&& error.message.startsWith("bluebubbles recovery turn timed out after "));
|
|
301
|
-
}
|
|
302
|
-
function withBlueBubblesActivityTimeout(operation, timeoutMs, task) {
|
|
333
|
+
function withBlueBubblesActivityTimeout(operation, timeoutMs, controller, task) {
|
|
303
334
|
let timer = null;
|
|
335
|
+
controller.signal.throwIfAborted();
|
|
336
|
+
let removeAbortListener;
|
|
337
|
+
const aborted = new Promise((_, reject) => {
|
|
338
|
+
const rejectFromAbort = () => {
|
|
339
|
+
reject(controller.signal.reason);
|
|
340
|
+
};
|
|
341
|
+
controller.signal.addEventListener("abort", rejectFromAbort, { once: true });
|
|
342
|
+
removeAbortListener = () => controller.signal.removeEventListener("abort", rejectFromAbort);
|
|
343
|
+
});
|
|
304
344
|
return Promise.race([
|
|
305
|
-
task(),
|
|
345
|
+
Promise.resolve().then(() => task(controller.signal)),
|
|
346
|
+
aborted,
|
|
306
347
|
new Promise((_, reject) => {
|
|
307
348
|
timer = setTimeout(() => {
|
|
308
|
-
|
|
349
|
+
const error = new BlueBubblesActivityTimeoutError(operation, timeoutMs);
|
|
350
|
+
controller.abort(error);
|
|
351
|
+
reject(error);
|
|
309
352
|
}, timeoutMs);
|
|
310
353
|
/* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
|
|
311
354
|
if (typeof timer.unref === "function") {
|
|
@@ -313,6 +356,7 @@ function withBlueBubblesActivityTimeout(operation, timeoutMs, task) {
|
|
|
313
356
|
}
|
|
314
357
|
}),
|
|
315
358
|
]).finally(() => {
|
|
359
|
+
removeAbortListener();
|
|
316
360
|
/* v8 ignore next -- timer is assigned unless a callback task throws synchronously; callback tasks are async @preserve */
|
|
317
361
|
if (timer !== null)
|
|
318
362
|
clearTimeout(timer);
|
|
@@ -339,7 +383,7 @@ async function waitForBlueBubblesCallbackCleanup(queue, timeoutMs, meta) {
|
|
|
339
383
|
clearTimeout(timer);
|
|
340
384
|
});
|
|
341
385
|
if (!timedOut)
|
|
342
|
-
return;
|
|
386
|
+
return true;
|
|
343
387
|
(0, runtime_1.emitNervesEvent)({
|
|
344
388
|
level: "warn",
|
|
345
389
|
component: "senses",
|
|
@@ -347,13 +391,13 @@ async function waitForBlueBubblesCallbackCleanup(queue, timeoutMs, meta) {
|
|
|
347
391
|
message: "bluebubbles callback cleanup timed out; releasing live turn lane",
|
|
348
392
|
meta: { ...meta, timeoutMs },
|
|
349
393
|
});
|
|
394
|
+
return false;
|
|
350
395
|
}
|
|
351
396
|
function resolveFriendParams(event) {
|
|
352
397
|
if (event.chat.isGroup) {
|
|
353
|
-
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
354
398
|
return {
|
|
355
399
|
provider: "imessage-handle",
|
|
356
|
-
externalId: `group:${
|
|
400
|
+
externalId: `group:${event.chat.chatGuid}`,
|
|
357
401
|
displayName: event.chat.displayName ?? "Unknown Group",
|
|
358
402
|
channel: "bluebubbles",
|
|
359
403
|
};
|
|
@@ -366,8 +410,7 @@ function resolveFriendParams(event) {
|
|
|
366
410
|
};
|
|
367
411
|
}
|
|
368
412
|
function resolveGroupExternalId(event) {
|
|
369
|
-
|
|
370
|
-
return `group:${groupKey}`;
|
|
413
|
+
return `group:${event.chat.chatGuid}`;
|
|
371
414
|
}
|
|
372
415
|
/**
|
|
373
416
|
* Check if any participant in a group chat is a known family member.
|
|
@@ -376,7 +419,7 @@ function resolveGroupExternalId(event) {
|
|
|
376
419
|
async function checkGroupHasFamilyMember(store, event) {
|
|
377
420
|
if (!event.chat.isGroup)
|
|
378
421
|
return false;
|
|
379
|
-
for (const handle of event.chat.participantHandles
|
|
422
|
+
for (const handle of event.chat.participantHandles) {
|
|
380
423
|
const friend = await store.findByExternalId("imessage-handle", handle);
|
|
381
424
|
if (friend?.trustLevel === "family")
|
|
382
425
|
return true;
|
|
@@ -477,8 +520,6 @@ function buildBlueBubblesOrientationSource(event, existingMessages, evidence, co
|
|
|
477
520
|
externalId: participant.externalId,
|
|
478
521
|
displayName: participant.displayName,
|
|
479
522
|
}));
|
|
480
|
-
const targetGuid = evidence.targetGuid;
|
|
481
|
-
const targetAuthorship = evidence.targetAuthorship;
|
|
482
523
|
const presentation = {
|
|
483
524
|
authority: "presentation_only",
|
|
484
525
|
conversationKind,
|
|
@@ -495,23 +536,7 @@ function buildBlueBubblesOrientationSource(event, existingMessages, evidence, co
|
|
|
495
536
|
displayName: actor.displayName,
|
|
496
537
|
},
|
|
497
538
|
participants,
|
|
498
|
-
...(targetGuid
|
|
499
|
-
? {
|
|
500
|
-
target: {
|
|
501
|
-
messageGuid: targetGuid,
|
|
502
|
-
authorship: targetAuthorship ?? "unknown",
|
|
503
|
-
},
|
|
504
|
-
}
|
|
505
|
-
: {}),
|
|
506
539
|
};
|
|
507
|
-
if (event.kind !== "message") {
|
|
508
|
-
return {
|
|
509
|
-
kind: "bluebubbles",
|
|
510
|
-
...presentation,
|
|
511
|
-
lane: "mutation",
|
|
512
|
-
...(repairNotice ? { repairNotice } : {}),
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
540
|
const summaries = extractHistoricalLaneSummary(existingMessages);
|
|
516
541
|
const threadId = event.threadOriginatorGuid?.trim() || undefined;
|
|
517
542
|
const hasThreadRoute = !!threadId || summaries.some((summary) => summary.key.startsWith("thread:"));
|
|
@@ -534,14 +559,11 @@ function buildInboundText(event) {
|
|
|
534
559
|
if (!event.chat.isGroup) {
|
|
535
560
|
return baseText;
|
|
536
561
|
}
|
|
537
|
-
if (event.kind === "mutation") {
|
|
538
|
-
return `${event.sender.displayName} ${baseText}`;
|
|
539
|
-
}
|
|
540
562
|
return `${event.sender.displayName}: ${baseText}`;
|
|
541
563
|
}
|
|
542
564
|
function buildInboundContent(event) {
|
|
543
565
|
const text = buildInboundText(event);
|
|
544
|
-
if (
|
|
566
|
+
if (!event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
|
|
545
567
|
return text;
|
|
546
568
|
}
|
|
547
569
|
return [
|
|
@@ -560,8 +582,6 @@ function sessionLikelyContainsMessage(event, existingMessages) {
|
|
|
560
582
|
});
|
|
561
583
|
}
|
|
562
584
|
function getBlueBubblesContinuityIngressTexts(event) {
|
|
563
|
-
if (event.kind !== "message")
|
|
564
|
-
return [];
|
|
565
585
|
const text = event.textForAgent.trim();
|
|
566
586
|
if (text.length > 0)
|
|
567
587
|
return [text];
|
|
@@ -629,10 +649,11 @@ function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
|
|
|
629
649
|
// Autonomy failure accounting must not turn a recovery error into a broader sense outage.
|
|
630
650
|
}
|
|
631
651
|
}
|
|
632
|
-
function insertEphemeralContextMessages(messages, contextMessages) {
|
|
652
|
+
function insertEphemeralContextMessages(messages, contextMessages, currentUserMessage) {
|
|
633
653
|
if (contextMessages.length === 0)
|
|
634
654
|
return messages;
|
|
635
|
-
const
|
|
655
|
+
const requiredCurrentIndex = currentUserMessage ? messages.indexOf(currentUserMessage) : -1;
|
|
656
|
+
const insertionIndex = requiredCurrentIndex >= 0 ? requiredCurrentIndex : Math.max(0, messages.length - 1);
|
|
636
657
|
return [
|
|
637
658
|
...messages.slice(0, insertionIndex),
|
|
638
659
|
...contextMessages,
|
|
@@ -646,23 +667,41 @@ function knownProviderMessageTexts(messages) {
|
|
|
646
667
|
.filter((text) => text.length > 0);
|
|
647
668
|
}
|
|
648
669
|
async function buildBlueBubblesContextMessages(input) {
|
|
649
|
-
if (input.event.kind !== "message")
|
|
650
|
-
return { messages: [], contextPacketIds: [] };
|
|
651
670
|
const event = input.event;
|
|
652
671
|
if (!Number.isFinite(event.timestamp))
|
|
653
672
|
return { messages: [], contextPacketIds: [] };
|
|
654
|
-
|
|
655
|
-
const anchorTimestamp = new Date(event.timestamp).toISOString();
|
|
673
|
+
let result;
|
|
656
674
|
try {
|
|
657
|
-
|
|
675
|
+
result = await (0, context_packet_1.buildBlueBubblesContextPacket)({
|
|
658
676
|
agentName: input.agentName,
|
|
659
677
|
client: input.client,
|
|
660
678
|
event,
|
|
661
|
-
knownMessageTexts: knownProviderMessageTexts(input.knownMessages
|
|
679
|
+
knownMessageTexts: knownProviderMessageTexts(input.knownMessages),
|
|
662
680
|
});
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
(0, runtime_1.emitNervesEvent)({
|
|
684
|
+
level: "warn",
|
|
685
|
+
component: "senses",
|
|
686
|
+
event: "senses.bluebubbles_context_packet_error",
|
|
687
|
+
message: "failed to build live bluebubbles context packet",
|
|
688
|
+
meta: {
|
|
689
|
+
messageGuid: event.messageGuid,
|
|
690
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
return { messages: [], contextPacketIds: [] };
|
|
694
|
+
}
|
|
695
|
+
if (!result)
|
|
696
|
+
return { messages: [], contextPacketIds: [] };
|
|
697
|
+
const { packet, optionalRendered, verifiedPredecessorMessage, historyCount } = result;
|
|
698
|
+
const prepared = {
|
|
699
|
+
messages: optionalRendered
|
|
700
|
+
? [{ role: "system", content: optionalRendered.text }]
|
|
701
|
+
: [],
|
|
702
|
+
verifiedPredecessorMessage,
|
|
703
|
+
};
|
|
704
|
+
try {
|
|
666
705
|
(0, context_packet_ledger_1.writeSenseContextPacket)(input.agentRoot, packet);
|
|
667
706
|
(0, runtime_1.emitNervesEvent)({
|
|
668
707
|
component: "senses",
|
|
@@ -675,7 +714,7 @@ async function buildBlueBubblesContextMessages(input) {
|
|
|
675
714
|
},
|
|
676
715
|
});
|
|
677
716
|
return {
|
|
678
|
-
|
|
717
|
+
...prepared,
|
|
679
718
|
contextPacketIds: [packet.packetId],
|
|
680
719
|
};
|
|
681
720
|
}
|
|
@@ -690,30 +729,16 @@ async function buildBlueBubblesContextMessages(input) {
|
|
|
690
729
|
reason: error instanceof Error ? error.message : String(error),
|
|
691
730
|
},
|
|
692
731
|
});
|
|
693
|
-
|
|
694
|
-
sense: "bluebubbles",
|
|
695
|
-
chatKeyHash: chatGuidHash,
|
|
696
|
-
beforeAnchorTimestamp: anchorTimestamp,
|
|
697
|
-
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
698
|
-
});
|
|
699
|
-
if (!fallback)
|
|
700
|
-
return { messages: [], contextPacketIds: [] };
|
|
701
|
-
const rendered = (0, context_packets_1.renderSenseContextPacketForPrompt)(fallback);
|
|
702
|
-
return {
|
|
703
|
-
messages: [{ role: "system", content: rendered.text }],
|
|
704
|
-
contextPacketIds: [fallback.packetId],
|
|
705
|
-
};
|
|
732
|
+
return { ...prepared, contextPacketIds: [] };
|
|
706
733
|
}
|
|
707
734
|
}
|
|
708
735
|
function createReplyTargetController(event) {
|
|
709
|
-
const defaultTargetLabel = event.
|
|
710
|
-
let selection = event.
|
|
736
|
+
const defaultTargetLabel = event.threadOriginatorGuid?.trim() ? "current_lane" : "top_level";
|
|
737
|
+
let selection = event.threadOriginatorGuid?.trim()
|
|
711
738
|
? { target: "current_lane" }
|
|
712
739
|
: { target: "top_level" };
|
|
713
740
|
return {
|
|
714
741
|
getReplyToMessageGuid() {
|
|
715
|
-
if (event.kind !== "message")
|
|
716
|
-
return undefined;
|
|
717
742
|
if (selection.target === "top_level")
|
|
718
743
|
return undefined;
|
|
719
744
|
if (selection.target === "thread")
|
|
@@ -747,10 +772,12 @@ function emitBlueBubblesMarkReadWarning(chat, error) {
|
|
|
747
772
|
function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVisibleActivity, options = {}) {
|
|
748
773
|
let textBuffer = "";
|
|
749
774
|
let typingActive = false;
|
|
775
|
+
let typingCouldBeActive = false;
|
|
750
776
|
let queue = Promise.resolve();
|
|
751
|
-
let lastVisibleActivityMs = Date.now();
|
|
752
|
-
let silenceWatchdog = null;
|
|
753
777
|
let outboundClosed = false;
|
|
778
|
+
let nextCleanupToken = 0;
|
|
779
|
+
const activeCleanupTokens = new Set();
|
|
780
|
+
const activeActivityControllers = new Set();
|
|
754
781
|
// Per-turn outward-send dedupe. A single createBlueBubblesCallbacks lifetime
|
|
755
782
|
// serves one inbound turn, so collapsing identical outward bodies inside
|
|
756
783
|
// this closure is scoped tightly: each fresh inbound turn starts with an
|
|
@@ -769,13 +796,37 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
769
796
|
// check catches near-duplicates from the same turn.
|
|
770
797
|
const sentOutwardTextNorms = new Set();
|
|
771
798
|
const sentOutwardTokenSets = [];
|
|
772
|
-
function
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
799
|
+
async function canRunOutbound() {
|
|
800
|
+
if (outboundClosed)
|
|
801
|
+
return false;
|
|
802
|
+
if (options.isOutboundCurrent && !options.isOutboundCurrent())
|
|
803
|
+
return false;
|
|
804
|
+
if (options.admitOutbound && !await options.admitOutbound())
|
|
805
|
+
return false;
|
|
806
|
+
if (outboundClosed)
|
|
807
|
+
return false;
|
|
808
|
+
if (options.isOutboundCurrent && !options.isOutboundCurrent())
|
|
809
|
+
return false;
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
function enqueue(operation, task, enqueueOptions = {}) {
|
|
813
|
+
const cleanupToken = enqueueOptions.cleanupToken;
|
|
814
|
+
const controller = new AbortController();
|
|
815
|
+
activeActivityControllers.add(controller);
|
|
776
816
|
queue = queue
|
|
777
|
-
.then(() =>
|
|
817
|
+
.then(async () => {
|
|
818
|
+
if (cleanupToken === undefined) {
|
|
819
|
+
if (!await canRunOutbound())
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
else if (!activeCleanupTokens.has(cleanupToken)) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
await withBlueBubblesActivityTimeout(operation, BLUEBUBBLES_ACTIVITY_OPERATION_TIMEOUT_MS, controller, task);
|
|
826
|
+
})
|
|
778
827
|
.catch((error) => {
|
|
828
|
+
if (controller.signal.aborted && !(error instanceof BlueBubblesActivityTimeoutError))
|
|
829
|
+
return;
|
|
779
830
|
(0, runtime_1.emitNervesEvent)({
|
|
780
831
|
level: "warn",
|
|
781
832
|
component: "senses",
|
|
@@ -783,8 +834,30 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
783
834
|
message: "bluebubbles activity transport failed",
|
|
784
835
|
meta: { operation, reason: error instanceof Error ? error.message : String(error) },
|
|
785
836
|
});
|
|
837
|
+
})
|
|
838
|
+
.finally(() => {
|
|
839
|
+
activeActivityControllers.delete(controller);
|
|
786
840
|
});
|
|
787
841
|
}
|
|
842
|
+
function abortActiveActivity(reason) {
|
|
843
|
+
for (const controller of activeActivityControllers) {
|
|
844
|
+
controller.abort(new Error(reason));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function enqueueTypingStop() {
|
|
848
|
+
typingActive = false;
|
|
849
|
+
const cleanupToken = ++nextCleanupToken;
|
|
850
|
+
activeCleanupTokens.add(cleanupToken);
|
|
851
|
+
enqueue("typing_stop", async (signal) => {
|
|
852
|
+
try {
|
|
853
|
+
await client.setTyping(chat, false, signal);
|
|
854
|
+
typingCouldBeActive = false;
|
|
855
|
+
}
|
|
856
|
+
finally {
|
|
857
|
+
activeCleanupTokens.delete(cleanupToken);
|
|
858
|
+
}
|
|
859
|
+
}, { cleanupToken });
|
|
860
|
+
}
|
|
788
861
|
function startTypingNow() {
|
|
789
862
|
/* v8 ignore next -- defensive guard: callers already check typingActive @preserve */
|
|
790
863
|
if (typingActive)
|
|
@@ -793,10 +866,11 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
793
866
|
if (outboundClosed)
|
|
794
867
|
return;
|
|
795
868
|
typingActive = true;
|
|
796
|
-
|
|
869
|
+
typingCouldBeActive = true;
|
|
870
|
+
enqueue("typing_start", async (signal) => {
|
|
797
871
|
const [markReadResult, typingResult] = await Promise.allSettled([
|
|
798
|
-
client.markChatRead(chat),
|
|
799
|
-
client.setTyping(chat, true),
|
|
872
|
+
client.markChatRead(chat, signal),
|
|
873
|
+
client.setTyping(chat, true, signal),
|
|
800
874
|
]);
|
|
801
875
|
if (markReadResult.status === "rejected") {
|
|
802
876
|
emitBlueBubblesMarkReadWarning(chat, markReadResult.reason);
|
|
@@ -807,7 +881,6 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
807
881
|
});
|
|
808
882
|
}
|
|
809
883
|
function recordVisibleActivity() {
|
|
810
|
-
lastVisibleActivityMs = Date.now();
|
|
811
884
|
onVisibleActivity?.();
|
|
812
885
|
}
|
|
813
886
|
function isDuplicateOutwardText(trimmed) {
|
|
@@ -848,62 +921,11 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
848
921
|
},
|
|
849
922
|
});
|
|
850
923
|
}
|
|
851
|
-
function stopSilenceWatchdog() {
|
|
852
|
-
if (silenceWatchdog === null)
|
|
853
|
-
return;
|
|
854
|
-
clearInterval(silenceWatchdog);
|
|
855
|
-
silenceWatchdog = null;
|
|
856
|
-
}
|
|
857
|
-
function startSilenceWatchdog() {
|
|
858
|
-
if (silenceWatchdog !== null)
|
|
859
|
-
return;
|
|
860
|
-
silenceWatchdog = setInterval(() => {
|
|
861
|
-
if (Date.now() - lastVisibleActivityMs < BLUEBUBBLES_SILENCE_WATCHDOG_MS)
|
|
862
|
-
return;
|
|
863
|
-
sendStatus("still working on this...");
|
|
864
|
-
}, BLUEBUBBLES_SILENCE_WATCHDOG_MS);
|
|
865
|
-
/* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
|
|
866
|
-
if (typeof silenceWatchdog.unref === "function") {
|
|
867
|
-
silenceWatchdog.unref();
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
function sendStatus(text) {
|
|
871
|
-
/* v8 ignore next -- defensive guard: cancelOutbound stops the only status caller before it can fire @preserve */
|
|
872
|
-
if (outboundClosed)
|
|
873
|
-
return;
|
|
874
|
-
const trimmed = text.trim();
|
|
875
|
-
/* v8 ignore next -- defensive guard; current status callers always provide non-empty text @preserve */
|
|
876
|
-
if (!trimmed)
|
|
877
|
-
return;
|
|
878
|
-
// Status surfaces share the per-turn dedupe set so a status-style
|
|
879
|
-
// outward message (tool description, error notice, watchdog ping)
|
|
880
|
-
// can't deliver a near-duplicate of an already-sent answer or status
|
|
881
|
-
// (post-#699 evidence: evt-001820 + evt-001821 + evt-001823 on
|
|
882
|
-
// 2026-05-09 — overlapping status surface updates about the same
|
|
883
|
-
// duplicate issue, which the flushNow/flush-only guard could not catch
|
|
884
|
-
// because sendStatus writes directly via client.sendText).
|
|
885
|
-
if (isDuplicateOutwardText(trimmed)) {
|
|
886
|
-
emitDuplicateOutwardSuppressed("status", trimmed.length);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
enqueue("send_status", async () => {
|
|
890
|
-
await client.sendText({
|
|
891
|
-
chat,
|
|
892
|
-
text: trimmed,
|
|
893
|
-
replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
|
|
894
|
-
});
|
|
895
|
-
recordVisibleActivity();
|
|
896
|
-
// Re-enable typing indicator — sending a message clears the typing bubble
|
|
897
|
-
await client.setTyping(chat, true);
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
924
|
return {
|
|
901
925
|
settleOutputMode: "retractable_buffer",
|
|
902
926
|
onModelStart() {
|
|
903
927
|
if (outboundClosed)
|
|
904
928
|
return;
|
|
905
|
-
if (options.enableSilenceWatchdog !== false)
|
|
906
|
-
startSilenceWatchdog();
|
|
907
929
|
if (options.enableActivitySignals !== false && !isGroupChat)
|
|
908
930
|
startTypingNow();
|
|
909
931
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -934,8 +956,8 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
934
956
|
return;
|
|
935
957
|
// Tool activity is a reply commitment, but iMessage is not a tool-progress
|
|
936
958
|
// console. Keep the human-facing thread quiet until the agent has real
|
|
937
|
-
// text
|
|
938
|
-
//
|
|
959
|
+
// text, while preserving native read/typing signals and nerves telemetry
|
|
960
|
+
// for debugging.
|
|
939
961
|
if (options.enableActivitySignals !== false && !typingActive && name !== "observe" && name !== "speak")
|
|
940
962
|
startTypingNow();
|
|
941
963
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -993,6 +1015,8 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
993
1015
|
emitDuplicateOutwardSuppressed("flushNow", trimmed.length);
|
|
994
1016
|
return;
|
|
995
1017
|
}
|
|
1018
|
+
if (!await canRunOutbound())
|
|
1019
|
+
return;
|
|
996
1020
|
await client.sendText({
|
|
997
1021
|
chat,
|
|
998
1022
|
text: trimmed,
|
|
@@ -1015,23 +1039,21 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
1015
1039
|
operation: "flush_after_close",
|
|
1016
1040
|
chatGuid: chat.chatGuid ?? null,
|
|
1017
1041
|
});
|
|
1018
|
-
return;
|
|
1042
|
+
return { status: "not_invoked", reason: "closed" };
|
|
1019
1043
|
}
|
|
1020
1044
|
await queue;
|
|
1021
1045
|
const trimmed = textBuffer.trim();
|
|
1022
1046
|
if (!trimmed) {
|
|
1023
1047
|
if (typingActive) {
|
|
1024
|
-
|
|
1025
|
-
enqueue("typing_stop", async () => { await client.setTyping(chat, false); }, { allowAfterClose: true });
|
|
1048
|
+
enqueueTypingStop();
|
|
1026
1049
|
await queue;
|
|
1027
1050
|
}
|
|
1028
|
-
return;
|
|
1051
|
+
return { status: "not_invoked", reason: "empty" };
|
|
1029
1052
|
}
|
|
1030
1053
|
textBuffer = "";
|
|
1031
1054
|
/* v8 ignore next 4 -- branch: typing may already be stopped before flush @preserve */
|
|
1032
1055
|
if (typingActive) {
|
|
1033
|
-
|
|
1034
|
-
enqueue("typing_stop", async () => { await client.setTyping(chat, false); }, { allowAfterClose: true });
|
|
1056
|
+
enqueueTypingStop();
|
|
1035
1057
|
await queue;
|
|
1036
1058
|
}
|
|
1037
1059
|
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(trimmed)) {
|
|
@@ -1043,35 +1065,181 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
1043
1065
|
messageLength: trimmed.length,
|
|
1044
1066
|
},
|
|
1045
1067
|
});
|
|
1046
|
-
return;
|
|
1068
|
+
return { status: "not_invoked", reason: "blocked_meta" };
|
|
1047
1069
|
}
|
|
1048
1070
|
if (isDuplicateOutwardText(trimmed)) {
|
|
1049
1071
|
emitDuplicateOutwardSuppressed("flush", trimmed.length);
|
|
1050
|
-
return;
|
|
1072
|
+
return { status: "not_invoked", reason: "duplicate" };
|
|
1051
1073
|
}
|
|
1052
|
-
await
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1074
|
+
if (!await canRunOutbound())
|
|
1075
|
+
return { status: "not_invoked", reason: "not_current" };
|
|
1076
|
+
const replyToMessageGuid = replyTarget.getReplyToMessageGuid();
|
|
1077
|
+
const durableOutbound = options.durableOutbound;
|
|
1078
|
+
const durableTempGuid = durableOutbound
|
|
1079
|
+
? `ouro-inbound-${(0, node_crypto_1.createHash)("sha256")
|
|
1080
|
+
.update(durableOutbound.idempotencyKey)
|
|
1081
|
+
.digest("hex")
|
|
1082
|
+
.slice(0, 32)}`
|
|
1083
|
+
: null;
|
|
1084
|
+
const boundaryState = { durableRecord: null, denialReason: "not_current" };
|
|
1085
|
+
const admitFinalTransport = () => {
|
|
1086
|
+
if (outboundClosed
|
|
1087
|
+
|| (options.isOutboundCurrent && !options.isOutboundCurrent())
|
|
1088
|
+
|| (options.isOutboundAdmittedNow && !options.isOutboundAdmittedNow())) {
|
|
1089
|
+
boundaryState.denialReason = "not_current";
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
if (!durableOutbound || boundaryState.durableRecord)
|
|
1093
|
+
return true;
|
|
1094
|
+
const reservation = (0, outbound_state_1.reserveBlueBubblesOutbound)({
|
|
1095
|
+
agentRoot: durableOutbound.agentRoot,
|
|
1096
|
+
idempotencyKey: durableOutbound.idempotencyKey,
|
|
1097
|
+
chat,
|
|
1098
|
+
attachment: durableOutbound.attachment,
|
|
1099
|
+
text: trimmed,
|
|
1100
|
+
tempGuid: durableTempGuid,
|
|
1101
|
+
...(replyToMessageGuid ? { replyToMessageGuid } : {}),
|
|
1102
|
+
});
|
|
1103
|
+
if (reservation.status !== "reserved") {
|
|
1104
|
+
boundaryState.denialReason = "durable_duplicate";
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
boundaryState.durableRecord = reservation.record;
|
|
1108
|
+
return true;
|
|
1109
|
+
};
|
|
1110
|
+
const markDurableFailure = (error) => {
|
|
1111
|
+
const durableRecord = boundaryState.durableRecord;
|
|
1112
|
+
if (!durableRecord || !durableOutbound)
|
|
1113
|
+
return;
|
|
1114
|
+
try {
|
|
1115
|
+
(0, outbound_state_1.markBlueBubblesOutboundFailed)({
|
|
1116
|
+
agentRoot: durableOutbound.agentRoot,
|
|
1117
|
+
recordId: durableRecord.recordId,
|
|
1118
|
+
failedAt: new Date().toISOString(),
|
|
1119
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
catch (stateError) {
|
|
1123
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1124
|
+
level: "error",
|
|
1125
|
+
component: "senses",
|
|
1126
|
+
event: "senses.bluebubbles_outbound_state_error",
|
|
1127
|
+
message: "failed to record bluebubbles outbound transport failure",
|
|
1128
|
+
meta: {
|
|
1129
|
+
recordId: durableRecord.recordId,
|
|
1130
|
+
reason: String(stateError),
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
let response;
|
|
1136
|
+
while (true) {
|
|
1137
|
+
try {
|
|
1138
|
+
flushOptions.signal?.throwIfAborted();
|
|
1139
|
+
response = await client.sendText({
|
|
1140
|
+
chat,
|
|
1141
|
+
text: trimmed,
|
|
1142
|
+
replyToMessageGuid,
|
|
1143
|
+
...(durableTempGuid ? { tempGuid: durableTempGuid } : {}),
|
|
1144
|
+
...(flushOptions.signal ? { signal: flushOptions.signal } : {}),
|
|
1145
|
+
beforeTransportInvocation: admitFinalTransport,
|
|
1146
|
+
onTransportInvocation: options.onFinalTransportInvoked,
|
|
1147
|
+
});
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
if (error instanceof client_1.BlueBubblesSendError
|
|
1152
|
+
&& error.transportInvoked === false
|
|
1153
|
+
&& error.errorCode === "admission_denied") {
|
|
1154
|
+
if (boundaryState.denialReason === "durable_duplicate") {
|
|
1155
|
+
return { status: "not_invoked", reason: "durable_duplicate" };
|
|
1156
|
+
}
|
|
1157
|
+
// A newer observation may be only a temporary fence (duplicate,
|
|
1158
|
+
// audit-only, or otherwise non-promoting). Re-await admission and
|
|
1159
|
+
// retry the same durable reservation if this turn remains current.
|
|
1160
|
+
if (!await canRunOutbound()) {
|
|
1161
|
+
markDurableFailure(error);
|
|
1162
|
+
return { status: "not_invoked", reason: "not_current" };
|
|
1163
|
+
}
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
markDurableFailure(error);
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const durableRecord = boundaryState.durableRecord;
|
|
1171
|
+
if (durableRecord && durableOutbound) {
|
|
1172
|
+
try {
|
|
1173
|
+
(0, outbound_state_1.markBlueBubblesOutboundAccepted)({
|
|
1174
|
+
agentRoot: durableOutbound.agentRoot,
|
|
1175
|
+
recordId: durableRecord.recordId,
|
|
1176
|
+
acceptedAt: new Date().toISOString(),
|
|
1177
|
+
...(response.messageGuid ? { messageGuid: response.messageGuid } : {}),
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
catch (stateError) {
|
|
1181
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1182
|
+
level: "error",
|
|
1183
|
+
component: "senses",
|
|
1184
|
+
event: "senses.bluebubbles_outbound_state_error",
|
|
1185
|
+
message: "failed to record accepted bluebubbles outbound transport",
|
|
1186
|
+
meta: {
|
|
1187
|
+
recordId: durableRecord.recordId,
|
|
1188
|
+
reason: String(stateError),
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
options.onFinalTransportAccepted?.();
|
|
1058
1194
|
recordVisibleActivity();
|
|
1195
|
+
return { status: "accepted" };
|
|
1059
1196
|
},
|
|
1060
1197
|
async finish(options = {}) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1198
|
+
const cleanupTimeoutMs = options.timeoutMs ?? BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS;
|
|
1199
|
+
const needsTypingStop = typingActive;
|
|
1200
|
+
if (needsTypingStop) {
|
|
1201
|
+
enqueueTypingStop();
|
|
1065
1202
|
}
|
|
1066
|
-
await waitForBlueBubblesCallbackCleanup(queue,
|
|
1203
|
+
const drained = await waitForBlueBubblesCallbackCleanup(queue, cleanupTimeoutMs, {
|
|
1067
1204
|
operation: "finish",
|
|
1068
1205
|
chatGuid: chat.chatGuid ?? null,
|
|
1069
1206
|
});
|
|
1207
|
+
if (!drained) {
|
|
1208
|
+
// The queued start/read request may outlive this turn's bounded lane.
|
|
1209
|
+
// Invalidate its deferred stop, then issue one stop request now while A
|
|
1210
|
+
// still owns the canonical lock so B cannot start typing ahead of it.
|
|
1211
|
+
activeCleanupTokens.clear();
|
|
1212
|
+
abortActiveActivity("bluebubbles activity superseded by bounded cleanup");
|
|
1213
|
+
await waitForBlueBubblesCallbackCleanup(queue, cleanupTimeoutMs, {
|
|
1214
|
+
operation: "finish_after_activity_abort",
|
|
1215
|
+
chatGuid: chat.chatGuid ?? null,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (typingCouldBeActive) {
|
|
1219
|
+
const fallbackController = new AbortController();
|
|
1220
|
+
try {
|
|
1221
|
+
await withBlueBubblesActivityTimeout("typing_stop_fallback", cleanupTimeoutMs, fallbackController, (signal) => client.setTyping(chat, false, signal));
|
|
1222
|
+
typingCouldBeActive = false;
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1226
|
+
level: "warn",
|
|
1227
|
+
component: "senses",
|
|
1228
|
+
event: "senses.bluebubbles_activity_error",
|
|
1229
|
+
message: "bluebubbles activity transport failed",
|
|
1230
|
+
meta: {
|
|
1231
|
+
operation: "typing_stop_fallback",
|
|
1232
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1233
|
+
},
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
activeCleanupTokens.clear();
|
|
1070
1238
|
},
|
|
1071
1239
|
cancelOutbound(reason) {
|
|
1072
1240
|
outboundClosed = true;
|
|
1073
1241
|
textBuffer = "";
|
|
1074
|
-
|
|
1242
|
+
abortActiveActivity(`bluebubbles outbound ${reason}`);
|
|
1075
1243
|
(0, runtime_1.emitNervesEvent)({
|
|
1076
1244
|
level: "warn",
|
|
1077
1245
|
component: "senses",
|
|
@@ -1204,62 +1372,98 @@ async function shouldFilterAgentSelfHandle(event, resolvedDeps) {
|
|
|
1204
1372
|
}
|
|
1205
1373
|
return true;
|
|
1206
1374
|
}
|
|
1207
|
-
function
|
|
1208
|
-
return
|
|
1375
|
+
function createBlueBubblesTurnStage(sessionKey) {
|
|
1376
|
+
return {
|
|
1377
|
+
failoverState: structuredClone(bbFailoverStates.get(sessionKey) ?? { pending: null }),
|
|
1378
|
+
};
|
|
1209
1379
|
}
|
|
1210
|
-
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1380
|
+
function persistAcceptedBlueBubblesProjection(input) {
|
|
1381
|
+
const { stage, deps, event } = input;
|
|
1382
|
+
if (stage.postTurn) {
|
|
1383
|
+
try {
|
|
1384
|
+
deps.postTurnPersist(stage.postTurn.sessionPath, stage.postTurn.prepared, stage.postTurn.usage, stage.postTurn.state);
|
|
1385
|
+
}
|
|
1386
|
+
catch (error) {
|
|
1387
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1388
|
+
level: "error",
|
|
1389
|
+
component: "senses",
|
|
1390
|
+
event: "senses.bluebubbles_accepted_projection_failed",
|
|
1391
|
+
message: "accepted bluebubbles reply could not be projected into canonical session history",
|
|
1392
|
+
meta: {
|
|
1393
|
+
messageGuid: event.messageGuid,
|
|
1394
|
+
sessionKey: event.chat.sessionKey,
|
|
1395
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1396
|
+
},
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function promoteAcceptedBlueBubblesAncillaryState(input) {
|
|
1402
|
+
const { stage, deps, agentName, event, source } = input;
|
|
1403
|
+
try {
|
|
1404
|
+
deps.promoteFailoverState(event.chat.sessionKey, stage.failoverState);
|
|
1405
|
+
}
|
|
1406
|
+
catch (error) {
|
|
1236
1407
|
(0, runtime_1.emitNervesEvent)({
|
|
1237
1408
|
level: "warn",
|
|
1238
1409
|
component: "senses",
|
|
1239
|
-
event: "senses.
|
|
1240
|
-
message: "
|
|
1410
|
+
event: "senses.bluebubbles_failover_state_promotion_failed",
|
|
1411
|
+
message: "accepted bluebubbles reply could not promote staged failover state",
|
|
1241
1412
|
meta: {
|
|
1242
1413
|
messageGuid: event.messageGuid,
|
|
1243
|
-
|
|
1244
|
-
|
|
1414
|
+
sessionKey: event.chat.sessionKey,
|
|
1415
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1245
1416
|
},
|
|
1246
1417
|
});
|
|
1247
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
|
|
1248
1418
|
}
|
|
1249
|
-
if (
|
|
1419
|
+
if (stage.tokens) {
|
|
1420
|
+
const emitTokenPromotionFailure = (error) => {
|
|
1421
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1422
|
+
level: "warn",
|
|
1423
|
+
component: "senses",
|
|
1424
|
+
event: "senses.bluebubbles_token_promotion_failed",
|
|
1425
|
+
message: "accepted bluebubbles reply could not promote staged token usage",
|
|
1426
|
+
meta: {
|
|
1427
|
+
messageGuid: event.messageGuid,
|
|
1428
|
+
sessionKey: event.chat.sessionKey,
|
|
1429
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1430
|
+
},
|
|
1431
|
+
});
|
|
1432
|
+
};
|
|
1433
|
+
try {
|
|
1434
|
+
void deps.accumulateFriendTokens(stage.tokens.store, stage.tokens.friendId, stage.tokens.usage).catch(emitTokenPromotionFailure);
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
emitTokenPromotionFailure(error);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
deps.recordProcessed(agentName, event, source, input.processedOutcome);
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1250
1444
|
(0, runtime_1.emitNervesEvent)({
|
|
1445
|
+
level: "warn",
|
|
1251
1446
|
component: "senses",
|
|
1252
|
-
event: "senses.
|
|
1253
|
-
message: "
|
|
1447
|
+
event: "senses.bluebubbles_processed_promotion_failed",
|
|
1448
|
+
message: "accepted bluebubbles reply could not promote processed sidecar state",
|
|
1254
1449
|
meta: {
|
|
1255
1450
|
messageGuid: event.messageGuid,
|
|
1256
|
-
|
|
1451
|
+
sessionKey: event.chat.sessionKey,
|
|
1452
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1257
1453
|
},
|
|
1258
1454
|
});
|
|
1259
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
|
|
1260
1455
|
}
|
|
1456
|
+
}
|
|
1457
|
+
function hasObservedBlueBubblesDirection(event) {
|
|
1458
|
+
return typeof event.fromMe === "boolean";
|
|
1459
|
+
}
|
|
1460
|
+
async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, options) {
|
|
1461
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1462
|
+
const client = resolvedDeps.createClient();
|
|
1463
|
+
const agentName = resolvedDeps.getAgentName();
|
|
1464
|
+
(0, runtime_cwd_1.recoverRuntimeCwd)();
|
|
1261
1465
|
let activeTurnId = null;
|
|
1262
|
-
if (
|
|
1466
|
+
if (shouldUseBlueBubblesRecoveryBudget(source, options)) {
|
|
1263
1467
|
const decision = (0, autonomy_budget_1.reserveAutonomyBudget)((0, identity_1.getAgentRoot)(agentName), {
|
|
1264
1468
|
agent: agentName,
|
|
1265
1469
|
triggerType: "recovery",
|
|
@@ -1275,66 +1479,100 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1275
1479
|
}
|
|
1276
1480
|
}
|
|
1277
1481
|
try {
|
|
1278
|
-
//
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
}
|
|
1301
|
-
return await (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", sessPath, async () => {
|
|
1302
|
-
if (event.kind === "message" && activeTurnId === null) {
|
|
1303
|
-
activeTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
|
|
1482
|
+
// Reserve the canonical chat lane before any asynchronous adapter setup.
|
|
1483
|
+
// Every admitted inbound is projected in observation order even when a
|
|
1484
|
+
// newer turn supersedes its inference before provider work begins.
|
|
1485
|
+
return await (0, turn_coordinator_1.withSharedTurnLock)("bluebubbles", `${agentName}:${event.chat.sessionKey}`, async () => {
|
|
1486
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1487
|
+
// Fallback self-detection: BlueBubbles sometimes broadcasts a group-chat
|
|
1488
|
+
// outbound message back through the webhook with `isFromMe` missing/false.
|
|
1489
|
+
// Keep this group-only: stale ownHandles entries must not hide real DMs.
|
|
1490
|
+
if (await shouldFilterAgentSelfHandle(event, resolvedDeps)) {
|
|
1491
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1492
|
+
level: "warn",
|
|
1493
|
+
component: "senses",
|
|
1494
|
+
event: "senses.bluebubbles_self_handle_filtered",
|
|
1495
|
+
message: "filtered bluebubbles event whose sender matched an agent-owned handle (isFromMe was missing/false)",
|
|
1496
|
+
meta: {
|
|
1497
|
+
messageGuid: event.messageGuid,
|
|
1498
|
+
kind: event.kind,
|
|
1499
|
+
senderExternalId: event.sender.externalId,
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
|
|
1304
1503
|
}
|
|
1305
|
-
|
|
1504
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1505
|
+
// ── Adapter setup: friend, session, content, callbacks ────────
|
|
1506
|
+
const store = resolvedDeps.createFriendStore();
|
|
1507
|
+
const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
|
|
1508
|
+
const baseContext = await resolver.resolve();
|
|
1509
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1510
|
+
const context = { ...baseContext, isGroupChat: event.chat.isGroup };
|
|
1511
|
+
const replyTarget = createReplyTargetController(event);
|
|
1512
|
+
const friendId = context.friend.id;
|
|
1513
|
+
const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
|
|
1514
|
+
try {
|
|
1515
|
+
(0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1519
|
+
level: "warn",
|
|
1520
|
+
component: "senses",
|
|
1521
|
+
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
1522
|
+
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
1523
|
+
meta: {
|
|
1524
|
+
sessionPath: sessPath,
|
|
1525
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1526
|
+
},
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
// Pre-load session inside the canonical turn lock so same-chat
|
|
1530
|
+
// deliveries cannot race on stale trunk state.
|
|
1306
1531
|
const existing = resolvedDeps.loadSession(sessPath);
|
|
1307
|
-
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
1308
1532
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
1309
|
-
? existing.messages
|
|
1533
|
+
? structuredClone(existing.messages)
|
|
1310
1534
|
: [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
1332
|
-
}
|
|
1535
|
+
// Record EARLY for audit and crash recovery. This is capture truth, not
|
|
1536
|
+
// a claim that the agent turn completed successfully.
|
|
1537
|
+
const inboundSource = (options.semanticRecovery || source !== "webhook")
|
|
1538
|
+
&& sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
|
|
1539
|
+
? "recovery-bootstrap"
|
|
1540
|
+
: source;
|
|
1541
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
|
|
1542
|
+
if (inboundSource === "recovery-bootstrap") {
|
|
1543
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1544
|
+
component: "senses",
|
|
1545
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
1546
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
1547
|
+
meta: {
|
|
1548
|
+
messageGuid: event.messageGuid,
|
|
1549
|
+
sessionKey: event.chat.sessionKey,
|
|
1550
|
+
source,
|
|
1551
|
+
},
|
|
1552
|
+
});
|
|
1553
|
+
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
|
|
1554
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
1333
1555
|
}
|
|
1334
|
-
|
|
1556
|
+
// The inbound event is canonical truth as soon as its canonical lane is
|
|
1557
|
+
// reserved. Persist it before any supersedable orientation/provider work.
|
|
1558
|
+
const priorMessages = sessionMessages;
|
|
1559
|
+
const userMessage = {
|
|
1560
|
+
role: "user",
|
|
1561
|
+
content: buildInboundContent(event),
|
|
1562
|
+
};
|
|
1563
|
+
resolvedDeps.saveSession(sessPath, [...structuredClone(sessionMessages), structuredClone(userMessage)], existing?.lastUsage, existing?.state);
|
|
1564
|
+
if (!await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
|
|
1565
|
+
|| !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
|
|
1566
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
|
|
1567
|
+
}
|
|
1568
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1569
|
+
const liveTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
|
|
1570
|
+
activeTurnId = liveTurnId;
|
|
1571
|
+
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
1572
|
+
if (event.chat.isGroup) {
|
|
1335
1573
|
await (0, friends_1.upsertGroupContextParticipants)({
|
|
1336
1574
|
store,
|
|
1337
|
-
participants:
|
|
1575
|
+
participants: event.chat.participantHandles.map((externalId) => ({
|
|
1338
1576
|
provider: "imessage-handle",
|
|
1339
1577
|
externalId,
|
|
1340
1578
|
})),
|
|
@@ -1342,10 +1580,10 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1342
1580
|
});
|
|
1343
1581
|
}
|
|
1344
1582
|
// Fetch the text of the message being replied to (if this is a threaded reply)
|
|
1345
|
-
const threadGuid = event.
|
|
1583
|
+
const threadGuid = event.threadOriginatorGuid?.trim();
|
|
1346
1584
|
let repliedToText = null;
|
|
1347
1585
|
if (threadGuid) {
|
|
1348
|
-
repliedToText = await client.getMessageText(threadGuid).catch(
|
|
1586
|
+
repliedToText = await client.getMessageText(threadGuid).catch(() => null);
|
|
1349
1587
|
(0, runtime_1.emitNervesEvent)({
|
|
1350
1588
|
component: "senses",
|
|
1351
1589
|
event: "senses.bluebubbles_reply_context",
|
|
@@ -1353,12 +1591,9 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1353
1591
|
meta: { threadGuid, hasText: !!repliedToText },
|
|
1354
1592
|
});
|
|
1355
1593
|
}
|
|
1356
|
-
//
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
role: "user",
|
|
1360
|
-
content: buildInboundContent(event),
|
|
1361
|
-
};
|
|
1594
|
+
// Shutdown can land during MCP/group/reply-context awaits above. Recheck
|
|
1595
|
+
// before the remaining synchronous setup installs its lifecycle listener.
|
|
1596
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
1362
1597
|
const orientationFrame = (0, orientation_frame_1.buildOrientationFrame)({
|
|
1363
1598
|
channel: "bluebubbles",
|
|
1364
1599
|
messages: [...priorMessages, userMessage],
|
|
@@ -1366,26 +1601,76 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1366
1601
|
structuredOutputs: existing?.structuredOutputs ?? [],
|
|
1367
1602
|
source: buildBlueBubblesOrientationSource(event, priorMessages, options.orientationEvidence, options.orientationConversationKind, repliedToText),
|
|
1368
1603
|
});
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1604
|
+
if (!(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
|
|
1605
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
|
|
1606
|
+
}
|
|
1607
|
+
const turnStage = createBlueBubblesTurnStage(event.chat.sessionKey);
|
|
1608
|
+
let finalTransportInvoked = false;
|
|
1609
|
+
let finalTransportAccepted = false;
|
|
1610
|
+
const outboundRuntimeConfig = (0, config_1.getBlueBubblesConfig)();
|
|
1611
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () => (0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, liveTurnId), {
|
|
1612
|
+
admitOutbound: async () => (!options.lifecycleSignal?.aborted
|
|
1613
|
+
&& await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
|
|
1614
|
+
&& !options.lifecycleSignal?.aborted),
|
|
1615
|
+
isOutboundCurrent: () => (!options.lifecycleSignal?.aborted
|
|
1616
|
+
&& (0, latest_turn_1.isCurrent)(options.latestTurnCapability)),
|
|
1617
|
+
isOutboundAdmittedNow: () => (!options.lifecycleSignal?.aborted
|
|
1618
|
+
&& (0, latest_turn_1.isDeliveryAdmittedNow)(options.latestTurnCapability)),
|
|
1619
|
+
onFinalTransportInvoked: () => {
|
|
1620
|
+
finalTransportInvoked = true;
|
|
1621
|
+
},
|
|
1622
|
+
onFinalTransportAccepted: () => {
|
|
1623
|
+
finalTransportAccepted = true;
|
|
1624
|
+
},
|
|
1625
|
+
durableOutbound: {
|
|
1626
|
+
agentRoot: (0, identity_1.getAgentRoot)(),
|
|
1627
|
+
idempotencyKey: blueBubblesInboundReplyIdempotencyKey(options.currentIngressEvidence.captureKeyHash),
|
|
1628
|
+
attachment: {
|
|
1629
|
+
serverUrl: outboundRuntimeConfig.serverUrl,
|
|
1630
|
+
accountId: outboundRuntimeConfig.accountId,
|
|
1631
|
+
},
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1373
1634
|
const controller = new AbortController();
|
|
1635
|
+
const lifecycleSignal = options.lifecycleSignal;
|
|
1636
|
+
let rejectLifecycle;
|
|
1637
|
+
const lifecyclePromise = new Promise((_resolve, reject) => {
|
|
1638
|
+
rejectLifecycle = reject;
|
|
1639
|
+
});
|
|
1640
|
+
let resolveSupersession;
|
|
1641
|
+
const supersessionPromise = new Promise((resolve) => {
|
|
1642
|
+
resolveSupersession = resolve;
|
|
1643
|
+
});
|
|
1644
|
+
const cancelForSupersession = () => {
|
|
1645
|
+
callbacks.cancelOutbound("superseded");
|
|
1646
|
+
if (!finalTransportInvoked) {
|
|
1647
|
+
controller.abort(options.latestTurnCapability.signal.reason);
|
|
1648
|
+
resolveSupersession({
|
|
1649
|
+
handled: true,
|
|
1650
|
+
notifiedAgent: false,
|
|
1651
|
+
kind: event.kind,
|
|
1652
|
+
reason: "superseded",
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
options.latestTurnCapability.signal.addEventListener("abort", cancelForSupersession, { once: true });
|
|
1657
|
+
const cancelForShutdown = () => {
|
|
1658
|
+
callbacks.cancelOutbound("shutdown");
|
|
1659
|
+
if (!finalTransportInvoked) {
|
|
1660
|
+
const reason = lifecycleSignal.reason;
|
|
1661
|
+
controller.abort(reason);
|
|
1662
|
+
rejectLifecycle(reason);
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
lifecycleSignal?.addEventListener("abort", cancelForShutdown, { once: true });
|
|
1374
1666
|
let timeoutTimer;
|
|
1375
1667
|
let timeoutPromise;
|
|
1376
|
-
let
|
|
1668
|
+
let resolveTimeout;
|
|
1377
1669
|
let recoveryTimedOut = false;
|
|
1378
|
-
let timeoutReason = null;
|
|
1379
1670
|
// BB-specific tool context wrappers
|
|
1380
1671
|
const summarize = (0, core_1.createSummarize)("human");
|
|
1381
1672
|
const bbCapabilities = (0, friends_1.getChannelCapabilities)("bluebubbles");
|
|
1382
1673
|
const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
|
|
1383
|
-
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
1384
|
-
const groupHasFamilyMember = await checkGroupHasFamilyMember(store, event);
|
|
1385
|
-
const hasExistingGroupWithFamily = event.chat.isGroup
|
|
1386
|
-
? false
|
|
1387
|
-
: await checkHasExistingGroupWithFamily(store, context.friend);
|
|
1388
|
-
// ── Call shared pipeline ──────────────────────────────────────────
|
|
1389
1674
|
// Buffer terminal errors so failover can suppress them.
|
|
1390
1675
|
// If failover produces a message, the buffered error is skipped.
|
|
1391
1676
|
// If failover doesn't fire, the buffered error is replayed.
|
|
@@ -1403,17 +1688,50 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1403
1688
|
};
|
|
1404
1689
|
/* v8 ignore stop */
|
|
1405
1690
|
try {
|
|
1691
|
+
// ── Compute trust gate context for group/acquaintance rules ─────
|
|
1692
|
+
// These store reads can block on filesystem I/O. Race them against the
|
|
1693
|
+
// runtime lifecycle before constructing the shared pipeline so a closed
|
|
1694
|
+
// worker cannot begin new durable or outward work after the read settles.
|
|
1695
|
+
const trustContext = await Promise.race([
|
|
1696
|
+
Promise.all([
|
|
1697
|
+
checkGroupHasFamilyMember(store, event),
|
|
1698
|
+
event.chat.isGroup
|
|
1699
|
+
? Promise.resolve(false)
|
|
1700
|
+
: checkHasExistingGroupWithFamily(store, context.friend),
|
|
1701
|
+
]),
|
|
1702
|
+
lifecyclePromise,
|
|
1703
|
+
supersessionPromise.then(() => null),
|
|
1704
|
+
]);
|
|
1705
|
+
if (trustContext === null) {
|
|
1706
|
+
return {
|
|
1707
|
+
handled: true,
|
|
1708
|
+
notifiedAgent: false,
|
|
1709
|
+
kind: event.kind,
|
|
1710
|
+
reason: "superseded",
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const [groupHasFamilyMember, hasExistingGroupWithFamily] = trustContext;
|
|
1714
|
+
lifecycleSignal?.throwIfAborted();
|
|
1715
|
+
// ── Call shared pipeline ────────────────────────────────────────
|
|
1406
1716
|
const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
|
|
1407
|
-
timeoutPromise = new Promise((
|
|
1408
|
-
|
|
1717
|
+
timeoutPromise = new Promise((resolve) => {
|
|
1718
|
+
resolveTimeout = resolve;
|
|
1409
1719
|
});
|
|
1410
1720
|
timeoutTimer = setTimeout(() => {
|
|
1411
1721
|
const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
|
|
1412
|
-
timeoutReason = reason;
|
|
1413
1722
|
recoveryTimedOut = true;
|
|
1723
|
+
if (!finalTransportInvoked) {
|
|
1724
|
+
resolveTimeout?.({
|
|
1725
|
+
handled: true,
|
|
1726
|
+
notifiedAgent: false,
|
|
1727
|
+
kind: event.kind,
|
|
1728
|
+
reason: "turn_timeout",
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1414
1731
|
callbacks.cancelOutbound("turn_timeout");
|
|
1415
|
-
|
|
1416
|
-
|
|
1732
|
+
(0, latest_turn_1.cancel)(options.latestTurnCapability, "turn_timeout");
|
|
1733
|
+
if (!finalTransportInvoked)
|
|
1734
|
+
controller.abort(reason);
|
|
1417
1735
|
(0, runtime_1.emitNervesEvent)({
|
|
1418
1736
|
level: "warn",
|
|
1419
1737
|
component: "senses",
|
|
@@ -1426,6 +1744,18 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1426
1744
|
timeoutMs,
|
|
1427
1745
|
},
|
|
1428
1746
|
});
|
|
1747
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1748
|
+
level: "warn",
|
|
1749
|
+
component: "senses",
|
|
1750
|
+
event: "senses.bluebubbles_timeout_notice_suppressed",
|
|
1751
|
+
message: "bluebubbles timeout notice suppressed from iMessage",
|
|
1752
|
+
meta: {
|
|
1753
|
+
messageGuid: event.messageGuid,
|
|
1754
|
+
sessionKey: event.chat.sessionKey,
|
|
1755
|
+
source,
|
|
1756
|
+
timeoutMs,
|
|
1757
|
+
},
|
|
1758
|
+
});
|
|
1429
1759
|
}, timeoutMs);
|
|
1430
1760
|
/* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
|
|
1431
1761
|
if (typeof timeoutTimer.unref === "function") {
|
|
@@ -1461,7 +1791,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1461
1791
|
enforceTrustGate: trust_gate_1.enforceTrustGate,
|
|
1462
1792
|
drainPending: pending_1.drainPending,
|
|
1463
1793
|
drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
|
|
1464
|
-
prepareRunAgentOptions: async ({ messages, runAgentOptions }) => {
|
|
1794
|
+
prepareRunAgentOptions: async ({ messages = [], currentUserMessages = [], runAgentOptions }) => {
|
|
1465
1795
|
preparedBlueBubblesContext = await buildBlueBubblesContextMessages({
|
|
1466
1796
|
agentName,
|
|
1467
1797
|
agentRoot: (0, identity_1.getAgentRoot)(),
|
|
@@ -1469,41 +1799,75 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1469
1799
|
event,
|
|
1470
1800
|
knownMessages: messages,
|
|
1471
1801
|
});
|
|
1472
|
-
|
|
1802
|
+
const currentUserMessage = currentUserMessages.findLast((message) => message.role === "user")
|
|
1803
|
+
?? messages.findLast((message) => message.role === "user");
|
|
1804
|
+
if (!currentUserMessage)
|
|
1473
1805
|
return undefined;
|
|
1806
|
+
Object.freeze(currentUserMessage);
|
|
1807
|
+
const { verifiedPredecessorMessage } = preparedBlueBubblesContext;
|
|
1474
1808
|
return {
|
|
1475
|
-
contextPacketIds
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1809
|
+
...(preparedBlueBubblesContext.contextPacketIds.length > 0 ? {
|
|
1810
|
+
contextPacketIds: [
|
|
1811
|
+
...(runAgentOptions.contextPacketIds ?? []),
|
|
1812
|
+
...preparedBlueBubblesContext.contextPacketIds,
|
|
1813
|
+
],
|
|
1814
|
+
} : {}),
|
|
1815
|
+
...(preparedBlueBubblesContext.messages.length > 0 ? {
|
|
1816
|
+
promptOnlyEvidenceMessages: preparedBlueBubblesContext.messages,
|
|
1817
|
+
} : {}),
|
|
1818
|
+
requiredPromptEvidence: {
|
|
1819
|
+
currentUserMessage,
|
|
1820
|
+
...(verifiedPredecessorMessage ? { verifiedPredecessorMessage } : {}),
|
|
1821
|
+
},
|
|
1479
1822
|
};
|
|
1480
1823
|
},
|
|
1481
|
-
runAgent: async (msgs, cb, channel, sig, opts) =>
|
|
1482
|
-
...opts
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
...
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1824
|
+
runAgent: async (msgs, cb, channel, sig, opts) => {
|
|
1825
|
+
const { codingFeedback: _omittedCodingFeedback, ...safeToolContext } = opts?.toolContext ?? {};
|
|
1826
|
+
const requiredCurrent = opts?.requiredPromptEvidence?.currentUserMessage;
|
|
1827
|
+
const providerMessages = insertEphemeralContextMessages(msgs.map((message) => message === requiredCurrent ? message : structuredClone(message)), [
|
|
1828
|
+
...preparedBlueBubblesContext.messages,
|
|
1829
|
+
...(preparedBlueBubblesContext.verifiedPredecessorMessage
|
|
1830
|
+
? [preparedBlueBubblesContext.verifiedPredecessorMessage]
|
|
1831
|
+
: []),
|
|
1832
|
+
], requiredCurrent);
|
|
1833
|
+
let generatedMessages = [];
|
|
1834
|
+
const agentResult = await resolvedDeps.runAgent(providerMessages, cb, channel, sig, {
|
|
1835
|
+
...opts,
|
|
1836
|
+
captureGeneratedMessages: (generated) => {
|
|
1837
|
+
opts?.captureGeneratedMessages?.(generated);
|
|
1838
|
+
generatedMessages = structuredClone(generated);
|
|
1490
1839
|
},
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1840
|
+
toolContext: {
|
|
1841
|
+
/* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
|
|
1842
|
+
signin: async () => undefined,
|
|
1843
|
+
...safeToolContext,
|
|
1844
|
+
summarize,
|
|
1845
|
+
bluebubblesReplyTarget: {
|
|
1846
|
+
setSelection: (selection) => replyTarget.setSelection(selection),
|
|
1498
1847
|
},
|
|
1499
1848
|
},
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1849
|
+
});
|
|
1850
|
+
if (generatedMessages.length > 0) {
|
|
1851
|
+
msgs.push(...generatedMessages);
|
|
1852
|
+
}
|
|
1853
|
+
return agentResult;
|
|
1854
|
+
},
|
|
1502
1855
|
postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
|
|
1503
1856
|
const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
|
|
1504
|
-
|
|
1857
|
+
turnStage.postTurn = {
|
|
1858
|
+
sessionPath: sessionPathArg,
|
|
1859
|
+
prepared,
|
|
1860
|
+
usage,
|
|
1861
|
+
state,
|
|
1862
|
+
};
|
|
1863
|
+
},
|
|
1864
|
+
accumulateFriendTokens: async (tokenStore, tokenFriendId, usage) => {
|
|
1865
|
+
turnStage.tokens = {
|
|
1866
|
+
store: tokenStore,
|
|
1867
|
+
friendId: tokenFriendId,
|
|
1868
|
+
usage,
|
|
1869
|
+
};
|
|
1505
1870
|
},
|
|
1506
|
-
accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
|
|
1507
1871
|
signal: controller.signal,
|
|
1508
1872
|
runAgentOptions: {
|
|
1509
1873
|
mcpManager,
|
|
@@ -1515,12 +1879,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1515
1879
|
},
|
|
1516
1880
|
},
|
|
1517
1881
|
callbacks: failoverAwareCallbacks,
|
|
1518
|
-
failoverState:
|
|
1519
|
-
if (!bbFailoverStates.has(event.chat.sessionKey)) {
|
|
1520
|
-
bbFailoverStates.set(event.chat.sessionKey, { pending: null });
|
|
1521
|
-
}
|
|
1522
|
-
return bbFailoverStates.get(event.chat.sessionKey);
|
|
1523
|
-
})(),
|
|
1882
|
+
failoverState: turnStage.failoverState,
|
|
1524
1883
|
});
|
|
1525
1884
|
/* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
|
|
1526
1885
|
void turnPromise
|
|
@@ -1543,43 +1902,108 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1543
1902
|
/* v8 ignore stop */
|
|
1544
1903
|
const runTurn = (async () => {
|
|
1545
1904
|
const result = await turnPromise;
|
|
1546
|
-
if (recoveryTimedOut)
|
|
1547
|
-
|
|
1905
|
+
if (recoveryTimedOut && !finalTransportInvoked) {
|
|
1906
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1907
|
+
level: "warn",
|
|
1908
|
+
component: "senses",
|
|
1909
|
+
event: "senses.bluebubbles_timed_out_turn_suppressed",
|
|
1910
|
+
message: "suppressed late bluebubbles turn result after timeout",
|
|
1911
|
+
meta: {
|
|
1912
|
+
messageGuid: event.messageGuid,
|
|
1913
|
+
sessionKey: event.chat.sessionKey,
|
|
1914
|
+
source,
|
|
1915
|
+
reason: "turn completed after its final-transport boundary closed",
|
|
1916
|
+
},
|
|
1917
|
+
});
|
|
1918
|
+
return {
|
|
1919
|
+
handled: true,
|
|
1920
|
+
notifiedAgent: false,
|
|
1921
|
+
kind: event.kind,
|
|
1922
|
+
reason: "turn_timeout",
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1548
1925
|
/* v8 ignore start -- failover display + error replay @preserve */
|
|
1549
1926
|
if (result.failoverMessage) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1927
|
+
callbacks.onClearText?.();
|
|
1928
|
+
callbacks.onTextChunk(result.failoverMessage);
|
|
1929
|
+
bufferedTerminalError = null;
|
|
1552
1930
|
}
|
|
1553
1931
|
else if (bufferedTerminalError) {
|
|
1554
|
-
// No failover — replay the buffered terminal error
|
|
1555
1932
|
callbacks.onError(bufferedTerminalError, "terminal");
|
|
1933
|
+
bufferedTerminalError = null;
|
|
1556
1934
|
}
|
|
1557
1935
|
/* v8 ignore stop */
|
|
1558
1936
|
// ── Handle gate result ────────────────────────────────────────
|
|
1937
|
+
let processedOutcome = "turn-complete";
|
|
1559
1938
|
if (!result.gateResult.allowed) {
|
|
1560
|
-
|
|
1939
|
+
callbacks.onClearText?.();
|
|
1561
1940
|
if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
|
|
1562
|
-
|
|
1563
|
-
chat: event.chat,
|
|
1564
|
-
text: result.gateResult.autoReply,
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
if (event.kind === "message") {
|
|
1568
|
-
(0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, source, "trust-gated");
|
|
1941
|
+
callbacks.onTextChunk(result.gateResult.autoReply);
|
|
1569
1942
|
}
|
|
1943
|
+
processedOutcome = "trust-gated";
|
|
1944
|
+
}
|
|
1945
|
+
let delivery;
|
|
1946
|
+
try {
|
|
1947
|
+
delivery = await callbacks.flush({ signal: controller.signal });
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1951
|
+
level: "warn",
|
|
1952
|
+
component: "senses",
|
|
1953
|
+
event: "senses.bluebubbles_final_transport_failed",
|
|
1954
|
+
message: "bluebubbles final transport failed after invocation",
|
|
1955
|
+
meta: {
|
|
1956
|
+
messageGuid: event.messageGuid,
|
|
1957
|
+
sessionKey: event.chat.sessionKey,
|
|
1958
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1959
|
+
},
|
|
1960
|
+
});
|
|
1961
|
+
return {
|
|
1962
|
+
handled: true,
|
|
1963
|
+
notifiedAgent: false,
|
|
1964
|
+
kind: event.kind,
|
|
1965
|
+
reason: "delivery_failed",
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
if (delivery.status !== "accepted" || !finalTransportAccepted) {
|
|
1570
1969
|
return {
|
|
1571
1970
|
handled: true,
|
|
1572
1971
|
notifiedAgent: false,
|
|
1573
1972
|
kind: event.kind,
|
|
1973
|
+
reason: !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)
|
|
1974
|
+
? "superseded"
|
|
1975
|
+
: "no_visible_reply",
|
|
1574
1976
|
};
|
|
1575
1977
|
}
|
|
1576
|
-
//
|
|
1577
|
-
|
|
1578
|
-
if
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1978
|
+
// Acceptance is the visibility boundary. Publish the terminal dedupe
|
|
1979
|
+
// receipt first, then project the private generated tail while this
|
|
1980
|
+
// chat's canonical lock remains held. Even if receipt publication
|
|
1981
|
+
// fails, make the best possible canonical projection before surfacing
|
|
1982
|
+
// the retained-claim failure. Ancillary accounting follows both.
|
|
1983
|
+
let receiptFailed = false;
|
|
1984
|
+
let receiptFailure;
|
|
1985
|
+
try {
|
|
1986
|
+
options.publishAcceptedReceipt();
|
|
1987
|
+
}
|
|
1988
|
+
catch (error) {
|
|
1989
|
+
receiptFailed = true;
|
|
1990
|
+
receiptFailure = error;
|
|
1582
1991
|
}
|
|
1992
|
+
persistAcceptedBlueBubblesProjection({
|
|
1993
|
+
stage: turnStage,
|
|
1994
|
+
deps: resolvedDeps,
|
|
1995
|
+
event,
|
|
1996
|
+
});
|
|
1997
|
+
if (receiptFailed)
|
|
1998
|
+
throw receiptFailure;
|
|
1999
|
+
promoteAcceptedBlueBubblesAncillaryState({
|
|
2000
|
+
stage: turnStage,
|
|
2001
|
+
deps: resolvedDeps,
|
|
2002
|
+
agentName,
|
|
2003
|
+
event,
|
|
2004
|
+
source,
|
|
2005
|
+
processedOutcome,
|
|
2006
|
+
});
|
|
1583
2007
|
(0, runtime_1.emitNervesEvent)({
|
|
1584
2008
|
component: "senses",
|
|
1585
2009
|
event: "senses.bluebubbles_turn_end",
|
|
@@ -1614,28 +2038,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1614
2038
|
});
|
|
1615
2039
|
});
|
|
1616
2040
|
/* v8 ignore stop */
|
|
1617
|
-
return await (
|
|
1618
|
-
try {
|
|
1619
|
-
return await Promise.race([runTurn, timeoutPromise]);
|
|
1620
|
-
}
|
|
1621
|
-
catch (error) {
|
|
1622
|
-
if (isBlueBubblesRecoveryTurnTimeout(error)) {
|
|
1623
|
-
(0, runtime_1.emitNervesEvent)({
|
|
1624
|
-
level: "warn",
|
|
1625
|
-
component: "senses",
|
|
1626
|
-
event: "senses.bluebubbles_timeout_notice_suppressed",
|
|
1627
|
-
message: "bluebubbles timeout notice suppressed from iMessage",
|
|
1628
|
-
meta: {
|
|
1629
|
-
messageGuid: event.messageGuid,
|
|
1630
|
-
sessionKey: event.chat.sessionKey,
|
|
1631
|
-
source,
|
|
1632
|
-
timeoutMs,
|
|
1633
|
-
},
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
throw error;
|
|
1637
|
-
}
|
|
1638
|
-
})();
|
|
2041
|
+
return await Promise.race([runTurn, timeoutPromise, supersessionPromise, lifecyclePromise]);
|
|
1639
2042
|
}
|
|
1640
2043
|
finally {
|
|
1641
2044
|
// If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
|
|
@@ -1647,10 +2050,10 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1647
2050
|
}
|
|
1648
2051
|
/* v8 ignore stop */
|
|
1649
2052
|
clearTimeout(timeoutTimer);
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2053
|
+
options.latestTurnCapability.signal.removeEventListener("abort", cancelForSupersession);
|
|
2054
|
+
lifecycleSignal?.removeEventListener("abort", cancelForShutdown);
|
|
2055
|
+
(0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, liveTurnId);
|
|
2056
|
+
activeTurnId = null;
|
|
1654
2057
|
await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
|
|
1655
2058
|
}
|
|
1656
2059
|
});
|
|
@@ -1683,11 +2086,9 @@ function recordBlueBubblesAuditSidecar(agentName, event, source, resolvedDeps) {
|
|
|
1683
2086
|
});
|
|
1684
2087
|
}
|
|
1685
2088
|
}
|
|
1686
|
-
function
|
|
1687
|
-
return target.fromMe === true ? "agent" : "non_agent_unknown";
|
|
1688
|
-
}
|
|
1689
|
-
async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source) {
|
|
2089
|
+
async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source, observationReservation) {
|
|
1690
2090
|
const agentName = resolvedDeps.getAgentName();
|
|
2091
|
+
const capturedAt = new Date().toISOString();
|
|
1691
2092
|
if (!hasObservedBlueBubblesDirection(normalized)) {
|
|
1692
2093
|
(0, runtime_1.emitNervesEvent)({
|
|
1693
2094
|
level: "warn",
|
|
@@ -1705,14 +2106,6 @@ async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source)
|
|
|
1705
2106
|
}
|
|
1706
2107
|
const observed = normalized;
|
|
1707
2108
|
const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
|
|
1708
|
-
let resolvedReactionTarget;
|
|
1709
|
-
let targetAuthorship = null;
|
|
1710
|
-
if (observed.kind === "mutation"
|
|
1711
|
-
&& observed.mutationType === "reaction"
|
|
1712
|
-
&& observed.targetMessageGuid) {
|
|
1713
|
-
resolvedReactionTarget = await resolveReactionTarget(resolvedDeps.createClient(), observed.targetMessageGuid);
|
|
1714
|
-
targetAuthorship = reactionTargetAuthorship(resolvedReactionTarget);
|
|
1715
|
-
}
|
|
1716
2109
|
let coordinateGeneration;
|
|
1717
2110
|
if (observed.kind === "mutation"
|
|
1718
2111
|
&& observed.mutationType === "reaction"
|
|
@@ -1741,9 +2134,11 @@ async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source)
|
|
|
1741
2134
|
}
|
|
1742
2135
|
const capture = (0, semantic_receipts_1.buildBlueBubblesSemanticCapture)({
|
|
1743
2136
|
cutover,
|
|
1744
|
-
capturedAt
|
|
2137
|
+
capturedAt,
|
|
2138
|
+
observationEpoch: observationReservation.observationEpoch,
|
|
2139
|
+
observationOrdinal: observationReservation.ordinal,
|
|
1745
2140
|
event: observed,
|
|
1746
|
-
targetAuthorship,
|
|
2141
|
+
targetAuthorship: null,
|
|
1747
2142
|
coordinateGeneration,
|
|
1748
2143
|
});
|
|
1749
2144
|
if (!capture) {
|
|
@@ -1761,7 +2156,6 @@ async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source)
|
|
|
1761
2156
|
capture,
|
|
1762
2157
|
normalized: observed,
|
|
1763
2158
|
writeResult,
|
|
1764
|
-
...(resolvedReactionTarget ? { resolvedReactionTarget } : {}),
|
|
1765
2159
|
};
|
|
1766
2160
|
}
|
|
1767
2161
|
function semanticCaptureChat(capture) {
|
|
@@ -1792,11 +2186,58 @@ function semanticCaptureChat(capture) {
|
|
|
1792
2186
|
participantHandles: event.participants.map((participant) => participant.externalId),
|
|
1793
2187
|
};
|
|
1794
2188
|
}
|
|
1795
|
-
function semanticCaptureRequiresRoutingRepair(capture) {
|
|
1796
|
-
return !capture.event.chatGuid?.trim() && !capture.event.chatIdentifier?.trim();
|
|
2189
|
+
function semanticCaptureRequiresRoutingRepair(capture) {
|
|
2190
|
+
return !capture.event.chatGuid?.trim() && !capture.event.chatIdentifier?.trim();
|
|
2191
|
+
}
|
|
2192
|
+
function hasResolvedBlueBubblesRouting(event) {
|
|
2193
|
+
return Boolean(event.chat.chatGuid?.trim() || event.chat.chatIdentifier?.trim());
|
|
2194
|
+
}
|
|
2195
|
+
function reserveBlueBubblesObservation(event) {
|
|
2196
|
+
return (0, latest_turn_1.reserveObservation)({
|
|
2197
|
+
chatGuid: event.chat.chatGuid,
|
|
2198
|
+
chatIdentifier: event.chat.chatIdentifier,
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
function groupBlueBubblesObservationsByLane(candidates) {
|
|
2202
|
+
let groups = [];
|
|
2203
|
+
for (const candidate of candidates) {
|
|
2204
|
+
const keys = new Set((0, latest_turn_1.observationSchedulingKeys)(candidate.observationReservation));
|
|
2205
|
+
const destination = { keys: new Set(keys), candidates: [candidate] };
|
|
2206
|
+
const independentGroups = [];
|
|
2207
|
+
for (const group of groups) {
|
|
2208
|
+
const intersects = (keys.has("unresolved:*")
|
|
2209
|
+
|| group.keys.has("unresolved:*")
|
|
2210
|
+
|| [...keys].some((key) => group.keys.has(key)));
|
|
2211
|
+
if (!intersects) {
|
|
2212
|
+
independentGroups.push(group);
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
destination.candidates.push(...group.candidates);
|
|
2216
|
+
for (const key of group.keys)
|
|
2217
|
+
destination.keys.add(key);
|
|
2218
|
+
}
|
|
2219
|
+
groups = [...independentGroups, destination];
|
|
2220
|
+
}
|
|
2221
|
+
return groups.map((group) => group.candidates.sort((left, right) => (right.observationReservation.ordinal - left.observationReservation.ordinal)));
|
|
2222
|
+
}
|
|
2223
|
+
function canonicalizeBlueBubblesEvent(event, capability) {
|
|
2224
|
+
const canonical = capability.canonicalChat;
|
|
2225
|
+
const { chatIdentifier: _observedIdentifier, ...observedChat } = event.chat;
|
|
2226
|
+
return {
|
|
2227
|
+
...event,
|
|
2228
|
+
chat: {
|
|
2229
|
+
...observedChat,
|
|
2230
|
+
chatGuid: canonical.chatGuid,
|
|
2231
|
+
...(canonical.chatIdentifier ? { chatIdentifier: canonical.chatIdentifier } : {}),
|
|
2232
|
+
sessionKey: canonical.sessionKey,
|
|
2233
|
+
sendTarget: { kind: "chat_guid", value: canonical.chatGuid },
|
|
2234
|
+
},
|
|
2235
|
+
};
|
|
1797
2236
|
}
|
|
1798
|
-
function
|
|
1799
|
-
return
|
|
2237
|
+
function isCaptureOnlyBlueBubblesMutation(capture) {
|
|
2238
|
+
return capture.event.kind === "reaction"
|
|
2239
|
+
|| capture.event.kind === "edit"
|
|
2240
|
+
|| capture.event.kind === "unsend";
|
|
1800
2241
|
}
|
|
1801
2242
|
function isAuditOnlySemanticCapture(capture) {
|
|
1802
2243
|
return capture.event.kind === "read" || capture.event.kind === "delivery";
|
|
@@ -1872,43 +2313,30 @@ function semanticCaptureToNormalizedEvent(capture) {
|
|
|
1872
2313
|
: {}),
|
|
1873
2314
|
};
|
|
1874
2315
|
}
|
|
1875
|
-
function
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
// paired normalized event from that same tuple, so it is the authoritative
|
|
1898
|
-
// reaction object; repair contributes routing only and may legitimately
|
|
1899
|
-
// promote its own transport-shaped result to `message`.
|
|
1900
|
-
const event = captured.normalized;
|
|
1901
|
-
return {
|
|
1902
|
-
...event,
|
|
1903
|
-
mutationType: "reaction",
|
|
1904
|
-
reaction: event.reaction,
|
|
1905
|
-
targetMessageGuid: event.targetMessageGuid,
|
|
1906
|
-
chat: repaired.chat,
|
|
1907
|
-
requiresRepair: repaired.requiresRepair,
|
|
1908
|
-
repairNotice: repaired.repairNotice,
|
|
1909
|
-
};
|
|
2316
|
+
function seedBlueBubblesStartupSemanticObservations(deps = {}) {
|
|
2317
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2318
|
+
const agentName = resolvedDeps.getAgentName();
|
|
2319
|
+
const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
|
|
2320
|
+
let seeded = 0;
|
|
2321
|
+
for (const capture of (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName)
|
|
2322
|
+
.sort(semantic_receipts_1.compareBlueBubblesSemanticCaptureOrder)) {
|
|
2323
|
+
const disposition = (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(capture, cutover);
|
|
2324
|
+
if (disposition.disposition === "audit_only")
|
|
2325
|
+
continue;
|
|
2326
|
+
const normalized = semanticCaptureToNormalizedEvent(capture);
|
|
2327
|
+
if (!normalized)
|
|
2328
|
+
continue;
|
|
2329
|
+
const generation = adoptBlueBubblesSemanticObservation(capture.keyHash, reserveBlueBubblesObservation(normalized), false);
|
|
2330
|
+
// Startup seeding runs synchronously before listen. Retain each fence so
|
|
2331
|
+
// persisted pre-restart observations remain causally older than any live
|
|
2332
|
+
// webhook accepted by this process and can reuse the same generation when
|
|
2333
|
+
// the delayed recovery pass begins.
|
|
2334
|
+
suspendBlueBubblesSemanticObservation(generation);
|
|
2335
|
+
seeded += 1;
|
|
2336
|
+
}
|
|
2337
|
+
return seeded;
|
|
1910
2338
|
}
|
|
1911
|
-
async function classifyCapturedBlueBubblesReaction(captured
|
|
2339
|
+
async function classifyCapturedBlueBubblesReaction(captured) {
|
|
1912
2340
|
const event = captured.capture.event;
|
|
1913
2341
|
if (event.kind !== "reaction" || !event.canonicalAction || !event.canonicalValue)
|
|
1914
2342
|
return null;
|
|
@@ -1916,151 +2344,78 @@ async function classifyCapturedBlueBubblesReaction(captured, resolvedDeps) {
|
|
|
1916
2344
|
fromMe: event.fromMe,
|
|
1917
2345
|
action: event.canonicalAction,
|
|
1918
2346
|
canonicalValue: event.canonicalValue,
|
|
1919
|
-
targetAuthorship: event.targetAuthorship,
|
|
1920
|
-
};
|
|
1921
|
-
const initial = (0, reaction_policy_1.classifyBlueBubblesReaction)(input);
|
|
1922
|
-
if (initial.route !== "trust_required") {
|
|
1923
|
-
return { decision: initial, trustedFriend: null };
|
|
1924
|
-
}
|
|
1925
|
-
let trustedActor = false;
|
|
1926
|
-
let trustedFriend = null;
|
|
1927
|
-
try {
|
|
1928
|
-
const friend = await resolvedDeps.createFriendStore().findByExternalId("imessage-handle", event.actor.externalId);
|
|
1929
|
-
trustedActor = friends_1.TRUSTED_LEVELS.has(friend?.trustLevel ?? "stranger");
|
|
1930
|
-
if (trustedActor && friend)
|
|
1931
|
-
trustedFriend = friend;
|
|
1932
|
-
}
|
|
1933
|
-
catch (error) {
|
|
1934
|
-
(0, runtime_1.emitNervesEvent)({
|
|
1935
|
-
level: "warn",
|
|
1936
|
-
component: "senses",
|
|
1937
|
-
event: "senses.bluebubbles_reaction_trust_lookup_error",
|
|
1938
|
-
message: "failed direct actor trust lookup for bluebubbles reaction",
|
|
1939
|
-
meta: {
|
|
1940
|
-
keyHash: captured.capture.keyHash,
|
|
1941
|
-
provider: event.actor.provider,
|
|
1942
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1943
|
-
},
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
return {
|
|
1947
|
-
decision: (0, reaction_policy_1.classifyBlueBubblesReaction)({ ...input, trustedActor }),
|
|
1948
|
-
trustedFriend,
|
|
1949
2347
|
};
|
|
2348
|
+
return { decision: (0, reaction_policy_1.classifyBlueBubblesReaction)(input) };
|
|
1950
2349
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
const target = input.resolvedReactionTarget
|
|
1954
|
-
?? await resolveReactionTarget(client, event.targetMessageGuid);
|
|
1955
|
-
event.textForAgent = (0, model_1.renderBlueBubblesReactionText)(event.reaction, target);
|
|
1956
|
-
const channel = (0, friends_1.getChannelCapabilities)("bluebubbles");
|
|
1957
|
-
const context = { friend: trustedFriend, channel, isGroupChat: event.chat.isGroup };
|
|
1958
|
-
const userMessage = {
|
|
1959
|
-
role: "user",
|
|
1960
|
-
content: buildInboundContent(event),
|
|
1961
|
-
};
|
|
1962
|
-
const orientationFrame = (0, orientation_frame_1.buildOrientationFrame)({
|
|
1963
|
-
channel: "bluebubbles",
|
|
1964
|
-
messages: [userMessage],
|
|
1965
|
-
currentUserMessages: [userMessage],
|
|
1966
|
-
source: buildBlueBubblesOrientationSource(event, [], options.orientationEvidence, options.orientationConversationKind),
|
|
1967
|
-
speechKind: "reaction",
|
|
1968
|
-
});
|
|
1969
|
-
const callbacks = createBlueBubblesCallbacks(client, event.chat, createReplyTargetController(event), event.chat.isGroup, undefined, { enableSilenceWatchdog: false, enableActivitySignals: false });
|
|
1970
|
-
const controller = new AbortController();
|
|
1971
|
-
const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
|
|
1972
|
-
let timeoutTimer;
|
|
1973
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1974
|
-
timeoutTimer = setTimeout(() => {
|
|
1975
|
-
const error = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
|
|
1976
|
-
callbacks.cancelOutbound("turn_timeout");
|
|
1977
|
-
controller.abort(error);
|
|
1978
|
-
reject(error);
|
|
1979
|
-
}, timeoutMs);
|
|
1980
|
-
if (typeof timeoutTimer.unref === "function") {
|
|
1981
|
-
timeoutTimer.unref();
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
try {
|
|
1985
|
-
const restrictedTurnAndDelivery = async () => {
|
|
1986
|
-
const result = await resolvedDeps.runAgent([userMessage], callbacks, "bluebubbles", controller.signal, {
|
|
1987
|
-
isReactionSignal: true,
|
|
1988
|
-
restrictedReactionFeedback: true,
|
|
1989
|
-
skipKeptNotes: true,
|
|
1990
|
-
orientationFrame,
|
|
1991
|
-
toolContext: {
|
|
1992
|
-
signin: async () => undefined,
|
|
1993
|
-
context,
|
|
1994
|
-
currentIngressEvidence: options.currentIngressEvidence,
|
|
1995
|
-
},
|
|
1996
|
-
});
|
|
1997
|
-
if (result.outcome === "settled" || result.outcome === "blocked") {
|
|
1998
|
-
await callbacks.flush({ signal: controller.signal });
|
|
1999
|
-
}
|
|
2000
|
-
return result;
|
|
2001
|
-
};
|
|
2002
|
-
const result = await Promise.race([
|
|
2003
|
-
restrictedTurnAndDelivery(),
|
|
2004
|
-
timeoutPromise,
|
|
2005
|
-
]);
|
|
2006
|
-
if (result.outcome === "settled" || result.outcome === "blocked") {
|
|
2007
|
-
return {
|
|
2008
|
-
handled: true,
|
|
2009
|
-
notifiedAgent: true,
|
|
2010
|
-
kind: event.kind,
|
|
2011
|
-
reason: "restricted_feedback_settled",
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
if (result.outcome === "observed") {
|
|
2015
|
-
return {
|
|
2016
|
-
handled: true,
|
|
2017
|
-
notifiedAgent: false,
|
|
2018
|
-
kind: event.kind,
|
|
2019
|
-
reason: "restricted_feedback_observed",
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2350
|
+
function captureOnlyHandledDisposition(capture, reactionDecision) {
|
|
2351
|
+
if (capture.event.kind !== "reaction") {
|
|
2022
2352
|
return {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
kind: event.kind,
|
|
2026
|
-
reason: "restricted_feedback_failed",
|
|
2353
|
+
outcome: capture.event.kind === "edit" ? "edit_capture_only" : "unsend_capture_only",
|
|
2354
|
+
detailCode: null,
|
|
2027
2355
|
};
|
|
2028
2356
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2357
|
+
const classifiedOutcome = reactionDecision.outcome;
|
|
2358
|
+
if (classifiedOutcome === "capture_only_negative" || classifiedOutcome === "capture_only_question") {
|
|
2031
2359
|
return {
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
kind: event.kind,
|
|
2035
|
-
reason: "restricted_feedback_failed",
|
|
2360
|
+
outcome: "capture_only_unknown",
|
|
2361
|
+
detailCode: classifiedOutcome,
|
|
2036
2362
|
};
|
|
2037
2363
|
}
|
|
2038
|
-
|
|
2039
|
-
clearTimeout(timeoutTimer);
|
|
2040
|
-
await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
|
|
2041
|
-
}
|
|
2364
|
+
return { outcome: classifiedOutcome, detailCode: null };
|
|
2042
2365
|
}
|
|
2043
|
-
function
|
|
2366
|
+
function auditOnlyHandledOutcome(kind) {
|
|
2367
|
+
return kind === "read" ? "read_audit_only" : "delivery_audit_only";
|
|
2368
|
+
}
|
|
2369
|
+
function writeCapturedBlueBubblesHandled(agentName, capture, outcome, detailCode = null) {
|
|
2044
2370
|
const handledResult = (0, semantic_receipts_1.writeBlueBubblesSemanticHandled)(agentName, {
|
|
2045
2371
|
schemaVersion: 1,
|
|
2046
2372
|
canonicalKey: capture.canonicalKey,
|
|
2047
2373
|
keyHash: capture.keyHash,
|
|
2048
2374
|
handledAt: new Date().toISOString(),
|
|
2049
2375
|
outcome,
|
|
2050
|
-
detailCode
|
|
2376
|
+
detailCode,
|
|
2051
2377
|
});
|
|
2052
2378
|
if (handledResult === "semantic_handled_collision") {
|
|
2053
2379
|
throw new Error("semantic_handled_collision");
|
|
2054
2380
|
}
|
|
2055
2381
|
}
|
|
2056
|
-
|
|
2382
|
+
function blueBubblesInboundReplyIdempotencyKey(captureKeyHash) {
|
|
2383
|
+
return `bluebubbles-inbound-reply:${captureKeyHash}`;
|
|
2384
|
+
}
|
|
2385
|
+
function blueBubblesRecoveredOutboundOutcome(record) {
|
|
2386
|
+
return BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES.has(record.status)
|
|
2387
|
+
? "message_completed"
|
|
2388
|
+
: "message_observed";
|
|
2389
|
+
}
|
|
2390
|
+
async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, source, options) {
|
|
2391
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2057
2392
|
const agentName = resolvedDeps.getAgentName();
|
|
2058
2393
|
const identity = {
|
|
2059
2394
|
canonicalKey: captured.capture.canonicalKey,
|
|
2060
2395
|
keyHash: captured.capture.keyHash,
|
|
2061
2396
|
};
|
|
2062
|
-
const
|
|
2397
|
+
const generation = adoptBlueBubblesSemanticObservation(identity.keyHash, options.observationReservation, true);
|
|
2398
|
+
const semanticHandling = await acquireBlueBubblesSemanticHandlingSlot(identity.keyHash, generation);
|
|
2399
|
+
if (semanticHandling.status === "handled_by_owner") {
|
|
2400
|
+
return {
|
|
2401
|
+
handled: true,
|
|
2402
|
+
notifiedAgent: false,
|
|
2403
|
+
kind: captured.normalized.kind,
|
|
2404
|
+
reason: "already_processed",
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
const reservation = semanticHandling.reservation;
|
|
2408
|
+
let claim;
|
|
2409
|
+
try {
|
|
2410
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2411
|
+
claim = await (0, semantic_receipts_1.acquireBlueBubblesSemanticClaim)(agentName, identity);
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "retryable");
|
|
2415
|
+
throw error;
|
|
2416
|
+
}
|
|
2063
2417
|
if (claim.status === "already_handled") {
|
|
2418
|
+
releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "terminal");
|
|
2064
2419
|
return {
|
|
2065
2420
|
handled: true,
|
|
2066
2421
|
notifiedAgent: false,
|
|
@@ -2069,6 +2424,7 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2069
2424
|
};
|
|
2070
2425
|
}
|
|
2071
2426
|
if (claim.status === "timeout") {
|
|
2427
|
+
releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, "retryable");
|
|
2072
2428
|
return {
|
|
2073
2429
|
handled: false,
|
|
2074
2430
|
notifiedAgent: false,
|
|
@@ -2076,32 +2432,97 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2076
2432
|
reason: "semantic_claim_timeout",
|
|
2077
2433
|
};
|
|
2078
2434
|
}
|
|
2435
|
+
let latestTurnCapability = null;
|
|
2436
|
+
let semanticHandlingCompleted = false;
|
|
2437
|
+
let retainSemanticClaim = false;
|
|
2438
|
+
let acceptedReceiptPublished = false;
|
|
2439
|
+
const publishTerminalReceipt = (outcome, detailCode = null) => {
|
|
2440
|
+
try {
|
|
2441
|
+
writeCapturedBlueBubblesHandled(agentName, captured.capture, outcome, detailCode);
|
|
2442
|
+
}
|
|
2443
|
+
catch (error) {
|
|
2444
|
+
// A missing terminal receipt must never reopen this semantic event. Keep
|
|
2445
|
+
// the durable claim live so recovery cannot resurrect a canceled turn or
|
|
2446
|
+
// duplicate a reply whose transport is already externally visible.
|
|
2447
|
+
retainSemanticClaim = true;
|
|
2448
|
+
semanticHandlingCompleted = true;
|
|
2449
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2450
|
+
level: "error",
|
|
2451
|
+
component: "senses",
|
|
2452
|
+
event: "senses.bluebubbles_terminal_receipt_failed",
|
|
2453
|
+
message: "bluebubbles event could not publish its terminal semantic receipt",
|
|
2454
|
+
meta: {
|
|
2455
|
+
messageGuid: captured.normalized.messageGuid,
|
|
2456
|
+
keyHash: captured.capture.keyHash,
|
|
2457
|
+
outcome,
|
|
2458
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2459
|
+
},
|
|
2460
|
+
});
|
|
2461
|
+
throw error;
|
|
2462
|
+
}
|
|
2463
|
+
};
|
|
2464
|
+
const publishAcceptedReceipt = () => {
|
|
2465
|
+
if (acceptedReceiptPublished)
|
|
2466
|
+
return;
|
|
2467
|
+
publishTerminalReceipt("message_completed");
|
|
2468
|
+
acceptedReceiptPublished = true;
|
|
2469
|
+
};
|
|
2079
2470
|
try {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
schemaVersion: 1,
|
|
2084
|
-
provider: "bluebubbles",
|
|
2085
|
-
captureKeyHash: captured.capture.keyHash,
|
|
2086
|
-
}),
|
|
2087
|
-
orientationEvidence: captured.capture.event,
|
|
2088
|
-
orientationConversationKind: captured.normalized.chat.isGroup ? "group" : "one_to_one",
|
|
2089
|
-
};
|
|
2090
|
-
const reactionClassification = await classifyCapturedBlueBubblesReaction(captured, resolvedDeps);
|
|
2471
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2472
|
+
const reactionClassification = await classifyCapturedBlueBubblesReaction(captured);
|
|
2473
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2091
2474
|
const reactionDecision = reactionClassification?.decision ?? null;
|
|
2092
|
-
if (
|
|
2475
|
+
if (captured.capture.event.fromMe) {
|
|
2476
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2477
|
+
component: "senses",
|
|
2478
|
+
event: "senses.bluebubbles_from_me_ignored",
|
|
2479
|
+
message: "ignored from-me bluebubbles event",
|
|
2480
|
+
meta: {
|
|
2481
|
+
messageGuid: captured.normalized.messageGuid,
|
|
2482
|
+
kind: captured.normalized.kind,
|
|
2483
|
+
},
|
|
2484
|
+
});
|
|
2485
|
+
if (captured.normalized.chat.isGroup) {
|
|
2486
|
+
recordDiscoveredOwnHandle(captured.normalized.sender.externalId);
|
|
2487
|
+
}
|
|
2093
2488
|
const result = {
|
|
2094
2489
|
handled: true,
|
|
2095
2490
|
notifiedAgent: false,
|
|
2096
2491
|
kind: captured.normalized.kind,
|
|
2097
|
-
reason:
|
|
2492
|
+
reason: "from_me",
|
|
2098
2493
|
};
|
|
2099
|
-
|
|
2494
|
+
publishTerminalReceipt("ignored_self");
|
|
2495
|
+
semanticHandlingCompleted = true;
|
|
2100
2496
|
return result;
|
|
2101
2497
|
}
|
|
2102
|
-
|
|
2498
|
+
const durableOutbound = (0, outbound_state_1.readBlueBubblesOutboundRecordByIdempotencyKey)((0, identity_1.getAgentRoot)(), blueBubblesInboundReplyIdempotencyKey(captured.capture.keyHash));
|
|
2499
|
+
if (durableOutbound) {
|
|
2500
|
+
const outcome = blueBubblesRecoveredOutboundOutcome(durableOutbound);
|
|
2501
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2502
|
+
level: "warn",
|
|
2503
|
+
component: "senses",
|
|
2504
|
+
event: "senses.bluebubbles_inbound_reply_recovery_suppressed",
|
|
2505
|
+
message: "suppressed bluebubbles inbound reply recovery at its durable send boundary",
|
|
2506
|
+
meta: {
|
|
2507
|
+
messageGuid: captured.normalized.messageGuid,
|
|
2508
|
+
recordId: durableOutbound.recordId,
|
|
2509
|
+
outboundStatus: durableOutbound.status,
|
|
2510
|
+
outcome,
|
|
2511
|
+
},
|
|
2512
|
+
});
|
|
2513
|
+
publishTerminalReceipt(outcome);
|
|
2514
|
+
semanticHandlingCompleted = true;
|
|
2515
|
+
return {
|
|
2516
|
+
handled: true,
|
|
2517
|
+
notifiedAgent: false,
|
|
2518
|
+
kind: captured.normalized.kind,
|
|
2519
|
+
reason: "already_processed",
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
let handledOutcome = "message_observed";
|
|
2103
2523
|
let result;
|
|
2104
2524
|
if (isAuditOnlySemanticCapture(captured.capture)) {
|
|
2525
|
+
handledOutcome = auditOnlyHandledOutcome(captured.capture.event.kind);
|
|
2105
2526
|
result = {
|
|
2106
2527
|
handled: true,
|
|
2107
2528
|
notifiedAgent: false,
|
|
@@ -2110,8 +2531,11 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2110
2531
|
};
|
|
2111
2532
|
}
|
|
2112
2533
|
else {
|
|
2113
|
-
const
|
|
2114
|
-
const repaired =
|
|
2534
|
+
const captureOnlyMutation = isCaptureOnlyBlueBubblesMutation(captured.capture);
|
|
2535
|
+
const repaired = captureOnlyMutation && captured.normalized.chat.chatGuid?.trim()
|
|
2536
|
+
? captured.normalized
|
|
2537
|
+
: await resolvedDeps.createClient().repairEvent(captured.normalized);
|
|
2538
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2115
2539
|
if (semanticCaptureRequiresRoutingRepair(captured.capture)
|
|
2116
2540
|
&& !hasResolvedBlueBubblesRouting(repaired)) {
|
|
2117
2541
|
throw new Error("semantic_capture_routing_invalid");
|
|
@@ -2121,31 +2545,89 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2121
2545
|
fromMe: captured.normalized.fromMe,
|
|
2122
2546
|
sender: captured.normalized.sender,
|
|
2123
2547
|
};
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2548
|
+
const promotion = (0, latest_turn_1.promote)(reservation, {
|
|
2549
|
+
chatGuid: identityGroundedRepaired.chat.chatGuid,
|
|
2550
|
+
chatIdentifier: identityGroundedRepaired.chat.chatIdentifier,
|
|
2551
|
+
}, {
|
|
2552
|
+
allowSameGenerationRetry: semanticHandling.allowSameGenerationRetry,
|
|
2553
|
+
});
|
|
2554
|
+
if (promotion.status !== "promoted") {
|
|
2555
|
+
if (!captureOnlyMutation && promotion.status === "unresolved") {
|
|
2556
|
+
throw new Error("semantic_capture_routing_invalid");
|
|
2557
|
+
}
|
|
2558
|
+
const disposition = captureOnlyMutation
|
|
2559
|
+
? captureOnlyHandledDisposition(captured.capture, reactionDecision)
|
|
2560
|
+
: { outcome: "message_observed", detailCode: null };
|
|
2561
|
+
publishTerminalReceipt(disposition.outcome, disposition.detailCode);
|
|
2562
|
+
semanticHandlingCompleted = true;
|
|
2563
|
+
return {
|
|
2564
|
+
handled: true,
|
|
2565
|
+
notifiedAgent: false,
|
|
2566
|
+
kind: captured.normalized.kind,
|
|
2567
|
+
reason: captureOnlyMutation ? "mutation_state_only" : "superseded",
|
|
2568
|
+
};
|
|
2136
2569
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2570
|
+
latestTurnCapability = promotion.capability;
|
|
2571
|
+
if (captureOnlyMutation) {
|
|
2572
|
+
const disposition = captureOnlyHandledDisposition(captured.capture, reactionDecision);
|
|
2573
|
+
publishTerminalReceipt(disposition.outcome, disposition.detailCode);
|
|
2574
|
+
semanticHandlingCompleted = true;
|
|
2575
|
+
return {
|
|
2576
|
+
handled: true,
|
|
2577
|
+
notifiedAgent: false,
|
|
2578
|
+
kind: captured.normalized.kind,
|
|
2579
|
+
reason: "mutation_state_only",
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
if (identityGroundedRepaired.kind === "mutation") {
|
|
2583
|
+
const outcome = auditOnlyHandledOutcome(identityGroundedRepaired.mutationType);
|
|
2584
|
+
publishTerminalReceipt(outcome);
|
|
2585
|
+
semanticHandlingCompleted = true;
|
|
2586
|
+
return {
|
|
2587
|
+
handled: true,
|
|
2588
|
+
notifiedAgent: false,
|
|
2589
|
+
kind: captured.normalized.kind,
|
|
2590
|
+
reason: "mutation_state_only",
|
|
2591
|
+
};
|
|
2142
2592
|
}
|
|
2593
|
+
const canonicalEvent = canonicalizeBlueBubblesEvent(identityGroundedRepaired, latestTurnCapability);
|
|
2594
|
+
const capturedOptions = {
|
|
2595
|
+
...options,
|
|
2596
|
+
latestTurnCapability,
|
|
2597
|
+
currentIngressEvidence: Object.freeze({
|
|
2598
|
+
schemaVersion: 1,
|
|
2599
|
+
provider: "bluebubbles",
|
|
2600
|
+
captureKeyHash: captured.capture.keyHash,
|
|
2601
|
+
}),
|
|
2602
|
+
orientationEvidence: captured.capture.event,
|
|
2603
|
+
orientationConversationKind: canonicalEvent.chat.isGroup ? "group" : "one_to_one",
|
|
2604
|
+
publishAcceptedReceipt,
|
|
2605
|
+
};
|
|
2606
|
+
result = await handleBlueBubblesNormalizedEvent(canonicalEvent, resolvedDeps, source, capturedOptions);
|
|
2607
|
+
handledOutcome = result.notifiedAgent ? "message_completed" : "message_observed";
|
|
2608
|
+
}
|
|
2609
|
+
if (result.notifiedAgent) {
|
|
2610
|
+
publishAcceptedReceipt();
|
|
2143
2611
|
}
|
|
2144
|
-
|
|
2612
|
+
else {
|
|
2613
|
+
publishTerminalReceipt(handledOutcome);
|
|
2614
|
+
}
|
|
2615
|
+
semanticHandlingCompleted = true;
|
|
2145
2616
|
return result;
|
|
2146
2617
|
}
|
|
2147
2618
|
finally {
|
|
2148
|
-
|
|
2619
|
+
if (latestTurnCapability)
|
|
2620
|
+
(0, latest_turn_1.finish)(latestTurnCapability);
|
|
2621
|
+
try {
|
|
2622
|
+
if (!retainSemanticClaim) {
|
|
2623
|
+
(0, semantic_receipts_1.releaseBlueBubblesSemanticClaim)(agentName, claim);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
finally {
|
|
2627
|
+
releaseBlueBubblesSemanticHandlingSlot(identity.keyHash, semanticHandling.slot, retainSemanticClaim
|
|
2628
|
+
? "retained"
|
|
2629
|
+
: semanticHandlingCompleted ? "terminal" : "retryable");
|
|
2630
|
+
}
|
|
2149
2631
|
}
|
|
2150
2632
|
}
|
|
2151
2633
|
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
@@ -2172,11 +2654,22 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
2172
2654
|
}
|
|
2173
2655
|
throw error;
|
|
2174
2656
|
}
|
|
2175
|
-
const
|
|
2657
|
+
const observationReservation = reserveBlueBubblesObservation(normalized);
|
|
2658
|
+
let captured;
|
|
2659
|
+
try {
|
|
2660
|
+
captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook", observationReservation);
|
|
2661
|
+
}
|
|
2662
|
+
catch (error) {
|
|
2663
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
2664
|
+
throw error;
|
|
2665
|
+
}
|
|
2176
2666
|
if (captured.status === "audit_only") {
|
|
2667
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
2177
2668
|
return { handled: true, notifiedAgent: false, kind: normalized.kind, reason: "ignored" };
|
|
2178
2669
|
}
|
|
2179
|
-
return handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook"
|
|
2670
|
+
return handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook", {
|
|
2671
|
+
observationReservation,
|
|
2672
|
+
});
|
|
2180
2673
|
}
|
|
2181
2674
|
function parseTimestampMs(value) {
|
|
2182
2675
|
if (!value)
|
|
@@ -2220,14 +2713,20 @@ function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
|
|
|
2220
2713
|
oldestPendingRecoveryAgeMs: oldest ? Math.max(0, nowMs - oldest.ms) : undefined,
|
|
2221
2714
|
};
|
|
2222
2715
|
}
|
|
2223
|
-
async function syncBlueBubblesRuntime(deps = {}) {
|
|
2716
|
+
async function syncBlueBubblesRuntime(deps = {}, options = {}) {
|
|
2717
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2224
2718
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2225
2719
|
const agentName = resolvedDeps.getAgentName();
|
|
2720
|
+
// Allocate before the first health-probe await. Any live webhook accepted
|
|
2721
|
+
// while that probe is pending must remain causally newer than this pass.
|
|
2722
|
+
const catchUpObservationBatch = options.catchUpObservationBatch ?? (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
|
|
2723
|
+
pruneBlueBubblesSemanticObservationGenerations();
|
|
2226
2724
|
const client = resolvedDeps.createClient();
|
|
2227
2725
|
const checkedAt = new Date().toISOString();
|
|
2228
2726
|
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
2229
2727
|
try {
|
|
2230
2728
|
await client.checkHealth();
|
|
2729
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2231
2730
|
const pendingBeforeCatchup = blueBubblesPendingRecoverySnapshot(agentName);
|
|
2232
2731
|
const activeBeforeCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
2233
2732
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
@@ -2242,7 +2741,10 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
2242
2741
|
});
|
|
2243
2742
|
const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState, {
|
|
2244
2743
|
processTurns: false,
|
|
2744
|
+
observationBatch: catchUpObservationBatch,
|
|
2745
|
+
lifecycleSignal: options.lifecycleSignal,
|
|
2245
2746
|
});
|
|
2747
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2246
2748
|
const failed = catchUp.failed;
|
|
2247
2749
|
const pendingAfterCatchup = blueBubblesPendingRecoverySnapshot(agentName);
|
|
2248
2750
|
const activeAfterCatchup = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
@@ -2265,6 +2767,8 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
2265
2767
|
});
|
|
2266
2768
|
}
|
|
2267
2769
|
catch (error) {
|
|
2770
|
+
if (options.lifecycleSignal?.aborted)
|
|
2771
|
+
return;
|
|
2268
2772
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
2269
2773
|
upstreamStatus: "error",
|
|
2270
2774
|
detail: error instanceof Error ? error.message : String(error),
|
|
@@ -2276,7 +2780,8 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
2276
2780
|
});
|
|
2277
2781
|
}
|
|
2278
2782
|
}
|
|
2279
|
-
async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
2783
|
+
async function recoverQueuedBlueBubblesMessages(deps = {}, options = {}) {
|
|
2784
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2280
2785
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2281
2786
|
const agentName = resolvedDeps.getAgentName();
|
|
2282
2787
|
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
@@ -2284,8 +2789,10 @@ async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
|
2284
2789
|
if (initialPending === 0) {
|
|
2285
2790
|
return { recovered: 0, skipped: 0, failed: 0, pendingRecoveryCount: 0 };
|
|
2286
2791
|
}
|
|
2287
|
-
const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps);
|
|
2792
|
+
const captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps, options);
|
|
2793
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2288
2794
|
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
2795
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2289
2796
|
const pendingSnapshot = blueBubblesPendingRecoverySnapshot(agentName);
|
|
2290
2797
|
const pendingRecoveryCount = pendingSnapshot.pendingRecoveryCount;
|
|
2291
2798
|
const failed = captured.failed + recovery.failed;
|
|
@@ -2294,6 +2801,7 @@ async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
|
2294
2801
|
const checkedAt = new Date().toISOString();
|
|
2295
2802
|
try {
|
|
2296
2803
|
await resolvedDeps.createClient().checkHealth();
|
|
2804
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2297
2805
|
const activeSnapshot = (0, active_turns_1.snapshotBlueBubblesActiveTurns)(agentName, BLUEBUBBLES_LIVE_TURN_STALLED_MS);
|
|
2298
2806
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
2299
2807
|
upstreamStatus: "ok",
|
|
@@ -2308,6 +2816,8 @@ async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
|
2308
2816
|
});
|
|
2309
2817
|
}
|
|
2310
2818
|
catch (error) {
|
|
2819
|
+
if (options.lifecycleSignal?.aborted)
|
|
2820
|
+
throw options.lifecycleSignal.reason;
|
|
2311
2821
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
2312
2822
|
upstreamStatus: "error",
|
|
2313
2823
|
detail: error instanceof Error ? error.message : String(error),
|
|
@@ -2323,6 +2833,7 @@ async function recoverQueuedBlueBubblesMessages(deps = {}) {
|
|
|
2323
2833
|
return { recovered, skipped, failed, pendingRecoveryCount };
|
|
2324
2834
|
}
|
|
2325
2835
|
async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, options = {}) {
|
|
2836
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2326
2837
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2327
2838
|
const agentName = resolvedDeps.getAgentName();
|
|
2328
2839
|
const client = resolvedDeps.createClient();
|
|
@@ -2344,16 +2855,21 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
|
|
|
2344
2855
|
maxPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
|
|
2345
2856
|
},
|
|
2346
2857
|
});
|
|
2347
|
-
const
|
|
2858
|
+
const preparedCandidates = [];
|
|
2859
|
+
const seenMessageGuids = new Set();
|
|
2860
|
+
const observationBatch = options.observationBatch ?? (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
|
|
2348
2861
|
for (let page = 0; page < BLUEBUBBLES_CATCHUP_MAX_PAGES; page++) {
|
|
2349
2862
|
let pageEvents;
|
|
2350
2863
|
try {
|
|
2351
2864
|
pageEvents = await client.listRecentMessages({
|
|
2352
2865
|
limit: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
2353
2866
|
offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
2867
|
+
beforeTimestamp: observationBatch.beforeTimestamp,
|
|
2354
2868
|
});
|
|
2355
2869
|
}
|
|
2356
2870
|
catch (error) {
|
|
2871
|
+
if (options.lifecycleSignal?.aborted)
|
|
2872
|
+
throw options.lifecycleSignal.reason;
|
|
2357
2873
|
result.failed++;
|
|
2358
2874
|
(0, runtime_1.emitNervesEvent)({
|
|
2359
2875
|
level: "warn",
|
|
@@ -2367,11 +2883,36 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
|
|
|
2367
2883
|
});
|
|
2368
2884
|
break;
|
|
2369
2885
|
}
|
|
2370
|
-
|
|
2886
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2887
|
+
const pageMessages = pageEvents
|
|
2888
|
+
.filter((event) => event.kind === "message")
|
|
2889
|
+
.sort((left, right) => right.timestamp - left.timestamp);
|
|
2890
|
+
for (let pageIndex = 0; pageIndex < pageMessages.length; pageIndex++) {
|
|
2891
|
+
const event = pageMessages[pageIndex];
|
|
2892
|
+
if (seenMessageGuids.has(event.messageGuid))
|
|
2893
|
+
continue;
|
|
2894
|
+
seenMessageGuids.add(event.messageGuid);
|
|
2895
|
+
if (event.fromMe
|
|
2896
|
+
|| event.timestamp < catchUpSince
|
|
2897
|
+
|| (Number.isFinite(event.timestamp) && event.timestamp > observationBatch.beforeTimestamp)) {
|
|
2898
|
+
preparedCandidates.push({ status: "skip", event });
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
// The bounded ordinal range and upstream snapshot were fixed before the
|
|
2902
|
+
// first query. Each returned page can therefore publish its fences
|
|
2903
|
+
// before the next await, while later ingress stays outside this pass.
|
|
2904
|
+
preparedCandidates.push({
|
|
2905
|
+
status: "ready",
|
|
2906
|
+
event,
|
|
2907
|
+
observationReservation: (0, latest_turn_1.reserveObservationFromBatch)(observationBatch, page * BLUEBUBBLES_CATCHUP_PAGE_SIZE + pageIndex, {
|
|
2908
|
+
chatGuid: event.chat.chatGuid,
|
|
2909
|
+
chatIdentifier: event.chat.chatIdentifier,
|
|
2910
|
+
}),
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2371
2913
|
if (pageEvents.length < BLUEBUBBLES_CATCHUP_PAGE_SIZE)
|
|
2372
2914
|
break;
|
|
2373
|
-
const oldestMessageTimestamp =
|
|
2374
|
-
.filter((event) => event.kind === "message")
|
|
2915
|
+
const oldestMessageTimestamp = pageMessages
|
|
2375
2916
|
.reduce((oldest, event) => Math.min(oldest, event.timestamp), Number.POSITIVE_INFINITY);
|
|
2376
2917
|
if (oldestMessageTimestamp <= catchUpSince)
|
|
2377
2918
|
break;
|
|
@@ -2389,68 +2930,91 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
|
|
|
2389
2930
|
});
|
|
2390
2931
|
}
|
|
2391
2932
|
}
|
|
2392
|
-
const
|
|
2393
|
-
const candidates = recentEvents
|
|
2394
|
-
.filter((event) => event.kind === "message")
|
|
2395
|
-
.filter((event) => {
|
|
2396
|
-
if (seenMessageGuids.has(event.messageGuid))
|
|
2397
|
-
return false;
|
|
2398
|
-
seenMessageGuids.add(event.messageGuid);
|
|
2399
|
-
return true;
|
|
2400
|
-
})
|
|
2401
|
-
.sort((left, right) => left.timestamp - right.timestamp);
|
|
2402
|
-
for (const event of candidates) {
|
|
2933
|
+
for (const prepared of preparedCandidates) {
|
|
2403
2934
|
result.inspected++;
|
|
2404
|
-
if (
|
|
2405
|
-
|| event.timestamp < catchUpSince) {
|
|
2935
|
+
if (prepared.status === "skip") {
|
|
2406
2936
|
result.skipped++;
|
|
2407
|
-
continue;
|
|
2408
2937
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2938
|
+
}
|
|
2939
|
+
const readyCandidates = preparedCandidates.filter((prepared) => prepared.status === "ready");
|
|
2940
|
+
let lastRecoveredOrdinal = -1;
|
|
2941
|
+
await Promise.all(groupBlueBubblesObservationsByLane(readyCandidates).map(async (lane) => {
|
|
2942
|
+
for (const prepared of lane) {
|
|
2943
|
+
const { event, observationReservation } = prepared;
|
|
2944
|
+
let semanticGenerationOwned = false;
|
|
2945
|
+
try {
|
|
2946
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2947
|
+
const captured = await captureBlueBubblesSemanticEvent(event, resolvedDeps, "upstream-catchup", observationReservation);
|
|
2948
|
+
if (captured.status === "audit_only") {
|
|
2949
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
2950
|
+
result.skipped++;
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
if (!processTurns) {
|
|
2954
|
+
const generation = adoptBlueBubblesSemanticObservation(captured.capture.keyHash, observationReservation, false);
|
|
2955
|
+
semanticGenerationOwned = true;
|
|
2956
|
+
if (captured.writeResult === "semantic_capture_duplicate") {
|
|
2957
|
+
if (!activeSemanticHandlingSlots.has(captured.capture.keyHash)) {
|
|
2958
|
+
suspendBlueBubblesSemanticObservation(generation);
|
|
2959
|
+
}
|
|
2960
|
+
result.skipped++;
|
|
2961
|
+
}
|
|
2962
|
+
else {
|
|
2963
|
+
// Runtime sync only captures work for the delayed recovery pass,
|
|
2964
|
+
// but its original generation must revoke older work now and be
|
|
2965
|
+
// reused by the queued recovery pass after any intervening turn.
|
|
2966
|
+
const promotion = (0, latest_turn_1.promote)(generation.reservation, {
|
|
2967
|
+
chatGuid: event.chat.chatGuid,
|
|
2968
|
+
chatIdentifier: event.chat.chatIdentifier,
|
|
2969
|
+
});
|
|
2970
|
+
if (promotion.status === "promoted")
|
|
2971
|
+
(0, latest_turn_1.finish)(promotion.capability);
|
|
2972
|
+
suspendBlueBubblesSemanticObservation(generation);
|
|
2973
|
+
result.queued = (result.queued ?? 0) + 1;
|
|
2974
|
+
}
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
semanticGenerationOwned = true;
|
|
2978
|
+
const handled = await handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "upstream-catchup", {
|
|
2979
|
+
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
2980
|
+
autonomyBudgetTrigger: "recovery",
|
|
2981
|
+
observationReservation,
|
|
2982
|
+
lifecycleSignal: options.lifecycleSignal,
|
|
2983
|
+
});
|
|
2984
|
+
if (handled.reason === "semantic_claim_timeout") {
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
|
|
2417
2988
|
result.skipped++;
|
|
2418
2989
|
}
|
|
2419
2990
|
else {
|
|
2420
|
-
result.
|
|
2991
|
+
result.recovered++;
|
|
2992
|
+
if (observationReservation.ordinal > lastRecoveredOrdinal) {
|
|
2993
|
+
lastRecoveredOrdinal = observationReservation.ordinal;
|
|
2994
|
+
result.lastRecoveredMessageGuid = event.messageGuid;
|
|
2995
|
+
}
|
|
2421
2996
|
}
|
|
2422
|
-
continue;
|
|
2423
|
-
}
|
|
2424
|
-
const handled = await handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "upstream-catchup", {
|
|
2425
|
-
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
2426
|
-
autonomyBudgetTrigger: "recovery",
|
|
2427
|
-
});
|
|
2428
|
-
if (handled.reason === "semantic_claim_timeout") {
|
|
2429
|
-
continue;
|
|
2430
2997
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2998
|
+
catch (error) {
|
|
2999
|
+
if (!semanticGenerationOwned)
|
|
3000
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
3001
|
+
if (options.lifecycleSignal?.aborted)
|
|
3002
|
+
throw options.lifecycleSignal.reason;
|
|
3003
|
+
recordBlueBubblesRecoveryFailureForBudget(agentName, event, "upstream-catchup");
|
|
3004
|
+
result.failed++;
|
|
3005
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3006
|
+
level: "warn",
|
|
3007
|
+
component: "senses",
|
|
3008
|
+
event: "senses.bluebubbles_catchup_error",
|
|
3009
|
+
message: "bluebubbles upstream catch-up message failed",
|
|
3010
|
+
meta: {
|
|
3011
|
+
messageGuid: event.messageGuid,
|
|
3012
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3013
|
+
},
|
|
3014
|
+
});
|
|
2437
3015
|
}
|
|
2438
3016
|
}
|
|
2439
|
-
|
|
2440
|
-
recordBlueBubblesRecoveryFailureForBudget(agentName, event, "upstream-catchup");
|
|
2441
|
-
result.failed++;
|
|
2442
|
-
(0, runtime_1.emitNervesEvent)({
|
|
2443
|
-
level: "warn",
|
|
2444
|
-
component: "senses",
|
|
2445
|
-
event: "senses.bluebubbles_catchup_error",
|
|
2446
|
-
message: "bluebubbles upstream catch-up message failed",
|
|
2447
|
-
meta: {
|
|
2448
|
-
messageGuid: event.messageGuid,
|
|
2449
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
2450
|
-
},
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
3017
|
+
}));
|
|
2454
3018
|
if (result.inspected > 0 || result.recovered > 0 || result.skipped > 0 || result.failed > 0) {
|
|
2455
3019
|
(0, runtime_1.emitNervesEvent)({
|
|
2456
3020
|
component: "senses",
|
|
@@ -2461,52 +3025,47 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
|
|
|
2461
3025
|
}
|
|
2462
3026
|
return result;
|
|
2463
3027
|
}
|
|
2464
|
-
async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
|
|
3028
|
+
async function recoverCapturedBlueBubblesInboundMessages(deps = {}, options = {}) {
|
|
3029
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2465
3030
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
2466
3031
|
const agentName = resolvedDeps.getAgentName();
|
|
2467
3032
|
const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
|
|
2468
3033
|
const result = { recovered: 0, skipped: 0, failed: 0 };
|
|
2469
3034
|
const seenKeyHashes = new Set();
|
|
2470
3035
|
const candidates = (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName)
|
|
2471
|
-
.sort(
|
|
3036
|
+
.sort(semantic_receipts_1.compareBlueBubblesSemanticCaptureOrder);
|
|
3037
|
+
const preparedCandidates = [];
|
|
2472
3038
|
for (const capture of candidates) {
|
|
3039
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
2473
3040
|
if (seenKeyHashes.has(capture.keyHash)) {
|
|
2474
|
-
|
|
3041
|
+
preparedCandidates.push({ status: "skip", capture });
|
|
2475
3042
|
continue;
|
|
2476
3043
|
}
|
|
2477
3044
|
seenKeyHashes.add(capture.keyHash);
|
|
2478
3045
|
const disposition = (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(capture, cutover);
|
|
2479
3046
|
if (disposition.disposition === "audit_only" && disposition.reason !== "audit_event") {
|
|
2480
|
-
|
|
3047
|
+
preparedCandidates.push({ status: "skip", capture });
|
|
2481
3048
|
continue;
|
|
2482
3049
|
}
|
|
2483
3050
|
const normalized = semanticCaptureToNormalizedEvent(capture);
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const handled = await handleCapturedBlueBubblesSemanticEvent({
|
|
2488
|
-
status: "captured",
|
|
2489
|
-
capture,
|
|
2490
|
-
normalized,
|
|
2491
|
-
writeResult: "semantic_capture_duplicate",
|
|
2492
|
-
}, resolvedDeps, "webhook", {
|
|
2493
|
-
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
2494
|
-
autonomyBudgetTrigger: "recovery",
|
|
2495
|
-
semanticRecovery: true,
|
|
2496
|
-
});
|
|
2497
|
-
if (handled.reason === "semantic_claim_timeout") {
|
|
2498
|
-
continue;
|
|
2499
|
-
}
|
|
2500
|
-
if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
|
|
2501
|
-
result.skipped++;
|
|
2502
|
-
}
|
|
2503
|
-
else {
|
|
2504
|
-
result.recovered++;
|
|
2505
|
-
}
|
|
3051
|
+
if (!normalized) {
|
|
3052
|
+
preparedCandidates.push({ status: "invalid", capture });
|
|
3053
|
+
continue;
|
|
2506
3054
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
3055
|
+
preparedCandidates.push({
|
|
3056
|
+
status: "ready",
|
|
3057
|
+
capture,
|
|
3058
|
+
normalized,
|
|
3059
|
+
observationReservation: reserveBlueBubblesObservation(normalized),
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
for (const prepared of preparedCandidates) {
|
|
3063
|
+
const { capture } = prepared;
|
|
3064
|
+
if (prepared.status === "skip") {
|
|
3065
|
+
result.skipped++;
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
if (prepared.status === "invalid") {
|
|
2510
3069
|
result.failed++;
|
|
2511
3070
|
(0, runtime_1.emitNervesEvent)({
|
|
2512
3071
|
level: "warn",
|
|
@@ -2516,11 +3075,58 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
|
|
|
2516
3075
|
meta: {
|
|
2517
3076
|
messageGuid: capture.event.eventGuid,
|
|
2518
3077
|
sessionKey: capture.event.sessionKey,
|
|
2519
|
-
reason:
|
|
3078
|
+
reason: "semantic_capture_routing_invalid",
|
|
2520
3079
|
},
|
|
2521
3080
|
});
|
|
2522
3081
|
}
|
|
2523
3082
|
}
|
|
3083
|
+
const readyCandidates = preparedCandidates.filter((prepared) => prepared.status === "ready");
|
|
3084
|
+
await Promise.all(groupBlueBubblesObservationsByLane(readyCandidates).map(async (lane) => {
|
|
3085
|
+
for (const prepared of lane) {
|
|
3086
|
+
const { capture, normalized, observationReservation } = prepared;
|
|
3087
|
+
try {
|
|
3088
|
+
options.lifecycleSignal?.throwIfAborted();
|
|
3089
|
+
const handled = await handleCapturedBlueBubblesSemanticEvent({
|
|
3090
|
+
status: "captured",
|
|
3091
|
+
capture,
|
|
3092
|
+
normalized,
|
|
3093
|
+
writeResult: "semantic_capture_duplicate",
|
|
3094
|
+
}, resolvedDeps, "webhook", {
|
|
3095
|
+
timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
|
|
3096
|
+
autonomyBudgetTrigger: "recovery",
|
|
3097
|
+
semanticRecovery: true,
|
|
3098
|
+
observationReservation,
|
|
3099
|
+
lifecycleSignal: options.lifecycleSignal,
|
|
3100
|
+
});
|
|
3101
|
+
if (handled.reason === "semantic_claim_timeout") {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
|
|
3105
|
+
result.skipped++;
|
|
3106
|
+
}
|
|
3107
|
+
else {
|
|
3108
|
+
result.recovered++;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
catch (error) {
|
|
3112
|
+
if (options.lifecycleSignal?.aborted)
|
|
3113
|
+
throw options.lifecycleSignal.reason;
|
|
3114
|
+
recordBlueBubblesRecoveryFailureForBudget(agentName, normalized, "webhook");
|
|
3115
|
+
result.failed++;
|
|
3116
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3117
|
+
level: "warn",
|
|
3118
|
+
component: "senses",
|
|
3119
|
+
event: "senses.bluebubbles_capture_recovery_error",
|
|
3120
|
+
message: "captured bluebubbles message recovery failed",
|
|
3121
|
+
meta: {
|
|
3122
|
+
messageGuid: capture.event.eventGuid,
|
|
3123
|
+
sessionKey: capture.event.sessionKey,
|
|
3124
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3125
|
+
},
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}));
|
|
2524
3130
|
const semanticEventGuids = new Set(candidates
|
|
2525
3131
|
.map((capture) => capture.event.eventGuid)
|
|
2526
3132
|
.filter((eventGuid) => typeof eventGuid === "string")
|
|
@@ -2552,7 +3158,7 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
|
2552
3158
|
}
|
|
2553
3159
|
return result;
|
|
2554
3160
|
}
|
|
2555
|
-
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
3161
|
+
function createBlueBubblesWebhookHandler(deps = {}, options = {}) {
|
|
2556
3162
|
return async (req, res) => {
|
|
2557
3163
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2558
3164
|
if (url.pathname === "/health") {
|
|
@@ -2633,11 +3239,13 @@ function createBlueBubblesWebhookHandler(deps = {}) {
|
|
|
2633
3239
|
return;
|
|
2634
3240
|
}
|
|
2635
3241
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
3242
|
+
const observationReservation = reserveBlueBubblesObservation(normalized);
|
|
2636
3243
|
let captured;
|
|
2637
3244
|
try {
|
|
2638
|
-
captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook");
|
|
3245
|
+
captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook", observationReservation);
|
|
2639
3246
|
}
|
|
2640
3247
|
catch (error) {
|
|
3248
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
2641
3249
|
(0, runtime_1.emitNervesEvent)({
|
|
2642
3250
|
level: "error",
|
|
2643
3251
|
component: "senses",
|
|
@@ -2652,6 +3260,7 @@ function createBlueBubblesWebhookHandler(deps = {}) {
|
|
|
2652
3260
|
return;
|
|
2653
3261
|
}
|
|
2654
3262
|
if (captured.status === "audit_only") {
|
|
3263
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
2655
3264
|
writeJson(res, 200, {
|
|
2656
3265
|
handled: true,
|
|
2657
3266
|
notifiedAgent: false,
|
|
@@ -2669,7 +3278,16 @@ function createBlueBubblesWebhookHandler(deps = {}) {
|
|
|
2669
3278
|
reason: "queued",
|
|
2670
3279
|
});
|
|
2671
3280
|
setTimeout(() => {
|
|
2672
|
-
|
|
3281
|
+
if (options.lifecycleSignal?.aborted) {
|
|
3282
|
+
(0, latest_turn_1.clearPending)(observationReservation);
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
void handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook", {
|
|
3286
|
+
observationReservation,
|
|
3287
|
+
lifecycleSignal: options.lifecycleSignal,
|
|
3288
|
+
}).catch((error) => {
|
|
3289
|
+
if (options.lifecycleSignal?.aborted)
|
|
3290
|
+
return;
|
|
2673
3291
|
(0, runtime_1.emitNervesEvent)({
|
|
2674
3292
|
level: "error",
|
|
2675
3293
|
component: "senses",
|
|
@@ -3088,20 +3706,34 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
|
|
|
3088
3706
|
}
|
|
3089
3707
|
function startBlueBubblesApp(deps = {}) {
|
|
3090
3708
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
3709
|
+
const lifecycleController = new AbortController();
|
|
3091
3710
|
(0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(resolvedDeps.getAgentName());
|
|
3092
3711
|
resolvedDeps.createClient();
|
|
3712
|
+
seedBlueBubblesStartupSemanticObservations(resolvedDeps);
|
|
3713
|
+
const startupCatchUpObservationBatch = (0, latest_turn_1.beginObservationBatch)(BLUEBUBBLES_CATCHUP_PAGE_SIZE * BLUEBUBBLES_CATCHUP_MAX_PAGES);
|
|
3093
3714
|
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
3094
|
-
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps
|
|
3715
|
+
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps, {
|
|
3716
|
+
lifecycleSignal: lifecycleController.signal,
|
|
3717
|
+
}));
|
|
3095
3718
|
let recoveryPassRunning = false;
|
|
3719
|
+
let runtimeSyncRunning = false;
|
|
3096
3720
|
let recoveryDelayTimer = null;
|
|
3721
|
+
let closed = false;
|
|
3097
3722
|
function triggerRecoveryPass() {
|
|
3723
|
+
/* v8 ignore next -- close clears both recovery timers; guard covers an already-queued host callback @preserve */
|
|
3724
|
+
if (closed)
|
|
3725
|
+
return;
|
|
3098
3726
|
/* v8 ignore next -- re-entrant timer guard; difficult to force deterministically without timing the turn lock @preserve */
|
|
3099
3727
|
if (recoveryPassRunning)
|
|
3100
3728
|
return;
|
|
3101
3729
|
recoveryPassRunning = true;
|
|
3102
|
-
void recoverQueuedBlueBubblesMessages(resolvedDeps
|
|
3730
|
+
void recoverQueuedBlueBubblesMessages(resolvedDeps, {
|
|
3731
|
+
lifecycleSignal: lifecycleController.signal,
|
|
3732
|
+
})
|
|
3103
3733
|
/* v8 ignore start -- defensive wrapper; expected per-message failures are handled inside recovery helpers @preserve */
|
|
3104
3734
|
.catch((error) => {
|
|
3735
|
+
if (closed || lifecycleController.signal.aborted)
|
|
3736
|
+
return;
|
|
3105
3737
|
(0, runtime_1.emitNervesEvent)({
|
|
3106
3738
|
level: "warn",
|
|
3107
3739
|
component: "senses",
|
|
@@ -3116,6 +3748,8 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3116
3748
|
});
|
|
3117
3749
|
}
|
|
3118
3750
|
function scheduleRecoveryPass() {
|
|
3751
|
+
if (closed)
|
|
3752
|
+
return;
|
|
3119
3753
|
/* v8 ignore next -- duplicate scheduling guard for overlapping health sync completions @preserve */
|
|
3120
3754
|
if (recoveryDelayTimer !== null)
|
|
3121
3755
|
return;
|
|
@@ -3124,11 +3758,40 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3124
3758
|
triggerRecoveryPass();
|
|
3125
3759
|
}, BLUEBUBBLES_RECOVERY_PASS_DELAY_MS);
|
|
3126
3760
|
}
|
|
3761
|
+
function triggerRuntimeSync(catchUpObservationBatch) {
|
|
3762
|
+
if (closed || runtimeSyncRunning)
|
|
3763
|
+
return;
|
|
3764
|
+
runtimeSyncRunning = true;
|
|
3765
|
+
void syncBlueBubblesRuntime(resolvedDeps, {
|
|
3766
|
+
...(catchUpObservationBatch ? { catchUpObservationBatch } : {}),
|
|
3767
|
+
lifecycleSignal: lifecycleController.signal,
|
|
3768
|
+
})
|
|
3769
|
+
.then(scheduleRecoveryPass)
|
|
3770
|
+
.catch((error) => {
|
|
3771
|
+
if (closed)
|
|
3772
|
+
return;
|
|
3773
|
+
(0, runtime_1.emitNervesEvent)({
|
|
3774
|
+
level: "warn",
|
|
3775
|
+
component: "senses",
|
|
3776
|
+
event: "senses.bluebubbles_recovery_error",
|
|
3777
|
+
message: "bluebubbles runtime sync failed",
|
|
3778
|
+
meta: { reason: error instanceof Error ? error.message : String(error) },
|
|
3779
|
+
});
|
|
3780
|
+
})
|
|
3781
|
+
.finally(() => {
|
|
3782
|
+
runtimeSyncRunning = false;
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3127
3785
|
const runtimeTimer = setInterval(() => {
|
|
3128
|
-
|
|
3786
|
+
/* v8 ignore next -- close clears this interval; guard covers an already-queued host callback @preserve */
|
|
3787
|
+
if (closed)
|
|
3788
|
+
return;
|
|
3789
|
+
triggerRuntimeSync();
|
|
3129
3790
|
}, BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS);
|
|
3130
3791
|
const recoveryTimer = setInterval(triggerRecoveryPass, BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS);
|
|
3131
3792
|
server.on?.("close", () => {
|
|
3793
|
+
closed = true;
|
|
3794
|
+
lifecycleController.abort(new Error("bluebubbles_runtime_shutdown"));
|
|
3132
3795
|
clearInterval(runtimeTimer);
|
|
3133
3796
|
clearInterval(recoveryTimer);
|
|
3134
3797
|
if (recoveryDelayTimer !== null) {
|
|
@@ -3144,6 +3807,6 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
3144
3807
|
meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
|
|
3145
3808
|
});
|
|
3146
3809
|
});
|
|
3147
|
-
|
|
3810
|
+
triggerRuntimeSync(startupCatchUpObservationBatch);
|
|
3148
3811
|
return server;
|
|
3149
3812
|
}
|