@camstack/addon-post-analysis 1.2.212 → 1.2.213

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.92",
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.172",
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.140",
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.172",
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.92",
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.140",
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");
@@ -34861,6 +34861,9 @@ var DEFAULT_TRACKER_CONFIG = {
34861
34861
  var PERSON_CLASS = "person";
34862
34862
  var ANIMAL_CLASS = "animal";
34863
34863
  var MAX_PATH_LENGTH = 300;
34864
+ /** No stationary-held detections on this frame — the default for every caller
34865
+ * that has no gate (replay, tests, the non-pipeline planes). */
34866
+ var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
34864
34867
  function clamp$1(value, min, max) {
34865
34868
  return Math.max(min, Math.min(max, value));
34866
34869
  }
@@ -35480,13 +35483,15 @@ var SortTracker = class SortTracker {
35480
35483
  * gates) so the two-tier ByteTrack association stays a single implementation.
35481
35484
  * Mutates `used` / `matchedTracks` / `matched` in place.
35482
35485
  */
35483
- greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
35486
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
35484
35487
  const pairs = [];
35485
35488
  for (const track of this.tracks) {
35486
35489
  if (matchedTracks.has(track)) continue;
35487
35490
  const pbox = this.predicted(track, timestamp);
35491
+ const confirmed = this.isConfirmedForEmit(track);
35488
35492
  for (const di of candidateIdxs) {
35489
35493
  if (used.has(di)) continue;
35494
+ if (!confirmed && continuationOnly.has(di)) continue;
35490
35495
  const det = detections[di];
35491
35496
  if (!this.associable(track.class, det)) continue;
35492
35497
  const score = iou$3(pbox, det.bbox);
@@ -35507,6 +35512,7 @@ var SortTracker = class SortTracker {
35507
35512
  }
35508
35513
  update(detections, timestamp, frameContext) {
35509
35514
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
35515
+ const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
35510
35516
  const resurrectedScene = /* @__PURE__ */ new Map();
35511
35517
  if (this.config.maxTrackLifetimeMs > 0) {
35512
35518
  const alive = [];
@@ -35543,12 +35549,12 @@ var SortTracker = class SortTracker {
35543
35549
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
35544
35550
  else highIdxs.push(di);
35545
35551
  const lowSet = new Set(lowIdxs);
35546
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
35552
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35547
35553
  const rescuePairs = [];
35548
35554
  for (const track of this.tracks) {
35549
35555
  if (matchedTracks.has(track)) continue;
35550
35556
  for (let di = 0; di < detections.length; di++) {
35551
- if (used.has(di) || lowSet.has(di)) continue;
35557
+ if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
35552
35558
  const det = detections[di];
35553
35559
  const gate = this.looseMatch(track, det, timestamp);
35554
35560
  if (gate === null) continue;
@@ -35570,7 +35576,7 @@ var SortTracker = class SortTracker {
35570
35576
  used.add(pair.detIdx);
35571
35577
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
35572
35578
  }
35573
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
35579
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35574
35580
  for (const [track, det] of matched) {
35575
35581
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
35576
35582
  const prevCenter = bboxCentroid({
@@ -35626,7 +35632,7 @@ var SortTracker = class SortTracker {
35626
35632
  } else surviving.push(track);
35627
35633
  }
35628
35634
  for (let di = 0; di < detections.length; di++) {
35629
- if (used.has(di)) continue;
35635
+ if (used.has(di) || continuationOnly.has(di)) continue;
35630
35636
  const det = detections[di];
35631
35637
  let best;
35632
35638
  let bestScore = -1;
@@ -35694,6 +35700,11 @@ var SortTracker = class SortTracker {
35694
35700
  });
35695
35701
  for (const di of unmatchedIdx) {
35696
35702
  const det = detections[di];
35703
+ if (continuationOnly.has(di)) {
35704
+ this.reportRefusal("stationary-hold", det);
35705
+ used.add(di);
35706
+ continue;
35707
+ }
35697
35708
  if (!this.meetsClassScoreFloor(det)) {
35698
35709
  this.reportRefusal("score-floor", det, {
35699
35710
  threshold: this.config.classMinScores[det.class] ?? 0,
@@ -36432,14 +36443,28 @@ var FrameProcessor = class {
36432
36443
  timestamp
36433
36444
  }) : {
36434
36445
  suppressedIndices: /* @__PURE__ */ new Set(),
36446
+ continuableIndices: /* @__PURE__ */ new Set(),
36435
36447
  confirmed: [],
36436
36448
  woken: []
36437
36449
  };
36438
- const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
36450
+ const trackerInput = [];
36451
+ const continuationOnly = /* @__PURE__ */ new Set();
36452
+ const continuableBoxes = /* @__PURE__ */ new Set();
36453
+ for (let i = 0; i < filteredDetections.length; i++) {
36454
+ const det = filteredDetections[i];
36455
+ if (gate.suppressedIndices.has(i)) {
36456
+ if (!gate.continuableIndices.has(i)) continue;
36457
+ continuationOnly.add(trackerInput.length);
36458
+ continuableBoxes.add(det.bbox);
36459
+ }
36460
+ trackerInput.push(det);
36461
+ }
36439
36462
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
36440
36463
  frameWidth,
36441
- frameHeight
36464
+ frameHeight,
36465
+ continuationOnly
36442
36466
  });
36467
+ const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
36443
36468
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
36444
36469
  const td = trackedDetections.find((t) => t.bbox === bbox && t.matchedThisFrame !== false);
36445
36470
  return td !== void 0 ? {
@@ -36643,6 +36668,7 @@ var FrameProcessor = class {
36643
36668
  rawTrackedDetections: trackedDetections,
36644
36669
  stationaryConfirmed: gate.confirmed,
36645
36670
  stationaryWoken: gate.woken,
36671
+ stationaryContinued,
36646
36672
  riderFolds
36647
36673
  };
36648
36674
  }
@@ -54989,6 +55015,31 @@ function thumbnailWouldShowParkedNotSubject(input) {
54989
55015
  * dropped, against every entry that did NOT wake this frame. Woken entries are
54990
55016
  * excluded so a departure still releases its object — a car that drives away is
54991
55017
  * noticed on the frame its second read lands, exactly as before.
55018
+ *
55019
+ * ## A weak hold may CONTINUE a track; only a strong read is the object (D410)
55020
+ *
55021
+ * D275 predicted that a moving vehicle overlapping a parked one "loses a frame
55022
+ * or two while it passes" and coasts through on `maxAge`. Measured on device
55023
+ * 617 on 2026-09-08 it lost the whole pass: the parked cars sit at the frame
55024
+ * edge, so a car driving across their front is held on every frame from the
55025
+ * first overlap to the moment it leaves the frame, and the tracker — which had
55026
+ * followed it for 5–10 frames — coasts `maxMissedMs` on nothing and drops it.
55027
+ * Four operator-marked tracks, 1.7–3.5 s old, died on exactly that geometry;
55028
+ * the sweep for the window reported `reach: 0`, so the hold was pass 1's: the
55029
+ * mover occludes the parked car, the detector reports one vehicle box, and it
55030
+ * is the entry's best match.
55031
+ *
55032
+ * The hold is right — that box covers the parked spot and must not OPEN a
55033
+ * track, which is the whole of the operator's requirement — but it is not the
55034
+ * parked car: a strong read (IoU ≥ `suppressIou`) is the parked object unmoved,
55035
+ * and everything below it is a box that merely overlaps the spot (swallow,
55036
+ * jitter, departing). So `suppressedIndices` still names every box that may
55037
+ * not spawn, and `continuableIndices` names the weak subset the tracker may use
55038
+ * to CONTINUE a track it has already confirmed — through its strict
55039
+ * predicted-box association only, never a rescue or a resurrection. The reach
55040
+ * pass reports strength the same way. Nothing about confirmation, anchoring or
55041
+ * departure changes: a weak hold confirms and never re-anchors, exactly as
55042
+ * before.
54992
55043
  */
54993
55044
  function partitionDetectionsAgainstRegistry(input) {
54994
55045
  const { entries, detections, config, now } = input;
@@ -54996,6 +55047,7 @@ function partitionDetectionsAgainstRegistry(input) {
54996
55047
  const departure = input.departure ?? DEFAULT_DEPARTURE_CONFIG;
54997
55048
  const warmUp = input.warmUp ?? false;
54998
55049
  const suppressed = /* @__PURE__ */ new Set();
55050
+ const continuable = /* @__PURE__ */ new Set();
54999
55051
  const confirmed = [];
55000
55052
  const woken = [];
55001
55053
  const strikeChanges = [];
@@ -55035,6 +55087,7 @@ function partitionDetectionsAgainstRegistry(input) {
55035
55087
  }
55036
55088
  if (holdIdx >= 0) {
55037
55089
  suppressed.add(holdIdx);
55090
+ if (!strong) continuable.add(holdIdx);
55038
55091
  confirmed.push({
55039
55092
  entryId: entry.id,
55040
55093
  className: entry.className,
@@ -55074,6 +55127,7 @@ function partitionDetectionsAgainstRegistry(input) {
55074
55127
  if (warmUp) {
55075
55128
  warmUpDepartureReads += 1;
55076
55129
  suppressed.add(bestIdx);
55130
+ continuable.add(bestIdx);
55077
55131
  continue;
55078
55132
  }
55079
55133
  if (strike === void 0) {
@@ -55086,6 +55140,7 @@ function partitionDetectionsAgainstRegistry(input) {
55086
55140
  reason: "armed"
55087
55141
  });
55088
55142
  suppressed.add(bestIdx);
55143
+ continuable.add(bestIdx);
55089
55144
  continue;
55090
55145
  }
55091
55146
  if (strikeAgeMs >= departure.confirmMinGapMs) {
@@ -55106,6 +55161,7 @@ function partitionDetectionsAgainstRegistry(input) {
55106
55161
  reason: "held"
55107
55162
  });
55108
55163
  suppressed.add(bestIdx);
55164
+ continuable.add(bestIdx);
55109
55165
  }
55110
55166
  const wokenIds = woken.length === 0 ? EMPTY_IDS : new Set(woken.map((w) => w.entryId));
55111
55167
  let reachSuppressedCount = 0;
@@ -55116,14 +55172,17 @@ function partitionDetectionsAgainstRegistry(input) {
55116
55172
  for (let ei = 0; ei < entries.length; ei++) {
55117
55173
  const entry = entries[ei];
55118
55174
  if (entryClasses[ei] !== detClass || wokenIds.has(entry.id)) continue;
55119
- if (!parkedEntryHoldsDetection(entry, det.bbox, config).holds) continue;
55175
+ const held = parkedEntryHoldsDetection(entry, det.bbox, config);
55176
+ if (!held.holds) continue;
55120
55177
  suppressed.add(di);
55178
+ if (!held.strong) continuable.add(di);
55121
55179
  reachSuppressedCount += 1;
55122
55180
  break;
55123
55181
  }
55124
55182
  }
55125
55183
  return {
55126
55184
  suppressedIndices: suppressed,
55185
+ continuableIndices: continuable,
55127
55186
  confirmed,
55128
55187
  woken,
55129
55188
  strikeChanges,
@@ -56747,6 +56806,10 @@ var StationaryObjectRegistry = class {
56747
56806
  */
56748
56807
  suppressedSinceSweep = /* @__PURE__ */ new Map();
56749
56808
  reachSuppressedSinceSweep = /* @__PURE__ */ new Map();
56809
+ /** Weak holds handed to the tracker as continuable (D410), per device. */
56810
+ continuableSinceSweep = /* @__PURE__ */ new Map();
56811
+ /** Of those, the ones the tracker reports actually continued a track. */
56812
+ continuedSinceSweep = /* @__PURE__ */ new Map();
56750
56813
  constructor(deps) {
56751
56814
  this.logger = deps.logger;
56752
56815
  this.ledger = new DurableLedger({
@@ -56830,6 +56893,7 @@ var StationaryObjectRegistry = class {
56830
56893
  const entries = this.list(deviceId);
56831
56894
  if (entries.length === 0) return {
56832
56895
  suppressedIndices: /* @__PURE__ */ new Set(),
56896
+ continuableIndices: /* @__PURE__ */ new Set(),
56833
56897
  confirmed: [],
56834
56898
  woken: [],
56835
56899
  reachSuppressedCount: 0
@@ -56850,14 +56914,23 @@ var StationaryObjectRegistry = class {
56850
56914
  if (result.suppressedIndices.size > 0) {
56851
56915
  this.suppressedSinceSweep.set(deviceId, (this.suppressedSinceSweep.get(deviceId) ?? 0) + result.suppressedIndices.size);
56852
56916
  if (result.reachSuppressedCount > 0) this.reachSuppressedSinceSweep.set(deviceId, (this.reachSuppressedSinceSweep.get(deviceId) ?? 0) + result.reachSuppressedCount);
56917
+ if (result.continuableIndices.size > 0) this.continuableSinceSweep.set(deviceId, (this.continuableSinceSweep.get(deviceId) ?? 0) + result.continuableIndices.size);
56853
56918
  }
56854
56919
  return {
56855
56920
  suppressedIndices: result.suppressedIndices,
56921
+ continuableIndices: result.continuableIndices,
56856
56922
  confirmed: result.confirmed,
56857
56923
  woken: result.woken,
56858
56924
  reachSuppressedCount: result.reachSuppressedCount
56859
56925
  };
56860
56926
  }
56927
+ /** The tracker's answer to {@link StationaryFrameFilterResult.continuableIndices}:
56928
+ * how many of this frame's weak holds continued a confirmed track (D410).
56929
+ * Counted here so the sweep line can report it beside `continuable`. */
56930
+ noteContinued(deviceId, count) {
56931
+ if (count <= 0) return;
56932
+ this.continuedSinceSweep.set(deviceId, (this.continuedSinceSweep.get(deviceId) ?? 0) + count);
56933
+ }
56861
56934
  /** Pending departure strikes for a device — the read-only diagnostic's whole
56862
56935
  * reason to exist. Ordered oldest first so "what has been waiting" reads
56863
56936
  * straight off the top. */
@@ -56996,6 +57069,8 @@ var StationaryObjectRegistry = class {
56996
57069
  absorbed,
56997
57070
  suppressed,
56998
57071
  reach: this.reachSuppressedSinceSweep.get(deviceId) ?? 0,
57072
+ continuable: this.continuableSinceSweep.get(deviceId) ?? 0,
57073
+ continued: this.continuedSinceSweep.get(deviceId) ?? 0,
56999
57074
  entries: this.count(deviceId)
57000
57075
  }
57001
57076
  });
@@ -57003,6 +57078,8 @@ var StationaryObjectRegistry = class {
57003
57078
  this.absorbedSinceSweep.clear();
57004
57079
  this.suppressedSinceSweep.clear();
57005
57080
  this.reachSuppressedSinceSweep.clear();
57081
+ this.continuableSinceSweep.clear();
57082
+ this.continuedSinceSweep.clear();
57006
57083
  for (const [deviceId, counters] of this.strikeCountersByDevice) this.logger.info("stationary departure strikes", {
57007
57084
  tags: { deviceId },
57008
57085
  meta: {
@@ -73573,6 +73650,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73573
73650
  woken: result.stationaryWoken,
73574
73651
  timestamp: result.timestamp
73575
73652
  });
73653
+ if (this.stationaryRegistry && result.stationaryContinued > 0) this.stationaryRegistry.noteContinued(deviceId, result.stationaryContinued);
73576
73654
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
73577
73655
  const stationaryAsTracked = stationaryViews.map((v) => ({
73578
73656
  trackId: `stationary:${v.id}`,
@@ -76943,6 +77021,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
76943
77021
  const s = this.stationarySettingsFromCache(deviceId);
76944
77022
  if (!s.enabled) return {
76945
77023
  suppressedIndices: /* @__PURE__ */ new Set(),
77024
+ continuableIndices: /* @__PURE__ */ new Set(),
76946
77025
  confirmed: [],
76947
77026
  woken: []
76948
77027
  };
@@ -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";
@@ -34836,6 +34836,9 @@ var DEFAULT_TRACKER_CONFIG = {
34836
34836
  var PERSON_CLASS = "person";
34837
34837
  var ANIMAL_CLASS = "animal";
34838
34838
  var MAX_PATH_LENGTH = 300;
34839
+ /** No stationary-held detections on this frame — the default for every caller
34840
+ * that has no gate (replay, tests, the non-pipeline planes). */
34841
+ var EMPTY_INDEX_SET = /* @__PURE__ */ new Set();
34839
34842
  function clamp$1(value, min, max) {
34840
34843
  return Math.max(min, Math.min(max, value));
34841
34844
  }
@@ -35455,13 +35458,15 @@ var SortTracker = class SortTracker {
35455
35458
  * gates) so the two-tier ByteTrack association stays a single implementation.
35456
35459
  * Mutates `used` / `matchedTracks` / `matched` in place.
35457
35460
  */
35458
- greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
35461
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly) {
35459
35462
  const pairs = [];
35460
35463
  for (const track of this.tracks) {
35461
35464
  if (matchedTracks.has(track)) continue;
35462
35465
  const pbox = this.predicted(track, timestamp);
35466
+ const confirmed = this.isConfirmedForEmit(track);
35463
35467
  for (const di of candidateIdxs) {
35464
35468
  if (used.has(di)) continue;
35469
+ if (!confirmed && continuationOnly.has(di)) continue;
35465
35470
  const det = detections[di];
35466
35471
  if (!this.associable(track.class, det)) continue;
35467
35472
  const score = iou$3(pbox, det.bbox);
@@ -35482,6 +35487,7 @@ var SortTracker = class SortTracker {
35482
35487
  }
35483
35488
  update(detections, timestamp, frameContext) {
35484
35489
  const frameArea = frameContext ? frameContext.frameWidth * frameContext.frameHeight : 0;
35490
+ const continuationOnly = frameContext?.continuationOnly ?? EMPTY_INDEX_SET;
35485
35491
  const resurrectedScene = /* @__PURE__ */ new Map();
35486
35492
  if (this.config.maxTrackLifetimeMs > 0) {
35487
35493
  const alive = [];
@@ -35518,12 +35524,12 @@ var SortTracker = class SortTracker {
35518
35524
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
35519
35525
  else highIdxs.push(di);
35520
35526
  const lowSet = new Set(lowIdxs);
35521
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
35527
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35522
35528
  const rescuePairs = [];
35523
35529
  for (const track of this.tracks) {
35524
35530
  if (matchedTracks.has(track)) continue;
35525
35531
  for (let di = 0; di < detections.length; di++) {
35526
- if (used.has(di) || lowSet.has(di)) continue;
35532
+ if (used.has(di) || lowSet.has(di) || continuationOnly.has(di)) continue;
35527
35533
  const det = detections[di];
35528
35534
  const gate = this.looseMatch(track, det, timestamp);
35529
35535
  if (gate === null) continue;
@@ -35545,7 +35551,7 @@ var SortTracker = class SortTracker {
35545
35551
  used.add(pair.detIdx);
35546
35552
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
35547
35553
  }
35548
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
35554
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched, continuationOnly);
35549
35555
  for (const [track, det] of matched) {
35550
35556
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
35551
35557
  const prevCenter = bboxCentroid({
@@ -35601,7 +35607,7 @@ var SortTracker = class SortTracker {
35601
35607
  } else surviving.push(track);
35602
35608
  }
35603
35609
  for (let di = 0; di < detections.length; di++) {
35604
- if (used.has(di)) continue;
35610
+ if (used.has(di) || continuationOnly.has(di)) continue;
35605
35611
  const det = detections[di];
35606
35612
  let best;
35607
35613
  let bestScore = -1;
@@ -35669,6 +35675,11 @@ var SortTracker = class SortTracker {
35669
35675
  });
35670
35676
  for (const di of unmatchedIdx) {
35671
35677
  const det = detections[di];
35678
+ if (continuationOnly.has(di)) {
35679
+ this.reportRefusal("stationary-hold", det);
35680
+ used.add(di);
35681
+ continue;
35682
+ }
35672
35683
  if (!this.meetsClassScoreFloor(det)) {
35673
35684
  this.reportRefusal("score-floor", det, {
35674
35685
  threshold: this.config.classMinScores[det.class] ?? 0,
@@ -36407,14 +36418,28 @@ var FrameProcessor = class {
36407
36418
  timestamp
36408
36419
  }) : {
36409
36420
  suppressedIndices: /* @__PURE__ */ new Set(),
36421
+ continuableIndices: /* @__PURE__ */ new Set(),
36410
36422
  confirmed: [],
36411
36423
  woken: []
36412
36424
  };
36413
- const trackerInput = gate.suppressedIndices.size > 0 ? filteredDetections.filter((_, i) => !gate.suppressedIndices.has(i)) : filteredDetections;
36425
+ const trackerInput = [];
36426
+ const continuationOnly = /* @__PURE__ */ new Set();
36427
+ const continuableBoxes = /* @__PURE__ */ new Set();
36428
+ for (let i = 0; i < filteredDetections.length; i++) {
36429
+ const det = filteredDetections[i];
36430
+ if (gate.suppressedIndices.has(i)) {
36431
+ if (!gate.continuableIndices.has(i)) continue;
36432
+ continuationOnly.add(trackerInput.length);
36433
+ continuableBoxes.add(det.bbox);
36434
+ }
36435
+ trackerInput.push(det);
36436
+ }
36414
36437
  const trackedDetections = this.tracker.update(trackerInput, timestamp, {
36415
36438
  frameWidth,
36416
- frameHeight
36439
+ frameHeight,
36440
+ continuationOnly
36417
36441
  });
36442
+ const stationaryContinued = continuableBoxes.size === 0 ? 0 : trackedDetections.filter((t) => t.matchedThisFrame !== false && continuableBoxes.has(t.bbox)).length;
36418
36443
  const riderFolds = foldByVehicleBbox.size === 0 ? EMPTY_RIDER_FOLDS : [...foldByVehicleBbox].map(([bbox, fold]) => {
36419
36444
  const td = trackedDetections.find((t) => t.bbox === bbox && t.matchedThisFrame !== false);
36420
36445
  return td !== void 0 ? {
@@ -36618,6 +36643,7 @@ var FrameProcessor = class {
36618
36643
  rawTrackedDetections: trackedDetections,
36619
36644
  stationaryConfirmed: gate.confirmed,
36620
36645
  stationaryWoken: gate.woken,
36646
+ stationaryContinued,
36621
36647
  riderFolds
36622
36648
  };
36623
36649
  }
@@ -54915,6 +54941,31 @@ function thumbnailWouldShowParkedNotSubject(input) {
54915
54941
  * dropped, against every entry that did NOT wake this frame. Woken entries are
54916
54942
  * excluded so a departure still releases its object — a car that drives away is
54917
54943
  * noticed on the frame its second read lands, exactly as before.
54944
+ *
54945
+ * ## A weak hold may CONTINUE a track; only a strong read is the object (D410)
54946
+ *
54947
+ * D275 predicted that a moving vehicle overlapping a parked one "loses a frame
54948
+ * or two while it passes" and coasts through on `maxAge`. Measured on device
54949
+ * 617 on 2026-09-08 it lost the whole pass: the parked cars sit at the frame
54950
+ * edge, so a car driving across their front is held on every frame from the
54951
+ * first overlap to the moment it leaves the frame, and the tracker — which had
54952
+ * followed it for 5–10 frames — coasts `maxMissedMs` on nothing and drops it.
54953
+ * Four operator-marked tracks, 1.7–3.5 s old, died on exactly that geometry;
54954
+ * the sweep for the window reported `reach: 0`, so the hold was pass 1's: the
54955
+ * mover occludes the parked car, the detector reports one vehicle box, and it
54956
+ * is the entry's best match.
54957
+ *
54958
+ * The hold is right — that box covers the parked spot and must not OPEN a
54959
+ * track, which is the whole of the operator's requirement — but it is not the
54960
+ * parked car: a strong read (IoU ≥ `suppressIou`) is the parked object unmoved,
54961
+ * and everything below it is a box that merely overlaps the spot (swallow,
54962
+ * jitter, departing). So `suppressedIndices` still names every box that may
54963
+ * not spawn, and `continuableIndices` names the weak subset the tracker may use
54964
+ * to CONTINUE a track it has already confirmed — through its strict
54965
+ * predicted-box association only, never a rescue or a resurrection. The reach
54966
+ * pass reports strength the same way. Nothing about confirmation, anchoring or
54967
+ * departure changes: a weak hold confirms and never re-anchors, exactly as
54968
+ * before.
54918
54969
  */
54919
54970
  function partitionDetectionsAgainstRegistry(input) {
54920
54971
  const { entries, detections, config, now } = input;
@@ -54922,6 +54973,7 @@ function partitionDetectionsAgainstRegistry(input) {
54922
54973
  const departure = input.departure ?? DEFAULT_DEPARTURE_CONFIG;
54923
54974
  const warmUp = input.warmUp ?? false;
54924
54975
  const suppressed = /* @__PURE__ */ new Set();
54976
+ const continuable = /* @__PURE__ */ new Set();
54925
54977
  const confirmed = [];
54926
54978
  const woken = [];
54927
54979
  const strikeChanges = [];
@@ -54961,6 +55013,7 @@ function partitionDetectionsAgainstRegistry(input) {
54961
55013
  }
54962
55014
  if (holdIdx >= 0) {
54963
55015
  suppressed.add(holdIdx);
55016
+ if (!strong) continuable.add(holdIdx);
54964
55017
  confirmed.push({
54965
55018
  entryId: entry.id,
54966
55019
  className: entry.className,
@@ -55000,6 +55053,7 @@ function partitionDetectionsAgainstRegistry(input) {
55000
55053
  if (warmUp) {
55001
55054
  warmUpDepartureReads += 1;
55002
55055
  suppressed.add(bestIdx);
55056
+ continuable.add(bestIdx);
55003
55057
  continue;
55004
55058
  }
55005
55059
  if (strike === void 0) {
@@ -55012,6 +55066,7 @@ function partitionDetectionsAgainstRegistry(input) {
55012
55066
  reason: "armed"
55013
55067
  });
55014
55068
  suppressed.add(bestIdx);
55069
+ continuable.add(bestIdx);
55015
55070
  continue;
55016
55071
  }
55017
55072
  if (strikeAgeMs >= departure.confirmMinGapMs) {
@@ -55032,6 +55087,7 @@ function partitionDetectionsAgainstRegistry(input) {
55032
55087
  reason: "held"
55033
55088
  });
55034
55089
  suppressed.add(bestIdx);
55090
+ continuable.add(bestIdx);
55035
55091
  }
55036
55092
  const wokenIds = woken.length === 0 ? EMPTY_IDS : new Set(woken.map((w) => w.entryId));
55037
55093
  let reachSuppressedCount = 0;
@@ -55042,14 +55098,17 @@ function partitionDetectionsAgainstRegistry(input) {
55042
55098
  for (let ei = 0; ei < entries.length; ei++) {
55043
55099
  const entry = entries[ei];
55044
55100
  if (entryClasses[ei] !== detClass || wokenIds.has(entry.id)) continue;
55045
- if (!parkedEntryHoldsDetection(entry, det.bbox, config).holds) continue;
55101
+ const held = parkedEntryHoldsDetection(entry, det.bbox, config);
55102
+ if (!held.holds) continue;
55046
55103
  suppressed.add(di);
55104
+ if (!held.strong) continuable.add(di);
55047
55105
  reachSuppressedCount += 1;
55048
55106
  break;
55049
55107
  }
55050
55108
  }
55051
55109
  return {
55052
55110
  suppressedIndices: suppressed,
55111
+ continuableIndices: continuable,
55053
55112
  confirmed,
55054
55113
  woken,
55055
55114
  strikeChanges,
@@ -56673,6 +56732,10 @@ var StationaryObjectRegistry = class {
56673
56732
  */
56674
56733
  suppressedSinceSweep = /* @__PURE__ */ new Map();
56675
56734
  reachSuppressedSinceSweep = /* @__PURE__ */ new Map();
56735
+ /** Weak holds handed to the tracker as continuable (D410), per device. */
56736
+ continuableSinceSweep = /* @__PURE__ */ new Map();
56737
+ /** Of those, the ones the tracker reports actually continued a track. */
56738
+ continuedSinceSweep = /* @__PURE__ */ new Map();
56676
56739
  constructor(deps) {
56677
56740
  this.logger = deps.logger;
56678
56741
  this.ledger = new DurableLedger({
@@ -56756,6 +56819,7 @@ var StationaryObjectRegistry = class {
56756
56819
  const entries = this.list(deviceId);
56757
56820
  if (entries.length === 0) return {
56758
56821
  suppressedIndices: /* @__PURE__ */ new Set(),
56822
+ continuableIndices: /* @__PURE__ */ new Set(),
56759
56823
  confirmed: [],
56760
56824
  woken: [],
56761
56825
  reachSuppressedCount: 0
@@ -56776,14 +56840,23 @@ var StationaryObjectRegistry = class {
56776
56840
  if (result.suppressedIndices.size > 0) {
56777
56841
  this.suppressedSinceSweep.set(deviceId, (this.suppressedSinceSweep.get(deviceId) ?? 0) + result.suppressedIndices.size);
56778
56842
  if (result.reachSuppressedCount > 0) this.reachSuppressedSinceSweep.set(deviceId, (this.reachSuppressedSinceSweep.get(deviceId) ?? 0) + result.reachSuppressedCount);
56843
+ if (result.continuableIndices.size > 0) this.continuableSinceSweep.set(deviceId, (this.continuableSinceSweep.get(deviceId) ?? 0) + result.continuableIndices.size);
56779
56844
  }
56780
56845
  return {
56781
56846
  suppressedIndices: result.suppressedIndices,
56847
+ continuableIndices: result.continuableIndices,
56782
56848
  confirmed: result.confirmed,
56783
56849
  woken: result.woken,
56784
56850
  reachSuppressedCount: result.reachSuppressedCount
56785
56851
  };
56786
56852
  }
56853
+ /** The tracker's answer to {@link StationaryFrameFilterResult.continuableIndices}:
56854
+ * how many of this frame's weak holds continued a confirmed track (D410).
56855
+ * Counted here so the sweep line can report it beside `continuable`. */
56856
+ noteContinued(deviceId, count) {
56857
+ if (count <= 0) return;
56858
+ this.continuedSinceSweep.set(deviceId, (this.continuedSinceSweep.get(deviceId) ?? 0) + count);
56859
+ }
56787
56860
  /** Pending departure strikes for a device — the read-only diagnostic's whole
56788
56861
  * reason to exist. Ordered oldest first so "what has been waiting" reads
56789
56862
  * straight off the top. */
@@ -56922,6 +56995,8 @@ var StationaryObjectRegistry = class {
56922
56995
  absorbed,
56923
56996
  suppressed,
56924
56997
  reach: this.reachSuppressedSinceSweep.get(deviceId) ?? 0,
56998
+ continuable: this.continuableSinceSweep.get(deviceId) ?? 0,
56999
+ continued: this.continuedSinceSweep.get(deviceId) ?? 0,
56925
57000
  entries: this.count(deviceId)
56926
57001
  }
56927
57002
  });
@@ -56929,6 +57004,8 @@ var StationaryObjectRegistry = class {
56929
57004
  this.absorbedSinceSweep.clear();
56930
57005
  this.suppressedSinceSweep.clear();
56931
57006
  this.reachSuppressedSinceSweep.clear();
57007
+ this.continuableSinceSweep.clear();
57008
+ this.continuedSinceSweep.clear();
56932
57009
  for (const [deviceId, counters] of this.strikeCountersByDevice) this.logger.info("stationary departure strikes", {
56933
57010
  tags: { deviceId },
56934
57011
  meta: {
@@ -73499,6 +73576,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73499
73576
  woken: result.stationaryWoken,
73500
73577
  timestamp: result.timestamp
73501
73578
  });
73579
+ if (this.stationaryRegistry && result.stationaryContinued > 0) this.stationaryRegistry.noteContinued(deviceId, result.stationaryContinued);
73502
73580
  const stationaryViews = this.stationaryRegistry?.listViews(deviceId) ?? [];
73503
73581
  const stationaryAsTracked = stationaryViews.map((v) => ({
73504
73582
  trackId: `stationary:${v.id}`,
@@ -76869,6 +76947,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
76869
76947
  const s = this.stationarySettingsFromCache(deviceId);
76870
76948
  if (!s.enabled) return {
76871
76949
  suppressedIndices: /* @__PURE__ */ new Set(),
76950
+ continuableIndices: /* @__PURE__ */ new Set(),
76872
76951
  confirmed: [],
76873
76952
  woken: []
76874
76953
  };
@@ -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-CxVox7bu.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.213",
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",