camstack 1.2.83 → 1.2.84

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.
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
23633
23633
  }
23634
23634
  });
23635
23635
 
23636
- // ../system/dist/dist-C7qWwrK1.js
23637
- var require_dist_C7qWwrK1 = __commonJS({
23638
- "../system/dist/dist-C7qWwrK1.js"(exports) {
23636
+ // ../system/dist/dist-TEwQZL3g.js
23637
+ var require_dist_TEwQZL3g = __commonJS({
23638
+ "../system/dist/dist-TEwQZL3g.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -28488,6 +28488,68 @@ var require_dist_C7qWwrK1 = __commonJS({
28488
28488
  fileCount: zod.z.number(),
28489
28489
  present: zod.z.boolean()
28490
28490
  });
28491
+ var BackupRunStateSchema = zod.z.enum([
28492
+ "queued",
28493
+ "running",
28494
+ "succeeded",
28495
+ "failed",
28496
+ "cancelled"
28497
+ ]);
28498
+ var BackupRunPhaseSchema = zod.z.enum([
28499
+ "queued",
28500
+ "building",
28501
+ "uploading",
28502
+ "done"
28503
+ ]);
28504
+ var BackupRunSchema = zod.z.object({
28505
+ /** Stable run id — the handle `backup.cancel` takes. */
28506
+ id: zod.z.string(),
28507
+ state: BackupRunStateSchema,
28508
+ phase: BackupRunPhaseSchema,
28509
+ /**
28510
+ * Resolved destination location ids. Empty while queued (targets are
28511
+ * resolved when the run starts, against the then-current policies).
28512
+ */
28513
+ destinationIds: zod.z.array(zod.z.string()).readonly(),
28514
+ label: zod.z.string().optional(),
28515
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
28516
+ requestedAt: zod.z.number(),
28517
+ /** ms-epoch when the run left the queue and started building. */
28518
+ startedAt: zod.z.number().optional(),
28519
+ /** ms-epoch when the run reached a terminal state. */
28520
+ finishedAt: zod.z.number().optional(),
28521
+ /** Compressed bytes of the staging archive written so far. */
28522
+ stagedBytes: zod.z.number(),
28523
+ /** Final staged archive size, once the build phase completes. */
28524
+ archiveSizeBytes: zod.z.number().optional(),
28525
+ /** Bytes pushed to the destination currently uploading. */
28526
+ uploadedBytes: zod.z.number(),
28527
+ /** Destinations where the archive fully landed (uploaded + indexed). */
28528
+ completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
28529
+ /** Destinations that failed during the fan-out. */
28530
+ failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
28531
+ /** Failure message when `state === 'failed'`. */
28532
+ error: zod.z.string().optional(),
28533
+ /**
28534
+ * 1-based place in the execution queue — 1 = runs next. Present only
28535
+ * while `state === 'queued'`. Stamped by the orchestrator from the
28536
+ * queue's OWN pending order, never derived from timestamps, so the
28537
+ * UI cannot show an order the executor will not honour.
28538
+ */
28539
+ queuePosition: zod.z.number().int().min(1).optional()
28540
+ });
28541
+ var BackupTriggerResultSchema = zod.z.object({
28542
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
28543
+ runId: zod.z.string(),
28544
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
28545
+ queued: zod.z.boolean(),
28546
+ /** True when this trigger was coalesced onto an identical already-queued run. */
28547
+ joined: zod.z.boolean(),
28548
+ /** True when the run was cancelled before completing every destination. */
28549
+ cancelled: zod.z.boolean(),
28550
+ /** One entry per destination the archive landed at (partial on cancel). */
28551
+ entries: zod.z.array(BackupEntrySchema).readonly()
28552
+ });
28491
28553
  var BackupScheduleSchema = zod.z.object({
28492
28554
  /** Stable id. Generated by the orchestrator on first upsert if absent. */
28493
28555
  id: zod.z.string(),
@@ -28524,6 +28586,11 @@ var require_dist_C7qWwrK1 = __commonJS({
28524
28586
  * Trigger a backup. Without `destinations` the orchestrator fans
28525
28587
  * out to every destination flagged as enabled in the routing
28526
28588
  * config; with it, only the listed addons receive the archive.
28589
+ *
28590
+ * At most ONE backup run executes at a time — the source tree and
28591
+ * the staging disk are shared by every run, so a second trigger
28592
+ * while one is in flight is enqueued (or joined onto an identical
28593
+ * queued run) and the result says so. See D342.
28527
28594
  */
28528
28595
  trigger: method(zod.z.object({
28529
28596
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -28537,7 +28604,30 @@ var require_dist_C7qWwrK1 = __commonJS({
28537
28604
  * retention (manual runs).
28538
28605
  */
28539
28606
  retentionCount: zod.z.number().int().min(1).max(1e3).optional()
28540
- }).optional(), zod.z.array(BackupEntrySchema).readonly(), {
28607
+ }).optional(), BackupTriggerResultSchema, {
28608
+ kind: "mutation",
28609
+ auth: "admin"
28610
+ }),
28611
+ /**
28612
+ * Every run the orchestrator knows about, in EXECUTION order: the
28613
+ * running run first, then queued runs in the exact order they will
28614
+ * execute (each with `queuePosition`, 1 = next), then the bounded
28615
+ * finished history newest-first. Only one run executes at a time
28616
+ * (D342) — the queued section IS the line. Each run carries live
28617
+ * phase + byte counters so a runaway build is visible in seconds,
28618
+ * not via `du`.
28619
+ */
28620
+ listRuns: method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
28621
+ /**
28622
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
28623
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
28624
+ * already terminal. A QUEUED run is removed before it ever starts;
28625
+ * the RUNNING run has its tar/upload stream actually aborted, the
28626
+ * half-written staging archive is deleted, and the in-flight
28627
+ * destination upload is aborted server-side (partial discarded).
28628
+ * Destinations that already completed keep their archive.
28629
+ */
28630
+ cancel: method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
28541
28631
  kind: "mutation",
28542
28632
  auth: "admin"
28543
28633
  }),
@@ -29277,6 +29367,38 @@ var require_dist_C7qWwrK1 = __commonJS({
29277
29367
  auth: "admin"
29278
29368
  }),
29279
29369
  /**
29370
+ * The HARDWARE behind a device number was replaced
29371
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
29372
+ * described the old box, so the next catalog pull derives everything from
29373
+ * the camera that is actually there:
29374
+ *
29375
+ * - every `derived:*` stream definition — a derived is authored against a
29376
+ * specific profile layout, and against the wrong hardware its feeder
29377
+ * respawns forever (observed at attempt 2732 on the live hub,
29378
+ * 2026-09-01);
29379
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
29380
+ * marks the slot manual, which would pin the stale choice instead of
29381
+ * letting `computeInitialAssignment` re-derive it);
29382
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
29383
+ * old hardware);
29384
+ * - the persisted RTSP token rows for the device's brokers (keyed
29385
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
29386
+ * so the rows are dead URLs).
29387
+ *
29388
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
29389
+ * and a dropped forget leaves a feeder respawning against a stream that
29390
+ * does not exist.
29391
+ */
29392
+ forgetDeviceHardware: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
29393
+ derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
29394
+ assignmentsPurged: zod.z.boolean(),
29395
+ probeSnapshotsDropped: zod.z.number().int().nonnegative(),
29396
+ rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
29397
+ }), {
29398
+ kind: "mutation",
29399
+ auth: "admin"
29400
+ }),
29401
+ /**
29280
29402
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
29281
29403
  *
29282
29404
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -51132,6 +51254,12 @@ var require_dist_C7qWwrK1 = __commonJS({
51132
51254
  addonId: null,
51133
51255
  access: "create"
51134
51256
  },
51257
+ "backup.cancel": {
51258
+ capName: "backup",
51259
+ capScope: "system",
51260
+ addonId: null,
51261
+ access: "create"
51262
+ },
51135
51263
  "backup.delete": {
51136
51264
  capName: "backup",
51137
51265
  capScope: "system",
@@ -51174,6 +51302,12 @@ var require_dist_C7qWwrK1 = __commonJS({
51174
51302
  addonId: null,
51175
51303
  access: "view"
51176
51304
  },
51305
+ "backup.listRuns": {
51306
+ capName: "backup",
51307
+ capScope: "system",
51308
+ addonId: null,
51309
+ access: "view"
51310
+ },
51177
51311
  "backup.listSchedules": {
51178
51312
  capName: "backup",
51179
51313
  capScope: "system",
@@ -55740,6 +55874,12 @@ var require_dist_C7qWwrK1 = __commonJS({
55740
55874
  addonId: null,
55741
55875
  access: "create"
55742
55876
  },
55877
+ "streamBroker.forgetDeviceHardware": {
55878
+ capName: "stream-broker",
55879
+ capScope: "system",
55880
+ addonId: null,
55881
+ access: "delete"
55882
+ },
55743
55883
  "streamBroker.getAllRtspEntries": {
55744
55884
  capName: "stream-broker",
55745
55885
  capScope: "system",
@@ -58198,6 +58338,11 @@ var require_dist_C7qWwrK1 = __commonJS({
58198
58338
  form: "single",
58199
58339
  optional: false
58200
58340
  }],
58341
+ "streamBroker.forgetDeviceHardware": [{
58342
+ name: "deviceId",
58343
+ form: "single",
58344
+ optional: false
58345
+ }],
58201
58346
  "streamBroker.getDeviceAudioMute": [{
58202
58347
  name: "deviceId",
58203
58348
  form: "single",
@@ -59854,7 +59999,7 @@ var require_alerts_addon = __commonJS({
59854
59999
  [Symbol.toStringTag]: { value: "Module" }
59855
60000
  });
59856
60001
  require_chunk_Cek0wNdY();
59857
- var require_dist10 = require_dist_C7qWwrK1();
60002
+ var require_dist10 = require_dist_TEwQZL3g();
59858
60003
  function selectExpired(alerts, cutoffMs) {
59859
60004
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
59860
60005
  }
@@ -60673,7 +60818,7 @@ var require_console_logging = __commonJS({
60673
60818
  [Symbol.toStringTag]: { value: "Module" }
60674
60819
  });
60675
60820
  require_chunk_Cek0wNdY();
60676
- var require_dist10 = require_dist_C7qWwrK1();
60821
+ var require_dist10 = require_dist_TEwQZL3g();
60677
60822
  var require_formatter = require_formatter_DqAKDlvN();
60678
60823
  var LEVEL_RANK = {
60679
60824
  debug: 0,
@@ -60767,7 +60912,7 @@ var require_core_blocks_addon = __commonJS({
60767
60912
  "use strict";
60768
60913
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
60769
60914
  var require_chunk = require_chunk_Cek0wNdY();
60770
- var require_dist10 = require_dist_C7qWwrK1();
60915
+ var require_dist10 = require_dist_TEwQZL3g();
60771
60916
  var node_crypto = __require("crypto");
60772
60917
  var node_fs_promises = __require("fs/promises");
60773
60918
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -61664,11 +61809,11 @@ var require_core_blocks = __commonJS({
61664
61809
  }
61665
61810
  });
61666
61811
 
61667
- // ../system/dist/retired-settings-keys-3soU9pfP.js
61668
- var require_retired_settings_keys_3soU9pfP = __commonJS({
61669
- "../system/dist/retired-settings-keys-3soU9pfP.js"(exports) {
61812
+ // ../system/dist/retired-settings-keys-DauPtbFR.js
61813
+ var require_retired_settings_keys_DauPtbFR = __commonJS({
61814
+ "../system/dist/retired-settings-keys-DauPtbFR.js"(exports) {
61670
61815
  "use strict";
61671
- var require_dist10 = require_dist_C7qWwrK1();
61816
+ var require_dist10 = require_dist_TEwQZL3g();
61672
61817
  function settingsStoreIsAuthoritativeHere(env) {
61673
61818
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
61674
61819
  return raw === "" || raw === "hub";
@@ -63882,8 +64027,8 @@ var require_device_manager_addon = __commonJS({
63882
64027
  [Symbol.toStringTag]: { value: "Module" }
63883
64028
  });
63884
64029
  require_chunk_Cek0wNdY();
63885
- var require_dist10 = require_dist_C7qWwrK1();
63886
- var require_retired_settings_keys = require_retired_settings_keys_3soU9pfP();
64030
+ var require_dist10 = require_dist_TEwQZL3g();
64031
+ var require_retired_settings_keys = require_retired_settings_keys_DauPtbFR();
63887
64032
  var node_crypto = __require("crypto");
63888
64033
  var _camstack_types_node = require_node();
63889
64034
  var JOB_HISTORY = 20;
@@ -65546,7 +65691,13 @@ var require_device_manager_addon = __commonJS({
65546
65691
  if (config.mode === "auto") {
65547
65692
  if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
65548
65693
  if (c.type === require_dist10.DeviceType.Container) continue;
65549
- if (c.location === selfLocation) ids.add(c.id);
65694
+ if (c.location !== selfLocation) continue;
65695
+ const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
65696
+ if (parent !== void 0 && parent.type === require_dist10.DeviceType.Container) {
65697
+ expand(parent.id);
65698
+ continue;
65699
+ }
65700
+ ids.add(c.id);
65550
65701
  }
65551
65702
  } else for (const id of config.manualIds) if (byId.has(id)) expand(id);
65552
65703
  ids.delete(selfId);
@@ -66409,6 +66560,16 @@ var require_device_manager_addon = __commonJS({
66409
66560
  const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
66410
66561
  const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
66411
66562
  await deps.swapIds(sourceId, targetId);
66563
+ await deps.swapHardwareState(sourceId, targetId);
66564
+ for (const deviceId of [sourceId, targetId]) try {
66565
+ await deps.forgetStreamState(deviceId);
66566
+ } catch (err) {
66567
+ deps.log("migration: broker hardware state NOT forgotten \u2014 stale stream state survives", {
66568
+ deviceId,
66569
+ error: err instanceof Error ? err.message : String(err),
66570
+ remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
66571
+ });
66572
+ }
66412
66573
  const up = await applyAll(deps, sourceId, true);
66413
66574
  await deps.respawnRunners([sourceId, targetId]);
66414
66575
  deps.log("camera migrated", {
@@ -66425,6 +66586,49 @@ var require_device_manager_addon = __commonJS({
66425
66586
  swapped: true
66426
66587
  };
66427
66588
  }
66589
+ var PROFILE_MAP_CONFIG_KEY = "_profileMap";
66590
+ var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
66591
+ function splitConfig(config) {
66592
+ const hardware = {};
66593
+ const role = {};
66594
+ for (const [key, value] of Object.entries(config)) {
66595
+ if (key === PROFILE_MAP_CONFIG_KEY) continue;
66596
+ if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
66597
+ else hardware[key] = value;
66598
+ }
66599
+ return {
66600
+ hardware,
66601
+ role
66602
+ };
66603
+ }
66604
+ async function swapHardwareState(settings, sourceId, targetId) {
66605
+ const sourceConfig = await settings.readDeviceStore(sourceId);
66606
+ const targetConfig = await settings.readDeviceStore(targetId);
66607
+ const source = splitConfig(sourceConfig);
66608
+ const target = splitConfig(targetConfig);
66609
+ await settings.clearDeviceStore(sourceId);
66610
+ await settings.writeDeviceStore(sourceId, {
66611
+ ...target.hardware,
66612
+ ...source.role
66613
+ });
66614
+ await settings.clearDeviceStore(targetId);
66615
+ await settings.writeDeviceStore(targetId, {
66616
+ ...source.hardware,
66617
+ ...target.role
66618
+ });
66619
+ const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
66620
+ const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
66621
+ await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
66622
+ await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
66623
+ return {
66624
+ runtimeStateNowAtSource: targetRuntimeState,
66625
+ runtimeStateNowAtTarget: sourceRuntimeState,
66626
+ hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
66627
+ hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
66628
+ roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
66629
+ roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
66630
+ };
66631
+ }
66428
66632
  async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
66429
66633
  await metaStore.withMetaWriteLock(async () => {
66430
66634
  if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
@@ -66484,13 +66688,42 @@ var require_device_manager_addon = __commonJS({
66484
66688
  swapIds: async (a, b) => {
66485
66689
  await pctx.metaStore.rows.swapIds(a, b, tempId);
66486
66690
  },
66691
+ swapHardwareState: async (a, b) => {
66692
+ const swapped = await swapHardwareState(pctx.settings, a, b);
66693
+ pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
66694
+ pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
66695
+ ctx.logger.info("migration: hardware-scoped device state swapped", {
66696
+ tags: { deviceId: a },
66697
+ meta: {
66698
+ sourceId: a,
66699
+ targetId: b,
66700
+ hardwareKeysToSource: swapped.hardwareKeysToSource,
66701
+ hardwareKeysToTarget: swapped.hardwareKeysToTarget,
66702
+ roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
66703
+ roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
66704
+ }
66705
+ });
66706
+ },
66707
+ forgetStreamState: async (deviceId) => {
66708
+ await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
66709
+ },
66487
66710
  respawnRunners: async (deviceIds) => {
66488
66711
  const owners = /* @__PURE__ */ new Set();
66489
66712
  for (const deviceId of deviceIds) {
66490
66713
  const addonId = pctx.metaStore.idToAddonId.get(deviceId);
66491
66714
  if (addonId !== void 0) owners.add(addonId);
66492
66715
  }
66493
- for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
66716
+ for (const addonId of owners) {
66717
+ const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
66718
+ ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
66719
+ tags: { deviceId: sourceId },
66720
+ meta: {
66721
+ addonId,
66722
+ devicesTakenDown: owned
66723
+ }
66724
+ });
66725
+ await ctx.api.addons.restartAddon.mutate({ addonId });
66726
+ }
66494
66727
  },
66495
66728
  log: (message, meta) => {
66496
66729
  ctx.logger.info(message, {
@@ -68923,7 +69156,7 @@ var require_hub_forwarder = __commonJS({
68923
69156
  [Symbol.toStringTag]: { value: "Module" }
68924
69157
  });
68925
69158
  require_chunk_Cek0wNdY();
68926
- var require_dist10 = require_dist_C7qWwrK1();
69159
+ var require_dist10 = require_dist_TEwQZL3g();
68927
69160
  var require_formatter = require_formatter_DqAKDlvN();
68928
69161
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
68929
69162
  var HubForwarderDestination = class {
@@ -69060,7 +69293,7 @@ var require_liveness_monitor_addon = __commonJS({
69060
69293
  "use strict";
69061
69294
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
69062
69295
  require_chunk_Cek0wNdY();
69063
- var require_dist10 = require_dist_C7qWwrK1();
69296
+ var require_dist10 = require_dist_TEwQZL3g();
69064
69297
  var NO_DEVICES = "liveness:no-devices";
69065
69298
  var ALL_OFFLINE = "liveness:all-devices-offline";
69066
69299
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -69250,7 +69483,7 @@ var require_local_auth_addon = __commonJS({
69250
69483
  [Symbol.toStringTag]: { value: "Module" }
69251
69484
  });
69252
69485
  var require_chunk = require_chunk_Cek0wNdY();
69253
- var require_dist10 = require_dist_C7qWwrK1();
69486
+ var require_dist10 = require_dist_TEwQZL3g();
69254
69487
  var node_crypto = __require("crypto");
69255
69488
  node_crypto = require_chunk.__toESM(node_crypto);
69256
69489
  var crypto$1 = __require("crypto");
@@ -77063,7 +77296,7 @@ var require_loki_logging = __commonJS({
77063
77296
  [Symbol.toStringTag]: { value: "Module" }
77064
77297
  });
77065
77298
  require_chunk_Cek0wNdY();
77066
- var require_dist10 = require_dist_C7qWwrK1();
77299
+ var require_dist10 = require_dist_TEwQZL3g();
77067
77300
  function sanitizeLabelName(raw) {
77068
77301
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
77069
77302
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -77628,7 +77861,7 @@ var require_native_metrics_addon = __commonJS({
77628
77861
  [Symbol.toStringTag]: { value: "Module" }
77629
77862
  });
77630
77863
  var require_chunk = require_chunk_Cek0wNdY();
77631
- var require_dist10 = require_dist_C7qWwrK1();
77864
+ var require_dist10 = require_dist_TEwQZL3g();
77632
77865
  var node_fs_promises = __require("fs/promises");
77633
77866
  var node_child_process = __require("child_process");
77634
77867
  var node_util = __require("util");
@@ -80212,7 +80445,7 @@ var require_filesystem_storage_addon = __commonJS({
80212
80445
  [Symbol.toStringTag]: { value: "Module" }
80213
80446
  });
80214
80447
  var require_chunk = require_chunk_Cek0wNdY();
80215
- var require_dist10 = require_dist_C7qWwrK1();
80448
+ var require_dist10 = require_dist_TEwQZL3g();
80216
80449
  var node_crypto = __require("crypto");
80217
80450
  var node_fs_promises = __require("fs/promises");
80218
80451
  var node_path = __require("path");
@@ -81328,8 +81561,8 @@ var require_sqlite_settings_addon = __commonJS({
81328
81561
  [Symbol.toStringTag]: { value: "Module" }
81329
81562
  });
81330
81563
  var require_chunk = require_chunk_Cek0wNdY();
81331
- var require_dist10 = require_dist_C7qWwrK1();
81332
- var require_retired_settings_keys = require_retired_settings_keys_3soU9pfP();
81564
+ var require_dist10 = require_dist_TEwQZL3g();
81565
+ var require_retired_settings_keys = require_retired_settings_keys_DauPtbFR();
81333
81566
  var node_crypto = __require("crypto");
81334
81567
  var node_fs = __require("fs");
81335
81568
  var node_module = __require("module");
@@ -83819,7 +84052,7 @@ var require_storage_orchestrator_addon = __commonJS({
83819
84052
  [Symbol.toStringTag]: { value: "Module" }
83820
84053
  });
83821
84054
  var require_chunk = require_chunk_Cek0wNdY();
83822
- var require_dist10 = require_dist_C7qWwrK1();
84055
+ var require_dist10 = require_dist_TEwQZL3g();
83823
84056
  var require_hub_cap_forward = require_hub_cap_forward_DmHNjbB0();
83824
84057
  var zod = require_zod();
83825
84058
  var node_crypto = __require("crypto");
@@ -86770,7 +87003,7 @@ var require_system_config_addon = __commonJS({
86770
87003
  [Symbol.toStringTag]: { value: "Module" }
86771
87004
  });
86772
87005
  require_chunk_Cek0wNdY();
86773
- var require_dist10 = require_dist_C7qWwrK1();
87006
+ var require_dist10 = require_dist_TEwQZL3g();
86774
87007
  var SECTION_TITLES = {
86775
87008
  server: "Server",
86776
87009
  auth: "Authentication"
@@ -104831,7 +105064,7 @@ var require_winston_logging = __commonJS({
104831
105064
  [Symbol.toStringTag]: { value: "Module" }
104832
105065
  });
104833
105066
  var require_chunk = require_chunk_Cek0wNdY();
104834
- var require_dist10 = require_dist_C7qWwrK1();
105067
+ var require_dist10 = require_dist_TEwQZL3g();
104835
105068
  var require_formatter = require_formatter_DqAKDlvN();
104836
105069
  var node_path = __require("path");
104837
105070
  node_path = require_chunk.__toESM(node_path);
@@ -106774,9 +107007,9 @@ var require_event_category_BaEgqJNv = __commonJS({
106774
107007
  }
106775
107008
  });
106776
107009
 
106777
- // ../types/dist/sleep-BwGJ_wL_.js
106778
- var require_sleep_BwGJ_wL = __commonJS({
106779
- "../types/dist/sleep-BwGJ_wL_.js"(exports) {
107010
+ // ../types/dist/sleep-BDmIj1HV.js
107011
+ var require_sleep_BDmIj1HV = __commonJS({
107012
+ "../types/dist/sleep-BDmIj1HV.js"(exports) {
106780
107013
  "use strict";
106781
107014
  var require_event_category = require_event_category_BaEgqJNv();
106782
107015
  var zod = require_zod();
@@ -109784,6 +110017,7 @@ var require_sleep_BwGJ_wL = __commonJS({
109784
110017
  retractCameraStream: (input) => dispatchSystem("streamBroker", "retractCameraStream", "mutation", input),
109785
110018
  assignProfile: (input) => dispatchSystem("streamBroker", "assignProfile", "mutation", input),
109786
110019
  unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
110020
+ forgetDeviceHardware: (input) => dispatchSystem("streamBroker", "forgetDeviceHardware", "mutation", input),
109787
110021
  renderPreBufferClip: (input) => dispatchSystem("streamBroker", "renderPreBufferClip", "mutation", input),
109788
110022
  produceEventMedia: (input) => dispatchSystem("streamBroker", "produceEventMedia", "mutation", input),
109789
110023
  restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
@@ -110448,7 +110682,7 @@ var require_addon = __commonJS({
110448
110682
  "use strict";
110449
110683
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
110450
110684
  var require_event_category = require_event_category_BaEgqJNv();
110451
- var require_sleep = require_sleep_BwGJ_wL();
110685
+ var require_sleep = require_sleep_BDmIj1HV();
110452
110686
  var require_err_msg = require_err_msg_COpsHMw2();
110453
110687
  var CAP_INPUT_DEFAULTS = Object.freeze({
110454
110688
  "addons": { "getLogs": { "limit": 100 } },
@@ -117331,12 +117565,12 @@ var require_dist2 = __commonJS({
117331
117565
  }
117332
117566
  });
117333
117567
 
117334
- // ../system/dist/manifest-system-deps-PAIfmNun.js
117335
- var require_manifest_system_deps_PAIfmNun = __commonJS({
117336
- "../system/dist/manifest-system-deps-PAIfmNun.js"(exports) {
117568
+ // ../system/dist/manifest-system-deps-BVIr3MYJ.js
117569
+ var require_manifest_system_deps_BVIr3MYJ = __commonJS({
117570
+ "../system/dist/manifest-system-deps-BVIr3MYJ.js"(exports) {
117337
117571
  "use strict";
117338
117572
  var require_chunk = require_chunk_Cek0wNdY();
117339
- require_dist_C7qWwrK1();
117573
+ require_dist_TEwQZL3g();
117340
117574
  require_hub_cap_forward_DmHNjbB0();
117341
117575
  var node_crypto = __require("crypto");
117342
117576
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -129403,7 +129637,7 @@ var require_dist3 = __commonJS({
129403
129637
  "use strict";
129404
129638
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
129405
129639
  var require_chunk = require_chunk_Cek0wNdY();
129406
- var require_dist10 = require_dist_C7qWwrK1();
129640
+ var require_dist10 = require_dist_TEwQZL3g();
129407
129641
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
129408
129642
  require_alerts();
129409
129643
  var require_formatter = require_formatter_DqAKDlvN();
@@ -129430,7 +129664,7 @@ var require_dist3 = __commonJS({
129430
129664
  var require_builtins_winston_logging_index = require_winston_logging();
129431
129665
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
129432
129666
  var require_tls$1 = require_tls_BxQlomxd();
129433
- var require_manifest_system_deps = require_manifest_system_deps_PAIfmNun();
129667
+ var require_manifest_system_deps = require_manifest_system_deps_BVIr3MYJ();
129434
129668
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
129435
129669
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
129436
129670
  var zod = require_zod();
@@ -132468,7 +132702,265 @@ var require_dist3 = __commonJS({
132468
132702
  };
132469
132703
  }
132470
132704
  };
132705
+ function parseVersion(value) {
132706
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value.trim());
132707
+ if (!m) return null;
132708
+ return {
132709
+ major: Number(m[1]),
132710
+ minor: Number(m[2]),
132711
+ patch: Number(m[3]),
132712
+ prerelease: m[4] ?? null
132713
+ };
132714
+ }
132715
+ function compareTriple(a, b) {
132716
+ if (a.major !== b.major) return a.major < b.major ? -1 : 1;
132717
+ if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1;
132718
+ if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1;
132719
+ return 0;
132720
+ }
132721
+ function closureSatisfiesRange(range, version2) {
132722
+ const trimmedRange = range.trim();
132723
+ const v2 = parseVersion(version2);
132724
+ if (v2 === null) return false;
132725
+ if (v2.prerelease !== null) return trimmedRange === version2.trim() || trimmedRange === `=${version2.trim()}`;
132726
+ const exactBase = parseVersion(trimmedRange.startsWith("=") ? trimmedRange.slice(1) : trimmedRange);
132727
+ if (exactBase !== null) return exactBase.prerelease === null && compareTriple(v2, exactBase) === 0;
132728
+ const operator = trimmedRange[0];
132729
+ if (operator !== "^" && operator !== "~") return false;
132730
+ const base = parseVersion(trimmedRange.slice(1));
132731
+ if (base === null || base.prerelease !== null) return false;
132732
+ if (operator === "~") return v2.major === base.major && v2.minor === base.minor && compareTriple(v2, base) >= 0;
132733
+ if (base.major > 0) return v2.major === base.major && compareTriple(v2, base) >= 0;
132734
+ if (base.minor > 0) return v2.major === 0 && v2.minor === base.minor && compareTriple(v2, base) >= 0;
132735
+ return compareTriple(v2, base) === 0;
132736
+ }
132737
+ function sameCompatLine(a, b) {
132738
+ const pa = parseVersion(a);
132739
+ const pb = parseVersion(b);
132740
+ if (pa === null || pb === null) return false;
132741
+ if (pa.major !== pb.major) return false;
132742
+ if (pa.major === 0) return pa.minor === pb.minor;
132743
+ return true;
132744
+ }
132745
+ function planClosureDedupe(input) {
132746
+ const links = [];
132747
+ const keeps = [];
132748
+ for (const candidate of input.candidates) {
132749
+ if (candidate.name.startsWith("@camstack/")) {
132750
+ keeps.push({
132751
+ name: candidate.name,
132752
+ reason: "camstack-package"
132753
+ });
132754
+ continue;
132755
+ }
132756
+ if (input.neverLink.has(candidate.name)) {
132757
+ keeps.push({
132758
+ name: candidate.name,
132759
+ reason: "never-link"
132760
+ });
132761
+ continue;
132762
+ }
132763
+ const closureVersion = input.closureVersions.get(candidate.name);
132764
+ if (closureVersion === void 0) {
132765
+ keeps.push({
132766
+ name: candidate.name,
132767
+ reason: "not-in-closure"
132768
+ });
132769
+ continue;
132770
+ }
132771
+ if (closureVersion === candidate.version) {
132772
+ links.push({
132773
+ name: candidate.name,
132774
+ closureVersion
132775
+ });
132776
+ continue;
132777
+ }
132778
+ const ranges = input.requirements.get(candidate.name) ?? [];
132779
+ if (ranges.length > 0 && ranges.every((range) => closureSatisfiesRange(range, closureVersion)) && sameCompatLine(closureVersion, candidate.version)) links.push({
132780
+ name: candidate.name,
132781
+ closureVersion
132782
+ });
132783
+ else keeps.push({
132784
+ name: candidate.name,
132785
+ reason: "version-incompatible"
132786
+ });
132787
+ }
132788
+ return {
132789
+ links,
132790
+ keeps
132791
+ };
132792
+ }
132793
+ function readTreeManifest(pkgJsonPath) {
132794
+ let raw;
132795
+ try {
132796
+ raw = require_dist10.asJsonObject(require_dist10.parseJsonUnknown(node_fs.readFileSync(pkgJsonPath, "utf-8")));
132797
+ } catch {
132798
+ return null;
132799
+ }
132800
+ if (raw === null) return null;
132801
+ const ranges = /* @__PURE__ */ new Map();
132802
+ for (const key of [
132803
+ "dependencies",
132804
+ "optionalDependencies",
132805
+ "peerDependencies"
132806
+ ]) {
132807
+ const deps = require_dist10.asJsonObject(raw[key]);
132808
+ if (deps === null) continue;
132809
+ for (const [name2, range] of Object.entries(deps)) if (typeof range === "string" && !ranges.has(name2)) ranges.set(name2, range);
132810
+ }
132811
+ const version2 = require_dist10.asString(raw["version"]);
132812
+ return {
132813
+ version: version2.length > 0 ? version2 : null,
132814
+ ranges
132815
+ };
132816
+ }
132817
+ function listTopLevelEntries(nodeModulesDir) {
132818
+ let entries;
132819
+ try {
132820
+ entries = node_fs.readdirSync(nodeModulesDir, { withFileTypes: true });
132821
+ } catch {
132822
+ return [];
132823
+ }
132824
+ const names = [];
132825
+ for (const entry of entries) {
132826
+ if (entry.name.startsWith(".")) continue;
132827
+ if (entry.name.startsWith("@")) {
132828
+ let inner;
132829
+ try {
132830
+ inner = node_fs.readdirSync(node_path.join(nodeModulesDir, entry.name), { withFileTypes: true });
132831
+ } catch {
132832
+ continue;
132833
+ }
132834
+ for (const sub of inner) if (!sub.name.startsWith(".")) names.push(`${entry.name}/${sub.name}`);
132835
+ } else names.push(entry.name);
132836
+ }
132837
+ return names;
132838
+ }
132839
+ function collectAddonTreeState(addonDir) {
132840
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
132841
+ const requirements = /* @__PURE__ */ new Map();
132842
+ const addRange = (name2, range) => {
132843
+ const existing = requirements.get(name2);
132844
+ if (existing === void 0) requirements.set(name2, [range]);
132845
+ else existing.push(range);
132846
+ };
132847
+ const rootManifest = readTreeManifest(node_path.join(addonDir, "package.json"));
132848
+ if (rootManifest !== null) for (const [name2, range] of rootManifest.ranges) addRange(name2, range);
132849
+ const candidates = [];
132850
+ const existingLinks = [];
132851
+ for (const name2 of listTopLevelEntries(nodeModulesDir)) {
132852
+ const entryDir = node_path.join(nodeModulesDir, name2);
132853
+ let isLink = false;
132854
+ try {
132855
+ isLink = node_fs.lstatSync(entryDir).isSymbolicLink();
132856
+ } catch {
132857
+ continue;
132858
+ }
132859
+ if (isLink) {
132860
+ let target = "";
132861
+ try {
132862
+ target = node_fs.readlinkSync(entryDir);
132863
+ } catch {
132864
+ }
132865
+ existingLinks.push({
132866
+ name: name2,
132867
+ target
132868
+ });
132869
+ }
132870
+ const manifest = readTreeManifest(node_path.join(entryDir, "package.json"));
132871
+ if (manifest === null) continue;
132872
+ if (!isLink && manifest.version !== null) candidates.push({
132873
+ name: name2,
132874
+ version: manifest.version
132875
+ });
132876
+ for (const [depName, range] of manifest.ranges) {
132877
+ if (node_fs.existsSync(node_path.join(entryDir, "node_modules", depName))) continue;
132878
+ addRange(depName, range);
132879
+ }
132880
+ }
132881
+ return {
132882
+ candidates,
132883
+ requirements,
132884
+ existingLinks
132885
+ };
132886
+ }
132887
+ function readClosureVersions(closureNodeModulesDir, names) {
132888
+ const versions = /* @__PURE__ */ new Map();
132889
+ for (const name2 of names) {
132890
+ const manifest = readTreeManifest(node_path.join(closureNodeModulesDir, name2, "package.json"));
132891
+ if (manifest !== null && manifest.version !== null) versions.set(name2, manifest.version);
132892
+ }
132893
+ return versions;
132894
+ }
132895
+ function resolveClosureNodeModulesDir(explicitDir) {
132896
+ const frameworkDir = explicitDir ?? process.env["CAMSTACK_FRAMEWORK_DIR"];
132897
+ if (frameworkDir === void 0 || frameworkDir.length === 0) return null;
132898
+ const nodeModulesDir = explicitDir ?? node_path.join(frameworkDir, "node_modules");
132899
+ return node_fs.existsSync(nodeModulesDir) ? nodeModulesDir : null;
132900
+ }
132901
+ function symlinkType() {
132902
+ return process.platform === "win32" ? "junction" : "dir";
132903
+ }
132904
+ function executeClosureDedupe(addonDir, closureNodeModulesDir, plan, logger) {
132905
+ if (plan.links.length === 0) return [];
132906
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
132907
+ const probePath = node_path.join(nodeModulesDir, ".closure-link-probe");
132908
+ try {
132909
+ node_fs.mkdirSync(nodeModulesDir, { recursive: true });
132910
+ node_fs.rmSync(probePath, { force: true });
132911
+ node_fs.symlinkSync(closureNodeModulesDir, probePath, symlinkType());
132912
+ node_fs.rmSync(probePath, { force: true });
132913
+ } catch (err) {
132914
+ logger.warn("closure dedupe skipped \u2014 symlinks unavailable on this volume", { meta: {
132915
+ addonDir,
132916
+ error: require_dist10.errMsg(err)
132917
+ } });
132918
+ return [];
132919
+ }
132920
+ const linked = [];
132921
+ for (const link of plan.links) {
132922
+ const target = node_path.join(closureNodeModulesDir, link.name);
132923
+ const manifest = readTreeManifest(node_path.join(target, "package.json"));
132924
+ if (manifest === null || manifest.version !== link.closureVersion) {
132925
+ logger.warn("closure dedupe \u2014 closure changed under the plan; keeping own copy", { meta: {
132926
+ name: link.name,
132927
+ planned: link.closureVersion,
132928
+ found: manifest?.version ?? null
132929
+ } });
132930
+ continue;
132931
+ }
132932
+ const entryDir = node_path.join(nodeModulesDir, link.name);
132933
+ try {
132934
+ node_fs.rmSync(entryDir, {
132935
+ recursive: true,
132936
+ force: true
132937
+ });
132938
+ node_fs.mkdirSync(node_path.dirname(entryDir), { recursive: true });
132939
+ node_fs.symlinkSync(target, entryDir, symlinkType());
132940
+ linked.push(link.name);
132941
+ } catch (err) {
132942
+ logger.warn("closure dedupe \u2014 link failed; entry removed copy may need reinstall", { meta: {
132943
+ name: link.name,
132944
+ error: require_dist10.errMsg(err)
132945
+ } });
132946
+ }
132947
+ }
132948
+ return linked;
132949
+ }
132950
+ function isClosureLinkTarget(linkTarget, closureNodeModulesDir) {
132951
+ if (linkTarget.length === 0) return false;
132952
+ const normalized = node_path.resolve(linkTarget);
132953
+ const root = node_path.resolve(closureNodeModulesDir);
132954
+ return normalized === root || normalized.startsWith(`${root}${node_path.sep}`);
132955
+ }
132471
132956
  var execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
132957
+ function readNativeDependencyNames(pkg) {
132958
+ if (pkg === void 0) return /* @__PURE__ */ new Set();
132959
+ const camstack = require_dist10.asJsonObject(pkg["camstack"]);
132960
+ if (camstack === null) return /* @__PURE__ */ new Set();
132961
+ const native = require_dist10.asJsonObject(camstack["nativeDependencies"]);
132962
+ return native === null ? /* @__PURE__ */ new Set() : new Set(Object.keys(native));
132963
+ }
132472
132964
  function parseInstallSource(value) {
132473
132965
  switch (value) {
132474
132966
  case "npm":
@@ -132530,6 +133022,8 @@ var require_dist3 = __commonJS({
132530
133022
  installSource;
132531
133023
  logger;
132532
133024
  isPackageResolvable;
133025
+ /** Test seam — production resolves via `CAMSTACK_FRAMEWORK_DIR`. */
133026
+ closureNodeModulesDir;
132533
133027
  /**
132534
133028
  * Central manifest of installed addons. Tracks version + source +
132535
133029
  * timestamps + last-backup pointer per package, written atomically
@@ -132551,6 +133045,7 @@ var require_dist3 = __commonJS({
132551
133045
  this.logger = logger;
132552
133046
  this.manifest = new AddonManifest(config.addonsDir);
132553
133047
  this.isPackageResolvable = config.isPackageResolvable;
133048
+ this.closureNodeModulesDir = config.closureNodeModulesDir;
132554
133049
  if (config.installSource) this.installSource = config.installSource;
132555
133050
  else this.installSource = "npm";
132556
133051
  }
@@ -132738,6 +133233,140 @@ var require_dist3 = __commonJS({
132738
133233
  return changes;
132739
133234
  }
132740
133235
  /**
133236
+ * Make every INSTALLED addon's runtime dependencies resolvable — doing the
133237
+ * least work that achieves it. Runs at boot on hub and agent, which is what
133238
+ * turns a restore back into a working node: a system backup archives no
133239
+ * node_modules (D338), so before this pass every restored non-bootstrap
133240
+ * addon failed its first import until someone redeployed it by hand.
133241
+ *
133242
+ * Per addon, in order:
133243
+ * 1. drop closure links that no longer hold (dangling, or the closure
133244
+ * moved to a version the addon tree's recorded ranges refuse) — those
133245
+ * addons get a reinstall;
133246
+ * 2. a declared dep that already resolves in the addon tree costs nothing;
133247
+ * 3. a missing declared dep the closure provides at a COMPATIBLE version
133248
+ * becomes a link — no npm, no network;
133249
+ * 4. only an addon left with a genuinely unresolvable dep gets an npm
133250
+ * install (then dedupe + native ensure, same tail as a fresh install).
133251
+ *
133252
+ * "Only when necessary" is the contract: an addon whose deps are all
133253
+ * satisfied is not touched at all. Failures are per-addon and non-fatal —
133254
+ * a dep that cannot be re-fetched leaves that addon exactly as broken as
133255
+ * it already was, but LOUDLY, and never blocks the rest of the boot.
133256
+ */
133257
+ async ensureAddonRuntimeDeps() {
133258
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133259
+ const linked = [];
133260
+ const reinstalled = [];
133261
+ const failed = [];
133262
+ const installedPackages = this.listInstalled();
133263
+ for (const pkg of installedPackages) {
133264
+ try {
133265
+ if (node_fs.lstatSync(pkg.dir).isSymbolicLink()) continue;
133266
+ } catch {
133267
+ continue;
133268
+ }
133269
+ try {
133270
+ const outcome = this.ensureOneAddonDeps(pkg.dir, pkg.name, closureNodeModules);
133271
+ for (const dep of outcome.linked) linked.push(`${pkg.name}:${dep}`);
133272
+ if (outcome.needsInstall) {
133273
+ await this.reinstallAddonDeps(pkg.dir, pkg.name);
133274
+ reinstalled.push(pkg.name);
133275
+ }
133276
+ } catch (err) {
133277
+ failed.push(pkg.name);
133278
+ this.logger.error(`${pkg.name} \u2014 runtime dependencies could NOT be ensured; the addon will fail to load until its deps install`, { meta: {
133279
+ dir: pkg.dir,
133280
+ error: require_dist10.errMsg(err)
133281
+ } });
133282
+ }
133283
+ }
133284
+ return {
133285
+ checked: installedPackages.length,
133286
+ linked,
133287
+ reinstalled,
133288
+ failed
133289
+ };
133290
+ }
133291
+ /**
133292
+ * One addon's dependency audit (steps 1–3 of `ensureAddonRuntimeDeps`).
133293
+ * Pure decision + link creation; the npm fallback is the caller's.
133294
+ */
133295
+ ensureOneAddonDeps(addonDir, packageName, closureNodeModules) {
133296
+ const pkgView = readPackageJson(node_path.join(addonDir, "package.json"));
133297
+ const declared = pkgView === null ? null : require_dist10.asJsonObject(pkgView.raw["dependencies"]);
133298
+ const declaredEntries = declared === null ? [] : Object.entries(declared);
133299
+ const state = collectAddonTreeState(addonDir);
133300
+ const nodeModulesDir = node_path.join(addonDir, "node_modules");
133301
+ let needsInstall = false;
133302
+ if (closureNodeModules !== null) for (const link of state.existingLinks) {
133303
+ if (!isClosureLinkTarget(link.target, closureNodeModules)) continue;
133304
+ const linkDir = node_path.join(nodeModulesDir, link.name);
133305
+ const targetManifest = node_fs.existsSync(node_path.join(linkDir, "package.json"));
133306
+ const ranges = state.requirements.get(link.name) ?? [];
133307
+ let holds = targetManifest;
133308
+ if (holds && ranges.length > 0) {
133309
+ const version2 = readClosureVersions(closureNodeModules, [link.name]).get(link.name);
133310
+ holds = version2 !== void 0 && ranges.every((range) => closureSatisfiesRange(range, version2));
133311
+ }
133312
+ if (!holds) {
133313
+ this.logger.warn(`${packageName} \u2014 closure link for ${link.name} no longer holds; reinstalling this addon's deps`, { meta: { target: link.target } });
133314
+ node_fs.rmSync(linkDir, { force: true });
133315
+ needsInstall = true;
133316
+ }
133317
+ }
133318
+ const linked = [];
133319
+ for (const [depName, range] of declaredEntries) {
133320
+ if (typeof range !== "string") continue;
133321
+ if (node_fs.existsSync(node_path.join(nodeModulesDir, depName, "package.json"))) continue;
133322
+ const closureVersion = closureNodeModules === null ? void 0 : readClosureVersions(closureNodeModules, [depName]).get(depName);
133323
+ if (closureNodeModules !== null && closureVersion !== void 0 && closureSatisfiesRange(range, closureVersion)) {
133324
+ if (executeClosureDedupe(addonDir, closureNodeModules, {
133325
+ links: [{
133326
+ name: depName,
133327
+ closureVersion
133328
+ }],
133329
+ keeps: []
133330
+ }, this.logger).length === 1) {
133331
+ this.logger.info(`${packageName} \u2014 ${depName} linked from the server closure`, { meta: { closureVersion } });
133332
+ linked.push(depName);
133333
+ continue;
133334
+ }
133335
+ }
133336
+ needsInstall = true;
133337
+ }
133338
+ return {
133339
+ linked,
133340
+ needsInstall
133341
+ };
133342
+ }
133343
+ /**
133344
+ * npm-install an installed addon's runtime deps IN PLACE (the restore /
133345
+ * repair path — the tree is not yet running at boot), then the same tail
133346
+ * every fresh install gets: closure dedupe + native deps + prebuild ensure.
133347
+ * Closure links are stripped first so npm reifies against plain missing
133348
+ * dirs instead of symlinks it does not own.
133349
+ */
133350
+ async reinstallAddonDeps(addonDir, packageName) {
133351
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133352
+ if (closureNodeModules !== null) {
133353
+ const state = collectAddonTreeState(addonDir);
133354
+ for (const link of state.existingLinks) if (isClosureLinkTarget(link.target, closureNodeModules)) node_fs.rmSync(node_path.join(addonDir, "node_modules", link.name), { force: true });
133355
+ }
133356
+ this.logger.info(`${packageName} \u2014 reinstalling runtime dependencies`, { meta: { addonDir } });
133357
+ await require_manifest_system_deps.runNpm([
133358
+ "install",
133359
+ "--omit=dev",
133360
+ "--omit=peer",
133361
+ "--no-audit",
133362
+ "--no-fund",
133363
+ "--no-package-lock",
133364
+ ...this.registry ? ["--registry", this.registry] : []
133365
+ ], this.npmRunOptions(addonDir, 24e4));
133366
+ this.dedupeTreeAgainstClosure(addonDir, packageName);
133367
+ await this.installNativeDependencies(addonDir, packageName);
133368
+ }
133369
+ /**
132741
133370
  * Install a single addon — on-demand installer used by AddonPackageService
132742
133371
  * (admin UI "Install addon" / "Reinstall" buttons).
132743
133372
  *
@@ -132845,6 +133474,7 @@ var require_dist3 = __commonJS({
132845
133474
  logger: this.logger,
132846
133475
  bundledSourceDir: sourceDir
132847
133476
  });
133477
+ this.dedupeTreeAgainstClosure(targetDir, packageName);
132848
133478
  this.logger.info(`${packageName} \u2014 copied from ${sourceDir}`);
132849
133479
  return {
132850
133480
  name: pkgData.name,
@@ -133149,6 +133779,40 @@ var require_dist3 = __commonJS({
133149
133779
  logger: this.logger
133150
133780
  });
133151
133781
  }
133782
+ /**
133783
+ * Stop copying what the server closure already provides: replace each
133784
+ * top-level `node_modules` entry the closure serves at a COMPATIBLE version
133785
+ * with a symlink into the closure (see `deps/closure-dedupe.ts` for the
133786
+ * plan rules and the live measurements). Runs LAST in tree
133787
+ * materialisation — after the runtime-dep npm install AND the native-dep
133788
+ * steps — so no later npm invocation can fight the links.
133789
+ *
133790
+ * Non-fatal by design: dedupe is an optimisation, and its failure mode is
133791
+ * "the addon keeps its own copies", which is exactly today's behaviour. A
133792
+ * refusal on version grounds is logged loudly — that line is the answer to
133793
+ * "why does this addon still carry its own zod".
133794
+ */
133795
+ dedupeTreeAgainstClosure(pkgDir, packageName) {
133796
+ const closureNodeModules = resolveClosureNodeModulesDir(this.closureNodeModulesDir);
133797
+ if (closureNodeModules === null) return;
133798
+ try {
133799
+ const state = collectAddonTreeState(pkgDir);
133800
+ if (state.candidates.length === 0) return;
133801
+ const pkgView = readPackageJson(node_path.join(pkgDir, "package.json"));
133802
+ const plan = planClosureDedupe({
133803
+ candidates: state.candidates,
133804
+ closureVersions: readClosureVersions(closureNodeModules, state.candidates.map((c) => c.name)),
133805
+ requirements: state.requirements,
133806
+ neverLink: readNativeDependencyNames(pkgView?.raw)
133807
+ });
133808
+ const linked = executeClosureDedupe(pkgDir, closureNodeModules, plan, this.logger);
133809
+ if (linked.length > 0) this.logger.info(`${packageName} \u2014 ${linked.length} node_modules entr${linked.length === 1 ? "y" : "ies"} now resolve from the server closure`, { meta: { linked } });
133810
+ const refused = plan.keeps.filter((k2) => k2.reason === "version-incompatible");
133811
+ if (refused.length > 0) this.logger.info(`${packageName} \u2014 kept own copy where the closure version is incompatible`, { meta: { kept: refused.map((k2) => k2.name) } });
133812
+ } catch (err) {
133813
+ this.logger.warn(`${packageName} \u2014 closure dedupe failed (copies kept)`, { meta: { error: require_dist10.errMsg(err) } });
133814
+ }
133815
+ }
133152
133816
  /** Install addon from a tgz file (uploaded or downloaded) */
133153
133817
  async installFromTgz(tgzPath) {
133154
133818
  const stagingRoot = node_path.join(this.addonsDir, ".staging");
@@ -133174,6 +133838,7 @@ var require_dist3 = __commonJS({
133174
133838
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
133175
133839
  await this.installRuntimeDependencies(pkgDir, pkgView.name);
133176
133840
  await this.installNativeDependencies(pkgDir, pkgView.name);
133841
+ this.dedupeTreeAgainstClosure(pkgDir, pkgView.name);
133177
133842
  await this.evictInstallDir(targetDir);
133178
133843
  ensureDir(node_path.dirname(targetDir));
133179
133844
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -133306,6 +133971,7 @@ var require_dist3 = __commonJS({
133306
133971
  if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
133307
133972
  await this.installRuntimeDependencies(stagedPath, packageName);
133308
133973
  await this.installNativeDependencies(stagedPath, packageName);
133974
+ this.dedupeTreeAgainstClosure(stagedPath, packageName);
133309
133975
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
133310
133976
  const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
133311
133977
  ensureDir(backupRoot);
@@ -163521,7 +164187,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163521
164187
  }));
163522
164188
  var require_symlink_type2 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports2, module2) => {
163523
164189
  var fs2 = require_graceful_fs2();
163524
- function symlinkType(srcpath, type2, callback) {
164190
+ function symlinkType2(srcpath, type2, callback) {
163525
164191
  callback = typeof type2 === "function" ? type2 : callback;
163526
164192
  type2 = typeof type2 === "function" ? false : type2;
163527
164193
  if (type2) return callback(null, type2);
@@ -163542,7 +164208,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163542
164208
  return stats && stats.isDirectory() ? "dir" : "file";
163543
164209
  }
163544
164210
  module2.exports = {
163545
- symlinkType,
164211
+ symlinkType: symlinkType2,
163546
164212
  symlinkTypeSync
163547
164213
  };
163548
164214
  }));
@@ -163557,7 +164223,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163557
164223
  var symlinkPaths = _symlinkPaths.symlinkPaths;
163558
164224
  var symlinkPathsSync = _symlinkPaths.symlinkPathsSync;
163559
164225
  var _symlinkType = require_symlink_type2();
163560
- var symlinkType = _symlinkType.symlinkType;
164226
+ var symlinkType2 = _symlinkType.symlinkType;
163561
164227
  var symlinkTypeSync = _symlinkType.symlinkTypeSync;
163562
164228
  var pathExists = require_path_exists2().pathExists;
163563
164229
  function createSymlink(srcpath, dstpath, type2, callback) {
@@ -163569,7 +164235,7 @@ globstar while`, t, d, e, u, m), this.matchOne(t.slice(d), e.slice(u), s)) retur
163569
164235
  symlinkPaths(srcpath, dstpath, (err2, relative) => {
163570
164236
  if (err2) return callback(err2);
163571
164237
  srcpath = relative.toDst;
163572
- symlinkType(relative.toCwd, type2, (err3, type3) => {
164238
+ symlinkType2(relative.toCwd, type2, (err3, type3) => {
163573
164239
  if (err3) return callback(err3);
163574
164240
  const dir = path$23.dirname(dstpath);
163575
164241
  pathExists(dir, (err4, dirExists) => {
@@ -210088,7 +210754,7 @@ var require_dist4 = __commonJS({
210088
210754
  "use strict";
210089
210755
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
210090
210756
  var require_event_category = require_event_category_BaEgqJNv();
210091
- var require_sleep = require_sleep_BwGJ_wL();
210757
+ var require_sleep = require_sleep_BDmIj1HV();
210092
210758
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
210093
210759
  var require_enums2 = require_enums();
210094
210760
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -214698,6 +215364,68 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214698
215364
  fileCount: zod.z.number(),
214699
215365
  present: zod.z.boolean()
214700
215366
  });
215367
+ var BackupRunStateSchema = zod.z.enum([
215368
+ "queued",
215369
+ "running",
215370
+ "succeeded",
215371
+ "failed",
215372
+ "cancelled"
215373
+ ]);
215374
+ var BackupRunPhaseSchema = zod.z.enum([
215375
+ "queued",
215376
+ "building",
215377
+ "uploading",
215378
+ "done"
215379
+ ]);
215380
+ var BackupRunSchema = zod.z.object({
215381
+ /** Stable run id — the handle `backup.cancel` takes. */
215382
+ id: zod.z.string(),
215383
+ state: BackupRunStateSchema,
215384
+ phase: BackupRunPhaseSchema,
215385
+ /**
215386
+ * Resolved destination location ids. Empty while queued (targets are
215387
+ * resolved when the run starts, against the then-current policies).
215388
+ */
215389
+ destinationIds: zod.z.array(zod.z.string()).readonly(),
215390
+ label: zod.z.string().optional(),
215391
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
215392
+ requestedAt: zod.z.number(),
215393
+ /** ms-epoch when the run left the queue and started building. */
215394
+ startedAt: zod.z.number().optional(),
215395
+ /** ms-epoch when the run reached a terminal state. */
215396
+ finishedAt: zod.z.number().optional(),
215397
+ /** Compressed bytes of the staging archive written so far. */
215398
+ stagedBytes: zod.z.number(),
215399
+ /** Final staged archive size, once the build phase completes. */
215400
+ archiveSizeBytes: zod.z.number().optional(),
215401
+ /** Bytes pushed to the destination currently uploading. */
215402
+ uploadedBytes: zod.z.number(),
215403
+ /** Destinations where the archive fully landed (uploaded + indexed). */
215404
+ completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
215405
+ /** Destinations that failed during the fan-out. */
215406
+ failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
215407
+ /** Failure message when `state === 'failed'`. */
215408
+ error: zod.z.string().optional(),
215409
+ /**
215410
+ * 1-based place in the execution queue — 1 = runs next. Present only
215411
+ * while `state === 'queued'`. Stamped by the orchestrator from the
215412
+ * queue's OWN pending order, never derived from timestamps, so the
215413
+ * UI cannot show an order the executor will not honour.
215414
+ */
215415
+ queuePosition: zod.z.number().int().min(1).optional()
215416
+ });
215417
+ var BackupTriggerResultSchema = zod.z.object({
215418
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
215419
+ runId: zod.z.string(),
215420
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
215421
+ queued: zod.z.boolean(),
215422
+ /** True when this trigger was coalesced onto an identical already-queued run. */
215423
+ joined: zod.z.boolean(),
215424
+ /** True when the run was cancelled before completing every destination. */
215425
+ cancelled: zod.z.boolean(),
215426
+ /** One entry per destination the archive landed at (partial on cancel). */
215427
+ entries: zod.z.array(BackupEntrySchema).readonly()
215428
+ });
214701
215429
  var BackupScheduleSchema = zod.z.object({
214702
215430
  /** Stable id. Generated by the orchestrator on first upsert if absent. */
214703
215431
  id: zod.z.string(),
@@ -214734,6 +215462,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214734
215462
  * Trigger a backup. Without `destinations` the orchestrator fans
214735
215463
  * out to every destination flagged as enabled in the routing
214736
215464
  * config; with it, only the listed addons receive the archive.
215465
+ *
215466
+ * At most ONE backup run executes at a time — the source tree and
215467
+ * the staging disk are shared by every run, so a second trigger
215468
+ * while one is in flight is enqueued (or joined onto an identical
215469
+ * queued run) and the result says so. See D342.
214737
215470
  */
214738
215471
  trigger: require_sleep.method(zod.z.object({
214739
215472
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -214747,7 +215480,30 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
214747
215480
  * retention (manual runs).
214748
215481
  */
214749
215482
  retentionCount: zod.z.number().int().min(1).max(1e3).optional()
214750
- }).optional(), zod.z.array(BackupEntrySchema).readonly(), {
215483
+ }).optional(), BackupTriggerResultSchema, {
215484
+ kind: "mutation",
215485
+ auth: "admin"
215486
+ }),
215487
+ /**
215488
+ * Every run the orchestrator knows about, in EXECUTION order: the
215489
+ * running run first, then queued runs in the exact order they will
215490
+ * execute (each with `queuePosition`, 1 = next), then the bounded
215491
+ * finished history newest-first. Only one run executes at a time
215492
+ * (D342) — the queued section IS the line. Each run carries live
215493
+ * phase + byte counters so a runaway build is visible in seconds,
215494
+ * not via `du`.
215495
+ */
215496
+ listRuns: require_sleep.method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
215497
+ /**
215498
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
215499
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
215500
+ * already terminal. A QUEUED run is removed before it ever starts;
215501
+ * the RUNNING run has its tar/upload stream actually aborted, the
215502
+ * half-written staging archive is deleted, and the in-flight
215503
+ * destination upload is aborted server-side (partial discarded).
215504
+ * Destinations that already completed keep their archive.
215505
+ */
215506
+ cancel: require_sleep.method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
214751
215507
  kind: "mutation",
214752
215508
  auth: "admin"
214753
215509
  }),
@@ -215487,6 +216243,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
215487
216243
  auth: "admin"
215488
216244
  }),
215489
216245
  /**
216246
+ * The HARDWARE behind a device number was replaced
216247
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
216248
+ * described the old box, so the next catalog pull derives everything from
216249
+ * the camera that is actually there:
216250
+ *
216251
+ * - every `derived:*` stream definition — a derived is authored against a
216252
+ * specific profile layout, and against the wrong hardware its feeder
216253
+ * respawns forever (observed at attempt 2732 on the live hub,
216254
+ * 2026-09-01);
216255
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
216256
+ * marks the slot manual, which would pin the stale choice instead of
216257
+ * letting `computeInitialAssignment` re-derive it);
216258
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
216259
+ * old hardware);
216260
+ * - the persisted RTSP token rows for the device's brokers (keyed
216261
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
216262
+ * so the rows are dead URLs).
216263
+ *
216264
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
216265
+ * and a dropped forget leaves a feeder respawning against a stream that
216266
+ * does not exist.
216267
+ */
216268
+ forgetDeviceHardware: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
216269
+ derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
216270
+ assignmentsPurged: zod.z.boolean(),
216271
+ probeSnapshotsDropped: zod.z.number().int().nonnegative(),
216272
+ rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
216273
+ }), {
216274
+ kind: "mutation",
216275
+ auth: "admin"
216276
+ }),
216277
+ /**
215490
216278
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
215491
216279
  *
215492
216280
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -241376,6 +242164,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241376
242164
  addonId: null,
241377
242165
  access: "create"
241378
242166
  },
242167
+ "backup.cancel": {
242168
+ capName: "backup",
242169
+ capScope: "system",
242170
+ addonId: null,
242171
+ access: "create"
242172
+ },
241379
242173
  "backup.delete": {
241380
242174
  capName: "backup",
241381
242175
  capScope: "system",
@@ -241418,6 +242212,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241418
242212
  addonId: null,
241419
242213
  access: "view"
241420
242214
  },
242215
+ "backup.listRuns": {
242216
+ capName: "backup",
242217
+ capScope: "system",
242218
+ addonId: null,
242219
+ access: "view"
242220
+ },
241421
242221
  "backup.listSchedules": {
241422
242222
  capName: "backup",
241423
242223
  capScope: "system",
@@ -245984,6 +246784,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
245984
246784
  addonId: null,
245985
246785
  access: "create"
245986
246786
  },
246787
+ "streamBroker.forgetDeviceHardware": {
246788
+ capName: "stream-broker",
246789
+ capScope: "system",
246790
+ addonId: null,
246791
+ access: "delete"
246792
+ },
245987
246793
  "streamBroker.getAllRtspEntries": {
245988
246794
  capName: "stream-broker",
245989
246795
  capScope: "system",
@@ -248700,6 +249506,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
248700
249506
  form: "single",
248701
249507
  optional: false
248702
249508
  }],
249509
+ "streamBroker.forgetDeviceHardware": [{
249510
+ name: "deviceId",
249511
+ form: "single",
249512
+ optional: false
249513
+ }],
248703
249514
  "streamBroker.getDeviceAudioMute": [{
248704
249515
  name: "deviceId",
248705
249516
  form: "single",
@@ -249071,6 +249882,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249071
249882
  "recordingExport.listExports",
249072
249883
  "streamBroker.acquireEgressTranscode",
249073
249884
  "streamBroker.assignProfile",
249885
+ "streamBroker.forgetDeviceHardware",
249074
249886
  "streamBroker.getDeviceAudioMute",
249075
249887
  "streamBroker.getStreamWithCodec",
249076
249888
  "streamBroker.produceEventMedia",
@@ -249569,6 +250381,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249569
250381
  backup: {
249570
250382
  listDestinations: (input) => dispatch("backup", "listDestinations", "query", input),
249571
250383
  trigger: (input) => dispatch("backup", "trigger", "mutation", input),
250384
+ listRuns: (input) => dispatch("backup", "listRuns", "query", input),
250385
+ cancel: (input) => dispatch("backup", "cancel", "mutation", input),
249572
250386
  list: (input) => dispatch("backup", "list", "query", input),
249573
250387
  listLocations: (input) => dispatch("backup", "listLocations", "query", input),
249574
250388
  getEntries: (input) => dispatch("backup", "getEntries", "query", input),
@@ -252848,6 +253662,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252848
253662
  exports.BacklightModeSchema = BacklightModeSchema;
252849
253663
  exports.BackupDestinationInfoSchema = BackupDestinationInfoSchema;
252850
253664
  exports.BackupEntrySchema = BackupEntrySchema;
253665
+ exports.BackupRunPhaseSchema = BackupRunPhaseSchema;
253666
+ exports.BackupRunSchema = BackupRunSchema;
253667
+ exports.BackupRunStateSchema = BackupRunStateSchema;
253668
+ exports.BackupTriggerResultSchema = BackupTriggerResultSchema;
252851
253669
  exports.BaseAddon = require_sleep.BaseAddon;
252852
253670
  exports.BaseDevice = BaseDevice;
252853
253671
  exports.BaseDeviceProvider = BaseDeviceProvider;
@@ -387819,6 +388637,16 @@ var require_main2 = __commonJS({
387819
388637
  installSource: resolvedSource
387820
388638
  });
387821
388639
  await installer.ensureRequiredPackages(system_1.AddonInstaller.AGENT_PACKAGES);
388640
+ if (typeof installer.ensureAddonRuntimeDeps === "function") {
388641
+ try {
388642
+ const depsResult = await installer.ensureAddonRuntimeDeps();
388643
+ if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
388644
+ console.log(`[Agent] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
388645
+ }
388646
+ } catch (err) {
388647
+ console.warn(`[Agent] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
388648
+ }
388649
+ }
387822
388650
  console.log(`[Agent] Addon packages ready in ${config.addonsDir}`);
387823
388651
  let broker = (0, system_1.createBroker)({
387824
388652
  nodeID: config.nodeId,
@@ -427674,6 +428502,18 @@ var require_launcher = __commonJS({
427674
428502
  console.warn(`[launcher] first-boot addon plan failed: ${err instanceof Error ? err.message : String(err)}`);
427675
428503
  }
427676
428504
  await installer.ensureRequiredPackages(bootstrapRequired);
428505
+ if (typeof installer.ensureAddonRuntimeDeps === "function") {
428506
+ try {
428507
+ const depsResult = await installer.ensureAddonRuntimeDeps();
428508
+ if (depsResult.linked.length > 0 || depsResult.reinstalled.length > 0 || depsResult.failed.length > 0) {
428509
+ console.log(`[launcher] Addon runtime deps ensured \u2014 linked: [${depsResult.linked.join(", ")}], reinstalled: [${depsResult.reinstalled.join(", ")}], failed: [${depsResult.failed.join(", ")}] (${depsResult.checked} checked)`);
428510
+ }
428511
+ } catch (err) {
428512
+ console.warn(`[launcher] ensureAddonRuntimeDeps failed: ${err instanceof Error ? err.message : String(err)}`);
428513
+ }
428514
+ } else {
428515
+ console.warn("[launcher] ensureAddonRuntimeDeps unavailable \u2014 skipping addon dep repair");
428516
+ }
427677
428517
  if (typeof installer.reconcileManifest === "function") {
427678
428518
  const reconciled = installer.reconcileManifest();
427679
428519
  if (reconciled > 0) {