@ouro.bot/cli 0.1.0-alpha.550 → 0.1.0-alpha.552

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,18 @@
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.552",
6
+ "changes": [
7
+ "`ouro up` now bootstraps the daemon LaunchAgent into the current login session when it is not already loaded, waits for the launchd-owned daemon socket to settle, and keeps the launchd adoption non-fatal if macOS refuses the bootstrap."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.551",
12
+ "changes": [
13
+ "`ouro up` now waits for SIGTERMed orphan daemon processes to settle before opening the replacement Unix socket, preventing the previous daemon from unlinking the new daemon's command socket during orphan cleanup."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.550",
6
18
  "changes": [
@@ -41,6 +41,8 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.defaultStartDaemonProcess = defaultStartDaemonProcess;
43
43
  exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
44
+ exports.isDaemonLaunchAgentLoaded = isDaemonLaunchAgentLoaded;
45
+ exports.waitForBootstrappedDaemonSocket = waitForBootstrappedDaemonSocket;
44
46
  exports.defaultListDiscoveredAgents = defaultListDiscoveredAgents;
45
47
  exports.defaultRunSerpentGuide = defaultRunSerpentGuide;
46
48
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
@@ -216,7 +218,56 @@ function defaultFallbackPendingMessage(command) {
216
218
  });
217
219
  return pendingPath;
218
220
  }
219
- function defaultEnsureDaemonBootPersistence(socketPath) {
221
+ function currentUserUid() {
222
+ return process.getuid?.() ?? 0;
223
+ }
224
+ function launchAgentDomain(userUid = currentUserUid()) {
225
+ return `gui/${userUid}`;
226
+ }
227
+ function isDaemonLaunchAgentLoaded(deps) {
228
+ const userUid = deps?.userUid ?? currentUserUid();
229
+ const exec = deps?.exec ?? ((cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); });
230
+ try {
231
+ exec(`launchctl print ${launchAgentDomain(userUid)}/${launchd_1.DAEMON_PLIST_LABEL}`);
232
+ return true;
233
+ }
234
+ catch {
235
+ return false;
236
+ }
237
+ }
238
+ async function waitForBootstrappedDaemonSocket(socketPath, deps = {}) {
239
+ const checkSocketAlive = deps.checkSocketAlive ?? socket_client_1.checkDaemonSocketAlive;
240
+ const now = deps.now ?? Date.now;
241
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
242
+ const timeoutMs = deps.timeoutMs ?? 30_000;
243
+ const initialSettleMs = deps.initialSettleMs ?? 1_000;
244
+ const pollIntervalMs = deps.pollIntervalMs ?? 250;
245
+ const requiredConsecutiveAliveChecks = deps.requiredConsecutiveAliveChecks ?? 2;
246
+ const deadline = now() + timeoutMs;
247
+ const firstTrustworthyCheckAt = now() + initialSettleMs;
248
+ let consecutiveAliveChecks = 0;
249
+ while (now() < deadline) {
250
+ await sleep(pollIntervalMs);
251
+ if (now() < firstTrustworthyCheckAt)
252
+ continue;
253
+ if (await checkSocketAlive(socketPath)) {
254
+ consecutiveAliveChecks += 1;
255
+ if (consecutiveAliveChecks >= requiredConsecutiveAliveChecks)
256
+ return;
257
+ }
258
+ else {
259
+ consecutiveAliveChecks = 0;
260
+ }
261
+ }
262
+ (0, runtime_1.emitNervesEvent)({
263
+ level: "warn",
264
+ component: "daemon",
265
+ event: "daemon.launchd_bootstrap_socket_wait_timeout",
266
+ message: "launchd bootstrap finished but daemon socket did not settle before timeout",
267
+ meta: { socketPath },
268
+ });
269
+ }
270
+ async function defaultEnsureDaemonBootPersistence(socketPath) {
220
271
  if (process.platform !== "darwin") {
221
272
  return;
222
273
  }
@@ -238,18 +289,48 @@ function defaultEnsureDaemonBootPersistence(socketPath) {
238
289
  });
239
290
  }
240
291
  const logDir = (0, identity_1.getAgentDaemonLogsDir)();
241
- // Write plist only do NOT launchctl bootstrap.
242
- // The daemon is already running (started by ouro up). Bootstrapping would
243
- // start a SECOND daemon via launchd's RunAtLoad, causing a race where
244
- // killOrphanProcesses kills the first daemon and both end up dead.
245
- // The plist on disk is sufficient: launchd picks it up on login.
246
- (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
292
+ const plistPath = (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
247
293
  nodePath: process.execPath,
248
294
  entryPath,
249
295
  socketPath,
250
296
  logDir,
251
297
  envPath: process.env.PATH,
252
298
  });
299
+ const userUid = currentUserUid();
300
+ if (isDaemonLaunchAgentLoaded({ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); }, userUid })) {
301
+ (0, runtime_1.emitNervesEvent)({
302
+ component: "daemon",
303
+ event: "daemon.launchd_bootstrap_skipped_loaded",
304
+ message: "daemon launch agent already loaded",
305
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
306
+ });
307
+ return;
308
+ }
309
+ try {
310
+ (0, runtime_1.emitNervesEvent)({
311
+ component: "daemon",
312
+ event: "daemon.launchd_bootstrap_start",
313
+ message: "bootstrapping daemon launch agent for current login session",
314
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
315
+ });
316
+ (0, child_process_1.execSync)(`launchctl bootstrap ${launchAgentDomain(userUid)} "${plistPath}"`, { stdio: "ignore" });
317
+ (0, runtime_1.emitNervesEvent)({
318
+ component: "daemon",
319
+ event: "daemon.launchd_bootstrap_end",
320
+ message: "daemon launch agent bootstrapped for current login session",
321
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL },
322
+ });
323
+ await waitForBootstrappedDaemonSocket(socketPath);
324
+ }
325
+ catch (error) {
326
+ (0, runtime_1.emitNervesEvent)({
327
+ level: "warn",
328
+ component: "daemon",
329
+ event: "daemon.launchd_bootstrap_error",
330
+ message: "failed to bootstrap daemon launch agent for current login session",
331
+ meta: { plistPath, label: launchd_1.DAEMON_PLIST_LABEL, error: error instanceof Error ? error.message : String(error) },
332
+ });
333
+ }
253
334
  }
254
335
  function defaultPrepareDaemonRuntimeReplacement() {
255
336
  if (process.platform !== "darwin") {
@@ -37,6 +37,7 @@ exports.OuroDaemon = void 0;
37
37
  exports.parseOrphanPidsFromPs = parseOrphanPidsFromPs;
38
38
  exports.filterPidfilePidsToActualOrphans = filterPidfilePidsToActualOrphans;
39
39
  exports.mergeUniqueOrphanPids = mergeUniqueOrphanPids;
40
+ exports.waitForOrphanProcessesToSettle = waitForOrphanProcessesToSettle;
40
41
  exports.killOrphanProcesses = killOrphanProcesses;
41
42
  exports.writePidfile = writePidfile;
42
43
  exports.handleAgentSenseTurn = handleAgentSenseTurn;
@@ -176,6 +177,40 @@ function mergeUniqueOrphanPids(...sources) {
176
177
  }
177
178
  return merged;
178
179
  }
180
+ const ORPHAN_CLEANUP_SETTLE_TIMEOUT_MS = 5_000;
181
+ const ORPHAN_CLEANUP_SETTLE_POLL_INTERVAL_MS = 50;
182
+ /* v8 ignore start -- process liveness probe; pure wait behavior covered via injected deps @preserve */
183
+ function defaultIsPidAlive(pid) {
184
+ try {
185
+ process.kill(pid, 0);
186
+ return true;
187
+ }
188
+ catch (error) {
189
+ return error.code === "EPERM";
190
+ }
191
+ }
192
+ /* v8 ignore stop */
193
+ /* v8 ignore start -- real timer wiring; wait behavior covered via injected sleep @preserve */
194
+ async function defaultSettleSleep(ms) {
195
+ await new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ /* v8 ignore stop */
198
+ async function waitForOrphanProcessesToSettle(pids, deps = {}) {
199
+ if (pids.length === 0)
200
+ return [];
201
+ const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
202
+ const now = deps.now ?? Date.now;
203
+ const sleep = deps.sleep ?? defaultSettleSleep;
204
+ const timeoutMs = deps.timeoutMs ?? ORPHAN_CLEANUP_SETTLE_TIMEOUT_MS;
205
+ const pollIntervalMs = deps.pollIntervalMs ?? ORPHAN_CLEANUP_SETTLE_POLL_INTERVAL_MS;
206
+ const deadline = now() + timeoutMs;
207
+ let survivors = pids.filter(isPidAlive);
208
+ while (survivors.length > 0 && now() < deadline) {
209
+ await sleep(pollIntervalMs);
210
+ survivors = pids.filter(isPidAlive);
211
+ }
212
+ return survivors;
213
+ }
179
214
  /* v8 ignore start -- shells out to ps; covered by filterPidfilePidsToActualOrphans unit tests via injected runner @preserve */
180
215
  function runPsCheck(pids) {
181
216
  try {
@@ -217,7 +252,7 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
217
252
  message: "blocked orphan cleanup for non-production daemon socket",
218
253
  meta: { socketPath, pidfilePath: PIDFILE_PATH },
219
254
  });
220
- return;
255
+ return [];
221
256
  }
222
257
  if (isVitestProcess()) {
223
258
  (0, runtime_1.emitNervesEvent)({
@@ -227,7 +262,7 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
227
262
  message: "blocked killOrphanProcesses from touching real pidfile under vitest",
228
263
  meta: { pidfilePath: PIDFILE_PATH },
229
264
  });
230
- return;
265
+ return [];
231
266
  }
232
267
  try {
233
268
  let pidfileOrphans = [];
@@ -269,6 +304,7 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
269
304
  meta: { pids: pidsToKill },
270
305
  });
271
306
  }
307
+ return pidsToKill;
272
308
  }
273
309
  catch (error) {
274
310
  (0, runtime_1.emitNervesEvent)({
@@ -278,6 +314,7 @@ function killOrphanProcesses(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_
278
314
  message: "failed to clean up orphaned ouro processes",
279
315
  meta: { error: error instanceof Error ? error.message : String(error) },
280
316
  });
317
+ return [];
281
318
  }
282
319
  }
283
320
  /**
@@ -594,7 +631,8 @@ class OuroDaemon {
594
631
  // MCP connections are lazily initialized per-agent during senseTurn
595
632
  // (daemon manages multiple agents; agent identity must be set before loading MCP config)
596
633
  /* v8 ignore start -- orphan cleanup + pidfile: calls process management functions @preserve */
597
- killOrphanProcesses(this.socketPath);
634
+ const killedOrphanPids = killOrphanProcesses(this.socketPath);
635
+ await waitForOrphanProcessesToSettle(killedOrphanPids);
598
636
  /* v8 ignore stop */
599
637
  await this.openCommandSocket();
600
638
  this.triggerAutoStartAgents();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.550",
3
+ "version": "0.1.0-alpha.552",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",