@tetsuo-ai/agenc 0.17.0 → 0.19.1
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/generated/agenc-runtime-manifest-v2.json +104 -102
- package/generated/startup-preflight.mjs +1 -0
- package/lib/generated-wrapper.mjs +49 -12
- package/lib/home-authority.d.mts +26 -0
- package/lib/home-authority.mjs +64 -0
- package/lib/runtime-manager.mjs +3 -13
- package/lib/sqlite-lock.d.mts +4 -0
- package/lib/sqlite-lock.mjs +164 -7
- package/package.json +6 -3
- package/scripts/postinstall.mjs +23 -2
- package/src/launcher.mjs +163 -38
package/src/launcher.mjs
CHANGED
|
@@ -5,8 +5,14 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { performance } from "node:perf_hooks";
|
|
9
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
+
import { resolveAgenCHome } from "../lib/home-authority.mjs";
|
|
11
|
+
import { preflightStartupArguments } from "../generated/startup-preflight.mjs";
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
export { resolveAgenCHome } from "../lib/home-authority.mjs";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_READY_TIMEOUT_MS = 45_000;
|
|
10
16
|
const DEFAULT_POLL_MS = 25;
|
|
11
17
|
const READY_TIMEOUT_ENV = "AGENC_DAEMON_READY_TIMEOUT_MS";
|
|
12
18
|
const AUTOSTART_ENV = "AGENC_DAEMON_AUTOSTART";
|
|
@@ -23,19 +29,12 @@ export function resolveReadyTimeoutMs(env = process.env) {
|
|
|
23
29
|
const raw = env[READY_TIMEOUT_ENV]?.trim();
|
|
24
30
|
if (raw === undefined || raw.length === 0) return DEFAULT_READY_TIMEOUT_MS;
|
|
25
31
|
const parsed = Number.parseInt(raw, 10);
|
|
26
|
-
if (!Number.isInteger(parsed) || String(parsed) !== raw || parsed <= 0) {
|
|
32
|
+
if (!Number.isInteger(parsed) || String(parsed) !== raw || parsed <= 0 || parsed > 2_147_483_647) {
|
|
27
33
|
throw new Error(`${READY_TIMEOUT_ENV} must be a positive integer`);
|
|
28
34
|
}
|
|
29
35
|
return parsed;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
export function resolveAgenCHome(env = process.env, userHome = homedir()) {
|
|
33
|
-
const configured = env.AGENC_HOME?.trim();
|
|
34
|
-
return configured && configured.length > 0
|
|
35
|
-
? configured
|
|
36
|
-
: join(userHome, ".agenc");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
38
|
export function resolveDaemonPidPath(env = process.env, userHome = homedir()) {
|
|
40
39
|
return join(resolveAgenCHome(env, userHome), "daemon.pid");
|
|
41
40
|
}
|
|
@@ -152,6 +151,8 @@ export async function spawnNodeScript(
|
|
|
152
151
|
spawnFn = spawn,
|
|
153
152
|
nodeBin = process.execPath,
|
|
154
153
|
nodeLibraryPath,
|
|
154
|
+
signal,
|
|
155
|
+
registerCleanup,
|
|
155
156
|
} = {},
|
|
156
157
|
) {
|
|
157
158
|
const childEnv = { ...env };
|
|
@@ -167,6 +168,15 @@ export async function spawnNodeScript(
|
|
|
167
168
|
// directory.
|
|
168
169
|
childEnv.LD_LIBRARY_PATH = nodeLibraryPath;
|
|
169
170
|
}
|
|
171
|
+
signal?.throwIfAborted();
|
|
172
|
+
if (signal !== undefined) {
|
|
173
|
+
const child = spawnFn(nodeBin, [scriptPath, ...args], { cwd, env: childEnv, stdio });
|
|
174
|
+
const result = observeStarter(child, signal);
|
|
175
|
+
registerCleanup?.(result.then(() => {}, (error) => {
|
|
176
|
+
if (error instanceof AggregateError) throw error;
|
|
177
|
+
}));
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
170
180
|
return new Promise((resolveExit, reject) => {
|
|
171
181
|
const child = spawnFn(nodeBin, [scriptPath, ...args], {
|
|
172
182
|
cwd,
|
|
@@ -217,14 +227,22 @@ export async function isDaemonReady(
|
|
|
217
227
|
export async function waitForDaemonReady(options = {}) {
|
|
218
228
|
const timeoutMs = options.timeoutMs ?? resolveReadyTimeoutMs(options.env);
|
|
219
229
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
const deadline = options.deadline ?? launchDeadline(timeoutMs, options.signal);
|
|
231
|
+
try {
|
|
232
|
+
for (;;) {
|
|
233
|
+
if (await withinLaunchDeadline(deadline, () => isDaemonReady(options))) return true;
|
|
234
|
+
if (options.probeOnly) return false;
|
|
235
|
+
const pause = Math.min(pollMs, deadline.remaining());
|
|
236
|
+
await withinLaunchDeadline(deadline, () => options.sleep
|
|
237
|
+
? options.sleep(pause)
|
|
238
|
+
: delay(pause, undefined, { signal: deadline.signal }));
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (options.deadline === undefined && error === deadline.timeoutError) return false;
|
|
242
|
+
throw error;
|
|
243
|
+
} finally {
|
|
244
|
+
if (options.deadline === undefined) deadline.dispose();
|
|
226
245
|
}
|
|
227
|
-
return isDaemonReady(options);
|
|
228
246
|
}
|
|
229
247
|
|
|
230
248
|
export async function ensureDaemonForLaunch({
|
|
@@ -239,35 +257,134 @@ export async function ensureDaemonForLaunch({
|
|
|
239
257
|
signalPid = process.kill,
|
|
240
258
|
spawnDaemonFn = spawnDaemon,
|
|
241
259
|
waitForReadyFn = waitForDaemonReady,
|
|
260
|
+
signal,
|
|
242
261
|
} = {}) {
|
|
243
262
|
if (isDaemonCommand(argv)) return { status: "skipped-daemon-command" };
|
|
244
263
|
if (!shouldAutostartDaemon(env)) return { status: "disabled" };
|
|
245
264
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
265
|
+
const deadline = launchDeadline(resolveReadyTimeoutMs(env), signal);
|
|
266
|
+
const cleanups = [];
|
|
267
|
+
const probeOptions = { env, userHome, readText, signalPid, deadline, signal: deadline.signal };
|
|
268
|
+
try {
|
|
269
|
+
if (await withinLaunchDeadline(deadline, () => waitForReadyFn({
|
|
270
|
+
...probeOptions, timeoutMs: deadline.remaining(), probeOnly: true,
|
|
271
|
+
}))) return { status: "already-running" };
|
|
272
|
+
|
|
273
|
+
await withinLaunchDeadline(deadline, () => spawnDaemonFn(runtimeBin, {
|
|
274
|
+
env: { ...env, [READY_TIMEOUT_ENV]: String(deadline.remaining()) },
|
|
275
|
+
cwd,
|
|
276
|
+
nodeBin: runtimeNodeBin,
|
|
277
|
+
nodeLibraryPath: runtimeNodeLibraryPath,
|
|
278
|
+
signal: deadline.signal,
|
|
279
|
+
registerCleanup: (cleanup) => {
|
|
280
|
+
cleanup.catch(() => {});
|
|
281
|
+
cleanups.push(cleanup);
|
|
282
|
+
},
|
|
283
|
+
}));
|
|
284
|
+
const ready = await withinLaunchDeadline(deadline, () => waitForReadyFn({
|
|
285
|
+
...probeOptions, timeoutMs: deadline.remaining(),
|
|
286
|
+
}));
|
|
287
|
+
if (!ready) throw deadline.timeoutError;
|
|
288
|
+
return { status: "started" };
|
|
289
|
+
} catch (error) {
|
|
290
|
+
deadline.abort(error);
|
|
291
|
+
throw error;
|
|
292
|
+
} finally {
|
|
293
|
+
deadline.dispose();
|
|
294
|
+
await Promise.all(cleanups);
|
|
258
295
|
}
|
|
296
|
+
}
|
|
259
297
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
nodeBin: runtimeNodeBin,
|
|
264
|
-
nodeLibraryPath: runtimeNodeLibraryPath,
|
|
265
|
-
});
|
|
266
|
-
const ready = await waitForReadyFn({ env, userHome, readText, signalPid });
|
|
267
|
-
if (!ready) {
|
|
268
|
-
throw new Error("AgenC daemon did not become ready before timeout");
|
|
298
|
+
function launchDeadline(timeoutMs, externalSignal) {
|
|
299
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
|
|
300
|
+
throw new RangeError("Daemon readiness timeout must be positive and no greater than 2147483647");
|
|
269
301
|
}
|
|
270
|
-
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const expires = performance.now() + timeoutMs;
|
|
304
|
+
const timeoutError = new Error(`AgenC daemon did not become ready within ${timeoutMs}ms`);
|
|
305
|
+
const abort = (reason) => controller.abort(reason);
|
|
306
|
+
const forwardAbort = () => abort(externalSignal.reason);
|
|
307
|
+
externalSignal?.addEventListener("abort", forwardAbort, { once: true });
|
|
308
|
+
if (externalSignal?.aborted) forwardAbort();
|
|
309
|
+
const timer = setTimeout(() => abort(timeoutError), timeoutMs);
|
|
310
|
+
return {
|
|
311
|
+
signal: controller.signal,
|
|
312
|
+
timeoutError,
|
|
313
|
+
abort,
|
|
314
|
+
remaining() {
|
|
315
|
+
if (performance.now() >= expires) abort(timeoutError);
|
|
316
|
+
controller.signal.throwIfAborted();
|
|
317
|
+
return Math.max(1, Math.ceil(expires - performance.now()));
|
|
318
|
+
},
|
|
319
|
+
dispose() {
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
externalSignal?.removeEventListener("abort", forwardAbort);
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function withinLaunchDeadline(deadline, operation) {
|
|
327
|
+
return new Promise((resolveResult, reject) => {
|
|
328
|
+
const onAbort = () => reject(deadline.signal.reason);
|
|
329
|
+
deadline.signal.addEventListener("abort", onAbort, { once: true });
|
|
330
|
+
Promise.resolve().then(() => {
|
|
331
|
+
deadline.remaining();
|
|
332
|
+
return operation();
|
|
333
|
+
}).then((value) => {
|
|
334
|
+
deadline.remaining();
|
|
335
|
+
resolveResult(value);
|
|
336
|
+
}).catch(reject).finally(() => deadline.signal.removeEventListener("abort", onAbort));
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function observeStarter(child, signal) {
|
|
341
|
+
return new Promise((resolveExit, reject) => {
|
|
342
|
+
let finished = false;
|
|
343
|
+
let cancelled = false;
|
|
344
|
+
let reason;
|
|
345
|
+
let killTimer;
|
|
346
|
+
let reapTimer;
|
|
347
|
+
const complete = (code, cleanupFailed = false) => {
|
|
348
|
+
if (finished) return;
|
|
349
|
+
finished = true;
|
|
350
|
+
clearTimeout(killTimer);
|
|
351
|
+
clearTimeout(reapTimer);
|
|
352
|
+
child.removeListener("close", onClose);
|
|
353
|
+
child.removeListener("error", onError);
|
|
354
|
+
signal.removeEventListener("abort", onAbort);
|
|
355
|
+
if (cleanupFailed) {
|
|
356
|
+
reject(new AggregateError([reason], "Daemon starter did not close within 1000ms of cancellation", { cause: reason }));
|
|
357
|
+
} else if (cancelled) {
|
|
358
|
+
reject(reason);
|
|
359
|
+
} else {
|
|
360
|
+
resolveExit(code ?? 1);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const kill = (terminationSignal) => {
|
|
364
|
+
try { child.kill(terminationSignal); } catch { return; }
|
|
365
|
+
};
|
|
366
|
+
const cancel = (error) => {
|
|
367
|
+
if (finished || cancelled) return;
|
|
368
|
+
cancelled = true;
|
|
369
|
+
reason = error;
|
|
370
|
+
killTimer = setTimeout(() => kill("SIGKILL"), 100);
|
|
371
|
+
reapTimer = setTimeout(() => complete(null, true), 1_000);
|
|
372
|
+
kill("SIGTERM");
|
|
373
|
+
};
|
|
374
|
+
const onError = (error) => cancel(error);
|
|
375
|
+
const onAbort = () => cancel(signal.reason);
|
|
376
|
+
const onClose = (code, exitSignal) => {
|
|
377
|
+
if (!cancelled && (signal.aborted || exitSignal)) {
|
|
378
|
+
cancelled = true;
|
|
379
|
+
reason = signal.aborted ? signal.reason : new Error(`agenc runtime exited from signal ${exitSignal}`);
|
|
380
|
+
}
|
|
381
|
+
complete(code);
|
|
382
|
+
};
|
|
383
|
+
child.on("error", onError);
|
|
384
|
+
child.once("close", onClose);
|
|
385
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
386
|
+
if (signal.aborted) onAbort();
|
|
387
|
+
});
|
|
271
388
|
}
|
|
272
389
|
|
|
273
390
|
export async function main(
|
|
@@ -282,6 +399,14 @@ export async function main(
|
|
|
282
399
|
userHome = homedir(),
|
|
283
400
|
} = {},
|
|
284
401
|
) {
|
|
402
|
+
const startupFailure = preflightStartupArguments(argv, {
|
|
403
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
404
|
+
isStdoutTTY: Boolean(process.stdout.isTTY),
|
|
405
|
+
});
|
|
406
|
+
if (startupFailure !== null) {
|
|
407
|
+
process.stderr.write(`${startupFailure.message}\n`);
|
|
408
|
+
return startupFailure.exitCode;
|
|
409
|
+
}
|
|
285
410
|
// Resolve (and, in a published install, download + verify) the runtime before
|
|
286
411
|
// anything tries to spawn it. Sync default is avoided: it returns null when
|
|
287
412
|
// runtime isn't an npm dep, which is the normal published case.
|