@ouro.bot/cli 0.1.0-alpha.540 → 0.1.0-alpha.542

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.542",
6
+ "changes": [
7
+ "BlueBubbles recovery timeouts now leave iMessage messages pending instead of marking them processed, and legacy `recovery-timeout` processed records no longer suppress retries.",
8
+ "Recovery turns now get a longer bounded timeout and keep their in-flight guard until the abandoned turn settles, preventing duplicate retries while preserving truthful pending recovery state.",
9
+ "iMessage recovery health now counts pending work by message GUID across captured inbound and mutation sidecars, preventing one missed message from being reported as multiple queued items."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.541",
14
+ "changes": [
15
+ "BlueBubbles recovery now treats message GUID completion as authoritative across repaired session keys, so stale `chat_identifier:unknown` mutation/capture sidecars stop keeping iMessage health in a false pending-recovery state after the real chat turn has completed.",
16
+ "Adds regression coverage for recovered captured-inbound and mutation backlog records whose original degraded session key differs from the canonical processed chat key."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.540",
6
21
  "changes": [
@@ -170,7 +170,7 @@ const defaultDeps = {
170
170
  const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
171
171
  const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
172
172
  const BLUEBUBBLES_RECOVERY_PASS_INTERVAL_MS = 30_000;
173
- const BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS = 60_000;
173
+ const BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS = 10 * 60_000;
174
174
  const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
175
175
  const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
176
176
  const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
@@ -182,10 +182,6 @@ class BlueBubblesRecoveryTurnTimeoutError extends Error {
182
182
  this.name = "BlueBubblesRecoveryTurnTimeoutError";
183
183
  }
184
184
  }
185
- function isBlueBubblesRecoveryTurnTimeoutError(error) {
186
- return error instanceof BlueBubblesRecoveryTurnTimeoutError
187
- || (error instanceof Error && error.name === "BlueBubblesRecoveryTurnTimeoutError");
188
- }
189
185
  function resolveFriendParams(event) {
190
186
  if (event.chat.isGroup) {
191
187
  const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
@@ -827,8 +823,10 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
827
823
  return { handled: true, notifiedAgent: false, kind: event.kind, reason: "mutation_state_only" };
828
824
  }
829
825
  let ownsInFlightMessage = false;
826
+ let releaseInFlightAfterTurnSettles = false;
830
827
  if (event.kind === "message") {
831
- if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)) {
828
+ if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
829
+ || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)) {
832
830
  (0, runtime_1.emitNervesEvent)({
833
831
  component: "senses",
834
832
  event: "senses.bluebubbles_recovery_skip",
@@ -988,6 +986,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
988
986
  timeoutTimer = setTimeout(() => {
989
987
  const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
990
988
  recoveryTimedOut = true;
989
+ releaseInFlightAfterTurnSettles = true;
991
990
  controller.abort(reason);
992
991
  timeoutReject?.(reason);
993
992
  (0, runtime_1.emitNervesEvent)({
@@ -1071,7 +1070,8 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1071
1070
  });
1072
1071
  /* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
1073
1072
  if (timeoutPromise) {
1074
- void turnPromise.catch((error) => {
1073
+ void turnPromise
1074
+ .catch((error) => {
1075
1075
  if (!recoveryTimedOut)
1076
1076
  return;
1077
1077
  (0, runtime_1.emitNervesEvent)({
@@ -1086,6 +1086,11 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1086
1086
  reason: error instanceof Error ? error.message : String(error),
1087
1087
  },
1088
1088
  });
1089
+ })
1090
+ .finally(() => {
1091
+ if (releaseInFlightAfterTurnSettles && ownsInFlightMessage && event.kind === "message") {
1092
+ endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
1093
+ }
1089
1094
  });
1090
1095
  }
1091
1096
  /* v8 ignore stop */
@@ -1159,7 +1164,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1159
1164
  });
1160
1165
  }
1161
1166
  finally {
1162
- if (ownsInFlightMessage && event.kind === "message") {
1167
+ if (ownsInFlightMessage && event.kind === "message" && !releaseInFlightAfterTurnSettles) {
1163
1168
  endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
1164
1169
  }
1165
1170
  }
@@ -1211,6 +1216,7 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
1211
1216
  // normalizeBlueBubblesEvent rejects guidless payloads, so duplicate handling
1212
1217
  // only needs to discriminate between known processed, in-flight, or new.
1213
1218
  const duplicateReason = (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, normalized.chat.sessionKey, normalized.messageGuid)
1219
+ || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, normalized.messageGuid)
1214
1220
  ? "processed"
1215
1221
  : isBlueBubblesMessageInFlight(normalized.chat.sessionKey, normalized.messageGuid)
1216
1222
  ? "in_flight"
@@ -1235,14 +1241,6 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
1235
1241
  const event = await client.repairEvent(normalized);
1236
1242
  return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
1237
1243
  }
1238
- function countPendingRecoveryCandidates(agentName) {
1239
- return (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
1240
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid))
1241
- .length;
1242
- }
1243
- function countPendingCapturedInboundMessages(agentName) {
1244
- return listPendingCapturedInboundMessages(agentName).length;
1245
- }
1246
1244
  function listPendingCapturedInboundMessages(agentName) {
1247
1245
  const seenMessageGuids = new Set();
1248
1246
  return (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
@@ -1252,7 +1250,31 @@ function listPendingCapturedInboundMessages(agentName) {
1252
1250
  seenMessageGuids.add(entry.messageGuid);
1253
1251
  return true;
1254
1252
  })
1255
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid));
1253
+ .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid));
1254
+ }
1255
+ function listPendingRecoveryEntries(agentName) {
1256
+ const pendingByGuid = new Map();
1257
+ const add = (messageGuid, recordedAt) => {
1258
+ if ((0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, messageGuid))
1259
+ return;
1260
+ const previous = pendingByGuid.get(messageGuid);
1261
+ if (!previous) {
1262
+ pendingByGuid.set(messageGuid, recordedAt);
1263
+ return;
1264
+ }
1265
+ const previousMs = Date.parse(previous);
1266
+ const nextMs = Date.parse(recordedAt);
1267
+ if (Number.isFinite(nextMs) && (!Number.isFinite(previousMs) || nextMs < previousMs)) {
1268
+ pendingByGuid.set(messageGuid, recordedAt);
1269
+ }
1270
+ };
1271
+ for (const entry of listPendingCapturedInboundMessages(agentName)) {
1272
+ add(entry.messageGuid, entry.recordedAt);
1273
+ }
1274
+ for (const entry of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
1275
+ add(entry.messageGuid, entry.recordedAt);
1276
+ }
1277
+ return [...pendingByGuid].map(([messageGuid, recordedAt]) => ({ messageGuid, recordedAt }));
1256
1278
  }
1257
1279
  function parseTimestampMs(value) {
1258
1280
  if (!value)
@@ -1278,18 +1300,15 @@ function formatBlueBubblesRuntimeDetail(queued, failed) {
1278
1300
  return "upstream reachable";
1279
1301
  }
1280
1302
  function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
1281
- const pendingRecordedAt = [
1282
- ...listPendingCapturedInboundMessages(agentName).map((entry) => entry.recordedAt),
1283
- ...(0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
1284
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid))
1285
- .map((entry) => entry.recordedAt),
1286
- ]
1303
+ const pendingEntries = listPendingRecoveryEntries(agentName);
1304
+ const pendingRecordedAt = pendingEntries
1305
+ .map((entry) => entry.recordedAt)
1287
1306
  .map((value) => ({ value, ms: Date.parse(value) }))
1288
1307
  .filter((entry) => Number.isFinite(entry.ms))
1289
1308
  .sort((left, right) => left.ms - right.ms);
1290
1309
  const oldest = pendingRecordedAt[0];
1291
1310
  return {
1292
- pendingRecoveryCount: countPendingCapturedInboundMessages(agentName) + countPendingRecoveryCandidates(agentName),
1311
+ pendingRecoveryCount: pendingEntries.length,
1293
1312
  oldestPendingRecoveryAt: oldest?.value,
1294
1313
  oldestPendingRecoveryAgeMs: oldest ? Math.max(0, nowMs - oldest.ms) : undefined,
1295
1314
  };
@@ -1468,7 +1487,7 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1468
1487
  result.inspected++;
1469
1488
  if (event.fromMe
1470
1489
  || event.timestamp < catchUpSince
1471
- || (0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
1490
+ || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)
1472
1491
  || isBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid)) {
1473
1492
  result.skipped++;
1474
1493
  continue;
@@ -1482,14 +1501,12 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1482
1501
  result.queued = (result.queued ?? 0) + 1;
1483
1502
  continue;
1484
1503
  }
1485
- let repairedMessage = null;
1486
1504
  try {
1487
1505
  const repaired = await client.repairEvent(event);
1488
1506
  if (repaired.kind !== "message") {
1489
1507
  result.skipped++;
1490
1508
  continue;
1491
1509
  }
1492
- repairedMessage = repaired;
1493
1510
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup", {
1494
1511
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1495
1512
  });
@@ -1503,9 +1520,6 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1503
1520
  }
1504
1521
  catch (error) {
1505
1522
  result.failed++;
1506
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1507
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, "upstream-catchup", "recovery-timeout");
1508
- }
1509
1523
  (0, runtime_1.emitNervesEvent)({
1510
1524
  level: "warn",
1511
1525
  component: "senses",
@@ -1578,18 +1592,17 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
1578
1592
  .sort((left, right) => (parseTimestampMs(left.recordedAt) ?? 0) - (parseTimestampMs(right.recordedAt) ?? 0));
1579
1593
  for (const entry of candidates) {
1580
1594
  if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid)
1595
+ || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid)
1581
1596
  || isBlueBubblesMessageInFlight(entry.sessionKey, entry.messageGuid)) {
1582
1597
  result.skipped++;
1583
1598
  continue;
1584
1599
  }
1585
- let repairedMessage = null;
1586
1600
  try {
1587
1601
  const repaired = await client.repairEvent(inboundEntryToRecoveryEvent(entry));
1588
1602
  if (repaired.kind !== "message") {
1589
1603
  result.skipped++;
1590
1604
  continue;
1591
1605
  }
1592
- repairedMessage = repaired;
1593
1606
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source, {
1594
1607
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1595
1608
  });
@@ -1602,9 +1615,6 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
1602
1615
  }
1603
1616
  catch (error) {
1604
1617
  result.failed++;
1605
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1606
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, entry.source, "recovery-timeout");
1607
- }
1608
1618
  (0, runtime_1.emitNervesEvent)({
1609
1619
  level: "warn",
1610
1620
  component: "senses",
@@ -1627,18 +1637,17 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
1627
1637
  const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
1628
1638
  for (const candidate of (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
1629
1639
  if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, candidate.sessionKey, candidate.messageGuid)
1640
+ || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, candidate.messageGuid)
1630
1641
  || isBlueBubblesMessageInFlight(candidate.sessionKey, candidate.messageGuid)) {
1631
1642
  result.skipped++;
1632
1643
  continue;
1633
1644
  }
1634
- let repairedMessage = null;
1635
1645
  try {
1636
1646
  const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
1637
1647
  if (repaired.kind !== "message") {
1638
1648
  result.pending++;
1639
1649
  continue;
1640
1650
  }
1641
- repairedMessage = repaired;
1642
1651
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery", {
1643
1652
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1644
1653
  });
@@ -1651,9 +1660,6 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
1651
1660
  }
1652
1661
  catch (error) {
1653
1662
  result.failed++;
1654
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1655
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, "mutation-recovery", "recovery-timeout");
1656
- }
1657
1663
  (0, runtime_1.emitNervesEvent)({
1658
1664
  level: "warn",
1659
1665
  component: "senses",
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getBlueBubblesProcessedLogPath = getBlueBubblesProcessedLogPath;
37
37
  exports.hasProcessedBlueBubblesMessage = hasProcessedBlueBubblesMessage;
38
+ exports.hasProcessedBlueBubblesMessageGuid = hasProcessedBlueBubblesMessageGuid;
38
39
  exports.recordProcessedBlueBubblesMessage = recordProcessedBlueBubblesMessage;
39
40
  const fs = __importStar(require("node:fs"));
40
41
  const path = __importStar(require("node:path"));
@@ -58,17 +59,38 @@ function readEntries(filePath) {
58
59
  return [];
59
60
  }
60
61
  }
62
+ function readAllEntries(agentName) {
63
+ const processedDir = path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "processed");
64
+ try {
65
+ return fs.readdirSync(processedDir)
66
+ .filter((name) => name.endsWith(".ndjson"))
67
+ .sort()
68
+ .flatMap((name) => readEntries(path.join(processedDir, name)));
69
+ }
70
+ catch {
71
+ return [];
72
+ }
73
+ }
74
+ function isCompletedProcessedEntry(entry) {
75
+ return entry.outcome !== "recovery-timeout";
76
+ }
61
77
  function hasProcessedBlueBubblesMessage(agentName, sessionKey, messageGuid) {
62
78
  if (!messageGuid.trim())
63
79
  return false;
64
80
  const filePath = getBlueBubblesProcessedLogPath(agentName, sessionKey);
65
- return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid);
81
+ return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid && isCompletedProcessedEntry(entry));
82
+ }
83
+ function hasProcessedBlueBubblesMessageGuid(agentName, messageGuid) {
84
+ if (!messageGuid.trim())
85
+ return false;
86
+ return readAllEntries(agentName).some((entry) => entry.messageGuid === messageGuid && isCompletedProcessedEntry(entry));
66
87
  }
67
88
  function recordProcessedBlueBubblesMessage(agentName, event, source, outcome) {
68
89
  const filePath = getBlueBubblesProcessedLogPath(agentName, event.chat.sessionKey);
69
90
  try {
70
91
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
71
- if (event.messageGuid.trim() && readEntries(filePath).some((entry) => entry.messageGuid === event.messageGuid)) {
92
+ if (event.messageGuid.trim() && readEntries(filePath).some((entry) => (entry.messageGuid === event.messageGuid
93
+ && isCompletedProcessedEntry(entry)))) {
72
94
  return filePath;
73
95
  }
74
96
  fs.appendFileSync(filePath, JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.540",
3
+ "version": "0.1.0-alpha.542",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",