camstack 1.2.65 → 1.2.66

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-Ck2jkBZk.js
23637
- var require_dist_Ck2jkBZk = __commonJS({
23638
- "../system/dist/dist-Ck2jkBZk.js"(exports) {
23636
+ // ../system/dist/dist-CcvXUhHK.js
23637
+ var require_dist_CcvXUhHK = __commonJS({
23638
+ "../system/dist/dist-CcvXUhHK.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -26259,6 +26259,21 @@ var require_dist_Ck2jkBZk = __commonJS({
26259
26259
  bytesMoved: zod.z.number().int(),
26260
26260
  /** Total files discovered up front; null while (or when) unknown. */
26261
26261
  filesTotal: zod.z.number().int().nullable(),
26262
+ /**
26263
+ * Rows this run CORRECTED while moving them — a durable mutation the move
26264
+ * made that nobody asked for, so it is reported where the operator reads the
26265
+ * job rather than only in a log line.
26266
+ *
26267
+ * A footage segment records its byte count in its own NAME, and the durable
26268
+ * hour row derives its aggregates from those names. A file that does not
26269
+ * match its name therefore makes the ledger's sums — and with them quota and
26270
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
26271
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
26272
+ *
26273
+ * Absent on lanes where the question has no meaning: a media blob's size is
26274
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
26275
+ */
26276
+ rowsReconciled: zod.z.number().int().nonnegative().optional(),
26262
26277
  startedAt: zod.z.number(),
26263
26278
  finishedAt: zod.z.number().nullable(),
26264
26279
  error: zod.z.string().nullable()
@@ -26301,11 +26316,18 @@ var require_dist_Ck2jkBZk = __commonJS({
26301
26316
  /** Omitted = `move`, the pre-existing behaviour. */
26302
26317
  mode: MediaRelocateModeSchema.optional()
26303
26318
  });
26304
- var UnstampedEventMediaCountSchema = zod.z.object({
26305
- media: zod.z.number().int().nonnegative(),
26306
- retrainFrames: zod.z.number().int().nonnegative(),
26307
- total: zod.z.number().int().nonnegative()
26319
+ var UnstampedRowsSchema = zod.z.object({
26320
+ present: zod.z.boolean(),
26321
+ rows: zod.z.number().int().nonnegative().nullable()
26308
26322
  });
26323
+ var UnstampedEventMediaCountSchema = zod.z.object({
26324
+ media: UnstampedRowsSchema,
26325
+ retrainFrames: UnstampedRowsSchema,
26326
+ /** True when EITHER collection holds one. The refusal reads this. */
26327
+ anyPresent: zod.z.boolean(),
26328
+ /** Sum across both, or `null` when either lane could not be counted. */
26329
+ total: zod.z.number().int().nonnegative().nullable()
26330
+ }).nullable();
26309
26331
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: zod.z.string().min(1) });
26310
26332
  var StorageMigrationClassSchema = zod.z.enum([
26311
26333
  "recordings",
@@ -26352,6 +26374,10 @@ var require_dist_Ck2jkBZk = __commonJS({
26352
26374
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
26353
26375
  filesTotal: zod.z.number().int().nonnegative().nullable(),
26354
26376
  bytesMoved: zod.z.number().int().nonnegative(),
26377
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
26378
+ * a lane that cannot reconcile. A migration that silently rewrote durable
26379
+ * rows would be the same failure as one that silently skipped them. */
26380
+ rowsReconciled: zod.z.number().int().nonnegative().optional(),
26355
26381
  /** The MOVER's start, not the migration's: a drain restarted after an addon
26356
26382
  * crash gets a new mover, and a rate computed from the migration's start
26357
26383
  * would silently average in the time nothing was running. */
@@ -31507,6 +31533,15 @@ var require_dist_Ck2jkBZk = __commonJS({
31507
31533
  * calls are sync. Bindings change rarely (only on wrapper toggle or
31508
31534
  * device add/remove) — clients invalidate via the
31509
31535
  * `capability.binding-changed` event.
31536
+ *
31537
+ * "A single round-trip" describes the CLIENT's side and used not to
31538
+ * describe the server's: until 2026-08-30 the resolver read the persisted
31539
+ * wrapper activations once per device, so answering this cost one
31540
+ * settings-door RPC per device — 1 020 on the live 1 019-device hub, and
31541
+ * it did not return in 240 s against `SystemMirror.init`'s 15 s budget.
31542
+ * The server side is now two reads for the whole fleet. Anything PERIODIC
31543
+ * still belongs on `getBindings` / `getBindingsBatch` (D12); this remains
31544
+ * a warm seed.
31510
31545
  */
31511
31546
  getAllBindings: method(zod.z.object({}), zod.z.array(DeviceBindingsForDeviceSchema)),
31512
31547
  /**
@@ -37480,6 +37515,17 @@ var require_dist_Ck2jkBZk = __commonJS({
37480
37515
  * happens to stamp it. This count is what the migration planner's
37481
37516
  * non-blocking gate reads; `relocateMedia({ mode: 'seal' })` is what drives
37482
37517
  * it to zero.
37518
+ *
37519
+ * TWO indexed statements per collection, not a walk. It used to page the
37520
+ * whole collection at 200 rows per RPC ordered by an unindexed column, so
37521
+ * on the live hub — 1 254 576 rows — it hit the 60 s RPC deadline every
37522
+ * time it was called, and the migration it gates could never start. The
37523
+ * cheap question (`present`: is there at least one) is asked first and
37524
+ * separately from the expensive one (`rows`), because only the first has
37525
+ * to be answerable for the gate to do its job.
37526
+ *
37527
+ * **`null` is "not measurable", never zero** — at either level. An
37528
+ * unreadable collection must not read as a sealed one.
37483
37529
  */
37484
37530
  countUnstampedEventMedia: method(zod.z.object({}), UnstampedEventMediaCountSchema, { auth: "admin" }),
37485
37531
  /**
@@ -59066,7 +59112,7 @@ var require_alerts_addon = __commonJS({
59066
59112
  [Symbol.toStringTag]: { value: "Module" }
59067
59113
  });
59068
59114
  require_chunk_Cek0wNdY();
59069
- var require_dist10 = require_dist_Ck2jkBZk();
59115
+ var require_dist10 = require_dist_CcvXUhHK();
59070
59116
  function selectExpired(alerts, cutoffMs) {
59071
59117
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
59072
59118
  }
@@ -59885,7 +59931,7 @@ var require_console_logging = __commonJS({
59885
59931
  [Symbol.toStringTag]: { value: "Module" }
59886
59932
  });
59887
59933
  require_chunk_Cek0wNdY();
59888
- var require_dist10 = require_dist_Ck2jkBZk();
59934
+ var require_dist10 = require_dist_CcvXUhHK();
59889
59935
  var require_formatter = require_formatter_DqAKDlvN();
59890
59936
  var LEVEL_RANK = {
59891
59937
  debug: 0,
@@ -59979,7 +60025,7 @@ var require_core_blocks_addon = __commonJS({
59979
60025
  "use strict";
59980
60026
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
59981
60027
  var require_chunk = require_chunk_Cek0wNdY();
59982
- var require_dist10 = require_dist_Ck2jkBZk();
60028
+ var require_dist10 = require_dist_CcvXUhHK();
59983
60029
  var node_crypto = __require("crypto");
59984
60030
  var node_fs_promises = __require("fs/promises");
59985
60031
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -60876,11 +60922,11 @@ var require_core_blocks = __commonJS({
60876
60922
  }
60877
60923
  });
60878
60924
 
60879
- // ../system/dist/retired-settings-keys-Dp_CyuCW.js
60880
- var require_retired_settings_keys_Dp_CyuCW = __commonJS({
60881
- "../system/dist/retired-settings-keys-Dp_CyuCW.js"(exports) {
60925
+ // ../system/dist/retired-settings-keys-DXZ2xe7C.js
60926
+ var require_retired_settings_keys_DXZ2xe7C = __commonJS({
60927
+ "../system/dist/retired-settings-keys-DXZ2xe7C.js"(exports) {
60882
60928
  "use strict";
60883
- var require_dist10 = require_dist_Ck2jkBZk();
60929
+ var require_dist10 = require_dist_CcvXUhHK();
60884
60930
  function settingsStoreIsAuthoritativeHere(env) {
60885
60931
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
60886
60932
  return raw === "" || raw === "hub";
@@ -63094,8 +63140,8 @@ var require_device_manager_addon = __commonJS({
63094
63140
  [Symbol.toStringTag]: { value: "Module" }
63095
63141
  });
63096
63142
  require_chunk_Cek0wNdY();
63097
- var require_dist10 = require_dist_Ck2jkBZk();
63098
- var require_retired_settings_keys = require_retired_settings_keys_Dp_CyuCW();
63143
+ var require_dist10 = require_dist_CcvXUhHK();
63144
+ var require_retired_settings_keys = require_retired_settings_keys_DXZ2xe7C();
63099
63145
  var node_crypto = __require("crypto");
63100
63146
  var _camstack_types_node = require_node();
63101
63147
  var JOB_HISTORY = 20;
@@ -63558,10 +63604,17 @@ var require_device_manager_addon = __commonJS({
63558
63604
  const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
63559
63605
  return typeof live === "string" && live.length > 0 ? live : void 0;
63560
63606
  }
63561
- async function resolveDevicePresence(deps, row, deviceId) {
63607
+ async function resolveDevicePresence(deps, row, deviceId, ledgerNonEmpty) {
63562
63608
  if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
63563
63609
  if (row !== null) return "present";
63564
- return await deps.rows.count() > 0 ? "absent" : "unknown";
63610
+ return await ledgerNonEmpty() ? "absent" : "unknown";
63611
+ }
63612
+ function onceLedgerNonEmpty(deps) {
63613
+ let pending = null;
63614
+ return () => {
63615
+ if (pending === null) pending = deps.rows.count().then((n) => n > 0);
63616
+ return pending;
63617
+ };
63565
63618
  }
63566
63619
  function capAppliesToDeviceType(def, deviceType) {
63567
63620
  if (deviceType === void 0) return true;
@@ -63570,14 +63623,17 @@ var require_device_manager_addon = __commonJS({
63570
63623
  return declared.some((t) => t === deviceType);
63571
63624
  }
63572
63625
  async function getBindings(deps, input) {
63573
- const row = await deps.rows.get(input.deviceId);
63574
- return resolveBindingsForDevice(deps, input.deviceId, row);
63626
+ const [row, store] = await Promise.all([deps.rows.get(input.deviceId), readBindingsStore(deps)]);
63627
+ return resolveBindingsForDevice(deps, input.deviceId, row, {
63628
+ store,
63629
+ ledgerNonEmpty: onceLedgerNonEmpty(deps)
63630
+ });
63575
63631
  }
63576
- async function resolveBindingsForDevice(deps, deviceId, row) {
63632
+ async function resolveBindingsForDevice(deps, deviceId, row, pass) {
63577
63633
  const storeKey = String(deviceId);
63578
- const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
63634
+ const perDevice = pass.store.deviceBindings[storeKey] ?? {};
63579
63635
  const deviceType = resolveDeviceType(deps, row, deviceId);
63580
- const presence = await resolveDevicePresence(deps, row, deviceId);
63636
+ const presence = await resolveDevicePresence(deps, row, deviceId, pass.ledgerNonEmpty);
63581
63637
  const entries = [];
63582
63638
  const seenCaps = /* @__PURE__ */ new Set();
63583
63639
  const resolveRemote = (capName) => deps.remoteNativeCaps.get(deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, deviceId);
@@ -63677,13 +63733,17 @@ var require_device_manager_addon = __commonJS({
63677
63733
  }
63678
63734
  async function getBindingsBatch(deps, input) {
63679
63735
  const ids = [...new Set(input.deviceIds)];
63680
- const rows = await deps.rows.getMany(ids);
63736
+ const [rows, store] = await Promise.all([deps.rows.getMany(ids), readBindingsStore(deps)]);
63737
+ const pass = {
63738
+ store,
63739
+ ledgerNonEmpty: onceLedgerNonEmpty(deps)
63740
+ };
63681
63741
  const out = [];
63682
- for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null));
63742
+ for (const deviceId of ids) out.push(await resolveBindingsForDevice(deps, deviceId, rows.get(deviceId) ?? null, pass));
63683
63743
  return out;
63684
63744
  }
63685
63745
  async function getAllBindings(deps) {
63686
- const fleet = await deps.rows.listAll();
63746
+ const [fleet, store] = await Promise.all([deps.rows.listAll(), readBindingsStore(deps)]);
63687
63747
  const rowById = /* @__PURE__ */ new Map();
63688
63748
  const ids = /* @__PURE__ */ new Set();
63689
63749
  for (const row of fleet) {
@@ -63696,8 +63756,12 @@ var require_device_manager_addon = __commonJS({
63696
63756
  deps.ctx.logger.warn("getAllBindings found no devices \u2014 warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
63697
63757
  return [];
63698
63758
  }
63759
+ const pass = {
63760
+ store,
63761
+ ledgerNonEmpty: onceLedgerNonEmpty(deps)
63762
+ };
63699
63763
  const out = [];
63700
- for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null));
63764
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await resolveBindingsForDevice(deps, deviceId, rowById.get(deviceId) ?? null, pass));
63701
63765
  return out;
63702
63766
  }
63703
63767
  async function lookupPersistedStableId(deps, deviceId) {
@@ -64257,7 +64321,12 @@ var require_device_manager_addon = __commonJS({
64257
64321
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
64258
64322
  };
64259
64323
  }
64260
- async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren) {
64324
+ function persistedChildIds(childRows, liveChildren) {
64325
+ const live = /* @__PURE__ */ new Set();
64326
+ for (const device of liveChildren) live.add(device.id);
64327
+ return childRows.filter((row) => !live.has(row.meta.id)).map((row) => row.meta.id);
64328
+ }
64329
+ function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs) {
64261
64330
  const results = [];
64262
64331
  const seen = /* @__PURE__ */ new Set();
64263
64332
  const rowById = /* @__PURE__ */ new Map();
@@ -64273,9 +64342,9 @@ var require_device_manager_addon = __commonJS({
64273
64342
  const childStableId = m.stableId;
64274
64343
  const key = String(m.id);
64275
64344
  if (seen.has(key)) continue;
64276
- const persistedConfig = await pctx.settings.readDeviceStore(m.id);
64345
+ const persistedConfig = configs.get(m.id) ?? {};
64277
64346
  const metadata = row.metadata;
64278
- const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
64347
+ const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig, childStableId, ownerAddonId);
64279
64348
  results.push({
64280
64349
  id: m.id,
64281
64350
  stableId: childStableId,
@@ -64290,7 +64359,7 @@ var require_device_manager_addon = __commonJS({
64290
64359
  probed: pctx.host.resolveDeviceProbed(m.id),
64291
64360
  features: persistedFeatures(m.features),
64292
64361
  isCamera: false,
64293
- config: persistedConfig ?? {},
64362
+ config: persistedConfig,
64294
64363
  metadata,
64295
64364
  ...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
64296
64365
  ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
@@ -64317,7 +64386,8 @@ var require_device_manager_addon = __commonJS({
64317
64386
  }
64318
64387
  const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
64319
64388
  const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
64320
- return [...await projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren)];
64389
+ const configs = await readDeviceConfigs(pctx, persistedChildIds(childRows, liveChildren));
64390
+ return [...projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren, configs)];
64321
64391
  }
64322
64392
  async function getChildrenBatch(pctx, input) {
64323
64393
  const parentIds = [...new Set(input.parentDeviceIds)];
@@ -64345,11 +64415,14 @@ var require_device_manager_addon = __commonJS({
64345
64415
  if (bucket === void 0) liveByParent.set(parentId, [device]);
64346
64416
  else bucket.push(device);
64347
64417
  }
64418
+ const configIds = [];
64419
+ for (const parentId of owners.keys()) configIds.push(...persistedChildIds(rowsByParent.get(parentId) ?? [], liveByParent.get(parentId) ?? []));
64420
+ const configs = await readDeviceConfigs(pctx, configIds);
64348
64421
  for (const [parentId, ownerAddonId] of owners) {
64349
64422
  const childRows = rowsByParent.get(parentId) ?? [];
64350
64423
  const liveChildren = liveByParent.get(parentId) ?? [];
64351
64424
  if (childRows.length === 0 && liveChildren.length === 0) continue;
64352
- const projected = await projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren);
64425
+ const projected = projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren, configs);
64353
64426
  if (projected.length > 0) out[String(parentId)] = [...projected];
64354
64427
  }
64355
64428
  return out;
@@ -65391,18 +65464,20 @@ var require_device_manager_addon = __commonJS({
65391
65464
  }
65392
65465
  async function setWrapperActive(deps, input) {
65393
65466
  const storeKey = String(input.deviceId);
65394
- const store = await readBindingsStore(deps.bindingsDeps);
65395
- const perDevice = { ...store.deviceBindings[storeKey] };
65396
- if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
65397
- else perDevice[input.capName] = { wrapperAddonId: null };
65398
- const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
65399
- ...store.deviceBindings,
65400
- [storeKey]: perDevice
65401
- } : (() => {
65402
- const { [storeKey]: _drop, ...rest } = store.deviceBindings;
65403
- return rest;
65404
- })();
65405
- await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
65467
+ await deps.bindingsDeps.withAddonStoreWriteLock(async () => {
65468
+ const store = await readBindingsStore(deps.bindingsDeps);
65469
+ const perDevice = { ...store.deviceBindings[storeKey] };
65470
+ if (input.active) perDevice[input.capName] = { wrapperAddonId: input.wrapperAddonId };
65471
+ else perDevice[input.capName] = { wrapperAddonId: null };
65472
+ const nextDeviceBindings = Object.keys(perDevice).length > 0 ? {
65473
+ ...store.deviceBindings,
65474
+ [storeKey]: perDevice
65475
+ } : (() => {
65476
+ const { [storeKey]: _drop, ...rest } = store.deviceBindings;
65477
+ return rest;
65478
+ })();
65479
+ await writeBindingsStore(deps.bindingsDeps, { deviceBindings: nextDeviceBindings });
65480
+ });
65406
65481
  deps.ctx.eventBus.emit({
65407
65482
  id: (0, node_crypto.randomUUID)(),
65408
65483
  timestamp: /* @__PURE__ */ new Date(),
@@ -65681,12 +65756,13 @@ var require_device_manager_addon = __commonJS({
65681
65756
  });
65682
65757
  await pctx.settings.clearDeviceStore(deviceId);
65683
65758
  await pctx.settings.clearDeviceRuntimeState(deviceId);
65684
- const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
65685
65759
  const bindingKey = String(deviceId);
65686
- if (bindingsStore.deviceBindings[bindingKey]) {
65760
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
65761
+ const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
65762
+ if (!bindingsStore.deviceBindings[bindingKey]) return;
65687
65763
  const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
65688
65764
  await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
65689
- }
65765
+ });
65690
65766
  pctx.host.remoteNativeCaps.delete(deviceId);
65691
65767
  pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
65692
65768
  pctx.metaStore.idToAddonId.delete(deviceId);
@@ -66028,7 +66104,9 @@ var require_device_manager_addon = __commonJS({
66028
66104
  ...def,
66029
66105
  unit: require_dist10.normalizeUnit(def.unit) ?? def.unit
66030
66106
  } : def]));
66031
- await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
66107
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
66108
+ await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
66109
+ });
66032
66110
  }
66033
66111
  async function applyInitialMeta(pctx, input) {
66034
66112
  const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
@@ -66171,16 +66249,21 @@ var require_device_manager_addon = __commonJS({
66171
66249
  async function addLocation(pctx, input) {
66172
66250
  const trimmed = input.name.trim();
66173
66251
  if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
66174
- const current = (await pctx.metaStore.readStore()).locations ?? [];
66175
- if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
66176
- await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
66252
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
66253
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
66254
+ if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
66255
+ await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
66256
+ });
66177
66257
  }
66178
66258
  async function removeLocation(pctx, input) {
66179
66259
  const trimmed = input.name.trim();
66180
66260
  if (trimmed.length === 0) return;
66181
- const current = (await pctx.metaStore.readStore()).locations ?? [];
66182
- const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
66183
- if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
66261
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
66262
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
66263
+ const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
66264
+ if (remaining.length === current.length) return;
66265
+ await pctx.settings.writeAddonStore({ locations: remaining });
66266
+ });
66184
66267
  if (input.cascade !== true) return;
66185
66268
  const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
66186
66269
  const out = [];
@@ -66221,23 +66304,40 @@ var require_device_manager_addon = __commonJS({
66221
66304
  function isRoleDisplayDefaults(value) {
66222
66305
  return value !== null && typeof value === "object" && !Array.isArray(value);
66223
66306
  }
66307
+ function createWriteLock() {
66308
+ let chain = Promise.resolve();
66309
+ return async (fn) => {
66310
+ const previous = chain;
66311
+ let release = () => {
66312
+ };
66313
+ chain = new Promise((resolve) => {
66314
+ release = resolve;
66315
+ });
66316
+ try {
66317
+ await previous.catch(() => {
66318
+ });
66319
+ return await fn();
66320
+ } finally {
66321
+ release();
66322
+ }
66323
+ };
66324
+ }
66224
66325
  var DeviceMetaStore = class {
66225
66326
  settings;
66226
66327
  registry;
66227
66328
  rows;
66329
+ withAddonStoreWriteLock;
66228
66330
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
66229
66331
  * The persisted row store is authoritative but reads are async; hub-side
66230
66332
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
66231
66333
  * ownership without awaiting. Kept in sync with every register/remove and
66232
66334
  * warmed from persistence on boot. */
66233
66335
  idToAddonId = /* @__PURE__ */ new Map();
66234
- /** Serialises every read-modify-write of a device row through one promise
66235
- * chain (see `withMetaWriteLock`). Per-instance state. */
66236
- metaWriteChain = Promise.resolve();
66237
- constructor(settings, registry, rows) {
66336
+ constructor(settings, registry, rows, withAddonStoreWriteLock = createWriteLock()) {
66238
66337
  this.settings = settings;
66239
66338
  this.registry = registry;
66240
66339
  this.rows = rows;
66340
+ this.withAddonStoreWriteLock = withAddonStoreWriteLock;
66241
66341
  }
66242
66342
  /** The read currently in flight, or null. Never a settled value — see
66243
66343
  * {@link readStore}. */
@@ -66268,22 +66368,7 @@ var require_device_manager_addon = __commonJS({
66268
66368
  this.inFlightRead = read;
66269
66369
  return read;
66270
66370
  };
66271
- withMetaWriteLock = async (fn) => {
66272
- const previous = this.metaWriteChain;
66273
- let release = () => {
66274
- };
66275
- const next = new Promise((resolve) => {
66276
- release = resolve;
66277
- });
66278
- this.metaWriteChain = next;
66279
- try {
66280
- await previous.catch(() => {
66281
- });
66282
- return await fn();
66283
- } finally {
66284
- release();
66285
- }
66286
- };
66371
+ withMetaWriteLock = createWriteLock();
66287
66372
  /** The whole persisted row for one device, or `null`. */
66288
66373
  getRow = async (deviceId) => this.rows.get(deviceId);
66289
66374
  /**
@@ -66317,11 +66402,24 @@ var require_device_manager_addon = __commonJS({
66317
66402
  ids.delete(parentId);
66318
66403
  return [...ids];
66319
66404
  };
66320
- allocateNextDeviceId = async () => {
66405
+ /**
66406
+ * Mint the next numeric device id.
66407
+ *
66408
+ * A read-modify-write of `nextDeviceId`, and it must hold the addon-store
66409
+ * lock for the whole of it. Serialising the WRITES is not enough and never
66410
+ * was: two callers that both read `N` both write `N + 1` and both return
66411
+ * `N`, so two devices receive the same id. (`writeAddonStore` is itself
66412
+ * read-modify-write over the addon's whole key range, so an unlocked bump
66413
+ * can also be reverted wholesale by a concurrent `setWrapperActive`.)
66414
+ *
66415
+ * Callers hold {@link withMetaWriteLock} — this is the one nesting, and it
66416
+ * only ever goes meta ⊃ addon-store.
66417
+ */
66418
+ allocateNextDeviceId = async () => this.withAddonStoreWriteLock(async () => {
66321
66419
  const current = (await this.readStore()).nextDeviceId ?? 1;
66322
66420
  await this.settings.writeAddonStore({ nextDeviceId: current + 1 });
66323
66421
  return current;
66324
- };
66422
+ });
66325
66423
  };
66326
66424
  var CENSUS_STACK_FRAMES = 3;
66327
66425
  function siteFromStack(stack) {
@@ -67515,6 +67613,17 @@ var require_device_manager_addon = __commonJS({
67515
67613
  * it through {@link bindingsDeps} or the `ProviderContext`.
67516
67614
  */
67517
67615
  deviceRows = null;
67616
+ /**
67617
+ * The ONE lock ordering every read-modify-write of this addon's
67618
+ * `addon-settings` key range (`deviceBindings`, `locations`, `nextDeviceId`,
67619
+ * `roleDisplayDefaults`).
67620
+ *
67621
+ * It lives on the addon rather than on `DeviceMetaStore` because the writers
67622
+ * do not all go through that class — `setWrapperActive` and `removeDevice`'s
67623
+ * bindings purge reach the store through `BindingsDeps` — and two locks over
67624
+ * one key range is the same defect one level up. Handed to both.
67625
+ */
67626
+ addonStoreWriteLock = createWriteLock();
67518
67627
  /** Build the dependency context the extracted binding resolvers consume. */
67519
67628
  get bindingsDeps() {
67520
67629
  const rows = this.deviceRows;
@@ -67523,7 +67632,8 @@ var require_device_manager_addon = __commonJS({
67523
67632
  ctx: this.ctx,
67524
67633
  capabilityRegistry: this.capabilityRegistry,
67525
67634
  remoteNativeCaps: this.remoteNativeCaps,
67526
- rows
67635
+ rows,
67636
+ withAddonStoreWriteLock: this.addonStoreWriteLock
67527
67637
  };
67528
67638
  }
67529
67639
  async getBindings(input) {
@@ -67592,7 +67702,7 @@ var require_device_manager_addon = __commonJS({
67592
67702
  } catch (err) {
67593
67703
  this.ctx.logger.warn("retired-row purge failed", { meta: { error: require_dist10.errMsg(err) } });
67594
67704
  }
67595
- const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
67705
+ const metaStore = new DeviceMetaStore(settings, registry, deviceRows, this.addonStoreWriteLock);
67596
67706
  this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
67597
67707
  const stateMirror = this.stateMirrorImpl;
67598
67708
  const resolvePersistedById = metaStore.resolvePersistedById;
@@ -67844,7 +67954,7 @@ var require_hub_forwarder = __commonJS({
67844
67954
  [Symbol.toStringTag]: { value: "Module" }
67845
67955
  });
67846
67956
  require_chunk_Cek0wNdY();
67847
- var require_dist10 = require_dist_Ck2jkBZk();
67957
+ var require_dist10 = require_dist_CcvXUhHK();
67848
67958
  var require_formatter = require_formatter_DqAKDlvN();
67849
67959
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
67850
67960
  var HubForwarderDestination = class {
@@ -67981,7 +68091,7 @@ var require_liveness_monitor_addon = __commonJS({
67981
68091
  "use strict";
67982
68092
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
67983
68093
  require_chunk_Cek0wNdY();
67984
- var require_dist10 = require_dist_Ck2jkBZk();
68094
+ var require_dist10 = require_dist_CcvXUhHK();
67985
68095
  var NO_DEVICES = "liveness:no-devices";
67986
68096
  var ALL_OFFLINE = "liveness:all-devices-offline";
67987
68097
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -68171,7 +68281,7 @@ var require_local_auth_addon = __commonJS({
68171
68281
  [Symbol.toStringTag]: { value: "Module" }
68172
68282
  });
68173
68283
  var require_chunk = require_chunk_Cek0wNdY();
68174
- var require_dist10 = require_dist_Ck2jkBZk();
68284
+ var require_dist10 = require_dist_CcvXUhHK();
68175
68285
  var node_crypto = __require("crypto");
68176
68286
  node_crypto = require_chunk.__toESM(node_crypto);
68177
68287
  var crypto$1 = __require("crypto");
@@ -75984,7 +76094,7 @@ var require_loki_logging = __commonJS({
75984
76094
  [Symbol.toStringTag]: { value: "Module" }
75985
76095
  });
75986
76096
  require_chunk_Cek0wNdY();
75987
- var require_dist10 = require_dist_Ck2jkBZk();
76097
+ var require_dist10 = require_dist_CcvXUhHK();
75988
76098
  function sanitizeLabelName(raw) {
75989
76099
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
75990
76100
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -76549,7 +76659,7 @@ var require_native_metrics_addon = __commonJS({
76549
76659
  [Symbol.toStringTag]: { value: "Module" }
76550
76660
  });
76551
76661
  var require_chunk = require_chunk_Cek0wNdY();
76552
- var require_dist10 = require_dist_Ck2jkBZk();
76662
+ var require_dist10 = require_dist_CcvXUhHK();
76553
76663
  var node_fs_promises = __require("fs/promises");
76554
76664
  var node_child_process = __require("child_process");
76555
76665
  var node_util = __require("util");
@@ -79171,7 +79281,7 @@ var require_filesystem_storage_addon = __commonJS({
79171
79281
  [Symbol.toStringTag]: { value: "Module" }
79172
79282
  });
79173
79283
  var require_chunk = require_chunk_Cek0wNdY();
79174
- var require_dist10 = require_dist_Ck2jkBZk();
79284
+ var require_dist10 = require_dist_CcvXUhHK();
79175
79285
  var node_crypto = __require("crypto");
79176
79286
  var node_fs_promises = __require("fs/promises");
79177
79287
  var node_path = __require("path");
@@ -80287,8 +80397,8 @@ var require_sqlite_settings_addon = __commonJS({
80287
80397
  [Symbol.toStringTag]: { value: "Module" }
80288
80398
  });
80289
80399
  var require_chunk = require_chunk_Cek0wNdY();
80290
- var require_dist10 = require_dist_Ck2jkBZk();
80291
- var require_retired_settings_keys = require_retired_settings_keys_Dp_CyuCW();
80400
+ var require_dist10 = require_dist_CcvXUhHK();
80401
+ var require_retired_settings_keys = require_retired_settings_keys_DXZ2xe7C();
80292
80402
  var node_crypto = __require("crypto");
80293
80403
  var node_fs = __require("fs");
80294
80404
  var node_module = __require("module");
@@ -80619,11 +80729,16 @@ var require_sqlite_settings_addon = __commonJS({
80619
80729
  const resolve = (field) => {
80620
80730
  const expr = fieldExprFor(field, shape);
80621
80731
  if (expr === null && mode === "mutate") throw new UnsafeFilterError(`filter refers to "${field}", which this collection cannot express \u2014 refusing to run a bulk mutation with a dropped predicate`);
80732
+ if (expr === null && mode === "measure") throw new UnsafeFilterError(`filter refers to "${field}", which this collection cannot express \u2014 refusing to answer with a number counted over a different question`);
80622
80733
  return expr;
80623
80734
  };
80624
80735
  for (const [field, value] of Object.entries(filter?.where ?? {})) {
80625
80736
  const expr = resolve(field);
80626
80737
  if (expr === null) continue;
80738
+ if (value === null) {
80739
+ clauses.push(`${expr} IS NULL`);
80740
+ continue;
80741
+ }
80627
80742
  clauses.push(`${expr} = ?`);
80628
80743
  params.push(serialize2(value));
80629
80744
  }
@@ -80646,6 +80761,10 @@ var require_sqlite_settings_addon = __commonJS({
80646
80761
  for (const [field, value] of Object.entries(filter?.whereNot ?? {})) {
80647
80762
  const expr = resolve(field);
80648
80763
  if (expr === null) continue;
80764
+ if (value === null) {
80765
+ clauses.push(`${expr} IS NOT NULL`);
80766
+ continue;
80767
+ }
80649
80768
  clauses.push(`(${expr} IS NULL OR ${expr} != ?)`);
80650
80769
  params.push(serialize2(value));
80651
80770
  }
@@ -81158,13 +81277,17 @@ var require_sqlite_settings_addon = __commonJS({
81158
81277
  * (visibility, kind, device, time window, expiry) — so `total` counted rows
81159
81278
  * the page could never show, including other users'.
81160
81279
  *
81161
- * `select` mode, deliberately: `count` must be forgiving in exactly the way
81162
- * `query` is, or the two disagree again for a new reason.
81280
+ * `measure` mode. `count` compiles the same predicates `query` does — the
81281
+ * two must never disagree about WHICH rows they are talking about — but it
81282
+ * refuses a predicate it cannot express rather than skipping it. `query`
81283
+ * hands back rows and a caller can see the filter did not bite; a count
81284
+ * hands back a number, and the whole collection and the intended subset are
81285
+ * the same shape.
81163
81286
  */
81164
81287
  async count({ namespace, collection, filter }) {
81165
81288
  const scoped = this.scopedName(namespace, collection);
81166
81289
  const decl = this.requireDeclared(scoped);
81167
- const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "select", (v) => this.serializeColumnValue(v));
81290
+ const { whereSql, params } = compileFilter$1(filter, this.shapeOf(decl), "measure", (v) => this.serializeColumnValue(v));
81168
81291
  const sql = `SELECT COUNT(*) AS cnt FROM "${scoped}"${whereSql}`;
81169
81292
  return this.measured({
81170
81293
  op: "count",
@@ -81182,8 +81305,10 @@ var require_sqlite_settings_addon = __commonJS({
81182
81305
  * projection — an unresolvable field THROWS rather than being dropped, because
81183
81306
  * a missing aggregate comes back as a number that looks real.
81184
81307
  *
81185
- * `select` mode on the filter, so this agrees with `query` and `count` about
81186
- * which rows it is talking about.
81308
+ * `measure` mode on the filter, so this agrees with `query` and `count`
81309
+ * about which rows it is talking about — and, since 2026-08-30, refuses an
81310
+ * unresolvable PREDICATE for the same reason it already refused an
81311
+ * unresolvable FIELD. Both come back as a number that looks real.
81187
81312
  */
81188
81313
  async aggregate({ namespace, collection, fields, filter }) {
81189
81314
  const scoped = this.scopedName(namespace, collection);
@@ -81195,7 +81320,7 @@ var require_sqlite_settings_addon = __commonJS({
81195
81320
  if (col === null) throw new UnsafeFilterError(`aggregate cannot read "${f.field}" on "${scoped}" \u2014 it is not a column of this collection, and answering without it would return a number that looks real`);
81196
81321
  selects.push(`${f.op.toUpperCase()}(${col}) AS "a${i}"`);
81197
81322
  });
81198
- const { whereSql, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
81323
+ const { whereSql, params } = compileFilter$1(filter, shape, "measure", (v) => this.serializeColumnValue(v));
81199
81324
  const sql = `SELECT ${selects.join(", ")} FROM "${scoped}"${whereSql}`;
81200
81325
  const row = this.measured({
81201
81326
  op: "aggregate",
@@ -81219,7 +81344,7 @@ var require_sqlite_settings_addon = __commonJS({
81219
81344
  const shape = this.shapeOf(decl);
81220
81345
  const col = fieldExprFor(field, shape);
81221
81346
  if (col === null) return [];
81222
- const { whereSql: where, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
81347
+ const { whereSql: where, params } = compileFilter$1(filter, shape, "measure", (v) => this.serializeColumnValue(v));
81223
81348
  const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
81224
81349
  return this.measured({
81225
81350
  op: "histogram",
@@ -81500,8 +81625,10 @@ var require_sqlite_settings_addon = __commonJS({
81500
81625
  * connection whose `sqlite_stat1` visibility and temp schema match.
81501
81626
  *
81502
81627
  * `EXPLAIN QUERY PLAN` prepares and plans; it does not execute the statement,
81503
- * so it costs no page reads of its own. It is still called only from the
81504
- * slow-call path, once per shape per window.
81628
+ * so it costs no page reads of its own. In production it is called only from
81629
+ * the slow-call path, once per shape per window; it is public so a guard can
81630
+ * assert that a declared index is the one the planner actually picks — "we
81631
+ * added an index" and "the query uses it" are different claims.
81505
81632
  *
81506
81633
  * Never throws. A plan that cannot be taken (a statement the engine will no
81507
81634
  * longer prepare, a closed handle mid-shutdown) is a missing ANSWER, not a
@@ -82650,7 +82777,7 @@ var require_storage_orchestrator_addon = __commonJS({
82650
82777
  [Symbol.toStringTag]: { value: "Module" }
82651
82778
  });
82652
82779
  var require_chunk = require_chunk_Cek0wNdY();
82653
- var require_dist10 = require_dist_Ck2jkBZk();
82780
+ var require_dist10 = require_dist_CcvXUhHK();
82654
82781
  var node_crypto = __require("crypto");
82655
82782
  var node_fs_promises = __require("fs/promises");
82656
82783
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -82856,6 +82983,10 @@ var require_storage_orchestrator_addon = __commonJS({
82856
82983
  "recorder",
82857
82984
  "pipeline"
82858
82985
  ];
82986
+ function describeUnstamped(lane) {
82987
+ if (!lane.present) return "0";
82988
+ return lane.rows === null ? "an unknown number of" : lane.rows.toString();
82989
+ }
82859
82990
  var StorageMigrationCoordinator = class {
82860
82991
  deps;
82861
82992
  active = null;
@@ -82899,12 +83030,21 @@ var require_storage_orchestrator_addon = __commonJS({
82899
83030
  if (moves.length === 0) throw new Error("select at least one storage class");
82900
83031
  if (moves.some((move) => move.storageClass === "eventMedia")) {
82901
83032
  const unstamped = await this.deps.participants.analytics.countUnstamped();
82902
- if (unstamped.total > 0) {
82903
- if (mode === "nonBlocking") throw new Error(`${unstamped.total.toString()} event-media row(s) still carry no locationId (${unstamped.media.toString()} media, ${unstamped.retrainFrames.toString()} retrain frames). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.getDefaultLocation("eventMedia")?.id ?? "eventMedia"}" }).`);
83033
+ if (unstamped === null) {
83034
+ if (mode === "nonBlocking") throw new Error("could not determine whether any event-media row still carries no locationId. A non-blocking cutover repoints the default and would silently orphan every such row, so an unmeasured collection is refused exactly like a non-empty one. Retry, or run the blocking mode, which stamps rows as it moves them.");
83035
+ findings.push({
83036
+ code: "unstampedEventMediaRows",
83037
+ storageClass: "eventMedia",
83038
+ message: "the count of event-media rows carrying no locationId could not be taken \u2014 treat it as unknown, not as zero. This blocking migration stamps them as it moves them, so it is safe regardless; a non-blocking one would be refused."
83039
+ });
83040
+ } else if (unstamped.anyPresent) {
83041
+ const scale = unstamped.total === null ? "an unknown number of" : unstamped.total.toString();
83042
+ const perLane = `${describeUnstamped(unstamped.media)} media, ${describeUnstamped(unstamped.retrainFrames)} retrain frames`;
83043
+ if (mode === "nonBlocking") throw new Error(`${scale} event-media row(s) still carry no locationId (${perLane}). A non-blocking cutover would silently orphan them. Run the seal first: pipelineAnalytics.relocateMedia({ mode: "seal", toLocationId: "${this.deps.locations.getDefaultLocation("eventMedia")?.id ?? "eventMedia"}" }).`);
82904
83044
  findings.push({
82905
83045
  code: "unstampedEventMediaRows",
82906
83046
  storageClass: "eventMedia",
82907
- message: `${unstamped.total.toString()} event-media row(s) carry no locationId. This blocking migration stamps them as it moves them, but a non-blocking one would be refused until a seal pass drives the count to zero.`
83047
+ message: `${scale} event-media row(s) carry no locationId (${perLane}). This blocking migration stamps them as it moves them, but a non-blocking one would be refused until a seal pass drives the count to zero.`
82908
83048
  });
82909
83049
  }
82910
83050
  }
@@ -83153,6 +83293,7 @@ var require_storage_orchestrator_addon = __commonJS({
83153
83293
  const remaining = residue.filter((entry) => entry.storageClass === storageClass);
83154
83294
  if (remaining.length === 0) throw new Error(`"${storageClass}" has nothing left outside its default location \u2014 there is nothing to drain. To CHANGE where it writes, use storageMigration.start with a new destination.`);
83155
83295
  for (const entry of remaining) {
83296
+ if (this.deps.locations.getLocationById(entry.toLocationId)?.enabled === false) throw new Error(`The "${storageClass}" drain destination "${entry.toLocationId}" is DISABLED \u2014 a disabled location takes no writes, and a drain is the largest write there is. Enable it on the Storage screen, or make an enabled location the "${storageClass}" default first.`);
83156
83297
  moves.push({
83157
83298
  storageClass,
83158
83299
  fromLocationId: entry.fromLocationId,
@@ -83404,6 +83545,7 @@ var require_storage_orchestrator_addon = __commonJS({
83404
83545
  filesMoved: status.filesMoved,
83405
83546
  filesTotal: status.filesTotal,
83406
83547
  bytesMoved: status.bytesMoved,
83548
+ ...status.rowsReconciled === void 0 ? {} : { rowsReconciled: status.rowsReconciled },
83407
83549
  startedAt: status.startedAt,
83408
83550
  observedAt: this.deps.now()
83409
83551
  };
@@ -83426,9 +83568,10 @@ var require_storage_orchestrator_addon = __commonJS({
83426
83568
  *
83427
83569
  * The gate is the RE-COUNT, not the seal job's terminal state: a seal that
83428
83570
  * failed some rows still finishes, and "finished" is not "there are none
83429
- * left". A non-zero count here fails the job while nothing has been paused
83571
+ * left". A remaining row here fails the job while nothing has been paused
83430
83572
  * and nothing has been repointed, which is the cheapest possible place to
83431
- * discover it.
83573
+ * discover it. So does a re-count that could not be TAKEN: the gate opens on
83574
+ * a measured absence, and only on that.
83432
83575
  *
83433
83576
  * The seal's own mover job id is deliberately NOT durable. It is idempotent
83434
83577
  * and cheap, so a coordinator restart mid-seal simply re-runs the whole
@@ -83438,7 +83581,8 @@ var require_storage_orchestrator_addon = __commonJS({
83438
83581
  async sealEventMedia(job) {
83439
83582
  const move = job.moves.find((candidate) => candidate.storageClass === "eventMedia");
83440
83583
  if (move === void 0) return;
83441
- if ((await this.deps.participants.analytics.countUnstamped()).total > 0) {
83584
+ const before = await this.deps.participants.analytics.countUnstamped();
83585
+ if (before === null || before.anyPresent) {
83442
83586
  const started = await this.deps.participants.analytics.startDrain({
83443
83587
  toLocationId: move.fromLocationId,
83444
83588
  mode: "seal"
@@ -83447,7 +83591,8 @@ var require_storage_orchestrator_addon = __commonJS({
83447
83591
  if (job.cancelRequested) return;
83448
83592
  }
83449
83593
  const after = await this.deps.participants.analytics.countUnstamped();
83450
- if (after.total > 0) throw new Error(`event-media seal left ${after.total.toString()} row(s) without a locationId (${after.media.toString()} media, ${after.retrainFrames.toString()} retrain frames). A non-blocking cutover would silently orphan them; nothing has been paused or repointed.`);
83594
+ if (after === null) throw new Error("event-media seal ran, but whether any row still carries no locationId could not be measured afterwards. Unknown is not zero; nothing has been paused or repointed.");
83595
+ if (after.anyPresent) throw new Error(`event-media seal left ${after.total === null ? "an unknown number of" : after.total.toString()} row(s) without a locationId (${describeUnstamped(after.media)} media, ${describeUnstamped(after.retrainFrames)} retrain frames). A non-blocking cutover would silently orphan them; nothing has been paused or repointed.`);
83451
83596
  }
83452
83597
  async waitForSeal(job, moverJobId) {
83453
83598
  const sleep = this.deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -83796,6 +83941,34 @@ var require_storage_orchestrator_addon = __commonJS({
83796
83941
  backfilled: backfill.length,
83797
83942
  total: this.locations.size
83798
83943
  } });
83944
+ this.reportDisabledDefaults();
83945
+ }
83946
+ /**
83947
+ * Shout about a persisted row that is BOTH the type default and disabled.
83948
+ *
83949
+ * `upsertLocation` cannot produce this (`resolveEnabled` force-enables a
83950
+ * default) and neither can `setDefaultLocations` — but hydrate writes rows
83951
+ * straight into the map without either, so a hand-edited store, a partial
83952
+ * migration or a row written by another build can. It matters because every
83953
+ * bare-type ref resolves through {@link getDefaultLocation}: on such a row
83954
+ * `storage.write({ location: 'eventMedia' })` and the migration coordinator's
83955
+ * drain destination both point at a disk the operator turned off. That is the
83956
+ * one thing this model must never do silently.
83957
+ *
83958
+ * Reported, never repaired: flipping an operator's flag back on at boot would
83959
+ * be the system overruling the switch instead of obeying it. The coordinator
83960
+ * refuses the drain (`storage-migration-coordinator.drain`); the bare-ref read
83961
+ * path is deliberately left working, because narrowing a READ is a worse bug
83962
+ * than the one this line reports.
83963
+ */
83964
+ reportDisabledDefaults() {
83965
+ for (const loc of this.locations.values()) {
83966
+ if (!loc.isDefault || loc.enabled !== false) continue;
83967
+ this.logger.error("storage-orchestrator: the type DEFAULT is disabled \u2014 every bare-type ref for this type resolves to a location the operator turned off. Enable it, or make another location the default.", { meta: {
83968
+ id: loc.id,
83969
+ type: loc.type
83970
+ } });
83971
+ }
83799
83972
  }
83800
83973
  /**
83801
83974
  * Inject the declaration-driven cardinality source. Called once by the
@@ -83867,6 +84040,7 @@ var require_storage_orchestrator_addon = __commonJS({
83867
84040
  loaded: this.locations.size,
83868
84041
  isSystemUpgraded: upgraded
83869
84042
  } });
84043
+ this.reportDisabledDefaults();
83870
84044
  }
83871
84045
  /**
83872
84046
  * Boot backfill (SP1): stamp `nodeId` on every persisted node-local
@@ -85178,7 +85352,7 @@ var require_system_config_addon = __commonJS({
85178
85352
  [Symbol.toStringTag]: { value: "Module" }
85179
85353
  });
85180
85354
  require_chunk_Cek0wNdY();
85181
- var require_dist10 = require_dist_Ck2jkBZk();
85355
+ var require_dist10 = require_dist_CcvXUhHK();
85182
85356
  var SECTION_TITLES = {
85183
85357
  server: "Server",
85184
85358
  auth: "Authentication"
@@ -103239,7 +103413,7 @@ var require_winston_logging = __commonJS({
103239
103413
  [Symbol.toStringTag]: { value: "Module" }
103240
103414
  });
103241
103415
  var require_chunk = require_chunk_Cek0wNdY();
103242
- var require_dist10 = require_dist_Ck2jkBZk();
103416
+ var require_dist10 = require_dist_CcvXUhHK();
103243
103417
  var require_formatter = require_formatter_DqAKDlvN();
103244
103418
  var node_path = __require("path");
103245
103419
  node_path = require_chunk.__toESM(node_path);
@@ -115732,12 +115906,12 @@ var require_dist2 = __commonJS({
115732
115906
  }
115733
115907
  });
115734
115908
 
115735
- // ../system/dist/manifest-system-deps-8boi90D9.js
115736
- var require_manifest_system_deps_8boi90D9 = __commonJS({
115737
- "../system/dist/manifest-system-deps-8boi90D9.js"(exports) {
115909
+ // ../system/dist/manifest-system-deps-oJMkWSX-.js
115910
+ var require_manifest_system_deps_oJMkWSX = __commonJS({
115911
+ "../system/dist/manifest-system-deps-oJMkWSX-.js"(exports) {
115738
115912
  "use strict";
115739
115913
  var require_chunk = require_chunk_Cek0wNdY();
115740
- require_dist_Ck2jkBZk();
115914
+ require_dist_CcvXUhHK();
115741
115915
  var node_crypto = __require("crypto");
115742
115916
  node_crypto = require_chunk.__toESM(node_crypto);
115743
115917
  var _camstack_types_node = require_node();
@@ -119845,6 +120019,174 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
119845
120019
  createClient: (nodeId) => new UdsLocalTransportClient(localEndpointPath(nodeId))
119846
120020
  };
119847
120021
  }
120022
+ var DEFAULT_RETENTION_SECONDS = 300;
120023
+ var DEFAULT_MAX_TRIPLES = 4e3;
120024
+ var PARENT_ROUTED_PROVIDER_ADDON_ID = "(unresolved: parent-routed)";
120025
+ var CapUsageRegistry = class {
120026
+ retentionSeconds;
120027
+ maxTriples;
120028
+ map = /* @__PURE__ */ new Map();
120029
+ triples = 0;
120030
+ droppedTriples = 0;
120031
+ constructor(opts) {
120032
+ this.retentionSeconds = Math.max(1, opts?.retentionSeconds ?? DEFAULT_RETENTION_SECONDS);
120033
+ this.maxTriples = Math.max(1, opts?.maxTriples ?? DEFAULT_MAX_TRIPLES);
120034
+ }
120035
+ /**
120036
+ * Record one observed cap call.
120037
+ *
120038
+ * On the hot path: this runs once per cross-process cap call on hub-main's
120039
+ * event loop, which is the cluster's only queue (D181). Three Map lookups, an
120040
+ * integer increment, and — only when the wall clock has moved on — a bounded
120041
+ * clear. No allocation once a triple exists, and it NEVER throws into the
120042
+ * call it observes.
120043
+ */
120044
+ recordCall(rec) {
120045
+ try {
120046
+ if (rec.callerAddonId === "" || rec.providerAddonId === "" || rec.capName === "") return;
120047
+ if (!Number.isFinite(rec.atMs) || rec.atMs < 0) return;
120048
+ const window2 = this.windowFor(rec);
120049
+ if (window2 === null) return;
120050
+ const sec = Math.floor(rec.atMs / 1e3);
120051
+ if (window2.lastCallAtMs !== 0) {
120052
+ const lastSec = Math.floor(window2.lastCallAtMs / 1e3);
120053
+ const advanced = Math.min(sec - lastSec, this.retentionSeconds);
120054
+ for (let k = 1; k <= advanced; k++) window2.buckets[(lastSec + k) % this.retentionSeconds] = 0;
120055
+ }
120056
+ const idx = sec % this.retentionSeconds;
120057
+ window2.buckets[idx] = (window2.buckets[idx] ?? 0) + 1;
120058
+ if (rec.atMs > window2.lastCallAtMs) window2.lastCallAtMs = rec.atMs;
120059
+ } catch {
120060
+ }
120061
+ }
120062
+ getGraph(opts) {
120063
+ const windowSeconds = Math.max(1, Math.min(opts.windowSeconds, this.retentionSeconds));
120064
+ const minMs = opts.nowMs - windowSeconds * 1e3;
120065
+ const nowSec = Math.floor(opts.nowMs / 1e3);
120066
+ const out = [];
120067
+ for (const [caller, byProvider] of this.map) for (const [provider, byCap] of byProvider) for (const [capName, window2] of byCap) {
120068
+ if (window2.lastCallAtMs < minMs) continue;
120069
+ const count = this.sumWindow(window2, nowSec, windowSeconds);
120070
+ if (count === 0) continue;
120071
+ const callsPerMin = count / windowSeconds * 60;
120072
+ out.push({
120073
+ callerAddonId: caller,
120074
+ providerAddonId: provider,
120075
+ capName,
120076
+ callsPerMin,
120077
+ lastCallAtMs: window2.lastCallAtMs
120078
+ });
120079
+ }
120080
+ return out;
120081
+ }
120082
+ /** See {@link CapUsageStats}. Cheap — three counters, no scan. */
120083
+ getStats() {
120084
+ return {
120085
+ triples: this.triples,
120086
+ maxTriples: this.maxTriples,
120087
+ droppedTriples: this.droppedTriples
120088
+ };
120089
+ }
120090
+ /** Test / diagnostic helper — drops all recorded calls. */
120091
+ clear() {
120092
+ this.map.clear();
120093
+ this.triples = 0;
120094
+ this.droppedTriples = 0;
120095
+ }
120096
+ /**
120097
+ * Sum the buckets covering `(nowSec - windowSeconds, nowSec]`.
120098
+ *
120099
+ * The scan starts at `lastSec`, never at `nowSec`. `recordCall` clears the
120100
+ * buckets the ring has advanced OVER, which by definition stops at the last
120101
+ * write: the buckets for the seconds since then still hold whatever the
120102
+ * previous pass left in them, and reading `nowSec` down would count it. A
120103
+ * triple that went quiet for a couple of minutes and then made one call would
120104
+ * report the burst it made five minutes earlier.
120105
+ *
120106
+ * No lower bound is needed beyond the window itself: every bucket at or below
120107
+ * `lastSec - retention` was cleared by the advance that reached `lastSec`, so
120108
+ * it reads zero anyway. The loop is bounded by `windowSeconds ≤ retention`.
120109
+ */
120110
+ sumWindow(window2, nowSec, windowSeconds) {
120111
+ const lastSec = Math.floor(window2.lastCallAtMs / 1e3);
120112
+ const from = Math.min(nowSec, lastSec);
120113
+ const floorSec = nowSec - windowSeconds;
120114
+ let count = 0;
120115
+ for (let s = from; s > floorSec; s--) count += window2.buckets[s % this.retentionSeconds] ?? 0;
120116
+ return count;
120117
+ }
120118
+ /**
120119
+ * The ring for this triple, creating it if the ceiling allows. `null` means
120120
+ * the observation is dropped — counted, never silent (see
120121
+ * {@link CapUsageStats.droppedTriples}).
120122
+ */
120123
+ windowFor(rec) {
120124
+ let byProvider = this.map.get(rec.callerAddonId);
120125
+ if (!byProvider) {
120126
+ byProvider = /* @__PURE__ */ new Map();
120127
+ this.map.set(rec.callerAddonId, byProvider);
120128
+ }
120129
+ let byCap = byProvider.get(rec.providerAddonId);
120130
+ if (!byCap) {
120131
+ byCap = /* @__PURE__ */ new Map();
120132
+ byProvider.set(rec.providerAddonId, byCap);
120133
+ }
120134
+ const existing = byCap.get(rec.capName);
120135
+ if (existing) return existing;
120136
+ if (this.triples >= this.maxTriples) {
120137
+ this.reclaimExpired(rec.atMs);
120138
+ if (this.triples >= this.maxTriples) {
120139
+ this.droppedTriples++;
120140
+ return null;
120141
+ }
120142
+ byProvider = this.map.get(rec.callerAddonId);
120143
+ if (!byProvider) {
120144
+ byProvider = /* @__PURE__ */ new Map();
120145
+ this.map.set(rec.callerAddonId, byProvider);
120146
+ }
120147
+ byCap = byProvider.get(rec.providerAddonId);
120148
+ if (!byCap) {
120149
+ byCap = /* @__PURE__ */ new Map();
120150
+ byProvider.set(rec.providerAddonId, byCap);
120151
+ }
120152
+ }
120153
+ const created = {
120154
+ buckets: new Uint32Array(this.retentionSeconds),
120155
+ lastCallAtMs: 0
120156
+ };
120157
+ byCap.set(rec.capName, created);
120158
+ this.triples++;
120159
+ return created;
120160
+ }
120161
+ /**
120162
+ * Drop every triple whose whole ring has aged out — its last call is older
120163
+ * than the retention window, so it can contribute to no query. O(triples),
120164
+ * and only reachable when the ceiling is full, so it is amortised away by the
120165
+ * admissions it enables.
120166
+ */
120167
+ reclaimExpired(nowMs) {
120168
+ const cutoff = nowMs - this.retentionSeconds * 1e3;
120169
+ for (const [caller, byProvider] of this.map) {
120170
+ for (const [provider, byCap] of byProvider) {
120171
+ for (const [capName, window2] of byCap) {
120172
+ if (window2.lastCallAtMs >= cutoff) continue;
120173
+ byCap.delete(capName);
120174
+ this.triples--;
120175
+ }
120176
+ if (byCap.size === 0) byProvider.delete(provider);
120177
+ }
120178
+ if (byProvider.size === 0) this.map.delete(caller);
120179
+ }
120180
+ }
120181
+ };
120182
+ var singleton = null;
120183
+ function getCapUsageRegistry() {
120184
+ if (!singleton) singleton = new CapUsageRegistry();
120185
+ return singleton;
120186
+ }
120187
+ function __resetCapUsageRegistryForTests() {
120188
+ singleton = null;
120189
+ }
119848
120190
  function createSharedBusState(retainRecent = false) {
119849
120191
  return {
119850
120192
  handlers: /* @__PURE__ */ new Map(),
@@ -120391,6 +120733,8 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
120391
120733
  isAddonPinnedCall;
120392
120734
  /** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
120393
120735
  capTimeoutMs;
120736
+ /** See {@link LocalChildRegistryOptions.capUsageObserver}. */
120737
+ capUsageObserver;
120394
120738
  /** Tracks capNames already logged as UDS-routed; one INFO line per capName per process. */
120395
120739
  egressRoutedCaps = /* @__PURE__ */ new Set();
120396
120740
  /** Active event fan-out mode, read once from `CAMSTACK_UDS_EVENT_FANOUT`. */
@@ -120422,6 +120766,7 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
120422
120766
  this.isAggregatedCollectionMethod = opts.isAggregatedCollectionMethod;
120423
120767
  this.isAddonPinnedCall = opts.isAddonPinnedCall;
120424
120768
  this.capTimeoutMs = opts.capTimeoutMs;
120769
+ this.capUsageObserver = opts.capUsageObserver;
120425
120770
  } else {
120426
120771
  this.server = serverOrOptions;
120427
120772
  this.onUnownedCall = onUnownedCallArg;
@@ -120588,6 +120933,30 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
120588
120933
  }
120589
120934
  return candidates[0];
120590
120935
  }
120936
+ /**
120937
+ * Publish one cap-usage observation, if a sink is wired.
120938
+ *
120939
+ * Cost discipline — this is on the hot path (D181: hub-main's event loop is
120940
+ * the cluster's only queue): no sink means one undefined check; with a sink
120941
+ * it is one object literal and one `Date.now()`, and the sink itself is O(1).
120942
+ * Wrapped so a broken observer can never fail the call it observes — the
120943
+ * registry swallows too, and BOTH matter: this catch also covers a sink that
120944
+ * is not the registry.
120945
+ */
120946
+ recordCapUsage(callerChildId, providerChildId, capName, methodName) {
120947
+ const sink = this.capUsageObserver;
120948
+ if (sink === void 0 || callerChildId === null) return;
120949
+ try {
120950
+ sink({
120951
+ callerAddonId: callerChildId,
120952
+ providerAddonId: providerChildId ?? "(unresolved: parent-routed)",
120953
+ capName,
120954
+ methodName,
120955
+ atMs: Date.now()
120956
+ });
120957
+ } catch {
120958
+ }
120959
+ }
120591
120960
  /** First child whose cap manifest contains a descriptor matching `predicate`. */
120592
120961
  findChildId(predicate) {
120593
120962
  for (const entry of this.children.values()) if (entry.caps.some(predicate)) return entry.childId;
@@ -120857,7 +121226,9 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
120857
121226
  const pinTargetsThisNode = pinnedNodeId !== void 0 && this.ownNodeId !== void 0 && pinnedNodeId === this.ownNodeId;
120858
121227
  const aggregated = pinnedNodeId === void 0 && this.isAggregatedCollectionMethod?.(out.capName, out.method) === true;
120859
121228
  const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, out.args) === true;
120860
- if ((!(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null) !== null) {
121229
+ const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
121230
+ this.recordCapUsage(childId, target, out.capName, out.method);
121231
+ if (target !== null) {
120861
121232
  if (!this.egressRoutedCaps.has(out.capName)) {
120862
121233
  this.egressRoutedCaps.add(out.capName);
120863
121234
  this.logger?.info("routed child egress over UDS", { capName: out.capName });
@@ -121714,77 +122085,6 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
121714
122085
  }
121715
122086
  return links;
121716
122087
  }
121717
- var CapUsageRegistry = class {
121718
- retentionSeconds;
121719
- map = /* @__PURE__ */ new Map();
121720
- constructor(opts) {
121721
- this.retentionSeconds = Math.max(1, opts?.retentionSeconds ?? 300);
121722
- }
121723
- recordCall(rec) {
121724
- try {
121725
- if (rec.callerAddonId === "" || rec.providerAddonId === "" || rec.capName === "") return;
121726
- if (!Number.isFinite(rec.atMs) || rec.atMs < 0) return;
121727
- let byProvider = this.map.get(rec.callerAddonId);
121728
- if (!byProvider) {
121729
- byProvider = /* @__PURE__ */ new Map();
121730
- this.map.set(rec.callerAddonId, byProvider);
121731
- }
121732
- let byCap = byProvider.get(rec.providerAddonId);
121733
- if (!byCap) {
121734
- byCap = /* @__PURE__ */ new Map();
121735
- byProvider.set(rec.providerAddonId, byCap);
121736
- }
121737
- let window2 = byCap.get(rec.capName);
121738
- if (!window2) {
121739
- window2 = {
121740
- buckets: Array.from({ length: this.retentionSeconds }, () => 0),
121741
- lastCallAtMs: 0
121742
- };
121743
- byCap.set(rec.capName, window2);
121744
- }
121745
- const bucketIdx = Math.floor(rec.atMs / 1e3) % this.retentionSeconds;
121746
- window2.buckets[bucketIdx] = (window2.buckets[bucketIdx] ?? 0) + 1;
121747
- if (rec.atMs > window2.lastCallAtMs) window2.lastCallAtMs = rec.atMs;
121748
- } catch {
121749
- }
121750
- }
121751
- getGraph(opts) {
121752
- const windowSeconds = Math.max(1, Math.min(opts.windowSeconds, this.retentionSeconds));
121753
- const minMs = opts.nowMs - windowSeconds * 1e3;
121754
- const out = [];
121755
- for (const [caller, byProvider] of this.map) for (const [provider, byCap] of byProvider) for (const [capName, window2] of byCap) {
121756
- if (window2.lastCallAtMs < minMs) continue;
121757
- const nowBucket = Math.floor(opts.nowMs / 1e3);
121758
- let count = 0;
121759
- for (let i = 0; i < windowSeconds; i++) {
121760
- const b = (nowBucket - i + this.retentionSeconds * 1e6) % this.retentionSeconds;
121761
- count += window2.buckets[b] ?? 0;
121762
- }
121763
- if (count === 0) continue;
121764
- const callsPerMin = count / windowSeconds * 60;
121765
- out.push({
121766
- callerAddonId: caller,
121767
- providerAddonId: provider,
121768
- capName,
121769
- callsPerMin,
121770
- lastCallAtMs: window2.lastCallAtMs
121771
- });
121772
- }
121773
- return out;
121774
- }
121775
- /** Test / diagnostic helper — drops all recorded calls. */
121776
- clear() {
121777
- this.map.clear();
121778
- }
121779
- };
121780
- var singleton = null;
121781
- function getCapUsageRegistry() {
121782
- if (!singleton) singleton = new CapUsageRegistry();
121783
- return singleton;
121784
- }
121785
- function __resetCapUsageRegistryForTests() {
121786
- singleton = null;
121787
- }
121788
122088
  function safeExistsSync(path) {
121789
122089
  try {
121790
122090
  return node_fs.existsSync(path);
@@ -123220,6 +123520,12 @@ var require_manifest_system_deps_8boi90D9 = __commonJS({
123220
123520
  return NATIVE_PROVIDER_SERVICE_INFIX;
123221
123521
  }
123222
123522
  });
123523
+ Object.defineProperty(exports, "PARENT_ROUTED_PROVIDER_ADDON_ID", {
123524
+ enumerable: true,
123525
+ get: function() {
123526
+ return PARENT_ROUTED_PROVIDER_ADDON_ID;
123527
+ }
123528
+ });
123223
123529
  Object.defineProperty(exports, "RSS_BUDGET_REANNOUNCE_MIN_MS", {
123224
123530
  enumerable: true,
123225
123531
  get: function() {
@@ -127634,7 +127940,7 @@ var require_dist3 = __commonJS({
127634
127940
  "use strict";
127635
127941
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
127636
127942
  var require_chunk = require_chunk_Cek0wNdY();
127637
- var require_dist10 = require_dist_Ck2jkBZk();
127943
+ var require_dist10 = require_dist_CcvXUhHK();
127638
127944
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
127639
127945
  require_alerts();
127640
127946
  var require_formatter = require_formatter_DqAKDlvN();
@@ -127660,7 +127966,7 @@ var require_dist3 = __commonJS({
127660
127966
  var require_builtins_winston_logging_index = require_winston_logging();
127661
127967
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
127662
127968
  var require_tls$1 = require_tls_BxQlomxd();
127663
- var require_manifest_system_deps = require_manifest_system_deps_8boi90D9();
127969
+ var require_manifest_system_deps = require_manifest_system_deps_oJMkWSX();
127664
127970
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
127665
127971
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
127666
127972
  var zod = require_zod();
@@ -208146,6 +208452,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
208146
208452
  exports.NativeMetricsProvider = require_builtins_native_metrics_native_metrics_addon.NativeMetricsProvider;
208147
208453
  exports.NetworkQualityTracker = NetworkQualityTracker;
208148
208454
  exports.NotificationService = NotificationService;
208455
+ exports.PARENT_ROUTED_PROVIDER_ADDON_ID = require_manifest_system_deps.PARENT_ROUTED_PROVIDER_ADDON_ID;
208149
208456
  Object.defineProperty(exports, "PYTHON_VERSION", {
208150
208457
  enumerable: true,
208151
208458
  get: function() {
@@ -209794,6 +210101,21 @@ var require_dist4 = __commonJS({
209794
210101
  bytesMoved: zod.z.number().int(),
209795
210102
  /** Total files discovered up front; null while (or when) unknown. */
209796
210103
  filesTotal: zod.z.number().int().nullable(),
210104
+ /**
210105
+ * Rows this run CORRECTED while moving them — a durable mutation the move
210106
+ * made that nobody asked for, so it is reported where the operator reads the
210107
+ * job rather than only in a log line.
210108
+ *
210109
+ * A footage segment records its byte count in its own NAME, and the durable
210110
+ * hour row derives its aggregates from those names. A file that does not
210111
+ * match its name therefore makes the ledger's sums — and with them quota and
210112
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
210113
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
210114
+ *
210115
+ * Absent on lanes where the question has no meaning: a media blob's size is
210116
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
210117
+ */
210118
+ rowsReconciled: zod.z.number().int().nonnegative().optional(),
209797
210119
  startedAt: zod.z.number(),
209798
210120
  finishedAt: zod.z.number().nullable(),
209799
210121
  error: zod.z.string().nullable()
@@ -209836,11 +210158,18 @@ var require_dist4 = __commonJS({
209836
210158
  /** Omitted = `move`, the pre-existing behaviour. */
209837
210159
  mode: MediaRelocateModeSchema.optional()
209838
210160
  });
209839
- var UnstampedEventMediaCountSchema = zod.z.object({
209840
- media: zod.z.number().int().nonnegative(),
209841
- retrainFrames: zod.z.number().int().nonnegative(),
209842
- total: zod.z.number().int().nonnegative()
210161
+ var UnstampedRowsSchema = zod.z.object({
210162
+ present: zod.z.boolean(),
210163
+ rows: zod.z.number().int().nonnegative().nullable()
209843
210164
  });
210165
+ var UnstampedEventMediaCountSchema = zod.z.object({
210166
+ media: UnstampedRowsSchema,
210167
+ retrainFrames: UnstampedRowsSchema,
210168
+ /** True when EITHER collection holds one. The refusal reads this. */
210169
+ anyPresent: zod.z.boolean(),
210170
+ /** Sum across both, or `null` when either lane could not be counted. */
210171
+ total: zod.z.number().int().nonnegative().nullable()
210172
+ }).nullable();
209844
210173
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: zod.z.string().min(1) });
209845
210174
  var StorageMigrationClassSchema = zod.z.enum([
209846
210175
  "recordings",
@@ -209887,6 +210216,10 @@ var require_dist4 = __commonJS({
209887
210216
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
209888
210217
  filesTotal: zod.z.number().int().nonnegative().nullable(),
209889
210218
  bytesMoved: zod.z.number().int().nonnegative(),
210219
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
210220
+ * a lane that cannot reconcile. A migration that silently rewrote durable
210221
+ * rows would be the same failure as one that silently skipped them. */
210222
+ rowsReconciled: zod.z.number().int().nonnegative().optional(),
209890
210223
  /** The MOVER's start, not the migration's: a drain restarted after an addon
209891
210224
  * crash gets a new mover, and a rate computed from the migration's start
209892
210225
  * would silently average in the time nothing was running. */
@@ -216211,6 +216544,15 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
216211
216544
  * calls are sync. Bindings change rarely (only on wrapper toggle or
216212
216545
  * device add/remove) — clients invalidate via the
216213
216546
  * `capability.binding-changed` event.
216547
+ *
216548
+ * "A single round-trip" describes the CLIENT's side and used not to
216549
+ * describe the server's: until 2026-08-30 the resolver read the persisted
216550
+ * wrapper activations once per device, so answering this cost one
216551
+ * settings-door RPC per device — 1 020 on the live 1 019-device hub, and
216552
+ * it did not return in 240 s against `SystemMirror.init`'s 15 s budget.
216553
+ * The server side is now two reads for the whole fleet. Anything PERIODIC
216554
+ * still belongs on `getBindings` / `getBindingsBatch` (D12); this remains
216555
+ * a warm seed.
216214
216556
  */
216215
216557
  getAllBindings: require_sleep.method(zod.z.object({}), zod.z.array(DeviceBindingsForDeviceSchema)),
216216
216558
  /**
@@ -222996,6 +223338,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
222996
223338
  * happens to stamp it. This count is what the migration planner's
222997
223339
  * non-blocking gate reads; `relocateMedia({ mode: 'seal' })` is what drives
222998
223340
  * it to zero.
223341
+ *
223342
+ * TWO indexed statements per collection, not a walk. It used to page the
223343
+ * whole collection at 200 rows per RPC ordered by an unindexed column, so
223344
+ * on the live hub — 1 254 576 rows — it hit the 60 s RPC deadline every
223345
+ * time it was called, and the migration it gates could never start. The
223346
+ * cheap question (`present`: is there at least one) is asked first and
223347
+ * separately from the expensive one (`rows`), because only the first has
223348
+ * to be answerable for the gate to do its job.
223349
+ *
223350
+ * **`null` is "not measurable", never zero** — at either level. An
223351
+ * unreadable collection must not read as a sealed one.
222999
223352
  */
223000
223353
  countUnstampedEventMedia: require_sleep.method(zod.z.object({}), UnstampedEventMediaCountSchema, { auth: "admin" }),
223001
223354
  /**
@@ -251323,6 +251676,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
251323
251676
  exports.UnifiedBrokerInfoSchema = BrokerInfoSchema$1;
251324
251677
  exports.UnitConversionError = UnitConversionError;
251325
251678
  exports.UnstampedEventMediaCountSchema = UnstampedEventMediaCountSchema;
251679
+ exports.UnstampedRowsSchema = UnstampedRowsSchema;
251326
251680
  exports.UpdateIntegrationInputSchema = UpdateIntegrationInputSchema;
251327
251681
  exports.UpdateStatusSchema = UpdateStatusSchema;
251328
251682
  exports.UpdateUserInputSchema = UpdateUserInputSchema;
@@ -385633,6 +385987,14 @@ var require_main2 = __commonJS({
385633
385987
  // cap). A pin to ANOTHER node still bypasses the sibling → onUnownedCall.
385634
385988
  ownNodeId: agentNodeId,
385635
385989
  onUnownedCall
385990
+ // NO `capUsageObserver` here, deliberately. `getCapUsageRegistry()` is a
385991
+ // per-process singleton and the reader (`nodes.getCapUsageGraph`) runs on
385992
+ // hub-main; recording in the agent would produce a map nobody ever reads,
385993
+ // which is exactly the shape that made this graph return `[]` for
385994
+ // months. An agent's addon traffic is a KNOWN blind spot of the graph —
385995
+ // see `LocalChildRegistryOptions.capUsageObserver`. Closing it needs the
385996
+ // caller identity to survive `$hub-cap-fwd`'s envelope, which today it
385997
+ // does not.
385636
385998
  });
385637
385999
  await agentUdsRegistry.start();
385638
386000
  agentUdsRegistry.onChildRegistered((child) => {
@@ -410443,8 +410805,30 @@ var require_cap_providers = __commonJS({
410443
410805
  }
410444
410806
  return result;
410445
410807
  },
410808
+ /**
410809
+ * Observed caller → provider → cap edges over the last `windowSeconds`.
410810
+ *
410811
+ * **Read the blind spots before drawing a conclusion.** This reads
410812
+ * hub-main's `CapUsageRegistry`, written by the hub's `LocalChildRegistry`
410813
+ * on every `cap-call-out` a hub-local forked addon sends. It therefore does
410814
+ * NOT contain: hub-main → hub-main in-process calls (one shared `ctx.api`
410815
+ * client, no per-caller identity), a runner's own co-located calls, or any
410816
+ * traffic originating on an agent. An edge missing here means "not seen on
410817
+ * this plane", never "did not happen" — the full list is on
410818
+ * `LocalChildRegistryOptions.capUsageObserver`.
410819
+ */
410446
410820
  getCapUsageGraph: async (input) => {
410447
410821
  const reg = (0, system_1.getCapUsageRegistry)();
410822
+ const stats = reg.getStats();
410823
+ if (stats.droppedTriples > 0) {
410824
+ logger?.warn("cap-usage graph is truncated \u2014 the triple ceiling was reached", {
410825
+ meta: {
410826
+ triples: stats.triples,
410827
+ maxTriples: stats.maxTriples,
410828
+ droppedTriples: stats.droppedTriples
410829
+ }
410830
+ });
410831
+ }
410448
410832
  return reg.getGraph({ windowSeconds: input.windowSeconds, nowMs: Date.now() });
410449
410833
  },
410450
410834
  setProcessLogLevel: async (input) => {
@@ -421469,6 +421853,14 @@ var require_moleculer_service = __commonJS({
421469
421853
  // below, so a method that survives 16 minutes across nodes survives it
421470
421854
  // across a socket too — and one that hangs is cut off on both.
421471
421855
  capTimeoutMs: (capName, method) => this.capabilityService.getRegistry()?.getDefinition(capName)?.methods?.[method]?.timeoutMs,
421856
+ // Feed the cap-usage graph from the one place that sees a forked
421857
+ // addon's outbound `ctx.api` traffic AND runs in the process that reads
421858
+ // it back (`nodes.getCapUsageGraph` → `cap-providers.ts`). The
421859
+ // runner-side observer in `addon-context-factory.ts` writes to the
421860
+ // RUNNER's own module singleton, which hub-main never sees — which is
421861
+ // why the graph answered `[]` on a hub running 64 addons. Blind spots
421862
+ // that remain are listed on `LocalChildRegistryOptions.capUsageObserver`.
421863
+ capUsageObserver: (obs) => (0, system_1.getCapUsageRegistry)().recordCall(obs),
421472
421864
  logger: {
421473
421865
  info: (msg, meta) => logger.info(msg, meta !== null && meta !== void 0 ? { meta } : void 0)
421474
421866
  },