@trillboards/edge-sdk 0.2.61 → 0.2.64

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 (71) hide show
  1. package/deploy/litertlm/test_trillboards_litertlm_shim.py +132 -0
  2. package/deploy/litertlm/trillboards-litertlm-shim.pth +1 -0
  3. package/deploy/litertlm/trillboards_litertlm_shim.py +214 -0
  4. package/deploy/macos/bench-local-vlm.sh +8 -4
  5. package/deploy/macos/build-macos-pkg.sh +4 -0
  6. package/deploy/macos/scripts/setup-litertlm.sh +99 -2
  7. package/deploy/systemd/setup-litertlm.sh +99 -2
  8. package/deploy/windows/build-windows.ps1 +4 -0
  9. package/deploy/windows/register-litertlm-task.ps1 +22 -0
  10. package/deploy/windows/setup-litertlm.ps1 +131 -3
  11. package/deploy/windows/trillboards-edge.cmd +19 -0
  12. package/dist/CloudSensingManager.d.ts +65 -0
  13. package/dist/CloudSensingManager.d.ts.map +1 -1
  14. package/dist/CloudSensingManager.js +65 -30
  15. package/dist/CloudSensingManager.js.map +1 -1
  16. package/dist/DurableVlmCandidateSpool.d.ts +21 -0
  17. package/dist/DurableVlmCandidateSpool.d.ts.map +1 -1
  18. package/dist/DurableVlmCandidateSpool.js +126 -1
  19. package/dist/DurableVlmCandidateSpool.js.map +1 -1
  20. package/dist/EdgeAgent.js +1 -1
  21. package/dist/LiteRtVlmProvider.d.ts +74 -7
  22. package/dist/LiteRtVlmProvider.d.ts.map +1 -1
  23. package/dist/LiteRtVlmProvider.js.map +1 -1
  24. package/dist/MosaicSource.d.ts +18 -0
  25. package/dist/MosaicSource.d.ts.map +1 -1
  26. package/dist/MosaicSource.js +23 -0
  27. package/dist/MosaicSource.js.map +1 -1
  28. package/dist/VlmCandidateBuffer.d.ts +45 -6
  29. package/dist/VlmCandidateBuffer.d.ts.map +1 -1
  30. package/dist/VlmCandidateBuffer.js +76 -7
  31. package/dist/VlmCandidateBuffer.js.map +1 -1
  32. package/dist/analyzeCostObserver.d.ts +96 -0
  33. package/dist/analyzeCostObserver.d.ts.map +1 -0
  34. package/dist/analyzeCostObserver.js +130 -0
  35. package/dist/analyzeCostObserver.js.map +1 -0
  36. package/dist/config.d.ts +8 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js.map +1 -1
  39. package/dist/connectorCadence.d.ts +286 -57
  40. package/dist/connectorCadence.d.ts.map +1 -1
  41. package/dist/connectorCadence.js +313 -61
  42. package/dist/connectorCadence.js.map +1 -1
  43. package/dist/deployedCadence.d.ts +8 -2
  44. package/dist/deployedCadence.d.ts.map +1 -1
  45. package/dist/deployedCadence.js +6 -3
  46. package/dist/deployedCadence.js.map +1 -1
  47. package/dist/deployedCadenceSource.d.ts +10 -0
  48. package/dist/deployedCadenceSource.d.ts.map +1 -1
  49. package/dist/deployedCadenceSource.js +11 -0
  50. package/dist/deployedCadenceSource.js.map +1 -1
  51. package/dist/diagnose.d.ts +1 -1
  52. package/dist/diagnose.js +1 -1
  53. package/dist/localInferenceSchema.d.ts +8 -3
  54. package/dist/localInferenceSchema.d.ts.map +1 -1
  55. package/dist/localInferenceSchema.js +8 -3
  56. package/dist/localInferenceSchema.js.map +1 -1
  57. package/dist/localSensing.d.ts +22 -1
  58. package/dist/localSensing.d.ts.map +1 -1
  59. package/dist/localSensing.js +101 -7
  60. package/dist/localSensing.js.map +1 -1
  61. package/dist/localVlmBench.d.ts +82 -0
  62. package/dist/localVlmBench.d.ts.map +1 -1
  63. package/dist/localVlmBench.js +88 -0
  64. package/dist/localVlmBench.js.map +1 -1
  65. package/dist/sensingCatchRate.d.ts.map +1 -1
  66. package/dist/sensingCatchRate.js +75 -10
  67. package/dist/sensingCatchRate.js.map +1 -1
  68. package/dist/sensingRunMode.d.ts.map +1 -1
  69. package/dist/sensingRunMode.js +38 -2
  70. package/dist/sensingRunMode.js.map +1 -1
  71. package/package.json +1 -1
@@ -44,11 +44,13 @@
44
44
  * of 493 analyses/hour for the WHOLE connector, however many
45
45
  * cameras it runs.
46
46
  * STALE — see the block under MEASURED_LOCAL_ANALYZE_MS. The same
47
- * harness measures 1.90 s (and 1.22 s with the 2026-08-19
48
- * config.json change) on the pinned 0.16.0, so the real ceiling on
49
- * this machine class is ~1,895/hour and ~2,951/hour respectively.
50
- * The constant is kept as a conservative FLEET default because the
51
- * fleet varies ~7x; do not quote 493 as this machine's ceiling.
47
+ * harness measures 1.90 s, then 1.22 s with the 2026-08-19
48
+ * config.json change, then a further paired 2.42x with the 2026-08-19
49
+ * fast-conversation shim, on the pinned 0.16.0 — so the real ceiling
50
+ * on this machine class is ~1,895/hour, ~2,951/hour and higher again
51
+ * respectively. The constant is kept as a conservative FLEET default
52
+ * because the fleet varies ~7x; do not quote 493 as this machine's
53
+ * ceiling.
52
54
  * (The DOOH audience schema measured 12.48 s median and ranged to
53
55
  * 23.8 s because it emits a free-text narrative. The SOP field set
54
56
  * is both cheaper and far tighter — 6.0-7.5 s — which is a second,
@@ -141,25 +143,81 @@ export declare const MEASURED_LOCAL_ANALYZE_MS = 7300;
141
143
  * the box can afford, which costs hazard-persistence resolution against the
142
144
  * 111 s constructive-notice bracket — the one thing this file exists to protect.
143
145
  *
144
- * ── Why it is NOT simply lowered here ──────────────────────────────────────
145
- *
146
- * Because this is ONE constant for a fleet that varies ~7x, and every number
147
- * above is one desk. `cctv-local-inference-hardware-floor.md` §7 measures
148
- * 34.78 s/frame on the `windows-latest` class. Replacing 7.30 s with 1.22 s (the
149
- * M4 Max WITH the 2026-08-19 config.json change) would demand ~28x what that box
150
- * can deliver, and replacing it with the slow box's number makes every heartbeat
151
- * worse than today. A single global constant cannot be right for both, and
152
- * picking the fast desk is exactly the error `.claude/lessons/2026-08-18-a-
153
- * benchmark-with-no-machine-axis-is-a-spec-for-one-desk.md` was written about.
154
- *
155
- * The fix is to DERIVE this per machine — the connector already measures its own
156
- * frame cost, and `deployedCadence` already exists to reconcile a deployed
157
- * cadence with what a machine can afford. Until that lands, treat 7.30 s as a
158
- * deliberately conservative fleet default and NOT as a measurement of any
159
- * machine: quoting it as one is what this block exists to stop. Changing the
160
- * value also moves `sensingCatchRate.DEFAULT_SAMPLER.analyzeS`, so it requires
161
- * re-running `replay-catch-rate.mjs` and regenerating
162
- * `.github/contracts/sensing-catch-rate-baseline.json` in the same change.
146
+ * ── AND IT MOVED AGAIN, 2026-08-19 (the shim) ──────────────────────────────
147
+ *
148
+ * `litert-lm serve` built `ConstrainedDecodingConfig(enable=True, …)` for every
149
+ * request even when nothing asked for a `response_format`, so every frame paid
150
+ * a constraint build for a grammar nothing used. Removing it (see
151
+ * `deploy/litertlm/trillboards_litertlm_shim.py`) is worth a further **paired
152
+ * 2.42x** on the shipped path — two stock serves on two ports, interleaved,
153
+ * n=16, 16/16 schema-valid and 16/16 BYTE-IDENTICAL generations on both arms.
154
+ * `bench-local-vlm.mjs --arms gpu-sop`, back to back in the same window, moves
155
+ * **1.32 -> 0.72 s/frame, i.e. 2,727 -> 5,000 analyses/hour**, at load 22-25
156
+ * where the same harness measures the stock arm at 1.32 s against the 1.22 s
157
+ * this file already records for it — so that window is close to the regime the
158
+ * 1.22 s was taken in, and 0.72 s is the nearest thing to a quotable per-frame
159
+ * number this machine class has.
160
+ *
161
+ * TWO CAVEATS THAT TRAVEL WITH IT. The ratio is itself load-dependent — 3.734x
162
+ * at 0.0% CPU idle against 2.42x at load 25 — because the removed work is
163
+ * CPU-bound and the retained work is GPU-bound, so a figure taken under
164
+ * contention flatters the change. And 0.72 s/frame is still BELOW the capacity
165
+ * target: 4 cameras resolving a 2 s bracket needs 0.5 s/frame (7,200
166
+ * analyses/hour), and this serves 2-3.
167
+ *
168
+ * ── IT IS NOT LOWERED HERE. IT IS NOW A BOOTSTRAP. (2026-08-19) ───────────
169
+ *
170
+ * This is ONE constant for a fleet whose measured spread on the SHIPPED path is
171
+ * 0.50 s/frame (M4 Max, post-#12567/#12594) against tens of seconds on a
172
+ * GPU-less x86 box. No constant is right for both: the fast desk's number would
173
+ * demand ~70x what the slow box can deliver, and the slow box's number makes
174
+ * every heartbeat worse than today. Picking the fast desk is exactly the error
175
+ * `.claude/lessons/2026-08-18-a-benchmark-with-no-machine-axis-is-a-spec-for-
176
+ * one-desk.md` was written about.
177
+ *
178
+ * So the fix was never a better constant, and as of 2026-08-19 it is built: the
179
+ * box DERIVES this for itself. `AnalyzeCostObserver` (analyzeCostObserver.ts)
180
+ * times every successful `analyze()` in `LocalSensingClient`, reports a rolling
181
+ * median over 16 frames, and `periodicIntervalMsForFeedCount` /
182
+ * `resolveDeployedCadence` take it as an optional input. A connector that has
183
+ * measured itself uses its OWN number; one that has not uses this one.
184
+ *
185
+ * NOTE WHAT THAT MAKES THIS CONSTANT. It is the value to assume BEFORE a machine
186
+ * has measured itself, so it should be conservative rather than accurate — and
187
+ * 7.30 s is exactly that. Guessing FAST on a slow box oversubscribes the single
188
+ * admission slot from the first minute; guessing slow costs only heartbeat
189
+ * resolution, and only until the observer settles (8 frames). It is therefore
190
+ * deliberately NOT lowered, and quoting it as a measurement of any machine is
191
+ * still what this block exists to stop.
192
+ *
193
+ * A CONSEQUENCE WORTH KNOWING: because this value does not move, neither does
194
+ * `sensingCatchRate.DEFAULT_SAMPLER.analyzeS`, so none of this required a
195
+ * corpus replay or a `.github/contracts/sensing-catch-rate-baseline.json`
196
+ * regeneration. If you ever DO move it, all of that becomes mandatory in the
197
+ * same change — and the catch-rate ratchet is shrink-only, so a baseline may
198
+ * never be regenerated merely to clear red.
199
+ *
200
+ * ── The idle-box objection, and why it does not block the derivation ────────
201
+ *
202
+ * The 2026-08-19 predecessor of this block held the per-machine derivation back
203
+ * for want of a per-frame number taken on an idle box, on the grounds that a
204
+ * figure measured at 0.0% CPU idle would hardcode one session's contention into
205
+ * every connector. That objection is answered twice over.
206
+ *
207
+ * First, it no longer applies to the DERIVATION at all: nothing global is
208
+ * derived from a measured number any more. Each box measures itself, in situ,
209
+ * under whatever load it actually runs at — which is the right load by
210
+ * definition, and is the one thing a bench on someone else's desk can never
211
+ * reproduce.
212
+ *
213
+ * Second, the verdict turned out not to depend on it. Three measurements of the
214
+ * shipped arm on one M4 Max across a 3x load range: 0.45 s at load 7.77, 0.50 s
215
+ * at load 11.87, 0.72 s at load 22-25. All three clear the 13.88 s/frame budget
216
+ * for 4 cameras by 19-31x, so "this class clears 4 cameras" is invariant across
217
+ * everything that could be measured. Load moves the quotable figure, not the
218
+ * verdict — and the post-merge path is markedly LESS load-sensitive than the one
219
+ * that motivated the objection, because the work #12594 removed was CPU-bound
220
+ * and what remains is GPU-bound.
163
221
  */
164
222
  /**
165
223
  * Share of the single admission slot the STILL-scene heartbeat floor may take.
@@ -213,8 +271,13 @@ export declare const STILL_FLOOR_SLOT_SHARE = 0.5;
213
271
  */
214
272
  export declare const MIN_ANALYZE_INTERVAL_MS = 30000;
215
273
  /**
216
- * Measured wall-clock cost of ONE LOOK, in ms — a real RTSP grab, not the
217
- * decode-and-detect that follows it.
274
+ * Measured wall-clock cost of ONE LOOK on the PER-GRAB capture path, in ms — a
275
+ * real RTSP grab, not the decode-and-detect that follows it.
276
+ *
277
+ * STILL LIVE, and still exactly right for the path it describes: the per-grab
278
+ * provider is what a feed runs when it cannot hold a persistent session (see
279
+ * `MEASURED_STREAM_LOOK_MS`), so this is the number `lookIntervalMsForFeedCount`
280
+ * uses in `'per_grab'` mode and nothing about it has changed.
218
281
  *
219
282
  * The cascade budget above quotes 3.6 ms for a look. That is the cost AFTER the
220
283
  * frame is in hand: pull it down to ~178x100 grey and run the detector. Getting
@@ -247,6 +310,75 @@ export declare const MIN_ANALYZE_INTERVAL_MS = 30000;
247
310
  * the connector is not running.
248
311
  */
249
312
  export declare const MEASURED_LOOK_MS = 2000;
313
+ /**
314
+ * Measured wall-clock cost of ONE LOOK on the PERSISTENT capture path, in ms.
315
+ *
316
+ * `RTSPStreamCameraProvider` holds one long-lived ffmpeg per feed decoding into
317
+ * a bounded ring, so `getFrame()` is a memory read of the newest frame rather
318
+ * than a process start plus a DESCRIBE/SETUP/PLAY plus a wait for an IDR. The
319
+ * handshake is paid ONCE, at open.
320
+ *
321
+ * MEASURED 2026-08-18 against the same rig, same clip, same degraded profile
322
+ * (640x360 / 399 kbps / GOP 2 s), driving looks at the shipped cadence so a
323
+ * fresh frame is already in hand — which is the condition the connector runs
324
+ * in and the only one where "cost of a look" means anything:
325
+ *
326
+ * per-grab p50 2,000 ms
327
+ * persistent p50 0 ms (sub-millisecond; the ring read is a pointer)
328
+ *
329
+ * Reproduce: `node scripts/measure-persistent-capture.mjs --feeds 1
330
+ * --seconds 60 --pace-ms 500`. The rig is committed — see that file's header
331
+ * for why, and for why a `file://` fixture cannot stand in for it.
332
+ *
333
+ * It is 1 rather than 0 so that `lookIntervalMsForFeedCount` can never divide
334
+ * by it, and so nobody reads a zero as "free" — a look still costs the
335
+ * detector's `MEASURED_LOOK_DETECT_MS`, which is what actually bounds the
336
+ * persistent path.
337
+ */
338
+ export declare const MEASURED_STREAM_LOOK_MS = 1;
339
+ /**
340
+ * Cost of a look AFTER the frame is in hand, in ms: pull it down to ~178x100
341
+ * grey and run `SceneChangeDetector`. Measured 3.6 ms median / 7.1 ms p95.
342
+ *
343
+ * This is the number the cascade budget has always quoted, and until the
344
+ * capture path changed it was irrelevant — a 3.6 ms stage behind a 2,000 ms
345
+ * one bounds nothing. On the persistent path it is the ONLY per-look cost
346
+ * left, so it becomes the binding term and gets named here rather than living
347
+ * in a comment.
348
+ */
349
+ export declare const MEASURED_LOOK_DETECT_MS = 3.6;
350
+ /**
351
+ * Share of ONE core the motion detector may take across every feed.
352
+ *
353
+ * The one judgement in the persistent derivation, stated rather than hidden —
354
+ * the same shape as `STILL_FLOOR_SLOT_SHARE`. Detection is the cheapest stage
355
+ * on the box and it must not become the noisy neighbour: the connector also
356
+ * runs one ffmpeg decode per feed (measured on the rig at ~2.4% of a core per
357
+ * feed at this profile) and one local VLM slot, and the VLM is the thing the
358
+ * product is actually spending money on.
359
+ *
360
+ * A quarter of a core does not bind at any realistic feed count — see
361
+ * `lookIntervalMsForFeedCount` — which is the point: it is a guard rail, not a
362
+ * throttle, and it exists so that a future 64-feed host derives a cadence it
363
+ * can serve instead of one it merely asked for.
364
+ */
365
+ export declare const LOOK_DETECT_CPU_SHARE = 0.25;
366
+ /**
367
+ * The persistent session's frame interval, in ms — the freshest a look can
368
+ * possibly be, because a look reads the newest DECODED frame.
369
+ *
370
+ * `RTSPStreamCameraProvider` defaults `streamFps` to 4, i.e. 250 ms. Restated
371
+ * here rather than imported because `edge-sdk` must not depend on a platform
372
+ * package, and asserted against the real default by `connectorCadence.test.ts`
373
+ * so the two cannot drift — the same treatment `LOOK_CAPTURE_CONCURRENCY` gets.
374
+ *
375
+ * `LOOK_INTERVAL_MS` must never be finer than this: asking for a 100 ms look
376
+ * cadence against a 250 ms stream would hand the same frame to the motion
377
+ * detector twice, which the provider refuses (it throws rather than repeat a
378
+ * frame) and which would report as a stale-stream fault on a perfectly healthy
379
+ * camera.
380
+ */
381
+ export declare const MEASURED_STREAM_FRAME_INTERVAL_MS = 250;
250
382
  /**
251
383
  * Concurrent RTSP grabs one physical recorder will take —
252
384
  * `DEFAULT_MAX_CONCURRENT_CAPTURES_PER_SOURCE` in `RTSPCameraProvider`, which
@@ -296,32 +428,59 @@ export declare const LEGACY_LOOK_INTERVAL_MS = 30000;
296
428
  * T Instant Brief Sustained State
297
429
  * 30 s 8.4% 18.6% 100% 100%
298
430
  * 5 s 50.2% 55.1% 100% 100%
299
- * 2.44 s 89.1% 71.2% 100% 100%
300
431
  * 2 s 95.5% 75.2% 100% 100%
301
432
  * 1 s 100.0% 86.6% 100% 100%
302
433
  * 0.75 s 100.0% 90.1% 100% 100%
303
- *
304
- * Two things fall out of that table and both are load bearing:
305
- *
306
- * - The Instant group — "alert me if the door opens after hours", a 2.2 s
307
- * median event — clears the 90% target at 2.4 s and is at 8.4% today. This
308
- * is the promise the 30 s cadence was silently failing.
309
- *
310
- * - The Brief group does NOT reach 90% until 0.75 s, and 0.75 s is well below
311
- * the measured cost of a single grab (`MEASURED_LOOK_MS`). So 90% on every
312
- * shape is NOT reachable by look cadence on the current capture
313
- * architecture, and this constant does not pretend otherwise. Closing that
314
- * gap needs a persistent RTSP session — one long-lived ffmpeg per feed
315
- * instead of one per grab — which is an architecture change, not a number.
316
- *
317
- * 2 000 ms is therefore the fastest cadence that is BOTH an eval output and
318
- * physically deliverable: it is the measured single-grab floor, and at it the
319
- * Instant group clears the target. Do not lower it without first lowering
320
- * `MEASURED_LOOK_MS`, and do not raise it without re-running the replay — the
321
- * `sensing-catch-rate` guard re-reads this constant and fails if it drifts from
322
- * the cadence the corpus was measured at.
434
+ * 0.5 s 100.0% 93.2% 100% 100%
435
+ *
436
+ * ── 2 000 -> 500 ms ON 2026-08-18, AND THE REASON IS THE CAPTURE PATH ──────
437
+ *
438
+ * This constant was 2 000 ms, and the comment here said why in as many words:
439
+ * "the Brief group does NOT reach 90% until 0.75 s, and 0.75 s is well below
440
+ * the measured cost of a single grab. So 90% on every shape is NOT reachable by
441
+ * look cadence on the current capture architecture… Closing that gap needs a
442
+ * persistent RTSP session — one long-lived ffmpeg per feed instead of one per
443
+ * grab — which is an architecture change, not a number."
444
+ *
445
+ * That is now built (`RTSPStreamCameraProvider`), the constraint it named is
446
+ * gone, and this is the number that moves as a result. A look is a memory read
447
+ * of the newest decoded frame — `MEASURED_STREAM_LOOK_MS`, sub-millisecond —
448
+ * so the recorder no longer bounds the look cadence at all. Brief clears the
449
+ * 90% target at 0.75 s; 500 ms is the next value that is BOTH under it and a
450
+ * whole multiple of the persistent session's 250 ms frame interval
451
+ * (`MEASURED_STREAM_FRAME_INTERVAL_MS`), so a look always finds an unseen frame
452
+ * in hand and never blocks.
453
+ *
454
+ * ── WHY STOP AT 500 ms ────────────────────────────────────────────────────
455
+ *
456
+ * The same stopping rule that picked 2 000 ms: go until the last unmet target
457
+ * is met, and no further. At 500 ms every reporting group clears 90% — the
458
+ * first cadence in the product's history where that is true. Below it the
459
+ * corpus has nothing left to buy (Instant and Brief are saturated, Sustained
460
+ * and State were never look-limited) while the cost is real and linear: each
461
+ * halving doubles both the detector's CPU (`MEASURED_LOOK_DETECT_MS` per look
462
+ * per feed) and the session's JPEG-encode rate.
463
+ *
464
+ * ── AND IT MUST NOT COST ANALYSES ─────────────────────────────────────────
465
+ *
466
+ * Looking 4x more often must not analyse 4x more often, or the change has
467
+ * failed: one 7.30 s slot is 493 analyses/hour for the whole connector and
468
+ * there is no cloud path left to absorb an overflow. Three things hold that,
469
+ * and all three are asserted rather than assumed:
470
+ * - `MIN_ANALYZE_INTERVAL_MS` (30 s) is a SEPARATE constant and is what
471
+ * `MotionGate.minAnalyzeIntervalMs` floors on.
472
+ * - `periodicIntervalMsForFeedCount` floors on the ANALYZE floor, never on
473
+ * the look cadence, so the still-scene heartbeat does not move.
474
+ * - a still scene contributes exactly ZERO motion-triggered analyses, which
475
+ * is what makes the extra looks free at the model.
476
+ * The `sensing-catch-rate` guard's volume ratchet is the mechanical version:
477
+ * `volume.idleAnalyses` may only go DOWN.
478
+ *
479
+ * Do not raise or lower this without re-running the replay — the guard re-reads
480
+ * this constant and fails if it drifts from the cadence the corpus baseline was
481
+ * measured at.
323
482
  */
324
- export declare const LOOK_INTERVAL_MS = 2000;
483
+ export declare const LOOK_INTERVAL_MS = 500;
325
484
  /** The DOOH audience default (`DEFAULT_PERIODIC_MS`). The slow end of the clamp. */
326
485
  export declare const DOOH_PERIODIC_MS = 300000;
327
486
  /**
@@ -334,29 +493,99 @@ export declare const DOOH_PERIODIC_MS = 300000;
334
493
  * Returns `null` when the count is not a positive integer, so a caller can omit
335
494
  * the key entirely rather than write a cadence derived from a bad count.
336
495
  */
337
- export declare function periodicIntervalMsForFeedCount(feedCount: unknown): number | null;
496
+ export declare function periodicIntervalMsForFeedCount(feedCount: unknown,
338
497
  /**
339
- * The fastest look cadence a connector running `feedCount` feeds against ONE
340
- * recorder can actually SUSTAIN, in ms.
498
+ * What a frame ACTUALLY costs on this box, in ms, when this box has measured
499
+ * itself (`AnalyzeCostObserver.medianMs()`). Omitted or `null` falls back to
500
+ * `MEASURED_LOCAL_ANALYZE_MS`, which is the BOOTSTRAP — deliberately
501
+ * conservative, and not a measurement of any machine.
502
+ *
503
+ * This parameter is the whole reason a fleet spanning 0.45 s/frame to tens of
504
+ * seconds can share one code path: the equation was always per-machine, and
505
+ * until 2026-08-19 only its input was global.
506
+ */
507
+ analyzeMs?: number | null): number | null;
508
+ /**
509
+ * How a feed's frames are acquired. The look cadence a connector can SUSTAIN
510
+ * depends on this and on nothing else about the box.
511
+ *
512
+ * 'persistent' one long-lived ffmpeg per feed into a bounded ring
513
+ * (`RTSPStreamCameraProvider`). A look is a memory read.
514
+ * 'per_grab' one ffmpeg per grab (`RTSPCameraProvider`). A look is an RTSP
515
+ * handshake, and the RECORDER is the bound.
516
+ */
517
+ export type CaptureMode = 'persistent' | 'per_grab';
518
+ /**
519
+ * The fastest look cadence a connector running `feedCount` feeds can actually
520
+ * SUSTAIN, in ms.
341
521
  *
342
522
  * Same shape as `periodicIntervalMsForFeedCount` and for the same reason: a
343
523
  * cadence is only real if the machine can deliver it. That function divides one
344
- * inference slot among N feeds; this one divides one recorder's capture
345
- * capacity among N feeds.
524
+ * inference slot among N feeds; this one divides whatever the capture path's
525
+ * scarce resource is — and the whole point of the persistent session is that it
526
+ * CHANGES WHICH RESOURCE THAT IS.
527
+ *
528
+ * ── 'per_grab' — the RECORDER is the bound (unchanged) ────────────────────
346
529
  *
347
530
  * N feeds x (1 look / T) <= LOOK_CAPTURE_CONCURRENCY / MEASURED_LOOK_MS
348
531
  * T >= N x MEASURED_LOOK_MS / LOOK_CAPTURE_CONCURRENCY
349
532
  *
350
533
  * Measured against that formula at 16 feeds on one recorder, GOP 2 s: predicted
351
- * 16.0 s/feed, observed 16.0 s/feed.
534
+ * 16.0 s/feed, observed 16.0 s/feed. This is still exactly right for a feed that
535
+ * could not hold a persistent session and degraded — see
536
+ * `RTSPStreamCameraProvider`'s degrade path — so the branch stays live rather
537
+ * than being deleted as legacy.
538
+ *
539
+ * ── 'persistent' — the recorder drops out and the DETECTOR is the bound ───
540
+ *
541
+ * A look no longer touches the recorder, so `LOOK_CAPTURE_CONCURRENCY` and
542
+ * `MEASURED_LOOK_MS` have nothing to say about it. What is left is the cost
543
+ * AFTER the frame is in hand:
544
+ *
545
+ * N feeds x (1 look / T) x MEASURED_LOOK_DETECT_MS <= LOOK_DETECT_CPU_SHARE
546
+ * T >= N x 3.6 / 0.25 ms
547
+ *
548
+ * That is 230 ms at 16 feeds and 461 ms at 32 — both BELOW `LOOK_INTERVAL_MS`,
549
+ * so the clamp wins and a realistic connector simply gets the eval target. It
550
+ * begins to bind past ~34 feeds, which is the honest place for it to bind: a
551
+ * host with 34 cameras has other problems (its still-scene heartbeat pinned at
552
+ * `DOOH_PERIODIC_MS` since 21 feeds) and `getSensingHealth()` reports the
553
+ * effective interval either way.
352
554
  *
353
555
  * Never faster than `LOOK_INTERVAL_MS` (the eval target — looking finer than
354
556
  * that buys nothing the corpus can see) and never slower than
355
557
  * `LEGACY_LOOK_INTERVAL_MS`, so this can only make a connector look MORE often
356
- * than it did, never less. Returns `null` on an uncountable feed count so a caller can
357
- * omit the key rather than derive a cadence from nothing.
558
+ * than it did, never less. Returns `null` on an uncountable feed count so a
559
+ * caller can omit the key rather than derive a cadence from nothing.
560
+ */
561
+ export declare function lookIntervalMsForFeedCount(feedCount: unknown, captureMode?: CaptureMode): number | null;
562
+ /**
563
+ * Decoded frames a persistent session must produce per LOOK.
564
+ *
565
+ * TWO, and the second one is not waste. `RTSPStreamCameraProvider.getFrame()`
566
+ * refuses to hand back a frame it has already served — that refusal is the
567
+ * whole reason a frozen camera cannot report as healthy — so a look that
568
+ * arrives before the next frame does BLOCKS, and if it blocks past the
569
+ * staleness deadline it throws on a camera that is working perfectly.
570
+ *
571
+ * Running the stream at exactly the look rate makes that a coin flip on every
572
+ * tick, because neither clock is locked to the other: the session's frame
573
+ * cadence follows the CAMERA's timestamps and the look cadence follows a Node
574
+ * timer, and they drift. One frame of headroom removes the race for the cost of
575
+ * one extra scale+encode per look — the cheapest stage in the pipeline, and one
576
+ * that never touches the recorder or the model.
577
+ */
578
+ export declare const STREAM_LOOK_HEADROOM = 2;
579
+ /**
580
+ * Frames per second a persistent session should decode to serve a given look
581
+ * cadence. Derived, so the two can never be set independently and drift into
582
+ * the blocking case above.
583
+ *
584
+ * At the shipped 500 ms look cadence this is 4 fps — which is also the
585
+ * provider's own default (`MEASURED_STREAM_FRAME_INTERVAL_MS`), and
586
+ * `connectorCadence.test.ts` asserts the agreement rather than trusting it.
358
587
  */
359
- export declare function lookIntervalMsForFeedCount(feedCount: unknown): number | null;
588
+ export declare function streamFpsForLookIntervalMs(lookIntervalMs: unknown): number;
360
589
  /**
361
590
  * The still-scene analyses/hour a whole connector demands at a heartbeat, and
362
591
  * what fraction of the single slot that is. Exposed so the derivation is
@@ -1 +1 @@
1
- {"version":3,"file":"connectorCadence.d.ts","sourceRoot":"","sources":["../src/connectorCadence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgHG;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAEtC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA4BhF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK5E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,OAAO,EAClB,kBAAkB,EAAE,OAAO,GAC1B;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAOhD"}
1
+ {"version":3,"file":"connectorCadence.d.ts","sourceRoot":"","sources":["../src/connectorCadence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkHG;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmGG;AAEH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAU,CAAC;AAExC;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,OAAO;AAClB;;;;;;;;;GASG;AACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAkCf;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,UAAU,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,OAAO,EAClB,WAAW,GAAE,WAAwB,GACpC,MAAM,GAAG,IAAI,CA0Bf;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,OAAO,GAAG,MAAM,CAI1E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,OAAO,EAClB,kBAAkB,EAAE,OAAO,GAC1B;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAOhD"}