@camstack/addon-pipeline 1.2.192 → 1.2.193

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/detection-pipeline/index.js +7 -7
  4. package/dist/detection-pipeline/index.mjs +5 -5
  5. package/dist/{dist-CXO_Fmx6.js → dist-HG5geL9t.js} +129 -1
  6. package/dist/{dist-B8J7wrtY.mjs → dist-mYDK58Jp.mjs} +118 -2
  7. package/dist/{lazy-sharp-o-aNNXuz.js → lazy-sharp-DmAwZ0s9.js} +1 -1
  8. package/dist/{local-frame-registry-BDeDpYQk.mjs → local-frame-registry-av9ObDG0.mjs} +1 -1
  9. package/dist/{local-frame-registry-P7tSfvaY.js → local-frame-registry-x3m6SNqf.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-DF98pVP7.js → node--my8JTOW.js} +1 -1
  13. package/dist/{node-BSbB0or0.mjs → node-xRsaPwBa.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +255 -221
  15. package/dist/pipeline-runner/index.mjs +253 -219
  16. package/dist/{process-memory-BZTgJyE8.mjs → process-memory-BDHR3lxe.mjs} +1 -1
  17. package/dist/{process-memory-BVMk3gfY.js → process-memory-CoCSce63.js} +1 -1
  18. package/dist/recorder/index.js +319 -8
  19. package/dist/recorder/index.mjs +319 -8
  20. package/dist/{segment-demux-js-CLaSHw0n.js → segment-demux-js-CBynn9Gh.js} +1 -1
  21. package/dist/{segment-demux-js-0L1EF4Fg.mjs → segment-demux-js-CXvgKfsn.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +2 -2
  23. package/dist/session-decode/decode-worker-child.mjs +1 -1
  24. package/dist/stream-broker/_stub.js +1 -1
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BmNfrv_U.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CUjbLOm9.mjs} +3 -3
  26. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D8AK-_F6.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B_NVJXAy.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-DQLtjntf.mjs → hostInit-DnBCyuSA.mjs} +3 -3
  30. package/dist/stream-broker/index.js +93 -43
  31. package/dist/stream-broker/index.mjs +93 -43
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-WiXU2VuW.mjs → worker-protocol-C0R1DDC0.mjs} +1 -1
  34. package/dist/{worker-protocol-BJzhw3t0.js → worker-protocol-DCsi4YS1.js} +1 -1
  35. package/package.json +1 -1
@@ -2,67 +2,22 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CXO_Fmx6.js");
6
- const require_node = require("../node-DF98pVP7.js");
7
- const require_local_frame_registry = require("../local-frame-registry-P7tSfvaY.js");
8
- const require_worker_protocol = require("../worker-protocol-BJzhw3t0.js");
5
+ const require_dist = require("../dist-HG5geL9t.js");
6
+ const require_node = require("../node--my8JTOW.js");
7
+ const require_local_frame_registry = require("../local-frame-registry-x3m6SNqf.js");
8
+ const require_worker_protocol = require("../worker-protocol-DCsi4YS1.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-o-aNNXuz.js");
10
+ const require_lazy_sharp = require("../lazy-sharp-DmAwZ0s9.js");
11
11
  const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
12
12
  const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
13
13
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
14
+ let node_os = require("node:os");
15
+ node_os = require_dist.__toESM(node_os);
14
16
  let node_child_process = require("node:child_process");
15
17
  let sharp = require("sharp");
16
18
  sharp = require_dist.__toESM(sharp);
17
19
  let node_url = require("node:url");
18
20
  let node_perf_hooks = require("node:perf_hooks");
19
- let node_os = require("node:os");
20
- node_os = require_dist.__toESM(node_os);
21
- //#region src/pipeline-runner/first-detection-frame.ts
22
- /**
23
- * How long a camera waits, after `attachCamera`, before a decoded frame
24
- * actually reaches the detection queue.
25
- *
26
- * The orchestrator already measures `session motion → attach latency` and it
27
- * is small — 11, 13 and 18 ms across three consecutive sessions on the same
28
- * camera. What nothing measured is the NEXT leg: from the attach to the first
29
- * frame the detector is given. An operator reporting "detection starts late"
30
- * is describing that leg, and answering them required reading the timestamps
31
- * of unrelated INFO lines and inferring — which produced a wrong first answer
32
- * (the absence of a `frame processed` line means the frame produced nothing
33
- * notable, not that no frame arrived).
34
- *
35
- * One number, once per attach, so a slow start is a value rather than an
36
- * inference.
37
- */
38
- /** Pending attach marks, keyed by device. */
39
- var FirstDetectionFrameTimer = class {
40
- attachedAt = /* @__PURE__ */ new Map();
41
- /** A camera attached. Replaces any pending mark: the newest attach is the
42
- * one the next frame belongs to. */
43
- markAttached(deviceId, nowMs) {
44
- this.attachedAt.set(deviceId, nowMs);
45
- }
46
- /**
47
- * A decoded frame was admitted to the detection queue. Returns the elapsed
48
- * ms since the attach the FIRST time it is called after one, and `null`
49
- * every time after — so the caller logs once per attach, not once per frame.
50
- */
51
- takeLatency(deviceId, nowMs) {
52
- const at = this.attachedAt.get(deviceId);
53
- if (at === void 0) return null;
54
- this.attachedAt.delete(deviceId);
55
- return Math.max(0, nowMs - at);
56
- }
57
- /** A camera detached with no frame ever admitted — drop the mark so the
58
- * NEXT attach is not measured from this one. Returns whether one was
59
- * pending, which is itself the interesting fact: a session that ran and
60
- * fed the detector nothing. */
61
- forget(deviceId) {
62
- return this.attachedAt.delete(deviceId);
63
- }
64
- };
65
- //#endregion
66
21
  //#region src/session-decode/fork-decode-worker.ts
67
22
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
68
23
  var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
@@ -527,6 +482,57 @@ var NativeCropRegistry = class {
527
482
  }
528
483
  };
529
484
  //#endregion
485
+ //#region src/session-decode/node-decode-budget.ts
486
+ /**
487
+ * The decode RAM budget and worker cap THIS node runs under.
488
+ *
489
+ * `nodeDecodeBudgetMb` and `maxLiveDecodeWorkers` are cluster settings — one
490
+ * number for a 64 GB hub and a 16 GB Mac alike. Until 2026-09-05 the shipped
491
+ * 8192 MB was the hub's number applied everywhere; with the footprint fixed
492
+ * (D361) that admits 8 workers on the Mac, whose workers measure RSS p90
493
+ * 952 MB on a machine already at 80% — 7.6 GB of decode on 16 GB is a swap
494
+ * away from a stall — while the hub leaves 56 GB idle.
495
+ *
496
+ * Three sources, in order, and the winner is NAMED so a log line can say why:
497
+ * 1. the node's own agent setting (`decodeBudgetMb` / `maxLiveDecodeWorkers`
498
+ * in `AgentPipelineSettings`) — the operator sized this node;
499
+ * 2. the cluster setting when the operator MOVED it off the shipped default
500
+ * (a value equal to the default reads as "nobody chose", the same rule the
501
+ * native-lease knobs use, `readKnob`);
502
+ * 3. otherwise a share of the node's RAM, clamped — the shipped default
503
+ * becomes a per-node derivation instead of the hub's number.
504
+ */
505
+ var DECODE_BUDGET_RAM_SHARE = .5;
506
+ var DECODE_BUDGET_MIN_MB = 2048;
507
+ var DECODE_BUDGET_MAX_MB = 16384;
508
+ function derivedDecodeBudgetMb(totalMemMb) {
509
+ const share = Math.round(totalMemMb * DECODE_BUDGET_RAM_SHARE);
510
+ return Math.max(DECODE_BUDGET_MIN_MB, Math.min(DECODE_BUDGET_MAX_MB, share));
511
+ }
512
+ function resolveNodeDecodeLimits(input) {
513
+ const budget = input.node.decodeBudgetMb !== void 0 ? {
514
+ budgetMb: input.node.decodeBudgetMb,
515
+ budgetSource: "node"
516
+ } : input.clusterBudgetMb !== input.defaultBudgetMb ? {
517
+ budgetMb: input.clusterBudgetMb,
518
+ budgetSource: "cluster"
519
+ } : {
520
+ budgetMb: derivedDecodeBudgetMb(input.totalMemMb),
521
+ budgetSource: "derived"
522
+ };
523
+ const workers = input.node.maxLiveDecodeWorkers !== void 0 ? {
524
+ maxWorkers: input.node.maxLiveDecodeWorkers,
525
+ maxWorkersSource: "node"
526
+ } : {
527
+ maxWorkers: input.clusterMaxWorkers,
528
+ maxWorkersSource: "cluster"
529
+ };
530
+ return {
531
+ ...budget,
532
+ ...workers
533
+ };
534
+ }
535
+ //#endregion
530
536
  //#region src/session-decode/retained-content-fallback.ts
531
537
  /**
532
538
  * Honest ≤640 RAM fallback for `getNativeCrop` full-frame misses.
@@ -928,55 +934,6 @@ function cropRgb(frame, bbox, maxWidth) {
928
934
  };
929
935
  }
930
936
  //#endregion
931
- //#region src/pipeline-runner/frame-cutter-binding.ts
932
- /** The per-frame binding key — the same identity the retention handle carries. */
933
- function frameCutterKey(handle) {
934
- return `${handle.shmId}#${handle.slot}#${handle.seq}`;
935
- }
936
- var FrameCutterBinding = class {
937
- max;
938
- map = /* @__PURE__ */ new Map();
939
- constructor(max) {
940
- this.max = max;
941
- }
942
- /** Bind at delivery. Re-binding the same handle replaces without stranding the first. */
943
- bind(handle, cutter) {
944
- const key = frameCutterKey(handle);
945
- this.map.delete(key);
946
- this.map.set(key, cutter);
947
- while (this.map.size > this.max) {
948
- const oldest = this.map.keys().next().value;
949
- if (oldest === void 0) break;
950
- const stale = this.map.get(oldest);
951
- this.map.delete(oldest);
952
- release(stale);
953
- }
954
- }
955
- /** Consume the binding for a frame whose result arrived. `undefined` when already ended. */
956
- take(handle) {
957
- const key = frameCutterKey(handle);
958
- const cutter = this.map.get(key);
959
- this.map.delete(key);
960
- return cutter;
961
- }
962
- /**
963
- * End the hold of a frame the runner let go without a result. A no-op for a
964
- * frame whose result already consumed the binding, so the two paths cannot
965
- * double-release.
966
- */
967
- drop(handle) {
968
- release(this.take(handle));
969
- }
970
- get size() {
971
- return this.map.size;
972
- }
973
- };
974
- function release(cutter) {
975
- try {
976
- cutter?.release();
977
- } catch {}
978
- }
979
- //#endregion
980
937
  //#region src/session-decode/session-decode-coordinator.ts
981
938
  /** How long to wait after `kill()` before escalating to `kill('SIGKILL')`. */
982
939
  var SIGKILL_FALLBACK_MS = 1e3;
@@ -2927,6 +2884,168 @@ function summarizeDeviceLoad(attachedDeviceKeys) {
2927
2884
  })).toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
2928
2885
  }
2929
2886
  //#endregion
2887
+ //#region src/pipeline-runner/failure-report.ts
2888
+ /**
2889
+ * What `pipeline-runner` reports through `failure-contribution` — the inference
2890
+ * deadline, per camera, with the frames that made it as the denominator.
2891
+ *
2892
+ * ## The number that was never divided
2893
+ *
2894
+ * ~120 `inference timed out — permit released, result discarded` lines in six
2895
+ * hours across seven cameras, on 2026-08-28, never investigated. The line has
2896
+ * carried `tags: { deviceId }` for a while, so the count per camera was always
2897
+ * available; what was not available was the number to divide it by. 120
2898
+ * timeouts out of 120 000 admitted frames and 120 out of 1 200 are opposite
2899
+ * findings and produce identical log volume.
2900
+ *
2901
+ * So the counter takes BOTH outcomes of the same await, from the same place in
2902
+ * `awaitInferenceBounded`. A success counted anywhere else would eventually
2903
+ * drift from the failure and turn the ratio into fiction.
2904
+ *
2905
+ * ## Why there is no `variant` yet
2906
+ *
2907
+ * `failure-contribution` has a `variant` slot exactly so "which camera AND
2908
+ * which model" is one read, and the operator asked for it. The runner cannot
2909
+ * fill it honestly: `awaitInferenceBounded` wraps
2910
+ * `config.processFrame(deviceId, frameInput, executorHandle)`, which runs the
2911
+ * camera's WHOLE pipeline graph behind one deadline. Nothing at this seam knows
2912
+ * which step was still running when the 20 s expired — `CameraRegistration`
2913
+ * carries fps and mode, not models.
2914
+ *
2915
+ * Naming the step means the executor attributing the deadline to the step that
2916
+ * held it, and reporting THAT. Emitting a `variant` the runner guessed (the
2917
+ * camera's configured graph id, say) would be worse than leaving it absent: it
2918
+ * would look like an attribution and be a label. Absent, never invented — the
2919
+ * same rule `load-contribution` states for a measurement nobody made.
2920
+ */
2921
+ /** One bounded inference deadline per admitted detection frame. */
2922
+ var FAMILY_INFERENCE = "inference";
2923
+ /** The call did not return inside `inferenceTimeoutMs`. */
2924
+ var REASON_INFERENCE_TIMEOUT = "timeout";
2925
+ /**
2926
+ * The runner's `failure-contribution` counters.
2927
+ *
2928
+ * One instance per runner process, read (never drained) by the provider.
2929
+ */
2930
+ var RunnerFailureReport = class {
2931
+ now;
2932
+ counters;
2933
+ constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
2934
+ this.now = now;
2935
+ this.counters = counters;
2936
+ }
2937
+ /** One bounded inference call. `reason` absent = it returned in time. */
2938
+ noteInference(deviceId, reason) {
2939
+ this.counters.note({
2940
+ deviceId,
2941
+ family: FAMILY_INFERENCE,
2942
+ ...reason !== void 0 ? { reason } : {}
2943
+ }, this.now());
2944
+ }
2945
+ /** The `failure-contribution` provider's payload. Reads, never resets. */
2946
+ list() {
2947
+ return this.counters.snapshot(this.now());
2948
+ }
2949
+ /** Addon disposal. */
2950
+ clear() {
2951
+ this.counters.clear();
2952
+ }
2953
+ };
2954
+ //#endregion
2955
+ //#region src/pipeline-runner/first-detection-frame.ts
2956
+ /**
2957
+ * How long a camera waits, after `attachCamera`, before a decoded frame
2958
+ * actually reaches the detection queue.
2959
+ *
2960
+ * The orchestrator already measures `session motion → attach latency` and it
2961
+ * is small — 11, 13 and 18 ms across three consecutive sessions on the same
2962
+ * camera. What nothing measured is the NEXT leg: from the attach to the first
2963
+ * frame the detector is given. An operator reporting "detection starts late"
2964
+ * is describing that leg, and answering them required reading the timestamps
2965
+ * of unrelated INFO lines and inferring — which produced a wrong first answer
2966
+ * (the absence of a `frame processed` line means the frame produced nothing
2967
+ * notable, not that no frame arrived).
2968
+ *
2969
+ * One number, once per attach, so a slow start is a value rather than an
2970
+ * inference.
2971
+ */
2972
+ /** Pending attach marks, keyed by device. */
2973
+ var FirstDetectionFrameTimer = class {
2974
+ attachedAt = /* @__PURE__ */ new Map();
2975
+ /** A camera attached. Replaces any pending mark: the newest attach is the
2976
+ * one the next frame belongs to. */
2977
+ markAttached(deviceId, nowMs) {
2978
+ this.attachedAt.set(deviceId, nowMs);
2979
+ }
2980
+ /**
2981
+ * A decoded frame was admitted to the detection queue. Returns the elapsed
2982
+ * ms since the attach the FIRST time it is called after one, and `null`
2983
+ * every time after — so the caller logs once per attach, not once per frame.
2984
+ */
2985
+ takeLatency(deviceId, nowMs) {
2986
+ const at = this.attachedAt.get(deviceId);
2987
+ if (at === void 0) return null;
2988
+ this.attachedAt.delete(deviceId);
2989
+ return Math.max(0, nowMs - at);
2990
+ }
2991
+ /** A camera detached with no frame ever admitted — drop the mark so the
2992
+ * NEXT attach is not measured from this one. Returns whether one was
2993
+ * pending, which is itself the interesting fact: a session that ran and
2994
+ * fed the detector nothing. */
2995
+ forget(deviceId) {
2996
+ return this.attachedAt.delete(deviceId);
2997
+ }
2998
+ };
2999
+ //#endregion
3000
+ //#region src/pipeline-runner/frame-cutter-binding.ts
3001
+ /** The per-frame binding key — the same identity the retention handle carries. */
3002
+ function frameCutterKey(handle) {
3003
+ return `${handle.shmId}#${handle.slot}#${handle.seq}`;
3004
+ }
3005
+ var FrameCutterBinding = class {
3006
+ max;
3007
+ map = /* @__PURE__ */ new Map();
3008
+ constructor(max) {
3009
+ this.max = max;
3010
+ }
3011
+ /** Bind at delivery. Re-binding the same handle replaces without stranding the first. */
3012
+ bind(handle, cutter) {
3013
+ const key = frameCutterKey(handle);
3014
+ this.map.delete(key);
3015
+ this.map.set(key, cutter);
3016
+ while (this.map.size > this.max) {
3017
+ const oldest = this.map.keys().next().value;
3018
+ if (oldest === void 0) break;
3019
+ const stale = this.map.get(oldest);
3020
+ this.map.delete(oldest);
3021
+ release(stale);
3022
+ }
3023
+ }
3024
+ /** Consume the binding for a frame whose result arrived. `undefined` when already ended. */
3025
+ take(handle) {
3026
+ const key = frameCutterKey(handle);
3027
+ const cutter = this.map.get(key);
3028
+ this.map.delete(key);
3029
+ return cutter;
3030
+ }
3031
+ /**
3032
+ * End the hold of a frame the runner let go without a result. A no-op for a
3033
+ * frame whose result already consumed the binding, so the two paths cannot
3034
+ * double-release.
3035
+ */
3036
+ drop(handle) {
3037
+ release(this.take(handle));
3038
+ }
3039
+ get size() {
3040
+ return this.map.size;
3041
+ }
3042
+ };
3043
+ function release(cutter) {
3044
+ try {
3045
+ cutter?.release();
3046
+ } catch {}
3047
+ }
3048
+ //#endregion
2930
3049
  //#region src/pipeline-runner/frame-lazy-metrics.ts
2931
3050
  function emptyFrameLazyCounters() {
2932
3051
  return {
@@ -3499,57 +3618,6 @@ var NodeDecodeLimitsSource = class {
3499
3618
  }
3500
3619
  };
3501
3620
  //#endregion
3502
- //#region src/session-decode/node-decode-budget.ts
3503
- /**
3504
- * The decode RAM budget and worker cap THIS node runs under.
3505
- *
3506
- * `nodeDecodeBudgetMb` and `maxLiveDecodeWorkers` are cluster settings — one
3507
- * number for a 64 GB hub and a 16 GB Mac alike. Until 2026-09-05 the shipped
3508
- * 8192 MB was the hub's number applied everywhere; with the footprint fixed
3509
- * (D361) that admits 8 workers on the Mac, whose workers measure RSS p90
3510
- * 952 MB on a machine already at 80% — 7.6 GB of decode on 16 GB is a swap
3511
- * away from a stall — while the hub leaves 56 GB idle.
3512
- *
3513
- * Three sources, in order, and the winner is NAMED so a log line can say why:
3514
- * 1. the node's own agent setting (`decodeBudgetMb` / `maxLiveDecodeWorkers`
3515
- * in `AgentPipelineSettings`) — the operator sized this node;
3516
- * 2. the cluster setting when the operator MOVED it off the shipped default
3517
- * (a value equal to the default reads as "nobody chose", the same rule the
3518
- * native-lease knobs use, `readKnob`);
3519
- * 3. otherwise a share of the node's RAM, clamped — the shipped default
3520
- * becomes a per-node derivation instead of the hub's number.
3521
- */
3522
- var DECODE_BUDGET_RAM_SHARE = .5;
3523
- var DECODE_BUDGET_MIN_MB = 2048;
3524
- var DECODE_BUDGET_MAX_MB = 16384;
3525
- function derivedDecodeBudgetMb(totalMemMb) {
3526
- const share = Math.round(totalMemMb * DECODE_BUDGET_RAM_SHARE);
3527
- return Math.max(DECODE_BUDGET_MIN_MB, Math.min(DECODE_BUDGET_MAX_MB, share));
3528
- }
3529
- function resolveNodeDecodeLimits(input) {
3530
- const budget = input.node.decodeBudgetMb !== void 0 ? {
3531
- budgetMb: input.node.decodeBudgetMb,
3532
- budgetSource: "node"
3533
- } : input.clusterBudgetMb !== input.defaultBudgetMb ? {
3534
- budgetMb: input.clusterBudgetMb,
3535
- budgetSource: "cluster"
3536
- } : {
3537
- budgetMb: derivedDecodeBudgetMb(input.totalMemMb),
3538
- budgetSource: "derived"
3539
- };
3540
- const workers = input.node.maxLiveDecodeWorkers !== void 0 ? {
3541
- maxWorkers: input.node.maxLiveDecodeWorkers,
3542
- maxWorkersSource: "node"
3543
- } : {
3544
- maxWorkers: input.clusterMaxWorkers,
3545
- maxWorkersSource: "cluster"
3546
- };
3547
- return {
3548
- ...budget,
3549
- ...workers
3550
- };
3551
- }
3552
- //#endregion
3553
3621
  //#region src/session-decode/on-demand-jpeg-fallback.ts
3554
3622
  /** Single content-sized JPEG requested after a detection, never a continuous raw. */
3555
3623
  var ON_DEMAND_JPEG_VIEW = {
@@ -3895,74 +3963,6 @@ function packageZoneUnion(polygons) {
3895
3963
  };
3896
3964
  }
3897
3965
  //#endregion
3898
- //#region src/pipeline-runner/failure-report.ts
3899
- /**
3900
- * What `pipeline-runner` reports through `failure-contribution` — the inference
3901
- * deadline, per camera, with the frames that made it as the denominator.
3902
- *
3903
- * ## The number that was never divided
3904
- *
3905
- * ~120 `inference timed out — permit released, result discarded` lines in six
3906
- * hours across seven cameras, on 2026-08-28, never investigated. The line has
3907
- * carried `tags: { deviceId }` for a while, so the count per camera was always
3908
- * available; what was not available was the number to divide it by. 120
3909
- * timeouts out of 120 000 admitted frames and 120 out of 1 200 are opposite
3910
- * findings and produce identical log volume.
3911
- *
3912
- * So the counter takes BOTH outcomes of the same await, from the same place in
3913
- * `awaitInferenceBounded`. A success counted anywhere else would eventually
3914
- * drift from the failure and turn the ratio into fiction.
3915
- *
3916
- * ## Why there is no `variant` yet
3917
- *
3918
- * `failure-contribution` has a `variant` slot exactly so "which camera AND
3919
- * which model" is one read, and the operator asked for it. The runner cannot
3920
- * fill it honestly: `awaitInferenceBounded` wraps
3921
- * `config.processFrame(deviceId, frameInput, executorHandle)`, which runs the
3922
- * camera's WHOLE pipeline graph behind one deadline. Nothing at this seam knows
3923
- * which step was still running when the 20 s expired — `CameraRegistration`
3924
- * carries fps and mode, not models.
3925
- *
3926
- * Naming the step means the executor attributing the deadline to the step that
3927
- * held it, and reporting THAT. Emitting a `variant` the runner guessed (the
3928
- * camera's configured graph id, say) would be worse than leaving it absent: it
3929
- * would look like an attribution and be a label. Absent, never invented — the
3930
- * same rule `load-contribution` states for a measurement nobody made.
3931
- */
3932
- /** One bounded inference deadline per admitted detection frame. */
3933
- var FAMILY_INFERENCE = "inference";
3934
- /** The call did not return inside `inferenceTimeoutMs`. */
3935
- var REASON_INFERENCE_TIMEOUT = "timeout";
3936
- /**
3937
- * The runner's `failure-contribution` counters.
3938
- *
3939
- * One instance per runner process, read (never drained) by the provider.
3940
- */
3941
- var RunnerFailureReport = class {
3942
- now;
3943
- counters;
3944
- constructor(now = Date.now, counters = new require_dist.FailureCounters()) {
3945
- this.now = now;
3946
- this.counters = counters;
3947
- }
3948
- /** One bounded inference call. `reason` absent = it returned in time. */
3949
- noteInference(deviceId, reason) {
3950
- this.counters.note({
3951
- deviceId,
3952
- family: FAMILY_INFERENCE,
3953
- ...reason !== void 0 ? { reason } : {}
3954
- }, this.now());
3955
- }
3956
- /** The `failure-contribution` provider's payload. Reads, never resets. */
3957
- list() {
3958
- return this.counters.snapshot(this.now());
3959
- }
3960
- /** Addon disposal. */
3961
- clear() {
3962
- this.counters.clear();
3963
- }
3964
- };
3965
- //#endregion
3966
3966
  //#region src/pipeline-runner/frame-governor.ts
3967
3967
  var FrameGovernor = class {
3968
3968
  config;
@@ -5272,6 +5272,40 @@ function isUsable(bbox) {
5272
5272
  //#endregion
5273
5273
  //#region src/pipeline-runner/index.ts
5274
5274
  /**
5275
+ * addon-pipeline-runner — per-node detection scheduler.
5276
+ *
5277
+ * Lives on every "vision node" (hub if hub is one, every agent that runs
5278
+ * decoders/cameras). Receives camera assignments from the hub-side
5279
+ * `addon-pipeline-orchestrator` via cap calls, owns the per-camera frame
5280
+ * queues + semaphore + motion → detection phase machine, calls the local
5281
+ * `motion-detection` and `pipeline-executor` capabilities for the actual
5282
+ * work, and emits typed events on the bus
5283
+ * (`detection.motion-analysis`, `pipeline.inference-result`, `pipeline.trace`).
5284
+ *
5285
+ * Design notes:
5286
+ *
5287
+ * - All cap inputs/outputs are serializable so the runner is transparently
5288
+ * routable across the cluster: when the runner is co-located with the
5289
+ * broker/decoder/motion/detection (the default and recommended deploy),
5290
+ * capability lookups return live singletons and frame data never crosses
5291
+ * a process boundary. When a deploy splits the runner from the executor
5292
+ * (rare), the lookup returns null and the runner falls back to
5293
+ * `this.ctx.api` (broker proxy) at the cost of serialization.
5294
+ *
5295
+ * - The scheduler core (`PipelineRunner`) is callback-driven and framework
5296
+ * agnostic. The addon class injects the inference + motion callbacks at
5297
+ * construction time, capturing references to the relevant providers
5298
+ * lazily on each invocation.
5299
+ *
5300
+ * - Result post-processing (analysis pipeline, class filters, notifications)
5301
+ * is intentionally NOT done by the runner. The runner emits the raw
5302
+ * inference output as a `pipeline.inference-result` event on the bus;
5303
+ * a hub-side subscriber consumes the event and runs the post-processing
5304
+ * pipeline. This keeps the runner stateless w.r.t. detection-side
5305
+ * business logic and makes it possible to deploy the post-processor
5306
+ * independently from the inference workers.
5307
+ */
5308
+ /**
5275
5309
  * How often a device's package zone-union geometry is re-read. An operator
5276
5310
  * redrawing a package zone takes effect within one interval; the read is a
5277
5311
  * cross-addon cap call reached from the per-frame package gate, so it must be
@@ -7830,6 +7864,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7830
7864
  workerToRunnerBytes: 0
7831
7865
  });
7832
7866
  const admitted = current.detection && runner.enqueueReservedDetectionFrame(config.deviceId, frame, handle);
7867
+ if (admitted) nativeCrop?.markInferred();
7833
7868
  if (current.motion) {
7834
7869
  const motion = await resolveMotionFrameView(require_local_frame_registry.localFrameRegistry, frame.frameRef);
7835
7870
  if (motion.kind === "view") runner.enqueueMotionFrame(config.deviceId, {
@@ -7841,7 +7876,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7841
7876
  capturedAt: frame.capturedAt
7842
7877
  }, void 0);
7843
7878
  }
7844
- if (admitted) nativeCrop?.markInferred();
7845
7879
  return admitted;
7846
7880
  }
7847
7881
  } : { inlineViewSpec: SPLIT_DETECTION_VIEW },