@camstack/addon-post-analysis 1.2.212 → 1.2.214

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.
@@ -22483,6 +22483,22 @@ var occupancyRecheckFramesField = {
22483
22483
  step: 1
22484
22484
  };
22485
22485
  /**
22486
+ * Frames one PRE-ROLL catch-up burst may send to inference at session open.
22487
+ *
22488
+ * The default of 12 over a ~4 s retained window is one frame per ~333 ms of
22489
+ * footage, and costs ~480 ms of ONE inference permit at the 40 ms/frame this
22490
+ * fleet measures — one-shot, and only ever taken while a permit would still be
22491
+ * spare for live. `0` turns the burst off entirely (the history is still
22492
+ * decoded, because that is how the dial reaches a decodable GOP, and is then
22493
+ * discarded — the pre-D411 behaviour).
22494
+ */
22495
+ var preRollInferenceFramesField = {
22496
+ min: 0,
22497
+ max: 30,
22498
+ default: 12,
22499
+ step: 1
22500
+ };
22501
+ /**
22486
22502
  * Source enum for motion signals fed to the runner. Extensible — add
22487
22503
  * new variants here when new motion-trigger paths are wired in
22488
22504
  * (`wasm-cross-camera`, `event-bus-relay`, etc.). The runner uses
@@ -22723,6 +22739,31 @@ var RunnerCameraConfigSchema = object({
22723
22739
  * to every occupancy rule until something moved in front of it. The churn is
22724
22740
  * now paid on the interval instead — see `occupancyRecheckSecField`.
22725
22741
  */
22742
+ /**
22743
+ * Infer the session's PRE-ROLL — the retained history the restreamer replays
22744
+ * at session open — instead of decoding it and throwing it away.
22745
+ *
22746
+ * DEFAULT `true`, and the default is the argument. This is not a new
22747
+ * capability being offered cautiously: the pre-roll is ALREADY requested,
22748
+ * already served, already decoded and already paid for on every on-motion
22749
+ * detection session that asks for history. What shipped was a last-moment
22750
+ * discard — `FrameSlot`'s throttle compares wall clocks, and ~4 s of media
22751
+ * arriving inside a few hundred ms of wall clock looks to it like one frame's
22752
+ * worth. Device 617, 2026-09-08: a re-run of the SAME model over the stored
22753
+ * recording scored the subject `vehicle 0.5669` — above the 0.50 floor —
22754
+ * 1.68 s BEFORE the live track was born, on a frame that was in the pre-roll,
22755
+ * was decoded, and was freed.
22756
+ *
22757
+ * Shipping that as opt-in would ship the bug: an operator cannot discover a
22758
+ * setting whose absence looks exactly like a camera that noticed the cyclist
22759
+ * late. What makes ON safe is not a switch but the BOUND —
22760
+ * `preRollInferenceFrames`, a wall-clock ceiling, and a lane that never takes
22761
+ * the last inference permit — so the switch exists for the camera where the
22762
+ * history is worthless (a doorbell whose subject is always already at the
22763
+ * door), not as a hedge against the feature.
22764
+ */
22765
+ preRollInferenceEnabled: boolean().default(true),
22766
+ preRollInferenceFrames: number().min(preRollInferenceFramesField.min).max(preRollInferenceFramesField.max).default(preRollInferenceFramesField.default),
22726
22767
  occupancyRecheckEnabled: boolean().default(true),
22727
22768
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
22728
22769
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
@@ -22751,7 +22792,7 @@ var RunnerCameraConfigSchema = object({
22751
22792
  */
22752
22793
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
22753
22794
  });
22754
- motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
22795
+ motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, preRollInferenceFramesField.min, preRollInferenceFramesField.max, preRollInferenceFramesField.step, preRollInferenceFramesField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
22755
22796
  /**
22756
22797
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
22757
22798
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -22514,6 +22514,22 @@ var occupancyRecheckFramesField = {
22514
22514
  step: 1
22515
22515
  };
22516
22516
  /**
22517
+ * Frames one PRE-ROLL catch-up burst may send to inference at session open.
22518
+ *
22519
+ * The default of 12 over a ~4 s retained window is one frame per ~333 ms of
22520
+ * footage, and costs ~480 ms of ONE inference permit at the 40 ms/frame this
22521
+ * fleet measures — one-shot, and only ever taken while a permit would still be
22522
+ * spare for live. `0` turns the burst off entirely (the history is still
22523
+ * decoded, because that is how the dial reaches a decodable GOP, and is then
22524
+ * discarded — the pre-D411 behaviour).
22525
+ */
22526
+ var preRollInferenceFramesField = {
22527
+ min: 0,
22528
+ max: 30,
22529
+ default: 12,
22530
+ step: 1
22531
+ };
22532
+ /**
22517
22533
  * Source enum for motion signals fed to the runner. Extensible — add
22518
22534
  * new variants here when new motion-trigger paths are wired in
22519
22535
  * (`wasm-cross-camera`, `event-bus-relay`, etc.). The runner uses
@@ -22754,6 +22770,31 @@ var RunnerCameraConfigSchema = object({
22754
22770
  * to every occupancy rule until something moved in front of it. The churn is
22755
22771
  * now paid on the interval instead — see `occupancyRecheckSecField`.
22756
22772
  */
22773
+ /**
22774
+ * Infer the session's PRE-ROLL — the retained history the restreamer replays
22775
+ * at session open — instead of decoding it and throwing it away.
22776
+ *
22777
+ * DEFAULT `true`, and the default is the argument. This is not a new
22778
+ * capability being offered cautiously: the pre-roll is ALREADY requested,
22779
+ * already served, already decoded and already paid for on every on-motion
22780
+ * detection session that asks for history. What shipped was a last-moment
22781
+ * discard — `FrameSlot`'s throttle compares wall clocks, and ~4 s of media
22782
+ * arriving inside a few hundred ms of wall clock looks to it like one frame's
22783
+ * worth. Device 617, 2026-09-08: a re-run of the SAME model over the stored
22784
+ * recording scored the subject `vehicle 0.5669` — above the 0.50 floor —
22785
+ * 1.68 s BEFORE the live track was born, on a frame that was in the pre-roll,
22786
+ * was decoded, and was freed.
22787
+ *
22788
+ * Shipping that as opt-in would ship the bug: an operator cannot discover a
22789
+ * setting whose absence looks exactly like a camera that noticed the cyclist
22790
+ * late. What makes ON safe is not a switch but the BOUND —
22791
+ * `preRollInferenceFrames`, a wall-clock ceiling, and a lane that never takes
22792
+ * the last inference permit — so the switch exists for the camera where the
22793
+ * history is worthless (a doorbell whose subject is always already at the
22794
+ * door), not as a hedge against the feature.
22795
+ */
22796
+ preRollInferenceEnabled: boolean().default(true),
22797
+ preRollInferenceFrames: number().min(preRollInferenceFramesField.min).max(preRollInferenceFramesField.max).default(preRollInferenceFramesField.default),
22757
22798
  occupancyRecheckEnabled: boolean().default(true),
22758
22799
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
22759
22800
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
@@ -22782,7 +22823,7 @@ var RunnerCameraConfigSchema = object({
22782
22823
  */
22783
22824
  inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
22784
22825
  });
22785
- motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
22826
+ motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, maxSessionHoldMsField.min, maxSessionHoldMsField.max, maxSessionHoldMsField.step, maxSessionHoldMsField.default, audioMotionWindowMsField.min, audioMotionWindowMsField.max, audioMotionWindowMsField.step, audioMotionWindowMsField.default, preRollInferenceFramesField.min, preRollInferenceFramesField.max, preRollInferenceFramesField.step, preRollInferenceFramesField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
22786
22827
  /**
22787
22828
  * Runtime load summary returned by `getLocalLoad`. Used by the orchestrator's
22788
22829
  * load-balancing levels (L2 capacity-based, L3 hardware-aware) to decide
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-IE6UOohI.js");
5
+ const require_dist = require("../dist-DA1sJKXM.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { At as resolvePoolMemoryPolicy, B as PoolMemoryWatchdog, Ct as parseProcStatus, gt as hfModelUrl, ut as embeddingEncoderCapability, zt as BaseAddon } from "../dist-GQbzNwJB.mjs";
1
+ import { At as resolvePoolMemoryPolicy, B as PoolMemoryWatchdog, Ct as parseProcStatus, gt as hfModelUrl, ut as embeddingEncoderCapability, zt as BaseAddon } from "../dist-CT2SouNJ.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.91",
6
+ version: "1.2.93",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.171",
21
+ version: "1.2.173",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.139",
36
+ version: "1.2.141",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.171",
39
+ version: "1.2.173",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.91",
48
+ version: "1.2.93",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.139",
84
+ version: "1.2.141",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-IE6UOohI.js");
5
+ const require_dist = require("../dist-DA1sJKXM.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -10,6 +10,7 @@ let node_path$1 = require_dist.__toESM(node_path, 1);
10
10
  node_path = require_dist.__toESM(node_path);
11
11
  let node_crypto = require("node:crypto");
12
12
  let node_child_process = require("node:child_process");
13
+ let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
13
14
  let sharp = require("sharp");
14
15
  sharp = require_dist.__toESM(sharp);
15
16
  let node_os = require("node:os");
@@ -34861,6 +34862,9 @@ var DEFAULT_TRACKER_CONFIG = {
34861
34862
  var PERSON_CLASS = "person";
34862
34863
  var ANIMAL_CLASS = "animal";
34863
34864
  var MAX_PATH_LENGTH = 300;
34865
+ /** No stationary-held detections on this frame — the default for every caller
34866
+ * that has no gate (replay, tests, the non-pipeline planes). */
34867
+ var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
34864
34868
  function clamp$1(value, min, max) {
34865
34869
  return Math.max(min, Math.min(max, value));
34866
34870
  }
@@ -35480,13 +35484,15 @@ var SortTracker = class SortTracker {
35480
35484
  * gates) so the two-tier ByteTrack association stays a single implementation.
35481
35485
  * Mutates `used` / `matchedTracks` / `matched` in place.
35482
35486
  */
35483
- greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
35487
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
35484
35488
  const pairs = [];
35485
35489
  for (const track of this.tracks) {
35486
35490
  if (matchedTracks.has(track)) continue;
35487
35491
  const pbox = this.predicted(track, timestamp);
35492
+ const confirmed = this.isConfirmedForEmit(track);
35488
35493
  for (const di of candidateIdxs) {
35489
35494
  if (used.has(di)) continue;
35495
+ if (!confirmed && continuationOnly.has(di)) continue;
35490
35496
  const det = detections[di];
35491
35497
  if (!this.associable(track.class, det)) continue;
35492
35498
  const score = iou$3(pbox, det.bbox);
@@ -35507,6 +35513,7 @@ var SortTracker = class SortTracker {
35507
35513
  }
35508
35514
  update(detections, timestamp, frameContext) {
35509
35515
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
35516
+ const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
35510
35517
  const resurrectedScene = /* @__PURE__ */ new Map();
35511
35518
  if (this.config.maxTrackLifetimeMs > 0) {
35512
35519
  const alive = [];
@@ -35543,12 +35550,12 @@ var SortTracker = class SortTracker {
35543
35550
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
35544
35551
  else highIdxs.push(di);
35545
35552
  const lowSet = new Set(lowIdxs);
35546
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
35553
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35547
35554
  const rescuePairs = [];
35548
35555
  for (const track of this.tracks) {
35549
35556
  if (matchedTracks.has(track)) continue;
35550
35557
  for (let di = 0; di < detections.length; di++) {
35551
- if (used.has(di) || lowSet.has(di)) continue;
35558
+ if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
35552
35559
  const det = detections[di];
35553
35560
  const gate = this.looseMatch(track, det, timestamp);
35554
35561
  if (gate === null) continue;
@@ -35570,7 +35577,7 @@ var SortTracker = class SortTracker {
35570
35577
  used.add(pair.detIdx);
35571
35578
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
35572
35579
  }
35573
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
35580
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35574
35581
  for (const [track, det] of matched) {
35575
35582
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
35576
35583
  const prevCenter = bboxCentroid({
@@ -35626,7 +35633,7 @@ var SortTracker = class SortTracker {
35626
35633
  } else surviving.push(track);
35627
35634
  }
35628
35635
  for (let di = 0; di < detections.length; di++) {
35629
- if (used.has(di)) continue;
35636
+ if (used.has(di) || continuationOnly.has(di)) continue;
35630
35637
  const det = detections[di];
35631
35638
  let best;
35632
35639
  let bestScore = -1;
@@ -35694,6 +35701,11 @@ var SortTracker = class SortTracker {
35694
35701
  });
35695
35702
  for (const di of unmatchedIdx) {
35696
35703
  const det = detections[di];
35704
+ if (continuationOnly.has(di)) {
35705
+ this.reportRefusal("stationary-hold", det);
35706
+ used.add(di);
35707
+ continue;
35708
+ }
35697
35709
  if (!this.meetsClassScoreFloor(det)) {
35698
35710
  this.reportRefusal("score-floor", det, {
35699
35711
  threshold: this.config.classMinScores[det.class] ?? 0,
@@ -36432,14 +36444,28 @@ var FrameProcessor = class {
36432
36444
  timestamp
36433
36445
  }) : {
36434
36446
  suppressedIndices: /* @__PURE__ */ new Set(),
36447
+ continuableIndices: /* @__PURE__ */ new Set(),
36435
36448
  confirmed: [],
36436
36449
  woken: []
36437
36450
  };
36438
- const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
36451
+ const trackerInput = [];
36452
+ const continuationOnly = /* @__PURE__ */ new Set();
36453
+ const continuableBoxes = /* @__PURE__ */ new Set();
36454
+ for (let i = 0; i < filteredDetections.length; i++) {
36455
+ const det = filteredDetections[i];
36456
+ if (gate.suppressedIndices.has(i)) {
36457
+ if (!gate.continuableIndices.has(i)) continue;
36458
+ continuationOnly.add(trackerInput.length);
36459
+ continuableBoxes.add(det.bbox);
36460
+ }
36461
+ trackerInput.push(det);
36462
+ }
36439
36463
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
36440
36464
  frameWidth,
36441
- frameHeight
36465
+ frameHeight,
36466
+ continuationOnly
36442
36467
  });
36468
+ const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
36443
36469
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
36444
36470
  const td = trackedDetections.find((t) => t.bbox === bbox && t.matchedThisFrame !== false);
36445
36471
  return td !== void 0 ? {
@@ -36643,6 +36669,7 @@ var FrameProcessor = class {
36643
36669
  rawTrackedDetections: trackedDetections,
36644
36670
  stationaryConfirmed: gate.confirmed,
36645
36671
  stationaryWoken: gate.woken,
36672
+ stationaryContinued,
36646
36673
  riderFolds
36647
36674
  };
36648
36675
  }
@@ -49534,6 +49561,87 @@ var FaceRecognizer = class {
49534
49561
  }
49535
49562
  }
49536
49563
  };
49564
+ /** Read the window. Pure — this is the judgement the module exists to make. */
49565
+ function lagVerdict(report) {
49566
+ const bus = report.busLagMaxMs;
49567
+ if (bus === null) return "unknown";
49568
+ return bus >= 5e3 ? "starved" : "delivered-on-time";
49569
+ }
49570
+ /** Does this window need an operator's eyes? Unknown never counts as alarming
49571
+ * — that is the fabricated verdict D8 forbids. */
49572
+ function isAlarmingLagReport(report) {
49573
+ const bus = report.busLagMaxMs;
49574
+ const capture = report.captureLagMaxMs;
49575
+ return bus !== null && bus >= 5e3 || capture !== null && capture >= 5e3;
49576
+ }
49577
+ /**
49578
+ * Folds per-frame delivery lag into per-device windows.
49579
+ *
49580
+ * Not a logger: it returns the line to write (or null) and the caller decides
49581
+ * level and tags. That keeps the throttle — the part that can wreck the frame
49582
+ * path — testable without a logger fake.
49583
+ */
49584
+ var FrameLagObserver = class {
49585
+ windows = /* @__PURE__ */ new Map();
49586
+ /**
49587
+ * Fold one inference result into `deviceId`'s window.
49588
+ *
49589
+ * @param now Receipt time, read by the caller once at handler entry.
49590
+ * @param emittedAt Runner-side emit stamp; undefined ⇒ unknown (D8).
49591
+ * @param capturedAt Frame shm-commit stamp; undefined ⇒ unknown.
49592
+ * @returns The closed window when this frame ends one, else null.
49593
+ */
49594
+ observe(deviceId, now, emittedAt, capturedAt) {
49595
+ const existing = this.windows.get(deviceId);
49596
+ const w = existing ?? {
49597
+ startedAt: now,
49598
+ frames: 0,
49599
+ busMax: null,
49600
+ busMin: null,
49601
+ busUnknown: 0,
49602
+ captureMax: null,
49603
+ captureUnknown: 0
49604
+ };
49605
+ if (existing === void 0) this.windows.set(deviceId, w);
49606
+ w.frames += 1;
49607
+ if (typeof emittedAt === "number") {
49608
+ const busLag = now - emittedAt;
49609
+ if (w.busMax === null || busLag > w.busMax) w.busMax = busLag;
49610
+ if (w.busMin === null || busLag < w.busMin) w.busMin = busLag;
49611
+ } else w.busUnknown += 1;
49612
+ if (typeof capturedAt === "number") {
49613
+ const captureLag = now - capturedAt;
49614
+ if (w.captureMax === null || captureLag > w.captureMax) w.captureMax = captureLag;
49615
+ } else w.captureUnknown += 1;
49616
+ if (existing !== void 0 && now - w.startedAt < 3e4) return null;
49617
+ const report = {
49618
+ deviceId,
49619
+ frames: w.frames,
49620
+ busLagMaxMs: w.busMax,
49621
+ busLagMinMs: w.busMin,
49622
+ busLagUnknownFrames: w.busUnknown,
49623
+ captureLagMaxMs: w.captureMax,
49624
+ captureLagUnknownFrames: w.captureUnknown,
49625
+ windowMs: now - w.startedAt
49626
+ };
49627
+ w.startedAt = now;
49628
+ w.frames = 0;
49629
+ w.busMax = null;
49630
+ w.busMin = null;
49631
+ w.busUnknown = 0;
49632
+ w.captureMax = null;
49633
+ w.captureUnknown = 0;
49634
+ return report;
49635
+ }
49636
+ /** Drop a device's window (unbound camera, shutdown). */
49637
+ forget(deviceId) {
49638
+ this.windows.delete(deviceId);
49639
+ }
49640
+ /** Drop every window. */
49641
+ clear() {
49642
+ this.windows.clear();
49643
+ }
49644
+ };
49537
49645
  //#endregion
49538
49646
  //#region src/pipeline-analytics/location-aware-media-storage.ts
49539
49647
  /**
@@ -54989,6 +55097,31 @@ function thumbnailWouldShowParkedNotSubject(input) {
54989
55097
  * dropped, against every entry that did NOT wake this frame. Woken entries are
54990
55098
  * excluded so a departure still releases its object — a car that drives away is
54991
55099
  * noticed on the frame its second read lands, exactly as before.
55100
+ *
55101
+ * ## A weak hold may CONTINUE a track; only a strong read is the object (D410)
55102
+ *
55103
+ * D275 predicted that a moving vehicle overlapping a parked one "loses a frame
55104
+ * or two while it passes" and coasts through on `maxAge`. Measured on device
55105
+ * 617 on 2026-09-08 it lost the whole pass: the parked cars sit at the frame
55106
+ * edge, so a car driving across their front is held on every frame from the
55107
+ * first overlap to the moment it leaves the frame, and the tracker — which had
55108
+ * followed it for 5–10 frames — coasts `maxMissedMs` on nothing and drops it.
55109
+ * Four operator-marked tracks, 1.7–3.5 s old, died on exactly that geometry;
55110
+ * the sweep for the window reported `reach: 0`, so the hold was pass 1's: the
55111
+ * mover occludes the parked car, the detector reports one vehicle box, and it
55112
+ * is the entry's best match.
55113
+ *
55114
+ * The hold is right — that box covers the parked spot and must not OPEN a
55115
+ * track, which is the whole of the operator's requirement — but it is not the
55116
+ * parked car: a strong read (IoU ≥ `suppressIou`) is the parked object unmoved,
55117
+ * and everything below it is a box that merely overlaps the spot (swallow,
55118
+ * jitter, departing). So `suppressedIndices` still names every box that may
55119
+ * not spawn, and `continuableIndices` names the weak subset the tracker may use
55120
+ * to CONTINUE a track it has already confirmed — through its strict
55121
+ * predicted-box association only, never a rescue or a resurrection. The reach
55122
+ * pass reports strength the same way. Nothing about confirmation, anchoring or
55123
+ * departure changes: a weak hold confirms and never re-anchors, exactly as
55124
+ * before.
54992
55125
  */
54993
55126
  function partitionDetectionsAgainstRegistry(input) {
54994
55127
  const { entries, detections, config, now } = input;
@@ -54996,6 +55129,7 @@ function partitionDetectionsAgainstRegistry(input) {
54996
55129
  const departure = input.departure ?? DEFAULT_DEPARTURE_CONFIG;
54997
55130
  const warmUp = input.warmUp ?? false;
54998
55131
  const suppressed = /* @__PURE__ */ new Set();
55132
+ const continuable = /* @__PURE__ */ new Set();
54999
55133
  const confirmed = [];
55000
55134
  const woken = [];
55001
55135
  const strikeChanges = [];
@@ -55035,6 +55169,7 @@ function partitionDetectionsAgainstRegistry(input) {
55035
55169
  }
55036
55170
  if (holdIdx >= 0) {
55037
55171
  suppressed.add(holdIdx);
55172
+ if (!strong) continuable.add(holdIdx);
55038
55173
  confirmed.push({
55039
55174
  entryId: entry.id,
55040
55175
  className: entry.className,
@@ -55074,6 +55209,7 @@ function partitionDetectionsAgainstRegistry(input) {
55074
55209
  if (warmUp) {
55075
55210
  warmUpDepartureReads += 1;
55076
55211
  suppressed.add(bestIdx);
55212
+ continuable.add(bestIdx);
55077
55213
  continue;
55078
55214
  }
55079
55215
  if (strike === void 0) {
@@ -55086,6 +55222,7 @@ function partitionDetectionsAgainstRegistry(input) {
55086
55222
  reason: "armed"
55087
55223
  });
55088
55224
  suppressed.add(bestIdx);
55225
+ continuable.add(bestIdx);
55089
55226
  continue;
55090
55227
  }
55091
55228
  if (strikeAgeMs >= departure.confirmMinGapMs) {
@@ -55106,6 +55243,7 @@ function partitionDetectionsAgainstRegistry(input) {
55106
55243
  reason: "held"
55107
55244
  });
55108
55245
  suppressed.add(bestIdx);
55246
+ continuable.add(bestIdx);
55109
55247
  }
55110
55248
  const wokenIds = woken.length === 0 ? EMPTY_IDS : new Set(woken.map((w) => w.entryId));
55111
55249
  let reachSuppressedCount = 0;
@@ -55116,14 +55254,17 @@ function partitionDetectionsAgainstRegistry(input) {
55116
55254
  for (let ei = 0; ei < entries.length; ei++) {
55117
55255
  const entry = entries[ei];
55118
55256
  if (entryClasses[ei] !== detClass || wokenIds.has(entry.id)) continue;
55119
- if (!parkedEntryHoldsDetection(entry, det.bbox, config).holds) continue;
55257
+ const held = parkedEntryHoldsDetection(entry, det.bbox, config);
55258
+ if (!held.holds) continue;
55120
55259
  suppressed.add(di);
55260
+ if (!held.strong) continuable.add(di);
55121
55261
  reachSuppressedCount += 1;
55122
55262
  break;
55123
55263
  }
55124
55264
  }
55125
55265
  return {
55126
55266
  suppressedIndices: suppressed,
55267
+ continuableIndices: continuable,
55127
55268
  confirmed,
55128
55269
  woken,
55129
55270
  strikeChanges,
@@ -56747,6 +56888,10 @@ var StationaryObjectRegistry = class {
56747
56888
  */
56748
56889
  suppressedSinceSweep = /* @__PURE__ */ new Map();
56749
56890
  reachSuppressedSinceSweep = /* @__PURE__ */ new Map();
56891
+ /** Weak holds handed to the tracker as continuable (D410), per device. */
56892
+ continuableSinceSweep = /* @__PURE__ */ new Map();
56893
+ /** Of those, the ones the tracker reports actually continued a track. */
56894
+ continuedSinceSweep = /* @__PURE__ */ new Map();
56750
56895
  constructor(deps) {
56751
56896
  this.logger = deps.logger;
56752
56897
  this.ledger = new DurableLedger({
@@ -56830,6 +56975,7 @@ var StationaryObjectRegistry = class {
56830
56975
  const entries = this.list(deviceId);
56831
56976
  if (entries.length === 0) return {
56832
56977
  suppressedIndices: /* @__PURE__ */ new Set(),
56978
+ continuableIndices: /* @__PURE__ */ new Set(),
56833
56979
  confirmed: [],
56834
56980
  woken: [],
56835
56981
  reachSuppressedCount: 0
@@ -56850,14 +56996,23 @@ var StationaryObjectRegistry = class {
56850
56996
  if (result.suppressedIndices.size > 0) {
56851
56997
  this.suppressedSinceSweep.set(deviceId, (this.suppressedSinceSweep.get(deviceId) ?? 0) + result.suppressedIndices.size);
56852
56998
  if (result.reachSuppressedCount > 0) this.reachSuppressedSinceSweep.set(deviceId, (this.reachSuppressedSinceSweep.get(deviceId) ?? 0) + result.reachSuppressedCount);
56999
+ if (result.continuableIndices.size > 0) this.continuableSinceSweep.set(deviceId, (this.continuableSinceSweep.get(deviceId) ?? 0) + result.continuableIndices.size);
56853
57000
  }
56854
57001
  return {
56855
57002
  suppressedIndices: result.suppressedIndices,
57003
+ continuableIndices: result.continuableIndices,
56856
57004
  confirmed: result.confirmed,
56857
57005
  woken: result.woken,
56858
57006
  reachSuppressedCount: result.reachSuppressedCount
56859
57007
  };
56860
57008
  }
57009
+ /** The tracker's answer to {@link StationaryFrameFilterResult.continuableIndices}:
57010
+ * how many of this frame's weak holds continued a confirmed track (D410).
57011
+ * Counted here so the sweep line can report it beside `continuable`. */
57012
+ noteContinued(deviceId, count) {
57013
+ if (count <= 0) return;
57014
+ this.continuedSinceSweep.set(deviceId, (this.continuedSinceSweep.get(deviceId) ?? 0) + count);
57015
+ }
56861
57016
  /** Pending departure strikes for a device — the read-only diagnostic's whole
56862
57017
  * reason to exist. Ordered oldest first so "what has been waiting" reads
56863
57018
  * straight off the top. */
@@ -56996,6 +57151,8 @@ var StationaryObjectRegistry = class {
56996
57151
  absorbed,
56997
57152
  suppressed,
56998
57153
  reach: this.reachSuppressedSinceSweep.get(deviceId) ?? 0,
57154
+ continuable: this.continuableSinceSweep.get(deviceId) ?? 0,
57155
+ continued: this.continuedSinceSweep.get(deviceId) ?? 0,
56999
57156
  entries: this.count(deviceId)
57000
57157
  }
57001
57158
  });
@@ -57003,6 +57160,8 @@ var StationaryObjectRegistry = class {
57003
57160
  this.absorbedSinceSweep.clear();
57004
57161
  this.suppressedSinceSweep.clear();
57005
57162
  this.reachSuppressedSinceSweep.clear();
57163
+ this.continuableSinceSweep.clear();
57164
+ this.continuedSinceSweep.clear();
57006
57165
  for (const [deviceId, counters] of this.strikeCountersByDevice) this.logger.info("stationary departure strikes", {
57007
57166
  tags: { deviceId },
57008
57167
  meta: {
@@ -70652,6 +70811,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70652
70811
  * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
70653
70812
  getNativeKeyFrameRgb = null;
70654
70813
  shuttingDown = false;
70814
+ /**
70815
+ * Per-device delivery-lag windows, folded at `handleInferenceResult` entry.
70816
+ * The instrument that tells a late DELIVERY (starved) apart from a slow
70817
+ * `processFrame` (waiting) — see `frame-lag-observer.ts`. Costs one map read
70818
+ * and ~10 numeric ops per frame, and writes one line per device per 30 s.
70819
+ */
70820
+ frameLag = new FrameLagObserver();
70821
+ /**
70822
+ * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
70823
+ *
70824
+ * The other half of the frame-lag line: that one says whether the events
70825
+ * arrived late, this one says whether THIS loop was blocked while they did.
70826
+ * The 5-minute `[mem]` heartbeat cannot see a 10 s freeze — it fired at
70827
+ * dt = 300020 / 300066 / 299991 ms straight through the 2026-09-09 silences.
70828
+ */
70829
+ stopStallMonitor = null;
70655
70830
  /** True only on the cluster's designated post-processing node. When false the
70656
70831
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
70657
70832
  isPostProcessingNode = true;
@@ -70870,6 +71045,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70870
71045
  super({});
70871
71046
  }
70872
71047
  async onInitialize() {
71048
+ this.stopStallMonitor = (0, _camstack_system_addon_utils.startEventLoopStallMonitor)(this.ctx.logger);
70873
71049
  const rawApi = this.ctx.api;
70874
71050
  if (!rawApi) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
70875
71051
  const api = censusApi(rawApi, this.readCensus);
@@ -73374,6 +73550,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73374
73550
  }
73375
73551
  async onShutdown() {
73376
73552
  this.shuttingDown = true;
73553
+ if (this.stopStallMonitor) {
73554
+ this.stopStallMonitor();
73555
+ this.stopStallMonitor = null;
73556
+ }
73377
73557
  this.sceneEngine?.stop();
73378
73558
  this.sceneEngine = null;
73379
73559
  if (this.sceneReloadTimer !== null) {
@@ -73418,6 +73598,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73418
73598
  this.overlaySynthesisWarnAt.clear();
73419
73599
  this.processors.clear();
73420
73600
  this.lastActiveTrackIds.clear();
73601
+ this.frameLag.clear();
73421
73602
  this.lastFrameDimsByDevice.clear();
73422
73603
  this.dropoutSkipsByKey.clear();
73423
73604
  this.residents.clearAll();
@@ -73457,6 +73638,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73457
73638
  async handleInferenceResult(data) {
73458
73639
  if (this.shuttingDown) return;
73459
73640
  const { deviceId, frame } = data;
73641
+ const lag = this.frameLag.observe(deviceId, Date.now(), data.emittedAt, data.capturedAt);
73642
+ if (lag !== null) {
73643
+ const verdict = lagVerdict(lag);
73644
+ (isAlarmingLagReport(lag) ? this.ctx.logger.warn.bind(this.ctx.logger) : this.ctx.logger.info.bind(this.ctx.logger))("pipeline-analytics frame lag", {
73645
+ tags: { deviceId },
73646
+ meta: {
73647
+ verdict,
73648
+ frames: lag.frames,
73649
+ windowMs: lag.windowMs,
73650
+ busLagMaxMs: lag.busLagMaxMs,
73651
+ busLagMinMs: lag.busLagMinMs,
73652
+ busLagUnknownFrames: lag.busLagUnknownFrames,
73653
+ captureLagMaxMs: lag.captureLagMaxMs,
73654
+ captureLagUnknownFrames: lag.captureLagUnknownFrames
73655
+ }
73656
+ });
73657
+ }
73460
73658
  await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
73461
73659
  }
73462
73660
  /**
@@ -73573,6 +73771,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73573
73771
  woken: result.stationaryWoken,
73574
73772
  timestamp: result.timestamp
73575
73773
  });
73774
+ if (this.stationaryRegistry && result.stationaryContinued > 0) this.stationaryRegistry.noteContinued(deviceId, result.stationaryContinued);
73576
73775
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
73577
73776
  const stationaryAsTracked = stationaryViews.map((v) => ({
73578
73777
  trackId: `stationary:${v.id}`,
@@ -76943,6 +77142,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
76943
77142
  const s = this.stationarySettingsFromCache(deviceId);
76944
77143
  if (!s.enabled) return {
76945
77144
  suppressedIndices: /* @__PURE__ */ new Set(),
77145
+ continuableIndices: /* @__PURE__ */ new Set(),
76946
77146
  confirmed: [],
76947
77147
  woken: []
76948
77148
  };
@@ -1,4 +1,4 @@
1
- import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-GQbzNwJB.mjs";
1
+ import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-CT2SouNJ.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -6,6 +6,7 @@ import * as path$1 from "node:path";
6
6
  import path from "node:path";
7
7
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
8
8
  import { execFile } from "node:child_process";
9
+ import { startEventLoopStallMonitor } from "@camstack/system/addon-utils";
9
10
  import sharp from "sharp";
10
11
  import os from "node:os";
11
12
  import { Buffer as Buffer$1 } from "node:buffer";
@@ -34836,6 +34837,9 @@ var DEFAULT_TRACKER_CONFIG = {
34836
34837
  var PERSON_CLASS = "person";
34837
34838
  var ANIMAL_CLASS = "animal";
34838
34839
  var MAX_PATH_LENGTH = 300;
34840
+ /** No stationary-held detections on this frame — the default for every caller
34841
+ * that has no gate (replay, tests, the non-pipeline planes). */
34842
+ var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
34839
34843
  function clamp$1(value, min, max) {
34840
34844
  return Math.max(min, Math.min(max, value));
34841
34845
  }
@@ -35455,13 +35459,15 @@ var SortTracker = class SortTracker {
35455
35459
  * gates) so the two-tier ByteTrack association stays a single implementation.
35456
35460
  * Mutates `used` / `matchedTracks` / `matched` in place.
35457
35461
  */
35458
- greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
35462
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
35459
35463
  const pairs = [];
35460
35464
  for (const track of this.tracks) {
35461
35465
  if (matchedTracks.has(track)) continue;
35462
35466
  const pbox = this.predicted(track, timestamp);
35467
+ const confirmed = this.isConfirmedForEmit(track);
35463
35468
  for (const di of candidateIdxs) {
35464
35469
  if (used.has(di)) continue;
35470
+ if (!confirmed && continuationOnly.has(di)) continue;
35465
35471
  const det = detections[di];
35466
35472
  if (!this.associable(track.class, det)) continue;
35467
35473
  const score = iou$3(pbox, det.bbox);
@@ -35482,6 +35488,7 @@ var SortTracker = class SortTracker {
35482
35488
  }
35483
35489
  update(detections, timestamp, frameContext) {
35484
35490
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
35491
+ const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
35485
35492
  const resurrectedScene = /* @__PURE__ */ new Map();
35486
35493
  if (this.config.maxTrackLifetimeMs > 0) {
35487
35494
  const alive = [];
@@ -35518,12 +35525,12 @@ var SortTracker = class SortTracker {
35518
35525
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
35519
35526
  else highIdxs.push(di);
35520
35527
  const lowSet = new Set(lowIdxs);
35521
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
35528
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35522
35529
  const rescuePairs = [];
35523
35530
  for (const track of this.tracks) {
35524
35531
  if (matchedTracks.has(track)) continue;
35525
35532
  for (let di = 0; di < detections.length; di++) {
35526
- if (used.has(di) || lowSet.has(di)) continue;
35533
+ if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
35527
35534
  const det = detections[di];
35528
35535
  const gate = this.looseMatch(track, det, timestamp);
35529
35536
  if (gate === null) continue;
@@ -35545,7 +35552,7 @@ var SortTracker = class SortTracker {
35545
35552
  used.add(pair.detIdx);
35546
35553
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
35547
35554
  }
35548
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
35555
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35549
35556
  for (const [track, det] of matched) {
35550
35557
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
35551
35558
  const prevCenter = bboxCentroid({
@@ -35601,7 +35608,7 @@ var SortTracker = class SortTracker {
35601
35608
  } else surviving.push(track);
35602
35609
  }
35603
35610
  for (let di = 0; di < detections.length; di++) {
35604
- if (used.has(di)) continue;
35611
+ if (used.has(di) || continuationOnly.has(di)) continue;
35605
35612
  const det = detections[di];
35606
35613
  let best;
35607
35614
  let bestScore = -1;
@@ -35669,6 +35676,11 @@ var SortTracker = class SortTracker {
35669
35676
  });
35670
35677
  for (const di of unmatchedIdx) {
35671
35678
  const det = detections[di];
35679
+ if (continuationOnly.has(di)) {
35680
+ this.reportRefusal("stationary-hold", det);
35681
+ used.add(di);
35682
+ continue;
35683
+ }
35672
35684
  if (!this.meetsClassScoreFloor(det)) {
35673
35685
  this.reportRefusal("score-floor", det, {
35674
35686
  threshold: this.config.classMinScores[det.class] ?? 0,
@@ -36407,14 +36419,28 @@ var FrameProcessor = class {
36407
36419
  timestamp
36408
36420
  }) : {
36409
36421
  suppressedIndices: /* @__PURE__ */ new Set(),
36422
+ continuableIndices: /* @__PURE__ */ new Set(),
36410
36423
  confirmed: [],
36411
36424
  woken: []
36412
36425
  };
36413
- const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
36426
+ const trackerInput = [];
36427
+ const continuationOnly = /* @__PURE__ */ new Set();
36428
+ const continuableBoxes = /* @__PURE__ */ new Set();
36429
+ for (let i = 0; i < filteredDetections.length; i++) {
36430
+ const det = filteredDetections[i];
36431
+ if (gate.suppressedIndices.has(i)) {
36432
+ if (!gate.continuableIndices.has(i)) continue;
36433
+ continuationOnly.add(trackerInput.length);
36434
+ continuableBoxes.add(det.bbox);
36435
+ }
36436
+ trackerInput.push(det);
36437
+ }
36414
36438
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
36415
36439
  frameWidth,
36416
- frameHeight
36440
+ frameHeight,
36441
+ continuationOnly
36417
36442
  });
36443
+ const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
36418
36444
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
36419
36445
  const td = trackedDetections.find((t) => t.bbox === bbox && t.matchedThisFrame !== false);
36420
36446
  return td !== void 0 ? {
@@ -36618,6 +36644,7 @@ var FrameProcessor = class {
36618
36644
  rawTrackedDetections: trackedDetections,
36619
36645
  stationaryConfirmed: gate.confirmed,
36620
36646
  stationaryWoken: gate.woken,
36647
+ stationaryContinued,
36621
36648
  riderFolds
36622
36649
  };
36623
36650
  }
@@ -49460,6 +49487,87 @@ var FaceRecognizer = class {
49460
49487
  }
49461
49488
  }
49462
49489
  };
49490
+ /** Read the window. Pure — this is the judgement the module exists to make. */
49491
+ function lagVerdict(report) {
49492
+ const bus = report.busLagMaxMs;
49493
+ if (bus === null) return "unknown";
49494
+ return bus >= 5e3 ? "starved" : "delivered-on-time";
49495
+ }
49496
+ /** Does this window need an operator's eyes? Unknown never counts as alarming
49497
+ * — that is the fabricated verdict D8 forbids. */
49498
+ function isAlarmingLagReport(report) {
49499
+ const bus = report.busLagMaxMs;
49500
+ const capture = report.captureLagMaxMs;
49501
+ return bus !== null && bus >= 5e3 || capture !== null && capture >= 5e3;
49502
+ }
49503
+ /**
49504
+ * Folds per-frame delivery lag into per-device windows.
49505
+ *
49506
+ * Not a logger: it returns the line to write (or null) and the caller decides
49507
+ * level and tags. That keeps the throttle — the part that can wreck the frame
49508
+ * path — testable without a logger fake.
49509
+ */
49510
+ var FrameLagObserver = class {
49511
+ windows = /* @__PURE__ */ new Map();
49512
+ /**
49513
+ * Fold one inference result into `deviceId`'s window.
49514
+ *
49515
+ * @param now Receipt time, read by the caller once at handler entry.
49516
+ * @param emittedAt Runner-side emit stamp; undefined ⇒ unknown (D8).
49517
+ * @param capturedAt Frame shm-commit stamp; undefined ⇒ unknown.
49518
+ * @returns The closed window when this frame ends one, else null.
49519
+ */
49520
+ observe(deviceId, now, emittedAt, capturedAt) {
49521
+ const existing = this.windows.get(deviceId);
49522
+ const w = existing ?? {
49523
+ startedAt: now,
49524
+ frames: 0,
49525
+ busMax: null,
49526
+ busMin: null,
49527
+ busUnknown: 0,
49528
+ captureMax: null,
49529
+ captureUnknown: 0
49530
+ };
49531
+ if (existing === void 0) this.windows.set(deviceId, w);
49532
+ w.frames += 1;
49533
+ if (typeof emittedAt === "number") {
49534
+ const busLag = now - emittedAt;
49535
+ if (w.busMax === null || busLag > w.busMax) w.busMax = busLag;
49536
+ if (w.busMin === null || busLag < w.busMin) w.busMin = busLag;
49537
+ } else w.busUnknown += 1;
49538
+ if (typeof capturedAt === "number") {
49539
+ const captureLag = now - capturedAt;
49540
+ if (w.captureMax === null || captureLag > w.captureMax) w.captureMax = captureLag;
49541
+ } else w.captureUnknown += 1;
49542
+ if (existing !== void 0 && now - w.startedAt < 3e4) return null;
49543
+ const report = {
49544
+ deviceId,
49545
+ frames: w.frames,
49546
+ busLagMaxMs: w.busMax,
49547
+ busLagMinMs: w.busMin,
49548
+ busLagUnknownFrames: w.busUnknown,
49549
+ captureLagMaxMs: w.captureMax,
49550
+ captureLagUnknownFrames: w.captureUnknown,
49551
+ windowMs: now - w.startedAt
49552
+ };
49553
+ w.startedAt = now;
49554
+ w.frames = 0;
49555
+ w.busMax = null;
49556
+ w.busMin = null;
49557
+ w.busUnknown = 0;
49558
+ w.captureMax = null;
49559
+ w.captureUnknown = 0;
49560
+ return report;
49561
+ }
49562
+ /** Drop a device's window (unbound camera, shutdown). */
49563
+ forget(deviceId) {
49564
+ this.windows.delete(deviceId);
49565
+ }
49566
+ /** Drop every window. */
49567
+ clear() {
49568
+ this.windows.clear();
49569
+ }
49570
+ };
49463
49571
  //#endregion
49464
49572
  //#region src/pipeline-analytics/location-aware-media-storage.ts
49465
49573
  /**
@@ -54915,6 +55023,31 @@ function thumbnailWouldShowParkedNotSubject(input) {
54915
55023
  * dropped, against every entry that did NOT wake this frame. Woken entries are
54916
55024
  * excluded so a departure still releases its object — a car that drives away is
54917
55025
  * noticed on the frame its second read lands, exactly as before.
55026
+ *
55027
+ * ## A weak hold may CONTINUE a track; only a strong read is the object (D410)
55028
+ *
55029
+ * D275 predicted that a moving vehicle overlapping a parked one "loses a frame
55030
+ * or two while it passes" and coasts through on `maxAge`. Measured on device
55031
+ * 617 on 2026-09-08 it lost the whole pass: the parked cars sit at the frame
55032
+ * edge, so a car driving across their front is held on every frame from the
55033
+ * first overlap to the moment it leaves the frame, and the tracker — which had
55034
+ * followed it for 5–10 frames — coasts `maxMissedMs` on nothing and drops it.
55035
+ * Four operator-marked tracks, 1.7–3.5 s old, died on exactly that geometry;
55036
+ * the sweep for the window reported `reach: 0`, so the hold was pass 1's: the
55037
+ * mover occludes the parked car, the detector reports one vehicle box, and it
55038
+ * is the entry's best match.
55039
+ *
55040
+ * The hold is right — that box covers the parked spot and must not OPEN a
55041
+ * track, which is the whole of the operator's requirement — but it is not the
55042
+ * parked car: a strong read (IoU ≥ `suppressIou`) is the parked object unmoved,
55043
+ * and everything below it is a box that merely overlaps the spot (swallow,
55044
+ * jitter, departing). So `suppressedIndices` still names every box that may
55045
+ * not spawn, and `continuableIndices` names the weak subset the tracker may use
55046
+ * to CONTINUE a track it has already confirmed — through its strict
55047
+ * predicted-box association only, never a rescue or a resurrection. The reach
55048
+ * pass reports strength the same way. Nothing about confirmation, anchoring or
55049
+ * departure changes: a weak hold confirms and never re-anchors, exactly as
55050
+ * before.
54918
55051
  */
54919
55052
  function partitionDetectionsAgainstRegistry(input) {
54920
55053
  const { entries, detections, config, now } = input;
@@ -54922,6 +55055,7 @@ function partitionDetectionsAgainstRegistry(input) {
54922
55055
  const departure = input.departure ?? DEFAULT_DEPARTURE_CONFIG;
54923
55056
  const warmUp = input.warmUp ?? false;
54924
55057
  const suppressed = /* @__PURE__ */ new Set();
55058
+ const continuable = /* @__PURE__ */ new Set();
54925
55059
  const confirmed = [];
54926
55060
  const woken = [];
54927
55061
  const strikeChanges = [];
@@ -54961,6 +55095,7 @@ function partitionDetectionsAgainstRegistry(input) {
54961
55095
  }
54962
55096
  if (holdIdx >= 0) {
54963
55097
  suppressed.add(holdIdx);
55098
+ if (!strong) continuable.add(holdIdx);
54964
55099
  confirmed.push({
54965
55100
  entryId: entry.id,
54966
55101
  className: entry.className,
@@ -55000,6 +55135,7 @@ function partitionDetectionsAgainstRegistry(input) {
55000
55135
  if (warmUp) {
55001
55136
  warmUpDepartureReads += 1;
55002
55137
  suppressed.add(bestIdx);
55138
+ continuable.add(bestIdx);
55003
55139
  continue;
55004
55140
  }
55005
55141
  if (strike === void 0) {
@@ -55012,6 +55148,7 @@ function partitionDetectionsAgainstRegistry(input) {
55012
55148
  reason: "armed"
55013
55149
  });
55014
55150
  suppressed.add(bestIdx);
55151
+ continuable.add(bestIdx);
55015
55152
  continue;
55016
55153
  }
55017
55154
  if (strikeAgeMs >= departure.confirmMinGapMs) {
@@ -55032,6 +55169,7 @@ function partitionDetectionsAgainstRegistry(input) {
55032
55169
  reason: "held"
55033
55170
  });
55034
55171
  suppressed.add(bestIdx);
55172
+ continuable.add(bestIdx);
55035
55173
  }
55036
55174
  const wokenIds = woken.length === 0 ? EMPTY_IDS : new Set(woken.map((w) => w.entryId));
55037
55175
  let reachSuppressedCount = 0;
@@ -55042,14 +55180,17 @@ function partitionDetectionsAgainstRegistry(input) {
55042
55180
  for (let ei = 0; ei < entries.length; ei++) {
55043
55181
  const entry = entries[ei];
55044
55182
  if (entryClasses[ei] !== detClass || wokenIds.has(entry.id)) continue;
55045
- if (!parkedEntryHoldsDetection(entry, det.bbox, config).holds) continue;
55183
+ const held = parkedEntryHoldsDetection(entry, det.bbox, config);
55184
+ if (!held.holds) continue;
55046
55185
  suppressed.add(di);
55186
+ if (!held.strong) continuable.add(di);
55047
55187
  reachSuppressedCount += 1;
55048
55188
  break;
55049
55189
  }
55050
55190
  }
55051
55191
  return {
55052
55192
  suppressedIndices: suppressed,
55193
+ continuableIndices: continuable,
55053
55194
  confirmed,
55054
55195
  woken,
55055
55196
  strikeChanges,
@@ -56673,6 +56814,10 @@ var StationaryObjectRegistry = class {
56673
56814
  */
56674
56815
  suppressedSinceSweep = /* @__PURE__ */ new Map();
56675
56816
  reachSuppressedSinceSweep = /* @__PURE__ */ new Map();
56817
+ /** Weak holds handed to the tracker as continuable (D410), per device. */
56818
+ continuableSinceSweep = /* @__PURE__ */ new Map();
56819
+ /** Of those, the ones the tracker reports actually continued a track. */
56820
+ continuedSinceSweep = /* @__PURE__ */ new Map();
56676
56821
  constructor(deps) {
56677
56822
  this.logger = deps.logger;
56678
56823
  this.ledger = new DurableLedger({
@@ -56756,6 +56901,7 @@ var StationaryObjectRegistry = class {
56756
56901
  const entries = this.list(deviceId);
56757
56902
  if (entries.length === 0) return {
56758
56903
  suppressedIndices: /* @__PURE__ */ new Set(),
56904
+ continuableIndices: /* @__PURE__ */ new Set(),
56759
56905
  confirmed: [],
56760
56906
  woken: [],
56761
56907
  reachSuppressedCount: 0
@@ -56776,14 +56922,23 @@ var StationaryObjectRegistry = class {
56776
56922
  if (result.suppressedIndices.size > 0) {
56777
56923
  this.suppressedSinceSweep.set(deviceId, (this.suppressedSinceSweep.get(deviceId) ?? 0) + result.suppressedIndices.size);
56778
56924
  if (result.reachSuppressedCount > 0) this.reachSuppressedSinceSweep.set(deviceId, (this.reachSuppressedSinceSweep.get(deviceId) ?? 0) + result.reachSuppressedCount);
56925
+ if (result.continuableIndices.size > 0) this.continuableSinceSweep.set(deviceId, (this.continuableSinceSweep.get(deviceId) ?? 0) + result.continuableIndices.size);
56779
56926
  }
56780
56927
  return {
56781
56928
  suppressedIndices: result.suppressedIndices,
56929
+ continuableIndices: result.continuableIndices,
56782
56930
  confirmed: result.confirmed,
56783
56931
  woken: result.woken,
56784
56932
  reachSuppressedCount: result.reachSuppressedCount
56785
56933
  };
56786
56934
  }
56935
+ /** The tracker's answer to {@link StationaryFrameFilterResult.continuableIndices}:
56936
+ * how many of this frame's weak holds continued a confirmed track (D410).
56937
+ * Counted here so the sweep line can report it beside `continuable`. */
56938
+ noteContinued(deviceId, count) {
56939
+ if (count <= 0) return;
56940
+ this.continuedSinceSweep.set(deviceId, (this.continuedSinceSweep.get(deviceId) ?? 0) + count);
56941
+ }
56787
56942
  /** Pending departure strikes for a device — the read-only diagnostic's whole
56788
56943
  * reason to exist. Ordered oldest first so "what has been waiting" reads
56789
56944
  * straight off the top. */
@@ -56922,6 +57077,8 @@ var StationaryObjectRegistry = class {
56922
57077
  absorbed,
56923
57078
  suppressed,
56924
57079
  reach: this.reachSuppressedSinceSweep.get(deviceId) ?? 0,
57080
+ continuable: this.continuableSinceSweep.get(deviceId) ?? 0,
57081
+ continued: this.continuedSinceSweep.get(deviceId) ?? 0,
56925
57082
  entries: this.count(deviceId)
56926
57083
  }
56927
57084
  });
@@ -56929,6 +57086,8 @@ var StationaryObjectRegistry = class {
56929
57086
  this.absorbedSinceSweep.clear();
56930
57087
  this.suppressedSinceSweep.clear();
56931
57088
  this.reachSuppressedSinceSweep.clear();
57089
+ this.continuableSinceSweep.clear();
57090
+ this.continuedSinceSweep.clear();
56932
57091
  for (const [deviceId, counters] of this.strikeCountersByDevice) this.logger.info("stationary departure strikes", {
56933
57092
  tags: { deviceId },
56934
57093
  meta: {
@@ -70578,6 +70737,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70578
70737
  * a ≤640 RAM fallback (`tier: 'ram-fullframe'`). Null until init completes. */
70579
70738
  getNativeKeyFrameRgb = null;
70580
70739
  shuttingDown = false;
70740
+ /**
70741
+ * Per-device delivery-lag windows, folded at `handleInferenceResult` entry.
70742
+ * The instrument that tells a late DELIVERY (starved) apart from a slow
70743
+ * `processFrame` (waiting) — see `frame-lag-observer.ts`. Costs one map read
70744
+ * and ~10 numeric ops per frame, and writes one line per device per 30 s.
70745
+ */
70746
+ frameLag = new FrameLagObserver();
70747
+ /**
70748
+ * Stops the GC-attributing stall monitor armed in {@link onInitialize}.
70749
+ *
70750
+ * The other half of the frame-lag line: that one says whether the events
70751
+ * arrived late, this one says whether THIS loop was blocked while they did.
70752
+ * The 5-minute `[mem]` heartbeat cannot see a 10 s freeze — it fired at
70753
+ * dt = 300020 / 300066 / 299991 ms straight through the 2026-09-09 silences.
70754
+ */
70755
+ stopStallMonitor = null;
70581
70756
  /** True only on the cluster's designated post-processing node. When false the
70582
70757
  * addon subscribes to NOTHING — fully inert (no event/media generation). */
70583
70758
  isPostProcessingNode = true;
@@ -70796,6 +70971,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70796
70971
  super({});
70797
70972
  }
70798
70973
  async onInitialize() {
70974
+ this.stopStallMonitor = startEventLoopStallMonitor(this.ctx.logger);
70799
70975
  const rawApi = this.ctx.api;
70800
70976
  if (!rawApi) throw new Error("pipeline-analytics requires ctx.api (device-manager + settings-store)");
70801
70977
  const api = censusApi(rawApi, this.readCensus);
@@ -73300,6 +73476,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73300
73476
  }
73301
73477
  async onShutdown() {
73302
73478
  this.shuttingDown = true;
73479
+ if (this.stopStallMonitor) {
73480
+ this.stopStallMonitor();
73481
+ this.stopStallMonitor = null;
73482
+ }
73303
73483
  this.sceneEngine?.stop();
73304
73484
  this.sceneEngine = null;
73305
73485
  if (this.sceneReloadTimer !== null) {
@@ -73344,6 +73524,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73344
73524
  this.overlaySynthesisWarnAt.clear();
73345
73525
  this.processors.clear();
73346
73526
  this.lastActiveTrackIds.clear();
73527
+ this.frameLag.clear();
73347
73528
  this.lastFrameDimsByDevice.clear();
73348
73529
  this.dropoutSkipsByKey.clear();
73349
73530
  this.residents.clearAll();
@@ -73383,6 +73564,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73383
73564
  async handleInferenceResult(data) {
73384
73565
  if (this.shuttingDown) return;
73385
73566
  const { deviceId, frame } = data;
73567
+ const lag = this.frameLag.observe(deviceId, Date.now(), data.emittedAt, data.capturedAt);
73568
+ if (lag !== null) {
73569
+ const verdict = lagVerdict(lag);
73570
+ (isAlarmingLagReport(lag) ? this.ctx.logger.warn.bind(this.ctx.logger) : this.ctx.logger.info.bind(this.ctx.logger))("pipeline-analytics frame lag", {
73571
+ tags: { deviceId },
73572
+ meta: {
73573
+ verdict,
73574
+ frames: lag.frames,
73575
+ windowMs: lag.windowMs,
73576
+ busLagMaxMs: lag.busLagMaxMs,
73577
+ busLagMinMs: lag.busLagMinMs,
73578
+ busLagUnknownFrames: lag.busLagUnknownFrames,
73579
+ captureLagMaxMs: lag.captureLagMaxMs,
73580
+ captureLagUnknownFrames: lag.captureLagUnknownFrames
73581
+ }
73582
+ });
73583
+ }
73386
73584
  await this.processFrame(deviceId, frame, "pipeline", data.frameHandle, data.detailSteps);
73387
73585
  }
73388
73586
  /**
@@ -73499,6 +73697,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73499
73697
  woken: result.stationaryWoken,
73500
73698
  timestamp: result.timestamp
73501
73699
  });
73700
+ if (this.stationaryRegistry && result.stationaryContinued > 0) this.stationaryRegistry.noteContinued(deviceId, result.stationaryContinued);
73502
73701
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
73503
73702
  const stationaryAsTracked = stationaryViews.map((v) => ({
73504
73703
  trackId: `stationary:${v.id}`,
@@ -76869,6 +77068,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
76869
77068
  const s = this.stationarySettingsFromCache(deviceId);
76870
77069
  if (!s.enabled) return {
76871
77070
  suppressedIndices: /* @__PURE__ */ new Set(),
77071
+ continuableIndices: /* @__PURE__ */ new Set(),
76872
77072
  confirmed: [],
76873
77073
  woken: []
76874
77074
  };
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-utVZjlcV.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-TUlCd-Zk.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.212",
3
+ "version": "1.2.214",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",