@camstack/addon-post-analysis 1.2.48 → 1.2.49

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.
@@ -6531,9 +6531,38 @@ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
6531
6531
  /**
6532
6532
  * Build the tRPC request options that pin a single capability call to `nodeId`.
6533
6533
  * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
6534
+ *
6535
+ * ## The id is normalised here, and it has to be
6536
+ *
6537
+ * A forked addon reads its own node from `ctx.kernel.localNodeId`, and inside a
6538
+ * worker that value is a RUNNER id — `hub/export-hap`, not `hub`. Routing
6539
+ * compares a pin against real node ids, so such a pin matches nothing and the
6540
+ * call fails with `no provider registered for cap "…"`. The local-first
6541
+ * resolver already guarded against this (`localNodeId.split('/')[0]`), which
6542
+ * made the hazard invisible: unpinned calls worked, and only an explicit pin —
6543
+ * the thing you reach for when you specifically need THIS node — silently
6544
+ * addressed a node that does not exist.
6545
+ *
6546
+ * Cost of it being missing: `addon-export-hap` pinned `decoder.getInfo` to its
6547
+ * own node to read the host's hardware-decode backend. It never once answered,
6548
+ * so every HomeKit egress transcode decoded in SOFTWARE — including 4K H.265 —
6549
+ * while D67's whole premise was that the decoder addon is the authority on
6550
+ * hardware. The warn said `decoding in SOFTWARE` and read as "this node has no
6551
+ * hardware", which was false.
6552
+ *
6553
+ * Normalising in the ONE constructor fixes every caller at once, which is why
6554
+ * it is here and not at the call sites.
6534
6555
  */
6535
6556
  function nodePin(nodeId) {
6536
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
6557
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: toNodeId(nodeId) } };
6558
+ }
6559
+ /**
6560
+ * A runner id is `<nodeId>/<addonId>`; a node id has no slash. Taking the head
6561
+ * is idempotent, so passing an already-clean id costs nothing.
6562
+ */
6563
+ function toNodeId(idOrRunnerId) {
6564
+ const head = idOrRunnerId.split("/")[0];
6565
+ return head === void 0 || head.length === 0 ? idOrRunnerId : head;
6537
6566
  }
6538
6567
  /**
6539
6568
  * Output schema shared by the contribution + live methods.
@@ -7271,7 +7300,7 @@ object({
7271
7300
  * ## This file adds no state
7272
7301
  *
7273
7302
  * Every switch here is a VIEW onto an authority that already existed
7274
- * ([D61](../../../../docs/decisions/adr-0062.md)). The whole point of the
7303
+ * ([D62](../../../../docs/decisions/adr-0062.md)). The whole point of the
7275
7304
  * group is that there is exactly one place each function is turned off, and
7276
7305
  * the group routes to it:
7277
7306
  *
@@ -7282,6 +7311,40 @@ object({
7282
7311
  * | `audio-analysis` | `deviceManager.setWrapperActive('audio-analysis')` | `AudioSubscriptionController.subscribeAudioStream` returns `null` before opening the stream |
7283
7312
  * | `recording` | `recording.setDeviceConfig` → `RecordingConfig.enabled` | `band-decision.shouldRecord` returns false; the controller detaches the device |
7284
7313
  * | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
7314
+ * | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
7315
+ * | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
7316
+ *
7317
+ * ## The two switches whose authority is not on this server
7318
+ *
7319
+ * `privacy-mask` and `device-audio` write the CAMERA. That is not a loophole
7320
+ * in "the group stores nothing" — it is the purest form of it: the camera
7321
+ * holds the fact, every read is a read-through, and there is no server-side
7322
+ * copy that could drift. Their availability therefore cannot come from
7323
+ * `listBindableCapsForDeviceType` (a device-NATIVE cap carries no wrappers and
7324
+ * is filtered out there); it comes from the cap's own camera-probed
7325
+ * `privacyMask.getOptions()`, which is strictly more honest — it answers for
7326
+ * THIS camera rather than for the device type
7327
+ * ([D74](../../../../docs/decisions/adr-0074.md)).
7328
+ *
7329
+ * ## `privacy-mask` is the one row whose ON is not "the function is working"
7330
+ *
7331
+ * Every other switch means *this camera's function is doing its job*, so
7332
+ * `enabled: false` is a thing an operator took away. `privacy-mask` means **the
7333
+ * MASK is active** — `enabled: true` is video deliberately obscured. The
7334
+ * polarity is not a choice made here: `addon-export-hap`'s privacy `Switch`
7335
+ * (`builders/privacy-switch.ts`) already mirrors `patch.enabled` verbatim, and
7336
+ * a HomeKit toggle that disagreed with the app's toggle for the same camera is
7337
+ * worse than either surface not having one.
7338
+ *
7339
+ * Two consequences follow and both are load-bearing:
7340
+ *
7341
+ * - **It never counts as `switchedOff`.** `countsAsSwitchedOff` is `false` for
7342
+ * exactly this row. With the polarity above, every camera that has NOT drawn
7343
+ * a privacy mask would otherwise report `switchedOff: ['privacy-mask']` — the
7344
+ * normal, healthy state of most cameras rendered as an operator disablement.
7345
+ * - **Its cost line names BOTH directions.** `costWhenOff` is rendered
7346
+ * unconditionally by both clients, so for this row it has to read correctly
7347
+ * whichever way the switch is sitting.
7285
7348
  *
7286
7349
  * The wrapper-binding pair is not a new idea: `legacy-migrations.ts` already
7287
7350
  * migrated the legacy `audioEnabled` / `pipelineEnabled` /
@@ -7300,14 +7363,17 @@ object({
7300
7363
  * `CameraStatus.switchedOff`.
7301
7364
  */
7302
7365
  /**
7303
- * The five functions the operator named (2026-08-05). Deliberately NOT one id
7304
- * per pipeline step: face recognition and plate/LPR are per-step toggles on
7366
+ * The functions the operator named — five on 2026-08-05, plus the camera's own
7367
+ * microphone on 2026-08-07. Deliberately NOT one id per pipeline step: face
7368
+ * recognition and plate/LPR are per-step toggles on
7305
7369
  * `pipelineOrchestrator.setCameraStepToggle` and belong in the pipeline
7306
- * editor, not in a five-button safety group.
7370
+ * editor, not in a safety group.
7307
7371
  */
7308
7372
  var CameraSwitchIdSchema = _enum([
7309
7373
  "stream-broker",
7310
7374
  "object-detection",
7375
+ "privacy-mask",
7376
+ "device-audio",
7311
7377
  "audio-analysis",
7312
7378
  "recording",
7313
7379
  "notifications"
@@ -7325,14 +7391,26 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
7325
7391
  capName: string()
7326
7392
  }),
7327
7393
  object({ kind: literal("recording-config") }),
7328
- object({ kind: literal("notification-mute") })
7394
+ object({ kind: literal("notification-mute") }),
7395
+ object({
7396
+ kind: literal("camera-audio"),
7397
+ capName: string()
7398
+ }),
7399
+ object({
7400
+ kind: literal("camera-mask"),
7401
+ capName: string()
7402
+ })
7329
7403
  ]);
7330
7404
  /**
7331
7405
  * Why a switch is not offered for this camera. Rendered instead of the
7332
7406
  * control, never as a dead control — an absent function and a broken one must
7333
7407
  * not look the same.
7334
7408
  */
7335
- var CameraSwitchUnavailableReasonSchema = _enum(["no-provider", "source-unreachable"]);
7409
+ var CameraSwitchUnavailableReasonSchema = _enum([
7410
+ "no-provider",
7411
+ "source-unreachable",
7412
+ "not-configured"
7413
+ ]);
7336
7414
  /**
7337
7415
  * One switch, resolved for one camera.
7338
7416
  *
@@ -9325,6 +9403,26 @@ var EgressTranscodeRequestSchema = object({
9325
9403
  "h264_mp4toannexb",
9326
9404
  "hevc_mp4toannexb"
9327
9405
  ]).optional(),
9406
+ /**
9407
+ * Publish the transcode as a LOCAL push cam stream, instead of leaving the
9408
+ * consumer to dial the returned url. The broker picks the id and returns it
9409
+ * as `camStreamId` — a caller-supplied one would be circular, since the
9410
+ * sharing key is computed FROM this request.
9411
+ *
9412
+ * The url is still returned and still the contract for a transcode pinned to
9413
+ * another node. But dialling it locally costs an RTSP round trip that changes
9414
+ * the transport underneath the consumer: a dialled stream is an RTP source,
9415
+ * so `isRtpSource()` is true and the session takes the RTP-passthrough +
9416
+ * repacketizer branch. The push branch — the one the derived mechanism has
9417
+ * live hours on — is never reached. Measured on Alexa: broker registered, RTP
9418
+ * arriving, key frame arriving, black screen, on a chain healthy at every
9419
+ * other point.
9420
+ *
9421
+ * Same idea the transport already applies to CALLS, where `classifyCapRoute`
9422
+ * gives priority to `hub-in-process` so a local call never leaves the node.
9423
+ * This is that rule for media.
9424
+ */
9425
+ publishLocally: boolean().optional(),
9328
9426
  pixelFormat: _enum(["yuv420p", "nv12"]).optional(),
9329
9427
  /**
9330
9428
  * Operator/consumer override for decode hardware. ABSENT is the normal case
@@ -9369,7 +9467,13 @@ var EgressTranscodeSchema = object({
9369
9467
  * Returned rather than assumed: a consumer that asked for hardware and got
9370
9468
  * software needs to be able to see that without reading the broker's logs.
9371
9469
  */
9372
- decodeHwAccel: string().nullable()
9470
+ decodeHwAccel: string().nullable(),
9471
+ /**
9472
+ * Set when `publishLocally` was honoured: attach to THIS instead of dialling
9473
+ * `url`, and the session takes the push/deframe transport rather than the
9474
+ * RTP-passthrough one. `null` means the consumer must dial.
9475
+ */
9476
+ camStreamId: string().nullable()
9373
9477
  });
9374
9478
  method(object({
9375
9479
  deviceId: number().int().nonnegative(),
@@ -18128,9 +18232,15 @@ DeviceType.Camera, method(object({
18128
18232
  * Bypass the cache freshness check and fetch directly from the
18129
18233
  * native (or stream-broker fallback). Triggered by the UI's
18130
18234
  * "refresh" button so an operator can force a fresh frame
18131
- * even when the cache is well within `snapshotMaxAgeMs`.
18132
- * On battery cams this WILL wake the camera — accept the
18133
- * cost only when the user explicitly asks for it.
18235
+ * even when the cache is well within the device's
18236
+ * `snapshotMaxAgeS` window.
18237
+ *
18238
+ * **`force` is an OPERATOR signal, not a freshness preference.** On a
18239
+ * battery camera it is the one thing that walks past the wrapper's
18240
+ * sleep gate and wakes the camera, so a background caller — a poller,
18241
+ * an event handler, a thumbnail — must NEVER set it. Every such caller
18242
+ * gets the cached frame, which on a sleeping battery camera is the
18243
+ * correct answer: stale but honest beats woken.
18134
18244
  */
18135
18245
  force: boolean().optional()
18136
18246
  }), SnapshotImageSchema.nullable()), method(object({ deviceId: number() }), _void(), {
@@ -23612,12 +23722,30 @@ var pressureSensorCapability = {
23612
23722
  runtimeState: PressureSensorStatusSchema
23613
23723
  };
23614
23724
  /**
23615
- * Privacy mask = up to `maxRegions` SHAPES the camera blanks out (NOT a
23616
- * cell grid). Reolink `<shelterList>` zones are rectangles; Hikvision
23617
- * ISAPI `<RegionCoordinatesList>` zones are free polygons (this camera:
23618
- * exactly 4 vertices, not necessarily axis-aligned). The cap composes the
23619
- * shared rect|polygon subset of the MaskShape vocabulary. All coords are
23620
- * normalized 0..1 (top-left origin).
23725
+ * PRIVACY — what the camera deliberately does not capture. Two planes:
23726
+ *
23727
+ * - **video**: up to `maxRegions` SHAPES the camera blanks out (NOT a cell
23728
+ * grid). Reolink `<shelterList>` zones are rectangles; Hikvision ISAPI
23729
+ * `<RegionCoordinatesList>` zones are free polygons (this camera: exactly
23730
+ * 4 vertices, not necessarily axis-aligned). The cap composes the shared
23731
+ * rect|polygon subset of the MaskShape vocabulary. All coords are
23732
+ * normalized 0..1 (top-left origin).
23733
+ * - **audio**: the camera's microphone. `setAudioEnabled(false)` stops the
23734
+ * camera encoding an audio track at all, so EVERY consumer — live view,
23735
+ * recording, the audio analyzer, an export — sees silent video. There is
23736
+ * no server-side copy of this fact; the camera is the store and every read
23737
+ * is a read-through, which is why a switch over it cannot drift
23738
+ * ([D62](../../../../docs/decisions/adr-0062.md)).
23739
+ *
23740
+ * Both belong here for one reason: they are the two things an operator turns
23741
+ * off when the answer to "what is this camera allowed to record" changes, and
23742
+ * both are applied ON the device, before anything leaves it.
23743
+ *
23744
+ * **The audio flag has exactly one writer.** `stream-params` used to carry a
23745
+ * per-profile `audio` in its patch schema — reachable from no UI and honoured
23746
+ * by one provider — and it was removed when this landed. A second writer onto
23747
+ * one device register is the shape of every knob this repo has shipped that
23748
+ * disagreed with the one the reader read.
23621
23749
  */
23622
23750
  /** A privacy-mask region's geometry — rectangle or free polygon. */
23623
23751
  var PrivacyMaskShapeSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
@@ -23629,21 +23757,45 @@ var PrivacyMaskRegionSchema = object({
23629
23757
  enabled: boolean(),
23630
23758
  shape: PrivacyMaskShapeSchema
23631
23759
  });
23632
- /** Current on-camera privacy-mask state — master enable + zones. */
23760
+ /** Current on-camera privacy state — mask master enable + zones + microphone. */
23633
23761
  var PrivacyMaskStatusSchema = object({
23634
23762
  enabled: boolean(),
23635
23763
  /** Active zones (normalized 0..1). Length ≤ maxRegions. */
23636
23764
  regions: array(PrivacyMaskRegionSchema),
23765
+ /**
23766
+ * Is the camera capturing sound right now? Read from the camera, never from
23767
+ * a server-side mirror.
23768
+ *
23769
+ * `null` means "no answer" — either this camera exposes no controllable
23770
+ * microphone (`getOptions().supportsAudioMute === false`) or the read
23771
+ * failed. A consumer must render `null` as UNKNOWN and never as `false`:
23772
+ * "the microphone is off" and "we could not ask" look identical to an
23773
+ * operator only until one of them is wrong.
23774
+ *
23775
+ * On a camera whose profiles carry the flag independently (Reolink writes
23776
+ * it per stream), `true` means AT LEAST ONE profile still carries audio —
23777
+ * privacy is only satisfied when every one of them is silent.
23778
+ */
23779
+ audioEnabled: boolean().nullable(),
23637
23780
  lastFetchedAt: number()
23638
23781
  });
23639
- /** Per-camera availability. */
23782
+ /** Per-camera availability. Probed, never assumed from the model name. */
23640
23783
  var PrivacyMaskOptionsSchema = object({
23641
23784
  /** Maximum number of supported zones. */
23642
23785
  maxRegions: number(),
23643
23786
  /** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
23644
23787
  supportedShapes: array(MaskShapeKindSchema),
23645
23788
  /** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
23646
- polygonVertices: MaskPolygonVerticesSchema.optional()
23789
+ polygonVertices: MaskPolygonVerticesSchema.optional(),
23790
+ /**
23791
+ * Does this camera expose a microphone switch we can actually write?
23792
+ *
23793
+ * Camera-probed: `true` only when the firmware answered with an audio flag
23794
+ * we know how to patch. A camera that never answered is `false` — a control
23795
+ * the operator can press that changes nothing is worse than no control, and
23796
+ * the switch group renders "not available" instead.
23797
+ */
23798
+ supportsAudioMute: boolean()
23647
23799
  });
23648
23800
  /** Partial change — every field optional. */
23649
23801
  var PrivacyMaskPatchSchema = object({
@@ -23670,6 +23822,27 @@ var privacyMaskCapability = {
23670
23822
  }), _void(), {
23671
23823
  kind: "mutation",
23672
23824
  auth: "admin"
23825
+ }),
23826
+ /**
23827
+ * Turn the camera's microphone on or off, at the camera.
23828
+ *
23829
+ * Deliberately its OWN mutation rather than a field on
23830
+ * {@link PrivacyMaskPatchSchema}: `patch.enabled` already means "the video
23831
+ * mask master switch", and overloading it would make one boolean mean two
23832
+ * unrelated things on the same call. It is also the only method here whose
23833
+ * write leaves the device in a state a later `getStatus` reads back
23834
+ * verbatim, which is what makes it safe as a switch authority.
23835
+ *
23836
+ * A camera whose `getOptions().supportsAudioMute` is false must REJECT
23837
+ * this rather than silently accept it — a write nothing applies is exactly
23838
+ * what the switch group exists to remove.
23839
+ */
23840
+ setAudioEnabled: method(object({
23841
+ deviceId: number(),
23842
+ enabled: boolean()
23843
+ }), _void(), {
23844
+ kind: "mutation",
23845
+ auth: "admin"
23673
23846
  })
23674
23847
  },
23675
23848
  status: {
@@ -23952,6 +24125,21 @@ var LocateSegmentResultSchema = discriminatedUnion("kind", [object({
23952
24125
  })]);
23953
24126
  /** Raw bytes of one finalized footage segment (read off disk on the recording node). */
23954
24127
  var ReadSegmentBytesResultSchema = object({ data: _instanceof(Uint8Array) });
24128
+ /**
24129
+ * One GOP of a finalized segment, cut by byte range through the segment's own
24130
+ * `mfra` (D31 on the D42 feeder path). `data` is the `ftyp`+`moov` head plus
24131
+ * the single `moof`+`mdat` covering the requested instant — standalone-
24132
+ * demuxable, never the whole file. When the segment's index cannot be parsed
24133
+ * the provider degrades INSIDE the mechanism to the whole segment (still one
24134
+ * `data`, `gopStartMs` = the segment start) — a worse read, not another path.
24135
+ */
24136
+ var ReadGopBytesResultSchema = object({
24137
+ data: _instanceof(Uint8Array),
24138
+ /** Absolute epoch ms of the returned fragment's first sample. */
24139
+ gopStartMs: number(),
24140
+ /** Media ms the returned fragment covers. */
24141
+ gopDurMs: number()
24142
+ });
23955
24143
  method(object({
23956
24144
  deviceId: number(),
23957
24145
  fromMs: number(),
@@ -23994,6 +24182,14 @@ method(object({
23994
24182
  }), ReadSegmentBytesResultSchema, {
23995
24183
  kind: "query",
23996
24184
  auth: "admin"
24185
+ }), method(object({
24186
+ deviceId: number(),
24187
+ profile: string(),
24188
+ startMs: number(),
24189
+ epochMs: number()
24190
+ }), ReadGopBytesResultSchema, {
24191
+ kind: "query",
24192
+ auth: "admin"
23997
24193
  }), method(object({
23998
24194
  deviceId: number(),
23999
24195
  config: RecordingConfigSchema
@@ -24553,6 +24749,16 @@ var StreamProfileConfigSchema = object({
24553
24749
  "baseline"
24554
24750
  ]).optional(),
24555
24751
  gop: number().optional(),
24752
+ /**
24753
+ * Whether THIS profile currently carries an audio track. READ-ONLY here.
24754
+ *
24755
+ * There is no matching field on {@link StreamProfilePatchSchema}: the
24756
+ * camera's microphone is owned by `privacy-mask` (`setAudioEnabled`), which
24757
+ * writes every profile at once so "audio off" means silent everywhere. A
24758
+ * per-profile writer beside it would let a camera be half-muted and would be
24759
+ * a second knob onto one device register — the failure D62 exists to
24760
+ * prevent. Absent when the firmware does not report the flag.
24761
+ */
24556
24762
  audio: boolean().optional()
24557
24763
  });
24558
24764
  var StreamParamsStatusSchema = object({
@@ -24593,7 +24799,13 @@ var StreamParamsOptionsSchema = object({
24593
24799
  ext: StreamProfileOptionsSchema.optional()
24594
24800
  });
24595
24801
  /** A partial change to one profile — every field optional; a provider
24596
- * ignores fields it doesn't support. */
24802
+ * ignores fields it doesn't support.
24803
+ *
24804
+ * There is deliberately NO `audio` here. It existed until 2026-08-07,
24805
+ * reachable from no form and honoured by exactly one provider, while the
24806
+ * camera's microphone is a whole-device fact. It now has one writer,
24807
+ * `privacyMask.setAudioEnabled`, which writes every profile — see
24808
+ * `privacy-mask.cap.ts`. */
24597
24809
  var StreamProfilePatchSchema = object({
24598
24810
  width: number().optional(),
24599
24811
  height: number().optional(),
@@ -24606,8 +24818,7 @@ var StreamProfilePatchSchema = object({
24606
24818
  "main",
24607
24819
  "baseline"
24608
24820
  ]).optional(),
24609
- gop: number().optional(),
24610
- audio: boolean().optional()
24821
+ gop: number().optional()
24611
24822
  });
24612
24823
  var streamParamsCapability = {
24613
24824
  name: "stream-params",
@@ -30339,6 +30550,12 @@ Object.freeze({
30339
30550
  addonId: null,
30340
30551
  access: "view"
30341
30552
  },
30553
+ "privacyMask.setAudioEnabled": {
30554
+ capName: "privacy-mask",
30555
+ capScope: "device",
30556
+ addonId: null,
30557
+ access: "create"
30558
+ },
30342
30559
  "privacyMask.setMask": {
30343
30560
  capName: "privacy-mask",
30344
30561
  capScope: "device",
@@ -30507,6 +30724,12 @@ Object.freeze({
30507
30724
  addonId: null,
30508
30725
  access: "create"
30509
30726
  },
30727
+ "recording.readGopBytes": {
30728
+ capName: "recording",
30729
+ capScope: "system",
30730
+ addonId: null,
30731
+ access: "view"
30732
+ },
30510
30733
  "recording.readSegmentBytes": {
30511
30734
  capName: "recording",
30512
30735
  capScope: "system",
@@ -31973,6 +32196,88 @@ object({
31973
32196
  paddingRatio: .15,
31974
32197
  square: false
31975
32198
  }).paddingRatio;
32199
+ /**
32200
+ * WHICH delivered frames the decode worker retains a native copy of.
32201
+ *
32202
+ * - `all` — every frame the worker delivered to the runner. The shipped
32203
+ * behaviour, and the only correct one if something can ask for a crop of a
32204
+ * frame the runner never sent to inference.
32205
+ * - `inferred` — only the frames the runner ADMITTED to its detection queue.
32206
+ * A native-crop request always names a `frameId` that rode an inference
32207
+ * result, so that is the only set a request can name. How much it drops is
32208
+ * the two-plane governor's admit ratio and nothing else: measured at ~50% on
32209
+ * this cluster, not the ~80% the design sketch assumed, because the governor
32210
+ * was not throttling as hard as the sketch supposed. Read
32211
+ * `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
32212
+ * of you rather than quoting a number from here. The newest delivered frame is
32213
+ * croppable regardless — it is still the worker's reserved slot, not a lease —
32214
+ * which covers the one-frame race between a mark and the supersede that
32215
+ * consumes it.
32216
+ */
32217
+ var NativeLeaseAdmissionSchema = _enum(["all", "inferred"]);
32218
+ object({
32219
+ /**
32220
+ * How long a retained native frame is served before it counts as a miss.
32221
+ *
32222
+ * Must cover the FULL late-crop horizon: detection inference + the
32223
+ * cross-process inference-result hop to hub post-analysis + tracking + the
32224
+ * tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
32225
+ * outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
32226
+ * RAM per busy camera grows linearly with no measured hit-rate gain.
32227
+ */
32228
+ ttlMs: number().int().min(250).max(1e4),
32229
+ /**
32230
+ * Hard per-decode-worker RAM ceiling for retained native frames, in MB.
32231
+ *
32232
+ * Intended as a SAFETY ceiling with the TTL as the effective cap — but check
32233
+ * which one is actually binding before reasoning from that. At the shipped
32234
+ * 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
32235
+ * at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
32236
+ * `leaseFrames` on the metrics line say which. When the ceiling binds, a
32237
+ * change that admits fewer frames buys retention WINDOW at constant RAM
32238
+ * rather than giving RAM back — lower this knob if RAM is what you wanted.
32239
+ * `0` DISABLES the lease entirely and falls the worker back to the tiny
32240
+ * leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
32241
+ * to replace).
32242
+ */
32243
+ budgetMb: number().int().min(0).max(4096),
32244
+ /**
32245
+ * Demand window: eager per-frame native retention runs only within this many
32246
+ * ms of the last native-crop request (or of the dial starting).
32247
+ *
32248
+ * `0` means ALWAYS ON — it disables the gate, it does not disable retention.
32249
+ * That is the legacy behaviour that saturated an N100 (24 native-4K downloads
32250
+ * per second on a camera with zero crop demand), so leave it non-zero unless
32251
+ * you are reproducing that.
32252
+ */
32253
+ activityMs: number().int().min(0).max(12e4),
32254
+ /**
32255
+ * Which delivered frames are retained at all — see
32256
+ * {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
32257
+ * changes WHAT is kept rather than for how long, so it is also the only one
32258
+ * that can turn a crop that used to hit into a miss. The worker counts every
32259
+ * crop request naming a frame it did NOT see marked
32260
+ * (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
32261
+ * there is the signal that some caller names frames outside the inference set
32262
+ * and that this must go back to `all`.
32263
+ */
32264
+ admission: NativeLeaseAdmissionSchema
32265
+ });
32266
+ /**
32267
+ * The values in force when the operator has set nothing — byte-for-byte the
32268
+ * constants the decode worker shipped with as env-var defaults, so making these
32269
+ * settings changed no behaviour on the day it landed.
32270
+ */
32271
+ var DEFAULT_NATIVE_LEASE_SETTINGS = {
32272
+ ttlMs: 1200,
32273
+ budgetMb: 1024,
32274
+ activityMs: 15e3,
32275
+ admission: "inferred"
32276
+ };
32277
+ DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs;
32278
+ DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
32279
+ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
32280
+ DEFAULT_NATIVE_LEASE_SETTINGS.admission;
31976
32281
  //#endregion
31977
32282
  Object.defineProperty(exports, "BaseAddon", {
31978
32283
  enumerable: true,