@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/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,965 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // ../node-root/dist/index.js
34
+ var require_dist = __commonJS({
35
+ "../node-root/dist/index.js"(exports2, module2) {
36
+ "use strict";
37
+ var __create2 = Object.create;
38
+ var __defProp2 = Object.defineProperty;
39
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
40
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
41
+ var __getProtoOf2 = Object.getPrototypeOf;
42
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
43
+ var __export2 = (target, all) => {
44
+ for (var name in all)
45
+ __defProp2(target, name, { get: all[name], enumerable: true });
46
+ };
47
+ var __copyProps2 = (to, from, except, desc) => {
48
+ if (from && typeof from === "object" || typeof from === "function") {
49
+ for (let key of __getOwnPropNames2(from))
50
+ if (!__hasOwnProp2.call(to, key) && key !== except)
51
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
52
+ }
53
+ return to;
54
+ };
55
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(
56
+ // If the importer is in node compatibility mode or this is not an ESM
57
+ // file that has been converted to a CommonJS file using a Babel-
58
+ // compatible transform (i.e. "__esModule" has not been set), then set
59
+ // "default" to the CommonJS "module.exports" for node compatibility.
60
+ isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target,
61
+ mod
62
+ ));
63
+ var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
64
+ var src_exports2 = {};
65
+ __export2(src_exports2, {
66
+ AGENT_ROOT_SPEC: () => AGENT_ROOT_SPEC2,
67
+ HOST_EXTERNAL_SPECIFIERS: () => HOST_EXTERNAL_SPECIFIERS,
68
+ HUB_ROOT_SPEC: () => HUB_ROOT_SPEC,
69
+ RESTART_INTENT_FILE: () => RESTART_INTENT_FILE,
70
+ RootUpdateService: () => RootUpdateService2,
71
+ SERVER_ROOT_DIRNAME: () => SERVER_ROOT_DIRNAME,
72
+ SERVER_ROOT_STATE_FILE: () => SERVER_ROOT_STATE_FILE,
73
+ clearRestartIntentMarker: () => clearRestartIntentMarker,
74
+ compareSemver: () => compareSemver,
75
+ detectWorkspaceRoot: () => detectWorkspaceRoot,
76
+ emptyServerRootState: () => emptyServerRootState,
77
+ isHostExternal: () => isHostExternal,
78
+ isServerRootState: () => isServerRootState,
79
+ minNodeMajorOf: () => minNodeMajorOf,
80
+ planBoot: () => planBoot,
81
+ readRestartIntentMarker: () => readRestartIntentMarker,
82
+ readServerRootState: () => readServerRootState,
83
+ registerActiveRootResolver: () => registerActiveRootResolver,
84
+ restartIntentMarkerPath: () => restartIntentMarkerPath,
85
+ rootEntryPath: () => rootEntryPath,
86
+ rootPackageDir: () => rootPackageDir,
87
+ runNodeStarter: () => runNodeStarter,
88
+ serverRootDir: () => serverRootDir2,
89
+ stateFilePath: () => stateFilePath,
90
+ validateVersionDir: () => validateVersionDir,
91
+ versionDir: () => versionDir,
92
+ versionsDir: () => versionsDir,
93
+ writeRestartIntentMarker: () => writeRestartIntentMarker2,
94
+ writeServerRootState: () => writeServerRootState
95
+ });
96
+ module2.exports = __toCommonJS2(src_exports2);
97
+ var HUB_ROOT_SPEC = {
98
+ packageName: "@camstack/server",
99
+ entryRelPath: ["dist", "launcher.js"]
100
+ };
101
+ var AGENT_ROOT_SPEC2 = {
102
+ packageName: "@camstack/agent",
103
+ entryRelPath: ["dist", "cli.js"]
104
+ };
105
+ var fs8 = __toESM2(require("fs"));
106
+ var path8 = __toESM2(require("path"));
107
+ var SERVER_ROOT_DIRNAME = "server-root";
108
+ var SERVER_ROOT_STATE_FILE = "state.json";
109
+ var RESTART_INTENT_FILE = ".restart-intent";
110
+ function emptyServerRootState() {
111
+ return {
112
+ schemaVersion: 1,
113
+ currentVersion: null,
114
+ previousVersion: null,
115
+ pendingBoot: null,
116
+ rolledBack: null
117
+ };
118
+ }
119
+ function serverRootDir2(dataDir) {
120
+ return path8.join(dataDir, SERVER_ROOT_DIRNAME);
121
+ }
122
+ function versionsDir(rootDir) {
123
+ return path8.join(rootDir, "versions");
124
+ }
125
+ function versionDir(rootDir, version) {
126
+ return path8.join(versionsDir(rootDir), version);
127
+ }
128
+ function rootPackageDir(versionDirPath, spec) {
129
+ return path8.join(versionDirPath, "node_modules", ...spec.packageName.split("/"));
130
+ }
131
+ function rootEntryPath(versionDirPath, spec) {
132
+ return path8.join(rootPackageDir(versionDirPath, spec), ...spec.entryRelPath);
133
+ }
134
+ function stateFilePath(rootDir) {
135
+ return path8.join(rootDir, SERVER_ROOT_STATE_FILE);
136
+ }
137
+ function isNullableString(v) {
138
+ return v === null || typeof v === "string";
139
+ }
140
+ function isPendingBoot(v) {
141
+ if (typeof v !== "object" || v === null) return false;
142
+ const p = v;
143
+ return typeof p["version"] === "string" && isNullableString(p["fromVersion"]) && typeof p["requestedAtMs"] === "number" && typeof p["bootAttempts"] === "number";
144
+ }
145
+ function isRollbackInfo(v) {
146
+ if (typeof v !== "object" || v === null) return false;
147
+ const r = v;
148
+ return typeof r["fromVersion"] === "string" && isNullableString(r["toVersion"]) && typeof r["atMs"] === "number" && typeof r["reason"] === "string";
149
+ }
150
+ function isServerRootState(v) {
151
+ if (typeof v !== "object" || v === null) return false;
152
+ const s = v;
153
+ if (s["schemaVersion"] !== 1) return false;
154
+ if (!isNullableString(s["currentVersion"])) return false;
155
+ if (!isNullableString(s["previousVersion"])) return false;
156
+ if (s["pendingBoot"] !== null && !isPendingBoot(s["pendingBoot"])) return false;
157
+ if (s["rolledBack"] !== null && !isRollbackInfo(s["rolledBack"])) return false;
158
+ return true;
159
+ }
160
+ function readServerRootState(rootDir) {
161
+ try {
162
+ const raw = JSON.parse(fs8.readFileSync(stateFilePath(rootDir), "utf-8"));
163
+ return isServerRootState(raw) ? raw : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ function writeServerRootState(rootDir, state) {
169
+ fs8.mkdirSync(rootDir, { recursive: true });
170
+ const target = stateFilePath(rootDir);
171
+ const tmp = `${target}.tmp`;
172
+ fs8.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
173
+ fs8.renameSync(tmp, target);
174
+ }
175
+ function restartIntentMarkerPath(rootDir) {
176
+ return path8.join(rootDir, RESTART_INTENT_FILE);
177
+ }
178
+ function isRestartIntentMarker(v) {
179
+ if (typeof v !== "object" || v === null) return false;
180
+ const m = v;
181
+ return typeof m["requestedAtMs"] === "number" && typeof m["reason"] === "string";
182
+ }
183
+ function writeRestartIntentMarker2(rootDir, marker) {
184
+ fs8.mkdirSync(rootDir, { recursive: true });
185
+ const target = restartIntentMarkerPath(rootDir);
186
+ const tmp = `${target}.tmp`;
187
+ fs8.writeFileSync(tmp, JSON.stringify(marker, null, 2), "utf-8");
188
+ fs8.renameSync(tmp, target);
189
+ }
190
+ function readRestartIntentMarker(rootDir) {
191
+ try {
192
+ const raw = JSON.parse(fs8.readFileSync(restartIntentMarkerPath(rootDir), "utf-8"));
193
+ return isRestartIntentMarker(raw) ? raw : null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ function clearRestartIntentMarker(rootDir) {
199
+ try {
200
+ fs8.rmSync(restartIntentMarkerPath(rootDir), { force: true });
201
+ } catch {
202
+ }
203
+ }
204
+ function minNodeMajorOf(enginesNode) {
205
+ const match = /(\d+)/.exec(enginesNode);
206
+ if (match === null) return null;
207
+ const major = Number.parseInt(match[1] ?? "", 10);
208
+ return Number.isNaN(major) ? null : major;
209
+ }
210
+ function validateVersionDir(rootDir, version, nodeMajor, spec) {
211
+ const vDir = versionDir(rootDir, version);
212
+ const entry = rootEntryPath(vDir, spec);
213
+ if (!fs8.existsSync(entry)) {
214
+ return `root entry missing: ${entry}`;
215
+ }
216
+ const pkgJsonPath = path8.join(rootPackageDir(vDir, spec), "package.json");
217
+ let pkg;
218
+ try {
219
+ const raw = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf-8"));
220
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
221
+ return `package.json malformed: ${pkgJsonPath}`;
222
+ }
223
+ pkg = raw;
224
+ } catch {
225
+ return `package.json unreadable: ${pkgJsonPath}`;
226
+ }
227
+ if (pkg["name"] !== spec.packageName) {
228
+ return `package name mismatch: expected ${spec.packageName}, got ${String(pkg["name"])}`;
229
+ }
230
+ if (pkg["version"] !== version) {
231
+ return `package version mismatch: dir says ${version}, package.json says ${String(pkg["version"])}`;
232
+ }
233
+ const engines = pkg["engines"];
234
+ if (typeof engines === "object" && engines !== null) {
235
+ const enginesNode = engines["node"];
236
+ if (typeof enginesNode === "string") {
237
+ const minMajor = minNodeMajorOf(enginesNode);
238
+ if (minMajor !== null && nodeMajor < minMajor) {
239
+ return `engines.node "${enginesNode}" requires Node >= ${minMajor}, runtime is ${nodeMajor}`;
240
+ }
241
+ }
242
+ }
243
+ return null;
244
+ }
245
+ function planBoot(state, isValidVersion, now) {
246
+ if (state === null) {
247
+ return { kind: "baked", reason: "no server-root state", stateToWrite: null };
248
+ }
249
+ let next = state;
250
+ let changed = false;
251
+ if (next.pendingBoot !== null) {
252
+ const pending = next.pendingBoot;
253
+ if (pending.bootAttempts >= 1) {
254
+ next = {
255
+ ...next,
256
+ pendingBoot: null,
257
+ rolledBack: {
258
+ fromVersion: pending.version,
259
+ toVersion: next.currentVersion,
260
+ atMs: now(),
261
+ reason: "probation boot did not reach ready \u2014 rolled back"
262
+ }
263
+ };
264
+ changed = true;
265
+ } else if (!isValidVersion(pending.version)) {
266
+ next = {
267
+ ...next,
268
+ pendingBoot: null,
269
+ rolledBack: {
270
+ fromVersion: pending.version,
271
+ toVersion: next.currentVersion,
272
+ atMs: now(),
273
+ reason: "staged version failed validation"
274
+ }
275
+ };
276
+ changed = true;
277
+ } else {
278
+ return {
279
+ kind: "data-root",
280
+ version: pending.version,
281
+ probation: true,
282
+ stateToWrite: {
283
+ ...next,
284
+ pendingBoot: { ...pending, bootAttempts: pending.bootAttempts + 1 }
285
+ }
286
+ };
287
+ }
288
+ }
289
+ if (next.currentVersion !== null) {
290
+ if (isValidVersion(next.currentVersion)) {
291
+ return {
292
+ kind: "data-root",
293
+ version: next.currentVersion,
294
+ probation: false,
295
+ stateToWrite: changed ? next : null
296
+ };
297
+ }
298
+ const broken = next.currentVersion;
299
+ if (next.previousVersion !== null && isValidVersion(next.previousVersion)) {
300
+ const fallback = next.previousVersion;
301
+ return {
302
+ kind: "data-root",
303
+ version: fallback,
304
+ probation: false,
305
+ stateToWrite: {
306
+ ...next,
307
+ currentVersion: fallback,
308
+ previousVersion: null,
309
+ rolledBack: {
310
+ fromVersion: broken,
311
+ toVersion: fallback,
312
+ atMs: now(),
313
+ reason: "active version failed validation"
314
+ }
315
+ }
316
+ };
317
+ }
318
+ return {
319
+ kind: "baked",
320
+ reason: `active version ${broken} failed validation and no valid previous version exists`,
321
+ stateToWrite: {
322
+ ...next,
323
+ currentVersion: null,
324
+ rolledBack: {
325
+ fromVersion: broken,
326
+ toVersion: null,
327
+ atMs: now(),
328
+ reason: "active version failed validation"
329
+ }
330
+ }
331
+ };
332
+ }
333
+ return {
334
+ kind: "baked",
335
+ reason: "no active data-root version",
336
+ stateToWrite: changed ? next : null
337
+ };
338
+ }
339
+ function parse(version) {
340
+ const dashIdx = version.indexOf("-");
341
+ const base = dashIdx === -1 ? version : version.slice(0, dashIdx);
342
+ const prerelease = dashIdx === -1 ? null : version.slice(dashIdx + 1);
343
+ const nums = base.split(".").map((seg) => {
344
+ const n = Number.parseInt(seg, 10);
345
+ return Number.isNaN(n) ? 0 : n;
346
+ });
347
+ return { nums, prerelease };
348
+ }
349
+ function compareSemver(a, b) {
350
+ const pa = parse(a);
351
+ const pb = parse(b);
352
+ const len = Math.max(pa.nums.length, pb.nums.length);
353
+ for (let i = 0; i < len; i++) {
354
+ const na = pa.nums[i] ?? 0;
355
+ const nb = pb.nums[i] ?? 0;
356
+ if (na < nb) return -1;
357
+ if (na > nb) return 1;
358
+ }
359
+ if (pa.prerelease === null && pb.prerelease === null) return 0;
360
+ if (pa.prerelease === null) return 1;
361
+ if (pb.prerelease === null) return -1;
362
+ if (pa.prerelease < pb.prerelease) return -1;
363
+ if (pa.prerelease > pb.prerelease) return 1;
364
+ return 0;
365
+ }
366
+ var fs22 = __toESM2(require("fs"));
367
+ var path22 = __toESM2(require("path"));
368
+ function detectWorkspaceRoot(fromDir) {
369
+ let dir = path22.resolve(fromDir);
370
+ for (; ; ) {
371
+ const pkgPath = path22.join(dir, "package.json");
372
+ try {
373
+ const raw = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
374
+ if (typeof raw === "object" && raw !== null && raw["workspaces"] !== void 0) {
375
+ return dir;
376
+ }
377
+ } catch {
378
+ }
379
+ const parent = path22.dirname(dir);
380
+ if (parent === dir) return null;
381
+ dir = parent;
382
+ }
383
+ }
384
+ var path32 = __toESM2(require("path"));
385
+ var import_node_url = require("url");
386
+ var HOST_EXTERNAL_SPECIFIERS = [
387
+ "@camstack/system",
388
+ "@camstack/shm-ring",
389
+ "@camstack/types",
390
+ "@camstack/sdk",
391
+ "zod",
392
+ "@trpc/server",
393
+ "@trpc/client",
394
+ "sharp",
395
+ "node-av"
396
+ ];
397
+ function isHostExternal(specifier) {
398
+ return HOST_EXTERNAL_SPECIFIERS.some(
399
+ (name) => specifier === name || specifier.startsWith(`${name}/`)
400
+ );
401
+ }
402
+ function registerActiveRootResolver(activeRootDir) {
403
+ const moduleApi = require("module");
404
+ const registerHooks = moduleApi["registerHooks"];
405
+ if (typeof registerHooks !== "function") {
406
+ console.warn(
407
+ "[starter] module.registerHooks unavailable (Node < 22.15) \u2014 host-external redirect skipped"
408
+ );
409
+ return;
410
+ }
411
+ const anchorURL = (0, import_node_url.pathToFileURL)(
412
+ path32.join(activeRootDir, "node_modules", "__camstack_starter_anchor__.js")
413
+ ).href;
414
+ const hooks = {
415
+ resolve: (specifier, context, nextResolve) => {
416
+ if (isHostExternal(specifier)) {
417
+ try {
418
+ return nextResolve(specifier, { ...context, parentURL: anchorURL });
419
+ } catch {
420
+ }
421
+ }
422
+ return nextResolve(specifier, context);
423
+ }
424
+ };
425
+ registerHooks(hooks);
426
+ }
427
+ function runNodeStarter(options) {
428
+ const env = options.env ?? process.env;
429
+ const now = options.now ?? Date.now;
430
+ const registerResolver = options.registerResolver ?? registerActiveRootResolver;
431
+ const disabled = env[options.envNames.killSwitch] === "off";
432
+ const workspaceRoot = detectWorkspaceRoot(options.starterDir);
433
+ if (workspaceRoot !== null) {
434
+ console.log(
435
+ `[starter] workspace checkout detected at ${workspaceRoot} \u2014 using plain resolution`
436
+ );
437
+ env[options.envNames.bootMode] = "workspace";
438
+ options.loadEntry(options.seedEntry);
439
+ return;
440
+ }
441
+ if (disabled) {
442
+ console.log(`[starter] ${options.envNames.killSwitch}=off \u2014 booting the baked seed closure`);
443
+ env[options.envNames.bootMode] = "baked";
444
+ options.loadEntry(options.seedEntry);
445
+ return;
446
+ }
447
+ const rootDir = serverRootDir2(options.dataDir);
448
+ const state = readServerRootState(rootDir);
449
+ const nodeMajor = options.nodeMajor ?? Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
450
+ const isValidVersion = (version) => {
451
+ const reason = validateVersionDir(rootDir, version, nodeMajor, options.spec);
452
+ if (reason !== null) console.warn(`[starter] version ${version} invalid: ${reason}`);
453
+ return reason === null;
454
+ };
455
+ const plan = planBoot(state, isValidVersion, now);
456
+ if (plan.stateToWrite !== null) {
457
+ try {
458
+ writeServerRootState(rootDir, plan.stateToWrite);
459
+ } catch (err) {
460
+ console.error("[starter] FAILED to persist server-root state \u2014 booting baked seed:", err);
461
+ env[options.envNames.bootMode] = "baked";
462
+ options.loadEntry(options.seedEntry);
463
+ return;
464
+ }
465
+ if (plan.stateToWrite.rolledBack !== null && (state?.rolledBack ?? null) !== plan.stateToWrite.rolledBack) {
466
+ const rb = plan.stateToWrite.rolledBack;
467
+ console.warn(
468
+ `[starter] ROLLED BACK ${rb.fromVersion} -> ${rb.toVersion ?? "baked seed"}: ${rb.reason}`
469
+ );
470
+ }
471
+ }
472
+ if (plan.kind === "data-root") {
473
+ const activeRootDir = versionDir(rootDir, plan.version);
474
+ const entry = rootEntryPath(activeRootDir, options.spec);
475
+ console.log(
476
+ `[starter] booting ${options.spec.packageName}@${plan.version} from ${activeRootDir}` + (plan.probation ? " (probation boot \u2014 health-check armed)" : "")
477
+ );
478
+ env[options.envNames.bootMode] = "data-root";
479
+ env[options.envNames.activeRoot] = activeRootDir;
480
+ env[options.envNames.activeVersion] = plan.version;
481
+ registerResolver(activeRootDir);
482
+ options.loadEntry(entry);
483
+ return;
484
+ }
485
+ console.log(`[starter] booting baked seed closure (${plan.reason})`);
486
+ env[options.envNames.bootMode] = "baked";
487
+ options.loadEntry(options.seedEntry);
488
+ }
489
+ var import_node_child_process = require("child_process");
490
+ var fs32 = __toESM2(require("fs"));
491
+ var path42 = __toESM2(require("path"));
492
+ var import_node_util = require("util");
493
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
494
+ function buildNpmRegistryArgs(registry) {
495
+ if (registry === void 0 || registry.length === 0) return [];
496
+ return ["--registry", registry, `--@camstack:registry=${registry}`];
497
+ }
498
+ var NPM_VIEW_TIMEOUT_MS = 2e4;
499
+ var NPM_INSTALL_TIMEOUT_MS = 15 * 6e4;
500
+ var RESTART_REASON_PREFIX = "server-update";
501
+ function readPackageVersion(pkgJsonPath) {
502
+ try {
503
+ const raw = JSON.parse(fs32.readFileSync(pkgJsonPath, "utf-8"));
504
+ if (typeof raw === "object" && raw !== null) {
505
+ const version = raw["version"];
506
+ if (typeof version === "string") return version;
507
+ }
508
+ } catch {
509
+ }
510
+ return null;
511
+ }
512
+ var RootUpdateService2 = class _RootUpdateService {
513
+ spec;
514
+ envNames;
515
+ logger;
516
+ restartServerFn;
517
+ dataDir;
518
+ execNpm;
519
+ env;
520
+ now;
521
+ runningPackageJsonPath;
522
+ workspaceProbeDir;
523
+ checkCache = null;
524
+ inFlight = "idle";
525
+ constructor(options) {
526
+ this.spec = options.spec;
527
+ this.envNames = options.envNames;
528
+ this.logger = options.logger;
529
+ this.restartServerFn = options.restartServer;
530
+ this.dataDir = path42.resolve(options.dataDir);
531
+ this.execNpm = options.execNpm ?? (async (args, opts) => {
532
+ const { stdout } = await execFileAsync("npm", [...args], {
533
+ cwd: opts.cwd,
534
+ timeout: opts.timeoutMs
535
+ });
536
+ return { stdout };
537
+ });
538
+ this.env = options.env ?? process.env;
539
+ this.now = options.now ?? Date.now;
540
+ this.runningPackageJsonPath = options.runningPackageJsonPath;
541
+ this.workspaceProbeDir = options.workspaceProbeDir;
542
+ }
543
+ // ── Status ────────────────────────────────────────────────────────────
544
+ resolveBootMode() {
545
+ const raw = this.env[this.envNames.bootMode];
546
+ if (raw === "workspace" || raw === "baked" || raw === "data-root") return raw;
547
+ return detectWorkspaceRoot(this.workspaceProbeDir) !== null ? "workspace" : "baked";
548
+ }
549
+ runningVersion() {
550
+ return readPackageVersion(this.runningPackageJsonPath);
551
+ }
552
+ /** The running root package's identity — feeds registerNode / topology rows. */
553
+ getRunningRootPackage() {
554
+ return { name: this.spec.packageName, version: this.runningVersion() };
555
+ }
556
+ seedVersion() {
557
+ const seedDir = this.env[this.envNames.seedDir];
558
+ if (seedDir === void 0 || seedDir.length === 0) return null;
559
+ return readPackageVersion(path42.join(seedDir, "package.json"));
560
+ }
561
+ rootDir() {
562
+ return serverRootDir2(this.dataDir);
563
+ }
564
+ readState() {
565
+ return readServerRootState(this.rootDir()) ?? emptyServerRootState();
566
+ }
567
+ /**
568
+ * Like `readState`, but distinguishes "state.json missing" (normal on a
569
+ * fresh node) from "state.json present but corrupt" — in the corrupt case
570
+ * the starter booted the baked seed while installed data-dir versions are
571
+ * silently ignored, which the status surface must make visible.
572
+ */
573
+ readStateInfo() {
574
+ const rootDir = this.rootDir();
575
+ const raw = readServerRootState(rootDir);
576
+ if (raw !== null) return { state: raw, corrupt: false };
577
+ return { state: emptyServerRootState(), corrupt: fs32.existsSync(stateFilePath(rootDir)) };
578
+ }
579
+ updateState(state) {
580
+ if (this.inFlight === "checking") return "checking";
581
+ if (this.inFlight === "staging") return "staging";
582
+ if (state.pendingBoot !== null) {
583
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
584
+ if (activeVersion !== null && activeVersion === state.pendingBoot.version) {
585
+ return "awaiting-confirmation";
586
+ }
587
+ return "pending-restart";
588
+ }
589
+ return "idle";
590
+ }
591
+ async getServerPackageStatus() {
592
+ const { state, corrupt } = this.readStateInfo();
593
+ const runningVersion = this.runningVersion();
594
+ const latestVersion = this.checkCache?.latestVersion ?? null;
595
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
596
+ return {
597
+ packageName: this.spec.packageName,
598
+ runningVersion,
599
+ nodeRuntimeVersion: process.versions.node,
600
+ activeVersion: this.env[this.envNames.activeVersion] ?? state.currentVersion,
601
+ previousVersion: state.previousVersion,
602
+ seedVersion: this.seedVersion(),
603
+ latestVersion,
604
+ updateAvailable,
605
+ bootMode: this.resolveBootMode(),
606
+ updateState: this.updateState(state),
607
+ pendingVersion: state.pendingBoot?.version ?? null,
608
+ rolledBack: state.rolledBack,
609
+ stateFileCorrupt: corrupt,
610
+ lastCheckedAtMs: this.checkCache?.checkedAtMs ?? null
611
+ };
612
+ }
613
+ // ── Check ─────────────────────────────────────────────────────────────
614
+ async checkServerUpdate() {
615
+ const runningVersion = this.runningVersion();
616
+ if (this.inFlight !== "idle") {
617
+ return {
618
+ packageName: this.spec.packageName,
619
+ runningVersion,
620
+ latestVersion: this.checkCache?.latestVersion ?? null,
621
+ updateAvailable: false,
622
+ checkedAtMs: this.checkCache?.checkedAtMs ?? this.now(),
623
+ error: `busy: ${this.inFlight}`
624
+ };
625
+ }
626
+ this.inFlight = "checking";
627
+ try {
628
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
629
+ const args = [
630
+ "view",
631
+ `${this.spec.packageName}@latest`,
632
+ "version",
633
+ ...buildNpmRegistryArgs(registry)
634
+ ];
635
+ const { stdout } = await this.execNpm(args, { timeoutMs: NPM_VIEW_TIMEOUT_MS });
636
+ const latestVersion = stdout.trim().length > 0 ? stdout.trim() : null;
637
+ this.checkCache = { latestVersion, checkedAtMs: this.now(), error: null };
638
+ const updateAvailable = latestVersion !== null && runningVersion !== null && compareSemver(latestVersion, runningVersion) > 0;
639
+ return {
640
+ packageName: this.spec.packageName,
641
+ runningVersion,
642
+ latestVersion,
643
+ updateAvailable,
644
+ checkedAtMs: this.checkCache.checkedAtMs,
645
+ error: null
646
+ };
647
+ } catch (err) {
648
+ const message = err instanceof Error ? err.message : String(err);
649
+ this.checkCache = {
650
+ latestVersion: this.checkCache?.latestVersion ?? null,
651
+ checkedAtMs: this.now(),
652
+ error: message
653
+ };
654
+ this.logger.warn("root package update check failed", { meta: { error: message } });
655
+ return {
656
+ packageName: this.spec.packageName,
657
+ runningVersion,
658
+ latestVersion: this.checkCache.latestVersion,
659
+ updateAvailable: false,
660
+ checkedAtMs: this.checkCache.checkedAtMs,
661
+ error: message
662
+ };
663
+ } finally {
664
+ this.inFlight = "idle";
665
+ }
666
+ }
667
+ // ── Apply ─────────────────────────────────────────────────────────────
668
+ async applyServerUpdate(input) {
669
+ const bootMode = this.resolveBootMode();
670
+ if (bootMode === "workspace") {
671
+ return {
672
+ accepted: false,
673
+ targetVersion: input.version ?? null,
674
+ restarting: false,
675
+ message: "Refused: running from a workspace checkout \u2014 the dev loop resolves code from the workspace, a data-root update would never be loaded."
676
+ };
677
+ }
678
+ if (this.inFlight !== "idle") {
679
+ return {
680
+ accepted: false,
681
+ targetVersion: input.version ?? null,
682
+ restarting: false,
683
+ message: `Refused: another operation is in flight (${this.inFlight}).`
684
+ };
685
+ }
686
+ const stateBefore = this.readState();
687
+ if (stateBefore.pendingBoot !== null) {
688
+ return {
689
+ accepted: false,
690
+ targetVersion: input.version ?? null,
691
+ restarting: false,
692
+ message: `Refused: version ${stateBefore.pendingBoot.version} is already staged and awaiting restart.`
693
+ };
694
+ }
695
+ let target = input.version ?? null;
696
+ if (target === null) {
697
+ const check = await this.checkServerUpdate();
698
+ if (check.error !== null || check.latestVersion === null) {
699
+ return {
700
+ accepted: false,
701
+ targetVersion: null,
702
+ restarting: false,
703
+ message: `Refused: could not resolve the latest version (${check.error ?? "no version returned"}).`
704
+ };
705
+ }
706
+ target = check.latestVersion;
707
+ }
708
+ const runningVersion = this.runningVersion();
709
+ if (target === runningVersion) {
710
+ return {
711
+ accepted: false,
712
+ targetVersion: target,
713
+ restarting: false,
714
+ message: `Refused: ${this.spec.packageName}@${target} is already running.`
715
+ };
716
+ }
717
+ this.inFlight = "staging";
718
+ try {
719
+ await this.stageAndActivate(target);
720
+ } catch (err) {
721
+ const message = err instanceof Error ? err.message : String(err);
722
+ this.logger.error("root package update staging failed", {
723
+ meta: { targetVersion: target, error: message }
724
+ });
725
+ return {
726
+ accepted: false,
727
+ targetVersion: target,
728
+ restarting: false,
729
+ message: `Staging failed: ${message}`
730
+ };
731
+ } finally {
732
+ this.inFlight = "idle";
733
+ }
734
+ const state = this.readState();
735
+ writeServerRootState(this.rootDir(), {
736
+ ...state,
737
+ pendingBoot: {
738
+ version: target,
739
+ fromVersion: state.currentVersion,
740
+ requestedAtMs: this.now(),
741
+ bootAttempts: 0
742
+ },
743
+ rolledBack: null
744
+ });
745
+ this.logger.info("root package update staged \u2014 restarting to apply", {
746
+ meta: { targetVersion: target, fromVersion: state.currentVersion }
747
+ });
748
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
749
+ return {
750
+ accepted: true,
751
+ targetVersion: target,
752
+ restarting: true,
753
+ message: `Staged ${this.spec.packageName}@${target} \u2014 restarting to apply (probation boot with auto-rollback).`
754
+ };
755
+ }
756
+ /**
757
+ * npm-install the target closure into a SAME-FS staging dir inside
758
+ * `versions/`, validate it, then atomically rename it into place.
759
+ */
760
+ async stageAndActivate(target) {
761
+ const rootDir = this.rootDir();
762
+ const vDir = versionsDir(rootDir);
763
+ fs32.mkdirSync(vDir, { recursive: true });
764
+ const stagingDir = path42.join(vDir, `.staging-${target}-${process.pid}-${this.now()}`);
765
+ fs32.mkdirSync(stagingDir, { recursive: true });
766
+ try {
767
+ fs32.writeFileSync(
768
+ path42.join(stagingDir, "package.json"),
769
+ JSON.stringify({ name: "camstack-node-root", private: true }, null, 2),
770
+ "utf-8"
771
+ );
772
+ const registry = this.env["CAMSTACK_NPM_REGISTRY"];
773
+ await this.execNpm(
774
+ [
775
+ "install",
776
+ "--omit=dev",
777
+ "--no-audit",
778
+ "--no-fund",
779
+ "--loglevel=error",
780
+ `${this.spec.packageName}@${target}`,
781
+ ...buildNpmRegistryArgs(registry)
782
+ ],
783
+ { cwd: stagingDir, timeoutMs: NPM_INSTALL_TIMEOUT_MS }
784
+ );
785
+ const entry = rootEntryPath(stagingDir, this.spec);
786
+ if (!fs32.existsSync(entry)) {
787
+ throw new Error(`staged closure is missing the root entry (${entry})`);
788
+ }
789
+ const stagedVersion = readPackageVersion(
790
+ path42.join(rootPackageDir(stagingDir, this.spec), "package.json")
791
+ );
792
+ if (stagedVersion !== target) {
793
+ throw new Error(
794
+ `staged closure version mismatch: expected ${target}, got ${stagedVersion ?? "unknown"}`
795
+ );
796
+ }
797
+ const dest = versionDir(rootDir, target);
798
+ if (fs32.existsSync(dest)) {
799
+ const aside = `${dest}.evicted-${this.now()}`;
800
+ await fs32.promises.rename(dest, aside);
801
+ await fs32.promises.rm(aside, { recursive: true, force: true }).catch(() => void 0);
802
+ }
803
+ await fs32.promises.rename(stagingDir, dest);
804
+ } catch (err) {
805
+ await fs32.promises.rm(stagingDir, { recursive: true, force: true }).catch(() => void 0);
806
+ throw err;
807
+ }
808
+ }
809
+ // ── Rollback ──────────────────────────────────────────────────────────
810
+ async rollbackServerUpdate() {
811
+ if (this.inFlight !== "idle") {
812
+ return {
813
+ accepted: false,
814
+ targetVersion: null,
815
+ restarting: false,
816
+ message: `Refused: another operation is in flight (${this.inFlight}).`
817
+ };
818
+ }
819
+ const state = this.readState();
820
+ if (state.pendingBoot !== null) {
821
+ return {
822
+ accepted: false,
823
+ targetVersion: null,
824
+ restarting: false,
825
+ message: `Refused: version ${state.pendingBoot.version} is already staged and awaiting restart.`
826
+ };
827
+ }
828
+ if (state.currentVersion === null || state.previousVersion === null) {
829
+ return {
830
+ accepted: false,
831
+ targetVersion: state.previousVersion,
832
+ restarting: false,
833
+ message: "Refused: no previous version available to roll back to."
834
+ };
835
+ }
836
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
837
+ const invalidReason = validateVersionDir(
838
+ this.rootDir(),
839
+ state.previousVersion,
840
+ nodeMajor,
841
+ this.spec
842
+ );
843
+ if (invalidReason !== null) {
844
+ return {
845
+ accepted: false,
846
+ targetVersion: state.previousVersion,
847
+ restarting: false,
848
+ message: `Refused: previous version ${state.previousVersion} is not loadable (${invalidReason}).`
849
+ };
850
+ }
851
+ writeServerRootState(this.rootDir(), {
852
+ ...state,
853
+ pendingBoot: {
854
+ version: state.previousVersion,
855
+ fromVersion: state.currentVersion,
856
+ requestedAtMs: this.now(),
857
+ bootAttempts: 0
858
+ }
859
+ });
860
+ this.logger.info("root package rollback requested \u2014 restarting", {
861
+ meta: { fromVersion: state.currentVersion, toVersion: state.previousVersion }
862
+ });
863
+ this.restartServerFn(
864
+ `${RESTART_REASON_PREFIX}: rollback to ${this.spec.packageName}@${state.previousVersion}`
865
+ );
866
+ return {
867
+ accepted: true,
868
+ targetVersion: state.previousVersion,
869
+ restarting: true,
870
+ message: `Rolling back to ${this.spec.packageName}@${state.previousVersion} \u2014 restarting.`
871
+ };
872
+ }
873
+ // ── Plain restart ─────────────────────────────────────────────────────
874
+ /**
875
+ * Plain process restart — no version change. Backs the `restartServer` cap
876
+ * method. Guards mirror apply/rollback: refuse while a stage is in flight
877
+ * (a concurrent npm install must not be interrupted) or while a version is
878
+ * already staged awaiting restart (a naive bounce would boot the OLD version
879
+ * and orphan the pending one — the operator should apply/rollback instead).
880
+ */
881
+ restartNode() {
882
+ const runningVersion = this.runningVersion();
883
+ if (this.inFlight !== "idle") {
884
+ return {
885
+ accepted: false,
886
+ targetVersion: runningVersion,
887
+ restarting: false,
888
+ message: `Refused: another operation is in flight (${this.inFlight}).`
889
+ };
890
+ }
891
+ const state = this.readState();
892
+ if (state.pendingBoot !== null) {
893
+ return {
894
+ accepted: false,
895
+ targetVersion: runningVersion,
896
+ restarting: false,
897
+ message: `Refused: version ${state.pendingBoot.version} is staged and awaiting restart \u2014 apply or roll it back instead of a plain restart.`
898
+ };
899
+ }
900
+ this.logger.info("plain node restart requested", { meta: { runningVersion } });
901
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: manual restart`);
902
+ return {
903
+ accepted: true,
904
+ targetVersion: runningVersion,
905
+ restarting: true,
906
+ message: "Restarting the node \u2014 the supervisor relaunches it on the same version."
907
+ };
908
+ }
909
+ // ── Boot health confirmation ──────────────────────────────────────────
910
+ /**
911
+ * Called by the post-boot path once the node is READY (hub: post-boot
912
+ * pipeline; agent: first acked `$hub.registerNode`). When THIS process is
913
+ * the probation boot of a pending version, promote it: `currentVersion` ←
914
+ * pending, `previousVersion` ← the version it replaced (N-1 retained for
915
+ * rollback), clear `pendingBoot` + `rolledBack`, and prune every version
916
+ * dir beyond {current, previous}. No-op otherwise.
917
+ */
918
+ confirmBootHealthy() {
919
+ const state = readServerRootState(this.rootDir());
920
+ if (state === null || state.pendingBoot === null) return { promoted: null };
921
+ const activeVersion = this.env[this.envNames.activeVersion] ?? null;
922
+ if (activeVersion !== state.pendingBoot.version) {
923
+ return { promoted: null };
924
+ }
925
+ const promoted = {
926
+ ...state,
927
+ currentVersion: state.pendingBoot.version,
928
+ previousVersion: state.pendingBoot.fromVersion,
929
+ pendingBoot: null,
930
+ rolledBack: null
931
+ };
932
+ writeServerRootState(this.rootDir(), promoted);
933
+ this.pruneVersions(
934
+ [promoted.currentVersion, promoted.previousVersion].filter(
935
+ (v) => typeof v === "string"
936
+ )
937
+ );
938
+ this.logger.info("root package update confirmed healthy", {
939
+ meta: {
940
+ version: promoted.currentVersion,
941
+ previousVersion: promoted.previousVersion
942
+ }
943
+ });
944
+ return { promoted: promoted.currentVersion };
945
+ }
946
+ /**
947
+ * Remove version dirs under `versions/` not in `keep`.
948
+ *
949
+ * Transient dot-entries (`.staging-*` / `.evicted-*`) are NOT pruned unless
950
+ * older than {@link RootUpdateService.STALE_TRANSIENT_MS}: the API starts
951
+ * serving BEFORE the post-boot `confirmBootHealthy()` runs, so a concurrent
952
+ * `applyServerUpdate` can be mid-npm-install into a fresh `.staging-*` dir
953
+ * when this prune fires — deleting it would corrupt that install. Fresh
954
+ * transient dirs are owned by their in-flight operation (which cleans up on
955
+ * failure); the staleness sweep only collects dirs orphaned by a crash.
956
+ */
957
+ pruneVersions(keep) {
958
+ const vDir = versionsDir(this.rootDir());
959
+ let entries;
960
+ try {
961
+ entries = fs32.readdirSync(vDir);
962
+ } catch {
963
+ return;
964
+ }
965
+ for (const entry of entries) {
966
+ if (keep.includes(entry)) continue;
967
+ const full = path42.join(vDir, entry);
968
+ if (entry.startsWith(".")) {
969
+ try {
970
+ const ageMs = this.now() - fs32.statSync(full).mtimeMs;
971
+ if (ageMs < _RootUpdateService.STALE_TRANSIENT_MS) continue;
972
+ } catch {
973
+ continue;
974
+ }
975
+ }
976
+ try {
977
+ fs32.rmSync(full, { recursive: true, force: true });
978
+ this.logger.debug("pruned server-root version dir", { meta: { entry } });
979
+ } catch (err) {
980
+ this.logger.warn("failed to prune server-root version dir", {
981
+ meta: { entry, error: err instanceof Error ? err.message : String(err) }
982
+ });
983
+ }
984
+ }
985
+ }
986
+ /** Transient (.staging- / .evicted-) dirs younger than this are never pruned. */
987
+ static STALE_TRANSIENT_MS = 24 * 60 * 60 * 1e3;
988
+ };
989
+ }
990
+ });
991
+
30
992
  // src/index.ts
31
993
  var src_exports = {};
32
994
  __export(src_exports, {
@@ -231,8 +1193,8 @@ function getLocalIps() {
231
1193
  }
232
1194
  async function withTimeout(p, ms, fallback) {
233
1195
  let timer;
234
- const timeout = new Promise((resolve5) => {
235
- timer = setTimeout(() => resolve5(fallback), ms);
1196
+ const timeout = new Promise((resolve6) => {
1197
+ timer = setTimeout(() => resolve6(fallback), ms);
236
1198
  });
237
1199
  try {
238
1200
  return await Promise.race([p, timeout]);
@@ -652,8 +1614,8 @@ function createAgentService(deps) {
652
1614
  }
653
1615
 
654
1616
  // src/agent-bootstrap.ts
655
- var fs6 = __toESM(require("fs"));
656
- var path6 = __toESM(require("path"));
1617
+ var fs7 = __toESM(require("fs"));
1618
+ var path7 = __toESM(require("path"));
657
1619
 
658
1620
  // src/agent-http.ts
659
1621
  var fs4 = __toESM(require("fs"));
@@ -969,15 +1931,98 @@ var import_system3 = require("@camstack/system");
969
1931
  var import_types2 = require("@camstack/types");
970
1932
  var import_types3 = require("@camstack/types");
971
1933
 
972
- // src/agent-group-runner.ts
1934
+ // src/agent-update-service.ts
973
1935
  var fs5 = __toESM(require("fs"));
974
1936
  var path5 = __toESM(require("path"));
1937
+ var import_node_root = __toESM(require_dist());
1938
+ var AGENT_RUNTIME_ADDON_ID = "agent-runtime";
1939
+ var AGENT_RESTART_GRACE_MS = 2e3;
1940
+ var AGENT_LOCAL_CONFIRM_GRACE_MS = 9e4;
1941
+ function armLocalConfirmFallback(onGrace, graceMs = AGENT_LOCAL_CONFIRM_GRACE_MS) {
1942
+ const timer = setTimeout(() => onGrace(), graceMs);
1943
+ const maybeUnref = timer.unref;
1944
+ if (typeof maybeUnref === "function") maybeUnref.call(timer);
1945
+ return () => clearTimeout(timer);
1946
+ }
1947
+ function resolveAgentPackageJsonPath(fromDir) {
1948
+ const candidates = [
1949
+ path5.resolve(fromDir, "..", "package.json"),
1950
+ path5.resolve(fromDir, "..", "..", "package.json")
1951
+ ];
1952
+ for (const candidate of candidates) {
1953
+ try {
1954
+ const raw = JSON.parse(fs5.readFileSync(candidate, "utf-8"));
1955
+ if (raw.name === import_node_root.AGENT_ROOT_SPEC.packageName) return candidate;
1956
+ } catch {
1957
+ }
1958
+ }
1959
+ return candidates[0] ?? path5.resolve(fromDir, "..", "package.json");
1960
+ }
1961
+ function scheduleAgentRestart(logger, requestedBy, dataDir) {
1962
+ if (dataDir !== void 0 && dataDir.length > 0) {
1963
+ try {
1964
+ (0, import_node_root.writeRestartIntentMarker)((0, import_node_root.serverRootDir)(dataDir), {
1965
+ requestedAtMs: Date.now(),
1966
+ reason: requestedBy
1967
+ });
1968
+ } catch (err) {
1969
+ logger.warn("failed to write restart-intent marker", {
1970
+ meta: { error: err instanceof Error ? err.message : String(err) }
1971
+ });
1972
+ }
1973
+ }
1974
+ logger.warn("agent restart requested \u2014 exiting for supervisor relaunch", {
1975
+ meta: { requestedBy, graceMs: AGENT_RESTART_GRACE_MS }
1976
+ });
1977
+ const grace = setTimeout(() => {
1978
+ process.kill(process.pid, "SIGTERM");
1979
+ const hard = setTimeout(() => process.exit(0), 15e3);
1980
+ hard.unref();
1981
+ }, AGENT_RESTART_GRACE_MS);
1982
+ grace.unref();
1983
+ }
1984
+ var AgentUpdateService = class extends import_node_root.RootUpdateService {
1985
+ constructor(options) {
1986
+ super({
1987
+ spec: import_node_root.AGENT_ROOT_SPEC,
1988
+ envNames: {
1989
+ bootMode: "CAMSTACK_AGENT_BOOT_MODE",
1990
+ activeVersion: "CAMSTACK_AGENT_ACTIVE_VERSION",
1991
+ seedDir: "CAMSTACK_SEED_AGENT_DIR"
1992
+ },
1993
+ logger: options.logger,
1994
+ restartServer: options.restartAgent ?? ((requestedBy) => scheduleAgentRestart(options.logger, requestedBy, options.dataDir)),
1995
+ dataDir: options.dataDir,
1996
+ runningPackageJsonPath: options.runningPackageJsonPath ?? resolveAgentPackageJsonPath(__dirname),
1997
+ workspaceProbeDir: __dirname,
1998
+ execNpm: options.execNpm,
1999
+ env: options.env,
2000
+ now: options.now
2001
+ });
2002
+ }
2003
+ };
2004
+ function buildAgentServerManagementProvider(service) {
2005
+ return {
2006
+ getServerPackageStatus: () => service.getServerPackageStatus(),
2007
+ checkServerUpdate: () => service.checkServerUpdate(),
2008
+ applyServerUpdate: (input) => service.applyServerUpdate(input),
2009
+ rollbackServerUpdate: () => service.rollbackServerUpdate(),
2010
+ restartServer: () => Promise.resolve(service.restartNode())
2011
+ };
2012
+ }
2013
+ function agentRuntimeManifestEntry() {
2014
+ return { addonId: AGENT_RUNTIME_ADDON_ID, capabilities: ["server-management"] };
2015
+ }
2016
+
2017
+ // src/agent-group-runner.ts
2018
+ var fs6 = __toESM(require("fs"));
2019
+ var path6 = __toESM(require("path"));
975
2020
  var import_types = require("@camstack/types");
976
2021
  function readPackageManifestAddons(dir) {
977
2022
  try {
978
- const pkgPath = path5.join(dir, "package.json");
979
- if (!fs5.existsSync(pkgPath)) return null;
980
- const raw = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
2023
+ const pkgPath = path6.join(dir, "package.json");
2024
+ if (!fs6.existsSync(pkgPath)) return null;
2025
+ const raw = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
981
2026
  const packageName = typeof raw.name === "string" ? raw.name : "";
982
2027
  const packageVersion = typeof raw.version === "string" ? raw.version : "0.0.0";
983
2028
  const entries = Array.isArray(raw.camstack?.addons) ? raw.camstack.addons : [];
@@ -1148,7 +2193,7 @@ async function startAgent(configPath) {
1148
2193
  const bundledDir = process.env["CAMSTACK_BUNDLED_ADDONS_DIR"];
1149
2194
  let workspaceDir = null;
1150
2195
  let resolvedSource = explicitSource;
1151
- if (bundledDir && fs6.existsSync(bundledDir)) {
2196
+ if (bundledDir && fs7.existsSync(bundledDir)) {
1152
2197
  workspaceDir = bundledDir;
1153
2198
  resolvedSource = "local";
1154
2199
  console.log(`[Agent] Using bundled addons from ${bundledDir}`);
@@ -1212,9 +2257,9 @@ async function startAgent(configPath) {
1212
2257
  return discoveryMode ? "searching" : "disconnected";
1213
2258
  }
1214
2259
  function readConfigFile2(p) {
1215
- if (!fs6.existsSync(p)) return {};
2260
+ if (!fs7.existsSync(p)) return {};
1216
2261
  try {
1217
- return JSON.parse(fs6.readFileSync(p, "utf-8"));
2262
+ return JSON.parse(fs7.readFileSync(p, "utf-8"));
1218
2263
  } catch {
1219
2264
  return {};
1220
2265
  }
@@ -1244,6 +2289,7 @@ async function startAgent(configPath) {
1244
2289
  const caps = addonCapMap.get(addonId) ?? [];
1245
2290
  result.push({ addonId, capabilities: caps });
1246
2291
  }
2292
+ result.push(agentRuntimeManifestEntry());
1247
2293
  return result;
1248
2294
  }
1249
2295
  function aggregateManifest() {
@@ -1260,11 +2306,13 @@ async function startAgent(configPath) {
1260
2306
  allNativeCaps.push(...childNativeCaps);
1261
2307
  }
1262
2308
  }
2309
+ const rootPackage = agentUpdateService.getRunningRootPackage();
1263
2310
  return (0, import_system3.buildNodeManifest)(
1264
2311
  config.nodeId,
1265
2312
  allAddons,
1266
2313
  allNativeCaps.length > 0 ? allNativeCaps : void 0,
1267
- config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0
2314
+ config.secret ? (0, import_system3.hashClusterSecret)(config.secret) : void 0,
2315
+ rootPackage.version === null ? void 0 : { name: rootPackage.name, version: rootPackage.version }
1268
2316
  );
1269
2317
  }
1270
2318
  function triggerUpwardRegistration() {
@@ -1276,7 +2324,9 @@ async function startAgent(configPath) {
1276
2324
  signal: registerAbortController.signal,
1277
2325
  log: (msg) => console.log(`[Agent] ${msg}`)
1278
2326
  }
1279
- ).catch((err) => {
2327
+ ).then(() => {
2328
+ confirmAgentBootHealthy("hub-ack");
2329
+ }).catch((err) => {
1280
2330
  if ((0, import_system3.isClusterSecretMismatchError)(err)) {
1281
2331
  consoleLogger.error(
1282
2332
  "hub registration rejected: cluster secret mismatch \u2014 correct CAMSTACK_CLUSTER_SECRET and restart the agent"
@@ -1306,11 +2356,41 @@ async function startAgent(configPath) {
1306
2356
  import_types3.pipelineExecutorCapability,
1307
2357
  import_types3.pipelineRunnerCapability,
1308
2358
  import_types3.audioAnalyzerCapability,
1309
- import_types3.platformProbeCapability
2359
+ import_types3.platformProbeCapability,
2360
+ import_types3.serverManagementCapability
1310
2361
  ];
1311
2362
  for (const cap of agentCapabilities) {
1312
2363
  capabilityRegistry.declareCapability(cap);
1313
2364
  }
2365
+ const agentUpdateService = new AgentUpdateService({
2366
+ logger: consoleLogger,
2367
+ dataDir: config.dataDir
2368
+ });
2369
+ capabilityRegistry.registerProvider(
2370
+ "server-management",
2371
+ AGENT_RUNTIME_ADDON_ID,
2372
+ buildAgentServerManagementProvider(agentUpdateService)
2373
+ );
2374
+ let agentBootConfirmed = false;
2375
+ let cancelLocalConfirmFallback = null;
2376
+ const confirmAgentBootHealthy = (source) => {
2377
+ if (agentBootConfirmed) return;
2378
+ agentBootConfirmed = true;
2379
+ cancelLocalConfirmFallback?.();
2380
+ cancelLocalConfirmFallback = null;
2381
+ try {
2382
+ const confirm = agentUpdateService.confirmBootHealthy();
2383
+ if (confirm.promoted !== null) {
2384
+ consoleLogger.info(
2385
+ `agent root package update confirmed healthy (${source}): @camstack/agent@${confirm.promoted}`
2386
+ );
2387
+ }
2388
+ } catch (err) {
2389
+ consoleLogger.warn(
2390
+ `agent root boot confirmation failed: ${err instanceof Error ? err.message : String(err)}`
2391
+ );
2392
+ }
2393
+ };
1314
2394
  const loggerFactory = (addonId) => agentLogManager.createLogger().withTags({ addonId });
1315
2395
  const agentServiceSchema = createAgentService({
1316
2396
  addonsDir: config.addonsDir,
@@ -1524,6 +2604,7 @@ async function startAgent(configPath) {
1524
2604
  capabilityRegistry
1525
2605
  );
1526
2606
  triggerUpwardRegistration();
2607
+ cancelLocalConfirmFallback = armLocalConfirmFallback(() => confirmAgentBootHealthy("local-grace"));
1527
2608
  let hubReachableFired = false;
1528
2609
  broker.localBus.on("$node.connected", (data) => {
1529
2610
  const node = data.node;
@@ -1547,6 +2628,8 @@ async function startAgent(configPath) {
1547
2628
  const shutdown = async () => {
1548
2629
  console.log("[Agent] Shutting down...");
1549
2630
  registerAbortController.abort();
2631
+ cancelLocalConfirmFallback?.();
2632
+ cancelLocalConfirmFallback = null;
1550
2633
  udsEventBridgeDispose?.();
1551
2634
  udsEventBridgeDispose = null;
1552
2635
  for (const [, entry] of loadedAddons) {
@@ -1707,7 +2790,7 @@ async function loadClusterCapableAddons(broker, config, capabilityRegistry, load
1707
2790
  }
1708
2791
  }
1709
2792
  async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1710
- if (!fs6.existsSync(addonsDir)) return;
2793
+ if (!fs7.existsSync(addonsDir)) return;
1711
2794
  const packageDirs = resolveAddonPackageDirs(addonsDir);
1712
2795
  const groupCandidates = [];
1713
2796
  const inProcessDirs = /* @__PURE__ */ new Set();
@@ -1767,7 +2850,7 @@ async function loadDeployedAddons(broker, addonsDir, dataDir, loadedAddons, stor
1767
2850
  }
1768
2851
  }
1769
2852
  async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons, storageProvider, loggerFactory, capabilityRegistry) {
1770
- const modelsDir = path6.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
2853
+ const modelsDir = path7.join(process.env["CAMSTACK_DATA"] ?? dataDir, "models");
1771
2854
  const contextOptions = {
1772
2855
  storageProvider,
1773
2856
  addonConfig: { modelsDir },
@@ -1817,13 +2900,13 @@ async function loadInProcessDeployedAddons(broker, dirs, dataDir, loadedAddons,
1817
2900
  }
1818
2901
  function readAgentVersion() {
1819
2902
  const candidates = [
1820
- path6.resolve(__dirname, "..", "package.json"),
1821
- path6.resolve(__dirname, "..", "..", "package.json")
2903
+ path7.resolve(__dirname, "..", "package.json"),
2904
+ path7.resolve(__dirname, "..", "..", "package.json")
1822
2905
  ];
1823
2906
  for (const candidate of candidates) {
1824
2907
  try {
1825
- if (!fs6.existsSync(candidate)) continue;
1826
- const raw = JSON.parse(fs6.readFileSync(candidate, "utf-8"));
2908
+ if (!fs7.existsSync(candidate)) continue;
2909
+ const raw = JSON.parse(fs7.readFileSync(candidate, "utf-8"));
1827
2910
  if (raw.name === "@camstack/agent" && typeof raw.version === "string") return raw.version;
1828
2911
  } catch {
1829
2912
  }
@@ -1832,39 +2915,44 @@ function readAgentVersion() {
1832
2915
  }
1833
2916
  function isDir(p) {
1834
2917
  try {
1835
- return fs6.statSync(p).isDirectory();
2918
+ return fs7.statSync(p).isDirectory();
1836
2919
  } catch {
1837
2920
  return false;
1838
2921
  }
1839
2922
  }
1840
2923
  function resolveAddonPackageDirs(addonsDir) {
1841
2924
  const dirs = [];
1842
- if (!fs6.existsSync(addonsDir)) return dirs;
1843
- for (const name of fs6.readdirSync(addonsDir)) {
1844
- const full = path6.join(addonsDir, name);
2925
+ if (!fs7.existsSync(addonsDir)) return dirs;
2926
+ for (const name of fs7.readdirSync(addonsDir)) {
2927
+ const full = path7.join(addonsDir, name);
1845
2928
  if (name.startsWith("@") && isDir(full)) {
1846
- for (const sub of fs6.readdirSync(full)) {
1847
- const subFull = path6.join(full, sub);
1848
- if (isDir(subFull) && fs6.existsSync(path6.join(subFull, "package.json"))) {
2929
+ for (const sub of fs7.readdirSync(full)) {
2930
+ const subFull = path7.join(full, sub);
2931
+ if (isDir(subFull) && fs7.existsSync(path7.join(subFull, "package.json"))) {
1849
2932
  dirs.push(subFull);
1850
2933
  }
1851
2934
  }
1852
- } else if (isDir(full) && fs6.existsSync(path6.join(full, "package.json"))) {
2935
+ } else if (isDir(full) && fs7.existsSync(path7.join(full, "package.json"))) {
1853
2936
  dirs.push(full);
1854
2937
  }
1855
2938
  }
1856
2939
  return dirs;
1857
2940
  }
1858
2941
  function buildAgentChildUdsManifest(nodeId, childId, caps) {
1859
- const systemCapNames = /* @__PURE__ */ new Set();
2942
+ const capsByAddon = /* @__PURE__ */ new Map();
1860
2943
  for (const cap of caps) {
1861
- if (cap.deviceId === void 0) {
1862
- systemCapNames.add(cap.capName);
1863
- }
2944
+ if (cap.deviceId !== void 0) continue;
2945
+ const addonId = cap.addonId ?? childId;
2946
+ const set = capsByAddon.get(addonId) ?? /* @__PURE__ */ new Set();
2947
+ set.add(cap.capName);
2948
+ capsByAddon.set(addonId, set);
1864
2949
  }
1865
- const addons = [
1866
- { addonId: childId, capabilities: [...systemCapNames] }
1867
- ];
2950
+ if (capsByAddon.size === 0) {
2951
+ capsByAddon.set(childId, /* @__PURE__ */ new Set());
2952
+ }
2953
+ const addons = [...capsByAddon.entries()].map(
2954
+ ([addonId, capNames]) => ({ addonId, capabilities: [...capNames] })
2955
+ );
1868
2956
  return { nodeId, addons };
1869
2957
  }
1870
2958
  var scriptName = process.argv[1] ?? "";