camstack 1.2.48 → 1.2.50

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-D3lqzV40.js
23637
- var require_dist_D3lqzV40 = __commonJS({
23638
- "../system/dist/dist-D3lqzV40.js"(exports) {
23636
+ // ../system/dist/dist-BVU5JADq.js
23637
+ var require_dist_BVU5JADq = __commonJS({
23638
+ "../system/dist/dist-BVU5JADq.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -25904,6 +25904,46 @@ var require_dist_D3lqzV40 = __commonJS({
25904
25904
  function logLevelAtMost(level, threshold) {
25905
25905
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
25906
25906
  }
25907
+ var LogChannelLevelSchema = zod.z.enum([
25908
+ "info",
25909
+ "warn",
25910
+ "error"
25911
+ ]);
25912
+ var LogChannelDescriptorSchema = zod.z.object({
25913
+ /**
25914
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
25915
+ * the addon's short name so an operator reading a channel list can tell who
25916
+ * owns it without a second lookup.
25917
+ */
25918
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
25919
+ /** One sentence: what the operator will SEE after arming it. */
25920
+ description: zod.z.string().min(1),
25921
+ /** The level its lines are emitted at. Never below `info`. */
25922
+ defaultLevel: LogChannelLevelSchema,
25923
+ /**
25924
+ * Whether this channel can be narrowed to a camera.
25925
+ *
25926
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
25927
+ * consulted with the numeric device id, AND every line the channel admits
25928
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
25929
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
25930
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
25931
+ * the body is the only way to filter.
25932
+ *
25933
+ * A channel whose lines carry the device only in `meta` (or not at all) is
25934
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
25935
+ * the operator narrows to one camera, sees nothing, and concludes the code
25936
+ * path was never taken.
25937
+ */
25938
+ perDevice: zod.z.boolean()
25939
+ });
25940
+ var LogChannelWindowSchema = zod.z.object({
25941
+ channel: zod.z.string().min(1),
25942
+ /** Epoch ms the window closes at. */
25943
+ armedUntilMs: zod.z.number(),
25944
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
25945
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
25946
+ });
25907
25947
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
25908
25948
  var OpsLogOpSchema = zod.z.enum([
25909
25949
  "prune",
@@ -29461,6 +29501,21 @@ var require_dist_D3lqzV40 = __commonJS({
29461
29501
  whereBetween: zod.z.record(zod.z.string(), zod.z.tuple([zod.z.unknown(), zod.z.unknown()])).optional(),
29462
29502
  whereNot: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
29463
29503
  });
29504
+ var AggregateFieldSchema = zod.z.object({
29505
+ /** Result key. */
29506
+ as: zod.z.string().min(1),
29507
+ /** Column to aggregate. Must be a real column of a declared collection. */
29508
+ field: zod.z.string().min(1),
29509
+ op: zod.z.enum([
29510
+ "sum",
29511
+ "min",
29512
+ "max"
29513
+ ])
29514
+ });
29515
+ var AggregateResultSchema = zod.z.object({
29516
+ count: zod.z.number().int(),
29517
+ values: zod.z.record(zod.z.string(), zod.z.number().nullable())
29518
+ });
29464
29519
  var SettingsRecordSchema = zod.z.object({
29465
29520
  id: zod.z.string(),
29466
29521
  data: zod.z.record(zod.z.string(), zod.z.unknown())
@@ -29588,6 +29643,32 @@ var require_dist_D3lqzV40 = __commonJS({
29588
29643
  collection: zod.z.string(),
29589
29644
  filter: QueryFilterSchema.optional()
29590
29645
  }), zod.z.number()),
29646
+ /**
29647
+ * `COUNT(*)` and one `SUM` / `MIN` / `MAX` per requested field, in ONE
29648
+ * statement, over the rows `filter` selects.
29649
+ *
29650
+ * Exists because "how much is there" was being answered by materialising
29651
+ * "what is there". The recorder's storage-pressure sweep asked its in-RAM
29652
+ * footage index for bytes/count/oldest/newest across a set of storage
29653
+ * locations twice a minute, and the only way to answer that from a map is
29654
+ * to visit every row — 7.1 M of them on the live hub, ~15 M row visits a
29655
+ * minute on the main thread, which is also why the whole archive had to
29656
+ * stay resident to be visited. The question is a sum; nothing needs to be
29657
+ * materialised to answer it.
29658
+ *
29659
+ * **The engine REFUSES a field it cannot serve**, exactly as
29660
+ * `query.columns` does and unlike a PREDICATE, which is skipped when
29661
+ * unresolvable. A dropped predicate over-matches and the caller sees extra
29662
+ * rows; a dropped aggregate returns a NUMBER that is wrong and looks
29663
+ * exactly like a real one. That asymmetry is what this repo has already
29664
+ * paid for once in `count`.
29665
+ */
29666
+ aggregate: method(zod.z.object({
29667
+ namespace: zod.z.string().optional(),
29668
+ collection: zod.z.string(),
29669
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
29670
+ filter: QueryFilterSchema.optional()
29671
+ }), AggregateResultSchema),
29591
29672
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
29592
29673
  histogram: method(zod.z.object({
29593
29674
  namespace: zod.z.string().optional(),
@@ -29746,6 +29827,15 @@ var require_dist_D3lqzV40 = __commonJS({
29746
29827
  collection: zod.z.string(),
29747
29828
  filter: QueryFilterSchema.optional()
29748
29829
  }), zod.z.number(), { auth: "admin" }),
29830
+ /** `COUNT(*)` plus one SUM/MIN/MAX per field, in one statement. Mirror of
29831
+ * `settings-store.aggregate` — see it for why an unresolvable field is
29832
+ * refused rather than dropped. */
29833
+ aggregate: method(zod.z.object({
29834
+ namespace: zod.z.string().optional(),
29835
+ collection: zod.z.string(),
29836
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
29837
+ filter: QueryFilterSchema.optional()
29838
+ }), AggregateResultSchema, { auth: "admin" }),
29749
29839
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
29750
29840
  histogram: method(zod.z.object({
29751
29841
  namespace: zod.z.string().optional(),
@@ -30857,6 +30947,39 @@ var require_dist_D3lqzV40 = __commonJS({
30857
30947
  /** List children of a parent device (by parent numeric id). */
30858
30948
  getChildren: method(zod.z.object({ parentDeviceId: zod.z.number() }), zod.z.array(DeviceInfoSchema)),
30859
30949
  /**
30950
+ * `getChildren` for a NAMED SET of parents, in one call.
30951
+ *
30952
+ * The accessory reconcile in `device-cap-proxy.ts` asks this question once
30953
+ * per registered device — every `BaseDevice` inherits a
30954
+ * `getAccessoryChildren()` that returns `[]`, so even a leaf accessory
30955
+ * pays a round-trip to learn it has nothing to prune. Measured on the live
30956
+ * hub 2026-08-27 over a 120-second boot window, fleet of 1 017 devices:
30957
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
30958
+ * **1 024 calls** returning **919 rows in total** — 1 024 RPCs and 1 024
30959
+ * indexed scans to move less than one row each. `listByParentMany`
30960
+ * collapses the scans; this collapses the RPCs.
30961
+ *
30962
+ * Keyed by parent id as a STRING — a JSON object cannot key by number
30963
+ * (same reason as `getDeviceStatusAggregateBatch`). The per-parent value
30964
+ * is exactly what `getChildren` returns for that parent.
30965
+ *
30966
+ * A parent with no children — or one the fleet does not know — is ABSENT
30967
+ * from the record, never an invented empty row: the same contract as
30968
+ * `DeviceRowStore.getMany`/`listByParentMany`. An EMPTY `parentDeviceIds`
30969
+ * reads nothing at all rather than degrading to "every device".
30970
+ *
30971
+ * `parentDeviceIds` is capped at {@link DEVICE_CHILDREN_BATCH_MAX} — see
30972
+ * that constant for why. A caller with more parents than that sends more
30973
+ * than one call; it never sends one pathological one.
30974
+ *
30975
+ * Version skew: this is a NEW method, not a new field on `getChildren`, so
30976
+ * a hub that predates it answers NOT_FOUND rather than silently stripping
30977
+ * an unknown input key and answering a DIFFERENT question. The kernel-side
30978
+ * loader degrades to per-parent `getChildren` on that error — see
30979
+ * `children-batch-loader.ts`.
30980
+ */
30981
+ getChildrenBatch: method(zod.z.object({ parentDeviceIds: zod.z.array(zod.z.number()).max(256) }), zod.z.record(zod.z.string(), zod.z.array(DeviceInfoSchema))),
30982
+ /**
30860
30983
  * Resolve the devices LINKED to a camera — the single policy authority
30861
30984
  * both consumers call (viewer devices panel + pipeline-analytics event
30862
30985
  * kinds/ingest). Device-tree children are ALWAYS included; mode 'auto'
@@ -31901,6 +32024,38 @@ var require_dist_D3lqzV40 = __commonJS({
31901
32024
  })
31902
32025
  }
31903
32026
  };
32027
+ var LogChannelApplyResultSchema = zod.z.object({
32028
+ /** How many declared channels are armed in this process after the call. */
32029
+ armed: zod.z.number().int().min(0),
32030
+ /**
32031
+ * Names the document armed that this process does not declare. Reported
32032
+ * rather than swallowed: a name here is either a typo or an addon that has
32033
+ * not booted, and both deserve a line instead of silence.
32034
+ */
32035
+ unknown: zod.z.array(zod.z.string()).readonly()
32036
+ });
32037
+ var logChannelsCapability = {
32038
+ name: "log-channels",
32039
+ scope: "system",
32040
+ mode: "collection",
32041
+ internal: true,
32042
+ methods: {
32043
+ /** The channels this addon declares. Inert: no value, no state. */
32044
+ list: method(zod.z.void(), zod.z.array(LogChannelDescriptorSchema).readonly()),
32045
+ /**
32046
+ * Refresh this process's mirror from the document's FULL set of armed
32047
+ * windows.
32048
+ *
32049
+ * Full and not incremental on purpose: the document is the authority, so a
32050
+ * channel it does not name is disarmed here. An incremental apply would
32051
+ * let a disarm get lost in transit and leave a channel running that
32052
+ * nobody can see is running.
32053
+ */
32054
+ apply: method(zod.z.object({ windows: zod.z.array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" })
32055
+ },
32056
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
32057
+ mount: { kind: "skip" }
32058
+ };
31904
32059
  var LogLevelSchema = zod.z.enum([
31905
32060
  "debug",
31906
32061
  "info",
@@ -47226,6 +47381,14 @@ var require_dist_D3lqzV40 = __commonJS({
47226
47381
  scope: LoggingScopeKindSchema,
47227
47382
  /** The node this layer speaks for; `null` on the cluster layer. */
47228
47383
  nodeId: zod.z.string().nullable(),
47384
+ /**
47385
+ * The declared channel this layer speaks for; `null` on every layer but
47386
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
47387
+ * by design — the convention this repo settled on is one orchestrator-wide
47388
+ * setting, never per node (D52) — so a component layer that carried a node
47389
+ * would invite a per-node copy of a value that has no per-node meaning.
47390
+ */
47391
+ component: zod.z.string().nullable(),
47229
47392
  /** Explicitly set here, or `null` when this layer inherits. */
47230
47393
  level: LogLevelSchema$1.nullable()
47231
47394
  });
@@ -47251,6 +47414,38 @@ var require_dist_D3lqzV40 = __commonJS({
47251
47414
  /** Cadence of the diagnostic's aggregated report line. Clamped by the server. */
47252
47415
  reportEveryMs: zod.z.number().int().positive().optional()
47253
47416
  });
47417
+ var LogChannelWindowStateSchema = zod.z.object({
47418
+ channel: zod.z.string(),
47419
+ armed: zod.z.boolean(),
47420
+ /** Epoch ms the window closes at. 0 when disarmed. */
47421
+ armedUntilMs: zod.z.number(),
47422
+ /** Ms left before it expires on its own. 0 when disarmed. */
47423
+ remainingMs: zod.z.number(),
47424
+ /**
47425
+ * The cameras it is narrowed to, or `null` for every camera.
47426
+ *
47427
+ * A channel declared `perDevice: false` can only ever report `null` here:
47428
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
47429
+ * produce a filter that silently matches nothing. The server REFUSES such a
47430
+ * patch rather than quietly widening it — ignoring the request would teach
47431
+ * the operator that per-camera filtering works on that channel when it does
47432
+ * not.
47433
+ */
47434
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
47435
+ });
47436
+ var LogChannelWindowPatchSchema = zod.z.object({
47437
+ channel: zod.z.string().min(1),
47438
+ armMs: zod.z.number().int().min(0),
47439
+ /**
47440
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
47441
+ *
47442
+ * Numeric because the repo's own rule makes it possible: every log line
47443
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
47444
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
47445
+ * diagnosed by hand, and this is the first thing that collects on it.
47446
+ */
47447
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable().optional()
47448
+ });
47254
47449
  var LoggingSettingsPatchSchema = zod.z.object({
47255
47450
  /**
47256
47451
  * Absent leaves the level untouched. `null` CLEARS the explicit value at the
@@ -47261,19 +47456,50 @@ var require_dist_D3lqzV40 = __commonJS({
47261
47456
  * Only the diagnostics NAMED here change. An armed window that is not listed
47262
47457
  * keeps running — a patch is never a full replacement.
47263
47458
  */
47264
- diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional()
47459
+ diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional(),
47460
+ /**
47461
+ * Only the channels NAMED here change. An armed channel that is not listed
47462
+ * keeps running — same rule as `diagnostics`, because a patch that silently
47463
+ * disarmed the channels it did not mention would make the Levels page and
47464
+ * the Diagnostics page fight over the same value.
47465
+ */
47466
+ channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
47467
+ });
47468
+ var GetLoggingSettingsInputSchema = zod.z.object({
47469
+ scopeNodeId: zod.z.string().optional(),
47470
+ /**
47471
+ * The declared CHANNEL this document is addressed at, when the caller wants
47472
+ * the `component` layer. Absent = the node/cluster hierarchy only.
47473
+ *
47474
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
47475
+ * axes from collapsing: a component level is cluster-wide, a node level is
47476
+ * not, and one selector for both would make "which of these two did I just
47477
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
47478
+ */
47479
+ scopeComponent: zod.z.string().optional()
47265
47480
  });
47266
- var GetLoggingSettingsInputSchema = zod.z.object({ scopeNodeId: zod.z.string().optional() });
47267
47481
  var SetLoggingSettingsInputSchema = zod.z.object({
47268
47482
  scopeNodeId: zod.z.string().optional(),
47483
+ scopeComponent: zod.z.string().optional(),
47269
47484
  patch: LoggingSettingsPatchSchema
47270
47485
  });
47271
47486
  var LoggingSettingsStateSchema = zod.z.object({
47272
47487
  /** The layer this document was read at. `null` = the cluster layer. */
47273
47488
  scopeNodeId: zod.z.string().nullable(),
47489
+ /** The channel this document was read at. `null` = no component layer. */
47490
+ scopeComponent: zod.z.string().nullable(),
47274
47491
  effective: LoggingEffectiveSchema,
47275
47492
  explicit: LoggingExplicitSchema,
47276
47493
  activeWindows: zod.z.array(DiagnosticWindowSchema).readonly(),
47494
+ /**
47495
+ * Every channel the cluster's addons DECLARE, gathered from the
47496
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
47497
+ * channel added by a redeployed addon appears without anybody editing a
47498
+ * list, and a channel whose addon is gone stops being offered.
47499
+ */
47500
+ channels: zod.z.array(LogChannelDescriptorSchema).readonly(),
47501
+ /** The channels ARMED right now, each with its deadline. */
47502
+ activeChannels: zod.z.array(LogChannelWindowStateSchema).readonly(),
47277
47503
  persisted: zod.z.boolean()
47278
47504
  });
47279
47505
  var systemCapability = {
@@ -48752,6 +48978,7 @@ var require_dist_D3lqzV40 = __commonJS({
48752
48978
  llmRuntimeCapability,
48753
48979
  localNetworkCapability,
48754
48980
  lockControlCapability,
48981
+ logChannelsCapability,
48755
48982
  logDestinationCapability,
48756
48983
  loginMethodCapability,
48757
48984
  mediaPlayerCapability,
@@ -49718,6 +49945,12 @@ var require_dist_D3lqzV40 = __commonJS({
49718
49945
  addonId: null,
49719
49946
  access: "view"
49720
49947
  },
49948
+ "dataStoreProvider.aggregate": {
49949
+ capName: "data-store-provider",
49950
+ capScope: "system",
49951
+ addonId: null,
49952
+ access: "view"
49953
+ },
49721
49954
  "dataStoreProvider.count": {
49722
49955
  capName: "data-store-provider",
49723
49956
  capScope: "system",
@@ -50132,6 +50365,12 @@ var require_dist_D3lqzV40 = __commonJS({
50132
50365
  addonId: null,
50133
50366
  access: "view"
50134
50367
  },
50368
+ "deviceManager.getChildrenBatch": {
50369
+ capName: "device-manager",
50370
+ capScope: "system",
50371
+ addonId: null,
50372
+ access: "view"
50373
+ },
50135
50374
  "deviceManager.getConfigSchema": {
50136
50375
  capName: "device-manager",
50137
50376
  capScope: "system",
@@ -51182,6 +51421,18 @@ var require_dist_D3lqzV40 = __commonJS({
51182
51421
  addonId: null,
51183
51422
  access: "create"
51184
51423
  },
51424
+ "logChannels.apply": {
51425
+ capName: "log-channels",
51426
+ capScope: "system",
51427
+ addonId: null,
51428
+ access: "create"
51429
+ },
51430
+ "logChannels.list": {
51431
+ capName: "log-channels",
51432
+ capScope: "system",
51433
+ addonId: null,
51434
+ access: "view"
51435
+ },
51185
51436
  "logDestination.query": {
51186
51437
  capName: "log-destination",
51187
51438
  capScope: "system",
@@ -53336,6 +53587,12 @@ var require_dist_D3lqzV40 = __commonJS({
53336
53587
  addonId: null,
53337
53588
  access: "create"
53338
53589
  },
53590
+ "settingsStore.aggregate": {
53591
+ capName: "settings-store",
53592
+ capScope: "system",
53593
+ addonId: null,
53594
+ access: "view"
53595
+ },
53339
53596
  "settingsStore.count": {
53340
53597
  capName: "settings-store",
53341
53598
  capScope: "system",
@@ -54915,6 +55172,11 @@ var require_dist_D3lqzV40 = __commonJS({
54915
55172
  form: "single",
54916
55173
  optional: false
54917
55174
  }],
55175
+ "deviceManager.getChildrenBatch": [{
55176
+ name: "parentDeviceIds",
55177
+ form: "array",
55178
+ optional: false
55179
+ }],
54918
55180
  "deviceManager.getConfigSchema": [{
54919
55181
  name: "deviceId",
54920
55182
  form: "single",
@@ -57730,7 +57992,7 @@ var require_alerts_addon = __commonJS({
57730
57992
  [Symbol.toStringTag]: { value: "Module" }
57731
57993
  });
57732
57994
  require_chunk_Cek0wNdY();
57733
- var require_dist10 = require_dist_D3lqzV40();
57995
+ var require_dist10 = require_dist_BVU5JADq();
57734
57996
  function selectExpired(alerts, cutoffMs) {
57735
57997
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
57736
57998
  }
@@ -58549,7 +58811,7 @@ var require_console_logging = __commonJS({
58549
58811
  [Symbol.toStringTag]: { value: "Module" }
58550
58812
  });
58551
58813
  require_chunk_Cek0wNdY();
58552
- var require_dist10 = require_dist_D3lqzV40();
58814
+ var require_dist10 = require_dist_BVU5JADq();
58553
58815
  var require_formatter = require_formatter_DqAKDlvN();
58554
58816
  var LEVEL_RANK = {
58555
58817
  debug: 0,
@@ -58643,7 +58905,7 @@ var require_core_blocks_addon = __commonJS({
58643
58905
  "use strict";
58644
58906
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
58645
58907
  var require_chunk = require_chunk_Cek0wNdY();
58646
- var require_dist10 = require_dist_D3lqzV40();
58908
+ var require_dist10 = require_dist_BVU5JADq();
58647
58909
  var node_crypto = __require("crypto");
58648
58910
  var node_fs_promises = __require("fs/promises");
58649
58911
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -59540,11 +59802,11 @@ var require_core_blocks = __commonJS({
59540
59802
  }
59541
59803
  });
59542
59804
 
59543
- // ../system/dist/retired-settings-keys-OtUQtEbq.js
59544
- var require_retired_settings_keys_OtUQtEbq = __commonJS({
59545
- "../system/dist/retired-settings-keys-OtUQtEbq.js"(exports) {
59805
+ // ../system/dist/retired-settings-keys-_PLI9w0k.js
59806
+ var require_retired_settings_keys_PLI9w0k = __commonJS({
59807
+ "../system/dist/retired-settings-keys-_PLI9w0k.js"(exports) {
59546
59808
  "use strict";
59547
- var require_dist10 = require_dist_D3lqzV40();
59809
+ var require_dist10 = require_dist_BVU5JADq();
59548
59810
  function settingsStoreIsAuthoritativeHere(env) {
59549
59811
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
59550
59812
  return raw === "" || raw === "hub";
@@ -61587,8 +61849,8 @@ var require_device_manager_addon = __commonJS({
61587
61849
  [Symbol.toStringTag]: { value: "Module" }
61588
61850
  });
61589
61851
  require_chunk_Cek0wNdY();
61590
- var require_dist10 = require_dist_D3lqzV40();
61591
- var require_retired_settings_keys = require_retired_settings_keys_OtUQtEbq();
61852
+ var require_dist10 = require_dist_BVU5JADq();
61853
+ var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
61592
61854
  var node_crypto = __require("crypto");
61593
61855
  var _camstack_types_node = require_node();
61594
61856
  var JOB_HISTORY = 20;
@@ -62750,30 +63012,16 @@ var require_device_manager_addon = __commonJS({
62750
63012
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
62751
63013
  };
62752
63014
  }
62753
- async function getChildren(pctx, input) {
62754
- const { parentDeviceId } = input;
62755
- let ownerAddonId = null;
62756
- if (pctx.registry) {
62757
- if (pctx.registry.getById(parentDeviceId)) ownerAddonId = pctx.registry.getAddonId(parentDeviceId);
62758
- }
62759
- if (!ownerAddonId) {
62760
- const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
62761
- if (!persisted) return [];
62762
- ownerAddonId = persisted.addonId;
62763
- }
63015
+ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren) {
62764
63016
  const results = [];
62765
63017
  const seen = /* @__PURE__ */ new Set();
62766
- const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
62767
63018
  const rowById = /* @__PURE__ */ new Map();
62768
63019
  for (const row of childRows) rowById.set(row.meta.id, row);
62769
- if (pctx.registry) {
62770
- const liveChildren = pctx.registry.getChildren(parentDeviceId);
62771
- for (const device of liveChildren) {
62772
- const key = String(device.id);
62773
- const row = rowById.get(device.id);
62774
- results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
62775
- seen.add(key);
62776
- }
63020
+ for (const device of liveChildren) {
63021
+ const key = String(device.id);
63022
+ const row = rowById.get(device.id);
63023
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
63024
+ seen.add(key);
62777
63025
  }
62778
63026
  for (const row of childRows) {
62779
63027
  const m = row.meta;
@@ -62809,6 +63057,58 @@ var require_device_manager_addon = __commonJS({
62809
63057
  }
62810
63058
  return results;
62811
63059
  }
63060
+ function liveOwnerOf(pctx, parentDeviceId) {
63061
+ if (!pctx.registry) return null;
63062
+ if (!pctx.registry.getById(parentDeviceId)) return null;
63063
+ return pctx.registry.getAddonId(parentDeviceId);
63064
+ }
63065
+ async function getChildren(pctx, input) {
63066
+ const { parentDeviceId } = input;
63067
+ let ownerAddonId = liveOwnerOf(pctx, parentDeviceId);
63068
+ if (!ownerAddonId) {
63069
+ const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
63070
+ if (!persisted) return [];
63071
+ ownerAddonId = persisted.addonId;
63072
+ }
63073
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
63074
+ const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
63075
+ return [...await projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren)];
63076
+ }
63077
+ async function getChildrenBatch(pctx, input) {
63078
+ const parentIds = [...new Set(input.parentDeviceIds)];
63079
+ const out = {};
63080
+ if (parentIds.length === 0) return out;
63081
+ const owners = /* @__PURE__ */ new Map();
63082
+ const unownedIds = [];
63083
+ for (const parentId of parentIds) {
63084
+ const live = liveOwnerOf(pctx, parentId);
63085
+ if (live !== null) owners.set(parentId, live);
63086
+ else unownedIds.push(parentId);
63087
+ }
63088
+ if (unownedIds.length > 0) {
63089
+ const rows = await pctx.metaStore.rows.getMany(unownedIds);
63090
+ for (const [parentId, row] of rows) owners.set(parentId, row.meta.addonId);
63091
+ }
63092
+ const requested = new Set(parentIds);
63093
+ const rowsByParent = await pctx.metaStore.rows.listByParentMany([...owners.keys()]);
63094
+ const liveByParent = /* @__PURE__ */ new Map();
63095
+ for (const device of pctx.registry?.getAll() ?? []) {
63096
+ const parentId = device.parentDeviceId;
63097
+ if (parentId === null || parentId === void 0) continue;
63098
+ if (!requested.has(parentId)) continue;
63099
+ const bucket = liveByParent.get(parentId);
63100
+ if (bucket === void 0) liveByParent.set(parentId, [device]);
63101
+ else bucket.push(device);
63102
+ }
63103
+ for (const [parentId, ownerAddonId] of owners) {
63104
+ const childRows = rowsByParent.get(parentId) ?? [];
63105
+ const liveChildren = liveByParent.get(parentId) ?? [];
63106
+ if (childRows.length === 0 && liveChildren.length === 0) continue;
63107
+ const projected = await projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren);
63108
+ if (projected.length > 0) out[String(parentId)] = [...projected];
63109
+ }
63110
+ return out;
63111
+ }
62812
63112
  async function getStreamSources(pctx, input) {
62813
63113
  const { deviceId } = input;
62814
63114
  if (pctx.registry) {
@@ -64255,16 +64555,17 @@ var require_device_manager_addon = __commonJS({
64255
64555
  async function collectDescendants(metaStore, rootId) {
64256
64556
  const out = [];
64257
64557
  const visited = /* @__PURE__ */ new Set([rootId]);
64258
- const queue = [rootId];
64259
- while (queue.length > 0) {
64260
- const parentId = queue.shift();
64261
- if (parentId === void 0) break;
64262
- for (const row of await metaStore.rows.listByParent(parentId)) {
64558
+ let frontier = [rootId];
64559
+ while (frontier.length > 0) {
64560
+ const byParent = await metaStore.rows.listByParentMany(frontier);
64561
+ const next = [];
64562
+ for (const parentId of frontier) for (const row of byParent.get(parentId) ?? []) {
64263
64563
  if (visited.has(row.meta.id)) continue;
64264
64564
  visited.add(row.meta.id);
64265
64565
  out.push(row.meta);
64266
- queue.push(row.meta.id);
64566
+ next.push(row.meta.id);
64267
64567
  }
64568
+ frontier = next;
64268
64569
  }
64269
64570
  return out;
64270
64571
  }
@@ -65323,6 +65624,46 @@ var require_device_manager_addon = __commonJS({
65323
65624
  limit: DEVICE_ROWS_FLEET_LIMIT
65324
65625
  });
65325
65626
  }
65627
+ /**
65628
+ * Direct children of MANY devices, in ONE query.
65629
+ *
65630
+ * `listByParent` called once per parent is the same shape `get` was before
65631
+ * {@link getMany}: an indexed lookup this store makes cheap, repeated until
65632
+ * it isn't. Measured on the live hub 2026-08-27, 120-second boot window:
65633
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
65634
+ * **1 024 calls** — one per device — for 919 rows in total. The BFS in
65635
+ * `device-meta-actions.ts#collectDescendants` walked a container tree the
65636
+ * same way, one query per node.
65637
+ *
65638
+ * `whereIn` on the indexed `parentDeviceId` column collapses a level of that
65639
+ * walk into a single statement. Contract, verbatim from {@link getMany}: an
65640
+ * EMPTY `parentDeviceIds` reads nothing at all rather than degrading to
65641
+ * "everything", and a parent with no children — or one the fleet does not
65642
+ * know — is simply ABSENT from the map, never an invented empty row.
65643
+ */
65644
+ async listByParentMany(parentDeviceIds) {
65645
+ const out = /* @__PURE__ */ new Map();
65646
+ const unique = [...new Set(parentDeviceIds)];
65647
+ if (unique.length === 0) return out;
65648
+ const rows = await this.list({
65649
+ whereIn: { parentDeviceId: unique },
65650
+ orderBy: {
65651
+ field: "deviceId",
65652
+ direction: "asc"
65653
+ },
65654
+ limit: DEVICE_ROWS_FLEET_LIMIT
65655
+ });
65656
+ const byParent = /* @__PURE__ */ new Map();
65657
+ for (const row of rows) {
65658
+ const parent = row.meta.parentDeviceId;
65659
+ if (parent === null || parent === void 0) continue;
65660
+ const bucket = byParent.get(parent);
65661
+ if (bucket === void 0) byParent.set(parent, [row]);
65662
+ else bucket.push(row);
65663
+ }
65664
+ for (const [parent, bucket] of byParent) out.set(parent, bucket);
65665
+ return out;
65666
+ }
65326
65667
  /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
65327
65668
  async count() {
65328
65669
  await this.declare();
@@ -66056,6 +66397,7 @@ var require_device_manager_addon = __commonJS({
66056
66397
  listAll: (input) => listAll(pctx, input),
66057
66398
  getDevice: (input) => getDevice(pctx, input),
66058
66399
  getChildren: (input) => getChildren(pctx, input),
66400
+ getChildrenBatch: (input) => getChildrenBatch(pctx, input),
66059
66401
  getLinkedDevices: (input) => getLinkedDevices(pctx, input),
66060
66402
  getLinkedDevicesBatch: (input) => getLinkedDevicesBatch(pctx, input),
66061
66403
  getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
@@ -66257,7 +66599,7 @@ var require_hub_forwarder = __commonJS({
66257
66599
  [Symbol.toStringTag]: { value: "Module" }
66258
66600
  });
66259
66601
  require_chunk_Cek0wNdY();
66260
- var require_dist10 = require_dist_D3lqzV40();
66602
+ var require_dist10 = require_dist_BVU5JADq();
66261
66603
  var require_formatter = require_formatter_DqAKDlvN();
66262
66604
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
66263
66605
  var HubForwarderDestination = class {
@@ -66394,7 +66736,7 @@ var require_liveness_monitor_addon = __commonJS({
66394
66736
  "use strict";
66395
66737
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
66396
66738
  require_chunk_Cek0wNdY();
66397
- var require_dist10 = require_dist_D3lqzV40();
66739
+ var require_dist10 = require_dist_BVU5JADq();
66398
66740
  var NO_DEVICES = "liveness:no-devices";
66399
66741
  var ALL_OFFLINE = "liveness:all-devices-offline";
66400
66742
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -66584,7 +66926,7 @@ var require_local_auth_addon = __commonJS({
66584
66926
  [Symbol.toStringTag]: { value: "Module" }
66585
66927
  });
66586
66928
  var require_chunk = require_chunk_Cek0wNdY();
66587
- var require_dist10 = require_dist_D3lqzV40();
66929
+ var require_dist10 = require_dist_BVU5JADq();
66588
66930
  var node_crypto = __require("crypto");
66589
66931
  node_crypto = require_chunk.__toESM(node_crypto);
66590
66932
  var crypto$1 = __require("crypto");
@@ -74268,7 +74610,7 @@ var require_loki_logging = __commonJS({
74268
74610
  [Symbol.toStringTag]: { value: "Module" }
74269
74611
  });
74270
74612
  require_chunk_Cek0wNdY();
74271
- var require_dist10 = require_dist_D3lqzV40();
74613
+ var require_dist10 = require_dist_BVU5JADq();
74272
74614
  function sanitizeLabelName(raw) {
74273
74615
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
74274
74616
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -74833,7 +75175,7 @@ var require_native_metrics_addon = __commonJS({
74833
75175
  [Symbol.toStringTag]: { value: "Module" }
74834
75176
  });
74835
75177
  var require_chunk = require_chunk_Cek0wNdY();
74836
- var require_dist10 = require_dist_D3lqzV40();
75178
+ var require_dist10 = require_dist_BVU5JADq();
74837
75179
  var node_child_process = __require("child_process");
74838
75180
  var node_util = __require("util");
74839
75181
  var node_os = __require("os");
@@ -75775,7 +76117,7 @@ var require_filesystem_storage_addon = __commonJS({
75775
76117
  [Symbol.toStringTag]: { value: "Module" }
75776
76118
  });
75777
76119
  var require_chunk = require_chunk_Cek0wNdY();
75778
- var require_dist10 = require_dist_D3lqzV40();
76120
+ var require_dist10 = require_dist_BVU5JADq();
75779
76121
  var node_crypto = __require("crypto");
75780
76122
  var node_fs_promises = __require("fs/promises");
75781
76123
  var node_path = __require("path");
@@ -76891,8 +77233,8 @@ var require_sqlite_settings_addon = __commonJS({
76891
77233
  [Symbol.toStringTag]: { value: "Module" }
76892
77234
  });
76893
77235
  var require_chunk = require_chunk_Cek0wNdY();
76894
- var require_dist10 = require_dist_D3lqzV40();
76895
- var require_retired_settings_keys = require_retired_settings_keys_OtUQtEbq();
77236
+ var require_dist10 = require_dist_BVU5JADq();
77237
+ var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
76896
77238
  var node_crypto = __require("crypto");
76897
77239
  var node_fs = __require("fs");
76898
77240
  var node_module = __require("module");
@@ -77733,6 +78075,46 @@ var require_sqlite_settings_addon = __commonJS({
77733
78075
  params
77734
78076
  }, () => this.getDb().prepare(sql).get(...params))?.cnt ?? 0;
77735
78077
  }
78078
+ /**
78079
+ * `COUNT(*)` and one SUM/MIN/MAX per requested field, in ONE statement.
78080
+ *
78081
+ * Result slots are aliased POSITIONALLY (`a0`, `a1`, …) and mapped back by
78082
+ * index: `as` is caller text and must never reach SQL as an identifier, and
78083
+ * the column name is validated through {@link fieldExprFor} exactly like a
78084
+ * projection — an unresolvable field THROWS rather than being dropped, because
78085
+ * a missing aggregate comes back as a number that looks real.
78086
+ *
78087
+ * `select` mode on the filter, so this agrees with `query` and `count` about
78088
+ * which rows it is talking about.
78089
+ */
78090
+ async aggregate({ namespace, collection, fields, filter }) {
78091
+ const scoped = this.scopedName(namespace, collection);
78092
+ const decl = this.requireDeclared(scoped);
78093
+ const shape = this.shapeOf(decl);
78094
+ const selects = ['COUNT(*) AS "n"'];
78095
+ fields.forEach((f, i) => {
78096
+ const col = fieldExprFor(f.field, shape);
78097
+ 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`);
78098
+ selects.push(`${f.op.toUpperCase()}(${col}) AS "a${i}"`);
78099
+ });
78100
+ const { whereSql, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
78101
+ const sql = `SELECT ${selects.join(", ")} FROM "${scoped}"${whereSql}`;
78102
+ const row = this.measured({
78103
+ op: "aggregate",
78104
+ collection: scoped,
78105
+ sql,
78106
+ params
78107
+ }, () => this.getDb().prepare(sql).get(...params));
78108
+ const values = {};
78109
+ fields.forEach((f, i) => {
78110
+ const raw = row?.[`a${i}`];
78111
+ values[f.as] = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
78112
+ });
78113
+ return {
78114
+ count: row?.["n"] ?? 0,
78115
+ values
78116
+ };
78117
+ }
77736
78118
  async histogram({ namespace, collection, field, bucketSize, origin, filter }) {
77737
78119
  const scoped = this.scopedName(namespace, collection);
77738
78120
  const decl = this.requireDeclared(scoped);
@@ -79086,7 +79468,7 @@ var require_storage_orchestrator_addon = __commonJS({
79086
79468
  [Symbol.toStringTag]: { value: "Module" }
79087
79469
  });
79088
79470
  var require_chunk = require_chunk_Cek0wNdY();
79089
- var require_dist10 = require_dist_D3lqzV40();
79471
+ var require_dist10 = require_dist_BVU5JADq();
79090
79472
  var node_crypto = __require("crypto");
79091
79473
  var node_fs_promises = __require("fs/promises");
79092
79474
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -79655,6 +80037,7 @@ var require_storage_orchestrator_addon = __commonJS({
79655
80037
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
79656
80038
  updateWhere: async (input) => (await engine()).updateWhere(input),
79657
80039
  count: async (input) => (await engine()).count(input),
80040
+ aggregate: async (input) => (await engine()).aggregate(input),
79658
80041
  histogram: async (input) => (await engine()).histogram(input),
79659
80042
  isEmpty: async (input) => (await engine()).isEmpty(input),
79660
80043
  declareCollection: async (input) => (await engine()).declareCollection(input)
@@ -80965,7 +81348,7 @@ var require_system_config_addon = __commonJS({
80965
81348
  [Symbol.toStringTag]: { value: "Module" }
80966
81349
  });
80967
81350
  require_chunk_Cek0wNdY();
80968
- var require_dist10 = require_dist_D3lqzV40();
81351
+ var require_dist10 = require_dist_BVU5JADq();
80969
81352
  var SECTION_TITLES = {
80970
81353
  server: "Server",
80971
81354
  auth: "Authentication"
@@ -99026,7 +99409,7 @@ var require_winston_logging = __commonJS({
99026
99409
  [Symbol.toStringTag]: { value: "Module" }
99027
99410
  });
99028
99411
  var require_chunk = require_chunk_Cek0wNdY();
99029
- var require_dist10 = require_dist_D3lqzV40();
99412
+ var require_dist10 = require_dist_BVU5JADq();
99030
99413
  var require_formatter = require_formatter_DqAKDlvN();
99031
99414
  var node_path = __require("path");
99032
99415
  node_path = require_chunk.__toESM(node_path);
@@ -104814,6 +105197,7 @@ var require_addon = __commonJS({
104814
105197
  "listProfiles",
104815
105198
  "listRuntimeNodes"
104816
105199
  ],
105200
+ "log-channels": ["list"],
104817
105201
  "log-destination": ["query"],
104818
105202
  "login-method": ["getLoginMethods"],
104819
105203
  "mqtt-broker": ["listBrokers"],
@@ -111401,11 +111785,12 @@ var require_dist2 = __commonJS({
111401
111785
  }
111402
111786
  });
111403
111787
 
111404
- // ../system/dist/manifest-python-deps-B_mCU6gz.js
111405
- var require_manifest_python_deps_B_mCU6gz = __commonJS({
111406
- "../system/dist/manifest-python-deps-B_mCU6gz.js"(exports) {
111788
+ // ../system/dist/manifest-python-deps-CktMcXzS.js
111789
+ var require_manifest_python_deps_CktMcXzS = __commonJS({
111790
+ "../system/dist/manifest-python-deps-CktMcXzS.js"(exports) {
111407
111791
  "use strict";
111408
111792
  var require_chunk = require_chunk_Cek0wNdY();
111793
+ require_dist_BVU5JADq();
111409
111794
  var node_crypto = __require("crypto");
111410
111795
  node_crypto = require_chunk.__toESM(node_crypto);
111411
111796
  var _camstack_types_node = require_node();
@@ -111496,6 +111881,34 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111496
111881
  function shouldReclaim(s, triggerMb = HEAP_RECLAIM_TRIGGER_MB) {
111497
111882
  return strandedMb(s) > triggerMb;
111498
111883
  }
111884
+ function share(partMb, rssMb) {
111885
+ return rssMb > 0 ? Math.round(partMb / rssMb * 100) : 0;
111886
+ }
111887
+ function describeRss(s, budgetMb) {
111888
+ const nativeResidueMb = Math.max(0, strandedMb(s));
111889
+ const shape = s.heapUsedMb >= s.externalMb && s.heapUsedMb >= nativeResidueMb ? "v8-heap" : s.externalMb >= nativeResidueMb ? "external" : "native-residue";
111890
+ return {
111891
+ rssMb: s.rssMb,
111892
+ budgetMb,
111893
+ overMb: Math.max(0, s.rssMb - budgetMb),
111894
+ heapUsedMb: s.heapUsedMb,
111895
+ externalMb: s.externalMb,
111896
+ nativeResidueMb,
111897
+ arrayBuffersMb: s.arrayBuffersMb,
111898
+ shape
111899
+ };
111900
+ }
111901
+ var SHAPE_ADVICE = {
111902
+ "v8-heap": "live JS retainers \u2014 bounded by --max-old-space-size; the fix is the retained set",
111903
+ external: "Buffers / native external \u2014 NO V8 flag bounds this, and the old-space ceiling never will",
111904
+ "native-residue": "freed native memory the allocator has not returned \u2014 MALLOC_ARENA_MAX/VIPS_CONCURRENCY are already applied, so this lever is spent"
111905
+ };
111906
+ var RSS_BUDGET_RELEASE_RATIO = 0.9;
111907
+ var RSS_BUDGET_REANNOUNCE_MIN_MS = 9e5;
111908
+ function nextRssBudgetState(current, rssMb, budgetMb, releaseRatio = RSS_BUDGET_RELEASE_RATIO) {
111909
+ if (current === "over") return rssMb < budgetMb * releaseRatio ? "within" : "over";
111910
+ return rssMb > budgetMb ? "over" : "within";
111911
+ }
111499
111912
  function isGcFunction(value) {
111500
111913
  return typeof value === "function";
111501
111914
  }
@@ -111513,12 +111926,15 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111513
111926
  return;
111514
111927
  }
111515
111928
  }
111516
- function format2(label, s, loop) {
111517
- const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
111929
+ function formatOverBudget(label, b, source) {
111930
+ return `[mem] ${label} OVER RSS BUDGET rss=${b.rssMb}MB budget=${b.budgetMb}MB over=+${b.overMb}MB shape=${b.shape} heapUsed=${b.heapUsedMb}MB(${share(b.heapUsedMb, b.rssMb)}%) external=${b.externalMb}MB(${share(b.externalMb, b.rssMb)}%) nativeResidue=${b.nativeResidueMb}MB(${share(b.nativeResidueMb, b.rssMb)}%) arrayBuffers=${b.arrayBuffersMb}MB budgetFrom="${source}" \u2014 ${SHAPE_ADVICE[b.shape]}`;
111931
+ }
111932
+ function format2(label, s, loop, budgetMb) {
111933
+ const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB` + (budgetMb === void 0 ? "" : ` rssBudget=${budgetMb}MB`);
111518
111934
  if (loop === void 0) return line;
111519
111935
  return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms`;
111520
111936
  }
111521
- function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true) {
111937
+ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv, announceCeilingOrigin = true, rssBudget) {
111522
111938
  const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
111523
111939
  const now = reclaimOptions?.now ?? (() => Date.now());
111524
111940
  const triggerMb = reclaimOptions?.triggerMb ?? 1024;
@@ -111560,6 +111976,29 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111560
111976
  };
111561
111977
  let mode = "steady";
111562
111978
  let lastLoggedAt = Number.NEGATIVE_INFINITY;
111979
+ const budgetMb = rssBudget?.budgetMb !== void 0 && rssBudget.budgetMb > 0 ? rssBudget.budgetMb : void 0;
111980
+ const budgetSource = rssBudget?.source ?? "unspecified";
111981
+ const releaseRatio = rssBudget?.releaseRatio ?? 0.9;
111982
+ const reannounceMinMs = rssBudget?.reannounceMinMs ?? 9e5;
111983
+ let budgetState = "within";
111984
+ let lastBudgetWarnAt = Number.NEGATIVE_INFINITY;
111985
+ let overBudgetAnnounced = false;
111986
+ const checkRssBudget = (sample, at) => {
111987
+ if (budgetMb === void 0) return;
111988
+ const previous = budgetState;
111989
+ budgetState = nextRssBudgetState(previous, sample.rssMb, budgetMb, releaseRatio);
111990
+ if (budgetState === previous) return;
111991
+ if (budgetState === "over") {
111992
+ if (at - lastBudgetWarnAt < reannounceMinMs) return;
111993
+ lastBudgetWarnAt = at;
111994
+ overBudgetAnnounced = true;
111995
+ sink.warn(formatOverBudget(label, describeRss(sample, budgetMb), budgetSource));
111996
+ return;
111997
+ }
111998
+ if (!overBudgetAnnounced) return;
111999
+ overBudgetAnnounced = false;
112000
+ sink.info(`[mem] ${label} back within its RSS budget \u2014 rss=${sample.rssMb}MB budget=${budgetMb}MB`);
112001
+ };
111563
112002
  const probeIntervalMs = Math.min(fastIntervalMs, intervalMs);
111564
112003
  const tick = () => {
111565
112004
  try {
@@ -111570,12 +112009,13 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111570
112009
  const due = at - lastLoggedAt >= intervalMs;
111571
112010
  if (mode === "escalated" || due) {
111572
112011
  lastLoggedAt = at;
111573
- const line = format2(label, sample, loopDelay?.read());
112012
+ const line = format2(label, sample, loopDelay?.read(), budgetMb);
111574
112013
  if (sample.nearLimit) sink.warn(`${line} \u2014 APPROACHING HEAP LIMIT`);
111575
112014
  else if (mode === "escalated") sink.warn(`${line} \u2014 heap elevated, sampling every ${probeIntervalMs}ms`);
111576
112015
  else sink.info(line);
111577
112016
  }
111578
112017
  if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine \u2014 logging every ${intervalMs}ms`);
112018
+ checkRssBudget(sample, at);
111579
112019
  maybeReclaim(sample);
111580
112020
  } catch {
111581
112021
  }
@@ -111583,6 +112023,7 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111583
112023
  const timer = setInterval(tick, probeIntervalMs);
111584
112024
  timer.unref?.();
111585
112025
  tick();
112026
+ if (rssBudget !== void 0 && budgetMb === void 0) sink.info(`[mem] ${label} is NOT WATCHED against an RSS budget \u2014 no budget declared (${budgetSource}). rss growth outside the V8 heap raises nothing here: --max-old-space-size bounds old space only, and stranded subtracts external by construction. Declare execution.rssBudgetMb once this process has a measured working set.`);
111586
112027
  if (announceCeilingOrigin && heapCeilingOrigin(execArgv) === "v8-default") sink.info(`[mem] ${label} heap ceiling is V8's DEFAULT (${read().heapLimitMb}MB) \u2014 no --max-old-space-size on argv. Nothing here CHOSE that number; it is derived from host RAM and moves with it.`);
111587
112028
  let stopped = false;
111588
112029
  return () => {
@@ -111593,16 +112034,40 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
111593
112034
  };
111594
112035
  }
111595
112036
  var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
112037
+ function parseRssBudgetMb(raw) {
112038
+ if (raw === void 0) return void 0;
112039
+ const parsed = Number(raw);
112040
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
112041
+ return Math.floor(parsed);
112042
+ }
112043
+ var RUNNER_RSS_BUDGET_ENV = "CAMSTACK_RUNNER_RSS_BUDGET_MB";
112044
+ var HUB_RSS_BUDGET_ENV = "CAMSTACK_HUB_RSS_BUDGET_MB";
112045
+ var HUB_MAIN_RSS_BUDGET_MB = 4096;
112046
+ function hubMainRssBudget(env = process.env) {
112047
+ const override = env[HUB_RSS_BUDGET_ENV];
112048
+ if (override !== void 0) return {
112049
+ budgetMb: parseRssBudgetMb(override),
112050
+ source: HUB_RSS_BUDGET_ENV
112051
+ };
112052
+ return {
112053
+ budgetMb: HUB_MAIN_RSS_BUDGET_MB,
112054
+ source: "HUB_MAIN_RSS_BUDGET_MB (kernel/heap-watch)"
112055
+ };
112056
+ }
111596
112057
  function startRunnerHeapWatch(options) {
111597
112058
  if (options.heapProfile !== "heavy") return void 0;
111598
112059
  const intervalMs = options.intervalMs ?? 3e5;
111599
- if (options.reclaimSwitch === "off") return startHeapWatch(options.label, options.sink, intervalMs, void 0, void 0, void 0, false);
112060
+ const rssBudget = {
112061
+ budgetMb: parseRssBudgetMb(options.rssBudgetMb),
112062
+ source: `manifest execution.rssBudgetMb (via ${RUNNER_RSS_BUDGET_ENV})`
112063
+ };
112064
+ if (options.reclaimSwitch === "off") return startHeapWatch(options.label, options.sink, intervalMs, void 0, void 0, void 0, false, rssBudget);
111600
112065
  let reclaimOptions = options.reclaimOptions;
111601
112066
  if (reclaimOptions === void 0) {
111602
112067
  const reclaimer = createV8Reclaimer();
111603
112068
  reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
111604
112069
  }
111605
- return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
112070
+ return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true, rssBudget);
111606
112071
  }
111607
112072
  function trimSlashes(s) {
111608
112073
  return s.replace(/^\/+/, "").replace(/\/+$/, "");
@@ -112479,6 +112944,105 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
112479
112944
  });
112480
112945
  return true;
112481
112946
  }
112947
+ function parseChildRows(value) {
112948
+ if (!Array.isArray(value)) return null;
112949
+ const rows = [];
112950
+ for (const entry of value) {
112951
+ if (typeof entry !== "object" || entry === null) return null;
112952
+ const id = Reflect.get(entry, "id");
112953
+ const stableId = Reflect.get(entry, "stableId");
112954
+ const name = Reflect.get(entry, "name");
112955
+ const linkDeviceId = Reflect.get(entry, "linkDeviceId");
112956
+ if (typeof id !== "number" || typeof stableId !== "string" || typeof name !== "string") return null;
112957
+ rows.push({
112958
+ id,
112959
+ stableId,
112960
+ name,
112961
+ ...typeof linkDeviceId === "number" || linkDeviceId === null ? { linkDeviceId } : {}
112962
+ });
112963
+ }
112964
+ return rows;
112965
+ }
112966
+ function parseChildrenByParent(value) {
112967
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
112968
+ const out = /* @__PURE__ */ new Map();
112969
+ for (const [key, entry] of Object.entries(value)) {
112970
+ const parentDeviceId = Number(key);
112971
+ if (!Number.isInteger(parentDeviceId)) return null;
112972
+ const rows = parseChildRows(entry);
112973
+ if (rows === null) return null;
112974
+ out.set(parentDeviceId, rows);
112975
+ }
112976
+ return out;
112977
+ }
112978
+ function looksLikeUnknownMethod(err) {
112979
+ const lowered = (err instanceof Error ? err.message : String(err)).toLowerCase();
112980
+ return lowered.includes("not_found") || lowered.includes("no procedure") || lowered.includes("unknown method") || lowered.includes("no such method") || lowered.includes("not available");
112981
+ }
112982
+ function createChildrenBatchLoader(opts) {
112983
+ const maxBatch = Math.max(1, opts.maxBatch ?? 256);
112984
+ let pending = [];
112985
+ let flushScheduled = false;
112986
+ let flushing = false;
112987
+ let batchUnsupported = false;
112988
+ const answerOneByOne = async (waiters) => {
112989
+ await Promise.all(waiters.map(async (waiter) => {
112990
+ try {
112991
+ waiter.resolve(await opts.fetchOne(waiter.parentDeviceId));
112992
+ } catch (err) {
112993
+ waiter.reject(err);
112994
+ }
112995
+ }));
112996
+ };
112997
+ const runBatch = async (waiters) => {
112998
+ const parentDeviceIds = [...new Set(waiters.map((w) => w.parentDeviceId))];
112999
+ try {
113000
+ const byParent = await opts.fetchBatch(parentDeviceIds);
113001
+ for (const waiter of waiters) waiter.resolve(byParent.get(waiter.parentDeviceId) ?? []);
113002
+ } catch (err) {
113003
+ const unknown = looksLikeUnknownMethod(err);
113004
+ if (unknown) batchUnsupported = true;
113005
+ opts.logger.warn(unknown ? "device-manager.getChildrenBatch is not available on this hub \u2014 falling back to per-parent getChildren for the process lifetime" : "device-manager.getChildrenBatch failed \u2014 answering this batch per parent", { meta: {
113006
+ parents: parentDeviceIds.length,
113007
+ error: err instanceof Error ? err.message : String(err)
113008
+ } });
113009
+ await answerOneByOne(waiters);
113010
+ }
113011
+ };
113012
+ const flush = async () => {
113013
+ flushing = true;
113014
+ try {
113015
+ while (pending.length > 0) {
113016
+ const batch = pending.slice(0, maxBatch);
113017
+ pending = pending.slice(maxBatch);
113018
+ if (batchUnsupported) await answerOneByOne(batch);
113019
+ else await runBatch(batch);
113020
+ }
113021
+ } finally {
113022
+ flushing = false;
113023
+ }
113024
+ };
113025
+ const scheduleFlush = () => {
113026
+ if (flushScheduled || flushing) return;
113027
+ flushScheduled = true;
113028
+ setTimeout(() => {
113029
+ flushScheduled = false;
113030
+ flush().catch((err) => {
113031
+ opts.logger.warn("children batch flush threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
113032
+ });
113033
+ }, 0);
113034
+ };
113035
+ return { load(parentDeviceId) {
113036
+ return new Promise((resolve, reject) => {
113037
+ pending.push({
113038
+ parentDeviceId,
113039
+ resolve,
113040
+ reject
113041
+ });
113042
+ scheduleFlush();
113043
+ });
113044
+ } };
113045
+ }
112482
113046
  var ACCESSORY_SPAWN_CONCURRENCY = 12;
112483
113047
  async function runBounded(items, limit, worker2) {
112484
113048
  const effectiveLimit = Math.max(1, Math.min(limit, items.length));
@@ -112686,6 +113250,19 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
112686
113250
  api
112687
113251
  });
112688
113252
  let selfApi;
113253
+ const childrenLoader = createChildrenBatchLoader({
113254
+ logger: opts.logger,
113255
+ fetchBatch: async (parentDeviceIds) => {
113256
+ const parsed = parseChildrenByParent(await callDeviceManager(api, "getChildrenBatch", { parentDeviceIds }));
113257
+ if (parsed === null) throw new Error("getChildrenBatch returned a shape this kernel cannot read");
113258
+ return parsed;
113259
+ },
113260
+ fetchOne: async (parentDeviceId) => {
113261
+ const parsed = parseChildRows(await callDeviceManager(api, "getChildren", { parentDeviceId }));
113262
+ if (parsed === null) throw new Error("getChildren returned a shape this kernel cannot read");
113263
+ return parsed;
113264
+ }
113265
+ });
112689
113266
  const deviceRebuildFactories = /* @__PURE__ */ new Map();
112690
113267
  const buildContext = (stableId, id, parentDeviceId = null, initialRuntimeState = {}, persistedConfig = {}, deviceMeta = null) => {
112691
113268
  let runtimeStateRef = null;
@@ -113097,7 +113674,7 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
113097
113674
  return;
113098
113675
  }
113099
113676
  try {
113100
- const currentChildren = await callDeviceManager(api, "getChildren", { parentDeviceId: device.id });
113677
+ const currentChildren = await childrenLoader.load(device.id);
113101
113678
  for (const child of currentChildren) {
113102
113679
  if (child.linkDeviceId !== device.id) continue;
113103
113680
  if (expected.has(child.stableId)) continue;
@@ -118046,6 +118623,18 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
118046
118623
  return HUB_CAP_FWD_SERVICE;
118047
118624
  }
118048
118625
  });
118626
+ Object.defineProperty(exports, "HUB_MAIN_RSS_BUDGET_MB", {
118627
+ enumerable: true,
118628
+ get: function() {
118629
+ return HUB_MAIN_RSS_BUDGET_MB;
118630
+ }
118631
+ });
118632
+ Object.defineProperty(exports, "HUB_RSS_BUDGET_ENV", {
118633
+ enumerable: true,
118634
+ get: function() {
118635
+ return HUB_RSS_BUDGET_ENV;
118636
+ }
118637
+ });
118049
118638
  Object.defineProperty(exports, "LocalChildClient", {
118050
118639
  enumerable: true,
118051
118640
  get: function() {
@@ -118064,12 +118653,30 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
118064
118653
  return NATIVE_PROVIDER_SERVICE_INFIX;
118065
118654
  }
118066
118655
  });
118656
+ Object.defineProperty(exports, "RSS_BUDGET_REANNOUNCE_MIN_MS", {
118657
+ enumerable: true,
118658
+ get: function() {
118659
+ return RSS_BUDGET_REANNOUNCE_MIN_MS;
118660
+ }
118661
+ });
118662
+ Object.defineProperty(exports, "RSS_BUDGET_RELEASE_RATIO", {
118663
+ enumerable: true,
118664
+ get: function() {
118665
+ return RSS_BUDGET_RELEASE_RATIO;
118666
+ }
118667
+ });
118067
118668
  Object.defineProperty(exports, "RUNNER_HEAP_WATCH_INTERVAL_MS", {
118068
118669
  enumerable: true,
118069
118670
  get: function() {
118070
118671
  return RUNNER_HEAP_WATCH_INTERVAL_MS;
118071
118672
  }
118072
118673
  });
118674
+ Object.defineProperty(exports, "RUNNER_RSS_BUDGET_ENV", {
118675
+ enumerable: true,
118676
+ get: function() {
118677
+ return RUNNER_RSS_BUDGET_ENV;
118678
+ }
118679
+ });
118073
118680
  Object.defineProperty(exports, "SocketChannel", {
118074
118681
  enumerable: true,
118075
118682
  get: function() {
@@ -118274,6 +118881,12 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
118274
118881
  return createV8Reclaimer;
118275
118882
  }
118276
118883
  });
118884
+ Object.defineProperty(exports, "describeRss", {
118885
+ enumerable: true,
118886
+ get: function() {
118887
+ return describeRss;
118888
+ }
118889
+ });
118277
118890
  Object.defineProperty(exports, "deserializeTypedArrays", {
118278
118891
  enumerable: true,
118279
118892
  get: function() {
@@ -118334,6 +118947,12 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
118334
118947
  return getWorkerNativeCapSnapshot;
118335
118948
  }
118336
118949
  });
118950
+ Object.defineProperty(exports, "hubMainRssBudget", {
118951
+ enumerable: true,
118952
+ get: function() {
118953
+ return hubMainRssBudget;
118954
+ }
118955
+ });
118337
118956
  Object.defineProperty(exports, "installManifestNativeDeps", {
118338
118957
  enumerable: true,
118339
118958
  get: function() {
@@ -118370,12 +118989,24 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
118370
118989
  return mountNativeCapService;
118371
118990
  }
118372
118991
  });
118992
+ Object.defineProperty(exports, "nextRssBudgetState", {
118993
+ enumerable: true,
118994
+ get: function() {
118995
+ return nextRssBudgetState;
118996
+ }
118997
+ });
118373
118998
  Object.defineProperty(exports, "parseCapAction", {
118374
118999
  enumerable: true,
118375
119000
  get: function() {
118376
119001
  return parseCapAction;
118377
119002
  }
118378
119003
  });
119004
+ Object.defineProperty(exports, "parseRssBudgetMb", {
119005
+ enumerable: true,
119006
+ get: function() {
119007
+ return parseRssBudgetMb;
119008
+ }
119009
+ });
118379
119010
  Object.defineProperty(exports, "registerEventBusService", {
118380
119011
  enumerable: true,
118381
119012
  get: function() {
@@ -122274,7 +122905,7 @@ var require_dist3 = __commonJS({
122274
122905
  "use strict";
122275
122906
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
122276
122907
  var require_chunk = require_chunk_Cek0wNdY();
122277
- var require_dist10 = require_dist_D3lqzV40();
122908
+ var require_dist10 = require_dist_BVU5JADq();
122278
122909
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
122279
122910
  require_alerts();
122280
122911
  var require_formatter = require_formatter_DqAKDlvN();
@@ -122300,7 +122931,7 @@ var require_dist3 = __commonJS({
122300
122931
  var require_builtins_winston_logging_index = require_winston_logging();
122301
122932
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
122302
122933
  var require_tls$1 = require_tls_u8QCJCFE();
122303
- var require_manifest_python_deps = require_manifest_python_deps_B_mCU6gz();
122934
+ var require_manifest_python_deps = require_manifest_python_deps_CktMcXzS();
122304
122935
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
122305
122936
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
122306
122937
  var zod = require_zod();
@@ -201766,7 +202397,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201766
202397
  }
201767
202398
  var EMPTY_HEAP_DECLARATION = {
201768
202399
  profile: void 0,
201769
- maxOldSpaceMb: void 0
202400
+ maxOldSpaceMb: void 0,
202401
+ rssBudgetMb: void 0
201770
202402
  };
201771
202403
  var heapProfileCache = /* @__PURE__ */ new Map();
201772
202404
  function readAddonHeapDeclaration(spec) {
@@ -201779,10 +202411,12 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201779
202411
  const parsed = JSON.parse(raw);
201780
202412
  const profile = extractHeapProfile(parsed, spec.addonId);
201781
202413
  const maxOldSpaceMb = extractMaxOldSpaceMb(parsed, spec.addonId);
201782
- if (profile !== void 0 || maxOldSpaceMb !== void 0) {
202414
+ const rssBudgetMb = extractRssBudgetMb(parsed, spec.addonId);
202415
+ if (profile !== void 0 || maxOldSpaceMb !== void 0 || rssBudgetMb !== void 0) {
201783
202416
  declaration = {
201784
202417
  profile,
201785
- maxOldSpaceMb
202418
+ maxOldSpaceMb,
202419
+ rssBudgetMb
201786
202420
  };
201787
202421
  break;
201788
202422
  }
@@ -201806,6 +202440,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201806
202440
  const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.maxOldSpaceMb;
201807
202441
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
201808
202442
  }
202443
+ function extractRssBudgetMb(parsed, addonId) {
202444
+ const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.rssBudgetMb;
202445
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
202446
+ }
201809
202447
  function readManifestAddons(parsed) {
201810
202448
  if (typeof parsed !== "object" || parsed === null) return [];
201811
202449
  const camstack = parsed.camstack;
@@ -201824,6 +202462,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201824
202462
  if (declared.includes(0)) return 0;
201825
202463
  return Math.max(...declared);
201826
202464
  }
202465
+ function runnerRssBudgetMb(addons) {
202466
+ const declared = addons.map((a) => readAddonHeapDeclaration(a).rssBudgetMb).filter((mb) => mb !== void 0);
202467
+ return declared.length === 0 ? void 0 : Math.max(...declared);
202468
+ }
201827
202469
  function runnerHeapFlags(addons) {
201828
202470
  if (process.env["CAMSTACK_RUNNER_HEAP_TUNING"] === "off") return [];
201829
202471
  const heavy = isHeavyRunner(addons);
@@ -201880,6 +202522,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201880
202522
  const nodeId = buildNodeId(runnerId);
201881
202523
  const runnerPath = node_path.resolve(__dirname, "addon-runner.js");
201882
202524
  const heavy = isHeavyRunner(addons);
202525
+ const rssBudgetMb = runnerRssBudgetMb(addons);
201883
202526
  const childEnv = {
201884
202527
  ...process.env,
201885
202528
  CAMSTACK_RUNNER_ID: runnerId,
@@ -201893,6 +202536,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
201893
202536
  ...applyRunnerNativeAllocator(process.env),
201894
202537
  ...env
201895
202538
  };
202539
+ if (rssBudgetMb === void 0) delete childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV];
202540
+ else childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV] = String(rssBudgetMb);
201896
202541
  const heapFlags = runnerHeapFlags(addons);
201897
202542
  capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
201898
202543
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
@@ -202683,6 +203328,8 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202683
203328
  exports.HEAP_WATCH_WARN_RATIO = require_manifest_python_deps.HEAP_WATCH_WARN_RATIO;
202684
203329
  exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
202685
203330
  exports.HUB_CAP_FWD_SERVICE = require_manifest_python_deps.HUB_CAP_FWD_SERVICE;
203331
+ exports.HUB_MAIN_RSS_BUDGET_MB = require_manifest_python_deps.HUB_MAIN_RSS_BUDGET_MB;
203332
+ exports.HUB_RSS_BUDGET_ENV = require_manifest_python_deps.HUB_RSS_BUDGET_ENV;
202686
203333
  exports.HubForwarderAddon = require_builtins_hub_forwarder_index.HubForwarderAddon$1;
202687
203334
  exports.HubForwarderDestination = require_builtins_hub_forwarder_index.HubForwarderDestination$1;
202688
203335
  exports.HubLogForwarder = HubLogForwarder;
@@ -202722,7 +203369,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202722
203369
  exports.PythonEnvManager = PythonEnvManager;
202723
203370
  exports.QUARANTINE_DIRNAME = QUARANTINE_DIRNAME;
202724
203371
  exports.RESTART_MARKER_FILE = RESTART_MARKER_FILE;
203372
+ exports.RSS_BUDGET_REANNOUNCE_MIN_MS = require_manifest_python_deps.RSS_BUDGET_REANNOUNCE_MIN_MS;
203373
+ exports.RSS_BUDGET_RELEASE_RATIO = require_manifest_python_deps.RSS_BUDGET_RELEASE_RATIO;
202725
203374
  exports.RUNNER_HEAP_WATCH_INTERVAL_MS = require_manifest_python_deps.RUNNER_HEAP_WATCH_INTERVAL_MS;
203375
+ exports.RUNNER_RSS_BUDGET_ENV = require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV;
202726
203376
  exports.RUNTIME_DEFAULTS = require_dist10.RUNTIME_DEFAULTS;
202727
203377
  exports.ReadinessRegistry = require_dist10.ReadinessRegistry;
202728
203378
  exports.ReadinessTimeoutError = require_dist10.ReadinessTimeoutError;
@@ -202819,6 +203469,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202819
203469
  exports.deleteModelFromDisk = require_file_data_plane.deleteModelFromDisk;
202820
203470
  exports.deriveAgentListenPort = deriveAgentListenPort;
202821
203471
  exports.describeProviderKindDrift = describeProviderKindDrift;
203472
+ exports.describeRss = require_manifest_python_deps.describeRss;
202822
203473
  exports.detectWorkspacePackagesDir = detectWorkspacePackagesDir;
202823
203474
  Object.defineProperty(exports, "downloadBinary", {
202824
203475
  enumerable: true,
@@ -202895,6 +203546,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202895
203546
  exports.getWorkerDeviceRegistry = require_manifest_python_deps.getWorkerDeviceRegistry;
202896
203547
  exports.hasDotNode = hasDotNode;
202897
203548
  exports.hashClusterSecret = hashClusterSecret;
203549
+ exports.hubMainRssBudget = require_manifest_python_deps.hubMainRssBudget;
202898
203550
  exports.installManifestNativeDeps = require_manifest_python_deps.installManifestNativeDeps;
202899
203551
  exports.installManifestPythonDeps = require_manifest_python_deps.installManifestPythonDeps;
202900
203552
  exports.installPackageFromNpm = installPackageFromNpm;
@@ -202923,8 +203575,10 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
202923
203575
  exports.localEndpointPath = require_manifest_python_deps.localEndpointPath;
202924
203576
  exports.localProviderLink = require_manifest_python_deps.localProviderLink;
202925
203577
  exports.mountNativeCapService = require_manifest_python_deps.mountNativeCapService;
203578
+ exports.nextRssBudgetState = require_manifest_python_deps.nextRssBudgetState;
202926
203579
  exports.parseCapAction = require_manifest_python_deps.parseCapAction;
202927
203580
  exports.parseRangeHeader = require_file_data_plane.parseRangeHeader;
203581
+ exports.parseRssBudgetMb = require_manifest_python_deps.parseRssBudgetMb;
202928
203582
  exports.parseTokenizedUrl = require_file_data_plane.parseTokenizedUrl;
202929
203583
  exports.partitionIsolatedBuiltinIds = partitionIsolatedBuiltinIds;
202930
203584
  exports.proxyToUpstream = proxyToUpstream;
@@ -203840,6 +204494,253 @@ var require_dist4 = __commonJS({
203840
204494
  function logLevelAtMost(level, threshold) {
203841
204495
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
203842
204496
  }
204497
+ var LogChannelLevelSchema = zod.z.enum([
204498
+ "info",
204499
+ "warn",
204500
+ "error"
204501
+ ]);
204502
+ var LogChannelDescriptorSchema = zod.z.object({
204503
+ /**
204504
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
204505
+ * the addon's short name so an operator reading a channel list can tell who
204506
+ * owns it without a second lookup.
204507
+ */
204508
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
204509
+ /** One sentence: what the operator will SEE after arming it. */
204510
+ description: zod.z.string().min(1),
204511
+ /** The level its lines are emitted at. Never below `info`. */
204512
+ defaultLevel: LogChannelLevelSchema,
204513
+ /**
204514
+ * Whether this channel can be narrowed to a camera.
204515
+ *
204516
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
204517
+ * consulted with the numeric device id, AND every line the channel admits
204518
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
204519
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
204520
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
204521
+ * the body is the only way to filter.
204522
+ *
204523
+ * A channel whose lines carry the device only in `meta` (or not at all) is
204524
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
204525
+ * the operator narrows to one camera, sees nothing, and concludes the code
204526
+ * path was never taken.
204527
+ */
204528
+ perDevice: zod.z.boolean()
204529
+ });
204530
+ var LogChannelWindowSchema = zod.z.object({
204531
+ channel: zod.z.string().min(1),
204532
+ /** Epoch ms the window closes at. */
204533
+ armedUntilMs: zod.z.number(),
204534
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
204535
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
204536
+ });
204537
+ var LogChannelGate = class {
204538
+ descriptor;
204539
+ /**
204540
+ * HOT PATH GUARD. A plain data FIELD, and it must stay one.
204541
+ *
204542
+ * `log-channel.spec.ts` asserts the property descriptor has no getter and
204543
+ * booby-traps the device set, so turning this into an accessor — or reading
204544
+ * anything before it — fails the spec instead of taxing every line the
204545
+ * process emits.
204546
+ */
204547
+ on = false;
204548
+ /** `null` while armed for every camera. Never read while `on` is false. */
204549
+ devices = null;
204550
+ level;
204551
+ closesAtMs = 0;
204552
+ constructor(descriptor) {
204553
+ this.descriptor = descriptor;
204554
+ this.level = descriptor.defaultLevel;
204555
+ }
204556
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
204557
+ get armedUntilMs() {
204558
+ return this.on ? this.closesAtMs : 0;
204559
+ }
204560
+ /**
204561
+ * Does this channel want a line about `deviceId`?
204562
+ *
204563
+ * Call it only behind `gate.on &&`. On its own it is still correct — the
204564
+ * guard is repeated inside — but the point of the prefix is that a disarmed
204565
+ * channel must not pay the call at all.
204566
+ */
204567
+ wants(deviceId) {
204568
+ if (!this.on) return false;
204569
+ return this.devices === null || this.devices.has(deviceId);
204570
+ }
204571
+ /**
204572
+ * Emit one line on this channel, at the channel's declared level.
204573
+ *
204574
+ * The channel name is added as `tags.logChannel` so LogQL can select the
204575
+ * channel without matching on the message text, and whatever `tags` the
204576
+ * caller passed — `deviceId` above all — is preserved.
204577
+ */
204578
+ log(logger, message, extras) {
204579
+ if (!this.on) return;
204580
+ const tags = {
204581
+ ...extras.tags,
204582
+ logChannel: this.descriptor.name
204583
+ };
204584
+ const line = {
204585
+ ...extras,
204586
+ tags
204587
+ };
204588
+ if (this.level === "error") logger.error(message, line);
204589
+ else if (this.level === "warn") logger.warn(message, line);
204590
+ else logger.info(message, line);
204591
+ }
204592
+ /**
204593
+ * Arm (or RE-arm, restarting) this channel. Off the hot path only.
204594
+ *
204595
+ * An empty `deviceIds` list is treated as "every camera" rather than "no
204596
+ * camera": a window that matches nothing is indistinguishable from a
204597
+ * disarmed one, and the operator who asked for it would wait for lines that
204598
+ * can never come.
204599
+ */
204600
+ arm(window2) {
204601
+ const ids = window2.deviceIds;
204602
+ this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
204603
+ this.closesAtMs = window2.armedUntilMs;
204604
+ this.on = true;
204605
+ }
204606
+ /** Disarm. Off the hot path only. */
204607
+ disarm() {
204608
+ this.on = false;
204609
+ this.devices = null;
204610
+ this.closesAtMs = 0;
204611
+ }
204612
+ };
204613
+ var LogChannelRegistry = class {
204614
+ gates = /* @__PURE__ */ new Map();
204615
+ /**
204616
+ * Declare a channel and get its gate.
204617
+ *
204618
+ * A duplicate name throws. Two declarations of one name is a programming
204619
+ * error, not a merge: the operator would arm one and the other would stay
204620
+ * dark, which is the dead-knob shape (D62) with an extra step.
204621
+ */
204622
+ declare(descriptor) {
204623
+ const parsed = LogChannelDescriptorSchema.parse(descriptor);
204624
+ if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process \u2014 two declarations of one name is a programming error, not a merge`);
204625
+ const gate = new LogChannelGate(parsed);
204626
+ this.gates.set(parsed.name, gate);
204627
+ return gate;
204628
+ }
204629
+ /** The declarations, sorted by name so a list is stable to read and diff. */
204630
+ list() {
204631
+ return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
204632
+ }
204633
+ /** The gate for a declared channel, or `undefined`. */
204634
+ gate(name) {
204635
+ return this.gates.get(name);
204636
+ }
204637
+ /**
204638
+ * Apply the FULL set of armed windows. Off the hot path.
204639
+ *
204640
+ * Full, not incremental, and that is the whole design: the document is the
204641
+ * authority, so a channel the document does not name is disarmed here. An
204642
+ * incremental apply would let a disarm get lost in transit and leave a
204643
+ * channel running that nobody can see is running.
204644
+ *
204645
+ * A window already past its deadline is ignored rather than armed — a
204646
+ * restore that re-armed an expired window would make a forgotten diagnostic
204647
+ * immortal across restarts.
204648
+ *
204649
+ * Returns the names it could not place, so the caller can log them: a
204650
+ * channel named in the document that this process does not declare is
204651
+ * either a typo or an addon that has not booted yet, and both deserve a
204652
+ * line rather than silence.
204653
+ */
204654
+ apply(windows, nowMs) {
204655
+ const wanted = /* @__PURE__ */ new Map();
204656
+ const unknown = [];
204657
+ for (const window2 of windows) {
204658
+ if (window2.armedUntilMs <= nowMs) continue;
204659
+ if (!this.gates.has(window2.channel)) {
204660
+ unknown.push(window2.channel);
204661
+ continue;
204662
+ }
204663
+ wanted.set(window2.channel, window2);
204664
+ }
204665
+ for (const [name, gate] of this.gates) {
204666
+ const window2 = wanted.get(name);
204667
+ if (window2 === void 0) gate.disarm();
204668
+ else gate.arm(window2);
204669
+ }
204670
+ return unknown;
204671
+ }
204672
+ /**
204673
+ * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
204674
+ * diagnostic that adds a `Date.now()` to the path it is measuring measures
204675
+ * itself.
204676
+ *
204677
+ * Returns the names it closed, so the caller can write the one line that
204678
+ * says a window ended and stops "it went quiet" from reading as "the branch
204679
+ * was not taken".
204680
+ */
204681
+ tick(nowMs) {
204682
+ const closed = [];
204683
+ for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
204684
+ gate.disarm();
204685
+ closed.push(name);
204686
+ }
204687
+ return closed;
204688
+ }
204689
+ /** The channels armed right now, as the document would describe them. */
204690
+ armed() {
204691
+ const out = [];
204692
+ for (const [name, gate] of this.gates) if (gate.on) out.push({
204693
+ channel: name,
204694
+ armedUntilMs: gate.armedUntilMs,
204695
+ deviceIds: null
204696
+ });
204697
+ return out;
204698
+ }
204699
+ };
204700
+ var instance = null;
204701
+ function getLogChannelRegistry() {
204702
+ instance ??= new LogChannelRegistry();
204703
+ return instance;
204704
+ }
204705
+ function declareLogChannel(descriptor) {
204706
+ return getLogChannelRegistry().declare(descriptor);
204707
+ }
204708
+ function __resetLogChannelRegistryForTests() {
204709
+ instance = null;
204710
+ }
204711
+ var LOG_CHANNEL_TICK_MS = 5e3;
204712
+ function createLogChannelsProvider(logger, options = {}) {
204713
+ const registry = getLogChannelRegistry();
204714
+ const now = options.now ?? Date.now;
204715
+ const tickMs = options.tickMs ?? 5e3;
204716
+ const timer = setInterval(() => {
204717
+ const closed = registry.tick(now());
204718
+ for (const name of closed) logger.info("log channel window closed", {
204719
+ tags: { logChannel: name },
204720
+ meta: { channel: name }
204721
+ });
204722
+ }, tickMs);
204723
+ timer.unref?.();
204724
+ return {
204725
+ list: () => registry.list(),
204726
+ apply: (input) => {
204727
+ const unknown = registry.apply(input.windows, now());
204728
+ const armed = registry.armed();
204729
+ logger.info("log channels applied", { meta: {
204730
+ armed: armed.map((window2) => window2.channel),
204731
+ unknown,
204732
+ declared: registry.list().length
204733
+ } });
204734
+ return {
204735
+ armed: armed.length,
204736
+ unknown
204737
+ };
204738
+ },
204739
+ stop: () => {
204740
+ clearInterval(timer);
204741
+ }
204742
+ };
204743
+ }
203843
204744
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
203844
204745
  var OpsLogOpSchema = zod.z.enum([
203845
204746
  "prune",
@@ -208416,6 +209317,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208416
209317
  whereBetween: zod.z.record(zod.z.string(), zod.z.tuple([zod.z.unknown(), zod.z.unknown()])).optional(),
208417
209318
  whereNot: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
208418
209319
  });
209320
+ var AggregateFieldSchema = zod.z.object({
209321
+ /** Result key. */
209322
+ as: zod.z.string().min(1),
209323
+ /** Column to aggregate. Must be a real column of a declared collection. */
209324
+ field: zod.z.string().min(1),
209325
+ op: zod.z.enum([
209326
+ "sum",
209327
+ "min",
209328
+ "max"
209329
+ ])
209330
+ });
209331
+ var AggregateResultSchema = zod.z.object({
209332
+ count: zod.z.number().int(),
209333
+ values: zod.z.record(zod.z.string(), zod.z.number().nullable())
209334
+ });
208419
209335
  var SettingsRecordSchema = zod.z.object({
208420
209336
  id: zod.z.string(),
208421
209337
  data: zod.z.record(zod.z.string(), zod.z.unknown())
@@ -208543,6 +209459,32 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208543
209459
  collection: zod.z.string(),
208544
209460
  filter: QueryFilterSchema.optional()
208545
209461
  }), zod.z.number()),
209462
+ /**
209463
+ * `COUNT(*)` and one `SUM` / `MIN` / `MAX` per requested field, in ONE
209464
+ * statement, over the rows `filter` selects.
209465
+ *
209466
+ * Exists because "how much is there" was being answered by materialising
209467
+ * "what is there". The recorder's storage-pressure sweep asked its in-RAM
209468
+ * footage index for bytes/count/oldest/newest across a set of storage
209469
+ * locations twice a minute, and the only way to answer that from a map is
209470
+ * to visit every row — 7.1 M of them on the live hub, ~15 M row visits a
209471
+ * minute on the main thread, which is also why the whole archive had to
209472
+ * stay resident to be visited. The question is a sum; nothing needs to be
209473
+ * materialised to answer it.
209474
+ *
209475
+ * **The engine REFUSES a field it cannot serve**, exactly as
209476
+ * `query.columns` does and unlike a PREDICATE, which is skipped when
209477
+ * unresolvable. A dropped predicate over-matches and the caller sees extra
209478
+ * rows; a dropped aggregate returns a NUMBER that is wrong and looks
209479
+ * exactly like a real one. That asymmetry is what this repo has already
209480
+ * paid for once in `count`.
209481
+ */
209482
+ aggregate: require_sleep.method(zod.z.object({
209483
+ namespace: zod.z.string().optional(),
209484
+ collection: zod.z.string(),
209485
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
209486
+ filter: QueryFilterSchema.optional()
209487
+ }), AggregateResultSchema),
208546
209488
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
208547
209489
  histogram: require_sleep.method(zod.z.object({
208548
209490
  namespace: zod.z.string().optional(),
@@ -208701,6 +209643,15 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208701
209643
  collection: zod.z.string(),
208702
209644
  filter: QueryFilterSchema.optional()
208703
209645
  }), zod.z.number(), { auth: "admin" }),
209646
+ /** `COUNT(*)` plus one SUM/MIN/MAX per field, in one statement. Mirror of
209647
+ * `settings-store.aggregate` — see it for why an unresolvable field is
209648
+ * refused rather than dropped. */
209649
+ aggregate: require_sleep.method(zod.z.object({
209650
+ namespace: zod.z.string().optional(),
209651
+ collection: zod.z.string(),
209652
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
209653
+ filter: QueryFilterSchema.optional()
209654
+ }), AggregateResultSchema, { auth: "admin" }),
208704
209655
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
208705
209656
  histogram: require_sleep.method(zod.z.object({
208706
209657
  namespace: zod.z.string().optional(),
@@ -209415,6 +210366,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209415
210366
  })
209416
210367
  }
209417
210368
  };
210369
+ var DEVICE_CHILDREN_BATCH_MAX = 256;
209418
210370
  var ChildLayoutEntrySchema = zod.z.object({
209419
210371
  childKey: zod.z.string(),
209420
210372
  section: zod.z.string(),
@@ -209896,6 +210848,39 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209896
210848
  /** List children of a parent device (by parent numeric id). */
209897
210849
  getChildren: require_sleep.method(zod.z.object({ parentDeviceId: zod.z.number() }), zod.z.array(DeviceInfoSchema)),
209898
210850
  /**
210851
+ * `getChildren` for a NAMED SET of parents, in one call.
210852
+ *
210853
+ * The accessory reconcile in `device-cap-proxy.ts` asks this question once
210854
+ * per registered device — every `BaseDevice` inherits a
210855
+ * `getAccessoryChildren()` that returns `[]`, so even a leaf accessory
210856
+ * pays a round-trip to learn it has nothing to prune. Measured on the live
210857
+ * hub 2026-08-27 over a 120-second boot window, fleet of 1 017 devices:
210858
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
210859
+ * **1 024 calls** returning **919 rows in total** — 1 024 RPCs and 1 024
210860
+ * indexed scans to move less than one row each. `listByParentMany`
210861
+ * collapses the scans; this collapses the RPCs.
210862
+ *
210863
+ * Keyed by parent id as a STRING — a JSON object cannot key by number
210864
+ * (same reason as `getDeviceStatusAggregateBatch`). The per-parent value
210865
+ * is exactly what `getChildren` returns for that parent.
210866
+ *
210867
+ * A parent with no children — or one the fleet does not know — is ABSENT
210868
+ * from the record, never an invented empty row: the same contract as
210869
+ * `DeviceRowStore.getMany`/`listByParentMany`. An EMPTY `parentDeviceIds`
210870
+ * reads nothing at all rather than degrading to "every device".
210871
+ *
210872
+ * `parentDeviceIds` is capped at {@link DEVICE_CHILDREN_BATCH_MAX} — see
210873
+ * that constant for why. A caller with more parents than that sends more
210874
+ * than one call; it never sends one pathological one.
210875
+ *
210876
+ * Version skew: this is a NEW method, not a new field on `getChildren`, so
210877
+ * a hub that predates it answers NOT_FOUND rather than silently stripping
210878
+ * an unknown input key and answering a DIFFERENT question. The kernel-side
210879
+ * loader degrades to per-parent `getChildren` on that error — see
210880
+ * `children-batch-loader.ts`.
210881
+ */
210882
+ getChildrenBatch: require_sleep.method(zod.z.object({ parentDeviceIds: zod.z.array(zod.z.number()).max(256) }), zod.z.record(zod.z.string(), zod.z.array(DeviceInfoSchema))),
210883
+ /**
209899
210884
  * Resolve the devices LINKED to a camera — the single policy authority
209900
210885
  * both consumers call (viewer devices panel + pipeline-analytics event
209901
210886
  * kinds/ingest). Device-tree children are ALWAYS included; mode 'auto'
@@ -210947,6 +211932,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
210947
211932
  })
210948
211933
  }
210949
211934
  };
211935
+ var LogChannelApplyResultSchema = zod.z.object({
211936
+ /** How many declared channels are armed in this process after the call. */
211937
+ armed: zod.z.number().int().min(0),
211938
+ /**
211939
+ * Names the document armed that this process does not declare. Reported
211940
+ * rather than swallowed: a name here is either a typo or an addon that has
211941
+ * not booted, and both deserve a line instead of silence.
211942
+ */
211943
+ unknown: zod.z.array(zod.z.string()).readonly()
211944
+ });
211945
+ var logChannelsCapability = {
211946
+ name: "log-channels",
211947
+ scope: "system",
211948
+ mode: "collection",
211949
+ internal: true,
211950
+ methods: {
211951
+ /** The channels this addon declares. Inert: no value, no state. */
211952
+ list: require_sleep.method(zod.z.void(), zod.z.array(LogChannelDescriptorSchema).readonly()),
211953
+ /**
211954
+ * Refresh this process's mirror from the document's FULL set of armed
211955
+ * windows.
211956
+ *
211957
+ * Full and not incremental on purpose: the document is the authority, so a
211958
+ * channel it does not name is disarmed here. An incremental apply would
211959
+ * let a disarm get lost in transit and leave a channel running that
211960
+ * nobody can see is running.
211961
+ */
211962
+ apply: require_sleep.method(zod.z.object({ windows: zod.z.array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" })
211963
+ },
211964
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
211965
+ mount: { kind: "skip" }
211966
+ };
210950
211967
  var LogLevelSchema = zod.z.enum([
210951
211968
  "debug",
210952
211969
  "info",
@@ -227266,6 +228283,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227266
228283
  scope: LoggingScopeKindSchema,
227267
228284
  /** The node this layer speaks for; `null` on the cluster layer. */
227268
228285
  nodeId: zod.z.string().nullable(),
228286
+ /**
228287
+ * The declared channel this layer speaks for; `null` on every layer but
228288
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
228289
+ * by design — the convention this repo settled on is one orchestrator-wide
228290
+ * setting, never per node (D52) — so a component layer that carried a node
228291
+ * would invite a per-node copy of a value that has no per-node meaning.
228292
+ */
228293
+ component: zod.z.string().nullable(),
227269
228294
  /** Explicitly set here, or `null` when this layer inherits. */
227270
228295
  level: LogLevelSchema$1.nullable()
227271
228296
  });
@@ -227291,6 +228316,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227291
228316
  /** Cadence of the diagnostic's aggregated report line. Clamped by the server. */
227292
228317
  reportEveryMs: zod.z.number().int().positive().optional()
227293
228318
  });
228319
+ var LogChannelWindowStateSchema = zod.z.object({
228320
+ channel: zod.z.string(),
228321
+ armed: zod.z.boolean(),
228322
+ /** Epoch ms the window closes at. 0 when disarmed. */
228323
+ armedUntilMs: zod.z.number(),
228324
+ /** Ms left before it expires on its own. 0 when disarmed. */
228325
+ remainingMs: zod.z.number(),
228326
+ /**
228327
+ * The cameras it is narrowed to, or `null` for every camera.
228328
+ *
228329
+ * A channel declared `perDevice: false` can only ever report `null` here:
228330
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
228331
+ * produce a filter that silently matches nothing. The server REFUSES such a
228332
+ * patch rather than quietly widening it — ignoring the request would teach
228333
+ * the operator that per-camera filtering works on that channel when it does
228334
+ * not.
228335
+ */
228336
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
228337
+ });
228338
+ var LogChannelWindowPatchSchema = zod.z.object({
228339
+ channel: zod.z.string().min(1),
228340
+ armMs: zod.z.number().int().min(0),
228341
+ /**
228342
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
228343
+ *
228344
+ * Numeric because the repo's own rule makes it possible: every log line
228345
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
228346
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
228347
+ * diagnosed by hand, and this is the first thing that collects on it.
228348
+ */
228349
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable().optional()
228350
+ });
227294
228351
  var LoggingSettingsPatchSchema = zod.z.object({
227295
228352
  /**
227296
228353
  * Absent leaves the level untouched. `null` CLEARS the explicit value at the
@@ -227301,19 +228358,50 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227301
228358
  * Only the diagnostics NAMED here change. An armed window that is not listed
227302
228359
  * keeps running — a patch is never a full replacement.
227303
228360
  */
227304
- diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional()
228361
+ diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional(),
228362
+ /**
228363
+ * Only the channels NAMED here change. An armed channel that is not listed
228364
+ * keeps running — same rule as `diagnostics`, because a patch that silently
228365
+ * disarmed the channels it did not mention would make the Levels page and
228366
+ * the Diagnostics page fight over the same value.
228367
+ */
228368
+ channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
228369
+ });
228370
+ var GetLoggingSettingsInputSchema = zod.z.object({
228371
+ scopeNodeId: zod.z.string().optional(),
228372
+ /**
228373
+ * The declared CHANNEL this document is addressed at, when the caller wants
228374
+ * the `component` layer. Absent = the node/cluster hierarchy only.
228375
+ *
228376
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
228377
+ * axes from collapsing: a component level is cluster-wide, a node level is
228378
+ * not, and one selector for both would make "which of these two did I just
228379
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
228380
+ */
228381
+ scopeComponent: zod.z.string().optional()
227305
228382
  });
227306
- var GetLoggingSettingsInputSchema = zod.z.object({ scopeNodeId: zod.z.string().optional() });
227307
228383
  var SetLoggingSettingsInputSchema = zod.z.object({
227308
228384
  scopeNodeId: zod.z.string().optional(),
228385
+ scopeComponent: zod.z.string().optional(),
227309
228386
  patch: LoggingSettingsPatchSchema
227310
228387
  });
227311
228388
  var LoggingSettingsStateSchema = zod.z.object({
227312
228389
  /** The layer this document was read at. `null` = the cluster layer. */
227313
228390
  scopeNodeId: zod.z.string().nullable(),
228391
+ /** The channel this document was read at. `null` = no component layer. */
228392
+ scopeComponent: zod.z.string().nullable(),
227314
228393
  effective: LoggingEffectiveSchema,
227315
228394
  explicit: LoggingExplicitSchema,
227316
228395
  activeWindows: zod.z.array(DiagnosticWindowSchema).readonly(),
228396
+ /**
228397
+ * Every channel the cluster's addons DECLARE, gathered from the
228398
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
228399
+ * channel added by a redeployed addon appears without anybody editing a
228400
+ * list, and a channel whose addon is gone stops being offered.
228401
+ */
228402
+ channels: zod.z.array(LogChannelDescriptorSchema).readonly(),
228403
+ /** The channels ARMED right now, each with its deadline. */
228404
+ activeChannels: zod.z.array(LogChannelWindowStateSchema).readonly(),
227317
228405
  persisted: zod.z.boolean()
227318
228406
  });
227319
228407
  var systemCapability = {
@@ -230945,6 +232033,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230945
232033
  llmRuntime: "llm-runtime",
230946
232034
  localNetwork: "local-network",
230947
232035
  lockControl: "lock-control",
232036
+ logChannels: "log-channels",
230948
232037
  logDestination: "log-destination",
230949
232038
  loginMethod: "login-method",
230950
232039
  mediaPlayer: "media-player",
@@ -231315,6 +232404,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
231315
232404
  key: "lockControl",
231316
232405
  name: "lock-control"
231317
232406
  },
232407
+ {
232408
+ key: "logChannels",
232409
+ name: "log-channels"
232410
+ },
231318
232411
  {
231319
232412
  key: "logDestination",
231320
232413
  name: "log-destination"
@@ -231694,6 +232787,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
231694
232787
  llmRuntimeCapability,
231695
232788
  localNetworkCapability,
231696
232789
  lockControlCapability,
232790
+ logChannelsCapability,
231697
232791
  logDestinationCapability,
231698
232792
  loginMethodCapability,
231699
232793
  mediaPlayerCapability,
@@ -232660,6 +233754,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232660
233754
  addonId: null,
232661
233755
  access: "view"
232662
233756
  },
233757
+ "dataStoreProvider.aggregate": {
233758
+ capName: "data-store-provider",
233759
+ capScope: "system",
233760
+ addonId: null,
233761
+ access: "view"
233762
+ },
232663
233763
  "dataStoreProvider.count": {
232664
233764
  capName: "data-store-provider",
232665
233765
  capScope: "system",
@@ -233074,6 +234174,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
233074
234174
  addonId: null,
233075
234175
  access: "view"
233076
234176
  },
234177
+ "deviceManager.getChildrenBatch": {
234178
+ capName: "device-manager",
234179
+ capScope: "system",
234180
+ addonId: null,
234181
+ access: "view"
234182
+ },
233077
234183
  "deviceManager.getConfigSchema": {
233078
234184
  capName: "device-manager",
233079
234185
  capScope: "system",
@@ -234124,6 +235230,18 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234124
235230
  addonId: null,
234125
235231
  access: "create"
234126
235232
  },
235233
+ "logChannels.apply": {
235234
+ capName: "log-channels",
235235
+ capScope: "system",
235236
+ addonId: null,
235237
+ access: "create"
235238
+ },
235239
+ "logChannels.list": {
235240
+ capName: "log-channels",
235241
+ capScope: "system",
235242
+ addonId: null,
235243
+ access: "view"
235244
+ },
234127
235245
  "logDestination.query": {
234128
235246
  capName: "log-destination",
234129
235247
  capScope: "system",
@@ -236278,6 +237396,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
236278
237396
  addonId: null,
236279
237397
  access: "create"
236280
237398
  },
237399
+ "settingsStore.aggregate": {
237400
+ capName: "settings-store",
237401
+ capScope: "system",
237402
+ addonId: null,
237403
+ access: "view"
237404
+ },
236281
237405
  "settingsStore.count": {
236282
237406
  capName: "settings-store",
236283
237407
  capScope: "system",
@@ -237626,6 +238750,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237626
238750
  "llm-runtime",
237627
238751
  "local-network",
237628
238752
  "lock-control",
238753
+ "log-channels",
237629
238754
  "log-destination",
237630
238755
  "login-method",
237631
238756
  "media-player",
@@ -237782,6 +238907,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237782
238907
  "llm",
237783
238908
  "llm-runtime",
237784
238909
  "local-network",
238910
+ "log-channels",
237785
238911
  "log-destination",
237786
238912
  "login-method",
237787
238913
  "mesh-network",
@@ -238109,6 +239235,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238109
239235
  form: "single",
238110
239236
  optional: false
238111
239237
  }],
239238
+ "deviceManager.getChildrenBatch": [{
239239
+ name: "parentDeviceIds",
239240
+ form: "array",
239241
+ optional: false
239242
+ }],
238112
239243
  "deviceManager.getConfigSchema": [{
238113
239244
  name: "deviceId",
238114
239245
  form: "single",
@@ -239583,6 +240714,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
239583
240714
  "deviceManager.getBindings",
239584
240715
  "deviceManager.getBindingsBatch",
239585
240716
  "deviceManager.getChildren",
240717
+ "deviceManager.getChildrenBatch",
239586
240718
  "deviceManager.getConfigSchema",
239587
240719
  "deviceManager.getDevice",
239588
240720
  "deviceManager.getDeviceAggregate",
@@ -240275,6 +241407,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240275
241407
  listPersistedByAddon: (input) => dispatch("deviceManager", "listPersistedByAddon", "query", input),
240276
241408
  listAll: (input) => dispatch("deviceManager", "listAll", "query", input),
240277
241409
  getChildren: (input) => dispatch("deviceManager", "getChildren", "query", input),
241410
+ getChildrenBatch: (input) => dispatch("deviceManager", "getChildrenBatch", "query", input),
240278
241411
  getLinkedDevicesBatch: (input) => dispatch("deviceManager", "getLinkedDevicesBatch", "query", input),
240279
241412
  removeByIntegration: (input) => dispatch("deviceManager", "removeByIntegration", "mutation", input),
240280
241413
  getBindingsBatch: (input) => dispatch("deviceManager", "getBindingsBatch", "query", input),
@@ -240589,6 +241722,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240589
241722
  deleteWhere: (input) => dispatch("settingsStore", "deleteWhere", "mutation", input),
240590
241723
  updateWhere: (input) => dispatch("settingsStore", "updateWhere", "mutation", input),
240591
241724
  count: (input) => dispatch("settingsStore", "count", "query", input),
241725
+ aggregate: (input) => dispatch("settingsStore", "aggregate", "query", input),
240592
241726
  histogram: (input) => dispatch("settingsStore", "histogram", "query", input),
240593
241727
  isEmpty: (input) => dispatch("settingsStore", "isEmpty", "query", input),
240594
241728
  declareCollection: (input) => dispatch("settingsStore", "declareCollection", "mutation", input)
@@ -243598,6 +244732,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243598
244732
  exports.DETECTION_PIPELINE_CAP_NAME = DETECTION_PIPELINE_CAP_NAME;
243599
244733
  exports.DEVICE_BACKEND_TO_FORMAT = DEVICE_BACKEND_TO_FORMAT;
243600
244734
  exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
244735
+ exports.DEVICE_CHILDREN_BATCH_MAX = DEVICE_CHILDREN_BATCH_MAX;
243601
244736
  exports.DEVICE_PROFILES = DEVICE_PROFILES;
243602
244737
  exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
243603
244738
  exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
@@ -243748,6 +244883,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243748
244883
  exports.IntercomStatusSchema = IntercomStatusSchema;
243749
244884
  exports.KNOWN_CAP_NAMES = KNOWN_CAP_NAMES;
243750
244885
  exports.KeyEventSchema = KeyEventSchema;
244886
+ exports.LOG_CHANNEL_TICK_MS = LOG_CHANNEL_TICK_MS;
243751
244887
  exports.LOG_LEVEL_RANK = LOG_LEVEL_RANK;
243752
244888
  exports.LabelAttributionSchema = LabelAttributionSchema;
243753
244889
  exports.LabelDefinitionSchema = LabelDefinitionSchema;
@@ -243783,6 +244919,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243783
244919
  exports.LocationStatSchema = LocationStatSchema;
243784
244920
  exports.LockControlStatusSchema = LockControlStatusSchema;
243785
244921
  exports.LockStateSchema = LockStateSchema;
244922
+ exports.LogChannelApplyResultSchema = LogChannelApplyResultSchema;
244923
+ exports.LogChannelDescriptorSchema = LogChannelDescriptorSchema;
244924
+ exports.LogChannelGate = LogChannelGate;
244925
+ exports.LogChannelLevelSchema = LogChannelLevelSchema;
244926
+ exports.LogChannelRegistry = LogChannelRegistry;
244927
+ exports.LogChannelWindowPatchSchema = LogChannelWindowPatchSchema;
244928
+ exports.LogChannelWindowSchema = LogChannelWindowSchema;
244929
+ exports.LogChannelWindowStateSchema = LogChannelWindowStateSchema;
243786
244930
  exports.LogEntrySchema = LogEntrySchema;
243787
244931
  exports.LogLevelSchema = LogLevelSchema;
243788
244932
  exports.LogStreamEntrySchema = LogStreamEntrySchema;
@@ -244333,6 +245477,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244333
245477
  exports.ZoneRulesArraySchema = ZoneRulesArraySchema;
244334
245478
  exports.ZoneSchema = ZoneSchema;
244335
245479
  exports.ZoneScopeBreakdownSchema = ZoneScopeBreakdownSchema;
245480
+ exports.__resetLogChannelRegistryForTests = __resetLogChannelRegistryForTests;
244336
245481
  exports.accessoriesCapability = accessoriesCapability;
244337
245482
  exports.accessoryStableId = accessoryStableId;
244338
245483
  exports.addonPagesCapability = addonPagesCapability;
@@ -244429,6 +245574,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244429
245574
  exports.createExpressionScope = createExpressionScope;
244430
245575
  exports.createHwAccelCache = createHwAccelCache;
244431
245576
  exports.createLazyTrpcSource = require_sleep.createLazyTrpcSource;
245577
+ exports.createLogChannelsProvider = createLogChannelsProvider;
244432
245578
  exports.createMirrorSource = require_sleep.createMirrorSource;
244433
245579
  exports.createRuntimeStateBridge = createRuntimeStateBridge;
244434
245580
  exports.createSliceHandle = require_sleep.createSliceHandle;
@@ -244438,6 +245584,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244438
245584
  exports.dataStoreProviderCapability = dataStoreProviderCapability;
244439
245585
  exports.dayNightCapability = dayNightCapability;
244440
245586
  exports.declarationOwnerNodeId = declarationOwnerNodeId;
245587
+ exports.declareLogChannel = declareLogChannel;
244441
245588
  exports.decodeVectorBase64 = decodeVectorBase64;
244442
245589
  exports.decoderCapability = decoderCapability;
244443
245590
  exports.defaultDeliveryForSection = defaultDeliveryForSection;
@@ -244500,6 +245647,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244500
245647
  exports.getAudioMacroClassIds = getAudioMacroClassIds;
244501
245648
  exports.getByPath = getByPath;
244502
245649
  exports.getCapsByProviderKind = getCapsByProviderKind;
245650
+ exports.getLogChannelRegistry = getLogChannelRegistry;
244503
245651
  exports.getTaxonomyEntry = getTaxonomyEntry;
244504
245652
  exports.hasMotionTrigger = hasMotionTrigger;
244505
245653
  exports.hfModelUrl = hfModelUrl;
@@ -244553,6 +245701,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244553
245701
  exports.locationSimilarity = locationSimilarity;
244554
245702
  exports.lockControlCapability = lockControlCapability;
244555
245703
  exports.logBannerArgs = require_canonical_hash.logBannerArgs;
245704
+ exports.logChannelsCapability = logChannelsCapability;
244556
245705
  exports.logDestinationCapability = logDestinationCapability;
244557
245706
  exports.logLevelAtMost = logLevelAtMost;
244558
245707
  exports.loginMethodCapability = loginMethodCapability;
@@ -401844,17 +402993,55 @@ var require_logging_settings = __commonJS({
401844
402993
  "../../server/backend/dist/api/core/logging-settings.js"(exports) {
401845
402994
  "use strict";
401846
402995
  Object.defineProperty(exports, "__esModule", { value: true });
401847
- exports.LoggingSettingsService = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
402996
+ exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
402997
+ exports.buildLogChannelPlane = buildLogChannelPlane;
401848
402998
  exports.toLoggingSettingsRecord = toLoggingSettingsRecord;
401849
402999
  exports.resolveLevel = resolveLevel;
401850
403000
  exports.describeLayers = describeLayers;
401851
403001
  exports.mergeLevel = mergeLevel;
403002
+ exports.validateChannelPatch = validateChannelPatch;
403003
+ exports.mergeChannelWindows = mergeChannelWindows;
403004
+ exports.liveChannelWindows = liveChannelWindows;
403005
+ exports.describeChannelWindows = describeChannelWindows;
401852
403006
  var types_1 = require_dist4();
401853
403007
  var system_1 = require_dist3();
401854
403008
  exports.LOGGING_SETTINGS_KEY = "logging-settings";
401855
403009
  var SYSTEM_SETTINGS_COLLECTION = "system-settings";
401856
403010
  exports.HUB_NODE_ID = "hub";
401857
403011
  var LOG_LEVELS = ["debug", "info", "warn", "error"];
403012
+ exports.EMPTY_LOG_CHANNEL_PLANE = {
403013
+ declarations: async () => [],
403014
+ apply: async () => {
403015
+ }
403016
+ };
403017
+ function buildLogChannelPlane(source, onProviderError) {
403018
+ return {
403019
+ declarations: async () => {
403020
+ const seen = /* @__PURE__ */ new Map();
403021
+ for (const [addonId, provider] of source.entries()) {
403022
+ try {
403023
+ for (const descriptor of await provider.list()) {
403024
+ if (!seen.has(descriptor.name))
403025
+ seen.set(descriptor.name, descriptor);
403026
+ }
403027
+ } catch (err) {
403028
+ onProviderError?.(addonId, "list", (0, types_1.errMsg)(err));
403029
+ }
403030
+ }
403031
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
403032
+ },
403033
+ apply: async (windows) => {
403034
+ for (const [addonId, provider] of source.entries()) {
403035
+ try {
403036
+ await provider.apply({ windows });
403037
+ } catch (err) {
403038
+ onProviderError?.(addonId, "apply", (0, types_1.errMsg)(err));
403039
+ }
403040
+ }
403041
+ }
403042
+ };
403043
+ }
403044
+ exports.MAX_CHANNEL_WINDOW_MS = 30 * 6e4;
401858
403045
  var LoggingSettingsService = class {
401859
403046
  store;
401860
403047
  gate;
@@ -401862,6 +403049,7 @@ var require_logging_settings = __commonJS({
401862
403049
  localNodeId;
401863
403050
  logger;
401864
403051
  now;
403052
+ channels;
401865
403053
  constructor(deps) {
401866
403054
  this.store = deps.store;
401867
403055
  this.gate = deps.gate;
@@ -401869,6 +403057,7 @@ var require_logging_settings = __commonJS({
401869
403057
  this.localNodeId = deps.localNodeId ?? exports.HUB_NODE_ID;
401870
403058
  this.logger = deps.logger;
401871
403059
  this.now = deps.now ?? Date.now;
403060
+ this.channels = deps.channels ?? exports.EMPTY_LOG_CHANNEL_PLANE;
401872
403061
  }
401873
403062
  /**
401874
403063
  * The document as it stands, resolved for `nodeId` (absent = the cluster).
@@ -401877,9 +403066,9 @@ var require_logging_settings = __commonJS({
401877
403066
  * and, crucially, does not touch the mirror. What the operator sees is
401878
403067
  * honestly labelled as un-persisted; what the hub does is unchanged.
401879
403068
  */
401880
- async get(scopeNodeId) {
403069
+ async get(scopeNodeId, scopeComponent) {
401881
403070
  const record = await this.read();
401882
- return this.describe(record, scopeNodeId ?? null);
403071
+ return this.describe(record, scopeNodeId ?? null, scopeComponent ?? null);
401883
403072
  }
401884
403073
  /**
401885
403074
  * Apply a patch. Fields absent from it are untouched.
@@ -401889,28 +403078,47 @@ var require_logging_settings = __commonJS({
401889
403078
  * must still give the operator a live measurement, and must say so rather
401890
403079
  * than reporting a durable arm that is not durable.
401891
403080
  */
401892
- async set(patch, scopeNodeId) {
403081
+ async set(patch, scopeNodeId, scopeComponent) {
401893
403082
  await this.applyDiagnostics(patch);
401894
403083
  const scope = scopeNodeId ?? null;
403084
+ const component = scopeComponent ?? null;
401895
403085
  const stored = await this.read();
401896
403086
  const levelChanged = patch.level !== void 0;
401897
- let persistedLevel = true;
403087
+ const channelsChanged = patch.channels !== void 0;
403088
+ let persisted = true;
403089
+ let next = stored ?? {};
401898
403090
  if (levelChanged) {
401899
- const next = mergeLevel(stored ?? {}, scope, patch.level ?? null, this.now());
401900
- persistedLevel = await this.write(next);
403091
+ next = mergeLevel(next, scope, component, patch.level ?? null, this.now());
403092
+ }
403093
+ if (channelsChanged) {
403094
+ const declared = await this.channels.declarations();
403095
+ const rejected = validateChannelPatch(patch.channels ?? [], declared);
403096
+ if (rejected.length > 0) {
403097
+ this.logger?.warn("log channel patch refused - nothing was armed or disarmed", {
403098
+ meta: { reasons: rejected }
403099
+ });
403100
+ throw new Error(`log channel patch refused: ${rejected.join("; ")}`);
403101
+ }
403102
+ next = mergeChannelWindows(next, patch.channels ?? [], this.now());
403103
+ }
403104
+ if (levelChanged || channelsChanged) {
403105
+ persisted = await this.write(next);
401901
403106
  this.refreshMirror(next);
403107
+ await this.pushChannels(next);
401902
403108
  this.logger?.info("logging settings written", {
401903
403109
  meta: {
401904
- scope: scope === null ? "cluster" : "node",
403110
+ scope: component !== null ? "component" : scope === null ? "cluster" : "node",
401905
403111
  nodeId: scope,
401906
- level: patch.level ?? null,
401907
- persisted: persistedLevel
403112
+ component,
403113
+ level: levelChanged ? patch.level ?? null : void 0,
403114
+ channels: (patch.channels ?? []).map((c) => `${c.channel}:${c.armMs}`),
403115
+ persisted
401908
403116
  }
401909
403117
  });
401910
- const state = await this.describe(next, scope);
401911
- return { ...state, persisted: state.persisted && persistedLevel };
403118
+ const state = await this.describe(next, scope, component);
403119
+ return { ...state, persisted: state.persisted && persisted };
401912
403120
  }
401913
- return this.describe(stored, scope);
403121
+ return this.describe(stored, scope, component);
401914
403122
  }
401915
403123
  /**
401916
403124
  * Re-establish the mirror and re-arm any diagnostic window that outlived the
@@ -401923,8 +403131,10 @@ var require_logging_settings = __commonJS({
401923
403131
  */
401924
403132
  async restore() {
401925
403133
  const record = await this.read();
401926
- if (record !== null)
403134
+ if (record !== null) {
401927
403135
  this.refreshMirror(record);
403136
+ await this.pushChannels(record);
403137
+ }
401928
403138
  await this.requestCensus.restore();
401929
403139
  }
401930
403140
  // ── internals ─────────────────────────────────────────────────────
@@ -401942,16 +403152,55 @@ var require_logging_settings = __commonJS({
401942
403152
  });
401943
403153
  }
401944
403154
  }
401945
- async describe(record, scopeNodeId) {
401946
- const resolved = resolveLevel(record ?? {}, scopeNodeId);
403155
+ /**
403156
+ * Push the live windows to every declared gate.
403157
+ *
403158
+ * A failure here is logged and swallowed: the document is written and the
403159
+ * operator is told what it says, and a runner that could not be reached
403160
+ * simply keeps the mirror it has. Throwing would turn one unreachable addon
403161
+ * into a failed write for the whole cluster.
403162
+ */
403163
+ async pushChannels(record) {
403164
+ try {
403165
+ await this.channels.apply(liveChannelWindows(record, this.now()));
403166
+ } catch (err) {
403167
+ this.logger?.warn("log channel windows written but NOT pushed to every gate", {
403168
+ meta: { error: (0, types_1.errMsg)(err) }
403169
+ });
403170
+ }
403171
+ }
403172
+ async describe(record, scopeNodeId, scopeComponent) {
403173
+ const resolved = resolveLevel(record ?? {}, scopeNodeId, scopeComponent);
401947
403174
  return {
401948
403175
  scopeNodeId,
403176
+ scopeComponent,
401949
403177
  effective: { level: resolved.level, levelSource: resolved.source },
401950
- explicit: { layers: describeLayers(record ?? {}, scopeNodeId) },
403178
+ explicit: { layers: describeLayers(record ?? {}, scopeNodeId, scopeComponent) },
401951
403179
  activeWindows: await this.describeWindows(),
403180
+ channels: await this.declaredChannels(),
403181
+ activeChannels: describeChannelWindows(record ?? {}, this.now()),
401952
403182
  persisted: record !== null
401953
403183
  };
401954
403184
  }
403185
+ /**
403186
+ * The declarations, assembled per read.
403187
+ *
403188
+ * Not cached and not stored: a channel added by a redeployed addon has to
403189
+ * appear without anybody editing a list, and a channel whose addon is gone
403190
+ * has to stop being offered. A read that throws yields the EMPTY list and
403191
+ * says so — "unknown" is the truth about a set of addons nobody reached, and
403192
+ * it changes no armed window (D49/D224).
403193
+ */
403194
+ async declaredChannels() {
403195
+ try {
403196
+ return await this.channels.declarations();
403197
+ } catch (err) {
403198
+ this.logger?.warn("log channel declarations unreadable - reporting none", {
403199
+ meta: { error: (0, types_1.errMsg)(err) }
403200
+ });
403201
+ return [];
403202
+ }
403203
+ }
401955
403204
  /**
401956
403205
  * The armed windows, with their deadline.
401957
403206
  *
@@ -402015,14 +403264,21 @@ var require_logging_settings = __commonJS({
402015
403264
  return {};
402016
403265
  const clusterLevel = asLogLevel(Reflect.get(value, "clusterLevel"));
402017
403266
  const nodeLevels = asNodeLevels(Reflect.get(value, "nodeLevels"));
403267
+ const componentLevels = asNodeLevels(Reflect.get(value, "componentLevels"));
403268
+ const channelWindows = asChannelWindows(Reflect.get(value, "channelWindows"));
402018
403269
  const updatedAt = Reflect.get(value, "updatedAt");
402019
403270
  return {
402020
403271
  ...clusterLevel !== null ? { clusterLevel } : {},
402021
403272
  ...nodeLevels !== null ? { nodeLevels } : {},
403273
+ ...componentLevels !== null ? { componentLevels } : {},
403274
+ ...channelWindows !== null ? { channelWindows } : {},
402022
403275
  ...typeof updatedAt === "number" && Number.isFinite(updatedAt) ? { updatedAt } : {}
402023
403276
  };
402024
403277
  }
402025
- function resolveLevel(record, nodeId) {
403278
+ function resolveLevel(record, nodeId, component = null) {
403279
+ const componentLevel = component === null ? void 0 : record.componentLevels?.[component];
403280
+ if (componentLevel !== void 0)
403281
+ return { level: componentLevel, source: "component" };
402026
403282
  const nodeLevel = nodeId === null ? void 0 : record.nodeLevels?.[nodeId];
402027
403283
  if (nodeLevel !== void 0)
402028
403284
  return { level: nodeLevel, source: "node" };
@@ -402030,17 +403286,37 @@ var require_logging_settings = __commonJS({
402030
403286
  return { level: record.clusterLevel, source: "cluster" };
402031
403287
  return { level: system_1.DEFAULT_LOG_LEVEL, source: "default" };
402032
403288
  }
402033
- function describeLayers(record, nodeId) {
402034
- const cluster = {
402035
- scope: "cluster",
402036
- nodeId: null,
402037
- level: record.clusterLevel ?? null
402038
- };
402039
- if (nodeId === null)
402040
- return [cluster];
402041
- return [cluster, { scope: "node", nodeId, level: record.nodeLevels?.[nodeId] ?? null }];
403289
+ function describeLayers(record, nodeId, component = null) {
403290
+ const layers = [
403291
+ { scope: "cluster", nodeId: null, component: null, level: record.clusterLevel ?? null }
403292
+ ];
403293
+ if (nodeId !== null) {
403294
+ layers.push({
403295
+ scope: "node",
403296
+ nodeId,
403297
+ component: null,
403298
+ level: record.nodeLevels?.[nodeId] ?? null
403299
+ });
403300
+ }
403301
+ if (component !== null) {
403302
+ layers.push({
403303
+ scope: "component",
403304
+ nodeId: null,
403305
+ component,
403306
+ level: record.componentLevels?.[component] ?? null
403307
+ });
403308
+ }
403309
+ return layers;
402042
403310
  }
402043
- function mergeLevel(record, nodeId, next, updatedAt) {
403311
+ function mergeLevel(record, nodeId, component, next, updatedAt) {
403312
+ if (component !== null) {
403313
+ const { [component]: _clearedComponent, ...otherComponents } = record.componentLevels ?? {};
403314
+ const componentLevels = {
403315
+ ...otherComponents,
403316
+ ...next !== null ? { [component]: next } : {}
403317
+ };
403318
+ return { ...record, componentLevels, updatedAt };
403319
+ }
402044
403320
  if (nodeId === null) {
402045
403321
  const { clusterLevel: _dropped, ...rest } = record;
402046
403322
  return { ...rest, ...next !== null ? { clusterLevel: next } : {}, updatedAt };
@@ -402056,6 +403332,81 @@ var require_logging_settings = __commonJS({
402056
403332
  updatedAt
402057
403333
  };
402058
403334
  }
403335
+ function validateChannelPatch(patches, declared) {
403336
+ const byName = new Map(declared.map((d) => [d.name, d]));
403337
+ const reasons = [];
403338
+ for (const patch of patches) {
403339
+ const descriptor = byName.get(patch.channel);
403340
+ if (descriptor === void 0) {
403341
+ const known = [...byName.keys()].sort().slice(0, 8).join(", ");
403342
+ reasons.push(`"${patch.channel}" is not declared by any addon on this cluster` + (known.length > 0 ? ` (declared: ${known})` : ""));
403343
+ continue;
403344
+ }
403345
+ const ids = patch.deviceIds;
403346
+ if (ids !== void 0 && ids !== null && ids.length > 0 && !descriptor.perDevice) {
403347
+ reasons.push(`"${patch.channel}" is declared perDevice: false \u2014 its lines carry no tags.deviceId, so narrowing it to a camera would match nothing`);
403348
+ }
403349
+ }
403350
+ return reasons;
403351
+ }
403352
+ function mergeChannelWindows(record, patches, nowMs) {
403353
+ const windows = { ...record.channelWindows };
403354
+ for (const patch of patches) {
403355
+ if (patch.armMs <= 0) {
403356
+ delete windows[patch.channel];
403357
+ continue;
403358
+ }
403359
+ const ids = patch.deviceIds;
403360
+ windows[patch.channel] = {
403361
+ armedUntilMs: nowMs + Math.min(patch.armMs, exports.MAX_CHANNEL_WINDOW_MS),
403362
+ deviceIds: ids === void 0 || ids === null || ids.length === 0 ? null : [...ids]
403363
+ };
403364
+ }
403365
+ return { ...record, channelWindows: windows, updatedAt: nowMs };
403366
+ }
403367
+ function liveChannelWindows(record, nowMs) {
403368
+ const out = [];
403369
+ for (const [channel, window2] of Object.entries(record.channelWindows ?? {})) {
403370
+ if (window2.armedUntilMs <= nowMs)
403371
+ continue;
403372
+ out.push({ channel, armedUntilMs: window2.armedUntilMs, deviceIds: window2.deviceIds });
403373
+ }
403374
+ return out.sort((a, b) => a.channel.localeCompare(b.channel));
403375
+ }
403376
+ function describeChannelWindows(record, nowMs) {
403377
+ const out = [];
403378
+ for (const [channel, window2] of Object.entries(record.channelWindows ?? {})) {
403379
+ const armed = window2.armedUntilMs > nowMs;
403380
+ out.push({
403381
+ channel,
403382
+ armed,
403383
+ armedUntilMs: armed ? window2.armedUntilMs : 0,
403384
+ remainingMs: armed ? window2.armedUntilMs - nowMs : 0,
403385
+ deviceIds: armed ? window2.deviceIds : null
403386
+ });
403387
+ }
403388
+ return out.sort((a, b) => a.channel.localeCompare(b.channel));
403389
+ }
403390
+ function asChannelWindows(value) {
403391
+ if (typeof value !== "object" || value === null)
403392
+ return null;
403393
+ const out = {};
403394
+ for (const [key, raw] of Object.entries(value)) {
403395
+ if (typeof raw !== "object" || raw === null)
403396
+ continue;
403397
+ const armedUntilMs = Reflect.get(raw, "armedUntilMs");
403398
+ if (typeof armedUntilMs !== "number" || !Number.isFinite(armedUntilMs))
403399
+ continue;
403400
+ out[key] = { armedUntilMs, deviceIds: asDeviceIds(Reflect.get(raw, "deviceIds")) };
403401
+ }
403402
+ return out;
403403
+ }
403404
+ function asDeviceIds(value) {
403405
+ if (!Array.isArray(value))
403406
+ return null;
403407
+ const ids = value.filter((id) => typeof id === "number" && Number.isInteger(id));
403408
+ return ids.length > 0 ? ids : null;
403409
+ }
402059
403410
  function asLogLevel(value) {
402060
403411
  return typeof value === "string" && isLogLevel(value) ? value : null;
402061
403412
  }
@@ -402692,10 +404043,14 @@ var require_cap_providers = __commonJS({
402692
404043
  store,
402693
404044
  ...logger !== void 0 ? { logger } : {}
402694
404045
  });
404046
+ const channels = (0, logging_settings_js_1.buildLogChannelPlane)({ entries: () => registry?.getCollectionEntries("log-channels") ?? [] }, (addonId, phase, error) => {
404047
+ logger?.warn("log-channels provider unreachable", { meta: { addonId, phase, error } });
404048
+ });
402695
404049
  const loggingSettings = new logging_settings_js_1.LoggingSettingsService({
402696
404050
  store,
402697
404051
  gate: (0, system_1.getLoggingGate)(),
402698
404052
  requestCensus,
404053
+ channels,
402699
404054
  ...logger !== void 0 ? { logger } : {}
402700
404055
  });
402701
404056
  return {
@@ -402729,8 +404084,8 @@ var require_cap_providers = __commonJS({
402729
404084
  setSiteLocation: async (input) => siteLocation.set(input),
402730
404085
  detectSiteLocation: async () => siteLocation.detect(),
402731
404086
  getRequestCensus: async () => requestCensus.status(),
402732
- getLoggingSettings: async (input) => loggingSettings.get(input.scopeNodeId),
402733
- setLoggingSettings: async (input) => loggingSettings.set(input.patch, input.scopeNodeId)
404087
+ getLoggingSettings: async (input) => loggingSettings.get(input.scopeNodeId, input.scopeComponent),
404088
+ setLoggingSettings: async (input) => loggingSettings.set(input.patch, input.scopeNodeId, input.scopeComponent)
402734
404089
  };
402735
404090
  }
402736
404091
  function buildSiteLocationService(registry, logger) {
@@ -405277,6 +406632,7 @@ var require_device_config_secret_redaction = __commonJS({
405277
406632
  exports.NON_ADMIN_CONFIG_REDACTED_METHODS = exports.REDACTED_SECRET = void 0;
405278
406633
  exports.isSecretConfigKey = isSecretConfigKey;
405279
406634
  exports.redactDeviceInfoSecrets = redactDeviceInfoSecrets;
406635
+ exports.redactDeviceInfoRecordSecrets = redactDeviceInfoRecordSecrets;
405280
406636
  exports.redactSettingsSections = redactSettingsSections;
405281
406637
  exports.redactSettingsAggregate = redactSettingsAggregate;
405282
406638
  exports.redactConfigEntries = redactConfigEntries;
@@ -405314,6 +406670,14 @@ var require_device_config_secret_redaction = __commonJS({
405314
406670
  return data;
405315
406671
  return { ...data, config: redactRecord(config) };
405316
406672
  }
406673
+ function redactDeviceInfoRecordSecrets(data) {
406674
+ if (!isRecord(data))
406675
+ return data;
406676
+ const out = {};
406677
+ for (const [key, value] of Object.entries(data))
406678
+ out[key] = redactDeviceInfoSecrets(value);
406679
+ return out;
406680
+ }
405317
406681
  function redactField(field) {
405318
406682
  if (!isRecord(field))
405319
406683
  return field;
@@ -405372,6 +406736,7 @@ var require_device_config_secret_redaction = __commonJS({
405372
406736
  ["deviceManager.getDevice", redactDeviceInfoSecrets],
405373
406737
  ["deviceManager.listAll", redactDeviceInfoSecrets],
405374
406738
  ["deviceManager.getChildren", redactDeviceInfoSecrets],
406739
+ ["deviceManager.getChildrenBatch", redactDeviceInfoRecordSecrets],
405375
406740
  ["deviceManager.getDeviceSettingsAggregate", redactSettingsSections],
405376
406741
  ["deviceManager.getSettingsSchema", redactSettingsSections],
405377
406742
  ["deviceManager.getDeviceAggregate", redactSettingsAggregate],
@@ -408962,6 +410327,234 @@ var require_addon_settings_provider = __commonJS({
408962
410327
  }
408963
410328
  });
408964
410329
 
410330
+ // ../../server/backend/dist/core/addon/single-flight-refresh.js
410331
+ var require_single_flight_refresh = __commonJS({
410332
+ "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
410333
+ "use strict";
410334
+ Object.defineProperty(exports, "__esModule", { value: true });
410335
+ exports.createSingleFlightRefresh = createSingleFlightRefresh;
410336
+ function widenScope(a, b) {
410337
+ if (a === null || b === null)
410338
+ return null;
410339
+ const out = new Set(a);
410340
+ for (const id of b)
410341
+ out.add(id);
410342
+ return out;
410343
+ }
410344
+ function createSingleFlightRefresh(run) {
410345
+ let inFlight = null;
410346
+ let pendingScope;
410347
+ let pendingPromise = null;
410348
+ let releasePending = () => {
410349
+ };
410350
+ const start = (scope) => {
410351
+ const active = (async () => {
410352
+ try {
410353
+ await run(scope);
410354
+ } finally {
410355
+ inFlight = null;
410356
+ if (pendingScope !== void 0) {
410357
+ const next = pendingScope;
410358
+ pendingScope = void 0;
410359
+ const release = releasePending;
410360
+ releasePending = () => {
410361
+ };
410362
+ pendingPromise = null;
410363
+ void start(next).finally(release);
410364
+ }
410365
+ }
410366
+ })();
410367
+ inFlight = active;
410368
+ return active;
410369
+ };
410370
+ return {
410371
+ request: async (scope) => {
410372
+ if (scope !== null && scope.size === 0)
410373
+ return;
410374
+ if (inFlight === null) {
410375
+ await start(scope);
410376
+ return;
410377
+ }
410378
+ pendingScope = pendingScope === void 0 ? scope : widenScope(pendingScope, scope);
410379
+ if (pendingPromise === null) {
410380
+ pendingPromise = new Promise((resolve) => {
410381
+ releasePending = resolve;
410382
+ });
410383
+ }
410384
+ const wait = pendingPromise;
410385
+ if (wait !== null)
410386
+ await wait;
410387
+ }
410388
+ };
410389
+ }
410390
+ }
410391
+ });
410392
+
410393
+ // ../../server/backend/dist/core/addon/device-meta-mirror.js
410394
+ var require_device_meta_mirror = __commonJS({
410395
+ "../../server/backend/dist/core/addon/device-meta-mirror.js"(exports) {
410396
+ "use strict";
410397
+ Object.defineProperty(exports, "__esModule", { value: true });
410398
+ exports.DeviceMetaMirror = exports.MIRROR_IRRELEVANT_META_FIELDS = void 0;
410399
+ exports.classifyDeviceEvent = classifyDeviceEvent;
410400
+ var types_1 = require_dist4();
410401
+ var single_flight_refresh_js_1 = require_single_flight_refresh();
410402
+ exports.MIRROR_IRRELEVANT_META_FIELDS = /* @__PURE__ */ new Set([
410403
+ "name",
410404
+ "disabled",
410405
+ "metadata",
410406
+ "display",
410407
+ "role",
410408
+ "integrationId",
410409
+ "linkDeviceId",
410410
+ "primaryChildEntityId",
410411
+ "childLayout"
410412
+ ]);
410413
+ function classifyDeviceEvent(data) {
410414
+ if (data === null || typeof data !== "object")
410415
+ return { kind: "fleet" };
410416
+ const field = Reflect.get(data, "field");
410417
+ if (typeof field === "string" && exports.MIRROR_IRRELEVANT_META_FIELDS.has(field)) {
410418
+ return { kind: "none" };
410419
+ }
410420
+ const deviceId = Reflect.get(data, "deviceId");
410421
+ if (typeof deviceId !== "number" || !Number.isFinite(deviceId))
410422
+ return { kind: "fleet" };
410423
+ return { kind: "device", deviceId };
410424
+ }
410425
+ var MAX_ANCESTOR_HOPS = 8;
410426
+ var DeviceMetaMirror = class {
410427
+ read;
410428
+ live;
410429
+ logger;
410430
+ parents = /* @__PURE__ */ new Map();
410431
+ meta = /* @__PURE__ */ new Map();
410432
+ gate;
410433
+ constructor(read, live, logger) {
410434
+ this.read = read;
410435
+ this.live = live;
410436
+ this.logger = logger;
410437
+ this.gate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)((scope) => this.refresh(scope));
410438
+ }
410439
+ /** Ask for a refresh covering exactly what the event changed. */
410440
+ onDeviceEvent(data) {
410441
+ const scope = classifyDeviceEvent(data);
410442
+ if (scope.kind === "none")
410443
+ return;
410444
+ void this.gate.request(scope.kind === "fleet" ? null : /* @__PURE__ */ new Set([scope.deviceId]));
410445
+ }
410446
+ /** Warm (or re-warm) the whole fleet — the boot sweep, and the D8 reconcile
410447
+ * that repairs anything a dropped event would have left behind. */
410448
+ async refreshAll() {
410449
+ await this.gate.request(null);
410450
+ }
410451
+ /** Parent of a device: mirror first (covers forked devices), then the live
410452
+ * hub registry (covers a hub-local device before the first warm). */
410453
+ parentOf(deviceId) {
410454
+ const mirrored = this.parents.get(deviceId);
410455
+ if (mirrored !== void 0)
410456
+ return mirrored;
410457
+ return this.live.parentDeviceId(deviceId);
410458
+ }
410459
+ /** Persisted `DeviceType` string, or null when unknown. */
410460
+ typeOf(deviceId) {
410461
+ const mirrored = this.meta.get(deviceId);
410462
+ if (mirrored !== void 0)
410463
+ return mirrored.type;
410464
+ return this.live.type(deviceId);
410465
+ }
410466
+ /** Persisted operator `location` label, or null when unset/unknown. */
410467
+ locationOf(deviceId) {
410468
+ const mirrored = this.meta.get(deviceId);
410469
+ if (mirrored !== void 0)
410470
+ return mirrored.location;
410471
+ return this.live.location(deviceId);
410472
+ }
410473
+ /** The whole mirrored fleet — fuels fleet-wide selector expansion. */
410474
+ list() {
410475
+ const out = [];
410476
+ for (const [id, m] of this.meta) {
410477
+ out.push({
410478
+ id,
410479
+ type: m.type,
410480
+ location: m.location,
410481
+ parentDeviceId: this.parents.get(id) ?? null
410482
+ });
410483
+ }
410484
+ return out;
410485
+ }
410486
+ /** Ancestor chain (parent, grandparent, …), bounded. Empty for a top-level
410487
+ * device or one the hub has never heard of. */
410488
+ ancestorsOf(deviceId) {
410489
+ const out = [];
410490
+ let current = deviceId;
410491
+ for (let hop = 0; hop < MAX_ANCESTOR_HOPS; hop++) {
410492
+ const parent = this.parentOf(current);
410493
+ if (parent === null || parent === current)
410494
+ break;
410495
+ out.push(parent);
410496
+ current = parent;
410497
+ }
410498
+ return out;
410499
+ }
410500
+ async refresh(scope) {
410501
+ try {
410502
+ const ids = scope === null ? null : [...scope];
410503
+ const rows = await this.read(ids);
410504
+ if (!Array.isArray(rows))
410505
+ return;
410506
+ const nextParents = /* @__PURE__ */ new Map();
410507
+ const nextMeta = /* @__PURE__ */ new Map();
410508
+ for (const row of rows) {
410509
+ if (row === null || typeof row !== "object")
410510
+ continue;
410511
+ const id = Reflect.get(row, "id");
410512
+ if (typeof id !== "number")
410513
+ continue;
410514
+ const parent = Reflect.get(row, "parentDeviceId");
410515
+ if (typeof parent === "number")
410516
+ nextParents.set(id, parent);
410517
+ const type = Reflect.get(row, "type");
410518
+ const location = Reflect.get(row, "location");
410519
+ nextMeta.set(id, {
410520
+ type: typeof type === "string" ? type : "",
410521
+ location: typeof location === "string" ? location : null
410522
+ });
410523
+ }
410524
+ if (ids === null) {
410525
+ this.parents.clear();
410526
+ for (const [k, v] of nextParents)
410527
+ this.parents.set(k, v);
410528
+ this.meta.clear();
410529
+ for (const [k, v] of nextMeta)
410530
+ this.meta.set(k, v);
410531
+ return;
410532
+ }
410533
+ for (const id of ids) {
410534
+ const m = nextMeta.get(id);
410535
+ if (m === void 0) {
410536
+ this.parents.delete(id);
410537
+ this.meta.delete(id);
410538
+ continue;
410539
+ }
410540
+ this.meta.set(id, m);
410541
+ const parent = nextParents.get(id);
410542
+ if (parent === void 0)
410543
+ this.parents.delete(id);
410544
+ else
410545
+ this.parents.set(id, parent);
410546
+ }
410547
+ } catch (err) {
410548
+ this.logger.debug("device-meta mirror refresh failed \u2014 keeping previous", {
410549
+ meta: { error: (0, types_1.errMsg)(err), scope: scope === null ? "fleet" : scope.size }
410550
+ });
410551
+ }
410552
+ }
410553
+ };
410554
+ exports.DeviceMetaMirror = DeviceMetaMirror;
410555
+ }
410556
+ });
410557
+
408965
410558
  // ../../server/backend/dist/core/addon/integration-visibility.js
408966
410559
  var require_integration_visibility = __commonJS({
408967
410560
  "../../server/backend/dist/core/addon/integration-visibility.js"(exports) {
@@ -409221,58 +410814,6 @@ var require_runner_spawn_fanout = __commonJS({
409221
410814
  }
409222
410815
  });
409223
410816
 
409224
- // ../../server/backend/dist/core/addon/single-flight-refresh.js
409225
- var require_single_flight_refresh = __commonJS({
409226
- "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
409227
- "use strict";
409228
- Object.defineProperty(exports, "__esModule", { value: true });
409229
- exports.createSingleFlightRefresh = createSingleFlightRefresh;
409230
- function createSingleFlightRefresh(run) {
409231
- let inFlight = null;
409232
- let pending = false;
409233
- let pendingPromise = null;
409234
- let releasePending = () => {
409235
- };
409236
- const start = () => {
409237
- const active = (async () => {
409238
- try {
409239
- await run();
409240
- } finally {
409241
- inFlight = null;
409242
- if (pending) {
409243
- pending = false;
409244
- const release = releasePending;
409245
- releasePending = () => {
409246
- };
409247
- pendingPromise = null;
409248
- void start().finally(release);
409249
- }
409250
- }
409251
- })();
409252
- inFlight = active;
409253
- return active;
409254
- };
409255
- return {
409256
- request: async () => {
409257
- if (inFlight === null) {
409258
- await start();
409259
- return;
409260
- }
409261
- if (!pending) {
409262
- pending = true;
409263
- pendingPromise = new Promise((resolve) => {
409264
- releasePending = resolve;
409265
- });
409266
- }
409267
- const wait = pendingPromise;
409268
- if (wait !== null)
409269
- await wait;
409270
- }
409271
- };
409272
- }
409273
- }
409274
- });
409275
-
409276
410817
  // ../../server/backend/dist/core/addon/addon-registry.service.js
409277
410818
  var require_addon_registry_service = __commonJS({
409278
410819
  "../../server/backend/dist/core/addon/addon-registry.service.js"(exports) {
@@ -409329,13 +410870,13 @@ var require_addon_registry_service = __commonJS({
409329
410870
  var addon_call_gateway_js_1 = require_addon_call_gateway();
409330
410871
  var addon_row_manifest_1 = require_addon_row_manifest();
409331
410872
  var addon_settings_provider_js_1 = require_addon_settings_provider();
410873
+ var device_meta_mirror_js_1 = require_device_meta_mirror();
409332
410874
  var integration_visibility_js_1 = require_integration_visibility();
409333
410875
  var package_dir_utils_1 = require_package_dir_utils();
409334
410876
  var prune_misplaced_addons_js_1 = require_prune_misplaced_addons();
409335
410877
  var require_cache_js_1 = require_require_cache();
409336
410878
  var runner_convergence_1 = require_runner_convergence();
409337
410879
  var runner_spawn_fanout_js_1 = require_runner_spawn_fanout();
409338
- var single_flight_refresh_js_1 = require_single_flight_refresh();
409339
410880
  function shouldEvictMissingOnDisk(entry, id, onDiskIds) {
409340
410881
  return entry.source === "installed" && entry.packageName !== "@camstack/system" && !onDiskIds.has(id);
409341
410882
  }
@@ -409542,6 +411083,14 @@ var require_addon_registry_service = __commonJS({
409542
411083
  this.streamProbe = streamProbe;
409543
411084
  this.logger = this.loggingService.createLogger("AddonRegistry");
409544
411085
  this.addonLoader = new system_1.AddonLoader(this.loggingService.createLogger("AddonLoader"));
411086
+ this.deviceMirror = new device_meta_mirror_js_1.DeviceMetaMirror(async (deviceIds) => this.getBrokerApi().deviceManager.listAll.query({
411087
+ projection: "slim",
411088
+ ...deviceIds === null ? {} : { deviceIds: [...deviceIds] }
411089
+ }), {
411090
+ parentDeviceId: (deviceId) => this.deviceRegistry.getById(deviceId)?.parentDeviceId ?? null,
411091
+ type: (deviceId) => this.deviceRegistry.getById(deviceId)?.type ?? null,
411092
+ location: (deviceId) => this.deviceRegistry.getById(deviceId)?.location ?? null
411093
+ }, this.logger);
409545
411094
  this.healthMonitor = new system_1.AddonHealthMonitor({
409546
411095
  eventBus: this.eventBusService,
409547
411096
  logger: this.loggingService.createLogger("AddonHealthMonitor"),
@@ -410103,56 +411652,22 @@ var require_addon_registry_service = __commonJS({
410103
411652
  // fleet — forked devices included. This is a hub-process, SYNCHRONOUSLY
410104
411653
  // readable mirror of that parentage, refreshed OFF the request path on every
410105
411654
  // device-meta lifecycle event — never a per-call DB query.
410106
- deviceParentMirror = /* @__PURE__ */ new Map();
410107
- /**
410108
- * Hub-process mirror of each device's persisted `type` + `location`, warmed
410109
- * from the same `listAll({projection:'slim'})` sweep as the parent mirror.
410110
- * Backs the v3 scope-model `types` / `locations` selectors: the enforcement
410111
- * matcher resolves a selector against a device's type/location synchronously,
410112
- * off the request path (D49). Empty for a device the hub has never heard of.
410113
- */
410114
- deviceMetaMirror = /* @__PURE__ */ new Map();
410115
- /** Parent of a device: the persisted mirror first (covers forked devices),
410116
- * falling back to the live hub registry (covers a hub-local device before the
410117
- * first mirror warm). null when top-level or unknown. */
410118
- parentOfDevice(deviceId) {
410119
- const mirrored = this.deviceParentMirror.get(deviceId);
410120
- if (mirrored !== void 0)
410121
- return mirrored;
410122
- return this.deviceRegistry.getById(deviceId)?.parentDeviceId ?? null;
410123
- }
411655
+ //
411656
+ // The mirror itself, and the rule that decides how much of the fleet one
411657
+ // event may re-read, live in `device-meta-mirror.ts` with the 4 904-call /
411658
+ // 4 987 368-row boot census that made the rule necessary.
411659
+ deviceMirror;
410124
411660
  /** Persisted `DeviceType` string of a device, or null when unknown. Mirror
410125
411661
  * first (covers forked devices), then the live hub registry. */
410126
- getPersistedType = (deviceId) => {
410127
- const mirrored = this.deviceMetaMirror.get(deviceId);
410128
- if (mirrored !== void 0)
410129
- return mirrored.type;
410130
- return this.deviceRegistry.getById(deviceId)?.type ?? null;
410131
- };
411662
+ getPersistedType = (deviceId) => this.deviceMirror.typeOf(deviceId);
410132
411663
  /** Persisted operator `location` label of a device, or null when unset/unknown. */
410133
- getPersistedLocation = (deviceId) => {
410134
- const mirrored = this.deviceMetaMirror.get(deviceId);
410135
- if (mirrored !== void 0)
410136
- return mirrored.location;
410137
- return this.deviceRegistry.getById(deviceId)?.location ?? null;
410138
- };
411664
+ getPersistedLocation = (deviceId) => this.deviceMirror.locationOf(deviceId);
410139
411665
  /**
410140
411666
  * The whole persisted fleet, slim — id + type + location + parentDeviceId.
410141
411667
  * Fuels the FLEET-wide selector expansion (response projection + `auth.me`
410142
411668
  * counts). Read synchronously off the mirror, never a per-call DB query.
410143
411669
  */
410144
- getPersistedDeviceList = () => {
410145
- const out = [];
410146
- for (const [id, m] of this.deviceMetaMirror) {
410147
- out.push({
410148
- id,
410149
- type: m.type,
410150
- location: m.location,
410151
- parentDeviceId: this.deviceParentMirror.get(id) ?? null
410152
- });
410153
- }
410154
- return out;
410155
- };
411670
+ getPersistedDeviceList = () => this.deviceMirror.list();
410156
411671
  /**
410157
411672
  * Ancestor chain (parent, grandparent, …) of a device, bounded to 8 hops
410158
411673
  * (defence-in-depth against a corrupt registry cycle). Synchronous — it is
@@ -410160,80 +411675,31 @@ var require_addon_registry_service = __commonJS({
410160
411675
  * a FORKED camera covers its accessory children. Empty for a top-level device
410161
411676
  * or one the hub has never heard of.
410162
411677
  */
410163
- getPersistedAncestors = (deviceId) => {
410164
- const out = [];
410165
- let current = deviceId;
410166
- for (let hop = 0; hop < 8; hop++) {
410167
- const parent = this.parentOfDevice(current);
410168
- if (parent == null || parent === current)
410169
- break;
410170
- out.push(parent);
410171
- current = parent;
410172
- }
410173
- return out;
410174
- };
410175
- /** One full-fleet rebuild in flight, at most one queued behind it. Every
410176
- * event below asks for the SAME total rebuild, so running it once per event
410177
- * was ~1 000 rebuilds of a 974-row mirror on a boot — each one a 625 KB read
410178
- * and parse on hub-main's event loop. No timer: the first event still
410179
- * refreshes immediately, because this mirror backs scope enforcement.
410180
- * See `single-flight-refresh.ts`. */
410181
- deviceMirrorGate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)(() => this.refreshDeviceParentMirror());
410182
- /** Subscribe the mirror to every device-meta lifecycle event, and warm it once
410183
- * the addon set (device-manager included) is up. */
411678
+ getPersistedAncestors = (deviceId) => this.deviceMirror.ancestorsOf(deviceId);
411679
+ /**
411680
+ * Subscribe the mirror to every device-meta lifecycle event, and warm it once
411681
+ * the addon set (device-manager included) is up.
411682
+ *
411683
+ * The device events ask for a SCOPED refresh — the one device they name, or
411684
+ * nothing at all when they name a field the mirror does not hold.
411685
+ * `SystemAddonsReady` is the only fleet-wide read, and it is also the D8
411686
+ * reconcile: whatever a dropped event would have left stale, the warm
411687
+ * repairs.
411688
+ */
410184
411689
  wireDeviceParentMirror() {
410185
- const refresh = () => {
410186
- void this.deviceMirrorGate.request();
410187
- };
410188
411690
  for (const category of [
410189
411691
  types_1.EventCategory.DeviceMetaChanged,
410190
411692
  types_1.EventCategory.DeviceRegistered,
410191
411693
  types_1.EventCategory.DeviceUnregistered,
410192
- types_1.EventCategory.DeviceProvisioned,
410193
- types_1.EventCategory.SystemAddonsReady
411694
+ types_1.EventCategory.DeviceProvisioned
410194
411695
  ]) {
410195
- this.eventBusService.subscribe({ category }, refresh);
410196
- }
410197
- }
410198
- /** Rebuild the parent mirror from the device-manager's persisted fleet
410199
- * (`listAll`, slim projection — no config blob). Off the request path; a
410200
- * failed read KEEPS the previous mirror (D49 — a read that fails changes
410201
- * nothing, and must never look like an unbind that destroys inheritance). */
410202
- async refreshDeviceParentMirror() {
410203
- try {
410204
- const api = this.getBrokerApi();
410205
- const rows = await api.deviceManager.listAll.query({ projection: "slim" });
410206
- if (!Array.isArray(rows))
410207
- return;
410208
- const nextParents = /* @__PURE__ */ new Map();
410209
- const nextMeta = /* @__PURE__ */ new Map();
410210
- for (const row of rows) {
410211
- if (row === null || typeof row !== "object")
410212
- continue;
410213
- const id = Reflect.get(row, "id");
410214
- if (typeof id !== "number")
410215
- continue;
410216
- const parent = Reflect.get(row, "parentDeviceId");
410217
- if (typeof parent === "number")
410218
- nextParents.set(id, parent);
410219
- const type = Reflect.get(row, "type");
410220
- const location = Reflect.get(row, "location");
410221
- nextMeta.set(id, {
410222
- type: typeof type === "string" ? type : "",
410223
- location: typeof location === "string" ? location : null
410224
- });
410225
- }
410226
- this.deviceParentMirror.clear();
410227
- for (const [k, v] of nextParents)
410228
- this.deviceParentMirror.set(k, v);
410229
- this.deviceMetaMirror.clear();
410230
- for (const [k, v] of nextMeta)
410231
- this.deviceMetaMirror.set(k, v);
410232
- } catch (err) {
410233
- this.logger.debug("device-parent mirror refresh failed \u2014 keeping previous", {
410234
- meta: { error: (0, types_1.errMsg)(err) }
411696
+ this.eventBusService.subscribe({ category }, (event) => {
411697
+ this.deviceMirror.onDeviceEvent(event.data);
410235
411698
  });
410236
411699
  }
411700
+ this.eventBusService.subscribe({ category: types_1.EventCategory.SystemAddonsReady }, () => {
411701
+ void this.deviceMirror.refreshAll();
411702
+ });
410237
411703
  }
410238
411704
  /** Load persisted collection disabled-lists from settings-store into the registry */
410239
411705
  loadCollectionPreferences() {
@@ -416301,7 +417767,7 @@ var require_main4 = __commonJS({
416301
417767
  async function bootstrap() {
416302
417768
  const heapReclaimer = (0, system_1.createV8Reclaimer)();
416303
417769
  const heapReclaim = heapReclaimer === void 0 ? void 0 : { reclaim: heapReclaimer };
416304
- (0, system_1.startHeapWatch)("hub-main", void 0, void 0, heapReclaim);
417770
+ (0, system_1.startHeapWatch)("hub-main", void 0, void 0, heapReclaim, void 0, void 0, true, (0, system_1.hubMainRssBudget)());
416305
417771
  cleanupOrphanProcesses();
416306
417772
  let spaIndexHtml = null;
416307
417773
  const configPath = process.env.CONFIG_PATH ?? path.join(process.env.CAMSTACK_DATA ?? path.join(process.cwd(), "camstack-data"), "config.yaml");