@sagentlab/navarch-runtime 0.1.50 → 0.1.51
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/README.md +1 -0
- package/dist/activity-watchdog.cjs +17 -6
- package/dist/adapter-watchdog.cjs +5 -2
- package/dist/cli.cjs +7 -3
- package/dist/config.cjs +1 -0
- package/dist/exit-conditions.cjs +5 -3
- package/dist/session.cjs +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -360,6 +360,7 @@ unchanged across the deployment.
|
|
|
360
360
|
| `NAVARCH_LEASE_HEARTBEAT_INTERVAL_MS` | `300000` | Per-lease heartbeat interval; must stay well under the 60-minute lease TTL (schema-design.md §4). |
|
|
361
361
|
| `NAVARCH_SESSION_TIMEOUT_MS` | `0` (disabled) | Optional absolute runtime budget across agent turns in a claimed session. |
|
|
362
362
|
| `NAVARCH_SESSION_IDLE_TIMEOUT_MS` | `2700000` (45 min) | Stop after a continuously observed window with no stdout/stderr or process-tree CPU activity. `0` disables idle detection. |
|
|
363
|
+
| `NAVARCH_OPENCODE_STARTUP_TIMEOUT_MS` | `120000` (2 min) | Stop OpenCode when it produces no stdout/stderr during startup. This catches provider auth/quota failures that leave its CLI process alive. `0` disables the startup deadline. |
|
|
363
364
|
| `NAVARCH_WORKTREE_CLEANUP_INTERVAL_MS` | `3600000` (1 hour) | How often the runtime scans for session worktrees abandoned by a hard exit or host restart. |
|
|
364
365
|
| `NAVARCH_WORKTREE_STALE_AFTER_MS` | `86400000` (24 hours) | Minimum inactivity age before an abandoned session worktree is removed. Active sessions are always protected. |
|
|
365
366
|
| `NAVARCH_GIT_AUTHOR_NAME` / `NAVARCH_GIT_AUTHOR_EMAIL` | `sagentlab` / `z@sagentlab.com` | Git identity forced into session commits so host-level personal config is not inherited; override both for a project-authorized bot. |
|
|
@@ -16,6 +16,7 @@ class ActivityWatchdog {
|
|
|
16
16
|
stopped = false;
|
|
17
17
|
polling = false;
|
|
18
18
|
pollTimer;
|
|
19
|
+
startupTimer;
|
|
19
20
|
hardTimer;
|
|
20
21
|
constructor(options) {
|
|
21
22
|
this.options = options;
|
|
@@ -24,16 +25,23 @@ class ActivityWatchdog {
|
|
|
24
25
|
if (options.idleTimeoutMs > 0) {
|
|
25
26
|
this.pollTimer = setInterval(() => void this.poll(), Math.min(30_000, options.idleTimeoutMs / 4));
|
|
26
27
|
}
|
|
28
|
+
if ((options.startupTimeoutMs ?? 0) > 0) {
|
|
29
|
+
this.startupTimer = setTimeout(() => this.expire("startup"), options.startupTimeoutMs);
|
|
30
|
+
}
|
|
27
31
|
if (options.timeoutMs > 0)
|
|
28
32
|
this.armHardLimit();
|
|
29
33
|
}
|
|
30
34
|
activity() {
|
|
31
|
-
if (!this.stopped)
|
|
35
|
+
if (!this.stopped) {
|
|
32
36
|
this.lastActivity = this.now();
|
|
37
|
+
clearTimeout(this.startupTimer);
|
|
38
|
+
this.startupTimer = undefined;
|
|
39
|
+
}
|
|
33
40
|
}
|
|
34
41
|
stop() {
|
|
35
42
|
this.stopped = true;
|
|
36
43
|
clearInterval(this.pollTimer);
|
|
44
|
+
clearTimeout(this.startupTimer);
|
|
37
45
|
clearTimeout(this.hardTimer);
|
|
38
46
|
}
|
|
39
47
|
armHardLimit() {
|
|
@@ -61,8 +69,11 @@ class ActivityWatchdog {
|
|
|
61
69
|
this.observed = false;
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
72
|
+
// Process-tree activity keeps the ordinary idle window alive, but only
|
|
73
|
+
// actual adapter output proves that a CLI got through startup. Some
|
|
74
|
+
// providers leave a quiet process behind after an auth/quota failure.
|
|
64
75
|
if (!this.observed || signature !== this.signature)
|
|
65
|
-
this.
|
|
76
|
+
this.lastActivity = this.now();
|
|
66
77
|
this.signature = signature;
|
|
67
78
|
this.observed = true;
|
|
68
79
|
if (this.now() - this.lastActivity >= this.options.idleTimeoutMs)
|
|
@@ -79,15 +90,15 @@ class ActivityWatchdog {
|
|
|
79
90
|
}
|
|
80
91
|
exports.ActivityWatchdog = ActivityWatchdog;
|
|
81
92
|
/** No command arguments or environment values are collected from other jobs. */
|
|
82
|
-
async function processActivity(pid, containerName) {
|
|
93
|
+
async function processActivity(pid, containerName, ignoreRootCpu = false) {
|
|
83
94
|
if (!containerName && pid === undefined)
|
|
84
95
|
return null;
|
|
85
96
|
const output = await new Promise((resolve) => {
|
|
86
97
|
(0, node_child_process_1.execFile)(containerName ? "docker" : "ps", containerName ? ["top", containerName, "-eo", "pid=,ppid=,time="] : ["-e", "-o", "pid=,ppid=,time="], { timeout: 5_000, maxBuffer: 4 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
|
|
87
98
|
});
|
|
88
|
-
return output === null ? null : processTreeSignature(output, containerName ? undefined : pid);
|
|
99
|
+
return output === null ? null : processTreeSignature(output, containerName ? undefined : pid, ignoreRootCpu);
|
|
89
100
|
}
|
|
90
|
-
function processTreeSignature(output, rootPid) {
|
|
101
|
+
function processTreeSignature(output, rootPid, ignoreRootCpu = false) {
|
|
91
102
|
const rows = output.split("\n").flatMap((line) => {
|
|
92
103
|
const match = /^\s*(\d+)\s+(\d+)\s+([\d:.\-]+)\s*$/.exec(line);
|
|
93
104
|
return match ? [{ pid: Number(match[1]), parent: Number(match[2]), cpu: match[3] }] : [];
|
|
@@ -106,5 +117,5 @@ function processTreeSignature(output, rootPid) {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
return rows.filter((row) => included.has(row.pid)).sort((a, b) => a.pid - b.pid)
|
|
109
|
-
.map((row) => `${row.pid}:${row.cpu}`).join(",");
|
|
120
|
+
.map((row) => `${row.pid}:${ignoreRootCpu && row.pid === rootPid ? "root" : row.cpu}`).join(",");
|
|
110
121
|
}
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runWithActivityWatchdog = runWithActivityWatchdog;
|
|
4
4
|
const activity_watchdog_cjs_1 = require("./activity-watchdog.cjs");
|
|
5
5
|
/** Shared policy for every host/Docker/ACP adapter; adapters still own cancellation. */
|
|
6
|
-
async function runWithActivityWatchdog(adapter, options, idleTimeoutMs) {
|
|
6
|
+
async function runWithActivityWatchdog(adapter, options, idleTimeoutMs, startupTimeoutMs = 0) {
|
|
7
7
|
const controller = new AbortController();
|
|
8
8
|
const onAbort = () => controller.abort();
|
|
9
9
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -13,8 +13,11 @@ async function runWithActivityWatchdog(adapter, options, idleTimeoutMs) {
|
|
|
13
13
|
let timeoutReason;
|
|
14
14
|
const watchdog = new activity_watchdog_cjs_1.ActivityWatchdog({
|
|
15
15
|
idleTimeoutMs,
|
|
16
|
+
startupTimeoutMs,
|
|
16
17
|
timeoutMs: options.timeoutMs,
|
|
17
|
-
|
|
18
|
+
// OpenCode keeps doing root-process housekeeping after provider failures.
|
|
19
|
+
// Descendant work and stdout/stderr still count, but that churn does not.
|
|
20
|
+
probe: () => (0, activity_watchdog_cjs_1.processActivity)(pid, options.dockerExec?.containerName, adapter.agentType === "opencode" && !options.dockerExec),
|
|
18
21
|
onTimeout: (reason) => {
|
|
19
22
|
timeoutReason = reason;
|
|
20
23
|
controller.abort();
|
package/dist/cli.cjs
CHANGED
|
@@ -210,6 +210,7 @@ async function startCommand(flags) {
|
|
|
210
210
|
const config = { ...baseConfig, agentType, model: modelFromFlags(flags, agentType, identity) };
|
|
211
211
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
212
212
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
213
|
+
const runtimeShutdown = new AbortController();
|
|
213
214
|
const activeSessionIds = new Set();
|
|
214
215
|
const worktreeJanitor = new worktree_janitor_cjs_1.WorktreeJanitor({
|
|
215
216
|
workspaceRoot: config.workspaceRoot,
|
|
@@ -219,7 +220,7 @@ async function startCommand(flags) {
|
|
|
219
220
|
});
|
|
220
221
|
const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => {
|
|
221
222
|
activeSessionIds.add(sessionId);
|
|
222
|
-
return (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId)
|
|
223
|
+
return (0, session_cjs_1.runSession)({ api, config, shutdownSignal: runtimeShutdown.signal }, claimed, sessionId)
|
|
223
224
|
.finally(() => activeSessionIds.delete(sessionId));
|
|
224
225
|
});
|
|
225
226
|
const bootId = (0, node_crypto_1.randomUUID)();
|
|
@@ -243,8 +244,11 @@ async function startCommand(flags) {
|
|
|
243
244
|
: `machine credentials for ${identity.name} were rejected by ${identity.api_base}; it can no longer claim tasks.`);
|
|
244
245
|
log.error(`Re-enroll this machine with a fresh "Connect an agent" token, then run ` +
|
|
245
246
|
`\`${superviseInvocation(config.configDir)}\` again.`);
|
|
246
|
-
//
|
|
247
|
-
//
|
|
247
|
+
// Removal/revocation is terminal: stop detached agent process groups so
|
|
248
|
+
// a provider-side hang cannot keep consuming local resources forever.
|
|
249
|
+
// runSession checkpoints or retains the worktree before releasing its
|
|
250
|
+
// capacity slot.
|
|
251
|
+
runtimeShutdown.abort();
|
|
248
252
|
void capacity.waitForIdle().then(() => process.exit(CREDENTIALS_REJECTED_EXIT_CODE));
|
|
249
253
|
});
|
|
250
254
|
updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
|
package/dist/config.cjs
CHANGED
|
@@ -108,6 +108,7 @@ function loadRuntimeConfig(env = process.env) {
|
|
|
108
108
|
leaseHeartbeatIntervalMs,
|
|
109
109
|
sessionTimeoutMs: envRuntimeLimit(env, "NAVARCH_SESSION_TIMEOUT_MS", 0),
|
|
110
110
|
sessionIdleTimeoutMs: envRuntimeLimit(env, "NAVARCH_SESSION_IDLE_TIMEOUT_MS", 45 * 60 * 1000),
|
|
111
|
+
opencodeStartupTimeoutMs: envRuntimeLimit(env, "NAVARCH_OPENCODE_STARTUP_TIMEOUT_MS", 2 * 60 * 1000),
|
|
111
112
|
worktreeCleanupIntervalMs: envInt(env, "NAVARCH_WORKTREE_CLEANUP_INTERVAL_MS", 60 * 60 * 1000),
|
|
112
113
|
worktreeStaleAfterMs: envInt(env, "NAVARCH_WORKTREE_STALE_AFTER_MS", 24 * 60 * 60 * 1000),
|
|
113
114
|
agentType,
|
package/dist/exit-conditions.cjs
CHANGED
|
@@ -233,9 +233,11 @@ function mapExitCondition(result) {
|
|
|
233
233
|
return {
|
|
234
234
|
leaseOutcome: "failed",
|
|
235
235
|
exitStatus: "killed",
|
|
236
|
-
reportSummary: (result.timeoutReason === "
|
|
237
|
-
? "Session stopped:
|
|
238
|
-
:
|
|
236
|
+
reportSummary: (result.timeoutReason === "startup"
|
|
237
|
+
? "Session stopped: the agent produced no output within the configured startup window."
|
|
238
|
+
: result.timeoutReason === "idle"
|
|
239
|
+
? "Session stopped: no output or process-tree CPU activity within the configured idle window."
|
|
240
|
+
: "Session killed: exceeded the configured max session duration.") +
|
|
239
241
|
(result.checkpointPath ? ` Local recovery checkpoint: ${result.checkpointPath}` : "") +
|
|
240
242
|
(result.retainedWorktreePath ? ` Recovery worktree retained: ${result.retainedWorktreePath}` : ""),
|
|
241
243
|
evidenceUrls,
|
package/dist/session.cjs
CHANGED
|
@@ -211,6 +211,14 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
211
211
|
let activeAbortController = null;
|
|
212
212
|
let leaseLost = false;
|
|
213
213
|
let leaseGone = false;
|
|
214
|
+
const abortForRuntimeShutdown = () => {
|
|
215
|
+
// Machine removal/revocation also makes lease completion impossible.
|
|
216
|
+
// Treat it like a terminal lease loss so the worktree is checkpointed
|
|
217
|
+
// and no remediation turn or authenticated API call is attempted.
|
|
218
|
+
leaseLost = true;
|
|
219
|
+
leaseGone = true;
|
|
220
|
+
activeAbortController?.abort();
|
|
221
|
+
};
|
|
214
222
|
let sessionClosing = false;
|
|
215
223
|
let retainWorktree = false;
|
|
216
224
|
let heartbeatInFlight = null;
|
|
@@ -392,6 +400,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
392
400
|
activeAbortController?.abort();
|
|
393
401
|
});
|
|
394
402
|
const heartbeatTimer = setInterval(() => void pollLease(), config.leaseHeartbeatIntervalMs);
|
|
403
|
+
deps.shutdownSignal?.addEventListener("abort", abortForRuntimeShutdown);
|
|
404
|
+
if (deps.shutdownSignal?.aborted)
|
|
405
|
+
abortForRuntimeShutdown();
|
|
395
406
|
lifecycle.enteredMainSessionTry = true;
|
|
396
407
|
try {
|
|
397
408
|
await gitWorktree.prepare();
|
|
@@ -427,7 +438,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
427
438
|
// notification and start the first turn with the corrected prompt.
|
|
428
439
|
pendingGuidance = [];
|
|
429
440
|
activeAbortController = new AbortController();
|
|
430
|
-
if (leaseLost)
|
|
441
|
+
if (leaseLost || deps.shutdownSignal?.aborted)
|
|
431
442
|
activeAbortController.abort();
|
|
432
443
|
const runPrompt = nextPrompt ??
|
|
433
444
|
(deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
@@ -459,7 +470,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
459
470
|
cwd: gitWorktree.worktreePath,
|
|
460
471
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
461
472
|
signal: activeAbortController.signal,
|
|
462
|
-
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000);
|
|
473
|
+
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
463
474
|
activeAbortController = null;
|
|
464
475
|
if (turnResult.timedOut || leaseLost)
|
|
465
476
|
await checkpointForRecovery(turnResult);
|
|
@@ -711,6 +722,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
711
722
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
712
723
|
}
|
|
713
724
|
finally {
|
|
725
|
+
deps.shutdownSignal?.removeEventListener("abort", abortForRuntimeShutdown);
|
|
714
726
|
sessionClosing = true;
|
|
715
727
|
clearInterval(heartbeatTimer);
|
|
716
728
|
heartbeatGuard.stop();
|
package/package.json
CHANGED