@camstack/system 1.2.58 → 1.2.60

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 (54) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  15. package/dist/builtins/doorbell/binding-mirror.d.ts +65 -0
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +56 -11
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +248 -26
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +248 -26
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +2 -2
  36. package/dist/builtins/snapshot/index.mjs +2 -2
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +94 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +142 -2
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +142 -2
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-UrXyJ9Zm.mjs → dist-C5XpOjbI.mjs} +841 -38
  49. package/dist/{dist-Chl2MvMd.js → dist-Cis_iV-u.js} +852 -37
  50. package/dist/index.js +187 -39
  51. package/dist/index.mjs +187 -39
  52. package/dist/kernel/addon-installer.d.ts +55 -7
  53. package/dist/kernel/addon-manifest.d.ts +36 -16
  54. package/package.json +1 -1
@@ -1814,7 +1814,20 @@ var BrokerStatsSchema = z.object({
1814
1814
  sampleRate: z.number(),
1815
1815
  channels: z.number(),
1816
1816
  supported: z.boolean()
1817
- }).nullable().optional()
1817
+ }).nullable().optional(),
1818
+ /**
1819
+ * BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
1820
+ * distributing none of the device's audio, on live or recording.
1821
+ *
1822
+ * Present so a silent camera can be told apart from a broken one on the
1823
+ * stream panel itself, without cross-referencing the switch group: a
1824
+ * broker holding an `audio` track descriptor while `audioMuted` is true is
1825
+ * working exactly as asked. `audioMutedDropped` counts the audio units
1826
+ * thrown away since the current dial — it is how you confirm from stats
1827
+ * alone that the mute is on the packet path and not merely persisted.
1828
+ */
1829
+ audioMuted: z.boolean().optional(),
1830
+ audioMutedDropped: z.number().optional()
1818
1831
  });
1819
1832
  /**
1820
1833
  * Exporter-facing "profile restream" entry. Returned by
@@ -2299,9 +2312,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
2299
2312
  /**
2300
2313
  * Build the tRPC request options that pin a single capability call to `nodeId`.
2301
2314
  * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
2315
+ *
2316
+ * ## The id is normalised here, and it has to be
2317
+ *
2318
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
2319
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
2320
+ * compares a pin against real node ids, so such a pin matches nothing and the
2321
+ * call fails with `no provider registered for cap "…"`. The local-first
2322
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
2323
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
2324
+ * the thing you reach for when you specifically need THIS node — silently
2325
+ * addressed a node that does not exist.
2326
+ *
2327
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
2328
+ * own node to read the host's hardware-decode backend. It never once answered,
2329
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
2330
+ * while D67's whole premise was that the decoder addon is the authority on
2331
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
2332
+ * hardware", which was false.
2333
+ *
2334
+ * Normalising in the ONE constructor fixes every caller at once, which is why
2335
+ * it is here and not at the call sites.
2302
2336
  */
2303
2337
  function nodePin(nodeId) {
2304
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
2338
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
2339
+ }
2340
+ /**
2341
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
2342
+ * is idempotent, so passing an already-clean id costs nothing.
2343
+ */
2344
+ function toNodeId(idOrRunnerId) {
2345
+ const head = idOrRunnerId.split("/")[0];
2346
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
2305
2347
  }
2306
2348
  /**
2307
2349
  * Generic types for capability definitions.
@@ -3164,7 +3206,7 @@ z.object({
3164
3206
  * ## This file adds no state
3165
3207
  *
3166
3208
  * Every switch here is a VIEW onto an authority that already existed
3167
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
3209
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
3168
3210
  * group is that there is exactly one place each function is turned off, and
3169
3211
  * the group routes to it:
3170
3212
  *
@@ -3175,6 +3217,53 @@ z.object({
3175
3217
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
3176
3218
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
3177
3219
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
3220
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
3221
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
3222
+ * | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
3223
+ *
3224
+ * ## `device-audio` and `broker-audio` are two functions, not two knobs
3225
+ *
3226
+ * They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
3227
+ * `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
3228
+ * genuinely stops, it survives CamStack entirely, and it costs a multi-second
3229
+ * encoder restart on every flip. `broker-audio` writes THIS server, so it is
3230
+ * instant, vendor-independent and reversible without touching the camera, and
3231
+ * a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
3232
+ * D62 forbids a second switch that *disagrees* with the first; these two
3233
+ * cannot disagree, because neither reads the other's store — the camera holds
3234
+ * one, the broker holds the other, and each reports its own fact.
3235
+ *
3236
+ * ## The two switches whose authority is not on this server
3237
+ *
3238
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
3239
+ * in "the group stores nothing" — it is the purest form of it: the camera
3240
+ * holds the fact, every read is a read-through, and there is no server-side
3241
+ * copy that could drift. Their availability therefore cannot come from
3242
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
3243
+ * is filtered out there); it comes from the cap's own camera-probed
3244
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
3245
+ * THIS camera rather than for the device type
3246
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
3247
+ *
3248
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
3249
+ *
3250
+ * Every other switch means *this camera's function is doing its job*, so
3251
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
3252
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
3253
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
3254
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
3255
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
3256
+ * worse than either surface not having one.
3257
+ *
3258
+ * Two consequences follow and both are load-bearing:
3259
+ *
3260
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
3261
+ * exactly this row. With the polarity above, every camera that has NOT drawn
3262
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
3263
+ * normal, healthy state of most cameras rendered as an operator disablement.
3264
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
3265
+ * unconditionally by both clients, so for this row it has to read correctly
3266
+ * whichever way the switch is sitting.
3178
3267
  *
3179
3268
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
3180
3269
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -3193,14 +3282,18 @@ z.object({
3193
3282
  * `CameraStatus.switchedOff`.
3194
3283
  */
3195
3284
  /**
3196
- * The five functions the operator named (2026-08-05). Deliberately NOT one id
3197
- * per pipeline step: face recognition and plate/LPR are per-step toggles on
3285
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
3286
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
3287
+ * recognition and plate/LPR are per-step toggles on
3198
3288
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
3199
- * editor, not in a five-button safety group.
3289
+ * editor, not in a safety group.
3200
3290
  */
3201
3291
  var CameraSwitchIdSchema = z.enum([
3202
3292
  "stream-broker",
3203
3293
  "object-detection",
3294
+ "privacy-mask",
3295
+ "device-audio",
3296
+ "broker-audio",
3204
3297
  "audio-analysis",
3205
3298
  "recording",
3206
3299
  "notifications"
@@ -3218,14 +3311,27 @@ var CameraSwitchAuthoritySchema = z.discriminatedUnion("kind", [
3218
3311
  capName: z.string()
3219
3312
  }),
3220
3313
  z.object({ kind: z.literal("recording-config") }),
3221
- z.object({ kind: z.literal("notification-mute") })
3314
+ z.object({ kind: z.literal("notification-mute") }),
3315
+ z.object({
3316
+ kind: z.literal("camera-audio"),
3317
+ capName: z.string()
3318
+ }),
3319
+ z.object({
3320
+ kind: z.literal("camera-mask"),
3321
+ capName: z.string()
3322
+ }),
3323
+ z.object({ kind: z.literal("broker-audio-mute") })
3222
3324
  ]);
3223
3325
  /**
3224
3326
  * Why a switch is not offered for this camera. Rendered instead of the
3225
3327
  * control, never as a dead control — an absent function and a broken one must
3226
3328
  * not look the same.
3227
3329
  */
3228
- var CameraSwitchUnavailableReasonSchema = z.enum(["no-provider", "source-unreachable"]);
3330
+ var CameraSwitchUnavailableReasonSchema = z.enum([
3331
+ "no-provider",
3332
+ "source-unreachable",
3333
+ "not-configured"
3334
+ ]);
3229
3335
  /**
3230
3336
  * One switch, resolved for one camera.
3231
3337
  *
@@ -5589,6 +5695,26 @@ var EgressTranscodeRequestSchema = z.object({
5589
5695
  "h264_mp4toannexb",
5590
5696
  "hevc_mp4toannexb"
5591
5697
  ]).optional(),
5698
+ /**
5699
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
5700
+ * consumer to dial the returned url. The broker picks the id and returns it
5701
+ * as `camStreamId` — a caller-supplied one would be circular, since the
5702
+ * sharing key is computed FROM this request.
5703
+ *
5704
+ * The url is still returned and still the contract for a transcode pinned to
5705
+ * another node. But dialling it locally costs an RTSP round trip that changes
5706
+ * the transport underneath the consumer: a dialled stream is an RTP source,
5707
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
5708
+ * repacketizer branch. The push branch — the one the derived mechanism has
5709
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
5710
+ * arriving, key frame arriving, black screen, on a chain healthy at every
5711
+ * other point.
5712
+ *
5713
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
5714
+ * gives priority to `hub-in-process` so a local call never leaves the node.
5715
+ * This is that rule for media.
5716
+ */
5717
+ publishLocally: z.boolean().optional(),
5592
5718
  pixelFormat: z.enum(["yuv420p", "nv12"]).optional(),
5593
5719
  /**
5594
5720
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -5633,7 +5759,13 @@ var EgressTranscodeSchema = z.object({
5633
5759
  * Returned rather than assumed: a consumer that asked for hardware and got
5634
5760
  * software needs to be able to see that without reading the broker's logs.
5635
5761
  */
5636
- decodeHwAccel: z.string().nullable()
5762
+ decodeHwAccel: z.string().nullable(),
5763
+ /**
5764
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
5765
+ * `url`, and the session takes the push/deframe transport rather than the
5766
+ * RTP-passthrough one. `null` means the consumer must dial.
5767
+ */
5768
+ camStreamId: z.string().nullable()
5637
5769
  });
5638
5770
  var streamBrokerCapability = {
5639
5771
  name: "stream-broker",
@@ -5927,7 +6059,44 @@ var streamBrokerCapability = {
5927
6059
  kind: "mutation",
5928
6060
  auth: "admin"
5929
6061
  }),
5930
- isRtspEnabled: method(z.object({ brokerId: z.string() }), z.boolean())
6062
+ isRtspEnabled: method(z.object({ brokerId: z.string() }), z.boolean()),
6063
+ /**
6064
+ * ── Per-device audio-plane policy (D83) ───────────────────────────
6065
+ *
6066
+ * The BROKER-side mute: while `muted`, this node distributes none of
6067
+ * the device's audio, on any plane it serves — live (WebRTC / encoded
6068
+ * subscribers) AND recording (the RTSP restreamer the recorder pulls,
6069
+ * which additionally serves the video-only SDP so the recorder's ffmpeg
6070
+ * never declares an audio stream it will not receive).
6071
+ *
6072
+ * Keyed by `deviceId`, not `brokerId`: every one of a camera's streams
6073
+ * carries the same microphone, and a per-stream answer would let main
6074
+ * and sub disagree about whether the camera is silent.
6075
+ *
6076
+ * The state lives in the broker's existing `DeviceOverride` blob — no
6077
+ * new store — and the mute is applied to a fresh broker at creation, so
6078
+ * a restart, a re-dial or a catalog republish never un-mutes a camera.
6079
+ */
6080
+ getDeviceAudioMute: method(z.object({ deviceId: z.number().int() }), z.object({
6081
+ muted: z.boolean(),
6082
+ /**
6083
+ * How many live non-derived brokers currently hold the mute. Purely
6084
+ * diagnostic: `muted` is the policy and is authoritative on its own
6085
+ * (it applies to brokers that do not exist yet), while this says
6086
+ * whether anything is presently being silenced.
6087
+ */
6088
+ appliedBrokers: z.number().int().nonnegative()
6089
+ })),
6090
+ setDeviceAudioMute: method(z.object({
6091
+ deviceId: z.number().int(),
6092
+ muted: z.boolean()
6093
+ }), z.object({
6094
+ muted: z.boolean(),
6095
+ appliedBrokers: z.number().int().nonnegative()
6096
+ }), {
6097
+ kind: "mutation",
6098
+ auth: "admin"
6099
+ })
5931
6100
  },
5932
6101
  events: {
5933
6102
  onCamStreamDemand: event(z.object({
@@ -12779,6 +12948,30 @@ var TrackSourceSchema = z.enum([
12779
12948
  "audio"
12780
12949
  ]);
12781
12950
  /**
12951
+ * Where a track sits in the RETRAIN lifecycle (D81).
12952
+ *
12953
+ * - `none` — never marked, or un-marked. Evictable.
12954
+ * - `staging` — the operator wants this track as training material and has not
12955
+ * finished with it. **This is the only state retention holds**: the track and
12956
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
12957
+ * the device's age window.
12958
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
12959
+ * were COPIED into the retrain dataset at selection time, so the dataset no
12960
+ * longer depends on the track's media and the track becomes EVICTABLE again.
12961
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
12962
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
12963
+ *
12964
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
12965
+ * the store's filter language has only positive equality and `whereIn` — no
12966
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
12967
+ * would make the entire pre-column history immortal in one deploy.
12968
+ */
12969
+ var RetrainStatusSchema = z.enum([
12970
+ "none",
12971
+ "staging",
12972
+ "trained"
12973
+ ]);
12974
+ /**
12782
12975
  * Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
12783
12976
  * by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
12784
12977
  * so the two surfaces cannot drift.
@@ -12788,18 +12981,31 @@ var TrackSourceSchema = z.enum([
12788
12981
  * columns existed read as absent, and a consumer that needs a boolean should say
12789
12982
  * `flag === true`, not `flag !== false`.
12790
12983
  *
12791
- * What the flags DO is deliberately UNDEFINED at the time of writing: they are
12792
- * operator curation, and the behaviour they drive will be specified separately.
12793
- * In particular a `markForTrain` track is NOT pinned against retention — see
12794
- * `docs/decisions/adr-0059.md` for why that is a store-level change, not a flag.
12984
+ * `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
12985
+ * it is exactly `retrainStatus === 'staging'`, in both directions. Writing
12986
+ * `true` moves `none → staging`, writing `false` moves `staging none`, and a
12987
+ * `trained` track reports `false` while refusing both writes. The boolean is
12988
+ * kept because three surfaces drive a toggle off it; anything that needs to tell
12989
+ * "never marked" from "already trained" must read `retrainStatus`.
12990
+ *
12991
+ * `debug` does NOT pin; it is attention, not durability.
12795
12992
  */
12796
12993
  var TrackFlagFields = {
12797
- /** Operator marked this track as training material. */
12994
+ /** Operator marked this track as training material — i.e. `retrainStatus` is
12995
+ * `'staging'`. */
12798
12996
  markForTrain: z.boolean().optional(),
12799
12997
  /** Operator marked this track for diagnostic attention. */
12800
12998
  debug: z.boolean().optional()
12801
12999
  };
12802
13000
  /**
13001
+ * The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
13002
+ * Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
13003
+ * write patch, and the status is not something the toggle sets — it is what the
13004
+ * toggle's boolean is derived from. Absent on an in-RAM track never touched;
13005
+ * always present on a persisted row (the column default materialises `'none'`).
13006
+ */
13007
+ var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
13008
+ /**
12803
13009
  * The write half: a PARTIAL patch. An omitted key is left untouched, so setting
12804
13010
  * one flag can never clear the other — the toggles are independent and are
12805
13011
  * driven from three surfaces that do not know about each other.
@@ -12813,7 +13019,32 @@ var TrackFlagsPatchSchema = z.object(TrackFlagFields);
12813
13019
  var TrackFlagsSchema = z.object({
12814
13020
  trackId: z.string(),
12815
13021
  markForTrain: z.boolean(),
12816
- debug: z.boolean()
13022
+ debug: z.boolean(),
13023
+ /** The lifecycle state the boolean was derived from. Required here (unlike on
13024
+ * a track row) because this shape is only ever produced by the write body,
13025
+ * which always knows it — and a surface that has just written needs to render
13026
+ * `trained` without a re-fetch. */
13027
+ retrainStatus: RetrainStatusSchema
13028
+ });
13029
+ /** Per-camera slice of a training-export estimate. */
13030
+ var TrainingExportDeviceTotalsSchema = z.object({
13031
+ deviceId: z.number(),
13032
+ tracks: z.number().int(),
13033
+ files: z.number().int(),
13034
+ bytes: z.number().int()
13035
+ });
13036
+ /**
13037
+ * What a training export WOULD contain. Computed from media index rows only —
13038
+ * no blob is read to produce this.
13039
+ */
13040
+ var TrainingExportSummarySchema = z.object({
13041
+ generatedAt: z.number(),
13042
+ trackCount: z.number().int(),
13043
+ fileCount: z.number().int(),
13044
+ byteCount: z.number().int(),
13045
+ /** More marked tracks exist than a single pass carries. */
13046
+ truncated: z.boolean(),
13047
+ devices: z.array(TrainingExportDeviceTotalsSchema).readonly()
12817
13048
  });
12818
13049
  var TrackSchema = z.object({
12819
13050
  trackId: z.string(),
@@ -12858,7 +13089,8 @@ var TrackSchema = z.object({
12858
13089
  * Populated from the persisted envelope columns on historical reads;
12859
13090
  * absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
12860
13091
  envelope: TrackEnvelopeSchema.optional(),
12861
- ...TrackFlagFields
13092
+ ...TrackFlagFields,
13093
+ ...TrackRetrainFields
12862
13094
  });
12863
13095
  var BaseEventFields = {
12864
13096
  id: z.string(),
@@ -13080,7 +13312,8 @@ var KeyEventSchema = z.object({
13080
13312
  bestEventId: z.string(),
13081
13313
  /** Track lifetime in ms (lastSeen - firstSeen). */
13082
13314
  windowMs: z.number().optional(),
13083
- ...TrackFlagFields
13315
+ ...TrackFlagFields,
13316
+ ...TrackRetrainFields
13084
13317
  });
13085
13318
  z.object({
13086
13319
  trackId: z.string(),
@@ -13442,11 +13675,29 @@ var pipelineAnalyticsCapability = {
13442
13675
  *
13443
13676
  * `auth: 'protected'` (the default), NOT `admin`: the viewer is an
13444
13677
  * authenticated non-admin surface and two of the three call sites are
13445
- * there. Revisit if a flag ever gains an effect that costs storage
13446
- * `deleteTracks` next door is admin for exactly that reason.
13678
+ * there. The note that used to sit here said to revisit this the day a flag
13679
+ * gained an effect that costs storage, and D81 is that day — `markForTrain`
13680
+ * now pins. It STAYS protected, and the reason is that the alternative
13681
+ * makes the feature pointless: marking a track is something you do while
13682
+ * looking at it, on the surface you were already looking at it on, and that
13683
+ * surface is the viewer. What the storage cost gets instead is a BOUND — a
13684
+ * per-device pin budget enforced in the body, refusing a new pin past the
13685
+ * limit while always allowing un-marking. `deleteTracks` next door is still
13686
+ * admin, because destroying evidence and preserving it are not symmetric.
13447
13687
  *
13448
- * Returns the RESOLVED state of both flags (absent `false`) so a caller
13449
- * can drive its toggle without a re-fetch. Rejects an unknown track.
13688
+ * `markForTrain` writes the retrain LIFECYCLE, not a boolean column: `true`
13689
+ * is `none staging`, `false` is `staging none`. A track already
13690
+ * `trained` refuses BOTH — its frames are copies inside the retrain dataset
13691
+ * and re-staging it from a generic toggle is how the same material gets
13692
+ * annotated twice under two ground truths. Returning a trained track to
13693
+ * staging is a deliberate action of the retrain page, which is also the only
13694
+ * thing that produces `trained` in the first place.
13695
+ *
13696
+ * Returns the RESOLVED state of both flags (absent → `false`) plus the
13697
+ * `retrainStatus` they were derived from, so a caller can drive its toggle
13698
+ * — and render a `trained` badge — without a re-fetch. Rejects an unknown
13699
+ * track, a new staging mark on a device already holding its full budget, and
13700
+ * any `markForTrain` write against a trained track.
13450
13701
  */
13451
13702
  setTrackFlags: method(z.object({
13452
13703
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -13510,6 +13761,42 @@ var pipelineAnalyticsCapability = {
13510
13761
  kind: "query",
13511
13762
  auth: "admin"
13512
13763
  }),
13764
+ /**
13765
+ * The CHEAP QUESTION, asked before any media moves: how big is the dataset
13766
+ * the marked (`markForTrain`) tracks would produce?
13767
+ *
13768
+ * Answered from media INDEX rows only — key, kind, size, timestamp — so it
13769
+ * costs ~2 KB of reads per track and no blob reads at all. The measured harm
13770
+ * behind D56 was a bulk pass that read and base64'd every blob a track owned
13771
+ * before deciding anything, taking hub-main to 82 s busy out of 120; an
13772
+ * export is that same I/O shape, so it inherits the same discipline: know
13773
+ * the size, then decide.
13774
+ *
13775
+ * `truncated` reports that more marked tracks exist than one pass carries.
13776
+ * Empty `deviceIds` ⇒ every device that has marked tracks.
13777
+ */
13778
+ getTrainingExportSummary: method(z.object({ deviceIds: z.array(z.number()).optional() }), TrainingExportSummarySchema, {
13779
+ kind: "query",
13780
+ auth: "admin"
13781
+ }),
13782
+ /**
13783
+ * Where to download the dataset archive.
13784
+ *
13785
+ * The BYTES do not come back through this cap — they come from the returned
13786
+ * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
13787
+ * archive base64'd through a unary RPC envelope would be held whole in
13788
+ * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
13789
+ * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
13790
+ * precedent, and this follows it deliberately.
13791
+ *
13792
+ * The archive contains a `manifest.json` FIRST, then the stored media
13793
+ * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
13794
+ * model is run: a training set's pixels must be the pixels the pipeline saw.
13795
+ */
13796
+ getTrainingExportUrl: method(z.object({ deviceIds: z.array(z.number()).optional() }), z.object({ url: z.string() }), {
13797
+ kind: "query",
13798
+ auth: "admin"
13799
+ }),
13513
13800
  getEventMedia: method(z.object({
13514
13801
  eventId: z.string(),
13515
13802
  kind: MediaFileKindEnum.optional()
@@ -16100,9 +16387,15 @@ var snapshotCapability = {
16100
16387
  * Bypass the cache freshness check and fetch directly from the
16101
16388
  * native (or stream-broker fallback). Triggered by the UI's
16102
16389
  * "refresh" button so an operator can force a fresh frame
16103
- * even when the cache is well within `snapshotMaxAgeMs`.
16104
- * On battery cams this WILL wake the camera — accept the
16105
- * cost only when the user explicitly asks for it.
16390
+ * even when the cache is well within the device's
16391
+ * `snapshotMaxAgeS` window.
16392
+ *
16393
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
16394
+ * battery camera it is the one thing that walks past the wrapper's
16395
+ * sleep gate and wakes the camera, so a background caller — a poller,
16396
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
16397
+ * gets the cached frame, which on a sleeping battery camera is the
16398
+ * correct answer: stale but honest beats woken.
16106
16399
  */
16107
16400
  force: z.boolean().optional()
16108
16401
  }), SnapshotImageSchema.nullable()),
@@ -21956,6 +22249,214 @@ setOverlay: method(z.object({
21956
22249
  }] }
21957
22250
  };
21958
22251
  /**
22252
+ * `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
22253
+ *
22254
+ * The `osd` cap is the firmware contract: it probes a camera's overlay
22255
+ * SLOTS and writes literal text into one. It has no idea WHERE that text
22256
+ * comes from, and it must not — a driver that grew a "show the temperature
22257
+ * here" feature would grow it once per vendor.
22258
+ *
22259
+ * This cap owns the other half: a per-(camera, slot) BINDING that says
22260
+ * which value feeds the slot, how it is formatted, and under which
22261
+ * conditions it is shown at all. One addon renders every binding on every
22262
+ * camera, so a new source costs zero driver code.
22263
+ *
22264
+ * Three deliberate choices, each with a rejected alternative:
22265
+ *
22266
+ * 1. A source is `(capName, valuePath)` over the kernel's device
22267
+ * runtime-state mirror — NOT a closed enum of source kinds. Every
22268
+ * cap-keyed slice a device publishes is bindable the day the cap
22269
+ * ships. The rejected alternative (one enum member per source, with
22270
+ * a resolver branch each) is what makes "add the humidity too" a
22271
+ * code change.
22272
+ * 2. The display gate reuses `NcConditionsSchema` verbatim — the
22273
+ * notification centre's condition vocabulary — rather than a parallel
22274
+ * model. An operator who has learned one condition editor has learned
22275
+ * both.
22276
+ * 3. Because the renderer's facts are device STATE and not a detection
22277
+ * record, only a SUBSET of that vocabulary can be answered here.
22278
+ * `setSlotBinding` REJECTS the rest at write time (see
22279
+ * `getConditionSupport`). It does not accept-then-fail-closed: a
22280
+ * condition that can never be true renders a permanently blank
22281
+ * overlay, and a blank overlay looks exactly like a broken camera.
22282
+ */
22283
+ /** Where a slot's value comes from. */
22284
+ var OsdSourceSchema = z.discriminatedUnion("kind", [
22285
+ z.object({
22286
+ kind: z.literal("static"),
22287
+ text: z.string().max(64)
22288
+ }),
22289
+ z.object({
22290
+ kind: z.literal("clock"),
22291
+ /** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
22292
+ pattern: z.string().min(1).max(32).default("HH:mm"),
22293
+ /** IANA zone. Omitted = the server's zone. */
22294
+ timezone: z.string().min(1).max(64).optional()
22295
+ }),
22296
+ z.object({
22297
+ kind: z.literal("device-state"),
22298
+ deviceId: z.number().int().optional(),
22299
+ capName: z.string().min(1).max(64),
22300
+ /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
22301
+ valuePath: z.string().min(1).max(64)
22302
+ })
22303
+ ]);
22304
+ var OsdSlotBindingSchema = z.object({
22305
+ /** Off = the manager stops driving this slot. It does NOT clear it. */
22306
+ enabled: z.boolean().default(true),
22307
+ source: OsdSourceSchema,
22308
+ /** `${value}` and `${unit}` are substituted; every occurrence. */
22309
+ template: z.string().max(96).default("${value}"),
22310
+ /** Truncate with an ellipsis past this length. Absent = no limit. */
22311
+ maxCharacters: z.number().int().min(4).max(64).optional(),
22312
+ /**
22313
+ * Decimal places for a numeric value. `0` yields an integer — the
22314
+ * documented workaround for firmwares that reject `.` in overlay text.
22315
+ */
22316
+ maxDecimals: z.number().int().min(0).max(4).default(1),
22317
+ /** Appended via `${unit}`. The state mirror does not carry units. */
22318
+ unitLabel: z.string().max(8).optional(),
22319
+ /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
22320
+ valueMap: z.record(z.string(), z.string()).optional(),
22321
+ /** Time windows in which the slot is shown. Absent = always. */
22322
+ schedule: NcScheduleSchema.optional(),
22323
+ /**
22324
+ * Display gate, in the notification centre's condition vocabulary.
22325
+ * Only the keys reported by `getConditionSupport` are accepted.
22326
+ */
22327
+ conditions: NcConditionsSchema.optional(),
22328
+ /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
22329
+ fallbackText: z.string().max(64).default("")
22330
+ });
22331
+ /** One camera slot, as the operator sees it: firmware truth + our binding. */
22332
+ var OsdSlotViewSchema = z.object({
22333
+ slotId: z.string(),
22334
+ kind: OsdOverlayKindEnum,
22335
+ /** Firmware refuses text edits (a timestamp, the channel name). */
22336
+ readOnly: z.boolean(),
22337
+ cameraEnabled: z.boolean(),
22338
+ cameraText: z.string().optional(),
22339
+ binding: OsdSlotBindingSchema.nullable()
22340
+ });
22341
+ /**
22342
+ * What happened to one slot on one render pass. `unchanged` exists so the
22343
+ * operator can tell "we are driving this and the value is steady" from
22344
+ * "we never got there" — and so the loop can prove it is not rewriting
22345
+ * identical text to the camera every tick.
22346
+ */
22347
+ var OsdRenderOutcomeEnum = z.enum([
22348
+ "written",
22349
+ "unchanged",
22350
+ "gated",
22351
+ "unreadable",
22352
+ "disabled",
22353
+ "unbound",
22354
+ "failed"
22355
+ ]);
22356
+ var OsdRenderResultSchema = z.object({
22357
+ slotId: z.string(),
22358
+ outcome: OsdRenderOutcomeEnum,
22359
+ /** The text the slot should carry. Empty = the slot is switched off. */
22360
+ text: z.string(),
22361
+ /** Why, whenever the outcome is not a plain write. Never silent. */
22362
+ reason: z.string().optional()
22363
+ });
22364
+ var OsdSourceValueTypeEnum = z.enum([
22365
+ "number",
22366
+ "boolean",
22367
+ "string",
22368
+ "enum"
22369
+ ]);
22370
+ /**
22371
+ * One bindable value, derived from a cap's `runtimeState` schema — never
22372
+ * hand-listed. The editor renders from this, so a cap that ships a new
22373
+ * state field becomes bindable with no UI change.
22374
+ */
22375
+ var OsdSourceOptionSchema = z.object({
22376
+ deviceId: z.number().int(),
22377
+ deviceName: z.string(),
22378
+ capName: z.string(),
22379
+ valuePath: z.string(),
22380
+ label: z.string(),
22381
+ valueType: OsdSourceValueTypeEnum,
22382
+ /** Present for `enum`; the editor offers these as `valueMap` keys. */
22383
+ enumValues: z.array(z.string()).readonly().optional()
22384
+ });
22385
+ var osdManagerCapability = {
22386
+ name: "osd-manager",
22387
+ scope: "system",
22388
+ mode: "singleton",
22389
+ methods: {
22390
+ /**
22391
+ * The camera's slots, each with its binding. `supported: false` when
22392
+ * the camera publishes no `osd` cap — the page then says so instead
22393
+ * of rendering an empty list that reads as "no overlays configured".
22394
+ */
22395
+ getDeviceOsd: method(z.object({ deviceId: z.number().int() }), z.object({
22396
+ supported: z.boolean(),
22397
+ slots: z.array(OsdSlotViewSchema)
22398
+ }), { auth: "admin" }),
22399
+ /**
22400
+ * Every value bindable on this camera: its own state leaves first,
22401
+ * then every other device's. Derived from the cap definitions, so it
22402
+ * cannot drift from what the resolver can actually read.
22403
+ */
22404
+ getSourceCatalog: method(z.object({ deviceId: z.number().int() }), z.object({ sources: z.array(OsdSourceOptionSchema) }), { auth: "admin" }),
22405
+ /**
22406
+ * The condition ids this gate can answer, alongside the FULL notification
22407
+ * condition catalog. Both, in one call, on purpose: an editor that showed
22408
+ * the catalog without the supported set would offer conditions that
22409
+ * `setSlotBinding` then rejects.
22410
+ */
22411
+ getConditionSupport: method(z.object({}), z.object({
22412
+ supported: z.array(z.string()),
22413
+ catalog: z.array(NcConditionDescriptorSchema)
22414
+ }), { auth: "admin" }),
22415
+ /**
22416
+ * Persist a binding and render it immediately, returning the slot as
22417
+ * it now stands. Throws when the binding names an unsupported
22418
+ * condition, an unknown cap, or a value path that cap does not have.
22419
+ */
22420
+ setSlotBinding: method(z.object({
22421
+ deviceId: z.number().int(),
22422
+ slotId: z.string().min(1),
22423
+ binding: OsdSlotBindingSchema
22424
+ }), z.object({
22425
+ slot: OsdSlotViewSchema,
22426
+ render: OsdRenderResultSchema
22427
+ }), {
22428
+ kind: "mutation",
22429
+ auth: "admin"
22430
+ }),
22431
+ /** Forget the binding. The slot keeps whatever text it last carried. */
22432
+ clearSlotBinding: method(z.object({
22433
+ deviceId: z.number().int(),
22434
+ slotId: z.string().min(1)
22435
+ }), z.object({ success: z.literal(true) }), {
22436
+ kind: "mutation",
22437
+ auth: "admin"
22438
+ }),
22439
+ /**
22440
+ * Render without writing. `binding` overrides the stored one so an
22441
+ * editor can preview an unsaved change. Mutation kind only to carry
22442
+ * the binding object safely; no side effects.
22443
+ */
22444
+ previewSlot: method(z.object({
22445
+ deviceId: z.number().int(),
22446
+ slotId: z.string().min(1),
22447
+ binding: OsdSlotBindingSchema.optional()
22448
+ }), OsdRenderResultSchema, {
22449
+ kind: "mutation",
22450
+ auth: "admin"
22451
+ }),
22452
+ /** Force a render pass over one camera now, instead of at the next tick. */
22453
+ renderDevice: method(z.object({ deviceId: z.number().int() }), z.object({ results: z.array(OsdRenderResultSchema) }), {
22454
+ kind: "mutation",
22455
+ auth: "admin"
22456
+ })
22457
+ }
22458
+ };
22459
+ /**
21959
22460
  * Feeder connectivity / power status — mirrors the HA petkit device-status
21960
22461
  * enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
21961
22462
  * `on_batteries` (running on battery backup). `null` until first reported.
@@ -22557,12 +23058,30 @@ var pressureSensorCapability = {
22557
23058
  runtimeState: PressureSensorStatusSchema
22558
23059
  };
22559
23060
  /**
22560
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
22561
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
22562
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
22563
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
22564
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
22565
- * normalized 0..1 (top-left origin).
23061
+ * PRIVACY what the camera deliberately does not capture. Two planes:
23062
+ *
23063
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
23064
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
23065
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
23066
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
23067
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
23068
+ * normalized 0..1 (top-left origin).
23069
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
23070
+ * camera encoding an audio track at all, so EVERY consumer — live view,
23071
+ * recording, the audio analyzer, an export — sees silent video. There is
23072
+ * no server-side copy of this fact; the camera is the store and every read
23073
+ * is a read-through, which is why a switch over it cannot drift
23074
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
23075
+ *
23076
+ * Both belong here for one reason: they are the two things an operator turns
23077
+ * off when the answer to "what is this camera allowed to record" changes, and
23078
+ * both are applied ON the device, before anything leaves it.
23079
+ *
23080
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
23081
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
23082
+ * by one provider — and it was removed when this landed. A second writer onto
23083
+ * one device register is the shape of every knob this repo has shipped that
23084
+ * disagreed with the one the reader read.
22566
23085
  */
22567
23086
  /** A privacy-mask region's geometry — rectangle or free polygon. */
22568
23087
  var PrivacyMaskShapeSchema = z.discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -22574,21 +23093,45 @@ var PrivacyMaskRegionSchema = z.object({
22574
23093
  enabled: z.boolean(),
22575
23094
  shape: PrivacyMaskShapeSchema
22576
23095
  });
22577
- /** Current on-camera privacy-mask state — master enable + zones. */
23096
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
22578
23097
  var PrivacyMaskStatusSchema = z.object({
22579
23098
  enabled: z.boolean(),
22580
23099
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
22581
23100
  regions: z.array(PrivacyMaskRegionSchema),
23101
+ /**
23102
+ * Is the camera capturing sound right now? Read from the camera, never from
23103
+ * a server-side mirror.
23104
+ *
23105
+ * `null` means "no answer" — either this camera exposes no controllable
23106
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
23107
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
23108
+ * "the microphone is off" and "we could not ask" look identical to an
23109
+ * operator only until one of them is wrong.
23110
+ *
23111
+ * On a camera whose profiles carry the flag independently (Reolink writes
23112
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
23113
+ * privacy is only satisfied when every one of them is silent.
23114
+ */
23115
+ audioEnabled: z.boolean().nullable(),
22582
23116
  lastFetchedAt: z.number()
22583
23117
  });
22584
- /** Per-camera availability. */
23118
+ /** Per-camera availability. Probed, never assumed from the model name. */
22585
23119
  var PrivacyMaskOptionsSchema = z.object({
22586
23120
  /** Maximum number of supported zones. */
22587
23121
  maxRegions: z.number(),
22588
23122
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
22589
23123
  supportedShapes: z.array(MaskShapeKindSchema),
22590
23124
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
22591
- polygonVertices: MaskPolygonVerticesSchema.optional()
23125
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
23126
+ /**
23127
+ * Does this camera expose a microphone switch we can actually write?
23128
+ *
23129
+ * Camera-probed: `true` only when the firmware answered with an audio flag
23130
+ * we know how to patch. A camera that never answered is `false` — a control
23131
+ * the operator can press that changes nothing is worse than no control, and
23132
+ * the switch group renders "not available" instead.
23133
+ */
23134
+ supportsAudioMute: z.boolean()
22592
23135
  });
22593
23136
  /** Partial change — every field optional. */
22594
23137
  var PrivacyMaskPatchSchema = z.object({
@@ -22615,6 +23158,27 @@ var privacyMaskCapability = {
22615
23158
  }), z.void(), {
22616
23159
  kind: "mutation",
22617
23160
  auth: "admin"
23161
+ }),
23162
+ /**
23163
+ * Turn the camera's microphone on or off, at the camera.
23164
+ *
23165
+ * Deliberately its OWN mutation rather than a field on
23166
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
23167
+ * mask master switch", and overloading it would make one boolean mean two
23168
+ * unrelated things on the same call. It is also the only method here whose
23169
+ * write leaves the device in a state a later `getStatus` reads back
23170
+ * verbatim, which is what makes it safe as a switch authority.
23171
+ *
23172
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
23173
+ * this rather than silently accept it — a write nothing applies is exactly
23174
+ * what the switch group exists to remove.
23175
+ */
23176
+ setAudioEnabled: method(z.object({
23177
+ deviceId: z.number(),
23178
+ enabled: z.boolean()
23179
+ }), z.void(), {
23180
+ kind: "mutation",
23181
+ auth: "admin"
22618
23182
  })
22619
23183
  },
22620
23184
  status: {
@@ -22999,6 +23563,21 @@ var LocateSegmentResultSchema = z.discriminatedUnion("kind", [z.object({
22999
23563
  })]);
23000
23564
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
23001
23565
  var ReadSegmentBytesResultSchema = z.object({ data: z.instanceof(Uint8Array) });
23566
+ /**
23567
+ * One GOP of a finalized segment, cut by byte range through the segment's own
23568
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
23569
+ * the single `moof`+`mdat` covering the requested instant — standalone-
23570
+ * demuxable, never the whole file. When the segment's index cannot be parsed
23571
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
23572
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
23573
+ */
23574
+ var ReadGopBytesResultSchema = z.object({
23575
+ data: z.instanceof(Uint8Array),
23576
+ /** Absolute epoch ms of the returned fragment's first sample. */
23577
+ gopStartMs: z.number(),
23578
+ /** Media ms the returned fragment covers. */
23579
+ gopDurMs: z.number()
23580
+ });
23002
23581
  var recordingCapability = {
23003
23582
  name: "recording",
23004
23583
  scope: "system",
@@ -23066,6 +23645,18 @@ var recordingCapability = {
23066
23645
  kind: "query",
23067
23646
  auth: "admin"
23068
23647
  }),
23648
+ /** Read the single GOP of segment `startMs` covering `epochMs`, by mfra
23649
+ * byte range — the scrub-granular read (D31 letter on the feeder path).
23650
+ * See {@link ReadGopBytesResultSchema} for the degradation contract. */
23651
+ readGopBytes: method(z.object({
23652
+ deviceId: z.number(),
23653
+ profile: z.string(),
23654
+ startMs: z.number(),
23655
+ epochMs: z.number()
23656
+ }), ReadGopBytesResultSchema, {
23657
+ kind: "query",
23658
+ auth: "admin"
23659
+ }),
23069
23660
  setDeviceConfig: method(z.object({
23070
23661
  deviceId: z.number(),
23071
23662
  config: RecordingConfigSchema
@@ -23709,6 +24300,16 @@ var StreamProfileConfigSchema = z.object({
23709
24300
  "baseline"
23710
24301
  ]).optional(),
23711
24302
  gop: z.number().optional(),
24303
+ /**
24304
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
24305
+ *
24306
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
24307
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
24308
+ * writes every profile at once so "audio off" means silent everywhere. A
24309
+ * per-profile writer beside it would let a camera be half-muted and would be
24310
+ * a second knob onto one device register — the failure D62 exists to
24311
+ * prevent. Absent when the firmware does not report the flag.
24312
+ */
23712
24313
  audio: z.boolean().optional()
23713
24314
  });
23714
24315
  var StreamParamsStatusSchema = z.object({
@@ -23749,7 +24350,13 @@ var StreamParamsOptionsSchema = z.object({
23749
24350
  ext: StreamProfileOptionsSchema.optional()
23750
24351
  });
23751
24352
  /** A partial change to one profile — every field optional; a provider
23752
- * ignores fields it doesn't support. */
24353
+ * ignores fields it doesn't support.
24354
+ *
24355
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
24356
+ * reachable from no form and honoured by exactly one provider, while the
24357
+ * camera's microphone is a whole-device fact. It now has one writer,
24358
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
24359
+ * `privacy-mask.cap.ts`. */
23753
24360
  var StreamProfilePatchSchema = z.object({
23754
24361
  width: z.number().optional(),
23755
24362
  height: z.number().optional(),
@@ -23762,8 +24369,7 @@ var StreamProfilePatchSchema = z.object({
23762
24369
  "main",
23763
24370
  "baseline"
23764
24371
  ]).optional(),
23765
- gop: z.number().optional(),
23766
- audio: z.boolean().optional()
24372
+ gop: z.number().optional()
23767
24373
  });
23768
24374
  var streamParamsCapability = {
23769
24375
  name: "stream-params",
@@ -25534,6 +26140,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
25534
26140
  numericSensorCapability,
25535
26141
  oauthIntegrationCapability,
25536
26142
  osdCapability,
26143
+ osdManagerCapability,
25537
26144
  petFeederCapability,
25538
26145
  pipelineAnalyticsCapability,
25539
26146
  pipelineExecutorCapability,
@@ -28450,6 +29057,48 @@ var METHOD_ACCESS_MAP = Object.freeze({
28450
29057
  addonId: null,
28451
29058
  access: "create"
28452
29059
  },
29060
+ "osdManager.clearSlotBinding": {
29061
+ capName: "osd-manager",
29062
+ capScope: "system",
29063
+ addonId: null,
29064
+ access: "delete"
29065
+ },
29066
+ "osdManager.getConditionSupport": {
29067
+ capName: "osd-manager",
29068
+ capScope: "system",
29069
+ addonId: null,
29070
+ access: "view"
29071
+ },
29072
+ "osdManager.getDeviceOsd": {
29073
+ capName: "osd-manager",
29074
+ capScope: "system",
29075
+ addonId: null,
29076
+ access: "view"
29077
+ },
29078
+ "osdManager.getSourceCatalog": {
29079
+ capName: "osd-manager",
29080
+ capScope: "system",
29081
+ addonId: null,
29082
+ access: "view"
29083
+ },
29084
+ "osdManager.previewSlot": {
29085
+ capName: "osd-manager",
29086
+ capScope: "system",
29087
+ addonId: null,
29088
+ access: "create"
29089
+ },
29090
+ "osdManager.renderDevice": {
29091
+ capName: "osd-manager",
29092
+ capScope: "system",
29093
+ addonId: null,
29094
+ access: "create"
29095
+ },
29096
+ "osdManager.setSlotBinding": {
29097
+ capName: "osd-manager",
29098
+ capScope: "system",
29099
+ addonId: null,
29100
+ access: "create"
29101
+ },
28453
29102
  "petFeeder.callPet": {
28454
29103
  capName: "pet-feeder",
28455
29104
  capScope: "device",
@@ -28612,6 +29261,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
28612
29261
  addonId: null,
28613
29262
  access: "view"
28614
29263
  },
29264
+ "pipelineAnalytics.getTrainingExportSummary": {
29265
+ capName: "pipeline-analytics",
29266
+ capScope: "device",
29267
+ addonId: null,
29268
+ access: "view"
29269
+ },
29270
+ "pipelineAnalytics.getTrainingExportUrl": {
29271
+ capName: "pipeline-analytics",
29272
+ capScope: "device",
29273
+ addonId: null,
29274
+ access: "view"
29275
+ },
28615
29276
  "pipelineAnalytics.listEventKinds": {
28616
29277
  capName: "pipeline-analytics",
28617
29278
  capScope: "device",
@@ -29368,6 +30029,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
29368
30029
  addonId: null,
29369
30030
  access: "view"
29370
30031
  },
30032
+ "privacyMask.setAudioEnabled": {
30033
+ capName: "privacy-mask",
30034
+ capScope: "device",
30035
+ addonId: null,
30036
+ access: "create"
30037
+ },
29371
30038
  "privacyMask.setMask": {
29372
30039
  capName: "privacy-mask",
29373
30040
  capScope: "device",
@@ -29536,6 +30203,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
29536
30203
  addonId: null,
29537
30204
  access: "create"
29538
30205
  },
30206
+ "recording.readGopBytes": {
30207
+ capName: "recording",
30208
+ capScope: "system",
30209
+ addonId: null,
30210
+ access: "view"
30211
+ },
29539
30212
  "recording.readSegmentBytes": {
29540
30213
  capName: "recording",
29541
30214
  capScope: "system",
@@ -30076,6 +30749,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30076
30749
  addonId: null,
30077
30750
  access: "view"
30078
30751
  },
30752
+ "streamBroker.getDeviceAudioMute": {
30753
+ capName: "stream-broker",
30754
+ capScope: "system",
30755
+ addonId: null,
30756
+ access: "view"
30757
+ },
30079
30758
  "streamBroker.getPreBufferInfo": {
30080
30759
  capName: "stream-broker",
30081
30760
  capScope: "system",
@@ -30196,6 +30875,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
30196
30875
  addonId: null,
30197
30876
  access: "create"
30198
30877
  },
30878
+ "streamBroker.setDeviceAudioMute": {
30879
+ capName: "stream-broker",
30880
+ capScope: "system",
30881
+ addonId: null,
30882
+ access: "create"
30883
+ },
30199
30884
  "streamBroker.setPreBufferDuration": {
30200
30885
  capName: "stream-broker",
30201
30886
  capScope: "system",
@@ -30933,6 +31618,42 @@ TimelapseRuleInputSchema.extend({
30933
31618
  updatedAt: z.number()
30934
31619
  });
30935
31620
  /**
31621
+ * The addon id as the REGISTRY, the wire and the durable stores spell it.
31622
+ *
31623
+ * `AddonContext.id` does not agree with itself across the two context
31624
+ * factories:
31625
+ *
31626
+ * - a FORKED addon gets the bare manifest id
31627
+ * (`kernel/moleculer/addon-context-factory.ts` → `id: addonId`);
31628
+ * - an addon co-located in hub-main gets it PREFIXED
31629
+ * (`server/backend/src/core/addon/addon-registry.service.ts` →
31630
+ * ``id: `addon:${addonId}` ``).
31631
+ *
31632
+ * Everything an addon might compare `ctx.id` AGAINST carries the bare form:
31633
+ * `DeviceBindingEntry.providerAddonId`, the `device-manager` bindings store's
31634
+ * `wrapperAddonId`, `CapabilityRegistry` provider keys, manifest ids.
31635
+ *
31636
+ * So `entry.providerAddonId === this.ctx.id` is silently, permanently false
31637
+ * for a builtin — and only for a builtin, which is why it survives a green
31638
+ * suite whose fake supplies the bare id. That is exactly how camera 615's
31639
+ * virtual doorbell latched `unbound` for twelve hours on 2026-08-07 while its
31640
+ * binding was intact ([D72](../../../../docs/decisions/adr-0072.md)).
31641
+ *
31642
+ * Route every comparison between `ctx.id` and a registry/store addon id
31643
+ * through {@link isSameAddonId}. `scripts/check-addon-id-comparison.ts`
31644
+ * enforces it in the processes where the prefix exists.
31645
+ */
31646
+ /** The prefix the hub-main context factory prepends to the manifest id. */
31647
+ var ADDON_ID_PREFIX = "addon:";
31648
+ /** The manifest id, whichever spelling of `ctx.id` you were handed. */
31649
+ function bareAddonId(id) {
31650
+ return id.startsWith(ADDON_ID_PREFIX) ? id.slice(6) : id;
31651
+ }
31652
+ /** True when both ids name the same addon, prefixed or not. */
31653
+ function isSameAddonId(a, b) {
31654
+ return bareAddonId(a) === bareAddonId(b);
31655
+ }
31656
+ /**
30936
31657
  * Decode base64 little-endian Float32 back into a vector.
30937
31658
  *
30938
31659
  * Throws on a byte length that is not a multiple of 4 — a truncated vector
@@ -30975,6 +31696,88 @@ z.object({
30975
31696
  square: false
30976
31697
  }).paddingRatio;
30977
31698
  /**
31699
+ * WHICH delivered frames the decode worker retains a native copy of.
31700
+ *
31701
+ * - `all` — every frame the worker delivered to the runner. The shipped
31702
+ * behaviour, and the only correct one if something can ask for a crop of a
31703
+ * frame the runner never sent to inference.
31704
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
31705
+ * A native-crop request always names a `frameId` that rode an inference
31706
+ * result, so that is the only set a request can name. How much it drops is
31707
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
31708
+ * this cluster, not the ~80% the design sketch assumed, because the governor
31709
+ * was not throttling as hard as the sketch supposed. Read
31710
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
31711
+ * of you rather than quoting a number from here. The newest delivered frame is
31712
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
31713
+ * which covers the one-frame race between a mark and the supersede that
31714
+ * consumes it.
31715
+ */
31716
+ var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
31717
+ z.object({
31718
+ /**
31719
+ * How long a retained native frame is served before it counts as a miss.
31720
+ *
31721
+ * Must cover the FULL late-crop horizon: detection inference + the
31722
+ * cross-process inference-result hop to hub post-analysis + tracking + the
31723
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
31724
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
31725
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
31726
+ */
31727
+ ttlMs: z.number().int().min(250).max(1e4),
31728
+ /**
31729
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
31730
+ *
31731
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
31732
+ * which one is actually binding before reasoning from that. At the shipped
31733
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
31734
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
31735
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
31736
+ * change that admits fewer frames buys retention WINDOW at constant RAM
31737
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
31738
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
31739
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
31740
+ * to replace).
31741
+ */
31742
+ budgetMb: z.number().int().min(0).max(4096),
31743
+ /**
31744
+ * Demand window: eager per-frame native retention runs only within this many
31745
+ * ms of the last native-crop request (or of the dial starting).
31746
+ *
31747
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
31748
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
31749
+ * per second on a camera with zero crop demand), so leave it non-zero unless
31750
+ * you are reproducing that.
31751
+ */
31752
+ activityMs: z.number().int().min(0).max(12e4),
31753
+ /**
31754
+ * Which delivered frames are retained at all — see
31755
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
31756
+ * changes WHAT is kept rather than for how long, so it is also the only one
31757
+ * that can turn a crop that used to hit into a miss. The worker counts every
31758
+ * crop request naming a frame it did NOT see marked
31759
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
31760
+ * there is the signal that some caller names frames outside the inference set
31761
+ * and that this must go back to `all`.
31762
+ */
31763
+ admission: NativeLeaseAdmissionSchema
31764
+ });
31765
+ /**
31766
+ * The values in force when the operator has set nothing — byte-for-byte the
31767
+ * constants the decode worker shipped with as env-var defaults, so making these
31768
+ * settings changed no behaviour on the day it landed.
31769
+ */
31770
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
31771
+ ttlMs: 1200,
31772
+ budgetMb: 1024,
31773
+ activityMs: 15e3,
31774
+ admission: "inferred"
31775
+ };
31776
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
31777
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
31778
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
31779
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
31780
+ /**
30978
31781
  * Scores all applicable inference backends for the given hardware.
30979
31782
  *
30980
31783
  * Rules (hardware-driven only — no Python import probes):
@@ -31088,4 +31891,4 @@ function enumerateInferenceDevices(hw) {
31088
31891
  return out;
31089
31892
  }
31090
31893
  //#endregion
31091
- export { setByPath as $, enumerateItemArrayFields as A, nodePin as At, kebabToCamel as B, dataStoreProviderCapability as C, asString as Ct, deviceStatusCapability as D, expandCapMethods as Dt, deviceStateCapability as E, emitReadiness as Et, getByPath as F, scopeKey as Ft, looseSchema as G, localNetworkCapability as H, isArrayOutputSchema as I, sleep as It, objectInputDeclaresAddonId as J, metricsProviderCapability as K, isCollectionArrayMethod as L, EventCategory as Lt, evaluateLinkExpression as M, parseJsonUnknown as Mt, extractNestedAddonId as N, readinessKey as Nt, doorbellCapability as O, hydrateSchema as Ot, filesystemBrowseCapability as P, resolveCapMount as Pt, scoreRuntimes as Q, isObjectInput as R, coreBlocksCapability as S, asNumber as St, deviceManagerCapability as T, emitDownForOwnedCaps as Tt, logDestinationCapability as U, lifecycleJobSchema as V, logLevelAtMost as W, platformProbeCapability as X, parseStreamParamsFormPatch as Y, procedureAuthKey as Z, alertsCapability as _, DeviceType as _t, CAP_NAMES_WITH_STATUS as a, toExpressionValue as at, backupCapability as b, WELL_KNOWN_TAB_MAP as bt, METHOD_ACCESS_MAP as c, vectorDimFromBase64 as ct, ScopedTokenSchema as d, BaseAddon as dt, settingsStoreCapability as et, StorageLocationTypeSchema as f, DATAPLANE_SECRET_HEADER as ft, addonWidgetsCapability as g, DeviceRole as gt, addonSettingsCapability as h, DeviceFeature as ht, BatteryStatusSchema as i, streamQualityLabel as it, enumerateSchemaFields as j, parseJsonObject as jt, enumerateInferenceDevices as k, isDeviceConfigCap as kt, RUNTIME_DEFAULTS as l, vectorStoreCapability as lt, addonPagesCapability as m, DEVICE_STATUS_METHOD as mt, AlertSchema as n, storageCapability as nt, CoreBlockSchema as o, userManagementCapability as ot, UserRecordSchema as p, DEVICE_SETTINGS_CONTRIBUTION_METHODS as pt, normalizeUnit as q, ApiKeyRecordSchema as r, storageProviderCapability as rt, DeviceStatusSchema as s, validateExpressionSource as st, ALL_CAPABILITY_DEFINITIONS as t, snapshotCapability as tt, STREAM_PROFILE_META as u, errMsg as ut, applyTransform as v, ReadinessRegistry as vt, decodeVectorBase64 as w, createEvent as wt, buildStreamParamsConfigSchema as x, asJsonObject as xt, authProviderCapability as y, ReadinessTimeoutError as yt, isVoidInput as z };
31894
+ export { procedureAuthKey as $, enumerateInferenceDevices as A, hydrateSchema as At, isSameAddonId as B, coreBlocksCapability as C, asJsonObject as Ct, deviceStateCapability as D, emitDownForOwnedCaps as Dt, deviceManagerCapability as E, createEvent as Et, filesystemBrowseCapability as F, readinessKey as Ft, logDestinationCapability as G, kebabToCamel as H, getByPath as I, resolveCapMount as It, metricsProviderCapability as J, logLevelAtMost as K, isArrayOutputSchema as L, scopeKey as Lt, enumerateSchemaFields as M, nodePin as Mt, evaluateLinkExpression as N, parseJsonObject as Nt, deviceStatusCapability as O, emitReadiness as Ot, extractNestedAddonId as P, parseJsonUnknown as Pt, platformProbeCapability as Q, isCollectionArrayMethod as R, sleep as Rt, buildStreamParamsConfigSchema as S, WELL_KNOWN_TAB_MAP as St, decodeVectorBase64 as T, asString as Tt, lifecycleJobSchema as U, isVoidInput as V, localNetworkCapability as W, objectInputDeclaresAddonId as X, normalizeUnit as Y, parseStreamParamsFormPatch as Z, alertsCapability as _, DeviceFeature as _t, CAP_NAMES_WITH_STATUS as a, storageProviderCapability as at, backupCapability as b, ReadinessRegistry as bt, METHOD_ACCESS_MAP as c, userManagementCapability as ct, ScopedTokenSchema as d, vectorStoreCapability as dt, scoreRuntimes as et, StorageLocationTypeSchema as f, errMsg as ft, addonWidgetsCapability as g, DEVICE_STATUS_METHOD as gt, addonSettingsCapability as h, DEVICE_SETTINGS_CONTRIBUTION_METHODS as ht, BatteryStatusSchema as i, storageCapability as it, enumerateItemArrayFields as j, isDeviceConfigCap as jt, doorbellCapability as k, expandCapMethods as kt, RUNTIME_DEFAULTS as l, validateExpressionSource as lt, addonPagesCapability as m, DATAPLANE_SECRET_HEADER as mt, AlertSchema as n, settingsStoreCapability as nt, CoreBlockSchema as o, streamQualityLabel as ot, UserRecordSchema as p, BaseAddon as pt, looseSchema as q, ApiKeyRecordSchema as r, snapshotCapability as rt, DeviceStatusSchema as s, toExpressionValue as st, ALL_CAPABILITY_DEFINITIONS as t, setByPath as tt, STREAM_PROFILE_META as u, vectorDimFromBase64 as ut, applyTransform as v, DeviceRole as vt, dataStoreProviderCapability as w, asNumber as wt, bareAddonId as x, ReadinessTimeoutError as xt, authProviderCapability as y, DeviceType as yt, isObjectInput as z, EventCategory as zt };