@sagentlab/navarch-runtime 0.1.49 → 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 +11 -0
- package/dist/activity-watchdog.cjs +17 -6
- package/dist/adapter-watchdog.cjs +5 -2
- package/dist/cli.cjs +27 -8
- package/dist/config.cjs +1 -0
- package/dist/exit-conditions.cjs +5 -3
- package/dist/session.cjs +32 -25
- 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. |
|
|
@@ -837,3 +838,13 @@ Each invocation rebuilds from Git blobs at the requested commit, avoiding stale
|
|
|
837
838
|
persistent indexes. Bounds are 2,000 files, 10 MB total source, 500 KB per file,
|
|
838
839
|
100 returned nodes and 300 edges. A later persistent index can optimize large
|
|
839
840
|
repositories without changing the query format.
|
|
841
|
+
|
|
842
|
+
### Pinning a connection model
|
|
843
|
+
|
|
844
|
+
`connect --agent codex --model gpt-6-astra` saves the model alongside the local
|
|
845
|
+
machine identity. `start` and `supervise` reuse that model, including after worker
|
|
846
|
+
restarts. Their optional `--model` flag overrides the saved choice. The pin wins
|
|
847
|
+
over the project's session model for the selected adapter only; switching agent
|
|
848
|
+
types does not reuse another adapter's pin. Reconnect without `--model` to return
|
|
849
|
+
to project-controlled model selection. Older identities without a model keep
|
|
850
|
+
using the control-plane execution policy.
|
|
@@ -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
|
@@ -76,6 +76,14 @@ function agentFromFlag(flags) {
|
|
|
76
76
|
}
|
|
77
77
|
return value;
|
|
78
78
|
}
|
|
79
|
+
/** Saved pins only apply to the adapter they were selected for. */
|
|
80
|
+
function modelFromFlags(flags, agentType, identity) {
|
|
81
|
+
const model = flags.model ?? (identity?.agent_type === agentType ? identity.model : undefined);
|
|
82
|
+
if (model !== undefined && (!model.trim() || model === "true" || /\s/.test(model))) {
|
|
83
|
+
throw new Error("--model requires a non-empty model reference without whitespace.");
|
|
84
|
+
}
|
|
85
|
+
return model;
|
|
86
|
+
}
|
|
79
87
|
function parseArgs(argv) {
|
|
80
88
|
const [command, ...rest] = argv;
|
|
81
89
|
const flags = {};
|
|
@@ -118,6 +126,7 @@ async function registerCommand(flags) {
|
|
|
118
126
|
.filter(Boolean);
|
|
119
127
|
const ownerZone = flags["owner-zone"] ?? config.ownerZone;
|
|
120
128
|
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
129
|
+
modelFromFlags(flags, agentType); // Validate before consuming an enrollment token.
|
|
121
130
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
122
131
|
const result = await client.registerMachine({
|
|
123
132
|
enrollment_token: enrollmentToken,
|
|
@@ -132,6 +141,7 @@ async function registerCommand(flags) {
|
|
|
132
141
|
name,
|
|
133
142
|
api_base: apiBase,
|
|
134
143
|
agent_type: agentType,
|
|
144
|
+
model: modelFromFlags(flags, agentType),
|
|
135
145
|
});
|
|
136
146
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
137
147
|
console.log("Machine registered.");
|
|
@@ -166,6 +176,7 @@ async function connectCommand(flags) {
|
|
|
166
176
|
.map((s) => s.trim())
|
|
167
177
|
.filter(Boolean);
|
|
168
178
|
const agentType = agentFromFlag(flags) ?? config.agentType;
|
|
179
|
+
modelFromFlags(flags, agentType); // Validate before consuming an enrollment token.
|
|
169
180
|
const client = new api_cjs_1.NavarchApiClient({ baseUrl: apiBase });
|
|
170
181
|
const result = await client.connectMachine({
|
|
171
182
|
enrollment_token: enrollmentToken,
|
|
@@ -180,6 +191,7 @@ async function connectCommand(flags) {
|
|
|
180
191
|
name,
|
|
181
192
|
api_base: apiBase,
|
|
182
193
|
agent_type: agentType,
|
|
194
|
+
model: modelFromFlags(flags, agentType),
|
|
183
195
|
});
|
|
184
196
|
// Printed exactly once. Never logged or echoed again after this point.
|
|
185
197
|
console.log("Machine connected.");
|
|
@@ -195,9 +207,10 @@ async function startCommand(flags) {
|
|
|
195
207
|
// time, and finally the backwards-compatible Claude Code default.
|
|
196
208
|
const agentType = agentFromFlag(flags) ??
|
|
197
209
|
(process.env.NAVARCH_AGENT ? baseConfig.agentType : identity.agent_type ?? baseConfig.agentType);
|
|
198
|
-
const config = { ...baseConfig, agentType };
|
|
210
|
+
const config = { ...baseConfig, agentType, model: modelFromFlags(flags, agentType, identity) };
|
|
199
211
|
const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
|
|
200
212
|
const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
|
|
213
|
+
const runtimeShutdown = new AbortController();
|
|
201
214
|
const activeSessionIds = new Set();
|
|
202
215
|
const worktreeJanitor = new worktree_janitor_cjs_1.WorktreeJanitor({
|
|
203
216
|
workspaceRoot: config.workspaceRoot,
|
|
@@ -207,7 +220,7 @@ async function startCommand(flags) {
|
|
|
207
220
|
});
|
|
208
221
|
const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => {
|
|
209
222
|
activeSessionIds.add(sessionId);
|
|
210
|
-
return (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId)
|
|
223
|
+
return (0, session_cjs_1.runSession)({ api, config, shutdownSignal: runtimeShutdown.signal }, claimed, sessionId)
|
|
211
224
|
.finally(() => activeSessionIds.delete(sessionId));
|
|
212
225
|
});
|
|
213
226
|
const bootId = (0, node_crypto_1.randomUUID)();
|
|
@@ -231,8 +244,11 @@ async function startCommand(flags) {
|
|
|
231
244
|
: `machine credentials for ${identity.name} were rejected by ${identity.api_base}; it can no longer claim tasks.`);
|
|
232
245
|
log.error(`Re-enroll this machine with a fresh "Connect an agent" token, then run ` +
|
|
233
246
|
`\`${superviseInvocation(config.configDir)}\` again.`);
|
|
234
|
-
//
|
|
235
|
-
//
|
|
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();
|
|
236
252
|
void capacity.waitForIdle().then(() => process.exit(CREDENTIALS_REJECTED_EXIT_CODE));
|
|
237
253
|
});
|
|
238
254
|
updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
|
|
@@ -277,6 +293,9 @@ async function superviseCommand(flags) {
|
|
|
277
293
|
const agentType = agentFromFlag(flags) ??
|
|
278
294
|
(process.env.NAVARCH_AGENT ? config.agentType : identity.agent_type ?? config.agentType);
|
|
279
295
|
const workerArgs = ["--agent", agentType];
|
|
296
|
+
const model = modelFromFlags(flags, agentType, identity);
|
|
297
|
+
if (model)
|
|
298
|
+
workerArgs.push("--model", model);
|
|
280
299
|
// Pin the identity for this supervisor's lifetime. Without this snapshot, a
|
|
281
300
|
// replacement worker rereads machine.json after an automatic update and can
|
|
282
301
|
// silently become a different machine if another terminal reused the same
|
|
@@ -332,11 +351,11 @@ function helpText() {
|
|
|
332
351
|
|
|
333
352
|
Usage:
|
|
334
353
|
navarch-runtime register --token <enrollment-token> --name <machine-name> \\
|
|
335
|
-
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
354
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>] [--capabilities a,b] [--max-sessions N] [--owner-zone z] [--api-base url]
|
|
336
355
|
navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
|
|
337
|
-
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
338
|
-
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp]
|
|
339
|
-
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp]
|
|
356
|
+
[--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
|
|
357
|
+
navarch-runtime start [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>]
|
|
358
|
+
navarch-runtime supervise [--config-dir <path>] [--agent claude-code|codex|gemini|opencode|acp] [--model <model>]
|
|
340
359
|
navarch-runtime doctor [--config-dir <path>]
|
|
341
360
|
navarch-runtime code-graph --query <symbol-or-file> [--mode search|callers|callees|impact] [--commit HEAD] [--depth 1..5] [--repo <path>]
|
|
342
361
|
|
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
|
@@ -38,6 +38,21 @@ const COMPLETION_REMEDIATION_RETRIES = 2;
|
|
|
38
38
|
*/
|
|
39
39
|
const DELIVERY_CORRECTION_RETRIES = 1;
|
|
40
40
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
41
|
+
function resolveSessionExecution(config, claimed) {
|
|
42
|
+
const runtime = claimed.runtime ?? config.agentType;
|
|
43
|
+
const execution = claimed.context_bundle.execution ?? {
|
|
44
|
+
profile: claimed.task.execution_profile ?? "standard",
|
|
45
|
+
model: runtime === "codex" ? "gpt-6-astra"
|
|
46
|
+
: runtime === "gemini" ? "auto"
|
|
47
|
+
: runtime === "opencode" || runtime === "acp" ? "default"
|
|
48
|
+
: "claude-opus-5",
|
|
49
|
+
reasoning_effort: "medium",
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
...execution,
|
|
53
|
+
...(runtime === config.agentType && config.model ? { model: config.model } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
41
56
|
/**
|
|
42
57
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
43
58
|
* 1. write the prompt file
|
|
@@ -70,19 +85,9 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
70
85
|
// explicit pre-start signal. Otherwise it waits for expiry and bypasses
|
|
71
86
|
// the repeat/backoff policy entirely.
|
|
72
87
|
const { api, config } = deps;
|
|
73
|
-
const { lease_id: leaseId, task
|
|
88
|
+
const { lease_id: leaseId, task } = claimed;
|
|
74
89
|
const runtime = claimed.runtime ?? config.agentType;
|
|
75
|
-
const execution =
|
|
76
|
-
profile: task.execution_profile ?? "standard",
|
|
77
|
-
model: runtime === "codex"
|
|
78
|
-
? "gpt-6-astra"
|
|
79
|
-
: runtime === "gemini"
|
|
80
|
-
? "auto"
|
|
81
|
-
: runtime === "opencode" || runtime === "acp"
|
|
82
|
-
? "default"
|
|
83
|
-
: "claude-opus-5",
|
|
84
|
-
reasoning_effort: "medium",
|
|
85
|
-
};
|
|
90
|
+
const execution = resolveSessionExecution(config, claimed);
|
|
86
91
|
const crashDetail = describeCompletionError(err).slice(0, 1800);
|
|
87
92
|
const failureSummary = `Session crashed: ${crashDetail}`;
|
|
88
93
|
log.error(`session ${leaseId} threw before adapter start: ${crashDetail}`);
|
|
@@ -110,17 +115,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
110
115
|
const { api, config } = deps;
|
|
111
116
|
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
112
117
|
const runtime = claimed.runtime ?? config.agentType;
|
|
113
|
-
const execution =
|
|
114
|
-
profile: task.execution_profile ?? "standard",
|
|
115
|
-
model: runtime === "codex"
|
|
116
|
-
? "gpt-6-astra"
|
|
117
|
-
: runtime === "gemini"
|
|
118
|
-
? "auto"
|
|
119
|
-
: runtime === "opencode" || runtime === "acp"
|
|
120
|
-
? "default"
|
|
121
|
-
: "claude-opus-5",
|
|
122
|
-
reasoning_effort: "medium",
|
|
123
|
-
};
|
|
118
|
+
const execution = resolveSessionExecution(config, claimed);
|
|
124
119
|
// Isolation posture reported on every completion so `sessions` records what
|
|
125
120
|
// a run actually executed under (#660). Derived from config alone, so the
|
|
126
121
|
// pre-start failure paths below can stamp it too. Host mode records
|
|
@@ -216,6 +211,14 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
216
211
|
let activeAbortController = null;
|
|
217
212
|
let leaseLost = false;
|
|
218
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
|
+
};
|
|
219
222
|
let sessionClosing = false;
|
|
220
223
|
let retainWorktree = false;
|
|
221
224
|
let heartbeatInFlight = null;
|
|
@@ -397,6 +400,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
397
400
|
activeAbortController?.abort();
|
|
398
401
|
});
|
|
399
402
|
const heartbeatTimer = setInterval(() => void pollLease(), config.leaseHeartbeatIntervalMs);
|
|
403
|
+
deps.shutdownSignal?.addEventListener("abort", abortForRuntimeShutdown);
|
|
404
|
+
if (deps.shutdownSignal?.aborted)
|
|
405
|
+
abortForRuntimeShutdown();
|
|
400
406
|
lifecycle.enteredMainSessionTry = true;
|
|
401
407
|
try {
|
|
402
408
|
await gitWorktree.prepare();
|
|
@@ -432,7 +438,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
432
438
|
// notification and start the first turn with the corrected prompt.
|
|
433
439
|
pendingGuidance = [];
|
|
434
440
|
activeAbortController = new AbortController();
|
|
435
|
-
if (leaseLost)
|
|
441
|
+
if (leaseLost || deps.shutdownSignal?.aborted)
|
|
436
442
|
activeAbortController.abort();
|
|
437
443
|
const runPrompt = nextPrompt ??
|
|
438
444
|
(deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
@@ -464,7 +470,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
464
470
|
cwd: gitWorktree.worktreePath,
|
|
465
471
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
466
472
|
signal: activeAbortController.signal,
|
|
467
|
-
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000);
|
|
473
|
+
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
468
474
|
activeAbortController = null;
|
|
469
475
|
if (turnResult.timedOut || leaseLost)
|
|
470
476
|
await checkpointForRecovery(turnResult);
|
|
@@ -716,6 +722,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
716
722
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
717
723
|
}
|
|
718
724
|
finally {
|
|
725
|
+
deps.shutdownSignal?.removeEventListener("abort", abortForRuntimeShutdown);
|
|
719
726
|
sessionClosing = true;
|
|
720
727
|
clearInterval(heartbeatTimer);
|
|
721
728
|
heartbeatGuard.stop();
|
package/package.json
CHANGED