@camstack/addon-pipeline 1.2.199 → 1.2.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-CMe44Kid.js → default-detection-model-BsmxtyZx.js} +1 -1
  4. package/dist/{default-detection-model-O1uBusV2.mjs → default-detection-model-c1LY6thP.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +5 -5
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-DmCrC2yx.js → dist-D2YTTkRf.js} +424 -426
  8. package/dist/{dist-B23u1tmN.mjs → dist-mEsrAAc3.mjs} +424 -426
  9. package/dist/{lazy-sharp-DBHsD2lh.js → lazy-sharp-B5bW34uC.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-D1MLn6oV.mjs → node-CUxep5tc.mjs} +1 -1
  13. package/dist/{node-DCSTbt6C.js → node-x3YokJkD.js} +1 -1
  14. package/dist/pipeline-runner/index.js +8 -7
  15. package/dist/pipeline-runner/index.mjs +7 -6
  16. package/dist/{process-memory-P24NTDb1.mjs → process-memory-BCqpTruN.mjs} +1 -1
  17. package/dist/{process-memory-9lvvhHym.js → process-memory-CFc4D9A2.js} +1 -1
  18. package/dist/recorder/index.js +169 -334
  19. package/dist/recorder/index.mjs +164 -329
  20. package/dist/restream-intent-B1Difcnq.js +313 -0
  21. package/dist/restream-intent-DWe2kInY.mjs +248 -0
  22. package/dist/{prebuffer-DmBYRQWg.mjs → retire-root-keys-Csb8z2f3.js} +12 -64
  23. package/dist/{prebuffer-DqdeLSsk.js → retire-root-keys-DZvlso2l.mjs} +1 -99
  24. package/dist/{segment-demux-js-Ck_F5bsO.js → segment-demux-js-DUZVURQd.js} +1 -1
  25. package/dist/{segment-demux-js-Bm0woPkn.mjs → segment-demux-js-xHtB450F.mjs} +1 -1
  26. package/dist/session-decode/decode-worker-child.js +25 -5
  27. package/dist/session-decode/decode-worker-child.mjs +24 -4
  28. package/dist/stream-broker/_stub.js +2 -2
  29. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Sq93hI-g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLXImQ3R.mjs} +3 -3
  30. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-NqqfpkYF.mjs +26 -0
  31. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dj_qoc1e.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D62O5bRa.mjs} +1 -1
  32. package/dist/stream-broker/demux-worker-child.js +1 -1
  33. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  34. package/dist/stream-broker/{hostInit-COt259tU.mjs → hostInit-D_cZQhBA.mjs} +3 -3
  35. package/dist/stream-broker/index.js +181 -48
  36. package/dist/stream-broker/index.mjs +172 -39
  37. package/dist/stream-broker/remoteEntry.js +1 -1
  38. package/dist/{worker-protocol-c1r1Yddg.js → worker-protocol-CRDaBlJl.js} +1 -1
  39. package/dist/{worker-protocol-BxpGZ0Dt.mjs → worker-protocol-oUamwZSn.mjs} +1 -1
  40. package/package.json +1 -1
  41. package/dist/restream-intent-B4BXZra7.mjs +0 -72
  42. package/dist/restream-intent-Cv9x3jmu.js +0 -89
  43. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BcjLrxhF.mjs +0 -26
@@ -8617,362 +8617,6 @@ var CameraSwitchGroupSchema = object({
8617
8617
  fetchedAt: number()
8618
8618
  });
8619
8619
  /**
8620
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
8621
- * an addon declares its channels in.
8622
- *
8623
- * ## Two axes, deliberately separated
8624
- *
8625
- * - **DECLARATION** — which channels exist. Only the addon knows:
8626
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
8627
- * baichuan/handshake. A hand-wired central list rots at the first addition,
8628
- * and rots silently. So a channel is declared where it is consulted, and the
8629
- * `log-channels` capability enumerates the declarations.
8630
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
8631
- * thing: the logging settings document on the `system` cap. Two authorities
8632
- * over the values is the exact defect
8633
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
8634
- * remove; re-introducing it from the cure side would be grotesque.
8635
- *
8636
- * Nothing in this file reads a clock, an env var or a store. The registry is
8637
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
8638
- * the hot path with a value somebody actually read, and by
8639
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
8640
- * never reaches here, so it can neither disarm an armed channel nor arm a
8641
- * disarmed one (D49).
8642
- *
8643
- * ## The canonical call shape
8644
- *
8645
- * ```ts
8646
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
8647
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
8648
- * }
8649
- * ```
8650
- *
8651
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
8652
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
8653
- * object literal is never constructed because it lives inside the branch. It
8654
- * is the same shape already proven in production at `stream-broker.ts:1650`,
8655
- * and the same discipline `LoggingGate.allowsDestination` uses for the
8656
- * destination floor (measured at 1.93 ns/call when off).
8657
- *
8658
- * ## Why a channel emits at `info`
8659
- *
8660
- * `loki-logging.addon.ts` pins the destination default at `info` and
8661
- * `loki-destination.ts` drops everything below it, so a line emitted at
8662
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
8663
- * minutes. A diagnostic that cannot be read an hour later is worse than no
8664
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
8665
- * emits at the channel's declared level, whose schema floor is `info`.
8666
- */
8667
- /**
8668
- * The level a channel writes at once armed.
8669
- *
8670
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
8671
- * not leave the process for Loki, and the whole point of arming a channel is
8672
- * to read it later.
8673
- */
8674
- var LogChannelLevelSchema = _enum([
8675
- "info",
8676
- "warn",
8677
- "error"
8678
- ]);
8679
- /**
8680
- * What an addon declares about one channel. No value, no state — a
8681
- * declaration is inert.
8682
- */
8683
- var LogChannelDescriptorSchema = object({
8684
- /**
8685
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
8686
- * the addon's short name so an operator reading a channel list can tell who
8687
- * owns it without a second lookup.
8688
- */
8689
- name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
8690
- /** One sentence: what the operator will SEE after arming it. */
8691
- description: string().min(1),
8692
- /** The level its lines are emitted at. Never below `info`. */
8693
- defaultLevel: LogChannelLevelSchema,
8694
- /**
8695
- * Whether this channel can be narrowed to a camera.
8696
- *
8697
- * `true` is a PROMISE with two halves, and both must hold: the gate is
8698
- * consulted with the numeric device id, AND every line the channel admits
8699
- * carries `tags: { deviceId }` with that same numeric id. The second half is
8700
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
8701
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
8702
- * the body is the only way to filter.
8703
- *
8704
- * A channel whose lines carry the device only in `meta` (or not at all) is
8705
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
8706
- * the operator narrows to one camera, sees nothing, and concludes the code
8707
- * path was never taken.
8708
- */
8709
- perDevice: boolean()
8710
- });
8711
- /**
8712
- * An armed window over one channel, as the document hands it to a mirror.
8713
- *
8714
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
8715
- * expires by itself, which is the one failure a boolean cannot avoid.
8716
- */
8717
- var LogChannelWindowSchema = object({
8718
- channel: string().min(1),
8719
- /** Epoch ms the window closes at. */
8720
- armedUntilMs: number(),
8721
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
8722
- deviceIds: array(number().int()).readonly().nullable()
8723
- });
8724
- /**
8725
- * The gate a hot path holds.
8726
- *
8727
- * Obtain it ONCE — at module scope or in a constructor — and keep the
8728
- * reference. Looking a channel up by name per line would put a Map lookup on
8729
- * the path this class exists to keep free.
8730
- */
8731
- var LogChannelGate = class {
8732
- descriptor;
8733
- /**
8734
- * HOT PATH GUARD. A plain data FIELD, and it must stay one.
8735
- *
8736
- * `log-channel.spec.ts` asserts the property descriptor has no getter and
8737
- * booby-traps the device set, so turning this into an accessor — or reading
8738
- * anything before it — fails the spec instead of taxing every line the
8739
- * process emits.
8740
- */
8741
- on = false;
8742
- /** `null` while armed for every camera. Never read while `on` is false. */
8743
- devices = null;
8744
- level;
8745
- closesAtMs = 0;
8746
- constructor(descriptor) {
8747
- this.descriptor = descriptor;
8748
- this.level = descriptor.defaultLevel;
8749
- }
8750
- /** Epoch ms this channel disarms itself at. 0 when disarmed. */
8751
- get armedUntilMs() {
8752
- return this.on ? this.closesAtMs : 0;
8753
- }
8754
- /**
8755
- * Does this channel want a line about `deviceId`?
8756
- *
8757
- * Call it only behind `gate.on &&`. On its own it is still correct — the
8758
- * guard is repeated inside — but the point of the prefix is that a disarmed
8759
- * channel must not pay the call at all.
8760
- */
8761
- wants(deviceId) {
8762
- if (!this.on) return false;
8763
- return this.devices === null || this.devices.has(deviceId);
8764
- }
8765
- /**
8766
- * Emit one line on this channel, at the channel's declared level.
8767
- *
8768
- * The channel name is added as `tags.logChannel` so LogQL can select the
8769
- * channel without matching on the message text, and whatever `tags` the
8770
- * caller passed — `deviceId` above all — is preserved.
8771
- */
8772
- log(logger, message, extras) {
8773
- if (!this.on) return;
8774
- const tags = {
8775
- ...extras.tags,
8776
- logChannel: this.descriptor.name
8777
- };
8778
- const line = {
8779
- ...extras,
8780
- tags
8781
- };
8782
- if (this.level === "error") logger.error(message, line);
8783
- else if (this.level === "warn") logger.warn(message, line);
8784
- else logger.info(message, line);
8785
- }
8786
- /**
8787
- * Arm (or RE-arm, restarting) this channel. Off the hot path only.
8788
- *
8789
- * An empty `deviceIds` list is treated as "every camera" rather than "no
8790
- * camera": a window that matches nothing is indistinguishable from a
8791
- * disarmed one, and the operator who asked for it would wait for lines that
8792
- * can never come.
8793
- */
8794
- arm(window) {
8795
- const ids = window.deviceIds;
8796
- this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
8797
- this.closesAtMs = window.armedUntilMs;
8798
- this.on = true;
8799
- }
8800
- /** Disarm. Off the hot path only. */
8801
- disarm() {
8802
- this.on = false;
8803
- this.devices = null;
8804
- this.closesAtMs = 0;
8805
- }
8806
- };
8807
- /**
8808
- * Every channel this PROCESS declares, and the mirror of what is armed on it.
8809
- *
8810
- * One per process. A forked runner has its own, and it is refreshed through
8811
- * the `log-channels` capability by the hub that owns the document — the
8812
- * registry never reaches for a value itself.
8813
- */
8814
- var LogChannelRegistry = class {
8815
- gates = /* @__PURE__ */ new Map();
8816
- /**
8817
- * Declare a channel and get its gate.
8818
- *
8819
- * A duplicate name throws. Two declarations of one name is a programming
8820
- * error, not a merge: the operator would arm one and the other would stay
8821
- * dark, which is the dead-knob shape (D62) with an extra step.
8822
- */
8823
- declare(descriptor) {
8824
- const parsed = LogChannelDescriptorSchema.parse(descriptor);
8825
- if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process — two declarations of one name is a programming error, not a merge`);
8826
- const gate = new LogChannelGate(parsed);
8827
- this.gates.set(parsed.name, gate);
8828
- return gate;
8829
- }
8830
- /** The declarations, sorted by name so a list is stable to read and diff. */
8831
- list() {
8832
- return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
8833
- }
8834
- /** The gate for a declared channel, or `undefined`. */
8835
- gate(name) {
8836
- return this.gates.get(name);
8837
- }
8838
- /**
8839
- * Apply the FULL set of armed windows. Off the hot path.
8840
- *
8841
- * Full, not incremental, and that is the whole design: the document is the
8842
- * authority, so a channel the document does not name is disarmed here. An
8843
- * incremental apply would let a disarm get lost in transit and leave a
8844
- * channel running that nobody can see is running.
8845
- *
8846
- * A window already past its deadline is ignored rather than armed — a
8847
- * restore that re-armed an expired window would make a forgotten diagnostic
8848
- * immortal across restarts.
8849
- *
8850
- * Returns the names it could not place, so the caller can log them: a
8851
- * channel named in the document that this process does not declare is
8852
- * either a typo or an addon that has not booted yet, and both deserve a
8853
- * line rather than silence.
8854
- */
8855
- apply(windows, nowMs) {
8856
- const wanted = /* @__PURE__ */ new Map();
8857
- const unknown = [];
8858
- for (const window of windows) {
8859
- if (window.armedUntilMs <= nowMs) continue;
8860
- if (!this.gates.has(window.channel)) {
8861
- unknown.push(window.channel);
8862
- continue;
8863
- }
8864
- wanted.set(window.channel, window);
8865
- }
8866
- for (const [name, gate] of this.gates) {
8867
- const window = wanted.get(name);
8868
- if (window === void 0) gate.disarm();
8869
- else gate.arm(window);
8870
- }
8871
- return unknown;
8872
- }
8873
- /**
8874
- * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
8875
- * diagnostic that adds a `Date.now()` to the path it is measuring measures
8876
- * itself.
8877
- *
8878
- * Returns the names it closed, so the caller can write the one line that
8879
- * says a window ended and stops "it went quiet" from reading as "the branch
8880
- * was not taken".
8881
- */
8882
- tick(nowMs) {
8883
- const closed = [];
8884
- for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
8885
- gate.disarm();
8886
- closed.push(name);
8887
- }
8888
- return closed;
8889
- }
8890
- /** The channels armed right now, as the document would describe them. */
8891
- armed() {
8892
- const out = [];
8893
- for (const [name, gate] of this.gates) if (gate.on) out.push({
8894
- channel: name,
8895
- armedUntilMs: gate.armedUntilMs,
8896
- deviceIds: null
8897
- });
8898
- return out;
8899
- }
8900
- };
8901
- /**
8902
- * Process-wide holder for the {@link LogChannelRegistry}.
8903
- *
8904
- * Three call sites that never meet need the SAME instance: the hot paths that
8905
- * declare a gate at module scope, the `log-channels` provider that enumerates
8906
- * the declarations for the hub, and the same provider applying the windows the
8907
- * document hands down. A registry built inside any one of them would be
8908
- * refreshed and collected — the shape of a knob that never does anything.
8909
- *
8910
- * Same idiom as `logging-gate.singleton.ts` and
8911
- * `http-request-census.singleton.ts`.
8912
- */
8913
- var instance = null;
8914
- /** The process-wide log channel registry. Created empty on first use. */
8915
- function getLogChannelRegistry() {
8916
- instance ??= new LogChannelRegistry();
8917
- return instance;
8918
- }
8919
- /**
8920
- * Declare a channel on the process-wide registry and get its gate.
8921
- *
8922
- * The one call an addon makes. Keep the returned gate in a module-scope
8923
- * `const`: looking a channel up by name per line would put a Map lookup on
8924
- * exactly the path this mechanism exists to keep free.
8925
- *
8926
- * `scripts/check-log-channel-gated.ts` reads these call sites. It pairs the
8927
- * declared name with the binding it is assigned to and refuses to let a
8928
- * channel ship that no `<binding>.on` anywhere consults — a declared channel
8929
- * nobody reads is a knob the operator turns with nothing happening, forever,
8930
- * and without a line. That is D62, and this repo has now shipped it three
8931
- * times (`audioThresholdDbfs`, the HA entities with no source, the second
8932
- * per-camera switch that wrote a store nobody read).
8933
- */
8934
- function declareLogChannel(descriptor) {
8935
- return getLogChannelRegistry().declare(descriptor);
8936
- }
8937
- /**
8938
- * Build the `log-channels` provider for this process.
8939
- *
8940
- * `logger` is used ONLY off the hot path — for the arm/expiry lines — so a
8941
- * channel that is never armed costs this module nothing but a timer.
8942
- */
8943
- function createLogChannelsProvider(logger, options = {}) {
8944
- const registry = getLogChannelRegistry();
8945
- const now = options.now ?? Date.now;
8946
- const tickMs = options.tickMs ?? 5e3;
8947
- const timer = setInterval(() => {
8948
- const closed = registry.tick(now());
8949
- for (const name of closed) logger.info("log channel window closed", {
8950
- tags: { logChannel: name },
8951
- meta: { channel: name }
8952
- });
8953
- }, tickMs);
8954
- timer.unref?.();
8955
- return {
8956
- list: () => registry.list(),
8957
- apply: (input) => {
8958
- const unknown = registry.apply(input.windows, now());
8959
- const armed = registry.armed();
8960
- logger.info("log channels applied", { meta: {
8961
- armed: armed.map((window) => window.channel),
8962
- unknown,
8963
- declared: registry.list().length
8964
- } });
8965
- return {
8966
- armed: armed.length,
8967
- unknown
8968
- };
8969
- },
8970
- stop: () => {
8971
- clearInterval(timer);
8972
- }
8973
- };
8974
- }
8975
- /**
8976
8620
  * Ops-log — the durable, append-only operations audit shared by the
8977
8621
  * recordings and events management surfaces.
8978
8622
  *
@@ -10111,8 +9755,11 @@ var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
10111
9755
  * `STORAGE_LOCATION_CARDINALITY` map has been removed.
10112
9756
  *
10113
9757
  * `id` is a stable namespaced string of the form `<type>:<slug>`.
10114
- * The default location for a type uses `id === <type>:default` by
10115
- * convention (the bare type ref like `'backups'` resolves to it).
9758
+ * The seed names its first instance `<type>:default` — a NAME, not a flag.
9759
+ * There is no default location any more (D383): `enabled` is the whole write
9760
+ * model, and a bare type ref resolves to the sole location of the type, or —
9761
+ * transitionally, only while legacy NULL-stamped rows exist — to the row whose
9762
+ * slug is `default`.
10116
9763
  *
10117
9764
  * `isSystem` is a legacy persisted flag. Seed still creates the initial
10118
9765
  * `<type>:default` locations; the flag is no longer a lock, a badge, or a
@@ -10133,21 +9780,20 @@ var StorageLocationSchema = object({
10133
9780
  * flag at upsert time, not here (the schema is provider-agnostic).
10134
9781
  */
10135
9782
  nodeId: string().optional(),
10136
- isDefault: boolean().default(false),
10137
9783
  isSystem: boolean().default(false),
10138
9784
  /**
10139
- * Operator opt-in: whether consumers that BALANCE across several locations
10140
- * of a type may write here. Recordings reads it today; event media and
10141
- * backups are the next consumers, which is why the flag lives on the
10142
- * location rather than in any one addon's store — nothing has to be
10143
- * extended to add the next consumer.
9785
+ * THE write switch, and the only one (D383). `enabled: true` means every
9786
+ * consumer that chooses a write target for this type may write here, and all
9787
+ * enabled locations of a type are used TOGETHER; `false` means read-only —
9788
+ * still read, still played back, still age-swept, still drained, never
9789
+ * written.
10144
9790
  *
10145
- * OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
10146
- * flag existed reads back with no flag and keeps working exactly as before;
10147
- * that is the whole compat story, and it is why no migration ships with it.
10148
- * A newly CREATED sibling is stamped `false` by the orchestrator (creating a
10149
- * disk must not silently start writing to it); the default of a type is
10150
- * always stamped `true`.
9791
+ * OPTIONAL only for the wire: an upsert that omits it means "leave what is
9792
+ * stored" on an update and "born inert unless it is the first location of its
9793
+ * type" on a create. On a PERSISTED row absence is legacy and it means
9794
+ * enabled — {@link isLocationEnabled} is the one place that says so, and the
9795
+ * orchestrator stamps every flagless row `true` once at hydrate so absence
9796
+ * stops existing rather than being re-derived on every read.
10151
9797
  */
10152
9798
  enabled: boolean().optional(),
10153
9799
  /** COMPUTED at read time by the orchestrator (statfs of the backing volume
@@ -10160,10 +9806,12 @@ var StorageLocationSchema = object({
10160
9806
  createdAt: number(),
10161
9807
  updatedAt: number()
10162
9808
  });
9809
+ object({ isDefault: boolean().optional() });
10163
9810
  /**
10164
9811
  * Reference accepted by consumer-facing `api.storage.*` calls.
10165
9812
  * Either:
10166
- * - a `StorageLocationType` (e.g. `'backups'`) → orchestrator resolves to the default of that type
9813
+ * - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
9814
+ * (transitionally, the `<type>:default`-slugged row when several exist)
10167
9815
  * - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
10168
9816
  *
10169
9817
  * The orchestrator's `resolveRef(ref)` handles both cases.
@@ -10281,64 +9929,420 @@ var DecoderStatsSchema = object({
10281
9929
  avgDecodeTimeMs: number(),
10282
9930
  droppedFrames: number(),
10283
9931
  /**
10284
- * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
10285
- * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
10286
- * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
10287
- * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
9932
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
9933
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
9934
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
9935
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
9936
+ */
9937
+ lagMs: number().optional(),
9938
+ effectiveFps: number().optional(),
9939
+ adaptiveFps: number().optional()
9940
+ });
9941
+ var DecoderSessionConfigSchema = object({
9942
+ codec: string(),
9943
+ maxFps: number().default(0),
9944
+ outputFormat: _enum([
9945
+ "jpeg",
9946
+ "rgb",
9947
+ "bgr",
9948
+ "yuv420",
9949
+ "gray"
9950
+ ]).default("jpeg"),
9951
+ scale: number().default(1),
9952
+ width: number().optional(),
9953
+ height: number().optional(),
9954
+ /**
9955
+ * Identifier of the camera this decoder session serves. Optional
9956
+ * because the cap is generic (any caller could request decode), but
9957
+ * stream-broker passes it so decoder logs include `deviceId` for
9958
+ * per-camera filtering when diagnosing failures (e.g. node-av
9959
+ * sendPacket errors on a single hung camera).
9960
+ */
9961
+ deviceId: number().int().nonnegative().optional(),
9962
+ /**
9963
+ * Free-form tag for log scoping. Stream-broker uses
9964
+ * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
9965
+ * on every line so `grep tag=broker:5/high` filters one camera
9966
+ * profile cleanly.
9967
+ */
9968
+ tag: string().optional(),
9969
+ /**
9970
+ * Where the session delivers decoded frames (Phase 5 / D9):
9971
+ *
9972
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
9973
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
9974
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
9975
+ * into an OS shared-memory ring and drained as zero-pixel
9976
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
9977
+ * other — `pullFrames` returns nothing for an `'shm'` session and
9978
+ * `pullHandles` returns nothing for a `'callback'` session.
9979
+ */
9980
+ frameSink: _enum(["callback", "shm"]).default("callback"),
9981
+ /**
9982
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
9983
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
9984
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
9985
+ * stream-broker's `streamingDebug` gate — off by default so production logs
9986
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
9987
+ */
9988
+ debug: boolean().optional()
9989
+ });
9990
+ /**
9991
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
9992
+ * an addon declares its channels in.
9993
+ *
9994
+ * ## Two axes, deliberately separated
9995
+ *
9996
+ * - **DECLARATION** — which channels exist. Only the addon knows:
9997
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
9998
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
9999
+ * and rots silently. So a channel is declared where it is consulted, and the
10000
+ * `log-channels` capability enumerates the declarations.
10001
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
10002
+ * thing: the logging settings document on the `system` cap. Two authorities
10003
+ * over the values is the exact defect
10004
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
10005
+ * remove; re-introducing it from the cure side would be grotesque.
10006
+ *
10007
+ * Nothing in this file reads a clock, an env var or a store. The registry is
10008
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
10009
+ * the hot path with a value somebody actually read, and by
10010
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
10011
+ * never reaches here, so it can neither disarm an armed channel nor arm a
10012
+ * disarmed one (D49).
10013
+ *
10014
+ * ## The canonical call shape
10015
+ *
10016
+ * ```ts
10017
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
10018
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
10019
+ * }
10020
+ * ```
10021
+ *
10022
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
10023
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
10024
+ * object literal is never constructed because it lives inside the branch. It
10025
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
10026
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
10027
+ * destination floor (measured at 1.93 ns/call when off).
10028
+ *
10029
+ * ## Why a channel emits at `info`
10030
+ *
10031
+ * `loki-logging.addon.ts` pins the destination default at `info` and
10032
+ * `loki-destination.ts` drops everything below it, so a line emitted at
10033
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
10034
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
10035
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
10036
+ * emits at the channel's declared level, whose schema floor is `info`.
10037
+ */
10038
+ /**
10039
+ * The level a channel writes at once armed.
10040
+ *
10041
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
10042
+ * not leave the process for Loki, and the whole point of arming a channel is
10043
+ * to read it later.
10044
+ */
10045
+ var LogChannelLevelSchema = _enum([
10046
+ "info",
10047
+ "warn",
10048
+ "error"
10049
+ ]);
10050
+ /**
10051
+ * What an addon declares about one channel. No value, no state — a
10052
+ * declaration is inert.
10053
+ */
10054
+ var LogChannelDescriptorSchema = object({
10055
+ /**
10056
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
10057
+ * the addon's short name so an operator reading a channel list can tell who
10058
+ * owns it without a second lookup.
10059
+ */
10060
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
10061
+ /** One sentence: what the operator will SEE after arming it. */
10062
+ description: string().min(1),
10063
+ /** The level its lines are emitted at. Never below `info`. */
10064
+ defaultLevel: LogChannelLevelSchema,
10065
+ /**
10066
+ * Whether this channel can be narrowed to a camera.
10067
+ *
10068
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
10069
+ * consulted with the numeric device id, AND every line the channel admits
10070
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
10071
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
10072
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
10073
+ * the body is the only way to filter.
10074
+ *
10075
+ * A channel whose lines carry the device only in `meta` (or not at all) is
10076
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
10077
+ * the operator narrows to one camera, sees nothing, and concludes the code
10078
+ * path was never taken.
10079
+ */
10080
+ perDevice: boolean()
10081
+ });
10082
+ /**
10083
+ * An armed window over one channel, as the document hands it to a mirror.
10084
+ *
10085
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
10086
+ * expires by itself, which is the one failure a boolean cannot avoid.
10087
+ */
10088
+ var LogChannelWindowSchema = object({
10089
+ channel: string().min(1),
10090
+ /** Epoch ms the window closes at. */
10091
+ armedUntilMs: number(),
10092
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
10093
+ deviceIds: array(number().int()).readonly().nullable()
10094
+ });
10095
+ /**
10096
+ * The gate a hot path holds.
10097
+ *
10098
+ * Obtain it ONCE — at module scope or in a constructor — and keep the
10099
+ * reference. Looking a channel up by name per line would put a Map lookup on
10100
+ * the path this class exists to keep free.
10101
+ */
10102
+ var LogChannelGate = class {
10103
+ descriptor;
10104
+ /**
10105
+ * HOT PATH GUARD. A plain data FIELD, and it must stay one.
10106
+ *
10107
+ * `log-channel.spec.ts` asserts the property descriptor has no getter and
10108
+ * booby-traps the device set, so turning this into an accessor — or reading
10109
+ * anything before it — fails the spec instead of taxing every line the
10110
+ * process emits.
10111
+ */
10112
+ on = false;
10113
+ /** `null` while armed for every camera. Never read while `on` is false. */
10114
+ devices = null;
10115
+ level;
10116
+ closesAtMs = 0;
10117
+ constructor(descriptor) {
10118
+ this.descriptor = descriptor;
10119
+ this.level = descriptor.defaultLevel;
10120
+ }
10121
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
10122
+ get armedUntilMs() {
10123
+ return this.on ? this.closesAtMs : 0;
10124
+ }
10125
+ /**
10126
+ * Does this channel want a line about `deviceId`?
10127
+ *
10128
+ * Call it only behind `gate.on &&`. On its own it is still correct — the
10129
+ * guard is repeated inside — but the point of the prefix is that a disarmed
10130
+ * channel must not pay the call at all.
10131
+ */
10132
+ wants(deviceId) {
10133
+ if (!this.on) return false;
10134
+ return this.devices === null || this.devices.has(deviceId);
10135
+ }
10136
+ /**
10137
+ * Emit one line on this channel, at the channel's declared level.
10138
+ *
10139
+ * The channel name is added as `tags.logChannel` so LogQL can select the
10140
+ * channel without matching on the message text, and whatever `tags` the
10141
+ * caller passed — `deviceId` above all — is preserved.
10288
10142
  */
10289
- lagMs: number().optional(),
10290
- effectiveFps: number().optional(),
10291
- adaptiveFps: number().optional()
10292
- });
10293
- var DecoderSessionConfigSchema = object({
10294
- codec: string(),
10295
- maxFps: number().default(0),
10296
- outputFormat: _enum([
10297
- "jpeg",
10298
- "rgb",
10299
- "bgr",
10300
- "yuv420",
10301
- "gray"
10302
- ]).default("jpeg"),
10303
- scale: number().default(1),
10304
- width: number().optional(),
10305
- height: number().optional(),
10143
+ log(logger, message, extras) {
10144
+ if (!this.on) return;
10145
+ const tags = {
10146
+ ...extras.tags,
10147
+ logChannel: this.descriptor.name
10148
+ };
10149
+ const line = {
10150
+ ...extras,
10151
+ tags
10152
+ };
10153
+ if (this.level === "error") logger.error(message, line);
10154
+ else if (this.level === "warn") logger.warn(message, line);
10155
+ else logger.info(message, line);
10156
+ }
10306
10157
  /**
10307
- * Identifier of the camera this decoder session serves. Optional
10308
- * because the cap is generic (any caller could request decode), but
10309
- * stream-broker passes it so decoder logs include `deviceId` for
10310
- * per-camera filtering when diagnosing failures (e.g. node-av
10311
- * sendPacket errors on a single hung camera).
10158
+ * Arm (or RE-arm, restarting) this channel. Off the hot path only.
10159
+ *
10160
+ * An empty `deviceIds` list is treated as "every camera" rather than "no
10161
+ * camera": a window that matches nothing is indistinguishable from a
10162
+ * disarmed one, and the operator who asked for it would wait for lines that
10163
+ * can never come.
10312
10164
  */
10313
- deviceId: number().int().nonnegative().optional(),
10165
+ arm(window) {
10166
+ const ids = window.deviceIds;
10167
+ this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
10168
+ this.closesAtMs = window.armedUntilMs;
10169
+ this.on = true;
10170
+ }
10171
+ /** Disarm. Off the hot path only. */
10172
+ disarm() {
10173
+ this.on = false;
10174
+ this.devices = null;
10175
+ this.closesAtMs = 0;
10176
+ }
10177
+ };
10178
+ /**
10179
+ * Every channel this PROCESS declares, and the mirror of what is armed on it.
10180
+ *
10181
+ * One per process. A forked runner has its own, and it is refreshed through
10182
+ * the `log-channels` capability by the hub that owns the document — the
10183
+ * registry never reaches for a value itself.
10184
+ */
10185
+ var LogChannelRegistry = class {
10186
+ gates = /* @__PURE__ */ new Map();
10314
10187
  /**
10315
- * Free-form tag for log scoping. Stream-broker uses
10316
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10317
- * on every line so `grep tag=broker:5/high` filters one camera
10318
- * profile cleanly.
10188
+ * Declare a channel and get its gate.
10189
+ *
10190
+ * A duplicate name throws. Two declarations of one name is a programming
10191
+ * error, not a merge: the operator would arm one and the other would stay
10192
+ * dark, which is the dead-knob shape (D62) with an extra step.
10319
10193
  */
10320
- tag: string().optional(),
10194
+ declare(descriptor) {
10195
+ const parsed = LogChannelDescriptorSchema.parse(descriptor);
10196
+ if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process — two declarations of one name is a programming error, not a merge`);
10197
+ const gate = new LogChannelGate(parsed);
10198
+ this.gates.set(parsed.name, gate);
10199
+ return gate;
10200
+ }
10201
+ /** The declarations, sorted by name so a list is stable to read and diff. */
10202
+ list() {
10203
+ return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
10204
+ }
10205
+ /** The gate for a declared channel, or `undefined`. */
10206
+ gate(name) {
10207
+ return this.gates.get(name);
10208
+ }
10321
10209
  /**
10322
- * Where the session delivers decoded frames (Phase 5 / D9):
10210
+ * Apply the FULL set of armed windows. Off the hot path.
10323
10211
  *
10324
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
10325
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
10326
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
10327
- * into an OS shared-memory ring and drained as zero-pixel
10328
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
10329
- * other — `pullFrames` returns nothing for an `'shm'` session and
10330
- * `pullHandles` returns nothing for a `'callback'` session.
10212
+ * Full, not incremental, and that is the whole design: the document is the
10213
+ * authority, so a channel the document does not name is disarmed here. An
10214
+ * incremental apply would let a disarm get lost in transit and leave a
10215
+ * channel running that nobody can see is running.
10216
+ *
10217
+ * A window already past its deadline is ignored rather than armed — a
10218
+ * restore that re-armed an expired window would make a forgotten diagnostic
10219
+ * immortal across restarts.
10220
+ *
10221
+ * Returns the names it could not place, so the caller can log them: a
10222
+ * channel named in the document that this process does not declare is
10223
+ * either a typo or an addon that has not booted yet, and both deserve a
10224
+ * line rather than silence.
10331
10225
  */
10332
- frameSink: _enum(["callback", "shm"]).default("callback"),
10226
+ apply(windows, nowMs) {
10227
+ const wanted = /* @__PURE__ */ new Map();
10228
+ const unknown = [];
10229
+ for (const window of windows) {
10230
+ if (window.armedUntilMs <= nowMs) continue;
10231
+ if (!this.gates.has(window.channel)) {
10232
+ unknown.push(window.channel);
10233
+ continue;
10234
+ }
10235
+ wanted.set(window.channel, window);
10236
+ }
10237
+ for (const [name, gate] of this.gates) {
10238
+ const window = wanted.get(name);
10239
+ if (window === void 0) gate.disarm();
10240
+ else gate.arm(window);
10241
+ }
10242
+ return unknown;
10243
+ }
10333
10244
  /**
10334
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10335
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10336
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10337
- * stream-broker's `streamingDebug` gate — off by default so production logs
10338
- * stay quiet and the emit path pays zero per-frame cost when disabled.
10245
+ * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
10246
+ * diagnostic that adds a `Date.now()` to the path it is measuring measures
10247
+ * itself.
10248
+ *
10249
+ * Returns the names it closed, so the caller can write the one line that
10250
+ * says a window ended and stops "it went quiet" from reading as "the branch
10251
+ * was not taken".
10339
10252
  */
10340
- debug: boolean().optional()
10341
- });
10253
+ tick(nowMs) {
10254
+ const closed = [];
10255
+ for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
10256
+ gate.disarm();
10257
+ closed.push(name);
10258
+ }
10259
+ return closed;
10260
+ }
10261
+ /** The channels armed right now, as the document would describe them. */
10262
+ armed() {
10263
+ const out = [];
10264
+ for (const [name, gate] of this.gates) if (gate.on) out.push({
10265
+ channel: name,
10266
+ armedUntilMs: gate.armedUntilMs,
10267
+ deviceIds: null
10268
+ });
10269
+ return out;
10270
+ }
10271
+ };
10272
+ /**
10273
+ * Process-wide holder for the {@link LogChannelRegistry}.
10274
+ *
10275
+ * Three call sites that never meet need the SAME instance: the hot paths that
10276
+ * declare a gate at module scope, the `log-channels` provider that enumerates
10277
+ * the declarations for the hub, and the same provider applying the windows the
10278
+ * document hands down. A registry built inside any one of them would be
10279
+ * refreshed and collected — the shape of a knob that never does anything.
10280
+ *
10281
+ * Same idiom as `logging-gate.singleton.ts` and
10282
+ * `http-request-census.singleton.ts`.
10283
+ */
10284
+ var instance = null;
10285
+ /** The process-wide log channel registry. Created empty on first use. */
10286
+ function getLogChannelRegistry() {
10287
+ instance ??= new LogChannelRegistry();
10288
+ return instance;
10289
+ }
10290
+ /**
10291
+ * Declare a channel on the process-wide registry and get its gate.
10292
+ *
10293
+ * The one call an addon makes. Keep the returned gate in a module-scope
10294
+ * `const`: looking a channel up by name per line would put a Map lookup on
10295
+ * exactly the path this mechanism exists to keep free.
10296
+ *
10297
+ * `scripts/check-log-channel-gated.ts` reads these call sites. It pairs the
10298
+ * declared name with the binding it is assigned to and refuses to let a
10299
+ * channel ship that no `<binding>.on` anywhere consults — a declared channel
10300
+ * nobody reads is a knob the operator turns with nothing happening, forever,
10301
+ * and without a line. That is D62, and this repo has now shipped it three
10302
+ * times (`audioThresholdDbfs`, the HA entities with no source, the second
10303
+ * per-camera switch that wrote a store nobody read).
10304
+ */
10305
+ function declareLogChannel(descriptor) {
10306
+ return getLogChannelRegistry().declare(descriptor);
10307
+ }
10308
+ /**
10309
+ * Build the `log-channels` provider for this process.
10310
+ *
10311
+ * `logger` is used ONLY off the hot path — for the arm/expiry lines — so a
10312
+ * channel that is never armed costs this module nothing but a timer.
10313
+ */
10314
+ function createLogChannelsProvider(logger, options = {}) {
10315
+ const registry = getLogChannelRegistry();
10316
+ const now = options.now ?? Date.now;
10317
+ const tickMs = options.tickMs ?? 5e3;
10318
+ const timer = setInterval(() => {
10319
+ const closed = registry.tick(now());
10320
+ for (const name of closed) logger.info("log channel window closed", {
10321
+ tags: { logChannel: name },
10322
+ meta: { channel: name }
10323
+ });
10324
+ }, tickMs);
10325
+ timer.unref?.();
10326
+ return {
10327
+ list: () => registry.list(),
10328
+ apply: (input) => {
10329
+ const unknown = registry.apply(input.windows, now());
10330
+ const armed = registry.armed();
10331
+ logger.info("log channels applied", { meta: {
10332
+ armed: armed.map((window) => window.channel),
10333
+ unknown,
10334
+ declared: registry.list().length
10335
+ } });
10336
+ return {
10337
+ armed: armed.length,
10338
+ unknown
10339
+ };
10340
+ },
10341
+ stop: () => {
10342
+ clearInterval(timer);
10343
+ }
10344
+ };
10345
+ }
10342
10346
  /**
10343
10347
  * Distinct (device, family, variant) counters one instance will hold.
10344
10348
  *
@@ -24331,7 +24335,7 @@ method(object({
24331
24335
  downloadId: string(),
24332
24336
  offset: number(),
24333
24337
  length: number()
24334
- }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(object({ type: StorageLocationTypeSchema }), StorageLocationSchema.nullable()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
24338
+ }), _instanceof(Uint8Array)), method(object({ downloadId: string() }), _void(), { kind: "mutation" }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
24335
24339
  createdAt: true,
24336
24340
  updatedAt: true
24337
24341
  }), StorageLocationSchema, {
@@ -37763,12 +37767,6 @@ Object.freeze({
37763
37767
  addonId: null,
37764
37768
  access: "view"
37765
37769
  },
37766
- "storage.getDefaultLocation": {
37767
- capName: "storage",
37768
- capScope: "system",
37769
- addonId: null,
37770
- access: "view"
37771
- },
37772
37770
  "storage.list": {
37773
37771
  capName: "storage",
37774
37772
  capScope: "system",