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