@ouro.bot/cli 0.1.0-alpha.521 → 0.1.0-alpha.523
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,22 @@
|
|
|
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.523",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Exits the daemon entrypoint after a command-plane `daemon.stop`, preventing a stopped daemon from staying alive without its command socket and leaving Slugger's iMessage worker marked degraded.",
|
|
8
|
+
"Shares daemon entrypoint cleanup between signal stops and command stops, stopping habit schedulers and daemon health polling before the process exits.",
|
|
9
|
+
"Keeps BlueBubbles runtime health green as soon as upstream is reachable while queued captured-inbound, mutation, or catch-up recovery work remains visible in `pendingRecoveryCount` instead of making live iMessage look down."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.522",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Keeps the BlueBubbles production HTTP worker responsive by changing runtime health sync to queue/discover missed iMessage recovery work instead of running full recovered agent turns inline.",
|
|
16
|
+
"Counts captured inbound sidecars and mutation backlog as pending recovery in BlueBubbles runtime state, so `ouro status`/health truth can show that live transport is up while old messages still need recovery.",
|
|
17
|
+
"Queues upstream catch-up candidates into the inbound sidecar during runtime sync without hydrating or invoking the agent, preserving idempotent recovery while preventing startup catch-up from starving live webhooks."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.521",
|
|
6
22
|
"changes": [
|
|
@@ -149,6 +149,25 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
|
|
|
149
149
|
catch { /* recovery is best-effort */ }
|
|
150
150
|
},
|
|
151
151
|
});
|
|
152
|
+
const habitSchedulers = [];
|
|
153
|
+
let entryRuntimeStopping = false;
|
|
154
|
+
let stopCommandExitScheduled = false;
|
|
155
|
+
function stopEntryRuntime() {
|
|
156
|
+
if (entryRuntimeStopping)
|
|
157
|
+
return;
|
|
158
|
+
entryRuntimeStopping = true;
|
|
159
|
+
for (const s of habitSchedulers) {
|
|
160
|
+
s.stopWatching();
|
|
161
|
+
s.stop();
|
|
162
|
+
}
|
|
163
|
+
healthMonitor.stopPeriodicChecks();
|
|
164
|
+
}
|
|
165
|
+
function scheduleCleanProcessExitAfterStopCommand() {
|
|
166
|
+
if (stopCommandExitScheduled)
|
|
167
|
+
return;
|
|
168
|
+
stopCommandExitScheduled = true;
|
|
169
|
+
setTimeout(() => process.exit(0), 100);
|
|
170
|
+
}
|
|
152
171
|
const daemon = new daemon_1.OuroDaemon({
|
|
153
172
|
socketPath,
|
|
154
173
|
processManager,
|
|
@@ -157,6 +176,10 @@ const daemon = new daemon_1.OuroDaemon({
|
|
|
157
176
|
healthMonitor,
|
|
158
177
|
router,
|
|
159
178
|
mode,
|
|
179
|
+
onStopCommandComplete: () => {
|
|
180
|
+
stopEntryRuntime();
|
|
181
|
+
scheduleCleanProcessExitAfterStopCommand();
|
|
182
|
+
},
|
|
160
183
|
});
|
|
161
184
|
const daemonStartedAt = new Date().toISOString();
|
|
162
185
|
const degradedComponents = [];
|
|
@@ -298,7 +321,6 @@ const healthWriter = new daemon_health_1.DaemonHealthWriter((0, daemon_health_1.
|
|
|
298
321
|
const healthSink = (0, daemon_health_1.createHealthNervesSink)(healthWriter, buildDaemonHealthState);
|
|
299
322
|
(0, index_1.registerGlobalLogSink)(healthSink);
|
|
300
323
|
/* v8 ignore stop */
|
|
301
|
-
const habitSchedulers = [];
|
|
302
324
|
/* v8 ignore start -- habit wiring: lambdas delegate to processManager/fs; tested via HabitScheduler unit tests @preserve */
|
|
303
325
|
void daemon.start().then(() => {
|
|
304
326
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
@@ -392,22 +414,14 @@ process.on("SIGINT", () => {
|
|
|
392
414
|
// tombstone is strictly better than silence.
|
|
393
415
|
_tombstoneWritten = true;
|
|
394
416
|
(0, daemon_tombstone_1.writeDaemonTombstone)("sigint", new Error("daemon received SIGINT"));
|
|
395
|
-
|
|
396
|
-
s.stopWatching();
|
|
397
|
-
s.stop();
|
|
398
|
-
}
|
|
399
|
-
healthMonitor.stopPeriodicChecks();
|
|
417
|
+
stopEntryRuntime();
|
|
400
418
|
setTimeout(() => process.exit(1), 5_000).unref();
|
|
401
419
|
void daemon.stop().then(() => process.exit(0));
|
|
402
420
|
});
|
|
403
421
|
process.on("SIGTERM", () => {
|
|
404
422
|
_tombstoneWritten = true;
|
|
405
423
|
(0, daemon_tombstone_1.writeDaemonTombstone)("sigterm", new Error("daemon received SIGTERM"));
|
|
406
|
-
|
|
407
|
-
s.stopWatching();
|
|
408
|
-
s.stop();
|
|
409
|
-
}
|
|
410
|
-
healthMonitor.stopPeriodicChecks();
|
|
424
|
+
stopEntryRuntime();
|
|
411
425
|
setTimeout(() => process.exit(1), 5_000).unref();
|
|
412
426
|
void daemon.stop().then(() => process.exit(0));
|
|
413
427
|
});
|
|
@@ -418,6 +418,7 @@ class OuroDaemon {
|
|
|
418
418
|
socketIdentity = null;
|
|
419
419
|
senseAutostartTimer = null;
|
|
420
420
|
outlookServerFactory;
|
|
421
|
+
onStopCommandComplete;
|
|
421
422
|
constructor(options) {
|
|
422
423
|
this.socketPath = options.socketPath;
|
|
423
424
|
this.processManager = options.processManager;
|
|
@@ -428,6 +429,7 @@ class OuroDaemon {
|
|
|
428
429
|
this.bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
429
430
|
this.mode = options.mode ?? "production";
|
|
430
431
|
this.outlookServerFactory = options.outlookServerFactory ?? this.createDefaultOutlookServer.bind(this);
|
|
432
|
+
this.onStopCommandComplete = options.onStopCommandComplete ?? null;
|
|
431
433
|
}
|
|
432
434
|
/* v8 ignore start -- default outlook server wiring: production-only path, tests inject outlookServerFactory stub instead. startOutlookHttpServer itself has full coverage in outlook-http.test.ts @preserve */
|
|
433
435
|
createDefaultOutlookServer() {
|
|
@@ -906,6 +908,7 @@ class OuroDaemon {
|
|
|
906
908
|
(0, update_checker_1.stopUpdateChecker)();
|
|
907
909
|
(0, mcp_manager_1.shutdownSharedMcpManager)();
|
|
908
910
|
this.scheduler.stop?.();
|
|
911
|
+
this.healthMonitor.stopPeriodicChecks?.();
|
|
909
912
|
if (this.senseAutostartTimer) {
|
|
910
913
|
clearTimeout(this.senseAutostartTimer);
|
|
911
914
|
this.senseAutostartTimer = null;
|
|
@@ -1022,6 +1025,7 @@ class OuroDaemon {
|
|
|
1022
1025
|
return { ok: true, message: "daemon started" };
|
|
1023
1026
|
case "daemon.stop":
|
|
1024
1027
|
await this.stop();
|
|
1028
|
+
this.onStopCommandComplete?.();
|
|
1025
1029
|
return { ok: true, message: "daemon stopped" };
|
|
1026
1030
|
case "daemon.status": {
|
|
1027
1031
|
const data = this.buildStatusPayload();
|
|
@@ -1165,6 +1165,18 @@ function countPendingRecoveryCandidates(agentName) {
|
|
|
1165
1165
|
.filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid))
|
|
1166
1166
|
.length;
|
|
1167
1167
|
}
|
|
1168
|
+
function countPendingCapturedInboundMessages(agentName) {
|
|
1169
|
+
const seenMessageGuids = new Set();
|
|
1170
|
+
return (0, inbound_log_1.listRecordedBlueBubblesInbound)(agentName)
|
|
1171
|
+
.filter((entry) => {
|
|
1172
|
+
if (seenMessageGuids.has(entry.messageGuid))
|
|
1173
|
+
return false;
|
|
1174
|
+
seenMessageGuids.add(entry.messageGuid);
|
|
1175
|
+
return true;
|
|
1176
|
+
})
|
|
1177
|
+
.filter((entry) => !(0, processed_log_1.hasProcessedBlueBubblesMessage)(agentName, entry.sessionKey, entry.messageGuid))
|
|
1178
|
+
.length;
|
|
1179
|
+
}
|
|
1168
1180
|
function parseTimestampMs(value) {
|
|
1169
1181
|
if (!value)
|
|
1170
1182
|
return null;
|
|
@@ -1181,9 +1193,6 @@ function resolveBlueBubblesCatchUpSince(previousState, nowMs = Date.now()) {
|
|
|
1181
1193
|
}
|
|
1182
1194
|
return nowMs - BLUEBUBBLES_FIRST_CATCHUP_LOOKBACK_MS;
|
|
1183
1195
|
}
|
|
1184
|
-
function formatRecoveredCount(count) {
|
|
1185
|
-
return `caught up ${count} missed message(s)`;
|
|
1186
|
-
}
|
|
1187
1196
|
async function syncBlueBubblesRuntime(deps = {}) {
|
|
1188
1197
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1189
1198
|
const agentName = resolvedDeps.getAgentName();
|
|
@@ -1192,30 +1201,38 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
1192
1201
|
const previousState = (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
1193
1202
|
try {
|
|
1194
1203
|
await client.checkHealth();
|
|
1195
|
-
const
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1204
|
+
const capturedPending = countPendingCapturedInboundMessages(agentName);
|
|
1205
|
+
const recoveryPending = countPendingRecoveryCandidates(agentName);
|
|
1206
|
+
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1207
|
+
upstreamStatus: "ok",
|
|
1208
|
+
detail: "upstream reachable; recovery pass running",
|
|
1209
|
+
lastCheckedAt: checkedAt,
|
|
1210
|
+
pendingRecoveryCount: capturedPending + recoveryPending,
|
|
1211
|
+
lastRecoveredAt: previousState.lastRecoveredAt,
|
|
1212
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1213
|
+
});
|
|
1214
|
+
const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState, {
|
|
1215
|
+
processTurns: false,
|
|
1216
|
+
});
|
|
1217
|
+
const failed = catchUp.failed;
|
|
1218
|
+
const queued = capturedPending + recoveryPending + (catchUp.queued ?? 0);
|
|
1219
|
+
// upstreamStatus reflects whether BlueBubbles itself is healthy and
|
|
1220
|
+
// whether the local bridge can answer webhook traffic. Queued recovery work
|
|
1221
|
+
// and per-cycle failures are noted in `detail` for transparency but do NOT
|
|
1222
|
+
// flip the status to error: a single permanently-unrecoverable message
|
|
1223
|
+
// would otherwise stick the sense in "error" forever, contradicting `ouro
|
|
1205
1224
|
// doctor` which only checks upstream reachability.
|
|
1206
1225
|
(0, runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
1207
|
-
upstreamStatus:
|
|
1208
|
-
detail:
|
|
1209
|
-
? `
|
|
1226
|
+
upstreamStatus: "ok",
|
|
1227
|
+
detail: queued > 0
|
|
1228
|
+
? `upstream reachable; ${queued} recovery item(s) queued`
|
|
1210
1229
|
: failed > 0
|
|
1211
1230
|
? `${failed} message(s) unrecoverable this cycle; upstream ok`
|
|
1212
|
-
:
|
|
1213
|
-
? formatRecoveredCount(catchUp.recovered)
|
|
1214
|
-
: "upstream reachable",
|
|
1231
|
+
: "upstream reachable",
|
|
1215
1232
|
lastCheckedAt: checkedAt,
|
|
1216
|
-
pendingRecoveryCount:
|
|
1217
|
-
lastRecoveredAt:
|
|
1218
|
-
lastRecoveredMessageGuid:
|
|
1233
|
+
pendingRecoveryCount: queued,
|
|
1234
|
+
lastRecoveredAt: previousState.lastRecoveredAt,
|
|
1235
|
+
lastRecoveredMessageGuid: previousState.lastRecoveredMessageGuid,
|
|
1219
1236
|
});
|
|
1220
1237
|
}
|
|
1221
1238
|
catch (error) {
|
|
@@ -1223,17 +1240,18 @@ async function syncBlueBubblesRuntime(deps = {}) {
|
|
|
1223
1240
|
upstreamStatus: "error",
|
|
1224
1241
|
detail: error instanceof Error ? error.message : String(error),
|
|
1225
1242
|
lastCheckedAt: checkedAt,
|
|
1226
|
-
pendingRecoveryCount: countPendingRecoveryCandidates(agentName),
|
|
1243
|
+
pendingRecoveryCount: countPendingCapturedInboundMessages(agentName) + countPendingRecoveryCandidates(agentName),
|
|
1227
1244
|
});
|
|
1228
1245
|
}
|
|
1229
1246
|
}
|
|
1230
|
-
async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
|
|
1247
|
+
async function catchUpMissedBlueBubblesMessages(deps = {}, previousState, options = {}) {
|
|
1231
1248
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1232
1249
|
const agentName = resolvedDeps.getAgentName();
|
|
1233
1250
|
const client = resolvedDeps.createClient();
|
|
1234
1251
|
const result = { inspected: 0, recovered: 0, skipped: 0, failed: 0 };
|
|
1235
1252
|
const state = previousState ?? (0, runtime_state_1.readBlueBubblesRuntimeState)(agentName);
|
|
1236
1253
|
const catchUpSince = resolveBlueBubblesCatchUpSince(state);
|
|
1254
|
+
const processTurns = options.processTurns !== false;
|
|
1237
1255
|
/* v8 ignore next -- older injected test doubles may omit the catch-up query method */
|
|
1238
1256
|
if (!client.listRecentMessages)
|
|
1239
1257
|
return result;
|
|
@@ -1311,6 +1329,15 @@ async function catchUpMissedBlueBubblesMessages(deps = {}, previousState) {
|
|
|
1311
1329
|
result.skipped++;
|
|
1312
1330
|
continue;
|
|
1313
1331
|
}
|
|
1332
|
+
if (!processTurns) {
|
|
1333
|
+
if ((0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
1334
|
+
result.skipped++;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
(0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, "upstream-catchup");
|
|
1338
|
+
result.queued = (result.queued ?? 0) + 1;
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1314
1341
|
try {
|
|
1315
1342
|
const repaired = await client.repairEvent(event);
|
|
1316
1343
|
if (repaired.kind !== "message") {
|