@camstack/agent 1.1.32 → 1.1.35

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/dist/cli.js CHANGED
@@ -6,6 +6,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
9
12
  var __copyProps = (to, from, except, desc) => {
10
13
  if (from && typeof from === "object" || typeof from === "function") {
11
14
  for (let key of __getOwnPropNames(from))
@@ -23,15 +26,974 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
26
  mod
24
27
  ));
25
28
 
29
+ // ../node-root/dist/index.js
30
+ var require_dist = __commonJS({
31
+ "../node-root/dist/index.js"(exports2, module2) {
32
+ "use strict";
33
+ var __create2 = Object.create;
34
+ var __defProp2 = Object.defineProperty;
35
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
36
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
37
+ var __getProtoOf2 = Object.getPrototypeOf;
38
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
39
+ var __export = (target, all) => {
40
+ for (var name in all)
41
+ __defProp2(target, name, { get: all[name], enumerable: true });
42
+ };
43
+ var __copyProps2 = (to, from, except, desc) => {
44
+ if (from && typeof from === "object" || typeof from === "function") {
45
+ for (let key of __getOwnPropNames2(from))
46
+ if (!__hasOwnProp2.call(to, key) && key !== except)
47
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
48
+ }
49
+ return to;
50
+ };
51
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(
52
+ // If the importer is in node compatibility mode or this is not an ESM
53
+ // file that has been converted to a CommonJS file using a Babel-
54
+ // compatible transform (i.e. "__esModule" has not been set), then set
55
+ // "default" to the CommonJS "module.exports" for node compatibility.
56
+ isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target,
57
+ mod
58
+ ));
59
+ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
60
+ var src_exports = {};
61
+ __export(src_exports, {
62
+ AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC2,
63
+ HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
64
+ HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
65
+ RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
66
+ RootUpdateService: () => RootUpdateService2,
67
+ SERVER_ROOT_DIRNAME: () => SERVER_ROOT_DIRNAME,
68
+ SERVER_ROOT_STATE_FILE: () => SERVER_ROOT_STATE_FILE,
69
+ clearRestartIntentMarker: () => clearRestartIntentMarker,
70
+ compareSemver: () => compareSemver,
71
+ detectWorkspaceRoot: () => detectWorkspaceRoot,
72
+ emptyServerRootState: () => emptyServerRootState,
73
+ isHostExternal: () => isHostExternal,
74
+ isServerRootState: () => isServerRootState,
75
+ minNodeMajorOf: () => minNodeMajorOf,
76
+ planBoot: () => planBoot,
77
+ readRestartIntentMarker: () => readRestartIntentMarker,
78
+ readServerRootState: () => readServerRootState,
79
+ registerActiveRootResolver: () => registerActiveRootResolver,
80
+ restartIntentMarkerPath: () => restartIntentMarkerPath,
81
+ rootEntryPath: () => rootEntryPath,
82
+ rootPackageDir: () => rootPackageDir,
83
+ runNodeStarter: () => runNodeStarter,
84
+ serverRootDir: () => serverRootDir2,
85
+ stateFilePath: () => stateFilePath,
86
+ validateVersionDir: () => validateVersionDir,
87
+ versionDir: () => versionDir,
88
+ versionsDir: () => versionsDir,
89
+ writeRestartIntentMarker: () => writeRestartIntentMarker2,
90
+ writeServerRootState: () => writeServerRootState
91
+ });
92
+ module2.exports = __toCommonJS(src_exports);
93
+ var HUB_ROOT_SPEC = {
94
+ packageName: "@camstack/server",
95
+ entryRelPath: ["dist", "launcher.js"]
96
+ };
97
+ var AGENT_ROOT_SPEC2 = {
98
+ packageName: "@camstack/agent",
99
+ entryRelPath: ["dist", "cli.js"]
100
+ };
101
+ var fs8 = __toESM2(require("fs"));
102
+ var path9 = __toESM2(require("path"));
103
+ var SERVER_ROOT_DIRNAME = "server-root";
104
+ var SERVER_ROOT_STATE_FILE = "state.json";
105
+ var RESTART_INTENT_FILE = ".restart-intent";
106
+ function emptyServerRootState() {
107
+ return {
108
+ schemaVersion: 1,
109
+ currentVersion: null,
110
+ previousVersion: null,
111
+ pendingBoot: null,
112
+ rolledBack: null
113
+ };
114
+ }
115
+ function serverRootDir2(dataDir) {
116
+ return path9.join(dataDir, SERVER_ROOT_DIRNAME);
117
+ }
118
+ function versionsDir(rootDir) {
119
+ return path9.join(rootDir, "versions");
120
+ }
121
+ function versionDir(rootDir, version) {
122
+ return path9.join(versionsDir(rootDir), version);
123
+ }
124
+ function rootPackageDir(versionDirPath, spec) {
125
+ return path9.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
126
+ }
127
+ function rootEntryPath(versionDirPath, spec) {
128
+ return path9.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
129
+ }
130
+ function stateFilePath(rootDir) {
131
+ return path9.join(rootDir, SERVER_ROOT_STATE_FILE);
132
+ }
133
+ function isNullableString(v) {
134
+ return v === null || typeof v === "string";
135
+ }
136
+ function isPendingBoot(v) {
137
+ if (typeof v !== "object" || v === null) return false;
138
+ const p = v;
139
+ return typeof p["version"] === "string" && isNullableString(p["fromVersion"]) && typeof p["requestedAtMs"] === "number" && typeof p["bootAttempts"] === "number";
140
+ }
141
+ function isRollbackInfo(v) {
142
+ if (typeof v !== "object" || v === null) return false;
143
+ const r = v;
144
+ return typeof r["fromVersion"] === "string" && isNullableString(r["toVersion"]) && typeof r["atMs"] === "number" && typeof r["reason"] === "string";
145
+ }
146
+ function isServerRootState(v) {
147
+ if (typeof v !== "object" || v === null) return false;
148
+ const s = v;
149
+ if (s["schemaVersion"] !== 1) return false;
150
+ if (!isNullableString(s["currentVersion"])) return false;
151
+ if (!isNullableString(s["previousVersion"])) return false;
152
+ if (s["pendingBoot"] !== null && !isPendingBoot(s["pendingBoot"])) return false;
153
+ if (s["rolledBack"] !== null && !isRollbackInfo(s["rolledBack"])) return false;
154
+ return true;
155
+ }
156
+ function readServerRootState(rootDir) {
157
+ try {
158
+ const raw = JSON.parse(fs8.readFileSync(stateFilePath(rootDir), "utf-8"));
159
+ return isServerRootState(raw) ? raw : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ function writeServerRootState(rootDir, state) {
165
+ fs8.mkdirSync(rootDir, { recursive: true });
166
+ const target = stateFilePath(rootDir);
167
+ const tmp = `${target}.tmp`;
168
+ fs8.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
169
+ fs8.renameSync(tmp, target);
170
+ }
171
+ function restartIntentMarkerPath(rootDir) {
172
+ return path9.join(rootDir, RESTART_INTENT_FILE);
173
+ }
174
+ function isRestartIntentMarker(v) {
175
+ if (typeof v !== "object" || v === null) return false;
176
+ const m = v;
177
+ return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
178
+ }
179
+ function writeRestartIntentMarker2(rootDir, marker) {
180
+ fs8.mkdirSync(rootDir, { recursive: true });
181
+ const target = restartIntentMarkerPath(rootDir);
182
+ const tmp = `${target}.tmp`;
183
+ fs8.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
184
+ fs8.renameSync(tmp, target);
185
+ }
186
+ function readRestartIntentMarker(rootDir) {
187
+ try {
188
+ const raw = JSON.parse(fs8.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
189
+ return isRestartIntentMarker(raw) ? raw : null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function clearRestartIntentMarker(rootDir) {
195
+ try {
196
+ fs8.rmSync(restartIntentMarkerPath(rootDir), { force: true });
197
+ } catch {
198
+ }
199
+ }
200
+ function minNodeMajorOf(enginesNode) {
201
+ const match = /(\d+)/.exec(enginesNode);
202
+ if (match === null) return null;
203
+ const major = Number.parseInt(match[1] ?? "", 10);
204
+ return Number.isNaN(major) ? null : major;
205
+ }
206
+ function validateVersionDir(rootDir, version, nodeMajor, spec) {
207
+ const vDir = versionDir(rootDir, version);
208
+ const entry = rootEntryPath(vDir, spec);
209
+ if (!fs8.existsSync(entry)) {
210
+ return `root entry missing: ${entry}`;
211
+ }
212
+ const pkgJsonPath = path9.join(rootPackageDir(vDir, spec), "package.json");
213
+ let pkg;
214
+ try {
215
+ const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
216
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
217
+ return `package.json malformed: ${pkgJsonPath}`;
218
+ }
219
+ pkg = raw;
220
+ } catch {
221
+ return `package.json unreadable: ${pkgJsonPath}`;
222
+ }
223
+ if (pkg["name"] !== spec.packageName) {
224
+ return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
225
+ }
226
+ if (pkg["version"] !== version) {
227
+ return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
228
+ }
229
+ const engines = pkg["engines"];
230
+ if (typeof engines === "object" && engines !== null) {
231
+ const enginesNode = engines["node"];
232
+ if (typeof enginesNode === "string") {
233
+ const minMajor = minNodeMajorOf(enginesNode);
234
+ if (minMajor !== null && nodeMajor < minMajor) {
235
+ return `engines.node "${enginesNode}" requires Node >= ${minMajor}, runtime is ${nodeMajor}`;
236
+ }
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ function planBoot(state, isValidVersion, now) {
242
+ if (state === null) {
243
+ return { kind: "baked", reason: "no server-root state", stateToWrite: null };
244
+ }
245
+ let next = state;
246
+ let changed = false;
247
+ if (next.pendingBoot !== null) {
248
+ const pending = next.pendingBoot;
249
+ if (pending.bootAttempts >= 1) {
250
+ next = {
251
+ ...next,
252
+ pendingBoot: null,
253
+ rolledBack: {
254
+ fromVersion: pending.version,
255
+ toVersion: next.currentVersion,
256
+ atMs: now(),
257
+ reason: "probation boot did not reach ready \u2014 rolled back"
258
+ }
259
+ };
260
+ changed = true;
261
+ } else if (!isValidVersion(pending.version)) {
262
+ next = {
263
+ ...next,
264
+ pendingBoot: null,
265
+ rolledBack: {
266
+ fromVersion: pending.version,
267
+ toVersion: next.currentVersion,
268
+ atMs: now(),
269
+ reason: "staged version failed validation"
270
+ }
271
+ };
272
+ changed = true;
273
+ } else {
274
+ return {
275
+ kind: "data-root",
276
+ version: pending.version,
277
+ probation: true,
278
+ stateToWrite: {
279
+ ...next,
280
+ pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 }
281
+ }
282
+ };
283
+ }
284
+ }
285
+ if (next.currentVersion !== null) {
286
+ if (isValidVersion(next.currentVersion)) {
287
+ return {
288
+ kind: "data-root",
289
+ version: next.currentVersion,
290
+ probation: false,
291
+ stateToWrite: changed ? next : null
292
+ };
293
+ }
294
+ const broken = next.currentVersion;
295
+ if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
296
+ const fallback = next.previousVersion;
297
+ return {
298
+ kind: "data-root",
299
+ version: fallback,
300
+ probation: false,
301
+ stateToWrite: {
302
+ ...next,
303
+ currentVersion: fallback,
304
+ previousVersion: null,
305
+ rolledBack: {
306
+ fromVersion: broken,
307
+ toVersion: fallback,
308
+ atMs: now(),
309
+ reason: "active version failed validation"
310
+ }
311
+ }
312
+ };
313
+ }
314
+ return {
315
+ kind: "baked",
316
+ reason: `active version ${broken} failed validation and no valid previous version exists`,
317
+ stateToWrite: {
318
+ ...next,
319
+ currentVersion: null,
320
+ rolledBack: {
321
+ fromVersion: broken,
322
+ toVersion: null,
323
+ atMs: now(),
324
+ reason: "active version failed validation"
325
+ }
326
+ }
327
+ };
328
+ }
329
+ return {
330
+ kind: "baked",
331
+ reason: "no active data-root version",
332
+ stateToWrite: changed ? next : null
333
+ };
334
+ }
335
+ function parse(version) {
336
+ const dashIdx = version.indexOf("-");
337
+ const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
338
+ const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
339
+ const nums = base.split(".").map((seg) => {
340
+ const n = Number.parseInt(seg, 10);
341
+ return Number.isNaN(n) ? 0 : n;
342
+ });
343
+ return { nums, prerelease };
344
+ }
345
+ function compareSemver(a, b) {
346
+ const pa = parse(a);
347
+ const pb = parse(b);
348
+ const len = Math.max(pa.nums.length, pb.nums.length);
349
+ for (let i = 0; i < len; i++) {
350
+ const na = pa.nums[i] ?? 0;
351
+ const nb = pb.nums[i] ?? 0;
352
+ if (na < nb) return -1;
353
+ if (na > nb) return 1;
354
+ }
355
+ if (pa.prerelease === null && pb.prerelease === null) return 0;
356
+ if (pa.prerelease === null) return 1;
357
+ if (pb.prerelease === null) return -1;
358
+ if (pa.prerelease < pb.prerelease) return -1;
359
+ if (pa.prerelease > pb.prerelease) return 1;
360
+ return 0;
361
+ }
362
+ var fs22 = __toESM2(require("fs"));
363
+ var path22 = __toESM2(require("path"));
364
+ function detectWorkspaceRoot(fromDir) {
365
+ let dir = path22.resolve(fromDir);
366
+ for (; ; ) {
367
+ const pkgPath = path22.join(dir, "package.json");
368
+ try {
369
+ const raw = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
370
+ if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
371
+ return dir;
372
+ }
373
+ } catch {
374
+ }
375
+ const parent = path22.dirname(dir);
376
+ if (parent === dir) return null;
377
+ dir = parent;
378
+ }
379
+ }
380
+ var path32 = __toESM2(require("path"));
381
+ var import_node_url = require("url");
382
+ var HOST_EXTERNAL_SPECIFIERS = [
383
+ "@camstack/system",
384
+ "@camstack/shm-ring",
385
+ "@camstack/types",
386
+ "@camstack/sdk",
387
+ "zod",
388
+ "@trpc/server",
389
+ "@trpc/client",
390
+ "sharp",
391
+ "node-av"
392
+ ];
393
+ function isHostExternal(specifier) {
394
+ return HOST_EXTERNAL_SPECIFIERS.some(
395
+ (name) => specifier === name || specifier.startsWith(`${name}/`)
396
+ );
397
+ }
398
+ function registerActiveRootResolver(activeRootDir) {
399
+ const moduleApi = require("module");
400
+ const registerHooks = moduleApi["registerHooks"];
401
+ if (typeof registerHooks !== "function") {
402
+ console.warn(
403
+ "[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped"
404
+ );
405
+ return;
406
+ }
407
+ const anchorURL = (0, import_node_url.pathToFileURL)(
408
+ path32.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
409
+ ).href;
410
+ const hooks = {
411
+ resolve: (specifier, context, nextResolve) => {
412
+ if (isHostExternal(specifier)) {
413
+ try {
414
+ return nextResolve(specifier, { ...context, parentURL: anchorURL });
415
+ } catch {
416
+ }
417
+ }
418
+ return nextResolve(specifier, context);
419
+ }
420
+ };
421
+ registerHooks(hooks);
422
+ }
423
+ function runNodeStarter(options) {
424
+ const env = options.env ?? process.env;
425
+ const now = options.now ?? Date.now;
426
+ const registerResolver = options.registerResolver ?? registerActiveRootResolver;
427
+ const disabled = env[options.envNames.killSwitch] === "off";
428
+ const workspaceRoot = detectWorkspaceRoot(options.starterDir);
429
+ if (workspaceRoot !== null) {
430
+ console.log(
431
+ `[starter] workspace checkout detected at ${workspaceRoot} \u2014 using plain resolution`
432
+ );
433
+ env[options.envNames.bootMode] = "workspace";
434
+ options.loadEntry(options.seedEntry);
435
+ return;
436
+ }
437
+ if (disabled) {
438
+ console.log(`[starter] ${options.envNames.killSwitch}=off \u2014 booting the baked seed closure`);
439
+ env[options.envNames.bootMode] = "baked";
440
+ options.loadEntry(options.seedEntry);
441
+ return;
442
+ }
443
+ const rootDir = serverRootDir2(options.dataDir);
444
+ const state = readServerRootState(rootDir);
445
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
446
+ const isValidVersion = (version) => {
447
+ const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
448
+ if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
449
+ return reason === null;
450
+ };
451
+ const plan = planBoot(state, isValidVersion, now);
452
+ if (plan.stateToWrite !== null) {
453
+ try {
454
+ writeServerRootState(rootDir, plan.stateToWrite);
455
+ } catch (err) {
456
+ console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
457
+ env[options.envNames.bootMode] = "baked";
458
+ options.loadEntry(options.seedEntry);
459
+ return;
460
+ }
461
+ if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
462
+ const rb = plan.stateToWrite.rolledBack;
463
+ console.warn(
464
+ `[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`
465
+ );
466
+ }
467
+ }
468
+ if (plan.kind === "data-root") {
469
+ const activeRootDir = versionDir(rootDir, plan.version);
470
+ const entry = rootEntryPath(activeRootDir, options.spec);
471
+ console.log(
472
+ `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
473
+ );
474
+ env[options.envNames.bootMode] = "data-root";
475
+ env[options.envNames.activeRoot] = activeRootDir;
476
+ env[options.envNames.activeVersion] = plan.version;
477
+ registerResolver(activeRootDir);
478
+ options.loadEntry(entry);
479
+ return;
480
+ }
481
+ console.log(`[starter] booting baked seed closure (${plan.reason})`);
482
+ env[options.envNames.bootMode] = "baked";
483
+ options.loadEntry(options.seedEntry);
484
+ }
485
+ var import_node_child_process = require("child_process");
486
+ var fs32 = __toESM2(require("fs"));
487
+ var path42 = __toESM2(require("path"));
488
+ var import_node_util = require("util");
489
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
490
+ function buildNpmRegistryArgs(registry) {
491
+ if (registry === void 0 || registry.length === 0) return [];
492
+ return ["--registry", registry, `--@camstack:registry=${registry}`];
493
+ }
494
+ var NPM_VIEW_TIMEOUT_MS = 2e4;
495
+ var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
496
+ var RESTART_REASON_PREFIX = "server-update";
497
+ function readPackageVersion(pkgJsonPath) {
498
+ try {
499
+ const raw = JSON.parse(fs32.readFileSync(pkgJsonPath, "utf-8"));
500
+ if (typeof raw === "object" && raw !== null) {
501
+ const version = raw["version"];
502
+ if (typeof version === "string") return version;
503
+ }
504
+ } catch {
505
+ }
506
+ return null;
507
+ }
508
+ var RootUpdateService2 = class _RootUpdateService {
509
+ spec;
510
+ envNames;
511
+ logger;
512
+ restartServerFn;
513
+ dataDir;
514
+ execNpm;
515
+ env;
516
+ now;
517
+ runningPackageJsonPath;
518
+ workspaceProbeDir;
519
+ checkCache = null;
520
+ inFlight = "idle";
521
+ constructor(options) {
522
+ this.spec = options.spec;
523
+ this.envNames = options.envNames;
524
+ this.logger = options.logger;
525
+ this.restartServerFn = options.restartServer;
526
+ this.dataDir = path42.resolve(options.dataDir);
527
+ this.execNpm = options.execNpm ?? (async (args2, opts) => {
528
+ const { stdout } = await execFileAsync("npm", [...args2], {
529
+ cwd: opts.cwd,
530
+ timeout: opts.timeoutMs
531
+ });
532
+ return { stdout };
533
+ });
534
+ this.env = options.env ?? process.env;
535
+ this.now = options.now ?? Date.now;
536
+ this.runningPackageJsonPath = options.runningPackageJsonPath;
537
+ this.workspaceProbeDir = options.workspaceProbeDir;
538
+ }
539
+ // ── Status ────────────────────────────────────────────────────────────
540
+ resolveBootMode() {
541
+ const raw = this.env[this.envNames.bootMode];
542
+ if (raw === "workspace" || raw === "baked" || raw === "data-root") return raw;
543
+ return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? "workspace" : "baked";
544
+ }
545
+ runningVersion() {
546
+ return readPackageVersion(this.runningPackageJsonPath);
547
+ }
548
+ /** The running root package's identity — feeds registerNode / topology rows. */
549
+ getRunningRootPackage() {
550
+ return { name: this.spec.packageName, version: this.runningVersion() };
551
+ }
552
+ seedVersion() {
553
+ const seedDir = this.env[this.envNames.seedDir];
554
+ if (seedDir === void 0 || seedDir.length === 0) return null;
555
+ return readPackageVersion(path42.join(seedDir, "package.json"));
556
+ }
557
+ rootDir() {
558
+ return serverRootDir2(this.dataDir);
559
+ }
560
+ readState() {
561
+ return readServerRootState(this.rootDir()) ?? emptyServerRootState();
562
+ }
563
+ /**
564
+ * Like `readState`, but distinguishes "state.json missing" (normal on a
565
+ * fresh node) from "state.json present but corrupt" — in the corrupt case
566
+ * the starter booted the baked seed while installed data-dir versions are
567
+ * silently ignored, which the status surface must make visible.
568
+ */
569
+ readStateInfo() {
570
+ const rootDir = this.rootDir();
571
+ const raw = readServerRootState(rootDir);
572
+ if (raw !== null) return { state: raw, corrupt: false };
573
+ return { state: emptyServerRootState(), corrupt: fs32.existsSync(stateFilePath(rootDir)) };
574
+ }
575
+ updateState(state) {
576
+ if (this.inFlight === "checking") return "checking";
577
+ if (this.inFlight === "staging") return "staging";
578
+ if (state.pendingBoot !== null) {
579
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
580
+ if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
581
+ return "awaiting-confirmation";
582
+ }
583
+ return "pending-restart";
584
+ }
585
+ return "idle";
586
+ }
587
+ async getServerPackageStatus() {
588
+ const { state, corrupt } = this.readStateInfo();
589
+ const runningVersion = this.runningVersion();
590
+ const latestVersion = this.checkCache?.latestVersion ?? null;
591
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
592
+ return {
593
+ packageName: this.spec.packageName,
594
+ runningVersion,
595
+ nodeRuntimeVersion: process.versions.node,
596
+ activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
597
+ previousVersion: state.previousVersion,
598
+ seedVersion: this.seedVersion(),
599
+ latestVersion,
600
+ updateAvailable,
601
+ bootMode: this.resolveBootMode(),
602
+ updateState: this.updateState(state),
603
+ pendingVersion: state.pendingBoot?.version ?? null,
604
+ rolledBack: state.rolledBack,
605
+ stateFileCorrupt: corrupt,
606
+ lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
607
+ };
608
+ }
609
+ // ── Check ─────────────────────────────────────────────────────────────
610
+ async checkServerUpdate() {
611
+ const runningVersion = this.runningVersion();
612
+ if (this.inFlight !== "idle") {
613
+ return {
614
+ packageName: this.spec.packageName,
615
+ runningVersion,
616
+ latestVersion: this.checkCache?.latestVersion ?? null,
617
+ updateAvailable: false,
618
+ checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),
619
+ error: `busy: ${this.inFlight}`
620
+ };
621
+ }
622
+ this.inFlight = "checking";
623
+ try {
624
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
625
+ const args2 = [
626
+ "view",
627
+ `${this.spec.packageName}@latest`,
628
+ "version",
629
+ ...buildNpmRegistryArgs(registry)
630
+ ];
631
+ const { stdout } = await this.execNpm(args2, { timeoutMs: NPM_VIEW_TIMEOUT_MS });
632
+ const latestVersion = stdout.trim().length > 0 ? stdout.trim() : null;
633
+ this.checkCache = { latestVersion, checkedAtMs: this.now(), error: null };
634
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
635
+ return {
636
+ packageName: this.spec.packageName,
637
+ runningVersion,
638
+ latestVersion,
639
+ updateAvailable,
640
+ checkedAtMs: this.checkCache.checkedAtMs,
641
+ error: null
642
+ };
643
+ } catch (err) {
644
+ const message = err instanceof Error ? err.message : String(err);
645
+ this.checkCache = {
646
+ latestVersion: this.checkCache?.latestVersion ?? null,
647
+ checkedAtMs: this.now(),
648
+ error: message
649
+ };
650
+ this.logger.warn("root package update check failed", { meta: { error: message } });
651
+ return {
652
+ packageName: this.spec.packageName,
653
+ runningVersion,
654
+ latestVersion: this.checkCache.latestVersion,
655
+ updateAvailable: false,
656
+ checkedAtMs: this.checkCache.checkedAtMs,
657
+ error: message
658
+ };
659
+ } finally {
660
+ this.inFlight = "idle";
661
+ }
662
+ }
663
+ // ── Apply ─────────────────────────────────────────────────────────────
664
+ async applyServerUpdate(input) {
665
+ const bootMode = this.resolveBootMode();
666
+ if (bootMode === "workspace") {
667
+ return {
668
+ accepted: false,
669
+ targetVersion: input.version ?? null,
670
+ restarting: false,
671
+ message: "Refused: running from a workspace checkout \u2014 the dev loop resolves code from the workspace, a data-root update would never be loaded."
672
+ };
673
+ }
674
+ if (this.inFlight !== "idle") {
675
+ return {
676
+ accepted: false,
677
+ targetVersion: input.version ?? null,
678
+ restarting: false,
679
+ message: `Refused: another operation is in flight (${this.inFlight}).`
680
+ };
681
+ }
682
+ const stateBefore = this.readState();
683
+ if (stateBefore.pendingBoot !== null) {
684
+ return {
685
+ accepted: false,
686
+ targetVersion: input.version ?? null,
687
+ restarting: false,
688
+ message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
689
+ };
690
+ }
691
+ let target = input.version ?? null;
692
+ if (target === null) {
693
+ const check = await this.checkServerUpdate();
694
+ if (check.error !== null || check.latestVersion === null) {
695
+ return {
696
+ accepted: false,
697
+ targetVersion: null,
698
+ restarting: false,
699
+ message: `Refused: could not resolve the latest version (${check.error ?? "no version returned"}).`
700
+ };
701
+ }
702
+ target = check.latestVersion;
703
+ }
704
+ const runningVersion = this.runningVersion();
705
+ if (target === runningVersion) {
706
+ return {
707
+ accepted: false,
708
+ targetVersion: target,
709
+ restarting: false,
710
+ message: `Refused: ${this.spec.packageName}@${target} is already running.`
711
+ };
712
+ }
713
+ this.inFlight = "staging";
714
+ try {
715
+ await this.stageAndActivate(target);
716
+ } catch (err) {
717
+ const message = err instanceof Error ? err.message : String(err);
718
+ this.logger.error("root package update staging failed", {
719
+ meta: { targetVersion: target, error: message }
720
+ });
721
+ return {
722
+ accepted: false,
723
+ targetVersion: target,
724
+ restarting: false,
725
+ message: `Staging failed: ${message}`
726
+ };
727
+ } finally {
728
+ this.inFlight = "idle";
729
+ }
730
+ const state = this.readState();
731
+ writeServerRootState(this.rootDir(), {
732
+ ...state,
733
+ pendingBoot: {
734
+ version: target,
735
+ fromVersion: state.currentVersion,
736
+ requestedAtMs: this.now(),
737
+ bootAttempts: 0
738
+ },
739
+ rolledBack: null
740
+ });
741
+ this.logger.info("root package update staged \u2014 restarting to apply", {
742
+ meta: { targetVersion: target, fromVersion: state.currentVersion }
743
+ });
744
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
745
+ return {
746
+ accepted: true,
747
+ targetVersion: target,
748
+ restarting: true,
749
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
750
+ };
751
+ }
752
+ /**
753
+ * npm-install the target closure into a SAME-FS staging dir inside
754
+ * `versions/`, validate it, then atomically rename it into place.
755
+ */
756
+ async stageAndActivate(target) {
757
+ const rootDir = this.rootDir();
758
+ const vDir = versionsDir(rootDir);
759
+ fs32.mkdirSync(vDir, { recursive: true });
760
+ const stagingDir = path42.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
761
+ fs32.mkdirSync(stagingDir, { recursive: true });
762
+ try {
763
+ fs32.writeFileSync(
764
+ path42.join(stagingDir, "package.json"),
765
+ JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
766
+ "utf-8"
767
+ );
768
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
769
+ await this.execNpm(
770
+ [
771
+ "install",
772
+ "--omit=dev",
773
+ "--no-audit",
774
+ "--no-fund",
775
+ "--loglevel=error",
776
+ `${this.spec.packageName}@${target}`,
777
+ ...buildNpmRegistryArgs(registry)
778
+ ],
779
+ { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
780
+ );
781
+ const entry = rootEntryPath(stagingDir, this.spec);
782
+ if (!fs32.existsSync(entry)) {
783
+ throw new Error(`staged closure is missing the root entry (${entry})`);
784
+ }
785
+ const stagedVersion = readPackageVersion(
786
+ path42.join(rootPackageDir(stagingDir, this.spec), "package.json")
787
+ );
788
+ if (stagedVersion !== target) {
789
+ throw new Error(
790
+ `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
791
+ );
792
+ }
793
+ const dest = versionDir(rootDir, target);
794
+ if (fs32.existsSync(dest)) {
795
+ const aside = `${dest}.evicted-${this.now()}`;
796
+ await fs32.promises.rename(dest, aside);
797
+ await fs32.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
798
+ }
799
+ await fs32.promises.rename(stagingDir, dest);
800
+ } catch (err) {
801
+ await fs32.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
802
+ throw err;
803
+ }
804
+ }
805
+ // ── Rollback ──────────────────────────────────────────────────────────
806
+ async rollbackServerUpdate() {
807
+ if (this.inFlight !== "idle") {
808
+ return {
809
+ accepted: false,
810
+ targetVersion: null,
811
+ restarting: false,
812
+ message: `Refused: another operation is in flight (${this.inFlight}).`
813
+ };
814
+ }
815
+ const state = this.readState();
816
+ if (state.pendingBoot !== null) {
817
+ return {
818
+ accepted: false,
819
+ targetVersion: null,
820
+ restarting: false,
821
+ message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
822
+ };
823
+ }
824
+ if (state.currentVersion === null || state.previousVersion === null) {
825
+ return {
826
+ accepted: false,
827
+ targetVersion: state.previousVersion,
828
+ restarting: false,
829
+ message: "Refused: no previous version available to roll back to."
830
+ };
831
+ }
832
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
833
+ const invalidReason = validateVersionDir(
834
+ this.rootDir(),
835
+ state.previousVersion,
836
+ nodeMajor,
837
+ this.spec
838
+ );
839
+ if (invalidReason !== null) {
840
+ return {
841
+ accepted: false,
842
+ targetVersion: state.previousVersion,
843
+ restarting: false,
844
+ message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
845
+ };
846
+ }
847
+ writeServerRootState(this.rootDir(), {
848
+ ...state,
849
+ pendingBoot: {
850
+ version: state.previousVersion,
851
+ fromVersion: state.currentVersion,
852
+ requestedAtMs: this.now(),
853
+ bootAttempts: 0
854
+ }
855
+ });
856
+ this.logger.info("root package rollback requested \u2014 restarting", {
857
+ meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion }
858
+ });
859
+ this.restartServerFn(
860
+ `${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`
861
+ );
862
+ return {
863
+ accepted: true,
864
+ targetVersion: state.previousVersion,
865
+ restarting: true,
866
+ message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
867
+ };
868
+ }
869
+ // ── Plain restart ─────────────────────────────────────────────────────
870
+ /**
871
+ * Plain process restart — no version change. Backs the `restartServer` cap
872
+ * method. Guards mirror apply/rollback: refuse while a stage is in flight
873
+ * (a concurrent npm install must not be interrupted) or while a version is
874
+ * already staged awaiting restart (a naive bounce would boot the OLD version
875
+ * and orphan the pending one — the operator should apply/rollback instead).
876
+ */
877
+ restartNode() {
878
+ const runningVersion = this.runningVersion();
879
+ if (this.inFlight !== "idle") {
880
+ return {
881
+ accepted: false,
882
+ targetVersion: runningVersion,
883
+ restarting: false,
884
+ message: `Refused: another operation is in flight (${this.inFlight}).`
885
+ };
886
+ }
887
+ const state = this.readState();
888
+ if (state.pendingBoot !== null) {
889
+ return {
890
+ accepted: false,
891
+ targetVersion: runningVersion,
892
+ restarting: false,
893
+ message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
894
+ };
895
+ }
896
+ this.logger.info("plain node restart requested", { meta: { runningVersion } });
897
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: manual restart`);
898
+ return {
899
+ accepted: true,
900
+ targetVersion: runningVersion,
901
+ restarting: true,
902
+ message: "Restarting the node \u2014 the supervisor relaunches it on the same version."
903
+ };
904
+ }
905
+ // ── Boot health confirmation ──────────────────────────────────────────
906
+ /**
907
+ * Called by the post-boot path once the node is READY (hub: post-boot
908
+ * pipeline; agent: first acked `$hub.registerNode`). When THIS process is
909
+ * the probation boot of a pending version, promote it: `currentVersion` ←
910
+ * pending, `previousVersion` ← the version it replaced (N-1 retained for
911
+ * rollback), clear `pendingBoot` + `rolledBack`, and prune every version
912
+ * dir beyond {current, previous}. No-op otherwise.
913
+ */
914
+ confirmBootHealthy() {
915
+ const state = readServerRootState(this.rootDir());
916
+ if (state === null || state.pendingBoot === null) return { promoted: null };
917
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
918
+ if (activeVersion !== state.pendingBoot.version) {
919
+ return { promoted: null };
920
+ }
921
+ const promoted = {
922
+ ...state,
923
+ currentVersion: state.pendingBoot.version,
924
+ previousVersion: state.pendingBoot.fromVersion,
925
+ pendingBoot: null,
926
+ rolledBack: null
927
+ };
928
+ writeServerRootState(this.rootDir(), promoted);
929
+ this.pruneVersions(
930
+ [promoted.currentVersion, promoted.previousVersion].filter(
931
+ (v) => typeof v === "string"
932
+ )
933
+ );
934
+ this.logger.info("root package update confirmed healthy", {
935
+ meta: {
936
+ version: promoted.currentVersion,
937
+ previousVersion: promoted.previousVersion
938
+ }
939
+ });
940
+ return { promoted: promoted.currentVersion };
941
+ }
942
+ /**
943
+ * Remove version dirs under `versions/` not in `keep`.
944
+ *
945
+ * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
946
+ * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
947
+ * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
948
+ * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
949
+ * when this prune fires — deleting it would corrupt that install. Fresh
950
+ * transient dirs are owned by their in-flight operation (which cleans up on
951
+ * failure); the staleness sweep only collects dirs orphaned by a crash.
952
+ */
953
+ pruneVersions(keep) {
954
+ const vDir = versionsDir(this.rootDir());
955
+ let entries;
956
+ try {
957
+ entries = fs32.readdirSync(vDir);
958
+ } catch {
959
+ return;
960
+ }
961
+ for (const entry of entries) {
962
+ if (keep.includes(entry)) continue;
963
+ const full = path42.join(vDir, entry);
964
+ if (entry.startsWith(".")) {
965
+ try {
966
+ const ageMs = this.now() - fs32.statSync(full).mtimeMs;
967
+ if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
968
+ } catch {
969
+ continue;
970
+ }
971
+ }
972
+ try {
973
+ fs32.rmSync(full, { recursive: true, force: true });
974
+ this.logger.debug("pruned server-root version dir", { meta: { entry } });
975
+ } catch (err) {
976
+ this.logger.warn("failed to prune server-root version dir", {
977
+ meta: { entry, error: err instanceof Error ? err.message : String(err) }
978
+ });
979
+ }
980
+ }
981
+ }
982
+ /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
983
+ static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
984
+ };
985
+ }
986
+ });
987
+
26
988
  // src/cli.ts
27
989
  var import_node_fs = require("fs");
28
990
  var import_node_module = require("module");
29
991
  var os3 = __toESM(require("os"));
30
- var path7 = __toESM(require("path"));
992
+ var path8 = __toESM(require("path"));
31
993
 
32
994
  // src/agent-bootstrap.ts
33
- var fs6 = __toESM(require("fs"));
34
- var path6 = __toESM(require("path"));
995
+ var fs7 = __toESM(require("fs"));
996
+ var path7 = __toESM(require("path"));
35
997
 
36
998
  // src/agent-http.ts
37
999
  var fs = __toESM(require("fs"));
@@ -411,27 +1373,110 @@ function loadAgentConfig(configPath, dataDirOverride) {
411
1373
  };
412
1374
  }
413
1375
 
1376
+ // src/agent-update-service.ts
1377
+ var fs3 = __toESM(require("fs"));
1378
+ var path3 = __toESM(require("path"));
1379
+ var import_node_root = __toESM(require_dist());
1380
+ var AGENT_RUNTIME_ADDON_ID = "agent-runtime";
1381
+ var AGENT_RESTART_GRACE_MS = 2e3;
1382
+ var AGENT_LOCAL_CONFIRM_GRACE_MS = 9e4;
1383
+ function armLocalConfirmFallback(onGrace, graceMs = AGENT_LOCAL_CONFIRM_GRACE_MS) {
1384
+ const timer = setTimeout(() => onGrace(), graceMs);
1385
+ const maybeUnref = timer.unref;
1386
+ if (typeof maybeUnref === "function") maybeUnref.call(timer);
1387
+ return () => clearTimeout(timer);
1388
+ }
1389
+ function resolveAgentPackageJsonPath(fromDir) {
1390
+ const candidates = [
1391
+ path3.resolve(fromDir, "..", "package.json"),
1392
+ path3.resolve(fromDir, "..", "..", "package.json")
1393
+ ];
1394
+ for (const candidate of candidates) {
1395
+ try {
1396
+ const raw = JSON.parse(fs3.readFileSync(candidate, "utf-8"));
1397
+ if (raw.name === import_node_root.AGENT_ROOT_SPEC.packageName) return candidate;
1398
+ } catch {
1399
+ }
1400
+ }
1401
+ return candidates[0] ?? path3.resolve(fromDir, "..", "package.json");
1402
+ }
1403
+ function scheduleAgentRestart(logger, requestedBy, dataDir) {
1404
+ if (dataDir !== void 0 && dataDir.length > 0) {
1405
+ try {
1406
+ (0, import_node_root.writeRestartIntentMarker)((0, import_node_root.serverRootDir)(dataDir), {
1407
+ requestedAtMs: Date.now(),
1408
+ reason: requestedBy
1409
+ });
1410
+ } catch (err) {
1411
+ logger.warn("failed to write restart-intent marker", {
1412
+ meta: { error: err instanceof Error ? err.message : String(err) }
1413
+ });
1414
+ }
1415
+ }
1416
+ logger.warn("agent restart requested \u2014 exiting for supervisor relaunch", {
1417
+ meta: { requestedBy, graceMs: AGENT_RESTART_GRACE_MS }
1418
+ });
1419
+ const grace = setTimeout(() => {
1420
+ process.kill(process.pid, "SIGTERM");
1421
+ const hard = setTimeout(() => process.exit(0), 15e3);
1422
+ hard.unref();
1423
+ }, AGENT_RESTART_GRACE_MS);
1424
+ grace.unref();
1425
+ }
1426
+ var AgentUpdateService = class extends import_node_root.RootUpdateService {
1427
+ constructor(options) {
1428
+ super({
1429
+ spec: import_node_root.AGENT_ROOT_SPEC,
1430
+ envNames: {
1431
+ bootMode: "CAMSTACK_AGENT_BOOT_MODE",
1432
+ activeVersion: "CAMSTACK_AGENT_ACTIVE_VERSION",
1433
+ seedDir: "CAMSTACK_SEED_AGENT_DIR"
1434
+ },
1435
+ logger: options.logger,
1436
+ restartServer: options.restartAgent ?? ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
1437
+ dataDir: options.dataDir,
1438
+ runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
1439
+ workspaceProbeDir: __dirname,
1440
+ execNpm: options.execNpm,
1441
+ env: options.env,
1442
+ now: options.now
1443
+ });
1444
+ }
1445
+ };
1446
+ function buildAgentServerManagementProvider(service) {
1447
+ return {
1448
+ getServerPackageStatus: () => service.getServerPackageStatus(),
1449
+ checkServerUpdate: () => service.checkServerUpdate(),
1450
+ applyServerUpdate: (input) => service.applyServerUpdate(input),
1451
+ rollbackServerUpdate: () => service.rollbackServerUpdate(),
1452
+ restartServer: () => Promise.resolve(service.restartNode())
1453
+ };
1454
+ }
1455
+ function agentRuntimeManifestEntry() {
1456
+ return { addonId: AGENT_RUNTIME_ADDON_ID, capabilities: ["server-management"] };
1457
+ }
1458
+
414
1459
  // src/agent-service.ts
415
- var fs4 = __toESM(require("fs"));
1460
+ var fs5 = __toESM(require("fs"));
416
1461
  var os2 = __toESM(require("os"));
417
- var path4 = __toESM(require("path"));
1462
+ var path5 = __toESM(require("path"));
418
1463
  var import_system = require("@camstack/system");
419
1464
  var import_moleculer = require("moleculer");
420
1465
 
421
1466
  // src/agent-deploy-swap.ts
422
- var fs3 = __toESM(require("fs"));
423
- var path3 = __toESM(require("path"));
1467
+ var fs4 = __toESM(require("fs"));
1468
+ var path4 = __toESM(require("path"));
424
1469
  var import_node_crypto = require("crypto");
425
1470
  function rmrf(target) {
426
- fs3.rmSync(target, { recursive: true, force: true });
1471
+ fs4.rmSync(target, { recursive: true, force: true });
427
1472
  }
428
1473
  function moveDir(from, to) {
429
1474
  try {
430
- fs3.renameSync(from, to);
1475
+ fs4.renameSync(from, to);
431
1476
  } catch (err) {
432
1477
  const code = err.code;
433
1478
  if (code === "EXDEV") {
434
- fs3.cpSync(from, to, { recursive: true });
1479
+ fs4.cpSync(from, to, { recursive: true });
435
1480
  rmrf(from);
436
1481
  } else {
437
1482
  throw err;
@@ -440,32 +1485,32 @@ function moveDir(from, to) {
440
1485
  }
441
1486
  async function applyDeployedBundle(input) {
442
1487
  const { addonsDir, addonId, bundle, extract, logger } = input;
443
- fs3.mkdirSync(addonsDir, { recursive: true });
444
- const liveDir = path3.join(addonsDir, addonId);
445
- const liveParent = path3.dirname(liveDir);
446
- const liveBase = path3.basename(liveDir);
447
- fs3.mkdirSync(liveParent, { recursive: true });
1488
+ fs4.mkdirSync(addonsDir, { recursive: true });
1489
+ const liveDir = path4.join(addonsDir, addonId);
1490
+ const liveParent = path4.dirname(liveDir);
1491
+ const liveBase = path4.basename(liveDir);
1492
+ fs4.mkdirSync(liveParent, { recursive: true });
448
1493
  const token = (0, import_node_crypto.randomBytes)(6).toString("hex");
449
- const nextDir = path3.join(liveParent, `.${liveBase}.next.${token}`);
450
- const backupDir = path3.join(liveParent, `.${liveBase}.backup.${token}`);
451
- fs3.mkdirSync(nextDir, { recursive: true });
1494
+ const nextDir = path4.join(liveParent, `.${liveBase}.next.${token}`);
1495
+ const backupDir = path4.join(liveParent, `.${liveBase}.backup.${token}`);
1496
+ fs4.mkdirSync(nextDir, { recursive: true });
452
1497
  try {
453
1498
  await extract(bundle, nextDir);
454
1499
  } catch (err) {
455
1500
  rmrf(nextDir);
456
1501
  throw err;
457
1502
  }
458
- const hadLive = fs3.existsSync(liveDir);
1503
+ const hadLive = fs4.existsSync(liveDir);
459
1504
  if (hadLive) {
460
- fs3.renameSync(liveDir, backupDir);
1505
+ fs4.renameSync(liveDir, backupDir);
461
1506
  }
462
1507
  try {
463
1508
  moveDir(nextDir, liveDir);
464
1509
  } catch (swapErr) {
465
1510
  if (hadLive) {
466
1511
  try {
467
- if (fs3.existsSync(liveDir)) rmrf(liveDir);
468
- fs3.renameSync(backupDir, liveDir);
1512
+ if (fs4.existsSync(liveDir)) rmrf(liveDir);
1513
+ fs4.renameSync(backupDir, liveDir);
469
1514
  } catch (restoreErr) {
470
1515
  logger.error(
471
1516
  `agent deploy swap: restore of "${addonId}" failed after a failed swap \u2014 manual recovery may be needed`,
@@ -540,8 +1585,8 @@ function getLocalIps() {
540
1585
  }
541
1586
  async function withTimeout(p, ms, fallback) {
542
1587
  let timer;
543
- const timeout = new Promise((resolve6) => {
544
- timer = setTimeout(() => resolve6(fallback), ms);
1588
+ const timeout = new Promise((resolve7) => {
1589
+ timer = setTimeout(() => resolve7(fallback), ms);
545
1590
  });
546
1591
  try {
547
1592
  return await Promise.race([p, timeout]);
@@ -575,8 +1620,8 @@ function resolveDeployAction(seam) {
575
1620
  function readHubAddressFromConfig(configPath) {
576
1621
  if (!configPath) return null;
577
1622
  try {
578
- if (!fs4.existsSync(configPath)) return null;
579
- const raw = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
1623
+ if (!fs5.existsSync(configPath)) return null;
1624
+ const raw = JSON.parse(fs5.readFileSync(configPath, "utf-8"));
580
1625
  return typeof raw.hubAddress === "string" && raw.hubAddress.length > 0 ? raw.hubAddress : null;
581
1626
  } catch {
582
1627
  return null;
@@ -584,9 +1629,9 @@ function readHubAddressFromConfig(configPath) {
584
1629
  }
585
1630
  function readDeployedAddonIds(addonDir) {
586
1631
  try {
587
- const manifestPath = path4.join(addonDir, "package.json");
588
- if (!fs4.existsSync(manifestPath)) return [];
589
- const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
1632
+ const manifestPath = path5.join(addonDir, "package.json");
1633
+ if (!fs5.existsSync(manifestPath)) return [];
1634
+ const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
590
1635
  const entries = raw.camstack?.addons ?? [];
591
1636
  const ids = [];
592
1637
  for (const entry of entries) {
@@ -726,17 +1771,17 @@ function createAgentService(deps) {
726
1771
  deps.agentName = newName.trim();
727
1772
  broker.logger.info(`Agent renamed: "${oldName}" \u2192 "${deps.agentName}"`);
728
1773
  try {
729
- const configFile = path4.resolve(deps.configPath);
1774
+ const configFile = path5.resolve(deps.configPath);
730
1775
  let raw = {};
731
- if (fs4.existsSync(configFile)) {
1776
+ if (fs5.existsSync(configFile)) {
732
1777
  try {
733
- raw = JSON.parse(fs4.readFileSync(configFile, "utf-8"));
1778
+ raw = JSON.parse(fs5.readFileSync(configFile, "utf-8"));
734
1779
  } catch {
735
1780
  }
736
1781
  }
737
1782
  raw.name = deps.agentName;
738
- fs4.mkdirSync(path4.dirname(configFile), { recursive: true });
739
- fs4.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
1783
+ fs5.mkdirSync(path5.dirname(configFile), { recursive: true });
1784
+ fs5.writeFileSync(configFile, JSON.stringify(raw, null, 2), "utf-8");
740
1785
  broker.logger.info(`Agent name persisted to ${configFile}`);
741
1786
  } catch (err) {
742
1787
  broker.logger.warn(
@@ -760,15 +1805,15 @@ function createAgentService(deps) {
760
1805
  const { promisify } = await import("util");
761
1806
  const execFileAsync = promisify(execFile);
762
1807
  const extract = async (tgz, destDir) => {
763
- const tgzPath = path4.join(destDir, "..", `.${path4.basename(destDir)}.tgz`);
764
- fs4.writeFileSync(tgzPath, tgz);
1808
+ const tgzPath = path5.join(destDir, "..", `.${path5.basename(destDir)}.tgz`);
1809
+ fs5.writeFileSync(tgzPath, tgz);
765
1810
  try {
766
1811
  await execFileAsync("tar", ["-xzf", tgzPath, "-C", destDir, "--strip-components=1"], {
767
1812
  timeout: 6e4
768
1813
  });
769
1814
  } finally {
770
1815
  try {
771
- fs4.unlinkSync(tgzPath);
1816
+ fs5.unlinkSync(tgzPath);
772
1817
  } catch {
773
1818
  }
774
1819
  }
@@ -819,26 +1864,26 @@ function createAgentService(deps) {
819
1864
  const { execFile } = await import("child_process");
820
1865
  const { promisify } = await import("util");
821
1866
  const execFileAsync = promisify(execFile);
822
- const modelsDir = path4.join(deps.dataDir, "models");
1867
+ const modelsDir = path5.join(deps.dataDir, "models");
823
1868
  const seam = {
824
1869
  modelsDir,
825
1870
  fetchBundle: (s) => fetchBundleFromHub(s),
826
1871
  extract: async (tgz, destDir) => {
827
- const tmp = path4.join(
1872
+ const tmp = path5.join(
828
1873
  destDir,
829
1874
  `.dist-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`
830
1875
  );
831
- fs4.writeFileSync(tmp, tgz);
1876
+ fs5.writeFileSync(tmp, tgz);
832
1877
  try {
833
1878
  await execFileAsync("tar", ["-xzf", tmp, "-C", destDir], { timeout: 6e4 });
834
1879
  } finally {
835
1880
  try {
836
- fs4.unlinkSync(tmp);
1881
+ fs5.unlinkSync(tmp);
837
1882
  } catch {
838
1883
  }
839
1884
  }
840
1885
  },
841
- mkdirp: (dir) => fs4.mkdirSync(dir, { recursive: true })
1886
+ mkdirp: (dir) => fs5.mkdirSync(dir, { recursive: true })
842
1887
  };
843
1888
  return applyModelDistribution(seam, params);
844
1889
  }
@@ -883,17 +1928,17 @@ function createAgentService(deps) {
883
1928
  } catch {
884
1929
  }
885
1930
  deps.loadedAddons.delete(addonId);
886
- const addonDir = path4.join(deps.addonsDir, addonId);
887
- if (fs4.existsSync(addonDir)) {
888
- fs4.rmSync(addonDir, { recursive: true, force: true });
1931
+ const addonDir = path5.join(deps.addonsDir, addonId);
1932
+ if (fs5.existsSync(addonDir)) {
1933
+ fs5.rmSync(addonDir, { recursive: true, force: true });
889
1934
  }
890
1935
  const pkgName = entry?.packageName;
891
1936
  if (pkgName && pkgName !== addonId) {
892
1937
  const pkgShared = [...deps.loadedAddons.values()].some((e) => e.packageName === pkgName);
893
1938
  if (!pkgShared) {
894
- const pkgDir = path4.join(deps.addonsDir, pkgName);
895
- if (fs4.existsSync(pkgDir)) {
896
- fs4.rmSync(pkgDir, { recursive: true, force: true });
1939
+ const pkgDir = path5.join(deps.addonsDir, pkgName);
1940
+ if (fs5.existsSync(pkgDir)) {
1941
+ fs5.rmSync(pkgDir, { recursive: true, force: true });
897
1942
  }
898
1943
  }
899
1944
  }
@@ -961,14 +2006,14 @@ function createAgentService(deps) {
961
2006
  }
962
2007
 
963
2008
  // src/agent-group-runner.ts
964
- var fs5 = __toESM(require("fs"));
965
- var path5 = __toESM(require("path"));
2009
+ var fs6 = __toESM(require("fs"));
2010
+ var path6 = __toESM(require("path"));
966
2011
  var import_types = require("@camstack/types");
967
2012
  function readPackageManifestAddons(dir) {
968
2013
  try {
969
- const pkgPath = path5.join(dir, "package.json");
970
- if (!fs5.existsSync(pkgPath)) return null;
971
- const raw = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2014
+ const pkgPath = path6.join(dir, "package.json");
2015
+ if (!fs6.existsSync(pkgPath)) return null;
2016
+ const raw = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
972
2017
  const packageName = typeof raw.name === "string" ? raw.name : "";
973
2018
  const packageVersion = typeof raw.version === "string" ? raw.version : "0.0.0";
974
2019
  const entries = Array.isArray(raw.camstack?.addons) ? raw.camstack.addons : [];
@@ -1139,7 +2184,7 @@ async function startAgent(configPath) {
1139
2184
  const bundledDir = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
1140
2185
  let workspaceDir = null;
1141
2186
  let resolvedSource = explicitSource;
1142
- if (bundledDir && fs6.existsSync(bundledDir)) {
2187
+ if (bundledDir && fs7.existsSync(bundledDir)) {
1143
2188
  workspaceDir = bundledDir;
1144
2189
  resolvedSource = "local";
1145
2190
  console.log(`[Agent] Using bundled addons from ${bundledDir}`);
@@ -1203,9 +2248,9 @@ async function startAgent(configPath) {
1203
2248
  return discoveryMode ? "searching" : "disconnected";
1204
2249
  }
1205
2250
  function readConfigFile2(p) {
1206
- if (!fs6.existsSync(p)) return {};
2251
+ if (!fs7.existsSync(p)) return {};
1207
2252
  try {
1208
- return JSON.parse(fs6.readFileSync(p, "utf-8"));
2253
+ return JSON.parse(fs7.readFileSync(p, "utf-8"));
1209
2254
  } catch {
1210
2255
  return {};
1211
2256
  }
@@ -1235,6 +2280,7 @@ async function startAgent(configPath) {
1235
2280
  const caps = addonCapMap.get(addonId) ?? [];
1236
2281
  result.push({ addonId, capabilities: caps });
1237
2282
  }
2283
+ result.push(agentRuntimeManifestEntry());
1238
2284
  return result;
1239
2285
  }
1240
2286
  function aggregateManifest() {
@@ -1251,11 +2297,13 @@ async function startAgent(configPath) {
1251
2297
  allNativeCaps.push(...childNativeCaps);
1252
2298
  }
1253
2299
  }
2300
+ const rootPackage = agentUpdateService.getRunningRootPackage();
1254
2301
  return (0, import_system3.buildNodeManifest)(
1255
2302
  config.nodeId,
1256
2303
  allAddons,
1257
2304
  allNativeCaps.length > 0 ? allNativeCaps : void 0,
1258
- config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
2305
+ config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
2306
+ rootPackage.version === null ? void 0 : { name: rootPackage.name, version: rootPackage.version }
1259
2307
  );
1260
2308
  }
1261
2309
  function triggerUpwardRegistration() {
@@ -1267,7 +2315,9 @@ async function startAgent(configPath) {
1267
2315
  signal: registerAbortController.signal,
1268
2316
  log: (msg) => console.log(`[Agent] ${msg}`)
1269
2317
  }
1270
- ).catch((err) => {
2318
+ ).then(() => {
2319
+ confirmAgentBootHealthy("hub-ack");
2320
+ }).catch((err) => {
1271
2321
  if ((0, import_system3.isClusterSecretMismatchError)(err)) {
1272
2322
  consoleLogger.error(
1273
2323
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
@@ -1297,11 +2347,41 @@ async function startAgent(configPath) {
1297
2347
  import_types3.pipelineExecutorCapability,
1298
2348
  import_types3.pipelineRunnerCapability,
1299
2349
  import_types3.audioAnalyzerCapability,
1300
- import_types3.platformProbeCapability
2350
+ import_types3.platformProbeCapability,
2351
+ import_types3.serverManagementCapability
1301
2352
  ];
1302
2353
  for (const cap of agentCapabilities) {
1303
2354
  capabilityRegistry.declareCapability(cap);
1304
2355
  }
2356
+ const agentUpdateService = new AgentUpdateService({
2357
+ logger: consoleLogger,
2358
+ dataDir: config.dataDir
2359
+ });
2360
+ capabilityRegistry.registerProvider(
2361
+ "server-management",
2362
+ AGENT_RUNTIME_ADDON_ID,
2363
+ buildAgentServerManagementProvider(agentUpdateService)
2364
+ );
2365
+ let agentBootConfirmed = false;
2366
+ let cancelLocalConfirmFallback = null;
2367
+ const confirmAgentBootHealthy = (source) => {
2368
+ if (agentBootConfirmed) return;
2369
+ agentBootConfirmed = true;
2370
+ cancelLocalConfirmFallback?.();
2371
+ cancelLocalConfirmFallback = null;
2372
+ try {
2373
+ const confirm = agentUpdateService.confirmBootHealthy();
2374
+ if (confirm.promoted !== null) {
2375
+ consoleLogger.info(
2376
+ `agent root package update confirmed healthy (${source}): @camstack/agent@${confirm.promoted}`
2377
+ );
2378
+ }
2379
+ } catch (err) {
2380
+ consoleLogger.warn(
2381
+ `agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`
2382
+ );
2383
+ }
2384
+ };
1305
2385
  const loggerFactory = (addonId) => agentLogManager.createLogger().withTags({ addonId });
1306
2386
  const agentServiceSchema = createAgentService({
1307
2387
  addonsDir: config.addonsDir,
@@ -1515,6 +2595,7 @@ async function startAgent(configPath) {
1515
2595
  capabilityRegistry
1516
2596
  );
1517
2597
  triggerUpwardRegistration();
2598
+ cancelLocalConfirmFallback = armLocalConfirmFallback(() => confirmAgentBootHealthy("local-grace"));
1518
2599
  let hubReachableFired = false;
1519
2600
  broker.localBus.on("$node.connected", (data) => {
1520
2601
  const node = data.node;
@@ -1538,6 +2619,8 @@ async function startAgent(configPath) {
1538
2619
  const shutdown = async () => {
1539
2620
  console.log("[Agent] Shutting down...");
1540
2621
  registerAbortController.abort();
2622
+ cancelLocalConfirmFallback?.();
2623
+ cancelLocalConfirmFallback = null;
1541
2624
  udsEventBridgeDispose?.();
1542
2625
  udsEventBridgeDispose = null;
1543
2626
  for (const [, entry] of loadedAddons) {
@@ -1698,7 +2781,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1698
2781
  }
1699
2782
  }
1700
2783
  async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1701
- if (!fs6.existsSync(addonsDir)) return;
2784
+ if (!fs7.existsSync(addonsDir)) return;
1702
2785
  const packageDirs = resolveAddonPackageDirs(addonsDir);
1703
2786
  const groupCandidates = [];
1704
2787
  const inProcessDirs = /* @__PURE__ */ new Set();
@@ -1758,7 +2841,7 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
1758
2841
  }
1759
2842
  }
1760
2843
  async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1761
- const modelsDir = path6.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
2844
+ const modelsDir = path7.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
1762
2845
  const contextOptions = {
1763
2846
  storageProvider,
1764
2847
  addonConfig: { modelsDir },
@@ -1808,13 +2891,13 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
1808
2891
  }
1809
2892
  function readAgentVersion() {
1810
2893
  const candidates = [
1811
- path6.resolve(__dirname, "..", "package.json"),
1812
- path6.resolve(__dirname, "..", "..", "package.json")
2894
+ path7.resolve(__dirname, "..", "package.json"),
2895
+ path7.resolve(__dirname, "..", "..", "package.json")
1813
2896
  ];
1814
2897
  for (const candidate of candidates) {
1815
2898
  try {
1816
- if (!fs6.existsSync(candidate)) continue;
1817
- const raw = JSON.parse(fs6.readFileSync(candidate, "utf-8"));
2899
+ if (!fs7.existsSync(candidate)) continue;
2900
+ const raw = JSON.parse(fs7.readFileSync(candidate, "utf-8"));
1818
2901
  if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
1819
2902
  } catch {
1820
2903
  }
@@ -1823,39 +2906,44 @@ function readAgentVersion() {
1823
2906
  }
1824
2907
  function isDir(p) {
1825
2908
  try {
1826
- return fs6.statSync(p).isDirectory();
2909
+ return fs7.statSync(p).isDirectory();
1827
2910
  } catch {
1828
2911
  return false;
1829
2912
  }
1830
2913
  }
1831
2914
  function resolveAddonPackageDirs(addonsDir) {
1832
2915
  const dirs = [];
1833
- if (!fs6.existsSync(addonsDir)) return dirs;
1834
- for (const name of fs6.readdirSync(addonsDir)) {
1835
- const full = path6.join(addonsDir, name);
2916
+ if (!fs7.existsSync(addonsDir)) return dirs;
2917
+ for (const name of fs7.readdirSync(addonsDir)) {
2918
+ const full = path7.join(addonsDir, name);
1836
2919
  if (name.startsWith("@") && isDir(full)) {
1837
- for (const sub of fs6.readdirSync(full)) {
1838
- const subFull = path6.join(full, sub);
1839
- if (isDir(subFull) && fs6.existsSync(path6.join(subFull, "package.json"))) {
2920
+ for (const sub of fs7.readdirSync(full)) {
2921
+ const subFull = path7.join(full, sub);
2922
+ if (isDir(subFull) && fs7.existsSync(path7.join(subFull, "package.json"))) {
1840
2923
  dirs.push(subFull);
1841
2924
  }
1842
2925
  }
1843
- } else if (isDir(full) && fs6.existsSync(path6.join(full, "package.json"))) {
2926
+ } else if (isDir(full) && fs7.existsSync(path7.join(full, "package.json"))) {
1844
2927
  dirs.push(full);
1845
2928
  }
1846
2929
  }
1847
2930
  return dirs;
1848
2931
  }
1849
2932
  function buildAgentChildUdsManifest(nodeId, childId, caps) {
1850
- const systemCapNames = /* @__PURE__ */ new Set();
2933
+ const capsByAddon = /* @__PURE__ */ new Map();
1851
2934
  for (const cap of caps) {
1852
- if (cap.deviceId === void 0) {
1853
- systemCapNames.add(cap.capName);
1854
- }
2935
+ if (cap.deviceId !== void 0) continue;
2936
+ const addonId = cap.addonId ?? childId;
2937
+ const set = capsByAddon.get(addonId) ?? /* @__PURE__ */ new Set();
2938
+ set.add(cap.capName);
2939
+ capsByAddon.set(addonId, set);
1855
2940
  }
1856
- const addons = [
1857
- { addonId: childId, capabilities: [...systemCapNames] }
1858
- ];
2941
+ if (capsByAddon.size === 0) {
2942
+ capsByAddon.set(childId, /* @__PURE__ */ new Set());
2943
+ }
2944
+ const addons = [...capsByAddon.entries()].map(
2945
+ ([addonId, capNames]) => ({ addonId, capabilities: [...capNames] })
2946
+ );
1859
2947
  return { nodeId, addons };
1860
2948
  }
1861
2949
  var scriptName = process.argv[1] ?? "";
@@ -1981,18 +3069,18 @@ if (args.statusPort && !process.env["CAMSTACK_STATUS_PORT"]) {
1981
3069
  if (!process.env["CAMSTACK_FRAMEWORK_DIR"]) {
1982
3070
  const scriptPath = process.argv[1];
1983
3071
  if (scriptPath) {
1984
- const frameworkDir = path7.resolve(path7.dirname(scriptPath), "..", "..");
1985
- const frameworkModules = path7.join(frameworkDir, "node_modules");
1986
- if ((0, import_node_fs.existsSync)(path7.join(frameworkModules, "@camstack", "shm-ring"))) {
3072
+ const frameworkDir = path8.resolve(path8.dirname(scriptPath), "..", "..");
3073
+ const frameworkModules = path8.join(frameworkDir, "node_modules");
3074
+ if ((0, import_node_fs.existsSync)(path8.join(frameworkModules, "@camstack", "shm-ring"))) {
1987
3075
  process.env["CAMSTACK_FRAMEWORK_DIR"] = frameworkDir;
1988
3076
  const existingNodePath = process.env["NODE_PATH"];
1989
- process.env["NODE_PATH"] = existingNodePath ? `${frameworkModules}${path7.delimiter}${existingNodePath}` : frameworkModules;
3077
+ process.env["NODE_PATH"] = existingNodePath ? `${frameworkModules}${path8.delimiter}${existingNodePath}` : frameworkModules;
1990
3078
  import_node_module.Module._initPaths();
1991
3079
  console.log(
1992
3080
  `[Agent] Self-contained framework detected \u2014 CAMSTACK_FRAMEWORK_DIR=${frameworkDir}`
1993
3081
  );
1994
3082
  if (!process.env["CAMSTACK_BUNDLED_ADDONS_DIR"]) {
1995
- const bundledAddons = path7.join(frameworkDir, "addons");
3083
+ const bundledAddons = path8.join(frameworkDir, "addons");
1996
3084
  if ((0, import_node_fs.existsSync)(bundledAddons)) {
1997
3085
  process.env["CAMSTACK_BUNDLED_ADDONS_DIR"] = bundledAddons;
1998
3086
  }