@ouro.bot/cli 0.1.0-alpha.726 → 0.1.0-alpha.728

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.
Files changed (35) hide show
  1. package/README.md +14 -1
  2. package/assets/bluebubbles-host +264 -0
  3. package/changelog.json +18 -0
  4. package/dist/heart/config.js +1 -2
  5. package/dist/heart/core.js +181 -209
  6. package/dist/heart/daemon/bluebubbles-cli-integration.js +71 -0
  7. package/dist/heart/daemon/bluebubbles-host-protocol.js +433 -0
  8. package/dist/heart/daemon/bluebubbles-host.js +273 -0
  9. package/dist/heart/daemon/cli-defaults.js +8 -1
  10. package/dist/heart/daemon/cli-exec.js +239 -34
  11. package/dist/heart/daemon/cli-help.js +17 -6
  12. package/dist/heart/daemon/cli-parse.js +69 -1
  13. package/dist/heart/daemon/cli-render.js +7 -0
  14. package/dist/heart/daemon/daemon.js +79 -14
  15. package/dist/heart/daemon/doctor.js +21 -1
  16. package/dist/heart/daemon/mcp-canary.js +160 -29
  17. package/dist/heart/daemon/process-manager.js +100 -18
  18. package/dist/heart/daemon/socket-client.js +28 -19
  19. package/dist/heart/providers/anthropic.js +17 -12
  20. package/dist/heart/streaming.js +6 -6
  21. package/dist/heart/versioning/ouro-path-installer.js +30 -24
  22. package/dist/heart/versioning/ouro-recovery-launcher.js +76 -0
  23. package/dist/heart/versioning/version-intent.js +114 -0
  24. package/dist/mind/prompt-budget.js +104 -5
  25. package/dist/repertoire/tools.js +0 -10
  26. package/dist/senses/bluebubbles/client.js +43 -57
  27. package/dist/senses/bluebubbles/context-packet.js +104 -19
  28. package/dist/senses/bluebubbles/entry.js +14 -1
  29. package/dist/senses/bluebubbles/index.js +1398 -719
  30. package/dist/senses/bluebubbles/latest-turn.js +311 -0
  31. package/dist/senses/bluebubbles/outbound-state.js +4 -0
  32. package/dist/senses/bluebubbles/reaction-policy.js +4 -12
  33. package/dist/senses/bluebubbles/semantic-receipts.js +178 -10
  34. package/dist/senses/bluebubbles/webhook-registration.js +228 -0
  35. package/package.json +4 -2
@@ -118,6 +118,7 @@ function usage() {
118
118
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
119
119
  " ouro bluebubbles replay [--agent <name>] --message-guid <guid> [--event-type new-message|updated-message] [--json]",
120
120
  " ouro bluebubbles context-smoke [--agent <name>] --message-guid <guid> [--persist] [--json]",
121
+ " ouro bluebubbles host <install|status|repair|remove|collect> [--username <name> --uid <uid> --home <path>] [--request-id <id>] [--json]",
121
122
  " ouro rsvp <doctor|incident|cutover|legacy-render|replay|config|habit|import-legacy|refresh|compare|smoke> ...",
122
123
  " ouro friend list [--agent <name>]",
123
124
  " ouro friend show <id> [--agent <name>]",
@@ -1543,6 +1544,7 @@ function parseMcpCommand(args) {
1543
1544
  if (sub === "doctor") {
1544
1545
  let socketOverride;
1545
1546
  let json = false;
1547
+ let hostStallObserved = false;
1546
1548
  for (let i = 0; i < rest.length; i++) {
1547
1549
  if (rest[i] === "--socket") {
1548
1550
  if (!rest[i + 1])
@@ -1554,6 +1556,10 @@ function parseMcpCommand(args) {
1554
1556
  json = true;
1555
1557
  continue;
1556
1558
  }
1559
+ if (rest[i] === "--host-stall-observed") {
1560
+ hostStallObserved = true;
1561
+ continue;
1562
+ }
1557
1563
  throw new Error(`Unknown mcp doctor flag: ${rest[i]}`);
1558
1564
  }
1559
1565
  if (!agent)
@@ -1563,6 +1569,7 @@ function parseMcpCommand(args) {
1563
1569
  agent,
1564
1570
  ...(socketOverride ? { socketOverride } : {}),
1565
1571
  ...(json ? { json: true } : {}),
1572
+ ...(hostStallObserved ? { hostStallObserved: true } : {}),
1566
1573
  };
1567
1574
  }
1568
1575
  throw new Error(`Usage\n${usage()}`);
@@ -1751,6 +1758,62 @@ function parseMigrateToDeskCommand(args) {
1751
1758
  }
1752
1759
  function parseBlueBubblesCommand(args) {
1753
1760
  const subcommand = args[0];
1761
+ if (subcommand === "host") {
1762
+ const action = args[1];
1763
+ const usageText = "Usage: ouro bluebubbles host <install|status|repair|remove|collect> [--username <name> --uid <uid> --home <path>] [--request-id <id>] [--json]";
1764
+ if (action === "collect") {
1765
+ let requestId;
1766
+ let json = false;
1767
+ for (let index = 2; index < args.length; index += 1) {
1768
+ if (args[index] === "--request-id" && args[index + 1]) {
1769
+ requestId = args[++index];
1770
+ }
1771
+ else if (args[index] === "--json") {
1772
+ json = true;
1773
+ }
1774
+ else {
1775
+ throw new Error(usageText);
1776
+ }
1777
+ }
1778
+ if (!requestId)
1779
+ throw new Error(`${usageText}\ncollect requires --request-id <id>`);
1780
+ return { kind: "bluebubbles.host.collect", requestId, ...(json ? { json: true } : {}) };
1781
+ }
1782
+ if (action !== "install" && action !== "status" && action !== "repair" && action !== "remove") {
1783
+ throw new Error(usageText);
1784
+ }
1785
+ let username;
1786
+ let uid;
1787
+ let homeDir;
1788
+ let json = false;
1789
+ for (let index = 2; index < args.length; index += 1) {
1790
+ const token = args[index];
1791
+ if (token === "--username" && args[index + 1])
1792
+ username = args[++index];
1793
+ else if (token === "--uid" && args[index + 1]) {
1794
+ const value = Number(args[++index]);
1795
+ if (!Number.isInteger(value))
1796
+ throw new Error(`${usageText}\n--uid must be an integer`);
1797
+ uid = value;
1798
+ }
1799
+ else if (token === "--home" && args[index + 1])
1800
+ homeDir = args[++index];
1801
+ else if (token === "--json")
1802
+ json = true;
1803
+ else
1804
+ throw new Error(usageText);
1805
+ }
1806
+ const targetFields = [username, uid, homeDir].filter((value) => value !== undefined).length;
1807
+ if (targetFields !== 0 && targetFields !== 3) {
1808
+ throw new Error(`${usageText}\ncross-user setup requires --username, --uid, and --home together`);
1809
+ }
1810
+ return {
1811
+ kind: "bluebubbles.host",
1812
+ action,
1813
+ ...(targetFields === 3 ? { target: { username: username, uid: uid, homeDir: homeDir } } : {}),
1814
+ ...(json ? { json: true } : {}),
1815
+ };
1816
+ }
1754
1817
  if (subcommand !== "replay" && subcommand !== "context-smoke") {
1755
1818
  throw new Error(`Usage\n${usage()}`);
1756
1819
  }
@@ -2442,7 +2505,12 @@ function parseOuroCommand(args) {
2442
2505
  }
2443
2506
  if (head === "up") {
2444
2507
  const noRepair = args.includes("--no-repair");
2445
- return noRepair ? { kind: "daemon.up", noRepair: true } : { kind: "daemon.up" };
2508
+ const latest = args.includes("--latest");
2509
+ return {
2510
+ kind: "daemon.up",
2511
+ ...(noRepair ? { noRepair: true } : {}),
2512
+ ...(latest ? { latest: true } : {}),
2513
+ };
2446
2514
  }
2447
2515
  if (head === "dev") {
2448
2516
  const devArgs = args.slice(1);
@@ -48,6 +48,7 @@ exports.buildStoppedStatusPayload = buildStoppedStatusPayload;
48
48
  exports.renderRollupStatusLine = renderRollupStatusLine;
49
49
  exports.daemonUnavailableStatusOutput = daemonUnavailableStatusOutput;
50
50
  exports.isDaemonUnavailableError = isDaemonUnavailableError;
51
+ exports.isDaemonTimeoutError = isDaemonTimeoutError;
51
52
  exports.formatMcpResponse = formatMcpResponse;
52
53
  const path = __importStar(require("path"));
53
54
  const runtime_metadata_1 = require("./runtime-metadata");
@@ -746,6 +747,12 @@ function isDaemonUnavailableError(error) {
746
747
  : "";
747
748
  return code === "ENOENT" || code === "ECONNREFUSED";
748
749
  }
750
+ function isDaemonTimeoutError(error) {
751
+ const code = typeof error === "object" && error !== null && "code" in error
752
+ ? String(error.code ?? "")
753
+ : "";
754
+ return code === "ETIMEDOUT";
755
+ }
749
756
  function formatMcpResponse(command, response) {
750
757
  if (command.kind === "mcp.list") {
751
758
  const allTools = response.data;
@@ -39,6 +39,8 @@ exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
39
39
  exports.filterPidfilePidsToActualOrphans = filterPidfilePidsToActualOrphans;
40
40
  exports.mergeUniqueOrphanPids = mergeUniqueOrphanPids;
41
41
  exports.waitForOrphanProcessesToSettle = waitForOrphanProcessesToSettle;
42
+ exports.assertOrphanCleanupComplete = assertOrphanCleanupComplete;
43
+ exports.drainOrphanProcessesBeforeStartup = drainOrphanProcessesBeforeStartup;
42
44
  exports.killOrphanProcesses = killOrphanProcesses;
43
45
  exports.writePidfile = writePidfile;
44
46
  exports.handleAgentSenseTurn = handleAgentSenseTurn;
@@ -124,7 +126,8 @@ function parseOrphanPidsFromPs(psOutput, selfPid) {
124
126
  && !line.includes("bluebubbles/entry.js")
125
127
  && !line.includes("mail-entry.js")
126
128
  && !line.includes("teams-entry.js")
127
- && !line.includes("a2a-entry.js"))
129
+ && !line.includes("a2a-entry.js")
130
+ && !line.includes("voice-entry.js"))
128
131
  continue;
129
132
  // Parse `<pid> <ppid> <command...>`. ps pads these with leading spaces.
130
133
  // Regex guarantees both groups are \d+ so parseInt can't produce NaN.
@@ -228,6 +231,41 @@ async function waitForOrphanProcessesToSettle(pids, deps = {}) {
228
231
  }
229
232
  return survivors;
230
233
  }
234
+ function assertOrphanCleanupComplete(survivingPids) {
235
+ if (survivingPids.length === 0)
236
+ return;
237
+ const errorReason = `refusing startup while ${survivingPids.length} orphaned Ouro processes remain: ${survivingPids.join(", ")}`;
238
+ (0, runtime_1.emitNervesEvent)({
239
+ level: "error",
240
+ component: "daemon",
241
+ event: "daemon.orphan_cleanup_incomplete",
242
+ message: errorReason,
243
+ meta: { survivingPids },
244
+ });
245
+ throw new Error(errorReason);
246
+ }
247
+ const ORPHAN_CLEANUP_MAX_SIGNAL_PASSES = 8;
248
+ async function drainOrphanProcessesBeforeStartup(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH, deps = {}) {
249
+ const signalOrphans = deps.signalOrphans ?? killOrphanProcesses;
250
+ const settleOrphans = deps.settleOrphans ?? waitForOrphanProcessesToSettle;
251
+ const maxSignalPasses = deps.maxSignalPasses ?? ORPHAN_CLEANUP_MAX_SIGNAL_PASSES;
252
+ for (let pass = 0; pass < maxSignalPasses; pass += 1) {
253
+ const signaledPids = signalOrphans(socketPath);
254
+ if (signaledPids.length === 0)
255
+ return;
256
+ const survivingPids = await settleOrphans(signaledPids);
257
+ assertOrphanCleanupComplete(survivingPids);
258
+ }
259
+ const errorReason = `orphan cleanup did not reach a fixed point after ${maxSignalPasses} signaling round(s)`;
260
+ (0, runtime_1.emitNervesEvent)({
261
+ level: "error",
262
+ component: "daemon",
263
+ event: "daemon.orphan_cleanup_not_converged",
264
+ message: errorReason,
265
+ meta: { maxSignalPasses },
266
+ });
267
+ throw new Error(errorReason);
268
+ }
231
269
  /* v8 ignore start -- shells out to ps; covered by filterPidfilePidsToActualOrphans unit tests via injected runner @preserve */
232
270
  function runPsCheck(pids) {
233
271
  try {
@@ -305,7 +343,10 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
305
343
  const result = (0, child_process_1.execSync)("ps -eo pid,ppid,command", { encoding: "utf-8", timeout: 5000 });
306
344
  scanOrphans = parseOrphanPidsFromPs(result, process.pid);
307
345
  }
308
- catch { /* ps failed — best effort */ }
346
+ catch (error) {
347
+ const detail = error instanceof Error ? error.message : String(error);
348
+ throw new Error(`could not discover orphaned Ouro processes: ${detail}`);
349
+ }
309
350
  const pidsToKill = mergeUniqueOrphanPids(pidfileOrphans, scanOrphans);
310
351
  if (pidsToKill.length > 0) {
311
352
  for (const pid of pidsToKill) {
@@ -325,13 +366,13 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
325
366
  }
326
367
  catch (error) {
327
368
  (0, runtime_1.emitNervesEvent)({
328
- level: "warn",
369
+ level: "error",
329
370
  component: "daemon",
330
371
  event: "daemon.orphan_cleanup_error",
331
372
  message: "failed to clean up orphaned ouro processes",
332
373
  meta: { error: error instanceof Error ? error.message : String(error) },
333
374
  });
334
- return [];
375
+ throw error instanceof Error ? error : new Error(String(error));
335
376
  }
336
377
  }
337
378
  /**
@@ -575,6 +616,7 @@ class OuroDaemon {
575
616
  onStopCommandComplete;
576
617
  externalEventRoot;
577
618
  rsvpHabitRunner;
619
+ orphanStartupDrain;
578
620
  constructor(options) {
579
621
  this.socketPath = options.socketPath;
580
622
  this.processManager = options.processManager;
@@ -589,6 +631,7 @@ class OuroDaemon {
589
631
  this.onStopCommandComplete = options.onStopCommandComplete ?? null;
590
632
  this.externalEventRoot = options.externalEventRoot ?? null;
591
633
  this.rsvpHabitRunner = options.rsvpHabitRunner;
634
+ this.orphanStartupDrain = options.orphanStartupDrain ?? drainOrphanProcessesBeforeStartup;
592
635
  }
593
636
  /* v8 ignore start -- default mailbox server wiring: production-only path, tests inject mailboxServerFactory stub instead. startMailboxHttpServer itself has full coverage in mailbox-http.test.ts @preserve */
594
637
  createDefaultMailboxServer() {
@@ -740,10 +783,7 @@ class OuroDaemon {
740
783
  }
741
784
  // MCP connections are lazily initialized per-agent during senseTurn
742
785
  // (daemon manages multiple agents; agent identity must be set before loading MCP config)
743
- /* v8 ignore start -- orphan cleanup + pidfile: calls process management functions @preserve */
744
- const killedOrphanPids = killOrphanProcesses(this.socketPath);
745
- await waitForOrphanProcessesToSettle(killedOrphanPids);
746
- /* v8 ignore stop */
786
+ await this.orphanStartupDrain(this.socketPath);
747
787
  await this.openCommandSocket();
748
788
  this.triggerAutoStartAgents();
749
789
  this.triggerAutoStartSensesWhenAgentsSettled();
@@ -1049,8 +1089,12 @@ class OuroDaemon {
1049
1089
  clearTimeout(this.senseAutostartTimer);
1050
1090
  this.senseAutostartTimer = null;
1051
1091
  }
1052
- await this.processManager.stopAll();
1053
- await this.senseManager?.stopAll();
1092
+ const workerStopTasks = [Promise.resolve().then(() => this.processManager.stopAll())];
1093
+ if (this.senseManager) {
1094
+ workerStopTasks.push(Promise.resolve().then(() => this.senseManager.stopAll()));
1095
+ }
1096
+ const workerStopResults = await Promise.allSettled(workerStopTasks);
1097
+ const workerStopFailures = workerStopResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1054
1098
  if (this.server) {
1055
1099
  // DO NOT `await` server.close() here. server.close() resolves only
1056
1100
  // after every open connection has closed. When stop() is invoked
@@ -1113,6 +1157,19 @@ class OuroDaemon {
1113
1157
  });
1114
1158
  }
1115
1159
  this.socketIdentity = null;
1160
+ if (workerStopFailures.length > 0) {
1161
+ const errorReason = `daemon shutdown could not drain ${workerStopFailures.length} managed worker group(s)`;
1162
+ (0, runtime_1.emitNervesEvent)({
1163
+ level: "error",
1164
+ component: "daemon",
1165
+ event: "daemon.worker_shutdown_error",
1166
+ message: errorReason,
1167
+ meta: {
1168
+ failures: workerStopFailures.map((error) => error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error rejection @preserve */ String(error)),
1169
+ },
1170
+ });
1171
+ throw new AggregateError(workerStopFailures, errorReason);
1172
+ }
1116
1173
  }
1117
1174
  async handleRawPayload(raw) {
1118
1175
  try {
@@ -1477,8 +1534,12 @@ class OuroDaemon {
1477
1534
  await this.start();
1478
1535
  return { ok: true, message: "daemon started" };
1479
1536
  case "daemon.stop":
1480
- await this.stop();
1481
- this.onStopCommandComplete?.();
1537
+ try {
1538
+ await this.stop();
1539
+ }
1540
+ finally {
1541
+ this.onStopCommandComplete?.();
1542
+ }
1482
1543
  return { ok: true, message: "daemon stopped" };
1483
1544
  case "daemon.restart": {
1484
1545
  // Restart is "stop + let launchctl respawn." Under launchctl's KeepAlive
@@ -1494,8 +1555,12 @@ class OuroDaemon {
1494
1555
  requestedBy: command.requestedBy ?? null,
1495
1556
  },
1496
1557
  });
1497
- await this.stop();
1498
- this.onStopCommandComplete?.();
1558
+ try {
1559
+ await this.stop();
1560
+ }
1561
+ finally {
1562
+ this.onStopCommandComplete?.();
1563
+ }
1499
1564
  return {
1500
1565
  ok: true,
1501
1566
  message: "daemon restarting — launchctl will respawn",
@@ -24,6 +24,7 @@ exports.checkLifecycle = checkLifecycle;
24
24
  exports.runDoctorChecks = runDoctorChecks;
25
25
  const runtime_1 = require("../../nerves/runtime");
26
26
  const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
27
+ const webhook_registration_1 = require("../../senses/bluebubbles/webhook-registration");
27
28
  const freshness_1 = require("./freshness");
28
29
  const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
29
30
  const runtime_credentials_1 = require("../runtime-credentials");
@@ -35,6 +36,8 @@ const config_1 = require("../../rsvp/config");
35
36
  const cutover_1 = require("../../rsvp/cutover");
36
37
  const diagnostics_1 = require("../../rsvp/diagnostics");
37
38
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
39
+ const DEFAULT_BLUEBUBBLES_PORT = 18_790;
40
+ const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
38
41
  // ── Category checkers ──
39
42
  function checkCliPath(deps) {
40
43
  const resolution = (0, ouro_path_installer_1.diagnoseOuroPath)({
@@ -412,10 +415,11 @@ async function checkSenses(deps) {
412
415
  });
413
416
  let upstreamContext = "upstream probe not run in this pass";
414
417
  if (deps.fetchImpl) {
418
+ const requestTimeoutMs = numberField(bluebubblesChannel, "requestTimeoutMs", DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS);
415
419
  const probe = await (0, bluebubbles_health_diagnostics_1.probeBlueBubblesHealth)({
416
420
  serverUrl,
417
421
  password,
418
- requestTimeoutMs: numberField(bluebubblesChannel, "requestTimeoutMs", DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS),
422
+ requestTimeoutMs,
419
423
  fetchImpl: deps.fetchImpl,
420
424
  });
421
425
  checks.push({
@@ -426,6 +430,22 @@ async function checkSenses(deps) {
426
430
  upstreamContext = probe.ok
427
431
  ? "upstream probe reachable — which does not prove inbound delivery"
428
432
  : "upstream probe failing";
433
+ const webhook = await (0, webhook_registration_1.inspectBlueBubblesWebhookRegistration)({
434
+ serverUrl,
435
+ password,
436
+ callbackPort: numberField(bluebubblesChannel, "port", DEFAULT_BLUEBUBBLES_PORT),
437
+ callbackPath: textField(bluebubblesChannel, "webhookPath") || DEFAULT_BLUEBUBBLES_WEBHOOK_PATH,
438
+ agentName,
439
+ machineId,
440
+ requestTimeoutMs,
441
+ listenerReady: true,
442
+ }, { fetchImpl: deps.fetchImpl });
443
+ checks.push({
444
+ id: "senses.bluebubbles.webhook",
445
+ label: `${agentDir} bluebubbles webhook`,
446
+ status: webhook.ok ? "pass" : "fail",
447
+ detail: webhook.detail,
448
+ });
429
449
  }
430
450
  const bluebubblesStateRoot = `${deps.bundlesRoot}/${agentDir}/state/senses/bluebubbles`;
431
451
  checks.push(senseInboundDeliveryCheck({