@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
- for (const s of habitSchedulers) {
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
- for (const s of habitSchedulers) {
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 captured = await recoverCapturedBlueBubblesInboundMessages(resolvedDeps);
1196
- const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
1197
- const catchUp = await catchUpMissedBlueBubblesMessages(resolvedDeps, previousState);
1198
- const failed = captured.failed + recovery.failed + catchUp.failed;
1199
- const recovered = captured.recovered + recovery.recovered + catchUp.recovered;
1200
- // upstreamStatus reflects whether BlueBubbles itself is healthy and we
1201
- // have unprocessed work (pendingRecoveryCount). Per-cycle recovery
1202
- // failures are noted in `detail` for transparency but do NOT flip the
1203
- // status to error: a single permanently-unrecoverable message would
1204
- // otherwise stick the sense in "error" forever, contradicting `ouro
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: recovery.pending > 0 ? "error" : "ok",
1208
- detail: recovery.pending > 0
1209
- ? `pending recovery: ${recovery.pending}`
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
- : catchUp.recovered > 0
1213
- ? formatRecoveredCount(catchUp.recovered)
1214
- : "upstream reachable",
1231
+ : "upstream reachable",
1215
1232
  lastCheckedAt: checkedAt,
1216
- pendingRecoveryCount: recovery.pending,
1217
- lastRecoveredAt: recovered > 0 ? checkedAt : previousState.lastRecoveredAt,
1218
- lastRecoveredMessageGuid: catchUp.lastRecoveredMessageGuid ?? previousState.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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.521",
3
+ "version": "0.1.0-alpha.523",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",