@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 +12 -0
- package/dist/heart/daemon/cli-defaults.js +88 -7
- package/dist/heart/daemon/daemon.js +41 -3
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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();
|