@ouro.bot/cli 0.1.0-alpha.541 → 0.1.0-alpha.543

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,20 @@
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.543",
6
+ "changes": [
7
+ "Daemon health-monitor MCP canaries now ignore the daemon's aggregate overview health while still validating MCP transport, daemon liveness, version alignment, and required sense health, preventing the canary's previous failure from keeping the daemon in a self-reinforcing warning state."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.542",
12
+ "changes": [
13
+ "BlueBubbles recovery timeouts now leave iMessage messages pending instead of marking them processed, and legacy `recovery-timeout` processed records no longer suppress retries.",
14
+ "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.",
15
+ "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."
16
+ ]
17
+ },
4
18
  {
5
19
  "version": "0.1.0-alpha.541",
6
20
  "changes": [
@@ -146,6 +146,7 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
146
146
  "--socket",
147
147
  socketPath,
148
148
  ],
149
+ ignoreOverviewHealth: true,
149
150
  })),
150
151
  ],
151
152
  alertSink: (message) => {
@@ -97,12 +97,12 @@ function parseMcpStatusText(text) {
97
97
  }
98
98
  return { daemon, senses, raw: text };
99
99
  }
100
- function validateMcpStatus(parsed, requiredSenses) {
100
+ function validateMcpStatus(parsed, requiredSenses, options = {}) {
101
101
  const failures = [];
102
102
  if (parsed.daemon.daemon !== "running") {
103
103
  failures.push(`daemon=${parsed.daemon.daemon ?? "missing"}`);
104
104
  }
105
- if (parsed.daemon.health !== "ok") {
105
+ if (!options.ignoreOverviewHealth && parsed.daemon.health !== "ok") {
106
106
  failures.push(`health=${parsed.daemon.health ?? "missing"}`);
107
107
  }
108
108
  if (parsed.daemon.daemonVersion &&
@@ -131,7 +131,7 @@ function validateMcpStatus(parsed, requiredSenses) {
131
131
  .map((row) => `${row.name}:${row.status}`)
132
132
  .join(",");
133
133
  const summary = failures.length === 0
134
- ? `mcp canary ok: daemon=${parsed.daemon.daemon} health=${parsed.daemon.health} senses=${senseSummary}`
134
+ ? `mcp canary ok: daemon=${parsed.daemon.daemon} health=${parsed.daemon.health}${options.ignoreOverviewHealth ? " (overview ignored)" : ""} senses=${senseSummary}`
135
135
  : `mcp canary failed: ${failures.join("; ")}`;
136
136
  return {
137
137
  ok: failures.length === 0,
@@ -151,7 +151,14 @@ async function runMcpStatusCanary(options) {
151
151
  component: "daemon",
152
152
  event: "daemon.mcp_canary_start",
153
153
  message: "starting MCP status canary",
154
- meta: { agent: options.agent, command, commandArgs, timeoutMs, requiredSenses },
154
+ meta: {
155
+ agent: options.agent,
156
+ command,
157
+ commandArgs,
158
+ timeoutMs,
159
+ requiredSenses,
160
+ ignoreOverviewHealth: options.ignoreOverviewHealth === true,
161
+ },
155
162
  });
156
163
  const child = spawnImpl(command, commandArgs, { stdio: ["pipe", "pipe", "pipe"] });
157
164
  let buffer = "";
@@ -244,7 +251,9 @@ async function runMcpStatusCanary(options) {
244
251
  throw new Error(responseText(statusResponse));
245
252
  }
246
253
  const parsed = parseMcpStatusText(responseText(statusResponse));
247
- const canary = validateMcpStatus(parsed, requiredSenses);
254
+ const canary = validateMcpStatus(parsed, requiredSenses, {
255
+ ignoreOverviewHealth: options.ignoreOverviewHealth,
256
+ });
248
257
  (0, runtime_1.emitNervesEvent)({
249
258
  component: "daemon",
250
259
  event: canary.ok ? "daemon.mcp_canary_end" : "daemon.mcp_canary_error",
@@ -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,6 +823,7 @@ 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
828
  if ((0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, event.chat.sessionKey, event.messageGuid)
832
829
  || (0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, event.messageGuid)) {
@@ -989,6 +986,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
989
986
  timeoutTimer = setTimeout(() => {
990
987
  const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
991
988
  recoveryTimedOut = true;
989
+ releaseInFlightAfterTurnSettles = true;
992
990
  controller.abort(reason);
993
991
  timeoutReject?.(reason);
994
992
  (0, runtime_1.emitNervesEvent)({
@@ -1072,7 +1070,8 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1072
1070
  });
1073
1071
  /* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
1074
1072
  if (timeoutPromise) {
1075
- void turnPromise.catch((error) => {
1073
+ void turnPromise
1074
+ .catch((error) => {
1076
1075
  if (!recoveryTimedOut)
1077
1076
  return;
1078
1077
  (0, runtime_1.emitNervesEvent)({
@@ -1087,6 +1086,11 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1087
1086
  reason: error instanceof Error ? error.message : String(error),
1088
1087
  },
1089
1088
  });
1089
+ })
1090
+ .finally(() => {
1091
+ if (releaseInFlightAfterTurnSettles && ownsInFlightMessage && event.kind === "message") {
1092
+ endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
1093
+ }
1090
1094
  });
1091
1095
  }
1092
1096
  /* v8 ignore stop */
@@ -1160,7 +1164,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1160
1164
  });
1161
1165
  }
1162
1166
  finally {
1163
- if (ownsInFlightMessage && event.kind === "message") {
1167
+ if (ownsInFlightMessage && event.kind === "message" && !releaseInFlightAfterTurnSettles) {
1164
1168
  endBlueBubblesMessageInFlight(event.chat.sessionKey, event.messageGuid);
1165
1169
  }
1166
1170
  }
@@ -1237,14 +1241,6 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
1237
1241
  const event = await client.repairEvent(normalized);
1238
1242
  return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
1239
1243
  }
1240
- function countPendingRecoveryCandidates(agentName) {
1241
- return (0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
1242
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid))
1243
- .length;
1244
- }
1245
- function countPendingCapturedInboundMessages(agentName) {
1246
- return listPendingCapturedInboundMessages(agentName).length;
1247
- }
1248
1244
  function listPendingCapturedInboundMessages(agentName) {
1249
1245
  const seenMessageGuids = new Set();
1250
1246
  return (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
@@ -1256,6 +1252,30 @@ function listPendingCapturedInboundMessages(agentName) {
1256
1252
  })
1257
1253
  .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid));
1258
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 }));
1278
+ }
1259
1279
  function parseTimestampMs(value) {
1260
1280
  if (!value)
1261
1281
  return null;
@@ -1280,18 +1300,15 @@ function formatBlueBubblesRuntimeDetail(queued, failed) {
1280
1300
  return "upstream reachable";
1281
1301
  }
1282
1302
  function blueBubblesPendingRecoverySnapshot(agentName, nowMs = Date.now()) {
1283
- const pendingRecordedAt = [
1284
- ...listPendingCapturedInboundMessages(agentName).map((entry) => entry.recordedAt),
1285
- ...(0, mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
1286
- .filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessageGuid)(agentName, entry.messageGuid))
1287
- .map((entry) => entry.recordedAt),
1288
- ]
1303
+ const pendingEntries = listPendingRecoveryEntries(agentName);
1304
+ const pendingRecordedAt = pendingEntries
1305
+ .map((entry) => entry.recordedAt)
1289
1306
  .map((value) => ({ value, ms: Date.parse(value) }))
1290
1307
  .filter((entry) => Number.isFinite(entry.ms))
1291
1308
  .sort((left, right) => left.ms - right.ms);
1292
1309
  const oldest = pendingRecordedAt[0];
1293
1310
  return {
1294
- pendingRecoveryCount: countPendingCapturedInboundMessages(agentName) + countPendingRecoveryCandidates(agentName),
1311
+ pendingRecoveryCount: pendingEntries.length,
1295
1312
  oldestPendingRecoveryAt: oldest?.value,
1296
1313
  oldestPendingRecoveryAgeMs: oldest ? Math.max(0, nowMs - oldest.ms) : undefined,
1297
1314
  };
@@ -1484,14 +1501,12 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1484
1501
  result.queued = (result.queued ?? 0) + 1;
1485
1502
  continue;
1486
1503
  }
1487
- let repairedMessage = null;
1488
1504
  try {
1489
1505
  const repaired = await client.repairEvent(event);
1490
1506
  if (repaired.kind !== "message") {
1491
1507
  result.skipped++;
1492
1508
  continue;
1493
1509
  }
1494
- repairedMessage = repaired;
1495
1510
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "upstream-catchup", {
1496
1511
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1497
1512
  });
@@ -1505,9 +1520,6 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, option
1505
1520
  }
1506
1521
  catch (error) {
1507
1522
  result.failed++;
1508
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1509
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, "upstream-catchup", "recovery-timeout");
1510
- }
1511
1523
  (0, runtime_1.emitNervesEvent)({
1512
1524
  level: "warn",
1513
1525
  component: "senses",
@@ -1585,14 +1597,12 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
1585
1597
  result.skipped++;
1586
1598
  continue;
1587
1599
  }
1588
- let repairedMessage = null;
1589
1600
  try {
1590
1601
  const repaired = await client.repairEvent(inboundEntryToRecoveryEvent(entry));
1591
1602
  if (repaired.kind !== "message") {
1592
1603
  result.skipped++;
1593
1604
  continue;
1594
1605
  }
1595
- repairedMessage = repaired;
1596
1606
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, entry.source, {
1597
1607
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1598
1608
  });
@@ -1605,9 +1615,6 @@ async function recoverCapturedBlueBubblesInboundMessages(deps = {}) {
1605
1615
  }
1606
1616
  catch (error) {
1607
1617
  result.failed++;
1608
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1609
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, entry.source, "recovery-timeout");
1610
- }
1611
1618
  (0, runtime_1.emitNervesEvent)({
1612
1619
  level: "warn",
1613
1620
  component: "senses",
@@ -1635,14 +1642,12 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
1635
1642
  result.skipped++;
1636
1643
  continue;
1637
1644
  }
1638
- let repairedMessage = null;
1639
1645
  try {
1640
1646
  const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
1641
1647
  if (repaired.kind !== "message") {
1642
1648
  result.pending++;
1643
1649
  continue;
1644
1650
  }
1645
- repairedMessage = repaired;
1646
1651
  const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery", {
1647
1652
  timeoutMs: BLUEBUBBLES_RECOVERY_TURN_TIMEOUT_MS,
1648
1653
  });
@@ -1655,9 +1660,6 @@ async function recoverMissedBlueBubblesMessages(deps = {}) {
1655
1660
  }
1656
1661
  catch (error) {
1657
1662
  result.failed++;
1658
- if (repairedMessage && isBlueBubblesRecoveryTurnTimeoutError(error)) {
1659
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, repairedMessage, "mutation-recovery", "recovery-timeout");
1660
- }
1661
1663
  (0, runtime_1.emitNervesEvent)({
1662
1664
  level: "warn",
1663
1665
  component: "senses",
@@ -71,22 +71,26 @@ function readAllEntries(agentName) {
71
71
  return [];
72
72
  }
73
73
  }
74
+ function isCompletedProcessedEntry(entry) {
75
+ return entry.outcome !== "recovery-timeout";
76
+ }
74
77
  function hasProcessedBlueBubblesMessage(agentName, sessionKey, messageGuid) {
75
78
  if (!messageGuid.trim())
76
79
  return false;
77
80
  const filePath = getBlueBubblesProcessedLogPath(agentName, sessionKey);
78
- return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid);
81
+ return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid && isCompletedProcessedEntry(entry));
79
82
  }
80
83
  function hasProcessedBlueBubblesMessageGuid(agentName, messageGuid) {
81
84
  if (!messageGuid.trim())
82
85
  return false;
83
- return readAllEntries(agentName).some((entry) => entry.messageGuid === messageGuid);
86
+ return readAllEntries(agentName).some((entry) => entry.messageGuid === messageGuid && isCompletedProcessedEntry(entry));
84
87
  }
85
88
  function recordProcessedBlueBubblesMessage(agentName, event, source, outcome) {
86
89
  const filePath = getBlueBubblesProcessedLogPath(agentName, event.chat.sessionKey);
87
90
  try {
88
91
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
89
- 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)))) {
90
94
  return filePath;
91
95
  }
92
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.541",
3
+ "version": "0.1.0-alpha.543",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",