@ouro.bot/cli 0.1.0-alpha.724 → 0.1.0-alpha.725

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.
Files changed (54) hide show
  1. package/changelog.json +12 -0
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +72 -14
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +21 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/pipeline.js +90 -24
  50. package/dist/senses/private-runtime-worker.js +21 -20
  51. package/dist/senses/private-runtime.js +79 -29
  52. package/dist/senses/shared-turn.js +1 -0
  53. package/dist/senses/teams.js +1 -0
  54. package/package.json +6 -1
@@ -79,8 +79,10 @@ const client_1 = require("./client");
79
79
  const inbound_log_1 = require("./inbound-log");
80
80
  const mutation_log_1 = require("./mutation-log");
81
81
  const processed_log_1 = require("./processed-log");
82
+ const semantic_receipts_1 = require("./semantic-receipts");
82
83
  const runtime_state_1 = require("./runtime-state");
83
84
  const session_cleanup_1 = require("./session-cleanup");
85
+ const reaction_policy_1 = require("./reaction-policy");
84
86
  const active_turns_1 = require("./active-turns");
85
87
  const trust_gate_1 = require("../trust-gate");
86
88
  const pipeline_1 = require("../pipeline");
@@ -358,8 +360,8 @@ function resolveFriendParams(event) {
358
360
  }
359
361
  return {
360
362
  provider: "imessage-handle",
361
- externalId: event.sender.externalId || event.sender.rawId,
362
- displayName: event.sender.displayName || "Unknown",
363
+ externalId: event.sender.externalId,
364
+ displayName: event.sender.displayName,
363
365
  channel: "bluebubbles",
364
366
  };
365
367
  }
@@ -463,11 +465,49 @@ function extractHistoricalLaneSummary(messages) {
463
465
  }
464
466
  return summaries;
465
467
  }
466
- function buildBlueBubblesOrientationSource(event, existingMessages, repliedToText) {
468
+ function buildBlueBubblesOrientationSource(event, existingMessages, evidence, conversationKind, repliedToText) {
467
469
  const repairNotice = event.repairNotice?.trim();
470
+ const actor = evidence.actor;
471
+ const participants = evidence.participants
472
+ .filter((participant) => !(participant.provider === actor.provider
473
+ && participant.externalId === actor.externalId))
474
+ .map((participant) => ({
475
+ role: "group_participant_only",
476
+ provider: participant.provider,
477
+ externalId: participant.externalId,
478
+ displayName: participant.displayName,
479
+ }));
480
+ const targetGuid = evidence.targetGuid;
481
+ const targetAuthorship = evidence.targetAuthorship;
482
+ const presentation = {
483
+ authority: "presentation_only",
484
+ conversationKind,
485
+ event: {
486
+ provider: evidence.provider,
487
+ kind: evidence.kind,
488
+ sourceEventType: evidence.sourceEventType,
489
+ fromMe: evidence.fromMe,
490
+ },
491
+ actor: {
492
+ role: "observed_actor",
493
+ provider: actor.provider,
494
+ externalId: actor.externalId,
495
+ displayName: actor.displayName,
496
+ },
497
+ participants,
498
+ ...(targetGuid
499
+ ? {
500
+ target: {
501
+ messageGuid: targetGuid,
502
+ authorship: targetAuthorship ?? "unknown",
503
+ },
504
+ }
505
+ : {}),
506
+ };
468
507
  if (event.kind !== "message") {
469
508
  return {
470
509
  kind: "bluebubbles",
510
+ ...presentation,
471
511
  lane: "mutation",
472
512
  ...(repairNotice ? { repairNotice } : {}),
473
513
  };
@@ -477,6 +517,7 @@ function buildBlueBubblesOrientationSource(event, existingMessages, repliedToTex
477
517
  const hasThreadRoute = !!threadId || summaries.some((summary) => summary.key.startsWith("thread:"));
478
518
  return {
479
519
  kind: "bluebubbles",
520
+ ...presentation,
480
521
  lane: threadId ? "thread" : "top_level",
481
522
  defaultReplyTarget: threadId ? "current_lane" : "top_level",
482
523
  ...(threadId ? { threadId } : {}),
@@ -518,37 +559,6 @@ function sessionLikelyContainsMessage(event, existingMessages) {
518
559
  return extractMessageText(message.content).includes(fragment);
519
560
  });
520
561
  }
521
- function mutationEntryToEvent(entry) {
522
- return {
523
- kind: "mutation",
524
- eventType: entry.eventType,
525
- mutationType: entry.mutationType,
526
- messageGuid: entry.messageGuid,
527
- targetMessageGuid: entry.targetMessageGuid ?? undefined,
528
- timestamp: Date.parse(entry.recordedAt) || Date.now(),
529
- fromMe: entry.fromMe,
530
- sender: {
531
- provider: "imessage-handle",
532
- externalId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
533
- rawId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
534
- displayName: entry.chatIdentifier ?? entry.chatGuid ?? "Unknown",
535
- },
536
- chat: {
537
- chatGuid: entry.chatGuid ?? undefined,
538
- chatIdentifier: entry.chatIdentifier ?? undefined,
539
- displayName: undefined,
540
- isGroup: Boolean(entry.chatGuid?.includes(";+;")),
541
- sessionKey: entry.sessionKey,
542
- sendTarget: entry.chatGuid
543
- ? { kind: "chat_guid", value: entry.chatGuid }
544
- : { kind: "chat_identifier", value: entry.chatIdentifier ?? "unknown" },
545
- participantHandles: [],
546
- },
547
- shouldNotifyAgent: entry.shouldNotifyAgent,
548
- textForAgent: entry.textForAgent,
549
- requiresRepair: true,
550
- };
551
- }
552
562
  function getBlueBubblesContinuityIngressTexts(event) {
553
563
  if (event.kind !== "message")
554
564
  return [];
@@ -603,7 +613,10 @@ function shouldUseBlueBubblesRecoveryBudget(source, options) {
603
613
  || source === "recovery-bootstrap";
604
614
  }
605
615
  function shouldCountBlueBubblesRecoveryResultAsSkipped(reason) {
606
- return reason === "already_processed" || reason === "autonomy_budget_blocked";
616
+ return reason === "already_processed"
617
+ || reason === "autonomy_budget_blocked"
618
+ || reason === "mutation_state_only"
619
+ || reason === "from_me";
607
620
  }
608
621
  function recordBlueBubblesRecoveryFailureForBudget(agentName, event, source) {
609
622
  try {
@@ -731,7 +744,7 @@ function emitBlueBubblesMarkReadWarning(chat, error) {
731
744
  },
732
745
  });
733
746
  }
734
- function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVisibleActivity) {
747
+ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVisibleActivity, options = {}) {
735
748
  let textBuffer = "";
736
749
  let typingActive = false;
737
750
  let queue = Promise.resolve();
@@ -885,11 +898,13 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
885
898
  });
886
899
  }
887
900
  return {
901
+ settleOutputMode: "retractable_buffer",
888
902
  onModelStart() {
889
903
  if (outboundClosed)
890
904
  return;
891
- startSilenceWatchdog();
892
- if (!isGroupChat)
905
+ if (options.enableSilenceWatchdog !== false)
906
+ startSilenceWatchdog();
907
+ if (options.enableActivitySignals !== false && !isGroupChat)
893
908
  startTypingNow();
894
909
  (0, runtime_1.emitNervesEvent)({
895
910
  component: "senses",
@@ -909,7 +924,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
909
924
  onTextChunk(text) {
910
925
  if (outboundClosed)
911
926
  return;
912
- if (isGroupChat && !typingActive)
927
+ if (options.enableActivitySignals !== false && isGroupChat && !typingActive)
913
928
  startTypingNow();
914
929
  textBuffer += text;
915
930
  },
@@ -921,7 +936,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
921
936
  // console. Keep the human-facing thread quiet until the agent has real
922
937
  // text (or the generic silence watchdog fires), while preserving native
923
938
  // read/typing signals and nerves telemetry for debugging.
924
- if (!typingActive && name !== "observe" && name !== "speak")
939
+ if (options.enableActivitySignals !== false && !typingActive && name !== "observe" && name !== "speak")
925
940
  startTypingNow();
926
941
  (0, runtime_1.emitNervesEvent)({
927
942
  component: "senses",
@@ -993,7 +1008,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
993
1008
  meta: { messageLength: trimmed.length },
994
1009
  });
995
1010
  },
996
- async flush() {
1011
+ async flush(flushOptions = {}) {
997
1012
  if (outboundClosed) {
998
1013
  textBuffer = "";
999
1014
  await waitForBlueBubblesCallbackCleanup(queue, BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS, {
@@ -1038,6 +1053,7 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
1038
1053
  chat,
1039
1054
  text: trimmed,
1040
1055
  replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
1056
+ ...(flushOptions.signal ? { signal: flushOptions.signal } : {}),
1041
1057
  });
1042
1058
  recordVisibleActivity();
1043
1059
  },
@@ -1078,6 +1094,11 @@ function writeJson(res, statusCode, payload) {
1078
1094
  res.setHeader("Content-Type", "application/json; charset=utf-8");
1079
1095
  res.end(JSON.stringify(payload));
1080
1096
  }
1097
+ function writeSemanticCaptureFailure(res) {
1098
+ res.statusCode = 503;
1099
+ res.setHeader("Content-Type", "application/json");
1100
+ res.end('{"ok":false,"error":"semantic_capture_failed"}');
1101
+ }
1081
1102
  function isWebhookPasswordValid(url, expectedPassword) {
1082
1103
  const provided = url.searchParams.get("password");
1083
1104
  return !provided || provided === expectedPassword;
@@ -1183,7 +1204,10 @@ async function shouldFilterAgentSelfHandle(event, resolvedDeps) {
1183
1204
  }
1184
1205
  return true;
1185
1206
  }
1186
- async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, options = {}) {
1207
+ function hasObservedBlueBubblesDirection(event) {
1208
+ return typeof event.fromMe === "boolean";
1209
+ }
1210
+ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, options) {
1187
1211
  const client = resolvedDeps.createClient();
1188
1212
  const agentName = resolvedDeps.getAgentName();
1189
1213
  (0, runtime_cwd_1.recoverRuntimeCwd)();
@@ -1222,24 +1246,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1222
1246
  });
1223
1247
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "from_me" };
1224
1248
  }
1225
- if (event.kind === "mutation") {
1226
- try {
1227
- resolvedDeps.recordMutation(resolvedDeps.getAgentName(), event);
1228
- }
1229
- catch (error) {
1230
- (0, runtime_1.emitNervesEvent)({
1231
- level: "error",
1232
- component: "senses",
1233
- event: "senses.bluebubbles_mutation_log_error",
1234
- message: "failed recording bluebubbles mutation sidecar",
1235
- meta: {
1236
- messageGuid: event.messageGuid,
1237
- mutationType: event.mutationType,
1238
- reason: error instanceof Error ? error.message : String(error),
1239
- },
1240
- });
1241
- }
1242
- }
1243
1249
  if (event.kind === "mutation" && !event.shouldNotifyAgent) {
1244
1250
  (0, runtime_1.emitNervesEvent)({
1245
1251
  component: "senses",
@@ -1252,52 +1258,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1252
1258
  });
1253
1259
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
1254
1260
  }
1255
- let ownsInFlightMessage = false;
1256
1261
  let activeTurnId = null;
1257
- const inFlightKey = event.kind === "message"
1258
- ? {
1259
- sessionKey: options.preclaimedInFlight?.sessionKey ?? event.chat.sessionKey,
1260
- messageGuid: options.preclaimedInFlight?.messageGuid ?? event.messageGuid,
1261
- }
1262
- : null;
1263
- if (event.kind === "message") {
1264
- if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
1265
- || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)) {
1266
- if (inFlightKey && options.preclaimedInFlight) {
1267
- endBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid);
1268
- }
1269
- (0, runtime_1.emitNervesEvent)({
1270
- component: "senses",
1271
- event: "senses.bluebubbles_recovery_skip",
1272
- message: "skipped bluebubbles message already marked as handled",
1273
- meta: {
1274
- messageGuid: event.messageGuid,
1275
- sessionKey: event.chat.sessionKey,
1276
- source,
1277
- dedupeReason: "processed",
1278
- },
1279
- });
1280
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
1281
- }
1282
- if (options.preclaimedInFlight) {
1283
- ownsInFlightMessage = true;
1284
- }
1285
- else if (!beginBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid)) {
1286
- (0, runtime_1.emitNervesEvent)({
1287
- component: "senses",
1288
- event: "senses.bluebubbles_recovery_skip",
1289
- message: "skipped bluebubbles message already in flight",
1290
- meta: {
1291
- messageGuid: event.messageGuid,
1292
- sessionKey: event.chat.sessionKey,
1293
- source,
1294
- dedupeReason: "in_flight",
1295
- },
1296
- });
1297
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
1298
- }
1299
- ownsInFlightMessage = true;
1300
- }
1301
1262
  if (event.kind === "message" && shouldUseBlueBubblesRecoveryBudget(source, options)) {
1302
1263
  const decision = (0, autonomy_budget_1.reserveAutonomyBudget)((0, identity_1.getAgentRoot)(agentName), {
1303
1264
  agent: agentName,
@@ -1310,8 +1271,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1310
1271
  storm: blueBubblesRecoveryStormInput(agentName, event, source),
1311
1272
  });
1312
1273
  if (!decision.allowed) {
1313
- endBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid);
1314
- ownsInFlightMessage = false;
1315
1274
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "autonomy_budget_blocked" };
1316
1275
  }
1317
1276
  }
@@ -1352,7 +1311,8 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1352
1311
  if (event.kind === "message") {
1353
1312
  // Record EARLY for audit and crash recovery. This is capture truth, not
1354
1313
  // a claim that the agent turn completed successfully.
1355
- const inboundSource = source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
1314
+ const inboundSource = (options.semanticRecovery || source !== "webhook")
1315
+ && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
1356
1316
  ? "recovery-bootstrap"
1357
1317
  : source;
1358
1318
  (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
@@ -1393,16 +1353,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1393
1353
  meta: { threadGuid, hasText: !!repliedToText },
1394
1354
  });
1395
1355
  }
1396
- // Anchor reaction mutations to the message they point at, including whose
1397
- // message it was. Unresolved targets keep the explicit "unidentified message"
1398
- // wording that normalizeBlueBubblesEvent already produced.
1399
- const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
1400
- if (isReaction && event.reaction && event.targetMessageGuid) {
1401
- /* v8 ignore start -- best-effort network lookup; resolveReactionTarget and renderBlueBubblesReactionText are covered by unit tests @preserve */
1402
- const target = await resolveReactionTarget(client, event.targetMessageGuid);
1403
- event.textForAgent = (0, model_1.renderBlueBubblesReactionText)(event.reaction, target);
1404
- /* v8 ignore stop */
1405
- }
1406
1356
  // Build inbound user message as user-visible speech only; source/routing facts live in the orientation frame.
1407
1357
  const priorMessages = existing?.messages ?? sessionMessages;
1408
1358
  const userMessage = {
@@ -1414,8 +1364,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1414
1364
  messages: [...priorMessages, userMessage],
1415
1365
  currentUserMessages: [userMessage],
1416
1366
  structuredOutputs: existing?.structuredOutputs ?? [],
1417
- source: buildBlueBubblesOrientationSource(event, priorMessages, repliedToText),
1418
- ...(isReaction ? { speechKind: "reaction" } : {}),
1367
+ source: buildBlueBubblesOrientationSource(event, priorMessages, options.orientationEvidence, options.orientationConversationKind, repliedToText),
1419
1368
  });
1420
1369
  const visibleActivityTurnId = activeTurnId;
1421
1370
  const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, visibleActivityTurnId
@@ -1463,10 +1412,6 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1463
1412
  timeoutReason = reason;
1464
1413
  recoveryTimedOut = true;
1465
1414
  callbacks.cancelOutbound("turn_timeout");
1466
- if (ownsInFlightMessage && inFlightKey) {
1467
- endBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid);
1468
- ownsInFlightMessage = false;
1469
- }
1470
1415
  controller.abort(reason);
1471
1416
  timeoutReject?.(reason);
1472
1417
  (0, runtime_1.emitNervesEvent)({
@@ -1509,7 +1454,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1509
1454
  pendingDir,
1510
1455
  friendStore: store,
1511
1456
  provider: "imessage-handle",
1512
- externalId: event.sender.externalId || event.sender.rawId,
1457
+ externalId: event.sender.externalId,
1513
1458
  isGroupChat: event.chat.isGroup,
1514
1459
  groupHasFamilyMember,
1515
1460
  hasExistingGroupWithFamily,
@@ -1560,7 +1505,15 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1560
1505
  },
1561
1506
  accumulateFriendTokens: resolvedDeps.accumulateFriendTokens,
1562
1507
  signal: controller.signal,
1563
- runAgentOptions: { mcpManager, orientationFrame, ...(isReaction ? { isReactionSignal: true } : {}) },
1508
+ runAgentOptions: {
1509
+ mcpManager,
1510
+ orientationFrame,
1511
+ toolContext: {
1512
+ signin: async () => undefined,
1513
+ agentRoot: (0, identity_1.getAgentRoot)(),
1514
+ currentIngressEvidence: options.currentIngressEvidence,
1515
+ },
1516
+ },
1564
1517
  callbacks: failoverAwareCallbacks,
1565
1518
  failoverState: (() => {
1566
1519
  if (!bbFailoverStates.has(event.chat.sessionKey)) {
@@ -1705,204 +1658,531 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1705
1658
  finally {
1706
1659
  if (activeTurnId)
1707
1660
  (0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, activeTurnId);
1708
- if (ownsInFlightMessage && inFlightKey) {
1709
- endBlueBubblesMessageInFlight(inFlightKey.sessionKey, inFlightKey.messageGuid);
1710
- }
1711
1661
  }
1712
1662
  }
1713
- async function handleBlueBubblesEvent(payload, deps = {}) {
1714
- const resolvedDeps = { ...defaultDeps, ...deps };
1715
- const client = resolvedDeps.createClient();
1716
- let normalized;
1663
+ function recordBlueBubblesAuditSidecar(agentName, event, source, resolvedDeps) {
1664
+ if (event.kind === "message") {
1665
+ if (event.fromMe !== true)
1666
+ (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, source);
1667
+ return;
1668
+ }
1717
1669
  try {
1718
- normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
1670
+ resolvedDeps.recordMutation(agentName, event);
1719
1671
  }
1720
1672
  catch (error) {
1721
- if (error instanceof model_1.BlueBubblesIgnoredEventError) {
1722
- (0, runtime_1.emitNervesEvent)({
1723
- component: "senses",
1724
- event: "senses.bluebubbles_event_skipped",
1725
- message: "skipped ignorable bluebubbles event",
1726
- meta: {
1727
- eventType: error.eventType,
1728
- },
1729
- });
1730
- return {
1731
- handled: true,
1732
- notifiedAgent: false,
1733
- reason: "ignored",
1734
- };
1735
- }
1736
- throw error;
1673
+ (0, runtime_1.emitNervesEvent)({
1674
+ level: "error",
1675
+ component: "senses",
1676
+ event: "senses.bluebubbles_mutation_log_error",
1677
+ message: "failed recording bluebubbles mutation sidecar",
1678
+ meta: {
1679
+ messageGuid: event.messageGuid,
1680
+ mutationType: event.mutationType,
1681
+ reason: error instanceof Error ? error.message : String(error),
1682
+ },
1683
+ });
1737
1684
  }
1738
- // Pre-repair dedup: if we've already processed this messageGuid, skip the
1739
- // repair+hydrate path entirely. Applies to BOTH `kind: "message"` AND
1740
- // `kind: "mutation"` events — BlueBubbles often sends a `new-message`
1741
- // webhook for a fresh message AND one or more follow-up `updated-message`
1742
- // webhooks for delivery/read status. The mutation path (inside repairEvent)
1743
- // can promote an updated-message back to a message if it has recoverable
1744
- // content, which then re-runs the full VLM-describe pipeline on the same
1745
- // attachment.
1746
- //
1747
- // Without this early check, we paid DOUBLE latency and double tokens on
1748
- // every image-bearing message. Verified live on 2026-04-08T00:58Z: two
1749
- // sequential VLM describes for attachment guid 317E37EB-..., 13.7s +
1750
- // 14.0s each, for the exact same 291KB JPEG — triggered by a sequence of
1751
- // `new-message` followed ~3s later by `updated-message` for the same guid.
1752
- //
1753
- // We still route the skip through `handleBlueBubblesNormalizedEvent` so
1754
- // the downstream `already_processed` path fires its observability events
1755
- // and the caller sees a consistent return shape.
1685
+ }
1686
+ function reactionTargetAuthorship(target) {
1687
+ return target.fromMe === true ? "agent" : "non_agent_unknown";
1688
+ }
1689
+ async function captureBlueBubblesSemanticEvent(normalized, resolvedDeps, source) {
1756
1690
  const agentName = resolvedDeps.getAgentName();
1757
- // normalizeBlueBubblesEvent rejects guidless payloads, so duplicate handling
1758
- // only needs to discriminate between known processed, in-flight, or new.
1759
- const duplicateReason = (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, normalized.chat.sessionKey, normalized.messageGuid)
1760
- || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, normalized.messageGuid)
1761
- ? "processed"
1762
- : isBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid)
1763
- ? "in_flight"
1764
- : null;
1765
- if (normalized.messageGuid
1766
- && duplicateReason) {
1691
+ if (!hasObservedBlueBubblesDirection(normalized)) {
1767
1692
  (0, runtime_1.emitNervesEvent)({
1768
1693
  level: "warn",
1769
1694
  component: "senses",
1770
- event: "senses.bluebubbles_repair_skipped_duplicate",
1771
- message: "skipped repair+hydrate for already-processed bluebubbles messageGuid",
1695
+ event: "senses.bluebubbles_direction_unobserved",
1696
+ message: "kept bluebubbles event audit-only because message direction was unobserved",
1772
1697
  meta: {
1773
1698
  messageGuid: normalized.messageGuid,
1774
- sessionKey: normalized.chat.sessionKey,
1775
- eventType: normalized.eventType,
1776
- normalizedKind: normalized.kind,
1777
- dedupeReason: duplicateReason,
1699
+ kind: normalized.kind,
1700
+ source,
1778
1701
  },
1779
1702
  });
1780
- return handleBlueBubblesNormalizedEvent(normalized, resolvedDeps, "webhook");
1781
- }
1782
- let preclaimedInFlight = false;
1783
- let handedPreclaimToTurn = false;
1784
- if (normalized.kind === "message") {
1785
- beginBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid);
1786
- preclaimedInFlight = true;
1787
- }
1788
- try {
1789
- const event = await client.repairEvent(normalized);
1790
- handedPreclaimToTurn = preclaimedInFlight && event.kind === "message";
1791
- return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook", handedPreclaimToTurn
1792
- ? { preclaimedInFlight: { sessionKey: normalized.chat.sessionKey, messageGuid: normalized.messageGuid } }
1793
- : {});
1794
- }
1795
- finally {
1796
- if (preclaimedInFlight && !handedPreclaimToTurn) {
1797
- endBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid);
1703
+ recordBlueBubblesAuditSidecar(agentName, normalized, source, resolvedDeps);
1704
+ return { status: "audit_only", normalized };
1705
+ }
1706
+ const observed = normalized;
1707
+ 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
+ let coordinateGeneration;
1717
+ if (observed.kind === "mutation"
1718
+ && observed.mutationType === "reaction"
1719
+ && observed.reaction
1720
+ && !observed.revision
1721
+ && !Number.isFinite(observed.effectiveTimestamp)
1722
+ && observed.sender.observed === true) {
1723
+ const identity = (0, semantic_receipts_1.buildBlueBubblesSemanticIdentity)({
1724
+ providerNamespace: cutover.providerNamespace,
1725
+ kind: "reaction",
1726
+ eventGuid: observed.messageGuid,
1727
+ targetGuid: observed.targetMessageGuid,
1728
+ actorExternalId: observed.sender.externalId,
1729
+ canonicalValue: observed.reaction.canonicalValue,
1730
+ canonicalAction: observed.reaction.action,
1731
+ coordinateGeneration: 0,
1732
+ });
1733
+ if (identity?.coordinateKey && identity.coordinateHash) {
1734
+ const coordinate = await (0, semantic_receipts_1.allocateBlueBubblesReactionCoordinate)(agentName, {
1735
+ coordinateKey: identity.coordinateKey,
1736
+ coordinateHash: identity.coordinateHash,
1737
+ canonicalAction: observed.reaction.action,
1738
+ });
1739
+ coordinateGeneration = coordinate.generation;
1798
1740
  }
1799
1741
  }
1800
- }
1801
- function listPendingCapturedInboundMessages(agentName) {
1802
- const seenMessageGuids = new Set();
1803
- return (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
1804
- .filter((entry) => {
1805
- if (seenMessageGuids.has(entry.messageGuid))
1806
- return false;
1807
- seenMessageGuids.add(entry.messageGuid);
1808
- return true;
1809
- })
1810
- .filter((entry) => {
1811
- if ((0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid))
1812
- return false;
1813
- return !markCapturedInboundSuperseded(agentName, entry);
1742
+ const capture = (0, semantic_receipts_1.buildBlueBubblesSemanticCapture)({
1743
+ cutover,
1744
+ capturedAt: new Date().toISOString(),
1745
+ event: observed,
1746
+ targetAuthorship,
1747
+ coordinateGeneration,
1814
1748
  });
1815
- }
1816
- function listPendingRecoveryEntries(agentName) {
1817
- const pendingByGuid = new Map();
1818
- const add = (messageGuid, recordedAt) => {
1819
- if ((0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, messageGuid))
1820
- return;
1821
- const previous = pendingByGuid.get(messageGuid);
1822
- if (!previous) {
1823
- pendingByGuid.set(messageGuid, recordedAt);
1824
- return;
1825
- }
1826
- const previousMs = Date.parse(previous);
1827
- const nextMs = Date.parse(recordedAt);
1828
- if (Number.isFinite(nextMs) && (!Number.isFinite(previousMs) || nextMs < previousMs)) {
1829
- pendingByGuid.set(messageGuid, recordedAt);
1830
- }
1831
- };
1832
- for (const entry of listPendingCapturedInboundMessages(agentName)) {
1833
- add(entry.messageGuid, entry.recordedAt);
1749
+ if (!capture) {
1750
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(observed, cutover);
1751
+ recordBlueBubblesAuditSidecar(agentName, observed, source, resolvedDeps);
1752
+ return { status: "audit_only", normalized: observed };
1834
1753
  }
1835
- for (const entry of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
1836
- add(entry.messageGuid, entry.recordedAt);
1754
+ const writeResult = (0, semantic_receipts_1.writeBlueBubblesSemanticCapture)(agentName, capture);
1755
+ if (writeResult === "semantic_identity_collision") {
1756
+ throw new Error("semantic_capture_failed");
1837
1757
  }
1838
- return [...pendingByGuid].map(([messageGuid, recordedAt]) => ({ messageGuid, recordedAt }));
1758
+ recordBlueBubblesAuditSidecar(agentName, observed, source, resolvedDeps);
1759
+ return {
1760
+ status: "captured",
1761
+ capture,
1762
+ normalized: observed,
1763
+ writeResult,
1764
+ ...(resolvedReactionTarget ? { resolvedReactionTarget } : {}),
1765
+ };
1839
1766
  }
1840
- function parseTimestampMs(value) {
1841
- if (!value)
1767
+ function semanticCaptureChat(capture) {
1768
+ const event = capture.event;
1769
+ const sessionKey = event.sessionKey?.trim();
1770
+ const chatGuid = event.chatGuid?.trim() || undefined;
1771
+ const chatIdentifier = event.chatIdentifier?.trim() || undefined;
1772
+ if (!sessionKey)
1842
1773
  return null;
1843
- const parsed = Date.parse(value);
1844
- return Number.isFinite(parsed) ? parsed : null;
1774
+ const fallbackSendTarget = sessionKey.startsWith("chat:")
1775
+ ? { kind: "chat_guid", value: sessionKey.slice("chat:".length).trim() }
1776
+ : sessionKey.startsWith("chat_identifier:")
1777
+ ? { kind: "chat_identifier", value: sessionKey.slice("chat_identifier:".length).trim() }
1778
+ : null;
1779
+ const sendTarget = chatGuid
1780
+ ? { kind: "chat_guid", value: chatGuid }
1781
+ : chatIdentifier
1782
+ ? { kind: "chat_identifier", value: chatIdentifier }
1783
+ : fallbackSendTarget;
1784
+ if (!sendTarget?.value)
1785
+ return null;
1786
+ return {
1787
+ chatGuid,
1788
+ chatIdentifier,
1789
+ isGroup: Boolean(chatGuid?.includes(";+;") || event.participants.length > 1),
1790
+ sessionKey,
1791
+ sendTarget,
1792
+ participantHandles: event.participants.map((participant) => participant.externalId),
1793
+ };
1794
+ }
1795
+ function semanticCaptureRequiresRoutingRepair(capture) {
1796
+ return !capture.event.chatGuid?.trim() && !capture.event.chatIdentifier?.trim();
1797
+ }
1798
+ function hasResolvedBlueBubblesRouting(event) {
1799
+ return Boolean(event.chat.chatGuid?.trim() || event.chat.chatIdentifier?.trim());
1800
+ }
1801
+ function isAuditOnlySemanticCapture(capture) {
1802
+ return capture.event.kind === "read" || capture.event.kind === "delivery";
1803
+ }
1804
+ function semanticCaptureToNormalizedEvent(capture) {
1805
+ const event = capture.event;
1806
+ const messageGuid = event.eventGuid?.trim();
1807
+ const chat = semanticCaptureChat(capture);
1808
+ if (!messageGuid || !chat)
1809
+ return null;
1810
+ const sender = {
1811
+ provider: "imessage-handle",
1812
+ externalId: event.actor.externalId,
1813
+ rawId: event.actor.externalId,
1814
+ displayName: event.actor.displayName ?? event.actor.externalId,
1815
+ observed: true,
1816
+ };
1817
+ const timestamp = Date.parse(event.effectiveAt ?? capture.capturedAt);
1818
+ if (event.kind === "message") {
1819
+ const text = event.text ?? "";
1820
+ return {
1821
+ kind: "message",
1822
+ eventType: event.sourceEventType,
1823
+ messageGuid,
1824
+ timestamp,
1825
+ fromMe: event.fromMe,
1826
+ sender,
1827
+ chat,
1828
+ text,
1829
+ textForAgent: text,
1830
+ attachments: [],
1831
+ hasPayloadData: false,
1832
+ requiresRepair: true,
1833
+ };
1834
+ }
1835
+ const shouldNotifyAgent = event.kind === "reaction" || event.kind === "edit" || event.kind === "unsend";
1836
+ const targetMessageGuid = event.targetGuid ?? undefined;
1837
+ const reaction = event.kind === "reaction" && event.canonicalAction && event.canonicalValue
1838
+ ? {
1839
+ ...(0, model_1.describeBlueBubblesReaction)(event.rawTransportValue ?? event.canonicalValue, event.rawTransportValue ?? event.canonicalValue),
1840
+ canonicalValue: event.canonicalValue,
1841
+ action: event.canonicalAction,
1842
+ }
1843
+ : undefined;
1844
+ const textForAgent = reaction
1845
+ ? (0, model_1.renderBlueBubblesReactionText)(reaction, { guid: targetMessageGuid })
1846
+ : event.kind === "edit"
1847
+ ? event.text ? `edited message: ${event.text}` : "edited a message"
1848
+ : event.kind === "unsend"
1849
+ ? "unsent a message"
1850
+ : event.kind === "read"
1851
+ ? "message marked as read"
1852
+ : "message marked as delivered";
1853
+ return {
1854
+ kind: "mutation",
1855
+ eventType: event.sourceEventType,
1856
+ mutationType: event.kind,
1857
+ messageGuid,
1858
+ targetMessageGuid,
1859
+ timestamp,
1860
+ fromMe: event.fromMe,
1861
+ sender,
1862
+ chat,
1863
+ shouldNotifyAgent,
1864
+ textForAgent,
1865
+ requiresRepair: true,
1866
+ ...(reaction ? { reaction } : {}),
1867
+ ...(event.kind === "edit" && event.text !== null ? { editedText: event.text } : {}),
1868
+ ...(event.revision ? { revision: event.revision } : {}),
1869
+ ...(event.effectiveAt ? { effectiveTimestamp: Date.parse(event.effectiveAt) } : {}),
1870
+ ...(event.kind === "unsend" && event.effectiveAt
1871
+ ? { retractionTimestamp: Date.parse(event.effectiveAt) }
1872
+ : {}),
1873
+ };
1845
1874
  }
1846
- function eventRecordedAtMs(event) {
1847
- return parseTimestampMs(event.time?.recordedAt ?? undefined);
1875
+ function semanticHandledOutcome(event, result) {
1876
+ if (result.reason === "from_me")
1877
+ return "ignored_self";
1878
+ if (event.kind === "message") {
1879
+ return result.notifiedAgent ? "message_completed" : "message_observed";
1880
+ }
1881
+ switch (event.mutationType) {
1882
+ case "edit": return "edit_capture_only";
1883
+ case "unsend": return "unsend_capture_only";
1884
+ case "read": return "read_audit_only";
1885
+ case "delivery": return "delivery_audit_only";
1886
+ case "reaction":
1887
+ if (result.reason === "restricted_feedback_settled")
1888
+ return "restricted_feedback_settled";
1889
+ if (result.reason === "restricted_feedback_observed")
1890
+ return "restricted_feedback_observed";
1891
+ return "restricted_feedback_failed";
1892
+ }
1893
+ }
1894
+ function materializeRestrictedBlueBubblesReactionEvent(captured, repaired) {
1895
+ // Classification reached `restricted_feedback` from the validated v1
1896
+ // capture's reaction tuple. Direct capture and recovery both construct the
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
+ };
1848
1910
  }
1849
- function blueBubblesSessionPathsForKey(agentName, sessionKey) {
1850
- const sessionsRoot = path.join((0, identity_1.getAgentRoot)(agentName), "state", "sessions");
1851
- const fileName = `${(0, config_1.sanitizeKey)(sessionKey)}.json`;
1852
- let friendDirs;
1911
+ async function classifyCapturedBlueBubblesReaction(captured, resolvedDeps) {
1912
+ const event = captured.capture.event;
1913
+ if (event.kind !== "reaction" || !event.canonicalAction || !event.canonicalValue)
1914
+ return null;
1915
+ const input = {
1916
+ fromMe: event.fromMe,
1917
+ action: event.canonicalAction,
1918
+ 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;
1853
1927
  try {
1854
- friendDirs = fs.readdirSync(sessionsRoot, { withFileTypes: true });
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;
1855
1932
  }
1856
- catch {
1857
- return [];
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
+ });
1858
1945
  }
1859
- return friendDirs
1860
- .filter((entry) => entry.isDirectory())
1861
- .map((entry) => path.join(sessionsRoot, entry.name, "bluebubbles", fileName))
1862
- .filter((filePath) => {
1863
- try {
1864
- return fs.statSync(filePath).isFile();
1946
+ return {
1947
+ decision: (0, reaction_policy_1.classifyBlueBubblesReaction)({ ...input, trustedActor }),
1948
+ trustedFriend,
1949
+ };
1950
+ }
1951
+ async function handleRestrictedBlueBubblesReactionFeedback(input) {
1952
+ const { event, trustedFriend, client, resolvedDeps, options } = input;
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();
1865
1982
  }
1866
- catch {
1867
- return false;
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
+ };
1868
2021
  }
2022
+ return {
2023
+ handled: true,
2024
+ notifiedAgent: false,
2025
+ kind: event.kind,
2026
+ reason: "restricted_feedback_failed",
2027
+ };
2028
+ }
2029
+ catch (error) {
2030
+ callbacks.onError(error instanceof Error ? error : new Error(String(error)), "terminal");
2031
+ return {
2032
+ handled: true,
2033
+ notifiedAgent: false,
2034
+ kind: event.kind,
2035
+ reason: "restricted_feedback_failed",
2036
+ };
2037
+ }
2038
+ finally {
2039
+ clearTimeout(timeoutTimer);
2040
+ await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
2041
+ }
2042
+ }
2043
+ function writeCapturedBlueBubblesHandled(agentName, capture, outcome) {
2044
+ const handledResult = (0, semantic_receipts_1.writeBlueBubblesSemanticHandled)(agentName, {
2045
+ schemaVersion: 1,
2046
+ canonicalKey: capture.canonicalKey,
2047
+ keyHash: capture.keyHash,
2048
+ handledAt: new Date().toISOString(),
2049
+ outcome,
2050
+ detailCode: null,
1869
2051
  });
2052
+ if (handledResult === "semantic_handled_collision") {
2053
+ throw new Error("semantic_handled_collision");
2054
+ }
1870
2055
  }
1871
- function hasNewerBlueBubblesSessionActivity(agentName, sessionKey, recordedAt) {
1872
- const recordedAtMs = parseTimestampMs(recordedAt);
1873
- if (recordedAtMs === null)
1874
- return false;
1875
- for (const sessionFilePath of blueBubblesSessionPathsForKey(agentName, sessionKey)) {
1876
- const session = (0, context_1.loadSession)(sessionFilePath);
1877
- for (const event of session?.events ?? []) {
1878
- if (event.role === "system")
1879
- continue;
1880
- const nextRecordedAtMs = eventRecordedAtMs(event);
1881
- if (nextRecordedAtMs !== null && nextRecordedAtMs > recordedAtMs) {
1882
- return true;
2056
+ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, source, options = {}) {
2057
+ const agentName = resolvedDeps.getAgentName();
2058
+ const identity = {
2059
+ canonicalKey: captured.capture.canonicalKey,
2060
+ keyHash: captured.capture.keyHash,
2061
+ };
2062
+ const claim = await (0, semantic_receipts_1.acquireBlueBubblesSemanticClaim)(agentName, identity);
2063
+ if (claim.status === "already_handled") {
2064
+ return {
2065
+ handled: true,
2066
+ notifiedAgent: false,
2067
+ kind: captured.normalized.kind,
2068
+ reason: "already_processed",
2069
+ };
2070
+ }
2071
+ if (claim.status === "timeout") {
2072
+ return {
2073
+ handled: false,
2074
+ notifiedAgent: false,
2075
+ kind: captured.normalized.kind,
2076
+ reason: "semantic_claim_timeout",
2077
+ };
2078
+ }
2079
+ try {
2080
+ const capturedOptions = {
2081
+ ...options,
2082
+ currentIngressEvidence: Object.freeze({
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);
2091
+ const reactionDecision = reactionClassification?.decision ?? null;
2092
+ if (reactionDecision?.route === "capture_only") {
2093
+ const result = {
2094
+ handled: true,
2095
+ notifiedAgent: false,
2096
+ kind: captured.normalized.kind,
2097
+ reason: reactionDecision.outcome === "ignored_self" ? "from_me" : "mutation_state_only",
2098
+ };
2099
+ writeCapturedBlueBubblesHandled(agentName, captured.capture, reactionDecision.outcome);
2100
+ return result;
2101
+ }
2102
+ let handledEvent = captured.normalized;
2103
+ let result;
2104
+ if (isAuditOnlySemanticCapture(captured.capture)) {
2105
+ result = {
2106
+ handled: true,
2107
+ notifiedAgent: false,
2108
+ kind: captured.normalized.kind,
2109
+ reason: "mutation_state_only",
2110
+ };
2111
+ }
2112
+ else {
2113
+ const client = resolvedDeps.createClient();
2114
+ const repaired = await client.repairEvent(captured.normalized);
2115
+ if (semanticCaptureRequiresRoutingRepair(captured.capture)
2116
+ && !hasResolvedBlueBubblesRouting(repaired)) {
2117
+ throw new Error("semantic_capture_routing_invalid");
2118
+ }
2119
+ const identityGroundedRepaired = {
2120
+ ...repaired,
2121
+ fromMe: captured.normalized.fromMe,
2122
+ sender: captured.normalized.sender,
2123
+ };
2124
+ handledEvent = identityGroundedRepaired;
2125
+ if (reactionDecision?.route === "restricted_feedback") {
2126
+ const restrictedEvent = materializeRestrictedBlueBubblesReactionEvent(captured, repaired);
2127
+ handledEvent = restrictedEvent;
2128
+ result = await handleRestrictedBlueBubblesReactionFeedback({
2129
+ event: restrictedEvent,
2130
+ trustedFriend: reactionClassification.trustedFriend,
2131
+ client,
2132
+ resolvedDeps,
2133
+ options: capturedOptions,
2134
+ resolvedReactionTarget: captured.resolvedReactionTarget ?? options.resolvedReactionTarget,
2135
+ });
2136
+ }
2137
+ else {
2138
+ result = await handleBlueBubblesNormalizedEvent(identityGroundedRepaired, resolvedDeps, source, {
2139
+ ...capturedOptions,
2140
+ resolvedReactionTarget: captured.resolvedReactionTarget ?? options.resolvedReactionTarget,
2141
+ });
1883
2142
  }
1884
2143
  }
2144
+ writeCapturedBlueBubblesHandled(agentName, captured.capture, semanticHandledOutcome(handledEvent, result));
2145
+ return result;
2146
+ }
2147
+ finally {
2148
+ (0, semantic_receipts_1.releaseBlueBubblesSemanticClaim)(agentName, claim);
1885
2149
  }
1886
- return false;
1887
2150
  }
1888
- function markCapturedInboundSuperseded(agentName, entry) {
1889
- if (!entry.messageGuid.trim())
1890
- return false;
1891
- if (!hasNewerBlueBubblesSessionActivity(agentName, entry.sessionKey, entry.recordedAt))
1892
- return false;
1893
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, inboundEntryToRecoveryEvent(entry), entry.source, "recovery-superseded");
1894
- (0, runtime_1.emitNervesEvent)({
1895
- component: "senses",
1896
- event: "senses.bluebubbles_recovery_skip",
1897
- message: "skipped stale captured bluebubbles recovery because the session already advanced",
1898
- meta: {
1899
- messageGuid: entry.messageGuid,
1900
- sessionKey: entry.sessionKey,
1901
- source: entry.source,
1902
- dedupeReason: "session_superseded",
1903
- },
1904
- });
1905
- return true;
2151
+ async function handleBlueBubblesEvent(payload, deps = {}) {
2152
+ const resolvedDeps = { ...defaultDeps, ...deps };
2153
+ let normalized;
2154
+ try {
2155
+ normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
2156
+ }
2157
+ catch (error) {
2158
+ if (error instanceof model_1.BlueBubblesIgnoredEventError) {
2159
+ (0, runtime_1.emitNervesEvent)({
2160
+ component: "senses",
2161
+ event: "senses.bluebubbles_event_skipped",
2162
+ message: "skipped ignorable bluebubbles event",
2163
+ meta: {
2164
+ eventType: error.eventType,
2165
+ },
2166
+ });
2167
+ return {
2168
+ handled: true,
2169
+ notifiedAgent: false,
2170
+ reason: "ignored",
2171
+ };
2172
+ }
2173
+ throw error;
2174
+ }
2175
+ const captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook");
2176
+ if (captured.status === "audit_only") {
2177
+ return { handled: true, notifiedAgent: false, kind: normalized.kind, reason: "ignored" };
2178
+ }
2179
+ return handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook");
2180
+ }
2181
+ function parseTimestampMs(value) {
2182
+ if (!value)
2183
+ return null;
2184
+ const parsed = Date.parse(value);
2185
+ return Number.isFinite(parsed) ? parsed : null;
1906
2186
  }
1907
2187
  function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
1908
2188
  if (previousState.upstreamStatus === "error") {
@@ -1927,9 +2207,9 @@ function formatBlueBubblesRuntimeDetail(queued, failed, active) {
1927
2207
  return "upstream reachable";
1928
2208
  }
1929
2209
  function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
1930
- const pendingEntries = listPendingRecoveryEntries(agentName);
2210
+ const pendingEntries = (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName);
1931
2211
  const pendingRecordedAt = pendingEntries
1932
- .map((entry) => entry.recordedAt)
2212
+ .map((entry) => entry.capturedAt)
1933
2213
  .map((value) => ({ value, ms: Date.parse(value) }))
1934
2214
  .filter((entry) => Number.isFinite(entry.ms))
1935
2215
  .sort((left, right) => left.ms - right.ms);
@@ -2048,7 +2328,8 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
2048
2328
  const client = resolvedDeps.createClient();
2049
2329
  const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
2050
2330
  const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
2051
- const catchUpSince = resolveBlueBubblesCatchUpSince(state);
2331
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2332
+ const catchUpSince = Math.max(resolveBlueBubblesCatchUpSince(state), Date.parse(cutover.effectiveAt));
2052
2333
  const processTurns = options.processTurns !== false;
2053
2334
  /* v8 ignore next -- older injected test doubles may omit the catch-up query method */
2054
2335
  if (!client.listRecentMessages)
@@ -2121,37 +2402,38 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
2121
2402
  for (const event of candidates) {
2122
2403
  result.inspected++;
2123
2404
  if (event.fromMe
2124
- || event.timestamp < catchUpSince
2125
- || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)
2126
- || isBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
2405
+ || event.timestamp < catchUpSince) {
2127
2406
  result.skipped++;
2128
2407
  continue;
2129
2408
  }
2130
- if (!processTurns) {
2131
- if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
2409
+ try {
2410
+ const captured = await captureBlueBubblesSemanticEvent(event, resolvedDeps, "upstream-catchup");
2411
+ if (captured.status === "audit_only") {
2132
2412
  result.skipped++;
2133
2413
  continue;
2134
2414
  }
2135
- (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, "upstream-catchup");
2136
- result.queued = (result.queued ?? 0) + 1;
2137
- continue;
2138
- }
2139
- try {
2140
- const repaired = await client.repairEvent(event);
2141
- if (repaired.kind !== "message") {
2142
- result.skipped++;
2415
+ if (!processTurns) {
2416
+ if (captured.writeResult === "semantic_capture_duplicate") {
2417
+ result.skipped++;
2418
+ }
2419
+ else {
2420
+ result.queued = (result.queued ?? 0) + 1;
2421
+ }
2143
2422
  continue;
2144
2423
  }
2145
- const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup", {
2424
+ const handled = await handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "upstream-catchup", {
2146
2425
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2147
2426
  autonomyBudgetTrigger: "recovery",
2148
2427
  });
2149
- if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
2428
+ if (handled.reason === "semantic_claim_timeout") {
2429
+ continue;
2430
+ }
2431
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
2150
2432
  result.skipped++;
2151
2433
  }
2152
2434
  else {
2153
2435
  result.recovered++;
2154
- result.lastRecoveredMessageGuid = repaired.messageGuid;
2436
+ result.lastRecoveredMessageGuid = event.messageGuid;
2155
2437
  }
2156
2438
  }
2157
2439
  catch (error) {
@@ -2179,72 +2461,43 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
2179
2461
  }
2180
2462
  return result;
2181
2463
  }
2182
- function inboundEntryToRecoveryEvent(entry) {
2183
- const chatIdentifier = entry.chatIdentifier ?? extractChatIdentifierFromSessionKey(entry.sessionKey) ?? "unknown";
2184
- const normalizedSessionKey = normalizeBlueBubblesSessionKey(entry.sessionKey);
2185
- const chatGuid = entry.chatGuid ?? (normalizedSessionKey.startsWith("chat:") ? normalizedSessionKey.slice("chat:".length) : undefined);
2186
- const isGroup = normalizedSessionKey.includes("+;");
2187
- return {
2188
- kind: "message",
2189
- eventType: "new-message",
2190
- messageGuid: entry.messageGuid,
2191
- timestamp: parseTimestampMs(entry.recordedAt) ?? Date.now(),
2192
- fromMe: false,
2193
- sender: {
2194
- provider: "imessage-handle",
2195
- externalId: chatIdentifier,
2196
- rawId: chatIdentifier,
2197
- displayName: chatIdentifier,
2198
- },
2199
- chat: {
2200
- chatGuid,
2201
- chatIdentifier,
2202
- isGroup,
2203
- sessionKey: normalizedSessionKey,
2204
- sendTarget: chatGuid
2205
- ? { kind: "chat_guid", value: chatGuid }
2206
- : { kind: "chat_identifier", value: chatIdentifier },
2207
- participantHandles: [],
2208
- },
2209
- text: entry.textForAgent,
2210
- textForAgent: entry.textForAgent,
2211
- attachments: [],
2212
- hasPayloadData: false,
2213
- requiresRepair: true,
2214
- };
2215
- }
2216
2464
  async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
2217
2465
  const resolvedDeps = { ...defaultDeps, ...deps };
2218
2466
  const agentName = resolvedDeps.getAgentName();
2219
- const client = resolvedDeps.createClient();
2467
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2220
2468
  const result = { recovered: 0, skipped: 0, failed: 0 };
2221
- const seenMessageGuids = new Set();
2222
- const candidates = (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
2223
- .filter((entry) => {
2224
- if (seenMessageGuids.has(entry.messageGuid))
2225
- return false;
2226
- seenMessageGuids.add(entry.messageGuid);
2227
- return true;
2228
- })
2229
- .sort((left, right) => (parseTimestampMs(left.recordedAt) ?? 0) - (parseTimestampMs(right.recordedAt) ?? 0));
2230
- for (const entry of candidates) {
2231
- if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid)
2232
- || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid)
2233
- || isBlueBubblesMessageInFlight(entry.sessionKey, entry.messageGuid)) {
2469
+ const seenKeyHashes = new Set();
2470
+ const candidates = (0, semantic_receipts_1.listPendingBlueBubblesSemanticCaptures)(agentName)
2471
+ .sort((left, right) => left.capturedAt.localeCompare(right.capturedAt));
2472
+ for (const capture of candidates) {
2473
+ if (seenKeyHashes.has(capture.keyHash)) {
2474
+ result.skipped++;
2475
+ continue;
2476
+ }
2477
+ seenKeyHashes.add(capture.keyHash);
2478
+ const disposition = (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(capture, cutover);
2479
+ if (disposition.disposition === "audit_only" && disposition.reason !== "audit_event") {
2234
2480
  result.skipped++;
2235
2481
  continue;
2236
2482
  }
2483
+ const normalized = semanticCaptureToNormalizedEvent(capture);
2237
2484
  try {
2238
- const repaired = await client.repairEvent(inboundEntryToRecoveryEvent(entry));
2239
- if (repaired.kind !== "message") {
2240
- result.skipped++;
2241
- continue;
2242
- }
2243
- const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source, {
2485
+ if (!normalized)
2486
+ throw new Error("semantic_capture_routing_invalid");
2487
+ const handled = await handleCapturedBlueBubblesSemanticEvent({
2488
+ status: "captured",
2489
+ capture,
2490
+ normalized,
2491
+ writeResult: "semantic_capture_duplicate",
2492
+ }, resolvedDeps, "webhook", {
2244
2493
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2245
2494
  autonomyBudgetTrigger: "recovery",
2495
+ semanticRecovery: true,
2246
2496
  });
2247
- if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
2497
+ if (handled.reason === "semantic_claim_timeout") {
2498
+ continue;
2499
+ }
2500
+ if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason) || !handled.notifiedAgent) {
2248
2501
  result.skipped++;
2249
2502
  }
2250
2503
  else {
@@ -2252,7 +2505,8 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
2252
2505
  }
2253
2506
  }
2254
2507
  catch (error) {
2255
- recordBlueBubblesRecoveryFailureForBudget(agentName, inboundEntryToRecoveryEvent(entry), entry.source);
2508
+ if (normalized)
2509
+ recordBlueBubblesRecoveryFailureForBudget(agentName, normalized, "webhook");
2256
2510
  result.failed++;
2257
2511
  (0, runtime_1.emitNervesEvent)({
2258
2512
  level: "warn",
@@ -2260,58 +2514,33 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
2260
2514
  event: "senses.bluebubbles_capture_recovery_error",
2261
2515
  message: "captured bluebubbles message recovery failed",
2262
2516
  meta: {
2263
- messageGuid: entry.messageGuid,
2264
- sessionKey: entry.sessionKey,
2517
+ messageGuid: capture.event.eventGuid,
2518
+ sessionKey: capture.event.sessionKey,
2265
2519
  reason: error instanceof Error ? error.message : String(error),
2266
2520
  },
2267
2521
  });
2268
2522
  }
2269
2523
  }
2524
+ const semanticEventGuids = new Set(candidates
2525
+ .map((capture) => capture.event.eventGuid)
2526
+ .filter((eventGuid) => typeof eventGuid === "string")
2527
+ .map((eventGuid) => eventGuid.trim().toLowerCase()));
2528
+ for (const legacyEntry of (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)) {
2529
+ if (semanticEventGuids.has(legacyEntry.messageGuid.trim().toLowerCase()))
2530
+ continue;
2531
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(legacyEntry, cutover);
2532
+ result.skipped++;
2533
+ }
2270
2534
  return result;
2271
2535
  }
2272
2536
  async function recoverMissedBlueBubblesMessages(deps = {}) {
2273
2537
  const resolvedDeps = { ...defaultDeps, ...deps };
2274
2538
  const agentName = resolvedDeps.getAgentName();
2275
- const client = resolvedDeps.createClient();
2539
+ const cutover = (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(agentName);
2276
2540
  const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
2277
2541
  for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
2278
- if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, candidate.sessionKey, candidate.messageGuid)
2279
- || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, candidate.messageGuid)
2280
- || isBlueBubblesMessageInFlight(candidate.sessionKey, candidate.messageGuid)) {
2281
- result.skipped++;
2282
- continue;
2283
- }
2284
- try {
2285
- const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
2286
- if (repaired.kind !== "message") {
2287
- result.pending++;
2288
- continue;
2289
- }
2290
- const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery", {
2291
- timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
2292
- autonomyBudgetTrigger: "recovery",
2293
- });
2294
- if (shouldCountBlueBubblesRecoveryResultAsSkipped(handled.reason)) {
2295
- result.skipped++;
2296
- }
2297
- else {
2298
- result.recovered++;
2299
- }
2300
- }
2301
- catch (error) {
2302
- recordBlueBubblesRecoveryFailureForBudget(agentName, mutationEntryToEvent(candidate), "mutation-recovery");
2303
- result.failed++;
2304
- (0, runtime_1.emitNervesEvent)({
2305
- level: "warn",
2306
- component: "senses",
2307
- event: "senses.bluebubbles_recovery_error",
2308
- message: "bluebubbles backlog recovery failed",
2309
- meta: {
2310
- messageGuid: candidate.messageGuid,
2311
- reason: error instanceof Error ? error.message : String(error),
2312
- },
2313
- });
2314
- }
2542
+ (0, semantic_receipts_1.classifyBlueBubblesRecoveryRecord)(candidate, cutover);
2543
+ result.skipped++;
2315
2544
  }
2316
2545
  if (result.recovered > 0 || result.skipped > 0 || result.pending > 0 || result.failed > 0) {
2317
2546
  (0, runtime_1.emitNervesEvent)({
@@ -2404,26 +2633,33 @@ function createBlueBubblesWebhookHandler(deps = {}) {
2404
2633
  return;
2405
2634
  }
2406
2635
  const resolvedDeps = { ...defaultDeps, ...deps };
2407
- if (normalized.kind === "message" && !normalized.fromMe) {
2408
- (0, inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), normalized, "webhook");
2636
+ let captured;
2637
+ try {
2638
+ captured = await captureBlueBubblesSemanticEvent(normalized, resolvedDeps, "webhook");
2409
2639
  }
2410
- else if (normalized.kind === "mutation") {
2411
- try {
2412
- resolvedDeps.recordMutation(resolvedDeps.getAgentName(), normalized);
2413
- }
2414
- catch (error) {
2415
- (0, runtime_1.emitNervesEvent)({
2416
- level: "error",
2417
- component: "senses",
2418
- event: "senses.bluebubbles_mutation_log_error",
2419
- message: "failed recording bluebubbles mutation sidecar",
2420
- meta: {
2421
- messageGuid: normalized.messageGuid,
2422
- mutationType: normalized.mutationType,
2423
- reason: error instanceof Error ? error.message : String(error),
2424
- },
2425
- });
2426
- }
2640
+ catch (error) {
2641
+ (0, runtime_1.emitNervesEvent)({
2642
+ level: "error",
2643
+ component: "senses",
2644
+ event: "senses.bluebubbles_webhook_error",
2645
+ message: "bluebubbles semantic capture failed before acknowledgement",
2646
+ meta: {
2647
+ messageGuid: normalized.messageGuid,
2648
+ reason: error instanceof Error ? error.message : String(error),
2649
+ },
2650
+ });
2651
+ writeSemanticCaptureFailure(res);
2652
+ return;
2653
+ }
2654
+ if (captured.status === "audit_only") {
2655
+ writeJson(res, 200, {
2656
+ handled: true,
2657
+ notifiedAgent: false,
2658
+ kind: normalized.kind,
2659
+ queued: false,
2660
+ reason: "ignored",
2661
+ });
2662
+ return;
2427
2663
  }
2428
2664
  writeJson(res, 200, {
2429
2665
  handled: true,
@@ -2433,7 +2669,7 @@ function createBlueBubblesWebhookHandler(deps = {}) {
2433
2669
  reason: "queued",
2434
2670
  });
2435
2671
  setTimeout(() => {
2436
- void handleBlueBubblesEvent(payload, deps).catch((error) => {
2672
+ void handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, "webhook").catch((error) => {
2437
2673
  (0, runtime_1.emitNervesEvent)({
2438
2674
  level: "error",
2439
2675
  component: "senses",
@@ -2852,6 +3088,7 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
2852
3088
  }
2853
3089
  function startBlueBubblesApp(deps = {}) {
2854
3090
  const resolvedDeps = { ...defaultDeps, ...deps };
3091
+ (0, semantic_receipts_1.initializeBlueBubblesSemanticCutover)(resolvedDeps.getAgentName());
2855
3092
  resolvedDeps.createClient();
2856
3093
  const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
2857
3094
  const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps));