@camstack/addon-pipeline 1.2.128 → 1.2.130

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 (123) hide show
  1. package/dist/{addon-utils-BnprivSs.js → addon-utils-DH78p79i.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +241 -95
  5. package/dist/detection-pipeline/index.mjs +239 -93
  6. package/dist/{dist-BZUXb5S5.js → dist-DOw-izoI.js} +243 -75
  7. package/dist/{dist-HLAOw6Pe.mjs → dist-Wt81pN5Y.mjs} +243 -75
  8. package/dist/{event-loop-stall-monitor-BdArVUlx.js → event-loop-stall-monitor-Bld7jNCb.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-BH-cL_hg.mjs → event-loop-stall-monitor-CtQCOZqm.mjs} +1 -1
  10. package/dist/{lazy-sharp-DTDXrhMG.js → lazy-sharp-BbNI2ZD-.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-CDPRLalN.mjs → process-memory-1_D3EZT9.mjs} +1 -1
  16. package/dist/{process-memory-Dh1SyzdJ.js → process-memory-D9H5qjDl.js} +1 -1
  17. package/dist/recorder/index.js +467 -29
  18. package/dist/recorder/index.mjs +465 -28
  19. package/dist/{segment-demux-js-D-TLcHW5.mjs → segment-demux-js-BW-Q-j2A.mjs} +1 -1
  20. package/dist/{segment-demux-js-DRutggd6.js → segment-demux-js-C2jBBqGW.js} +1 -1
  21. package/dist/session-decode/decode-worker-child.js +2 -2
  22. package/dist/session-decode/decode-worker-child.mjs +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BDR5z0Z9.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dw-iwP4y.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bme2OZQp.mjs +26 -0
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Do2DEtRt.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D36MoVV1.mjs} +1 -1
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Dmw5LWmP.mjs → hostInit-DEVCEYgu.mjs} +3 -3
  30. package/dist/stream-broker/index.js +906 -423
  31. package/dist/stream-broker/index.mjs +906 -423
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-BxkCoVjw.js → worker-protocol-nAZl_Rsv.js} +1 -1
  34. package/dist/{worker-protocol-h7Smb_0i.mjs → worker-protocol-rpnr7Ldd.mjs} +1 -1
  35. package/package.json +1 -1
  36. package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
  37. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  38. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  39. package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
  43. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
  44. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  45. package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
  46. package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
  47. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  48. package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
  49. package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
  50. package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
  51. package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
  52. package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
  53. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
  54. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
  55. package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
  56. package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
  57. package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
  58. package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
  59. package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
  60. package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
  61. package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
  62. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  63. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  64. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  65. package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
  66. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  67. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
  70. package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
  71. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  72. package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
  73. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
  76. package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
  77. package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
  78. package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
  79. package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
  80. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  81. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  82. package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
  83. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  84. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  85. package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
  86. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  87. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  88. package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
  89. package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
  90. package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
  91. package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
  92. package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
  93. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  94. package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
  95. package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
  96. package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
  97. package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
  98. package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
  99. package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
  100. package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
  101. package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
  102. package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
  103. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  104. package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
  105. package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
  106. package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
  107. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
  108. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
  109. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
  110. package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
  111. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  112. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  113. package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
  114. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  115. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  116. package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
  117. package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
  118. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  119. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  120. package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
  121. package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
  122. package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
  123. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cs8bmrWP.mjs +0 -26
@@ -1,6 +1,6 @@
1
- import { Bt as lazy, Et as createEvent, F as customAction, Gt as string, Ht as number, It as _enum, L as defineCustomActions, Lt as array, Rt as boolean, St as BaseAddon, Ut as object, Vt as literal, f as DEFAULT_NATIVE_LEASE_SETTINGS, gt as errMsg, it as pipelineRunnerCapability, jt as nodePin, nt as pickNativeLeaseOverride, p as DEVICE_BACKEND_TO_FORMAT, qt as EventCategory, tt as pickDetailCropConvention, u as DEFAULT_DETAIL_CROP_CONVENTION, z as deriveDetailCropRect } from "../dist-HLAOw6Pe.mjs";
2
- import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-BH-cL_hg.mjs";
3
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-h7Smb_0i.mjs";
1
+ import { Bt as lazy, Et as createEvent, F as customAction, Gt as string, Ht as number, It as _enum, L as defineCustomActions, Lt as array, Rt as boolean, St as BaseAddon, Ut as object, Vt as literal, f as DEFAULT_NATIVE_LEASE_SETTINGS, gt as errMsg, it as pipelineRunnerCapability, jt as nodePin, nt as pickNativeLeaseOverride, p as DEVICE_BACKEND_TO_FORMAT, qt as EventCategory, tt as pickDetailCropConvention, u as DEFAULT_DETAIL_CROP_CONVENTION, z as deriveDetailCropRect } from "../dist-Wt81pN5Y.mjs";
2
+ import { i as resolveFrameViewGeometry, r as localFrameRegistry, t as attributeStall, u as getStepDefinition } from "../event-loop-stall-monitor-CtQCOZqm.mjs";
3
+ import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-rpnr7Ldd.mjs";
4
4
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-BtT5x-qP.mjs";
6
6
  import { n as withDetectionIntent } from "../restream-intent-B4BXZra7.mjs";
@@ -1,4 +1,4 @@
1
- import { Q as parseProcStatus } from "./dist-HLAOw6Pe.mjs";
1
+ import { Q as parseProcStatus } from "./dist-Wt81pN5Y.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-BZUXb5S5.js");
1
+ const require_dist = require("./dist-DOw-izoI.js");
2
2
  let node_child_process = require("node:child_process");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_dist.__toESM(node_fs);
@@ -1,7 +1,7 @@
1
- const require_dist = require("../dist-BZUXb5S5.js");
1
+ const require_dist = require("../dist-DOw-izoI.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
3
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
4
- const require_addon_utils = require("../addon-utils-BnprivSs.js");
4
+ const require_addon_utils = require("../addon-utils-DH78p79i.js");
5
5
  const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
@@ -336,6 +336,70 @@ function fragmentRangeFor(index, offsetTicks) {
336
336
  sampleIndex: chosen
337
337
  };
338
338
  }
339
+ /**
340
+ * Convert a byte budget into the SET of consecutive fragments that fits,
341
+ * starting at the fragment covering `offsetTicks` and walking FORWARD —
342
+ * matching the read direction a scrub or a sequential-forward strategy
343
+ * actually uses. Pure: `index` is the already-parsed `tfra` table, so this
344
+ * never touches the segment's payload.
345
+ *
346
+ * Greedy: adds the next fragment only while the running total stays within
347
+ * `maxBytes`. The anchor fragment is ALWAYS included, even when it alone
348
+ * exceeds `maxBytes` — {@link FragmentBudgetPlan.oversizeFragment} says so
349
+ * explicitly rather than the caller receiving an empty set and reading it as
350
+ * "nothing to read" (D49's letter, applied to a byte budget: a fallible
351
+ * computation must never silently look like an unbind).
352
+ */
353
+ function fragmentsWithinBudget(index, offsetTicks, maxBytes) {
354
+ const { samples, mfraOffset } = index;
355
+ let lo = 0;
356
+ let hi = samples.length - 1;
357
+ let anchor = 0;
358
+ while (lo <= hi) {
359
+ const mid = lo + hi >> 1;
360
+ const s = samples[mid];
361
+ if (s === void 0) break;
362
+ if (s.timeTicks <= offsetTicks) {
363
+ anchor = mid;
364
+ lo = mid + 1;
365
+ } else hi = mid - 1;
366
+ }
367
+ const endOf = (i) => samples[i + 1]?.moofOffset ?? mfraOffset;
368
+ const anchorStart = samples[anchor]?.moofOffset ?? 0;
369
+ const anchorEnd = endOf(anchor);
370
+ const fragments = [{
371
+ start: anchorStart,
372
+ end: anchorEnd,
373
+ sampleIndex: anchor
374
+ }];
375
+ let total = anchorEnd - anchorStart;
376
+ const oversizeFragment = total > maxBytes;
377
+ let i = anchor;
378
+ while (i + 1 < samples.length) {
379
+ const nextIndex = i + 1;
380
+ const nextSample = samples[nextIndex];
381
+ if (nextSample === void 0) break;
382
+ const nextStart = nextSample.moofOffset;
383
+ const nextEnd = endOf(nextIndex);
384
+ const nextSize = nextEnd - nextStart;
385
+ if (total + nextSize > maxBytes) break;
386
+ fragments.push({
387
+ start: nextStart,
388
+ end: nextEnd,
389
+ sampleIndex: nextIndex
390
+ });
391
+ total += nextSize;
392
+ i = nextIndex;
393
+ }
394
+ const last = fragments[fragments.length - 1];
395
+ return {
396
+ fragments,
397
+ start: fragments[0].start,
398
+ end: last.end,
399
+ reachesSegmentEnd: last.end >= mfraOffset,
400
+ oversizeFragment
401
+ };
402
+ }
339
403
  //#endregion
340
404
  //#region src/recorder/mfra-capture.ts
341
405
  /**
@@ -2737,6 +2801,272 @@ function activeModeForConfig(config) {
2737
2801
  return require_dist.deriveRecordingMode(config);
2738
2802
  }
2739
2803
  //#endregion
2804
+ //#region src/recorder/addon/store/read-census.ts
2805
+ /**
2806
+ * Longest window the census may be armed for, in ms.
2807
+ *
2808
+ * Ten minutes is several report windows - enough to catch a sweep that fires
2809
+ * every few minutes - and short enough that an operator who forgets about it
2810
+ * pays for ten minutes, not until the next restart.
2811
+ */
2812
+ var CENSUS_MAX_WINDOW_MS = 10 * 6e4;
2813
+ /** Store procedures worth attributing. Reads only: a write is an operator
2814
+ * gesture or a pipeline product, and neither is a mystery. */
2815
+ var CENSUSED_PROCEDURES = new Set([
2816
+ "query",
2817
+ "count",
2818
+ "get",
2819
+ "histogram",
2820
+ "getAll"
2821
+ ]);
2822
+ /**
2823
+ * Can `new Proxy` wrap this, and is it worth wrapping?
2824
+ *
2825
+ * Functions count. The generated store client is a **tRPC proxy** and every
2826
+ * intermediate node of one is CALLABLE — `typeof client.query` is `'function'`,
2827
+ * not `'object'`. A wrapper that tested only for `'object'` handed the whole
2828
+ * client back unwrapped, deployed, and reported zero `query` and zero `count`
2829
+ * call sites while the engine was recording thousands a minute against the same
2830
+ * tables. Silence read as "nobody in this addon does it".
2831
+ */
2832
+ function isProxyable(value) {
2833
+ return typeof value === "object" && value !== null || typeof value === "function";
2834
+ }
2835
+ function rowsOf(value) {
2836
+ if (Array.isArray(value)) return value.length;
2837
+ if (typeof value === "number") return value;
2838
+ if (value === void 0 || value === null) return 0;
2839
+ return 1;
2840
+ }
2841
+ /**
2842
+ * One V8 stack line reduced to `Function@line`.
2843
+ *
2844
+ * A deployed addon is ONE bundled file, so the path is identical on every frame
2845
+ * and carries no information; the line number does. A frame that parses as
2846
+ * neither keeps its trimmed text rather than being dropped - an unrecognised
2847
+ * frame is still an attribution, and dropping it would silently shift the site
2848
+ * one frame up.
2849
+ */
2850
+ function compactFrame(line) {
2851
+ const trimmed = line.trim().replace(/^at\s+/, "");
2852
+ const named = /^(.+?)\s+\((?:.*?):(\d+):\d+\)$/.exec(trimmed);
2853
+ if (named !== null) return `${named[1]}@${named[2]}`;
2854
+ const bare = /^(?:.*?):(\d+):\d+$/.exec(trimmed);
2855
+ if (bare !== null) return `@${bare[1]}`;
2856
+ return trimmed;
2857
+ }
2858
+ /** The census's view of a stack: the top {@link CENSUS_STACK_FRAMES} frames,
2859
+ * innermost first, joined by ` < ` so a line reads caller-ward. */
2860
+ function siteFromStack(stack) {
2861
+ if (stack === void 0) return "unknown";
2862
+ const frames = stack.split("\n").slice(1, 4);
2863
+ if (frames.length === 0) return "unknown";
2864
+ return frames.map(compactFrame).join(" < ");
2865
+ }
2866
+ /**
2867
+ * Armed-window call-site counter.
2868
+ *
2869
+ * Disarmed it is one comparison per read and captures nothing - the stack is
2870
+ * the whole cost, and it is not taken.
2871
+ */
2872
+ var StoreReadCensus = class {
2873
+ now;
2874
+ counts = /* @__PURE__ */ new Map();
2875
+ armedUntil = 0;
2876
+ armedAt = 0;
2877
+ constructor(now = () => Date.now()) {
2878
+ this.now = now;
2879
+ }
2880
+ /** Arm for `forMs`, clamped to {@link CENSUS_MAX_WINDOW_MS}. Re-arming
2881
+ * restarts the window and CLEARS the counts: two overlapping measurements in
2882
+ * one table is a table nobody can divide by a duration. */
2883
+ arm(forMs) {
2884
+ const at = this.now();
2885
+ this.counts.clear();
2886
+ this.armedAt = at;
2887
+ this.armedUntil = at + Math.max(0, Math.min(forMs, CENSUS_MAX_WINDOW_MS));
2888
+ }
2889
+ disarm() {
2890
+ this.armedUntil = 0;
2891
+ }
2892
+ get armed() {
2893
+ return this.now() < this.armedUntil;
2894
+ }
2895
+ /** How long the current (or just-finished) window collected, in ms. The
2896
+ * divisor for a calls-per-minute rate - a count without it is a number that
2897
+ * cannot be compared to the next one. */
2898
+ elapsedMs() {
2899
+ const at = this.now();
2900
+ return Math.max(0, Math.min(at, this.armedUntil) - this.armedAt);
2901
+ }
2902
+ /**
2903
+ * The call site of whoever called `cutoff`, or `null` when disarmed.
2904
+ *
2905
+ * **Taken BEFORE the await, always.** The first version of this class
2906
+ * captured the stack next to the counter, after the wrapped call had
2907
+ * resolved - and by then the synchronous caller is gone: V8 stitches an async
2908
+ * frame in its place and the site reads
2909
+ * `Proxy.<anonymous> < processTicksAndRejections`, which is the census
2910
+ * describing itself. It passed one of its own tests anyway, because that one
2911
+ * caller happened to RETURN the promise rather than await it. An instrument
2912
+ * that reports its own frames is worse than no instrument: the counts are
2913
+ * real, so the wrong name gets believed.
2914
+ *
2915
+ * `cutoff` is the wrapper function itself, so every frame from the wrapper
2916
+ * inwards is dropped and frame 0 is the code that asked.
2917
+ */
2918
+ siteOf(cutoff) {
2919
+ if (!this.armed) return null;
2920
+ const holder = {};
2921
+ Error.captureStackTrace(holder, cutoff);
2922
+ return siteFromStack(holder.stack);
2923
+ }
2924
+ /**
2925
+ * Count one read at an already-captured site.
2926
+ *
2927
+ * A `null` site means the census was disarmed when the call STARTED. It is
2928
+ * not counted even if it lands inside a later window - a read belongs to the
2929
+ * window that issued it.
2930
+ */
2931
+ record(op, site, result) {
2932
+ if (site === null) return;
2933
+ const key = `${op} ${site}`;
2934
+ const existing = this.counts.get(key);
2935
+ if (existing === void 0) {
2936
+ this.counts.set(key, {
2937
+ op,
2938
+ site,
2939
+ calls: 1,
2940
+ rows: rowsOf(result)
2941
+ });
2942
+ return;
2943
+ }
2944
+ existing.calls += 1;
2945
+ existing.rows += rowsOf(result);
2946
+ }
2947
+ /** Busiest first. Does NOT clear - a window may be read as many times as the
2948
+ * operator likes and always describes the same window. */
2949
+ rows() {
2950
+ return [...this.counts.values()].map((r) => ({
2951
+ op: r.op,
2952
+ site: r.site,
2953
+ calls: r.calls,
2954
+ rows: r.rows
2955
+ })).sort((a, b) => b.calls - a.calls);
2956
+ }
2957
+ };
2958
+ /**
2959
+ * Wrap a settings-store client so every read is counted.
2960
+ *
2961
+ * A `Proxy` over the generated tRPC client rather than a hand-written facade:
2962
+ * the client has a procedure per cap method and a facade would need updating
2963
+ * every time the cap gains one - which is how a diagnostic quietly stops seeing
2964
+ * half the traffic. Unrecognised procedures pass straight through.
2965
+ */
2966
+ function censusStoreClient(client, census) {
2967
+ return new Proxy(client, { get(target, prop, receiver) {
2968
+ const procedure = Reflect.get(target, prop, receiver);
2969
+ if (typeof prop !== "string" || !CENSUSED_PROCEDURES.has(prop)) return procedure;
2970
+ if (!isProxyable(procedure)) return procedure;
2971
+ const op = prop;
2972
+ return new Proxy(procedure, { get(procTarget, procProp, procReceiver) {
2973
+ const call = Reflect.get(procTarget, procProp, procReceiver);
2974
+ if (procProp !== "query" && procProp !== "mutate") return call;
2975
+ if (typeof call !== "function") return call;
2976
+ const wrapped = async (...args) => {
2977
+ const site = census.siteOf(wrapped);
2978
+ const result = await Reflect.apply(call, procTarget, args);
2979
+ census.record(op, site, result);
2980
+ return result;
2981
+ };
2982
+ return wrapped;
2983
+ } });
2984
+ } });
2985
+ }
2986
+ /** Log the census as one line, including when it collected nothing - silence
2987
+ * after an arm is indistinguishable from a census that never armed. */
2988
+ function logCensus(census, logger, topN) {
2989
+ const rows = census.rows();
2990
+ const elapsedMs = census.elapsedMs();
2991
+ const perMin = (calls) => elapsedMs === 0 ? 0 : Math.round(calls * 6e4 / elapsedMs);
2992
+ logger.info("store read census", { meta: {
2993
+ elapsedMs,
2994
+ sites: rows.length,
2995
+ calls: rows.reduce((acc, r) => acc + r.calls, 0),
2996
+ top: rows.slice(0, topN).map((r) => ({
2997
+ op: r.op,
2998
+ site: r.site,
2999
+ calls: r.calls,
3000
+ perMin: perMin(r.calls),
3001
+ rows: r.rows
3002
+ }))
3003
+ } });
3004
+ return rows;
3005
+ }
3006
+ /**
3007
+ * `armMs: 0` (or omitted) READS the window that already ran — that is how a
3008
+ * measurement is collected after its window closed, and re-arming to read
3009
+ * would throw away exactly the numbers being asked for (`StoreReadCensus.arm`
3010
+ * clears on every call).
3011
+ */
3012
+ var StoreCensusInputSchema = require_dist.object({
3013
+ armMs: require_dist.number().int().min(0).max(CENSUS_MAX_WINDOW_MS).optional(),
3014
+ limit: require_dist.number().int().positive().max(200).optional()
3015
+ });
3016
+ var StoreCensusRowSchema = require_dist.object({
3017
+ op: require_dist.string(),
3018
+ site: require_dist.string(),
3019
+ calls: require_dist.number(),
3020
+ /** Calls extrapolated to a minute, directly comparable with the live-hub
3021
+ * profiler's own `get recorder:recording-configs` rate. */
3022
+ perMin: require_dist.number(),
3023
+ rows: require_dist.number()
3024
+ });
3025
+ var StoreCensusOutputSchema = require_dist.object({
3026
+ armed: require_dist.boolean(),
3027
+ elapsedMs: require_dist.number(),
3028
+ sites: require_dist.number(),
3029
+ calls: require_dist.number(),
3030
+ rows: require_dist.array(StoreCensusRowSchema).readonly()
3031
+ });
3032
+ var recorderDebugActions = require_dist.defineCustomActions({ "debug.storeCensus": require_dist.customAction(StoreCensusInputSchema, StoreCensusOutputSchema, { auth: "admin" }) });
3033
+ /**
3034
+ * Arm-or-read, pulled out of the addon class so it is testable without
3035
+ * booting `RecorderV2Addon` — feed it a bare `StoreReadCensus` and a logger.
3036
+ *
3037
+ * The result is also logged (via {@link logCensus}), because a measurement
3038
+ * that exists only in an HTTP response is a measurement that is gone when the
3039
+ * next session asks what happened.
3040
+ */
3041
+ function handleStoreCensusAction(census, logger, input) {
3042
+ const limit = input.limit ?? 200;
3043
+ if (input.armMs !== void 0 && input.armMs > 0) {
3044
+ census.arm(input.armMs);
3045
+ return {
3046
+ armed: true,
3047
+ elapsedMs: 0,
3048
+ sites: 0,
3049
+ calls: 0,
3050
+ rows: []
3051
+ };
3052
+ }
3053
+ const rows = logCensus(census, logger, limit);
3054
+ const elapsedMs = census.elapsedMs();
3055
+ return {
3056
+ armed: census.armed,
3057
+ elapsedMs,
3058
+ sites: rows.length,
3059
+ calls: rows.reduce((acc, r) => acc + r.calls, 0),
3060
+ rows: rows.slice(0, limit).map((r) => ({
3061
+ op: r.op,
3062
+ site: r.site,
3063
+ calls: r.calls,
3064
+ perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs),
3065
+ rows: r.rows
3066
+ }))
3067
+ };
3068
+ }
3069
+ //#endregion
2740
3070
  //#region src/recorder/addon/directory-handler.ts
2741
3071
  var HOUR_MS$3 = 36e5;
2742
3072
  /** A window wider than this is refused: it is a client bug, and answering it
@@ -4094,6 +4424,18 @@ function buildMasterPlaylist(variants) {
4094
4424
  * against a real captured segment with every read recorded — "never the whole
4095
4425
  * file" is asserted in bytes.
4096
4426
  */
4427
+ /**
4428
+ * Byte budget for the `readGopBytes` fallback read, used with
4429
+ * {@link GopRangeArgs.maxBytes}.
4430
+ *
4431
+ * Same conceptual ceiling as `SCRUB_WHOLE_SEGMENT_MAX_BYTES` in
4432
+ * `stream-broker/recorded/recorded-feeder.ts` (D163, 1.5 MB) — declared here
4433
+ * independently rather than imported, because that module is the CLIENT
4434
+ * feeder (a different addon-internal concern; importing its whole dependency
4435
+ * graph for one number is the wrong direction). If D163's number moves,
4436
+ * this one moves with it.
4437
+ */
4438
+ var SCRUB_GOP_BUDGET_MAX_BYTES = 15e5;
4097
4439
  /** Ticks → ms on the video timescale, rounded like the demux anchors samples. */
4098
4440
  function ticksToMs(ticks, timescale) {
4099
4441
  return Math.round(ticks / timescale * 1e3);
@@ -4123,11 +4465,36 @@ async function readGopRange(file, args) {
4123
4465
  if (init === null) return null;
4124
4466
  const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
4125
4467
  if (videoIndex === null) return null;
4126
- const { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
4468
+ const offsetTicks = Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale;
4469
+ let start;
4470
+ let end;
4471
+ let firstSampleIndex;
4472
+ let lastSampleIndex;
4473
+ let reachesSegmentEnd;
4474
+ let oversizeFragment;
4475
+ if (args.maxBytes === void 0) {
4476
+ const single = fragmentRangeFor(videoIndex, offsetTicks);
4477
+ start = single.start;
4478
+ end = single.end;
4479
+ firstSampleIndex = single.sampleIndex;
4480
+ lastSampleIndex = single.sampleIndex;
4481
+ reachesSegmentEnd = videoIndex.samples[single.sampleIndex + 1] === void 0;
4482
+ oversizeFragment = false;
4483
+ } else {
4484
+ const plan = fragmentsWithinBudget(videoIndex, offsetTicks, args.maxBytes);
4485
+ const firstFragment = plan.fragments[0];
4486
+ const lastFragment = plan.fragments[plan.fragments.length - 1];
4487
+ start = plan.start;
4488
+ end = plan.end;
4489
+ firstSampleIndex = firstFragment.sampleIndex;
4490
+ lastSampleIndex = lastFragment.sampleIndex;
4491
+ reachesSegmentEnd = plan.reachesSegmentEnd;
4492
+ oversizeFragment = plan.oversizeFragment;
4493
+ }
4127
4494
  if (start <= 0 || end <= start || end > file.size) return null;
4128
4495
  const fragment = await read(start, end - start);
4129
- const startTicks = videoIndex.samples[sampleIndex]?.timeTicks ?? 0;
4130
- const nextTicks = videoIndex.samples[sampleIndex + 1]?.timeTicks;
4496
+ const startTicks = videoIndex.samples[firstSampleIndex]?.timeTicks ?? 0;
4497
+ const nextTicks = videoIndex.samples[lastSampleIndex + 1]?.timeTicks;
4131
4498
  const gopStartOffsetMs = ticksToMs(startTicks, init.videoTimescale);
4132
4499
  const gopDurMs = nextTicks !== void 0 ? ticksToMs(nextTicks - startTicks, init.videoTimescale) : Math.max(1, args.segmentDurMs - gopStartOffsetMs);
4133
4500
  const data = new Uint8Array(head.length + fragment.length);
@@ -4137,7 +4504,9 @@ async function readGopRange(file, args) {
4137
4504
  data,
4138
4505
  gopStartMs: args.segmentStartMs + gopStartOffsetMs,
4139
4506
  gopDurMs,
4140
- bytesRead
4507
+ bytesRead,
4508
+ reachesSegmentEnd,
4509
+ oversizeFragment
4141
4510
  };
4142
4511
  }
4143
4512
  /**
@@ -4670,6 +5039,30 @@ function resolveSegmentFile(deps, deviceId, profile, startMs) {
4670
5039
  abs: node_path.default.join(loc.root, seg.path)
4671
5040
  };
4672
5041
  }
5042
+ /**
5043
+ * The log line's `meta`, or null when `readGopBytes` served nothing worth a
5044
+ * line. Split out from the handler so the decision is testable against a
5045
+ * plain object — no real fMP4 bytes needed to prove a >1.5 MB fragment
5046
+ * triggers it.
5047
+ *
5048
+ * The single-fragment budget overshoot is the notable case, not the routine
5049
+ * one: a `high`/`mid` scrub goes through this path on every tick by design
5050
+ * (client-side `profile-degraded`, already logged once per drag on the
5051
+ * feeder), and logging every such call here would spam nothing new. A
5052
+ * fragment that alone exceeds the budget is different — it is never returned
5053
+ * empty (never-empty is {@link fragmentsWithinBudget}'s contract), but an
5054
+ * operator debugging a slow drag needs to know it happened.
5055
+ */
5056
+ function oversizeFragmentLogMeta(gop, args) {
5057
+ if (!gop.oversizeFragment) return null;
5058
+ return {
5059
+ profile: args.profile,
5060
+ startMs: args.startMs,
5061
+ epochMs: args.epochMs,
5062
+ fragmentBytes: gop.data.length,
5063
+ maxBytes: SCRUB_GOP_BUDGET_MAX_BYTES
5064
+ };
5065
+ }
4673
5066
  /** Default ranged opener: ONE `fs.open` per GOP answer serving all its reads. */
4674
5067
  async function openRangeFromDisk(absPath, size) {
4675
5068
  const handle = await node_fs.promises.open(absPath, "r");
@@ -4889,13 +5282,25 @@ function buildRecordingProvider(deps) {
4889
5282
  segmentStartMs: seg.startMs,
4890
5283
  segmentDurMs: seg.durMs,
4891
5284
  epochMs,
4892
- seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
5285
+ seededIndex: deps.mfraFor?.(deviceId, profile, startMs),
5286
+ maxBytes: SCRUB_GOP_BUDGET_MAX_BYTES
4893
5287
  });
4894
- if (gop !== null) return {
4895
- data: gop.data,
4896
- gopStartMs: gop.gopStartMs,
4897
- gopDurMs: gop.gopDurMs
4898
- };
5288
+ if (gop !== null) {
5289
+ const oversizeMeta = oversizeFragmentLogMeta(gop, {
5290
+ profile,
5291
+ startMs,
5292
+ epochMs
5293
+ });
5294
+ if (oversizeMeta !== null) deps.logger.warn("recording: readGopBytes served one fragment that alone exceeds the scrub budget", {
5295
+ tags: { deviceId },
5296
+ meta: oversizeMeta
5297
+ });
5298
+ return {
5299
+ data: gop.data,
5300
+ gopStartMs: gop.gopStartMs,
5301
+ gopDurMs: gop.gopDurMs
5302
+ };
5303
+ }
4899
5304
  } finally {
4900
5305
  await file.close();
4901
5306
  }
@@ -11176,6 +11581,23 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
11176
11581
  retentionSweeper = null;
11177
11582
  /** Reversible migration pause lease; never reflected in RecordingConfig. */
11178
11583
  storageMigrationLeaseId = null;
11584
+ /**
11585
+ * Call-site attribution for `recorder:recording-configs` / `-exports` /
11586
+ * `-ops-log` row reads (`store/read-census.ts`).
11587
+ *
11588
+ * Disarmed by default; armed for a bounded window through
11589
+ * `debug.storeCensus`. It exists because the live-hub profiler names a
11590
+ * TABLE — `get recorder:recording-configs` ran 581-892 calls/min on
11591
+ * 2026-08-26, finding a row only ~56% of the time — and cannot say WHICH of
11592
+ * `loadDeviceConfig`'s several cap-handler call sites (`getStatus`,
11593
+ * `getDeviceConfig`, `rescanStorage`, `pruneFootage`) is issuing them.
11594
+ * Picking the reader that LOOKS like a loop has cost this repo half a day
11595
+ * twice, wrong both times — this measures instead.
11596
+ */
11597
+ readCensus = new StoreReadCensus();
11598
+ /** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
11599
+ * call shares ONE census instead of each wrapping the raw client afresh. */
11600
+ censusedSettingsStoreClient = null;
11179
11601
  constructor() {
11180
11602
  super({ ...DEFAULT_CONFIG });
11181
11603
  }
@@ -11580,20 +12002,24 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
11580
12002
  }
11581
12003
  });
11582
12004
  warnLostBands(this.configStore(), this.ctx.logger);
11583
- return { providers: [
11584
- {
11585
- capability: require_dist.recordingCapability,
11586
- provider: recordingProvider
11587
- },
11588
- {
11589
- capability: require_dist.storageEvictableCapability,
11590
- provider: evictableProvider
11591
- },
11592
- {
11593
- capability: require_dist.recordingExportCapability,
11594
- provider: exportProvider
11595
- }
11596
- ] };
12005
+ return {
12006
+ providers: [
12007
+ {
12008
+ capability: require_dist.recordingCapability,
12009
+ provider: recordingProvider
12010
+ },
12011
+ {
12012
+ capability: require_dist.storageEvictableCapability,
12013
+ provider: evictableProvider
12014
+ },
12015
+ {
12016
+ capability: require_dist.recordingExportCapability,
12017
+ provider: exportProvider
12018
+ }
12019
+ ],
12020
+ customActions: recorderDebugActions,
12021
+ actionHandlers: { "debug.storeCensus": async (input) => handleStoreCensusAction(this.readCensus, this.ctx.logger.child("Census"), input) }
12022
+ };
11597
12023
  }
11598
12024
  async onShutdown() {
11599
12025
  this.eventUnsub?.();
@@ -12242,17 +12668,29 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
12242
12668
  this.ctx.logger.warn("recorder: retiring the flattened settings keys failed", { meta: { error: require_dist.errMsg(err) } });
12243
12669
  }
12244
12670
  }
12671
+ /**
12672
+ * `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
12673
+ *
12674
+ * LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
12675
+ * runs, so this cannot be a field initialiser. The wrap is permanent and
12676
+ * cheap when disarmed (one `get` trap and a comparison) — what is armed and
12677
+ * disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
12678
+ */
12679
+ censusedSettingsStore() {
12680
+ this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
12681
+ return this.censusedSettingsStoreClient;
12682
+ }
12245
12683
  configStore() {
12246
- return recordingConfigsState(this.ctx.api.settingsStore, this.ctx.logger);
12684
+ return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
12247
12685
  }
12248
12686
  /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
12249
12687
  * wrapper as the config store). */
12250
12688
  exportsStore() {
12251
- return recordingExportsState(this.ctx.api.settingsStore, this.ctx.logger);
12689
+ return recordingExportsState(this.censusedSettingsStore(), this.ctx.logger);
12252
12690
  }
12253
12691
  /** Durable handle over the bounded recordings ops-log ring. */
12254
12692
  opsLogStore() {
12255
- return recordingOpsLogState(this.ctx.api.settingsStore, this.ctx.logger);
12693
+ return recordingOpsLogState(this.censusedSettingsStore(), this.ctx.logger);
12256
12694
  }
12257
12695
  /** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
12258
12696
  placementStore() {