@camstack/system 1.2.140 → 1.2.141

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.
Files changed (61) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-linked-devices.d.ts +4 -2
  16. package/dist/builtins/device-manager/device-manager.addon.js +210 -21
  17. package/dist/builtins/device-manager/device-manager.addon.mjs +210 -21
  18. package/dist/builtins/device-manager/device-meta-actions.d.ts +27 -0
  19. package/dist/builtins/device-manager/device-row-store.d.ts +21 -3
  20. package/dist/builtins/device-manager/fleet-read-census.d.ts +20 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +1 -1
  40. package/dist/builtins/snapshot/index.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/system-config/system-config.addon.js +1 -1
  48. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  49. package/dist/builtins/winston-logging/index.js +1 -1
  50. package/dist/builtins/winston-logging/index.mjs +1 -1
  51. package/dist/{dist-Clses4Bs.mjs → dist-6mn3LQvB.mjs} +2502 -2380
  52. package/dist/{dist-D5Jhh8Vk.js → dist-BNhvXrQk.js} +2502 -2380
  53. package/dist/index.js +2 -2
  54. package/dist/index.mjs +2 -2
  55. package/dist/kernel/transport/cap-route-resolver.d.ts +11 -1
  56. package/dist/kernel/transport/child-cap-protocol.d.ts +14 -0
  57. package/dist/{manifest-python-deps-Dxifi-o0.mjs → manifest-python-deps-D3zYlMEa.mjs} +35 -6
  58. package/dist/{manifest-python-deps-Bu8H1_rg.js → manifest-python-deps-DBQarwp9.js} +35 -6
  59. package/dist/{retired-settings-keys-Dflres-F.js → retired-settings-keys-DVgdWTd7.js} +1 -1
  60. package/dist/{retired-settings-keys-Cr8N6C6X.mjs → retired-settings-keys-SYwVv_Ym.mjs} +1 -1
  61. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { E as buildStreamParamsConfigSchema, Et as DeviceType, F as deviceStatusCapability, Gt as sleep, Kt as EventCategory, N as deviceManagerCapability, P as deviceStateCapability, R as enumerateItemArrayFields, Rt as isDeviceConfigCap, Tt as DeviceRole, a as CAP_NAMES_WITH_STATUS, bt as BaseAddon, et as normalizeUnit, kt as WELL_KNOWN_TAB_MAP, nt as parseStreamParamsFormPatch, ot as runtimeStatePolicyFor, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, wt as DeviceFeature, yt as errMsg, z as enumerateSchemaFields } from "../../dist-Clses4Bs.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Cr8N6C6X.mjs";
1
+ import { E as buildStreamParamsConfigSchema, Et as DeviceType, F as deviceStatusCapability, Gt as sleep, Kt as EventCategory, N as deviceManagerCapability, P as deviceStateCapability, R as enumerateItemArrayFields, Rt as isDeviceConfigCap, Tt as DeviceRole, a as CAP_NAMES_WITH_STATUS, bt as BaseAddon, et as normalizeUnit, kt as WELL_KNOWN_TAB_MAP, nt as parseStreamParamsFormPatch, ot as runtimeStatePolicyFor, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, wt as DeviceFeature, yt as errMsg, z as enumerateSchemaFields } from "../../dist-6mn3LQvB.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-SYwVv_Ym.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1293,6 +1293,20 @@ function resolveDeviceById(registry, deviceId) {
1293
1293
  }
1294
1294
  //#endregion
1295
1295
  //#region src/builtins/device-manager/device-queries.ts
1296
+ /**
1297
+ * The ORIGINATING addon id off a cap-channel call's `args`, or `undefined`.
1298
+ *
1299
+ * Not part of `IDeviceManagerProvider['listAll']`'s public Zod schema —
1300
+ * `CapRouteResolver`'s hub-in-process dispatch merges it into `args` as an
1301
+ * out-of-band field (mirrors how `nodeId` already rides inline in args on
1302
+ * this same path), so it never crosses the tRPC/HTTP boundary and never
1303
+ * reaches a browser client. Only ever present on a call the parent itself
1304
+ * routed from a registered UDS child, never client-suppliable.
1305
+ */
1306
+ function extractCallerAddonId(input) {
1307
+ const raw = Reflect.get(input, "callerAddonId");
1308
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
1309
+ }
1296
1310
  async function listPersistedByAddon(pctx, input) {
1297
1311
  const { addonId } = input;
1298
1312
  return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
@@ -1334,6 +1348,7 @@ async function readDeviceConfigs(pctx, deviceIds) {
1334
1348
  async function listAll(pctx, input) {
1335
1349
  const ownerFilter = Reflect.get(input, "addonId");
1336
1350
  if (ownerFilter !== void 0 && (typeof ownerFilter !== "string" || ownerFilter.length === 0)) throw new Error(`deviceManager.listAll: addonId must be a non-empty string or be omitted — got ${JSON.stringify(ownerFilter)}. An owner filter that is present but empty is never widened to every device. On an addon context the id is \`ctx.id\`; there is no \`ctx.addonId\`.`);
1351
+ const callerAddonId = pctx.metaStore.rows.censusArmed ? extractCallerAddonId(input) : void 0;
1337
1352
  const { addonId } = input;
1338
1353
  const slim = input.projection === "slim";
1339
1354
  const camerasOnly = input.isCamera === true;
@@ -1343,7 +1358,7 @@ async function listAll(pctx, input) {
1343
1358
  const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listFleet({
1344
1359
  ...input.deviceIds !== void 0 ? { deviceIds: input.deviceIds } : {},
1345
1360
  slim
1346
- });
1361
+ }, callerAddonId);
1347
1362
  const rowById = /* @__PURE__ */ new Map();
1348
1363
  for (const row of fleet) rowById.set(row.meta.id, row);
1349
1364
  if (pctx.registry) {
@@ -1864,21 +1879,58 @@ function parseLinkedDevicesConfig(blob) {
1864
1879
  };
1865
1880
  }
1866
1881
  /**
1882
+ * Resolve one candidate id to the ids it should actually contribute to a
1883
+ * linked set: itself, unless it is a CONTAINER, in which case its own
1884
+ * children (recursively expanded the same way — a container-of-containers
1885
+ * is vanishingly unlikely per `DeviceType.Container`'s contract, but this
1886
+ * costs nothing to handle). `visited` guards a pathological parent-cycle in
1887
+ * persisted data from recursing forever; a candidate already on the current
1888
+ * expansion path is dropped rather than re-expanded.
1889
+ *
1890
+ * This is the STRUCTURAL, location-independent way a container's child
1891
+ * becomes reachable: the device-tree "children of the camera" rule and a
1892
+ * persisted manual pick are parentDeviceId chains, never location-based —
1893
+ * a container named there is excluded by expanding into whatever it
1894
+ * actually contains, regardless of what its (or its children's) `location`
1895
+ * column holds.
1896
+ */
1897
+ function expandContainer(id, byId, byParent, visited, out) {
1898
+ if (visited.has(id)) return;
1899
+ const candidate = byId.get(id);
1900
+ if (!candidate) return;
1901
+ if (candidate.type !== DeviceType.Container) {
1902
+ out.add(id);
1903
+ return;
1904
+ }
1905
+ const nextVisited = new Set(visited);
1906
+ nextVisited.add(id);
1907
+ const children = byParent.get(id) ?? [];
1908
+ for (const child of children) expandContainer(child.id, byId, byParent, nextVisited, out);
1909
+ }
1910
+ /**
1867
1911
  * Pure resolution core: children ∪ (auto → same-location | manual → list),
1868
- * minus self, deduped. Order: children first, then mode additions.
1912
+ * minus self, deduped, CONTAINERS expanded to their children (see file
1913
+ * header). Order: children first, then mode additions.
1869
1914
  */
1870
1915
  function resolveLinkedDeviceIds(params) {
1871
1916
  const { selfId, selfLocation, config, candidates } = params;
1917
+ const byId = new Map(candidates.map((c) => [c.id, c]));
1918
+ const byParent = /* @__PURE__ */ new Map();
1919
+ for (const c of candidates) {
1920
+ if (c.parentDeviceId === null) continue;
1921
+ const siblings = byParent.get(c.parentDeviceId);
1922
+ if (siblings) siblings.push(c);
1923
+ else byParent.set(c.parentDeviceId, [c]);
1924
+ }
1872
1925
  const ids = /* @__PURE__ */ new Set();
1873
- for (const c of candidates) if (c.parentDeviceId === selfId) ids.add(c.id);
1926
+ const expand = (id) => expandContainer(id, byId, byParent, /* @__PURE__ */ new Set(), ids);
1927
+ for (const c of candidates) if (c.parentDeviceId === selfId) expand(c.id);
1874
1928
  if (config.mode === "auto") {
1875
- if (selfLocation !== null && selfLocation.length > 0) {
1876
- for (const c of candidates) if (c.location === selfLocation) ids.add(c.id);
1929
+ if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
1930
+ if (c.type === DeviceType.Container) continue;
1931
+ if (c.location === selfLocation) ids.add(c.id);
1877
1932
  }
1878
- } else {
1879
- const known = new Set(candidates.map((c) => c.id));
1880
- for (const id of config.manualIds) if (known.has(id)) ids.add(id);
1881
- }
1933
+ } else for (const id of config.manualIds) if (byId.has(id)) expand(id);
1882
1934
  ids.delete(selfId);
1883
1935
  return [...ids];
1884
1936
  }
@@ -2002,7 +2054,7 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2002
2054
  if (!persisted || persisted.meta.type !== DeviceType.Camera) return null;
2003
2055
  const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
2004
2056
  const config = parseLinkedDevicesConfig(blob);
2005
- const options = all.filter((d) => d.id !== deviceId).map((d) => ({
2057
+ const options = all.filter((d) => d.id !== deviceId && d.type !== DeviceType.Container).map((d) => ({
2006
2058
  value: String(d.id),
2007
2059
  label: d.location !== null ? `${d.name} (${d.location})` : d.name
2008
2060
  }));
@@ -3338,16 +3390,83 @@ async function setName(pctx, input) {
3338
3390
  });
3339
3391
  }
3340
3392
  /**
3393
+ * Every descendant of `rootId`, walked via `parentDeviceId` chains
3394
+ * (BFS, `listByParent` per level — the same indexed lookup `setName`'s
3395
+ * rename cascade uses one level of). `visited` seeded with `rootId` guards a
3396
+ * pathological parent-cycle in persisted data from recursing forever; a row
3397
+ * already seen is never re-queued or re-emitted. Depth is unbounded on
3398
+ * purpose — a nested container's own children are still `rootId`'s
3399
+ * descendants and must align too.
3400
+ */
3401
+ async function collectDescendants(metaStore, rootId) {
3402
+ const out = [];
3403
+ const visited = new Set([rootId]);
3404
+ const queue = [rootId];
3405
+ while (queue.length > 0) {
3406
+ const parentId = queue.shift();
3407
+ if (parentId === void 0) break;
3408
+ for (const row of await metaStore.rows.listByParent(parentId)) {
3409
+ if (visited.has(row.meta.id)) continue;
3410
+ visited.add(row.meta.id);
3411
+ out.push(row.meta);
3412
+ queue.push(row.meta.id);
3413
+ }
3414
+ }
3415
+ return out;
3416
+ }
3417
+ /**
3341
3418
  * Update the operator-organisational location label. `null`
3342
3419
  * clears it. Mirrors the same persist-then-emit shape as
3343
3420
  * `setName`; consumers subscribe to `DeviceMetaChanged` and
3344
3421
  * filter on `field: 'location'`.
3422
+ *
3423
+ * CASCADE (operator decision, 2026-08-26 — supersedes the read-time
3424
+ * inheritance `device-linked-devices.ts` used to resolve): when `deviceId`
3425
+ * is a `DeviceType.Container`, the SAME location is written to every
3426
+ * descendant's own `location` column, recursively, in this exact call —
3427
+ * never resolved live by a reader again. This DELIBERATELY overwrites a
3428
+ * descendant's own, possibly different, location: a cascade is a write, and
3429
+ * a write that stops at an existing value is not a cascade — the operator
3430
+ * setting the container's location is understood as organising the whole
3431
+ * subtree by it. Only a CONTAINER cascades; a non-container device's
3432
+ * location write is a single row, same as before.
3433
+ *
3434
+ * RETROACTIVITY: this only runs when `setLocation` is called. A container
3435
+ * that already had a location before this shipped does NOT retroactively
3436
+ * align its descendants — they stay unaligned until the operator re-saves
3437
+ * the container's location.
3438
+ *
3439
+ * ATOMICITY: the whole cascade runs inside ONE `withMetaWriteLock` critical
3440
+ * section, so no concurrent writer can interleave a read or a write into the
3441
+ * middle of it — but it is still N separate row writes, not one transaction.
3442
+ * A row write that throws does NOT abort the cascade (a swallowed continue,
3443
+ * not a silent one): it is collected as a failure, every other descendant is
3444
+ * still attempted, and if any failed the outcome is logged at `error` with
3445
+ * every failing id — never indistinguishable from a clean cascade. A
3446
+ * `DeviceMetaChanged` event fires for the container itself and for every
3447
+ * descendant actually written (never for one that failed), so linked-device
3448
+ * consumers (auto-link) see exactly what landed.
3345
3449
  */
3346
3450
  async function setLocation(pctx, input) {
3347
3451
  const { deviceId, location } = input;
3452
+ const cascaded = [];
3453
+ const failed = [];
3348
3454
  await pctx.metaStore.withMetaWriteLock(async () => {
3349
- if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
3455
+ const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
3456
+ if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
3350
3457
  await pctx.metaStore.rows.patch(deviceId, { location });
3458
+ if (persisted.meta.type === DeviceType.Container) {
3459
+ const descendants = await collectDescendants(pctx.metaStore, deviceId);
3460
+ for (const descendant of descendants) try {
3461
+ await pctx.metaStore.rows.patch(descendant.id, { location });
3462
+ cascaded.push(descendant.id);
3463
+ } catch (err) {
3464
+ failed.push({
3465
+ id: descendant.id,
3466
+ error: err
3467
+ });
3468
+ }
3469
+ }
3351
3470
  });
3352
3471
  pctx.host.ctx.eventBus.emit({
3353
3472
  id: randomUUID(),
@@ -3363,6 +3482,36 @@ async function setLocation(pctx, input) {
3363
3482
  value: location
3364
3483
  }
3365
3484
  });
3485
+ for (const childId of cascaded) pctx.host.ctx.eventBus.emit({
3486
+ id: randomUUID(),
3487
+ timestamp: /* @__PURE__ */ new Date(),
3488
+ source: {
3489
+ type: "device",
3490
+ id: childId
3491
+ },
3492
+ category: EventCategory.DeviceMetaChanged,
3493
+ data: {
3494
+ deviceId: childId,
3495
+ field: "location",
3496
+ value: location
3497
+ }
3498
+ });
3499
+ if (cascaded.length > 0 || failed.length > 0) pctx.host.ctx.logger.info("setLocation: cascaded location to container descendants", {
3500
+ tags: { deviceId },
3501
+ meta: {
3502
+ location,
3503
+ updated: cascaded.length,
3504
+ failed: failed.length
3505
+ }
3506
+ });
3507
+ if (failed.length > 0) pctx.host.ctx.logger.error("setLocation: cascade partially failed — some descendants NOT aligned to the new location", {
3508
+ tags: { deviceId },
3509
+ meta: {
3510
+ location,
3511
+ failedIds: failed.map((f) => f.id),
3512
+ errors: failed.map((f) => f.error instanceof Error ? f.error.message : String(f.error))
3513
+ }
3514
+ });
3366
3515
  }
3367
3516
  /**
3368
3517
  * Update the device type. Writes the meta row, emits a
@@ -3995,9 +4144,29 @@ var FleetReadCensus = class FleetReadCensus {
3995
4144
  * to it even if it lands after the close — otherwise the slow reads at the
3996
4145
  * edge would be attributed to nobody, and the slow ones are the interesting
3997
4146
  * ones.
4147
+ *
4148
+ * `callerAddonId`, when given, is formatted as the site VERBATIM instead of
4149
+ * walking the local stack. A call that arrived over the cap channel — a
4150
+ * forked addon's `ctx.api.deviceManager.listAll.query()`, routed through
4151
+ * `LocalChildRegistry` → `onUnownedCall` → `CapRouteResolver` — carries its
4152
+ * origin as DATA (the registered `childId` the parent captured off the UDS
4153
+ * connection), and no amount of stack depth would ever recover it: every
4154
+ * frame between the socket read and this call is the SAME generic
4155
+ * dispatcher code (`onUnownedCall`, `dispatch`, `invoke`) regardless of
4156
+ * which addon called — the 2026-08-25 boot census measured this directly,
4157
+ * five thousand calls landing on `listAll` with nothing nameable above it.
4158
+ *
4159
+ * `isOpen` is checked FIRST, before the id is even looked at — a caller
4160
+ * threading a (cheap, already-known) `callerAddonId` through on every
4161
+ * request, armed or not, must not pay for the template string when
4162
+ * disarmed. This is also why formatting lives HERE rather than at the call
4163
+ * site: `DeviceRowStore` passes the raw id through unconditionally, and
4164
+ * only this method — which already knows whether armed — decides whether
4165
+ * to spend anything on it.
3998
4166
  */
3999
- siteOf(nowMs = Date.now()) {
4167
+ siteOf(callerAddonId, nowMs = Date.now()) {
4000
4168
  if (!this.isOpen(nowMs)) return null;
4169
+ if (callerAddonId !== void 0) return `addon:${callerAddonId}`;
4001
4170
  const holder = {};
4002
4171
  Error.captureStackTrace(holder, this.siteOf);
4003
4172
  return siteFromStack(holder.stack);
@@ -4382,6 +4551,16 @@ var DeviceRowStore = class {
4382
4551
  /** Call-site attribution for fleet reads. Armed only by
4383
4552
  * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
4384
4553
  census;
4554
+ /**
4555
+ * Is the fleet-read census armed right now? Exposed so a caller ABOVE this
4556
+ * store (`device-queries.ts`) can skip extracting a caller identity off its
4557
+ * input entirely when nobody is measuring — "the boolean field read first"
4558
+ * pattern this repo already uses elsewhere to keep a diagnostic seam at
4559
+ * zero cost while disarmed.
4560
+ */
4561
+ get censusArmed() {
4562
+ return this.census.armed;
4563
+ }
4385
4564
  constructor(backend, logger, census) {
4386
4565
  this.backend = backend;
4387
4566
  this.logger = logger;
@@ -4430,15 +4609,23 @@ var DeviceRowStore = class {
4430
4609
  }
4431
4610
  return decoded;
4432
4611
  }
4433
- /** Every device, ordered by numeric id. */
4434
- async listAll() {
4612
+ /**
4613
+ * Every device, ordered by numeric id.
4614
+ *
4615
+ * `callerAddonId`, when known, is the addon that ORIGINATED this call —
4616
+ * captured off the cap-channel envelope (`CapCallInput.callerAddonId`, set
4617
+ * by `LocalChildRegistry` from the registered `childId`), never guessed.
4618
+ * Passed straight to the census; a stack walk can never name it (see
4619
+ * `fleet-read-census.ts#siteOf`).
4620
+ */
4621
+ async listAll(callerAddonId) {
4435
4622
  return this.list({
4436
4623
  orderBy: {
4437
4624
  field: "deviceId",
4438
4625
  direction: "asc"
4439
4626
  },
4440
4627
  limit: DEVICE_ROWS_FLEET_LIMIT
4441
- });
4628
+ }, void 0, callerAddonId);
4442
4629
  }
4443
4630
  /**
4444
4631
  * The fleet, read as narrowly as the question allows.
@@ -4455,8 +4642,10 @@ var DeviceRowStore = class {
4455
4642
  *
4456
4643
  * `isCamera` is deliberately NOT one of them — see that constant's note and
4457
4644
  * `device-row-store-filtered.spec.ts`.
4645
+ *
4646
+ * `callerAddonId` — see {@link listAll}'s doc; same meaning here.
4458
4647
  */
4459
- async listFleet(opts) {
4648
+ async listFleet(opts, callerAddonId) {
4460
4649
  if (opts.deviceIds !== void 0 && opts.deviceIds.length === 0) return [];
4461
4650
  return this.list({
4462
4651
  ...opts.deviceIds !== void 0 ? { whereIn: { deviceId: [...new Set(opts.deviceIds)] } } : {},
@@ -4465,7 +4654,7 @@ var DeviceRowStore = class {
4465
4654
  direction: "asc"
4466
4655
  },
4467
4656
  limit: DEVICE_ROWS_FLEET_LIMIT
4468
- }, opts.slim === true ? DEVICE_ROWS_SLIM_COLUMNS : void 0);
4657
+ }, opts.slim === true ? DEVICE_ROWS_SLIM_COLUMNS : void 0, callerAddonId);
4469
4658
  }
4470
4659
  /**
4471
4660
  * Many devices, by numeric id, in ONE query.
@@ -4556,8 +4745,8 @@ var DeviceRowStore = class {
4556
4745
  await this.declare();
4557
4746
  return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
4558
4747
  }
4559
- async list(filter, columns) {
4560
- const site = this.census.siteOf();
4748
+ async list(filter, columns, callerAddonId) {
4749
+ const site = this.census.siteOf(callerAddonId);
4561
4750
  await this.declare();
4562
4751
  const records = await this.backend.query({
4563
4752
  collection: DEVICE_ROWS_COLLECTION,
@@ -43,6 +43,33 @@ export declare function setName(pctx: ProviderContext, input: Parameters<IDevice
43
43
  * clears it. Mirrors the same persist-then-emit shape as
44
44
  * `setName`; consumers subscribe to `DeviceMetaChanged` and
45
45
  * filter on `field: 'location'`.
46
+ *
47
+ * CASCADE (operator decision, 2026-08-26 — supersedes the read-time
48
+ * inheritance `device-linked-devices.ts` used to resolve): when `deviceId`
49
+ * is a `DeviceType.Container`, the SAME location is written to every
50
+ * descendant's own `location` column, recursively, in this exact call —
51
+ * never resolved live by a reader again. This DELIBERATELY overwrites a
52
+ * descendant's own, possibly different, location: a cascade is a write, and
53
+ * a write that stops at an existing value is not a cascade — the operator
54
+ * setting the container's location is understood as organising the whole
55
+ * subtree by it. Only a CONTAINER cascades; a non-container device's
56
+ * location write is a single row, same as before.
57
+ *
58
+ * RETROACTIVITY: this only runs when `setLocation` is called. A container
59
+ * that already had a location before this shipped does NOT retroactively
60
+ * align its descendants — they stay unaligned until the operator re-saves
61
+ * the container's location.
62
+ *
63
+ * ATOMICITY: the whole cascade runs inside ONE `withMetaWriteLock` critical
64
+ * section, so no concurrent writer can interleave a read or a write into the
65
+ * middle of it — but it is still N separate row writes, not one transaction.
66
+ * A row write that throws does NOT abort the cascade (a swallowed continue,
67
+ * not a silent one): it is collected as a failure, every other descendant is
68
+ * still attempted, and if any failed the outcome is logged at `error` with
69
+ * every failing id — never indistinguishable from a clean cascade. A
70
+ * `DeviceMetaChanged` event fires for the container itself and for every
71
+ * descendant actually written (never for one that failed), so linked-device
72
+ * consumers (auto-link) see exactly what landed.
46
73
  */
47
74
  export declare function setLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocation']>[0]): ReturnType<IDeviceManagerProvider['setLocation']>;
48
75
  /**
@@ -215,6 +215,14 @@ export declare class DeviceRowStore {
215
215
  /** Call-site attribution for fleet reads. Armed only by
216
216
  * `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
217
217
  private readonly census;
218
+ /**
219
+ * Is the fleet-read census armed right now? Exposed so a caller ABOVE this
220
+ * store (`device-queries.ts`) can skip extracting a caller identity off its
221
+ * input entirely when nobody is measuring — "the boolean field read first"
222
+ * pattern this repo already uses elsewhere to keep a diagnostic seam at
223
+ * zero cost while disarmed.
224
+ */
225
+ get censusArmed(): boolean;
218
226
  constructor(backend: DeviceRowBackend, logger: IScopedLogger, census?: FleetReadCensus);
219
227
  /**
220
228
  * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
@@ -225,8 +233,16 @@ export declare class DeviceRowStore {
225
233
  declare(): Promise<void>;
226
234
  /** One device, by numeric id. `null` when the fleet does not know it. */
227
235
  get(deviceId: number): Promise<DeviceRow | null>;
228
- /** Every device, ordered by numeric id. */
229
- listAll(): Promise<readonly DeviceRow[]>;
236
+ /**
237
+ * Every device, ordered by numeric id.
238
+ *
239
+ * `callerAddonId`, when known, is the addon that ORIGINATED this call —
240
+ * captured off the cap-channel envelope (`CapCallInput.callerAddonId`, set
241
+ * by `LocalChildRegistry` from the registered `childId`), never guessed.
242
+ * Passed straight to the census; a stack walk can never name it (see
243
+ * `fleet-read-census.ts#siteOf`).
244
+ */
245
+ listAll(callerAddonId?: string): Promise<readonly DeviceRow[]>;
230
246
  /**
231
247
  * The fleet, read as narrowly as the question allows.
232
248
  *
@@ -242,11 +258,13 @@ export declare class DeviceRowStore {
242
258
  *
243
259
  * `isCamera` is deliberately NOT one of them — see that constant's note and
244
260
  * `device-row-store-filtered.spec.ts`.
261
+ *
262
+ * `callerAddonId` — see {@link listAll}'s doc; same meaning here.
245
263
  */
246
264
  listFleet(opts: {
247
265
  readonly deviceIds?: readonly number[];
248
266
  readonly slim?: boolean;
249
- }): Promise<readonly DeviceRow[]>;
267
+ }, callerAddonId?: string): Promise<readonly DeviceRow[]>;
250
268
  /**
251
269
  * Many devices, by numeric id, in ONE query.
252
270
  *
@@ -31,8 +31,27 @@ export declare class FleetReadCensus {
31
31
  * to it even if it lands after the close — otherwise the slow reads at the
32
32
  * edge would be attributed to nobody, and the slow ones are the interesting
33
33
  * ones.
34
+ *
35
+ * `callerAddonId`, when given, is formatted as the site VERBATIM instead of
36
+ * walking the local stack. A call that arrived over the cap channel — a
37
+ * forked addon's `ctx.api.deviceManager.listAll.query()`, routed through
38
+ * `LocalChildRegistry` → `onUnownedCall` → `CapRouteResolver` — carries its
39
+ * origin as DATA (the registered `childId` the parent captured off the UDS
40
+ * connection), and no amount of stack depth would ever recover it: every
41
+ * frame between the socket read and this call is the SAME generic
42
+ * dispatcher code (`onUnownedCall`, `dispatch`, `invoke`) regardless of
43
+ * which addon called — the 2026-08-25 boot census measured this directly,
44
+ * five thousand calls landing on `listAll` with nothing nameable above it.
45
+ *
46
+ * `isOpen` is checked FIRST, before the id is even looked at — a caller
47
+ * threading a (cheap, already-known) `callerAddonId` through on every
48
+ * request, armed or not, must not pay for the template string when
49
+ * disarmed. This is also why formatting lives HERE rather than at the call
50
+ * site: `DeviceRowStore` passes the raw id through unconditionally, and
51
+ * only this method — which already knows whether armed — decides whether
52
+ * to spend anything on it.
34
53
  */
35
- siteOf(nowMs?: number): string | null;
54
+ siteOf(callerAddonId?: string, nowMs?: number): string | null;
36
55
  /** Count one read at an already-captured site. A `null` site is not counted. */
37
56
  record(site: string | null, rowCount: number): void;
38
57
  /** Attributed sites, busiest first. */
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { Et as DeviceType, G as isSameAddonId, I as doorbellCapability, Kt as EventCategory, Nt as createEvent, bt as BaseAddon, w as bareAddonId, yt as errMsg } from "../../dist-Clses4Bs.mjs";
1
+ import { Et as DeviceType, G as isSameAddonId, I as doorbellCapability, Kt as EventCategory, Nt as createEvent, bt as BaseAddon, w as bareAddonId, yt as errMsg } from "../../dist-6mn3LQvB.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, bt as BaseAddon } from "../../dist-Clses4Bs.mjs";
1
+ import { X as logDestinationCapability, bt as BaseAddon } from "../../dist-6mn3LQvB.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D5Jhh8Vk.js");
3
+ const require_dist = require("../../dist-BNhvXrQk.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Kt as EventCategory, Nt as createEvent, bt as BaseAddon, yt as errMsg } from "../../dist-Clses4Bs.mjs";
1
+ import { Kt as EventCategory, Nt as createEvent, bt as BaseAddon, yt as errMsg } from "../../dist-6mn3LQvB.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { S as authProviderCapability, _ as UserRecordSchema, bt as BaseAddon, gt as userManagementCapability, m as ScopedTokenSchema, r as ApiKeyRecordSchema } from "../../dist-Clses4Bs.mjs";
2
+ import { S as authProviderCapability, _ as UserRecordSchema, bt as BaseAddon, gt as userManagementCapability, m as ScopedTokenSchema, r as ApiKeyRecordSchema } from "../../dist-6mn3LQvB.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D5Jhh8Vk.js");
3
+ const require_dist = require("../../dist-BNhvXrQk.js");
4
4
  const require_tls = require("../../tls-u8QCJCFE.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Kt as EventCategory, Y as localNetworkCapability, bt as BaseAddon } from "../../dist-Clses4Bs.mjs";
1
+ import { Kt as EventCategory, Y as localNetworkCapability, bt as BaseAddon } from "../../dist-6mn3LQvB.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-CQhPGSJm.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, bt as BaseAddon } from "../../dist-Clses4Bs.mjs";
1
+ import { X as logDestinationCapability, bt as BaseAddon } from "../../dist-6mn3LQvB.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,4 +1,4 @@
1
- import { $ as metricsProviderCapability, Kt as EventCategory, Nt as createEvent, bt as BaseAddon } from "../../dist-Clses4Bs.mjs";
1
+ import { $ as metricsProviderCapability, Kt as EventCategory, Nt as createEvent, bt as BaseAddon } from "../../dist-6mn3LQvB.mjs";
2
2
  import { execFile, execFileSync } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_os = require("node:os");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Ft as emitReadiness, Kt as EventCategory, L as enumerateInferenceDevices, bt as BaseAddon, lt as scoreRuntimes, rt as platformProbeCapability, yt as errMsg } from "../../dist-Clses4Bs.mjs";
2
+ import { Ft as emitReadiness, Kt as EventCategory, L as enumerateInferenceDevices, bt as BaseAddon, lt as scoreRuntimes, rt as platformProbeCapability, yt as errMsg } from "../../dist-6mn3LQvB.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as os from "node:os";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Kt as EventCategory, bt as BaseAddon } from "../../dist-Clses4Bs.mjs";
1
+ import { Kt as EventCategory, bt as BaseAddon } from "../../dist-6mn3LQvB.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  let zod = require("zod");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
@@ -1,4 +1,4 @@
1
- import { Et as DeviceType, Kt as EventCategory, M as deriveBatteryPresence, T as batteryCapability, bt as BaseAddon, dt as snapshotCapability, ht as streamQualityLabel, i as BatteryStatusSchema, w as bareAddonId, wt as DeviceFeature, yt as errMsg, zt as nodePin } from "../../dist-Clses4Bs.mjs";
1
+ import { Et as DeviceType, Kt as EventCategory, M as deriveBatteryPresence, T as batteryCapability, bt as BaseAddon, dt as snapshotCapability, ht as streamQualityLabel, i as BatteryStatusSchema, w as bareAddonId, wt as DeviceFeature, yt as errMsg, zt as nodePin } from "../../dist-6mn3LQvB.mjs";
2
2
  import { z } from "zod";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D5Jhh8Vk.js");
6
+ const require_dist = require("../../dist-BNhvXrQk.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");