@sagentlab/navarch-runtime 0.1.5 → 0.1.7

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.
@@ -20,6 +20,7 @@ class ClaimLoop {
20
20
  timer = null;
21
21
  stopped = false;
22
22
  claimInFlight = false;
23
+ quiescenceWaiters = new Set();
23
24
  constructor(api, config, capacity, runSession) {
24
25
  this.api = api;
25
26
  this.config = config;
@@ -29,6 +30,7 @@ class ClaimLoop {
29
30
  start() {
30
31
  if (this.timer)
31
32
  return;
33
+ this.stopped = false;
32
34
  this.timer = setInterval(() => void this.tick(), this.config.pollIntervalMs);
33
35
  }
34
36
  stop() {
@@ -36,6 +38,18 @@ class ClaimLoop {
36
38
  if (this.timer)
37
39
  clearInterval(this.timer);
38
40
  this.timer = null;
41
+ this.resolveQuiescenceWaiters();
42
+ }
43
+ /**
44
+ * Stops new polls and waits for a claim request already on the wire to
45
+ * settle. If that request returns a lease, the session is still run; callers
46
+ * must then wait for CapacityTracker.waitForIdle() before restarting.
47
+ */
48
+ async drain() {
49
+ this.stop();
50
+ if (!this.claimInFlight)
51
+ return;
52
+ await new Promise((resolve) => this.quiescenceWaiters.add(resolve));
39
53
  }
40
54
  async tick() {
41
55
  if (this.stopped || this.claimInFlight || !this.capacity.hasCapacity())
@@ -75,7 +89,15 @@ class ClaimLoop {
75
89
  }
76
90
  finally {
77
91
  this.claimInFlight = false;
92
+ this.resolveQuiescenceWaiters();
78
93
  }
79
94
  }
95
+ resolveQuiescenceWaiters() {
96
+ if (this.claimInFlight)
97
+ return;
98
+ for (const resolve of this.quiescenceWaiters)
99
+ resolve();
100
+ this.quiescenceWaiters.clear();
101
+ }
80
102
  }
81
103
  exports.ClaimLoop = ClaimLoop;
package/dist/cli.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.main = main;
4
4
  const config_cjs_1 = require("./config.cjs");
5
+ const node_crypto_1 = require("node:crypto");
5
6
  const machine_store_cjs_1 = require("./machine-store.cjs");
6
7
  const api_cjs_1 = require("./api.cjs");
7
8
  const capacity_cjs_1 = require("./capacity.cjs");
@@ -10,6 +11,8 @@ const claim_loop_cjs_1 = require("./claim-loop.cjs");
10
11
  const session_cjs_1 = require("./session.cjs");
11
12
  const sandbox_cjs_1 = require("./sandbox.cjs");
12
13
  const logger_cjs_1 = require("./logger.cjs");
14
+ const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
15
+ const supervisor_cjs_1 = require("./supervisor.cjs");
13
16
  const log = (0, logger_cjs_1.createLogger)("cli");
14
17
  const PACKAGE_NAME = "@sagentlab/navarch-runtime";
15
18
  /**
@@ -99,7 +102,7 @@ async function registerCommand(flags) {
99
102
  console.log("Machine registered.");
100
103
  console.log(` machine_id: ${result.machine_id}`);
101
104
  console.log(` token: ${result.token}`);
102
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
105
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
103
106
  }
104
107
  /**
105
108
  * `navarch-runtime connect` — "Connect an agent to a project"
@@ -147,7 +150,7 @@ async function connectCommand(flags) {
147
150
  console.log("Machine connected.");
148
151
  console.log(` machine_id: ${result.machine_id}`);
149
152
  console.log(` token: ${result.token}`);
150
- console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("start")}\` to begin serving tasks.`);
153
+ console.log(`\nStored in ${config.configDir}/machine.json (mode 0600). Run \`${invocation("supervise")}\` to begin serving tasks.`);
151
154
  }
152
155
  async function startCommand(flags) {
153
156
  const baseConfig = (0, config_cjs_1.loadRuntimeConfig)();
@@ -160,20 +163,52 @@ async function startCommand(flags) {
160
163
  const config = { ...baseConfig, agentType };
161
164
  const api = new api_cjs_1.NavarchApiClient({ baseUrl: identity.api_base, token: identity.token });
162
165
  const capacity = new capacity_cjs_1.CapacityTracker(config.maxSessions);
163
- const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity);
164
166
  const claimLoop = new claim_loop_cjs_1.ClaimLoop(api, config, capacity, (claimed, sessionId) => (0, session_cjs_1.runSession)({ api, config }, claimed, sessionId));
167
+ const bootId = (0, node_crypto_1.randomUUID)();
168
+ const updateCoordinatorRef = {};
169
+ let readySent = false;
170
+ const heartbeat = new heartbeat_loop_cjs_1.MachineHeartbeatLoop(api, identity.machine_id, config, capacity, bootId, (result) => updateCoordinatorRef.current?.consider(result.update), () => {
171
+ if (!readySent && process.send) {
172
+ readySent = true;
173
+ process.send({ type: "navarch-ready", boot_id: bootId });
174
+ }
175
+ });
176
+ updateCoordinatorRef.current = new update_coordinator_cjs_1.RuntimeUpdateCoordinator({
177
+ config,
178
+ claimLoop,
179
+ capacity,
180
+ heartbeat,
181
+ });
165
182
  heartbeat.start();
166
183
  claimLoop.start();
167
184
  log.info(`navarch-runtime started: machine=${identity.name} agent=${config.agentType} max_sessions=${config.maxSessions} api_base=${identity.api_base}`);
185
+ let shuttingDown = false;
168
186
  const shutdown = () => {
169
- log.info("shutting down...");
170
- heartbeat.stop();
171
- claimLoop.stop();
172
- process.exit(0);
187
+ if (shuttingDown) {
188
+ log.warn("second shutdown signal received; forcing exit with active work");
189
+ process.exit(1);
190
+ }
191
+ shuttingDown = true;
192
+ void (async () => {
193
+ log.info("shutting down: draining active sessions...");
194
+ heartbeat.setUpdateState("draining");
195
+ await claimLoop.drain();
196
+ await capacity.waitForIdle();
197
+ heartbeat.stop();
198
+ log.info("shutdown drain complete");
199
+ process.exit(0);
200
+ })();
173
201
  };
174
202
  process.on("SIGINT", shutdown);
175
203
  process.on("SIGTERM", shutdown);
176
204
  }
205
+ async function superviseCommand(flags) {
206
+ const config = (0, config_cjs_1.loadRuntimeConfig)();
207
+ const agentType = agentFromFlag(flags);
208
+ const workerArgs = agentType ? ["--agent", agentType] : [];
209
+ const exitCode = await (0, supervisor_cjs_1.superviseRuntime)(config.configDir, process.argv[1] ?? "", workerArgs);
210
+ process.exitCode = exitCode;
211
+ }
177
212
  async function doctorCommand(flags) {
178
213
  const config = (0, config_cjs_1.loadRuntimeConfig)();
179
214
  const dockerOk = await (0, sandbox_cjs_1.isDockerAvailable)();
@@ -206,6 +241,7 @@ Usage:
206
241
  navarch-runtime connect --token <enrollment-token> --name <machine-name> \\
207
242
  [--agent claude-code|codex] [--project <project-id>] [--capabilities a,b] [--max-sessions N] [--api-base url]
208
243
  navarch-runtime start [--agent claude-code|codex]
244
+ navarch-runtime supervise [--agent claude-code|codex]
209
245
  navarch-runtime doctor
210
246
 
211
247
  Configuration is via NAVARCH_* environment variables; see runtime/README.md.
@@ -224,6 +260,9 @@ async function main(argv = process.argv.slice(2)) {
224
260
  case "start":
225
261
  await startCommand(flags);
226
262
  break;
263
+ case "supervise":
264
+ await superviseCommand(flags);
265
+ break;
227
266
  case "doctor":
228
267
  await doctorCommand(flags);
229
268
  break;
package/dist/config.cjs CHANGED
@@ -60,5 +60,15 @@ function loadRuntimeConfig(env = process.env) {
60
60
  mcpConfigPath: env.NAVARCH_MCP_CONFIG_PATH ?? null,
61
61
  sandboxMode,
62
62
  dockerImage: env.NAVARCH_DOCKER_IMAGE ?? "node:20-slim",
63
+ // Multiple sessions share one machine; keeping each agent inside its own
64
+ // worktree is the safe default, so disabling is the explicit opt-out.
65
+ worktreeGuard: !["off", "false", "0"].includes(env.NAVARCH_WORKTREE_GUARD ?? ""),
66
+ guardExtraRoots: (env.NAVARCH_GUARD_EXTRA_ROOTS ?? "")
67
+ .split(node_path_1.default.delimiter)
68
+ .map((s) => s.trim())
69
+ .filter(Boolean),
70
+ updateChannel: env.NAVARCH_UPDATE_CHANNEL === "canary" ? "canary" : "stable",
71
+ autoUpdate: env.NAVARCH_SUPERVISED === "1" &&
72
+ !["off", "false", "0"].includes(env.NAVARCH_AUTO_UPDATE ?? ""),
63
73
  };
64
74
  }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GitWorktree = void 0;
7
+ exports.branchSlug = branchSlug;
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const node_fs_1 = require("node:fs");
9
10
  const sandbox_cjs_1 = require("./sandbox.cjs");
@@ -28,7 +29,7 @@ class GitWorktree {
28
29
  this.sessionRoot = node_path_1.default.join(options.workspaceRoot, "sessions", sessionKey);
29
30
  this.worktreePath = node_path_1.default.join(this.sessionRoot, "repo");
30
31
  this.repositoryPath = node_path_1.default.join(options.workspaceRoot, "repositories", `${projectKey}.git`);
31
- this.branch = `navarch/${safePathSegment(options.taskId).slice(0, 32)}-${sessionKey.slice(0, 12)}`;
32
+ this.branch = `navarch/${branchSlug(options)}-${sessionKey.toLowerCase().slice(0, 8)}`;
32
33
  this.runner = options.runner ?? sandbox_cjs_1.nodeCommandRunner;
33
34
  this.cloneUrl = options.cloneUrl;
34
35
  this.githubToken = options.githubToken;
@@ -130,6 +131,32 @@ async function pathExists(value) {
130
131
  return false;
131
132
  }
132
133
  }
134
+ /**
135
+ * Kebab-case slug for the session branch, derived from the task summary so
136
+ * branch names read like `navarch/fix-login-redirect-a1b2c3d4` instead of a
137
+ * UUID mash. Falls back to the task type, then a task-id prefix, when the
138
+ * summary yields nothing. Output contains only [a-z0-9-] with no leading or
139
+ * trailing dash, so it is always a valid git ref component (no `..`, no
140
+ * trailing `.lock`).
141
+ */
142
+ function branchSlug(options) {
143
+ for (const candidate of [options.taskSummary, options.taskType]) {
144
+ const slug = (candidate ?? "")
145
+ .toLowerCase()
146
+ .replace(/[^a-z0-9]+/g, "-")
147
+ .replace(/^-+/, "")
148
+ .slice(0, 40)
149
+ .replace(/-+$/, "");
150
+ if (slug)
151
+ return slug;
152
+ }
153
+ const idSlug = safePathSegment(options.taskId)
154
+ .toLowerCase()
155
+ .replace(/[^a-z0-9]+/g, "-")
156
+ .slice(0, 8)
157
+ .replace(/^-+|-+$/g, "");
158
+ return idSlug || "task";
159
+ }
133
160
  function safePathSegment(value) {
134
161
  const safe = value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
135
162
  if (!safe)
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MachineHeartbeatLoop = void 0;
4
+ const version_cjs_1 = require("./version.cjs");
4
5
  const logger_cjs_1 = require("./logger.cjs");
5
6
  const log = (0, logger_cjs_1.createLogger)("heartbeat");
6
7
  /**
@@ -14,12 +15,21 @@ class MachineHeartbeatLoop {
14
15
  machineId;
15
16
  config;
16
17
  capacity;
18
+ bootId;
19
+ onResult;
20
+ onHealthy;
17
21
  timer = null;
18
- constructor(api, machineId, config, capacity) {
22
+ updateState = "idle";
23
+ lastUpdateError;
24
+ draining = false;
25
+ constructor(api, machineId, config, capacity, bootId, onResult, onHealthy) {
19
26
  this.api = api;
20
27
  this.machineId = machineId;
21
28
  this.config = config;
22
29
  this.capacity = capacity;
30
+ this.bootId = bootId;
31
+ this.onResult = onResult;
32
+ this.onHealthy = onHealthy;
23
33
  }
24
34
  start() {
25
35
  if (this.timer)
@@ -32,12 +42,28 @@ class MachineHeartbeatLoop {
32
42
  clearInterval(this.timer);
33
43
  this.timer = null;
34
44
  }
45
+ setUpdateState(state, error) {
46
+ this.updateState = state;
47
+ this.lastUpdateError = error;
48
+ this.draining = state === "draining";
49
+ void this.tick();
50
+ }
35
51
  async tick() {
36
52
  try {
37
- await this.api.machineHeartbeat(this.machineId, {
38
- available_capacity: this.capacity.available(),
53
+ const result = await this.api.machineHeartbeat(this.machineId, {
54
+ available_capacity: this.draining ? 0 : this.capacity.available(),
39
55
  capabilities: this.config.capabilities,
56
+ runtime: {
57
+ version: version_cjs_1.RUNTIME_VERSION,
58
+ updater_protocol: version_cjs_1.UPDATER_PROTOCOL_VERSION,
59
+ channel: this.config.updateChannel,
60
+ state: this.updateState,
61
+ boot_id: this.bootId,
62
+ ...(this.lastUpdateError ? { last_update_error: this.lastUpdateError } : {}),
63
+ },
40
64
  });
65
+ this.onHealthy?.();
66
+ this.onResult?.(result);
41
67
  }
42
68
  catch (err) {
43
69
  log.warn(`machine heartbeat failed: ${String(err)}`);
package/dist/session.cjs CHANGED
@@ -16,6 +16,7 @@ const mcp_config_cjs_1 = require("./mcp-config.cjs");
16
16
  const logger_cjs_1 = require("./logger.cjs");
17
17
  const git_worktree_cjs_1 = require("./git-worktree.cjs");
18
18
  const github_pr_cjs_1 = require("./github-pr.cjs");
19
+ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
19
20
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
20
21
  const MCP_CONFIG_FILENAME = "mcp-config.json";
21
22
  const log = (0, logger_cjs_1.createLogger)("session");
@@ -42,7 +43,7 @@ async function runSession(deps, claimed, sessionId) {
42
43
  const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
43
44
  const execution = bundle.execution ?? {
44
45
  profile: task.execution_profile ?? "standard",
45
- model: config.agentType === "codex" ? "gpt-5.6" : "best",
46
+ model: config.agentType === "codex" ? "gpt-5.6-sol" : "best",
46
47
  reasoning_effort: "medium",
47
48
  };
48
49
  const executionReport = {
@@ -91,6 +92,8 @@ async function runSession(deps, claimed, sessionId) {
91
92
  workspaceRoot: config.workspaceRoot,
92
93
  projectId: task.project_id,
93
94
  taskId: task.id,
95
+ taskSummary: task.summary,
96
+ taskType: task.task_type,
94
97
  sessionId,
95
98
  cloneUrl,
96
99
  githubToken,
@@ -183,6 +186,39 @@ async function runSession(deps, claimed, sessionId) {
183
186
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
184
187
  mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
185
188
  }
189
+ // Worktree boundary guard (worktree-guard.cts): several sessions share this
190
+ // machine. Host-mode Claude gets a generated PreToolUse hook; host-mode
191
+ // Codex gets an OS-enforced native permission profile over the same roots.
192
+ // Docker mode already has a container boundary. NAVARCH_WORKTREE_GUARD=off
193
+ // opts out for either agent.
194
+ let claudeSettingsPath = null;
195
+ let codexGuardArgs;
196
+ if (config.sandboxMode === "host" && config.worktreeGuard) {
197
+ if (config.agentType === "claude-code") {
198
+ if (config.claudeExtraArgs.includes("--settings")) {
199
+ log.warn("NAVARCH_CLAUDE_EXTRA_ARGS supplies --settings; skipping the generated worktree-guard settings for this session.");
200
+ }
201
+ else {
202
+ const guard = await (0, worktree_guard_cjs_1.prepareWorktreeGuard)({
203
+ workDir,
204
+ worktreePath: gitWorktree.worktreePath,
205
+ repositoryPath: gitWorktree.repositoryPath,
206
+ workspaceRoot: config.workspaceRoot,
207
+ extraRoots: config.guardExtraRoots,
208
+ });
209
+ claudeSettingsPath = guard.settingsPath;
210
+ }
211
+ }
212
+ else {
213
+ codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
214
+ workDir,
215
+ worktreePath: gitWorktree.worktreePath,
216
+ repositoryPath: gitWorktree.repositoryPath,
217
+ workspaceRoot: config.workspaceRoot,
218
+ extraRoots: config.guardExtraRoots,
219
+ });
220
+ }
221
+ }
186
222
  try {
187
223
  await gitWorktree.prepare();
188
224
  if (sandbox) {
@@ -220,6 +256,8 @@ async function runSession(deps, claimed, sessionId) {
220
256
  reasoningEffort: execution.reasoning_effort,
221
257
  timeoutMs: config.sessionTimeoutMs,
222
258
  env: toEnvMap(secrets),
259
+ settingsPath: claudeSettingsPath,
260
+ codexGuardArgs,
223
261
  cwd: sandbox ? undefined : gitWorktree.worktreePath,
224
262
  dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
225
263
  signal: activeAbortController.signal,
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.superviseRuntime = superviseRuntime;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const logger_cjs_1 = require("./logger.cjs");
6
+ const update_installer_cjs_1 = require("./update-installer.cjs");
7
+ const log = (0, logger_cjs_1.createLogger)("supervisor");
8
+ const UPDATE_RESTART_EXIT_CODE = 75;
9
+ function runWorker(binPath, args, healthTimeoutMs, setCurrentChild, onHealthy) {
10
+ return new Promise((resolve) => {
11
+ const child = (0, node_child_process_1.spawn)(process.execPath, [binPath, "start", ...args], {
12
+ env: { ...process.env, NAVARCH_SUPERVISED: "1" },
13
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
14
+ });
15
+ setCurrentChild(child);
16
+ let healthy = false;
17
+ let healthSignalReceived = false;
18
+ let healthTimedOut = false;
19
+ let healthCommit = Promise.resolve();
20
+ let healthTimer = null;
21
+ let forceKillTimer = null;
22
+ let settled = false;
23
+ const finish = (code) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ if (healthTimer)
28
+ clearTimeout(healthTimer);
29
+ if (forceKillTimer)
30
+ clearTimeout(forceKillTimer);
31
+ setCurrentChild(null);
32
+ void healthCommit.finally(() => resolve({ code, healthy }));
33
+ };
34
+ if (healthTimeoutMs !== null) {
35
+ healthTimer = setTimeout(() => {
36
+ if (!healthy) {
37
+ healthTimedOut = true;
38
+ log.error("updated runtime missed its startup health deadline; rolling back");
39
+ child.kill("SIGTERM");
40
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 10_000);
41
+ }
42
+ }, healthTimeoutMs);
43
+ }
44
+ child.on("message", (message) => {
45
+ if (!healthSignalReceived &&
46
+ !healthTimedOut &&
47
+ typeof message === "object" &&
48
+ message !== null &&
49
+ message.type === "navarch-ready") {
50
+ healthSignalReceived = true;
51
+ healthCommit = onHealthy()
52
+ .then(() => {
53
+ healthy = true;
54
+ if (healthTimer)
55
+ clearTimeout(healthTimer);
56
+ if (forceKillTimer)
57
+ clearTimeout(forceKillTimer);
58
+ })
59
+ .catch((error) => {
60
+ log.error(`could not commit runtime activation: ${String(error)}`);
61
+ child.kill("SIGTERM");
62
+ });
63
+ }
64
+ });
65
+ child.once("error", (error) => {
66
+ log.error(`could not start runtime worker: ${String(error)}`);
67
+ finish(1);
68
+ });
69
+ child.once("exit", (code) => finish(code ?? 1));
70
+ });
71
+ }
72
+ /** Runs the worker and owns update activation, health checking, and rollback. */
73
+ async function superviseRuntime(configDir, initialBinPath, workerArgs = [], healthTimeoutMs = 120_000) {
74
+ let currentBin = initialBinPath;
75
+ let rollbackBin = null;
76
+ let candidatePending = null;
77
+ let awaitingCandidateHealth = false;
78
+ let currentChild = null;
79
+ let stopping = false;
80
+ const forwardSignal = (signal) => {
81
+ stopping = true;
82
+ currentChild?.kill(signal);
83
+ };
84
+ const onSigint = () => forwardSignal("SIGINT");
85
+ const onSigterm = () => forwardSignal("SIGTERM");
86
+ process.on("SIGINT", onSigint);
87
+ process.on("SIGTERM", onSigterm);
88
+ try {
89
+ const active = await (0, update_installer_cjs_1.readActiveRuntime)(configDir);
90
+ if (active) {
91
+ try {
92
+ const verified = await (0, update_installer_cjs_1.verifyManagedRuntime)(configDir, active);
93
+ currentBin = verified.bin_path;
94
+ log.info(`starting active managed runtime ${active.version}`);
95
+ }
96
+ catch (error) {
97
+ log.warn(`ignoring invalid active runtime pointer: ${String(error)}`);
98
+ }
99
+ }
100
+ while (!stopping) {
101
+ const candidateThisRun = awaitingCandidateHealth;
102
+ const result = await runWorker(currentBin, workerArgs, candidateThisRun ? healthTimeoutMs : null, (child) => {
103
+ currentChild = child;
104
+ }, async () => {
105
+ if (candidateThisRun) {
106
+ if (!candidatePending)
107
+ throw new Error("candidate has no pending release metadata");
108
+ await (0, update_installer_cjs_1.writeActiveRuntime)(configDir, candidatePending);
109
+ await (0, update_installer_cjs_1.clearPendingUpdate)(configDir);
110
+ awaitingCandidateHealth = false;
111
+ rollbackBin = null;
112
+ candidatePending = null;
113
+ log.info("updated runtime reported healthy; activation committed");
114
+ }
115
+ });
116
+ if (stopping)
117
+ return result.code;
118
+ if (candidateThisRun && !result.healthy) {
119
+ if (!rollbackBin)
120
+ return 1;
121
+ log.warn("updated runtime failed before becoming healthy; restoring previous version");
122
+ currentBin = rollbackBin;
123
+ rollbackBin = null;
124
+ awaitingCandidateHealth = false;
125
+ candidatePending = null;
126
+ await (0, update_installer_cjs_1.clearPendingUpdate)(configDir);
127
+ continue;
128
+ }
129
+ if (result.code !== UPDATE_RESTART_EXIT_CODE)
130
+ return result.code;
131
+ const pending = await (0, update_installer_cjs_1.readPendingUpdate)(configDir);
132
+ if (!pending) {
133
+ log.error("worker requested an update restart without a pending update");
134
+ return 1;
135
+ }
136
+ const verified = await (0, update_installer_cjs_1.verifyManagedRuntime)(configDir, pending);
137
+ rollbackBin = currentBin;
138
+ currentBin = verified.bin_path;
139
+ candidatePending = pending;
140
+ awaitingCandidateHealth = true;
141
+ log.info(`activating staged runtime ${pending.version}`);
142
+ }
143
+ return 0;
144
+ }
145
+ finally {
146
+ process.off("SIGINT", onSigint);
147
+ process.off("SIGTERM", onSigterm);
148
+ }
149
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeUpdateCoordinator = void 0;
4
+ const logger_cjs_1 = require("./logger.cjs");
5
+ const update_installer_cjs_1 = require("./update-installer.cjs");
6
+ const version_cjs_1 = require("./version.cjs");
7
+ const log = (0, logger_cjs_1.createLogger)("update");
8
+ class RuntimeUpdateCoordinator {
9
+ options;
10
+ targetInProgress = null;
11
+ warnedUnsupervised = false;
12
+ constructor(options) {
13
+ this.options = options;
14
+ }
15
+ consider(directive) {
16
+ if (!directive || directive.target_version === version_cjs_1.RUNTIME_VERSION)
17
+ return;
18
+ if (!this.options.config.autoUpdate) {
19
+ if (!this.warnedUnsupervised) {
20
+ log.warn(`runtime ${directive.target_version} is available; run under \`navarch-runtime supervise\` to activate updates automatically`);
21
+ this.warnedUnsupervised = true;
22
+ }
23
+ return;
24
+ }
25
+ if (this.targetInProgress)
26
+ return;
27
+ this.targetInProgress = directive.target_version;
28
+ void this.apply(directive);
29
+ }
30
+ async apply(directive) {
31
+ const { config, heartbeat, claimLoop, capacity } = this.options;
32
+ try {
33
+ heartbeat.setUpdateState("staging");
34
+ log.info(`staging runtime ${directive.target_version}`);
35
+ const staged = await (this.options.stage ?? update_installer_cjs_1.stageRuntimeUpdate)(config.configDir, directive);
36
+ heartbeat.setUpdateState("draining");
37
+ log.info("runtime update staged; draining active sessions before restart");
38
+ await claimLoop.drain();
39
+ await capacity.waitForIdle();
40
+ await (0, update_installer_cjs_1.writePendingUpdate)(config.configDir, staged, directive.rollout_id);
41
+ log.info(`runtime ${directive.target_version} ready; handing off to supervisor`);
42
+ (this.options.requestRestart ?? ((code) => process.exit(code)))(75);
43
+ }
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ log.error(`runtime update failed: ${message}`);
47
+ heartbeat.setUpdateState("failed", message.slice(0, 500));
48
+ claimLoop.start();
49
+ this.targetInProgress = null;
50
+ }
51
+ }
52
+ }
53
+ exports.RuntimeUpdateCoordinator = RuntimeUpdateCoordinator;