@ouro.bot/cli 0.1.0-alpha.322 → 0.1.0-alpha.323
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,13 @@
|
|
|
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.323",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(bluebubbles): drain missed upstream BlueBubbles messages after an outage by querying recent messages when the upstream health probe recovers, deduping against the inbound sidecar, repairing candidates, and replaying them through the normal BlueBubbles turn path oldest-first.",
|
|
8
|
+
"fix(bluebubbles): make upstream catch-up bounded, paginated, and observable with nerves events for query start/end/skip/error, catch-up start/complete/error, runtime status updates, and explicit failure state if the bounded page limit is reached before the outage window is drained."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.322",
|
|
6
13
|
"changes": [
|
|
@@ -58,6 +58,9 @@ async function parseJsonBody(response) {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
function describeCaughtValue(error) {
|
|
62
|
+
return error instanceof Error ? error.message : String(error);
|
|
63
|
+
}
|
|
61
64
|
function buildRepairUrl(baseUrl, messageGuid, password) {
|
|
62
65
|
const url = buildBlueBubblesApiUrl(baseUrl, `/api/v1/message/${encodeURIComponent(messageGuid)}`, password);
|
|
63
66
|
const parsed = new URL(url);
|
|
@@ -102,6 +105,17 @@ function extractChatQueryRows(payload) {
|
|
|
102
105
|
}
|
|
103
106
|
return data.map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
104
107
|
}
|
|
108
|
+
function extractMessageQueryRows(payload) {
|
|
109
|
+
const record = asRecord(payload);
|
|
110
|
+
const data = asRecord(record?.data);
|
|
111
|
+
const rows = Array.isArray(record?.data) ? record.data
|
|
112
|
+
: Array.isArray(data?.messages) ? data.messages
|
|
113
|
+
: Array.isArray(data?.results) ? data.results
|
|
114
|
+
: Array.isArray(record?.messages) ? record.messages
|
|
115
|
+
: Array.isArray(payload) ? payload
|
|
116
|
+
: [];
|
|
117
|
+
return rows.map((entry) => asRecord(entry)).filter((entry) => entry !== null);
|
|
118
|
+
}
|
|
105
119
|
async function resolveChatGuidForIdentifier(config, channelConfig, chatIdentifier) {
|
|
106
120
|
const trimmedIdentifier = chatIdentifier.trim();
|
|
107
121
|
if (!trimmedIdentifier)
|
|
@@ -368,6 +382,71 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
368
382
|
meta: { serverUrl: config.serverUrl },
|
|
369
383
|
});
|
|
370
384
|
},
|
|
385
|
+
async listRecentMessages(params = {}) {
|
|
386
|
+
const limit = Math.max(1, Math.min(100, Math.floor(params.limit ?? 50)));
|
|
387
|
+
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
388
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/query", config.password);
|
|
389
|
+
(0, runtime_1.emitNervesEvent)({
|
|
390
|
+
component: "senses",
|
|
391
|
+
event: "senses.bluebubbles_query_recent_start",
|
|
392
|
+
message: "querying recent bluebubbles messages",
|
|
393
|
+
meta: { limit, offset },
|
|
394
|
+
});
|
|
395
|
+
const response = await fetch(url, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "Content-Type": "application/json" },
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
limit,
|
|
400
|
+
offset,
|
|
401
|
+
sort: "DESC",
|
|
402
|
+
with: ["chats", "attachments", "payloadData", "messageSummaryInfo"],
|
|
403
|
+
}),
|
|
404
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
405
|
+
});
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
const errorText = await response.text().catch(() => "");
|
|
408
|
+
(0, runtime_1.emitNervesEvent)({
|
|
409
|
+
level: "warn",
|
|
410
|
+
component: "senses",
|
|
411
|
+
event: "senses.bluebubbles_query_recent_error",
|
|
412
|
+
message: "bluebubbles recent message query failed",
|
|
413
|
+
meta: {
|
|
414
|
+
status: response.status,
|
|
415
|
+
reason: errorText || "unknown",
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
throw new Error(`BlueBubbles recent message query failed (${response.status}): ${errorText || "unknown"}`);
|
|
419
|
+
}
|
|
420
|
+
const payload = await parseJsonBody(response);
|
|
421
|
+
const rows = extractMessageQueryRows(payload);
|
|
422
|
+
const messages = [];
|
|
423
|
+
for (const row of rows) {
|
|
424
|
+
try {
|
|
425
|
+
messages.push((0, model_1.normalizeBlueBubblesEvent)({ type: "new-message", data: row }));
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
(0, runtime_1.emitNervesEvent)({
|
|
429
|
+
level: "warn",
|
|
430
|
+
component: "senses",
|
|
431
|
+
event: "senses.bluebubbles_query_recent_skip",
|
|
432
|
+
message: "skipped unusable bluebubbles recent message row",
|
|
433
|
+
meta: {
|
|
434
|
+
reason: describeCaughtValue(error),
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
(0, runtime_1.emitNervesEvent)({
|
|
440
|
+
component: "senses",
|
|
441
|
+
event: "senses.bluebubbles_query_recent_end",
|
|
442
|
+
message: "queried recent bluebubbles messages",
|
|
443
|
+
meta: {
|
|
444
|
+
rows: rows.length,
|
|
445
|
+
normalized: messages.length,
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
return messages;
|
|
449
|
+
},
|
|
371
450
|
async repairEvent(event) {
|
|
372
451
|
if (!event.requiresRepair) {
|
|
373
452
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.enrichReactionText = enrichReactionText;
|
|
37
37
|
exports.createStatusBatcher = createStatusBatcher;
|
|
38
38
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
39
|
+
exports.catchUpMissedBlueBubblesMessages = catchUpMissedBlueBubblesMessages;
|
|
39
40
|
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
40
41
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
41
42
|
exports.sendProactiveBlueBubblesMessageToSession = sendProactiveBlueBubblesMessageToSession;
|
|
@@ -135,6 +136,11 @@ const defaultDeps = {
|
|
|
135
136
|
createServer: http.createServer,
|
|
136
137
|
};
|
|
137
138
|
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
139
|
+
const BLUEBUBBLES_CATCHUP_PAGE_SIZE = 50;
|
|
140
|
+
const BLUEBUBBLES_CATCHUP_MAX_PAGES = 20;
|
|
141
|
+
const BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS = 90_000;
|
|
142
|
+
const BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS = 24 * 60 * 60 * 1000;
|
|
143
|
+
const BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS = 10 * 60 * 1000;
|
|
138
144
|
function resolveFriendParams(event) {
|
|
139
145
|
if (event.chat.isGroup) {
|
|
140
146
|
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
@@ -921,24 +927,50 @@ function countPendingRecoveryCandidates(agentName) {
|
|
|
921
927
|
.filter((entry) => !(0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, entry.sessionKey, entry.messageGuid))
|
|
922
928
|
.length;
|
|
923
929
|
}
|
|
930
|
+
function parseTimestampMs(value) {
|
|
931
|
+
if (!value)
|
|
932
|
+
return null;
|
|
933
|
+
const parsed = Date.parse(value);
|
|
934
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
935
|
+
}
|
|
936
|
+
function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
|
|
937
|
+
if (previousState.upstreamStatus === "error") {
|
|
938
|
+
return nowMs - BLUEBUBBLES_RECOVERY_CATCHUP_LOOKBACK_MS;
|
|
939
|
+
}
|
|
940
|
+
const lastCheckedAt = parseTimestampMs(previousState.lastCheckedAt);
|
|
941
|
+
if (lastCheckedAt !== null) {
|
|
942
|
+
return Math.max(0, lastCheckedAt - BLUEBUBBLES_HEALTHY_CATCHUP_OVERLAP_MS);
|
|
943
|
+
}
|
|
944
|
+
return nowMs - BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS;
|
|
945
|
+
}
|
|
946
|
+
function formatRecoveredCount(count) {
|
|
947
|
+
return `caught up ${count} missed message(s)`;
|
|
948
|
+
}
|
|
924
949
|
async function syncBlueBubblesRuntime(deps = {}) {
|
|
925
950
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
926
951
|
const agentName = resolvedDeps.getAgentName();
|
|
927
952
|
const client = resolvedDeps.createClient();
|
|
928
953
|
const checkedAt = new Date().toISOString();
|
|
954
|
+
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
929
955
|
try {
|
|
930
956
|
await client.checkHealth();
|
|
931
957
|
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
958
|
+
const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState);
|
|
959
|
+
const failed = recovery.failed + catchUp.failed;
|
|
960
|
+
const recovered = recovery.recovered + catchUp.recovered;
|
|
932
961
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
933
|
-
upstreamStatus: recovery.pending > 0 ||
|
|
934
|
-
detail:
|
|
935
|
-
? `recovery failures: ${
|
|
962
|
+
upstreamStatus: recovery.pending > 0 || failed > 0 ? "error" : "ok",
|
|
963
|
+
detail: failed > 0
|
|
964
|
+
? `recovery failures: ${failed}`
|
|
936
965
|
: recovery.pending > 0
|
|
937
966
|
? `pending recovery: ${recovery.pending}`
|
|
938
|
-
:
|
|
967
|
+
: catchUp.recovered > 0
|
|
968
|
+
? formatRecoveredCount(catchUp.recovered)
|
|
969
|
+
: "upstream reachable",
|
|
939
970
|
lastCheckedAt: checkedAt,
|
|
940
971
|
pendingRecoveryCount: recovery.pending,
|
|
941
|
-
lastRecoveredAt:
|
|
972
|
+
lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
|
|
973
|
+
lastRecoveredMessageGuid: catchUp.lastRecoveredMessageGuid ?? previousState.lastRecoveredMessageGuid,
|
|
942
974
|
});
|
|
943
975
|
}
|
|
944
976
|
catch (error) {
|
|
@@ -950,6 +982,128 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
950
982
|
});
|
|
951
983
|
}
|
|
952
984
|
}
|
|
985
|
+
async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
|
|
986
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
987
|
+
const agentName = resolvedDeps.getAgentName();
|
|
988
|
+
const client = resolvedDeps.createClient();
|
|
989
|
+
const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
|
|
990
|
+
const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
991
|
+
const catchUpSince = resolveBlueBubblesCatchUpSince(state);
|
|
992
|
+
/* v8 ignore next -- older injected test doubles may omit the catch-up query method */
|
|
993
|
+
if (!client.listRecentMessages)
|
|
994
|
+
return result;
|
|
995
|
+
(0, runtime_1.emitNervesEvent)({
|
|
996
|
+
component: "senses",
|
|
997
|
+
event: "senses.bluebubbles_catchup_start",
|
|
998
|
+
message: "bluebubbles upstream catch-up pass started",
|
|
999
|
+
meta: {
|
|
1000
|
+
since: new Date(catchUpSince).toISOString(),
|
|
1001
|
+
pageSize: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1002
|
+
maxPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
const recentEvents = [];
|
|
1006
|
+
for (let page = 0; page < BLUEBUBBLES_CATCHUP_MAX_PAGES; page++) {
|
|
1007
|
+
let pageEvents;
|
|
1008
|
+
try {
|
|
1009
|
+
pageEvents = await client.listRecentMessages({
|
|
1010
|
+
limit: BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1011
|
+
offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
result.failed++;
|
|
1016
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1017
|
+
level: "warn",
|
|
1018
|
+
component: "senses",
|
|
1019
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1020
|
+
message: "bluebubbles upstream catch-up query failed",
|
|
1021
|
+
meta: {
|
|
1022
|
+
offset: page * BLUEBUBBLES_CATCHUP_PAGE_SIZE,
|
|
1023
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
recentEvents.push(...pageEvents);
|
|
1029
|
+
if (pageEvents.length < BLUEBUBBLES_CATCHUP_PAGE_SIZE)
|
|
1030
|
+
break;
|
|
1031
|
+
const oldestMessageTimestamp = pageEvents
|
|
1032
|
+
.filter((event) => event.kind === "message")
|
|
1033
|
+
.reduce((oldest, event) => Math.min(oldest, event.timestamp), Number.POSITIVE_INFINITY);
|
|
1034
|
+
if (oldestMessageTimestamp <= catchUpSince)
|
|
1035
|
+
break;
|
|
1036
|
+
if (page === BLUEBUBBLES_CATCHUP_MAX_PAGES - 1) {
|
|
1037
|
+
result.failed++;
|
|
1038
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1039
|
+
level: "warn",
|
|
1040
|
+
component: "senses",
|
|
1041
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1042
|
+
message: "bluebubbles upstream catch-up reached the bounded page limit",
|
|
1043
|
+
meta: {
|
|
1044
|
+
inspectedPages: BLUEBUBBLES_CATCHUP_MAX_PAGES,
|
|
1045
|
+
reason: "catch-up page limit reached before the outage window cutoff",
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const seenMessageGuids = new Set();
|
|
1051
|
+
const candidates = recentEvents
|
|
1052
|
+
.filter((event) => event.kind === "message")
|
|
1053
|
+
.filter((event) => {
|
|
1054
|
+
if (seenMessageGuids.has(event.messageGuid))
|
|
1055
|
+
return false;
|
|
1056
|
+
seenMessageGuids.add(event.messageGuid);
|
|
1057
|
+
return true;
|
|
1058
|
+
})
|
|
1059
|
+
.sort((left, right) => left.timestamp - right.timestamp);
|
|
1060
|
+
for (const event of candidates) {
|
|
1061
|
+
result.inspected++;
|
|
1062
|
+
if (event.fromMe
|
|
1063
|
+
|| event.timestamp < catchUpSince
|
|
1064
|
+
|| (0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
1065
|
+
result.skipped++;
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const repaired = await client.repairEvent(event);
|
|
1070
|
+
if (repaired.kind !== "message") {
|
|
1071
|
+
result.skipped++;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup");
|
|
1075
|
+
if (handled.reason === "already_processed") {
|
|
1076
|
+
result.skipped++;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
result.recovered++;
|
|
1080
|
+
result.lastRecoveredMessageGuid = repaired.messageGuid;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
result.failed++;
|
|
1085
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1086
|
+
level: "warn",
|
|
1087
|
+
component: "senses",
|
|
1088
|
+
event: "senses.bluebubbles_catchup_error",
|
|
1089
|
+
message: "bluebubbles upstream catch-up message failed",
|
|
1090
|
+
meta: {
|
|
1091
|
+
messageGuid: event.messageGuid,
|
|
1092
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (result.inspected > 0 || result.recovered > 0 || result.skipped > 0 || result.failed > 0) {
|
|
1098
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1099
|
+
component: "senses",
|
|
1100
|
+
event: "senses.bluebubbles_catchup_complete",
|
|
1101
|
+
message: "bluebubbles upstream catch-up pass completed",
|
|
1102
|
+
meta: { ...result },
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
953
1107
|
async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
954
1108
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
955
1109
|
const agentName = resolvedDeps.getAgentName();
|