@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
|
|
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.
|
|
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
|
|
1282
|
-
|
|
1283
|
-
|
|
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:
|
|
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.
|
|
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({
|