@camstack/addon-post-analysis 1.2.177 → 1.2.179

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.
@@ -13236,6 +13236,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
13236
13236
  kind: "mutation",
13237
13237
  auth: "admin"
13238
13238
  });
13239
+ /**
13240
+ * Device Manager capability — hub-side singleton that unifies device persistence,
13241
+ * live registry access, and all management operations into a single tRPC surface.
13242
+ *
13243
+ * Replaces:
13244
+ * - `device-persistence` capability (persistence methods absorbed here)
13245
+ * - `device-management.router.ts` (deleted in Phase 2)
13246
+ * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13247
+ *
13248
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13249
+ * fork into separate processes but never run on remote cluster agents. Therefore:
13250
+ * - No nodeId routing needed — this is a pure hub singleton.
13251
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
13252
+ * - No shadow registry or cross-node aggregation required.
13253
+ *
13254
+ * Forked workers register devices back to the hub via `ctx.devices`
13255
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
13256
+ */
13239
13257
  /** One child-placement directive on a container's `childLayout`. Structurally
13240
13258
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
13241
13259
  * shape for the same field. The child is identified by its re-sync-stable
@@ -19626,6 +19644,9 @@ var RetrainStatusSchema = _enum([
19626
19644
  *
19627
19645
  * `debug` does NOT pin; it is attention, not durability.
19628
19646
  *
19647
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19648
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19649
+ *
19629
19650
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19630
19651
  * A favourited track is skipped by retention the same way `staging` is, but
19631
19652
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19636,6 +19657,21 @@ var TrackFlagFields = {
19636
19657
  markForTrain: boolean().optional(),
19637
19658
  /** Operator marked this track for diagnostic attention. */
19638
19659
  debug: boolean().optional(),
19660
+ /**
19661
+ * What the operator wants CHECKED on this track, in their own words.
19662
+ *
19663
+ * The flag alone says "look at this" and nothing about what for; a debug set
19664
+ * reviewed hours later is a list of tracks with no question attached. Bound
19665
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19666
+ * and refuses it on a track whose debug is off — so a note can never outlive
19667
+ * the attention it belongs to.
19668
+ *
19669
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19670
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19671
+ * on without a note (a cancelled prompt) and what lets it edit the note
19672
+ * without touching the flag.
19673
+ */
19674
+ debugNote: string().max(500).optional(),
19639
19675
  /** Operator favourited this track. Pins it against pruning. */
19640
19676
  favourited: boolean().optional()
19641
19677
  };
@@ -19662,6 +19698,12 @@ var TrackFlagsSchema = object({
19662
19698
  trackId: string(),
19663
19699
  markForTrain: boolean(),
19664
19700
  debug: boolean(),
19701
+ /** The note as it STANDS after the write — never absent here (a track with no
19702
+ * note reports `''`), for the same reason the booleans are required: a
19703
+ * surface that has just written must be able to render the note it now owns
19704
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19705
+ * screen that has no previous value to keep. */
19706
+ debugNote: string(),
19665
19707
  favourited: boolean(),
19666
19708
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19667
19709
  * a track row) because this shape is only ever produced by the write body,
@@ -20954,11 +20996,20 @@ var pipelineAnalyticsCapability = {
20954
20996
  * staging is a deliberate action of the retrain page, which is also the only
20955
20997
  * thing that produces `trained` in the first place.
20956
20998
  *
20957
- * Returns the RESOLVED state of both flags (absent → `false`) plus the
20958
- * `retrainStatus` they were derived from, so a caller can drive its toggle
20959
- * — and render a `trained` badge — without a re-fetch. Rejects an unknown
20960
- * track, a new staging mark on a device already holding its full budget, and
20961
- * any `markForTrain` write against a trained track.
20999
+ * `debugNote` is the operator's sentence about WHAT to check on this track,
21000
+ * and it is bound to `debug`: writing `debug: false` clears it in the same
21001
+ * statement, and a note sent for a track whose debug is off (and is not
21002
+ * being turned on) is DROPPED with a warning rather than stored where
21003
+ * nothing will ever read it. An omitted `debugNote` leaves the stored one
21004
+ * alone — which is what lets a surface turn debug on when the operator
21005
+ * declined to type anything, without that refusal costing them the flag.
21006
+ *
21007
+ * Returns the RESOLVED state of all three flags (absent → `false`) plus the
21008
+ * note (absent → `''`) and the `retrainStatus` they were derived from, so a
21009
+ * caller can drive its toggle — and render a `trained` badge — without a
21010
+ * re-fetch. Rejects an unknown track, a new staging mark on a device already
21011
+ * holding its full budget, and any `markForTrain` write against a trained
21012
+ * track.
20962
21013
  */
20963
21014
  setTrackFlags: method(object({
20964
21015
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -13267,6 +13267,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
13267
13267
  kind: "mutation",
13268
13268
  auth: "admin"
13269
13269
  });
13270
+ /**
13271
+ * Device Manager capability — hub-side singleton that unifies device persistence,
13272
+ * live registry access, and all management operations into a single tRPC surface.
13273
+ *
13274
+ * Replaces:
13275
+ * - `device-persistence` capability (persistence methods absorbed here)
13276
+ * - `device-management.router.ts` (deleted in Phase 2)
13277
+ * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
13278
+ *
13279
+ * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
13280
+ * fork into separate processes but never run on remote cluster agents. Therefore:
13281
+ * - No nodeId routing needed — this is a pure hub singleton.
13282
+ * - The hub's DeviceRegistry is the single source of truth for all live devices.
13283
+ * - No shadow registry or cross-node aggregation required.
13284
+ *
13285
+ * Forked workers register devices back to the hub via `ctx.devices`
13286
+ * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
13287
+ */
13270
13288
  /** One child-placement directive on a container's `childLayout`. Structurally
13271
13289
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
13272
13290
  * shape for the same field. The child is identified by its re-sync-stable
@@ -19657,6 +19675,9 @@ var RetrainStatusSchema = _enum([
19657
19675
  *
19658
19676
  * `debug` does NOT pin; it is attention, not durability.
19659
19677
  *
19678
+ * `debugNote` is the operator's own words about WHAT should be checked on this
19679
+ * track, and it lives and dies with `debug` — see {@link MAX_TRACK_DEBUG_NOTE_LEN}.
19680
+ *
19660
19681
  * `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
19661
19682
  * A favourited track is skipped by retention the same way `staging` is, but
19662
19683
  * it does not enter `none|staging|trained` and has no staging budget.
@@ -19667,6 +19688,21 @@ var TrackFlagFields = {
19667
19688
  markForTrain: boolean().optional(),
19668
19689
  /** Operator marked this track for diagnostic attention. */
19669
19690
  debug: boolean().optional(),
19691
+ /**
19692
+ * What the operator wants CHECKED on this track, in their own words.
19693
+ *
19694
+ * The flag alone says "look at this" and nothing about what for; a debug set
19695
+ * reviewed hours later is a list of tracks with no question attached. Bound
19696
+ * to `debug` on the WRITE side — the body clears it when `debug` goes off,
19697
+ * and refuses it on a track whose debug is off — so a note can never outlive
19698
+ * the attention it belongs to.
19699
+ *
19700
+ * `''` is a real value ("marked, nothing to add"); the ABSENT key means
19701
+ * "leave whatever is stored alone", which is what lets a surface turn debug
19702
+ * on without a note (a cancelled prompt) and what lets it edit the note
19703
+ * without touching the flag.
19704
+ */
19705
+ debugNote: string().max(500).optional(),
19670
19706
  /** Operator favourited this track. Pins it against pruning. */
19671
19707
  favourited: boolean().optional()
19672
19708
  };
@@ -19693,6 +19729,12 @@ var TrackFlagsSchema = object({
19693
19729
  trackId: string(),
19694
19730
  markForTrain: boolean(),
19695
19731
  debug: boolean(),
19732
+ /** The note as it STANDS after the write — never absent here (a track with no
19733
+ * note reports `''`), for the same reason the booleans are required: a
19734
+ * surface that has just written must be able to render the note it now owns
19735
+ * without a re-fetch, and an absent key would read as "unchanged" on a
19736
+ * screen that has no previous value to keep. */
19737
+ debugNote: string(),
19696
19738
  favourited: boolean(),
19697
19739
  /** The lifecycle state the boolean was derived from. Required here (unlike on
19698
19740
  * a track row) because this shape is only ever produced by the write body,
@@ -20985,11 +21027,20 @@ var pipelineAnalyticsCapability = {
20985
21027
  * staging is a deliberate action of the retrain page, which is also the only
20986
21028
  * thing that produces `trained` in the first place.
20987
21029
  *
20988
- * Returns the RESOLVED state of both flags (absent → `false`) plus the
20989
- * `retrainStatus` they were derived from, so a caller can drive its toggle
20990
- * — and render a `trained` badge — without a re-fetch. Rejects an unknown
20991
- * track, a new staging mark on a device already holding its full budget, and
20992
- * any `markForTrain` write against a trained track.
21030
+ * `debugNote` is the operator's sentence about WHAT to check on this track,
21031
+ * and it is bound to `debug`: writing `debug: false` clears it in the same
21032
+ * statement, and a note sent for a track whose debug is off (and is not
21033
+ * being turned on) is DROPPED with a warning rather than stored where
21034
+ * nothing will ever read it. An omitted `debugNote` leaves the stored one
21035
+ * alone — which is what lets a surface turn debug on when the operator
21036
+ * declined to type anything, without that refusal costing them the flag.
21037
+ *
21038
+ * Returns the RESOLVED state of all three flags (absent → `false`) plus the
21039
+ * note (absent → `''`) and the `retrainStatus` they were derived from, so a
21040
+ * caller can drive its toggle — and render a `trained` badge — without a
21041
+ * re-fetch. Rejects an unknown track, a new staging mark on a device already
21042
+ * holding its full budget, and any `markForTrain` write against a trained
21043
+ * track.
20993
21044
  */
20994
21045
  setTrackFlags: method(object({
20995
21046
  /** Log/audit scope only — the trackId is globally unique on its own. */
@@ -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-BfdKgU7-.js");
5
+ const require_dist = require("../dist-QJ8XSy0F.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 BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-B1cFmGC0.mjs";
1
+ import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-NBkMwEZ0.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.66",
6
+ version: "1.2.67",
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.146",
21
+ version: "1.2.147",
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.112",
36
+ version: "1.2.113",
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.146",
39
+ version: "1.2.147",
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.66",
48
+ version: "1.2.67",
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.112",
84
+ version: "1.2.113",
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-BfdKgU7-.js");
5
+ const require_dist = require("../dist-QJ8XSy0F.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");
@@ -35839,16 +35839,43 @@ function positionsIntersectZone(positions, frameWidth, frameHeight, zone) {
35839
35839
  }, zone.points)) return true;
35840
35840
  return false;
35841
35841
  }
35842
+ //#endregion
35843
+ //#region src/pipeline-analytics/track-lifecycle.ts
35844
+ /** Minimum confidence increase (vs last emit) that counts as a material
35845
+ * improvement. */
35846
+ var UPDATE_MIN_CONF_DELTA = .1;
35847
+ /** Debounce floor between two `update` emits for the same track (ms). */
35848
+ var UPDATE_MIN_INTERVAL_MS = 4e3;
35849
+ /** Minimum fractional bbox-area growth (vs last emit) counted as a
35850
+ * materially larger representative crop (e.g. 0.15 = 15% bigger). */
35851
+ var UPDATE_MIN_CROP_AREA_GROWTH = .15;
35852
+ /**
35853
+ * The label a `phase:'update'` SIGNAL and PAYLOAD carry, resolved from its
35854
+ * two possible sources in order: this frame's tiered patch (the frame path
35855
+ * wrote a label this very frame), then the track ROW's stored display label
35856
+ * (`subLabel ?? label`, as `trackStore.peekActive` exposes it).
35857
+ *
35858
+ * The fallback is not a nicety. The face recognizer assigns an identity over
35859
+ * the async detail plane and writes it to the row (`applyLabelCandidate`) —
35860
+ * never into a frame patch — so a signal reading only the patch can never
35861
+ * fire the identity trigger for a recognised face. Measured on the live hub
35862
+ * (2026-09-05, device 590): six of six face identities reached Home
35863
+ * Assistant only on the `end` event at track close + sweep (~lastSeen+33 s),
35864
+ * zero during the track's life, while the subject was still on camera.
35865
+ */
35866
+ function resolveLifecycleUpdateLabel(labelPatch, storedDisplayLabel) {
35867
+ return patchDisplayLabel(labelPatch) ?? storedDisplayLabel;
35868
+ }
35842
35869
  var DEFAULT_UPDATE_GATE_CONFIG = {
35843
- minConfDelta: .1,
35844
- minIntervalMs: 4e3,
35845
- minCropAreaGrowth: .15
35870
+ minConfDelta: UPDATE_MIN_CONF_DELTA,
35871
+ minIntervalMs: UPDATE_MIN_INTERVAL_MS,
35872
+ minCropAreaGrowth: UPDATE_MIN_CROP_AREA_GROWTH
35846
35873
  };
35847
35874
  /**
35848
35875
  * Decide whether a `phase:'update'` event should fire this frame.
35849
35876
  *
35850
35877
  * Priority of triggers: newly-resolved identity/plate > confidence
35851
- * improvement > materially larger crop. All triggers are debounced by
35878
+ * improvement > newly-entered zone > materially larger crop. All triggers are debounced by
35852
35879
  * `minIntervalMs` relative to the last emit. Deltas are measured against
35853
35880
  * the LAST-EMITTED memory, so a run of sub-threshold new-bests accumulates
35854
35881
  * and fires once it crosses `minConfDelta`.
@@ -35856,22 +35883,32 @@ var DEFAULT_UPDATE_GATE_CONFIG = {
35856
35883
  function evaluateTrackLifecycleUpdate(signal, memory, config) {
35857
35884
  const identityTrigger = signal.label !== void 0 && signal.label !== "" && (memory === void 0 || memory.lastLabel !== signal.label);
35858
35885
  const confidenceTrigger = signal.isNewBest && (memory === void 0 || signal.confidence - memory.lastConfidence >= config.minConfDelta);
35886
+ const zoneTrigger = signal.zonesVisitedCount !== void 0 && memory?.lastZonesVisitedCount !== void 0 && signal.zonesVisitedCount > memory.lastZonesVisitedCount;
35859
35887
  const cropTrigger = signal.bboxArea !== void 0 && memory?.lastBboxArea !== void 0 && memory.lastBboxArea > 0 && signal.bboxArea >= memory.lastBboxArea * (1 + config.minCropAreaGrowth);
35860
- const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : cropTrigger ? "crop" : void 0;
35861
- const keptMemory = memory ?? {
35888
+ const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : zoneTrigger ? "zone" : cropTrigger ? "crop" : void 0;
35889
+ const keptMemory = memory === void 0 ? {
35862
35890
  lastConfidence: signal.confidence,
35863
- lastEmitAt: signal.now
35864
- };
35891
+ lastEmitAt: signal.now,
35892
+ ...signal.zonesVisitedCount !== void 0 ? { lastZonesVisitedCount: signal.zonesVisitedCount } : {}
35893
+ } : memory.lastZonesVisitedCount === void 0 && signal.zonesVisitedCount !== void 0 ? {
35894
+ ...memory,
35895
+ lastZonesVisitedCount: signal.zonesVisitedCount
35896
+ } : memory;
35865
35897
  if (reason === void 0) return {
35866
35898
  emit: false,
35867
35899
  memory: keptMemory
35868
35900
  };
35869
35901
  if (memory !== void 0 && signal.now - memory.lastEmitAt < config.minIntervalMs) return {
35870
35902
  emit: false,
35871
- memory
35903
+ suppressed: reason,
35904
+ memory: {
35905
+ ...keptMemory,
35906
+ suppressedSinceEmit: (keptMemory.suppressedSinceEmit ?? 0) + 1
35907
+ }
35872
35908
  };
35873
35909
  const nextLabel = signal.label ?? memory?.lastLabel;
35874
35910
  const nextBboxArea = signal.bboxArea ?? memory?.lastBboxArea;
35911
+ const nextZonesCount = signal.zonesVisitedCount ?? memory?.lastZonesVisitedCount;
35875
35912
  return {
35876
35913
  emit: true,
35877
35914
  reason,
@@ -35879,7 +35916,8 @@ function evaluateTrackLifecycleUpdate(signal, memory, config) {
35879
35916
  lastConfidence: memory === void 0 ? signal.confidence : Math.max(memory.lastConfidence, signal.confidence),
35880
35917
  lastEmitAt: signal.now,
35881
35918
  ...nextLabel !== void 0 ? { lastLabel: nextLabel } : {},
35882
- ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {}
35919
+ ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {},
35920
+ ...nextZonesCount !== void 0 ? { lastZonesVisitedCount: nextZonesCount } : {}
35883
35921
  }
35884
35922
  };
35885
35923
  }
@@ -39490,6 +39528,10 @@ var TRACKS_COLUMNS = [
39490
39528
  name: "debug",
39491
39529
  type: "BOOLEAN"
39492
39530
  },
39531
+ {
39532
+ name: "debugNote",
39533
+ type: "TEXT"
39534
+ },
39493
39535
  {
39494
39536
  name: "favourited",
39495
39537
  type: "BOOLEAN"
@@ -39685,6 +39727,7 @@ function cloneTrack(t) {
39685
39727
  markForTrain: t.retrainStatus === "staging"
39686
39728
  } : {},
39687
39729
  ...t.debug !== void 0 ? { debug: t.debug } : {},
39730
+ ...t.debugNote !== void 0 ? { debugNote: t.debugNote } : {},
39688
39731
  ...t.favourited !== void 0 ? { favourited: t.favourited } : {},
39689
39732
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
39690
39733
  ...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
@@ -39921,7 +39964,8 @@ var TrackStore = class {
39921
39964
  * Cheap read of an active track's promotion-relevant fields WITHOUT the deep
39922
39965
  * clone `getActiveByTrack` does — the returned `positions` is the live
39923
39966
  * internal array (read-only; callers must not mutate). Feeds the per-frame
39924
- * stationary-promotion check, called at inference fps. Null if unknown/expired.
39967
+ * stationary-promotion check AND the lifecycle update gate, both called at
39968
+ * inference fps. Null if unknown/expired.
39925
39969
  */
39926
39970
  peekActive(trackId) {
39927
39971
  const t = this.active.get(trackId);
@@ -39929,6 +39973,7 @@ var TrackStore = class {
39929
39973
  return {
39930
39974
  firstSeen: t.firstSeen,
39931
39975
  positions: t.positions,
39976
+ zonesVisitedCount: t.zonesVisited.length,
39932
39977
  ...displayLabel(t) !== void 0 ? { label: displayLabel(t) } : {}
39933
39978
  };
39934
39979
  }
@@ -40073,12 +40118,14 @@ var TrackStore = class {
40073
40118
  const nextStatus = patch.markForTrain === void 0 ? void 0 : patch.markForTrain ? "staging" : "none";
40074
40119
  if (nextStatus !== void 0) data["retrainStatus"] = nextStatus;
40075
40120
  if (patch.debug !== void 0) data["debug"] = patch.debug;
40121
+ if (patch.debugNote !== void 0) data["debugNote"] = patch.debugNote;
40076
40122
  if (patch.favourited !== void 0) data["favourited"] = patch.favourited;
40077
40123
  if (Object.keys(data).length === 0) return;
40078
40124
  const active = this.active.get(trackId);
40079
40125
  if (active) {
40080
40126
  if (nextStatus !== void 0) active.retrainStatus = nextStatus;
40081
40127
  if (patch.debug !== void 0) active.debug = patch.debug;
40128
+ if (patch.debugNote !== void 0) active.debugNote = patch.debugNote;
40082
40129
  if (patch.favourited !== void 0) active.favourited = patch.favourited;
40083
40130
  }
40084
40131
  try {
@@ -40281,6 +40328,7 @@ var TrackStore = class {
40281
40328
  if (active) return {
40282
40329
  retrainStatus: active.retrainStatus ?? "none",
40283
40330
  debug: active.debug === true,
40331
+ debugNote: active.debugNote ?? "",
40284
40332
  favourited: active.favourited === true
40285
40333
  };
40286
40334
  const persisted = await this.getPersistedByTrackId(trackId);
@@ -40288,6 +40336,7 @@ var TrackStore = class {
40288
40336
  return {
40289
40337
  retrainStatus: persisted.retrainStatus ?? "none",
40290
40338
  debug: persisted.debug === true,
40339
+ debugNote: persisted.debugNote ?? "",
40291
40340
  favourited: persisted.favourited === true
40292
40341
  };
40293
40342
  }
@@ -41187,6 +41236,7 @@ var TrackStore = class {
41187
41236
  ...t.audioLabels !== void 0 ? { audioLabels: [...t.audioLabels] } : {},
41188
41237
  ...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
41189
41238
  ...t.debug !== void 0 ? { debug: t.debug } : {},
41239
+ ...t.debugNote !== void 0 ? { debugNote: t.debugNote } : {},
41190
41240
  ...t.favourited !== void 0 ? { favourited: t.favourited } : {},
41191
41241
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
41192
41242
  ...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
@@ -41245,6 +41295,7 @@ var TrackStore = class {
41245
41295
  const audioLabels = readAudioLabels(data["audioLabels"]);
41246
41296
  const retrainParsed = require_dist.RetrainStatusSchema.safeParse(data["retrainStatus"]);
41247
41297
  const debug = data["debug"];
41298
+ const debugNote = data["debugNote"];
41248
41299
  const favourited = data["favourited"];
41249
41300
  const hasFace = data["hasFace"];
41250
41301
  /** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`.
@@ -41293,6 +41344,7 @@ var TrackStore = class {
41293
41344
  markForTrain: retrainParsed.data === "staging"
41294
41345
  } : {},
41295
41346
  ...typeof debug === "boolean" ? { debug } : {},
41347
+ ...typeof debugNote === "string" ? { debugNote } : {},
41296
41348
  ...typeof favourited === "boolean" ? { favourited } : {},
41297
41349
  ...typeof hasFace === "boolean" ? { hasFace } : {},
41298
41350
  ...typeof hasEmbeddedFace === "boolean" ? { hasEmbeddedFace } : {},
@@ -54864,6 +54916,47 @@ var TrackAlreadyTrainedError = class extends Error {
54864
54916
  }
54865
54917
  };
54866
54918
  /**
54919
+ * The `debugNote` half of the patch — `{}` when nothing should be written.
54920
+ *
54921
+ * The note is not an independent column: it is the operator's sentence about
54922
+ * why `debug` is on, so it exists exactly as long as the flag does. Three
54923
+ * cases, and the order matters:
54924
+ *
54925
+ * 1. **`debug: false` in the patch → the note is CLEARED**, whatever the
54926
+ * caller sent with it. A note surviving the flag is a leftover that reads
54927
+ * as a live request, and the invariant belongs here rather than in the
54928
+ * three surfaces that write this patch — one of which is a browser grid
54929
+ * that has no note UI at all and would never think to clear it.
54930
+ * 2. **No note in the patch → nothing is written.** This is what makes a
54931
+ * cancelled prompt free: the app turns debug on, the operator declines to
54932
+ * type, and the flag still lands. Refusing to write a note must never cost
54933
+ * the capture that was asked for.
54934
+ * 3. **A note, on a track whose debug is off and is not being turned on →
54935
+ * DROPPED.** Storing it would put the operator's words somewhere no reader
54936
+ * of a debug set will ever look. Dropped work is logged, per-camera: a
54937
+ * branch that silently discards a sentence is indistinguishable from one
54938
+ * that stored it.
54939
+ *
54940
+ * `''` is a WRITE, not an absence — clearing a note the operator no longer
54941
+ * stands behind is a real edit.
54942
+ */
54943
+ function resolveNotePatch(deps, input, current) {
54944
+ if (input.flags.debug === false) return { debugNote: "" };
54945
+ if (input.flags.debugNote === void 0) return {};
54946
+ if (!(input.flags.debug === true || current.debug)) {
54947
+ deps.logger.warn("setTrackFlags dropped a debug note — the track is not flagged for debug", {
54948
+ tags: { deviceId: input.deviceId },
54949
+ meta: {
54950
+ deviceId: input.deviceId,
54951
+ trackId: input.trackId,
54952
+ noteLength: input.flags.debugNote.length
54953
+ }
54954
+ });
54955
+ return {};
54956
+ }
54957
+ return { debugNote: input.flags.debugNote };
54958
+ }
54959
+ /**
54867
54960
  * Apply a partial flag patch and report the RESOLVED state of both flags.
54868
54961
  *
54869
54962
  * Existence is checked BEFORE the write: a patch against a track that no longer
@@ -54924,7 +55017,8 @@ async function applyTrackFlags(deps, input) {
54924
55017
  const patch = {
54925
55018
  ...input.flags.markForTrain !== void 0 ? { markForTrain: input.flags.markForTrain } : {},
54926
55019
  ...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {},
54927
- ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {}
55020
+ ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
55021
+ ...resolveNotePatch(deps, input, current)
54928
55022
  };
54929
55023
  if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
54930
55024
  const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
@@ -54932,6 +55026,7 @@ async function applyTrackFlags(deps, input) {
54932
55026
  trackId: input.trackId,
54933
55027
  markForTrain: retrainStatus === "staging",
54934
55028
  debug: patch.debug ?? current.debug,
55029
+ debugNote: patch.debugNote ?? current.debugNote,
54935
55030
  favourited: patch.favourited ?? current.favourited,
54936
55031
  retrainStatus
54937
55032
  };
@@ -58661,6 +58756,13 @@ var ANALYTICS_COLLECTIONS = [
58661
58756
  kind: "never-orphaned",
58662
58757
  why: "declares `sourceTrackId`, which is a REFERENCE and not ownership — a parked object outliving the track that first saw it is the entire purpose of the registry. It has its own TTL sweep keyed on OBSERVED frame time, not wall clock. Classified by hand precisely because the derivation must not see it."
58663
58758
  }
58759
+ },
58760
+ {
58761
+ collection: STATIONARY_EVIDENCE_COLLECTION,
58762
+ classification: {
58763
+ kind: "never-orphaned",
58764
+ why: "the candidate half of the registry above, and it inherits that entry's argument for the same reason: `sourceTrackId` is the PROVENANCE of whichever track happened to be looking, never ownership. It cannot be `track-owned` on the behaviour, not merely on the name — a candidate is fed by a SUCCESSION of tracks (`StationaryEvidenceLedger.observe` re-binds under D280, and `retainTracks` drops a binding the moment the tracker forgets the track) while the row itself survives, because agreement accumulating ACROSS tracks is the whole measurement. Cascading it on the first contributor's eviction would delete evidence still being gathered by tracks alive right now. Its bounds are its own and stay inside D61's one-clock rule: a candidate untouched for EVIDENCE_WINDOW_MS (6h) is dropped by `expire`, on the next OBSERVATION of that device and against the frame timestamp — the same observed-time clock the registry uses, never wall clock and never a sweep; a device is capped at EVIDENCE_MAX_CANDIDATES_PER_DEVICE (512, least-recently-touched evicted); a promotion deletes the candidate the instant the registry entry is born, so the two tables cannot both hold the object; and `clearDevice` takes the rest when a camera is removed. Deliberately NOT `owner-indirection`: that kind enrols a collection into `auditableCollections()`, and the audit has a resolver only for `pipeline-analytics:media`."
58765
+ }
58664
58766
  }
58665
58767
  ];
58666
58768
  /** The collections the orphan audit actually scans — everything with an owner. */
@@ -71158,15 +71260,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71158
71260
  */
71159
71261
  maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest) {
71160
71262
  const memory = this.residents.lifecycleUpdateMem(t.trackId);
71263
+ /**
71264
+ * The signal's label reads the frame patch AND the track row: the face
71265
+ * recognizer writes a recognised identity to the ROW over the async
71266
+ * detail plane, never into a frame patch, and a patch-only read left the
71267
+ * identity trigger unable to fire for a face — every face name reached
71268
+ * the export surfaces only on the `end` event, ~30 s after the subject
71269
+ * left the frame (measured live 2026-09-05).
71270
+ */
71271
+ const peeked = this.trackStore?.peekActive(t.trackId);
71272
+ const label = resolveLifecycleUpdateLabel(t.labelPatch, peeked?.label);
71161
71273
  const decision = evaluateTrackLifecycleUpdate({
71162
71274
  isNewBest,
71163
71275
  confidence: t.confidence,
71164
71276
  now: timestamp,
71165
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71277
+ ...label !== void 0 ? { label } : {},
71278
+ ...peeked !== null && peeked !== void 0 ? { zonesVisitedCount: peeked.zonesVisitedCount } : {},
71166
71279
  bboxArea: t.bbox.w * t.bbox.h
71167
71280
  }, memory, DEFAULT_UPDATE_GATE_CONFIG);
71168
71281
  this.residents.setLifecycleUpdateMem(deviceId, t.trackId, decision.memory);
71169
- if (!decision.emit) return;
71282
+ if (!decision.emit) {
71283
+ /**
71284
+ * Dropped for cadence (debounce) — counted, never silent. The count
71285
+ * since the last emit rides the memory and this line; a throttle
71286
+ * with no ledger is how a 30 s delivery delay stayed invisible.
71287
+ */
71288
+ if (decision.suppressed !== void 0) this.ctx.logger.debug("lifecycle update suppressed (debounce)", {
71289
+ tags: { deviceId },
71290
+ meta: {
71291
+ trackId: t.trackId,
71292
+ reason: decision.suppressed,
71293
+ suppressedSinceEmit: decision.memory.suppressedSinceEmit ?? 0
71294
+ }
71295
+ });
71296
+ return;
71297
+ }
71170
71298
  const track = this.trackStore?.getActiveByTrack(t.trackId);
71171
71299
  const keyFrameMediaKey = this.residents.keyFrameKey(t.trackId);
71172
71300
  const payload = buildTrackLifecyclePayload({
@@ -71178,7 +71306,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71178
71306
  bestConfidence: t.confidence,
71179
71307
  firstSeen: track?.firstSeen ?? timestamp,
71180
71308
  lastSeen: track?.lastSeen ?? timestamp,
71181
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71309
+ ...label !== void 0 ? { label } : {},
71182
71310
  ...t.plateText !== void 0 ? { plateText: t.plateText } : {},
71183
71311
  ...track?.zonesVisited !== void 0 ? { zonesVisited: track.zonesVisited } : {},
71184
71312
  ...track?.totalDistance !== void 0 ? { totalDistance: track.totalDistance } : {},
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-B1cFmGC0.mjs";
1
+ import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-NBkMwEZ0.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";
@@ -35814,16 +35814,43 @@ function positionsIntersectZone(positions, frameWidth, frameHeight, zone) {
35814
35814
  }, zone.points)) return true;
35815
35815
  return false;
35816
35816
  }
35817
+ //#endregion
35818
+ //#region src/pipeline-analytics/track-lifecycle.ts
35819
+ /** Minimum confidence increase (vs last emit) that counts as a material
35820
+ * improvement. */
35821
+ var UPDATE_MIN_CONF_DELTA = .1;
35822
+ /** Debounce floor between two `update` emits for the same track (ms). */
35823
+ var UPDATE_MIN_INTERVAL_MS = 4e3;
35824
+ /** Minimum fractional bbox-area growth (vs last emit) counted as a
35825
+ * materially larger representative crop (e.g. 0.15 = 15% bigger). */
35826
+ var UPDATE_MIN_CROP_AREA_GROWTH = .15;
35827
+ /**
35828
+ * The label a `phase:'update'` SIGNAL and PAYLOAD carry, resolved from its
35829
+ * two possible sources in order: this frame's tiered patch (the frame path
35830
+ * wrote a label this very frame), then the track ROW's stored display label
35831
+ * (`subLabel ?? label`, as `trackStore.peekActive` exposes it).
35832
+ *
35833
+ * The fallback is not a nicety. The face recognizer assigns an identity over
35834
+ * the async detail plane and writes it to the row (`applyLabelCandidate`) —
35835
+ * never into a frame patch — so a signal reading only the patch can never
35836
+ * fire the identity trigger for a recognised face. Measured on the live hub
35837
+ * (2026-09-05, device 590): six of six face identities reached Home
35838
+ * Assistant only on the `end` event at track close + sweep (~lastSeen+33 s),
35839
+ * zero during the track's life, while the subject was still on camera.
35840
+ */
35841
+ function resolveLifecycleUpdateLabel(labelPatch, storedDisplayLabel) {
35842
+ return patchDisplayLabel(labelPatch) ?? storedDisplayLabel;
35843
+ }
35817
35844
  var DEFAULT_UPDATE_GATE_CONFIG = {
35818
- minConfDelta: .1,
35819
- minIntervalMs: 4e3,
35820
- minCropAreaGrowth: .15
35845
+ minConfDelta: UPDATE_MIN_CONF_DELTA,
35846
+ minIntervalMs: UPDATE_MIN_INTERVAL_MS,
35847
+ minCropAreaGrowth: UPDATE_MIN_CROP_AREA_GROWTH
35821
35848
  };
35822
35849
  /**
35823
35850
  * Decide whether a `phase:'update'` event should fire this frame.
35824
35851
  *
35825
35852
  * Priority of triggers: newly-resolved identity/plate > confidence
35826
- * improvement > materially larger crop. All triggers are debounced by
35853
+ * improvement > newly-entered zone > materially larger crop. All triggers are debounced by
35827
35854
  * `minIntervalMs` relative to the last emit. Deltas are measured against
35828
35855
  * the LAST-EMITTED memory, so a run of sub-threshold new-bests accumulates
35829
35856
  * and fires once it crosses `minConfDelta`.
@@ -35831,22 +35858,32 @@ var DEFAULT_UPDATE_GATE_CONFIG = {
35831
35858
  function evaluateTrackLifecycleUpdate(signal, memory, config) {
35832
35859
  const identityTrigger = signal.label !== void 0 && signal.label !== "" && (memory === void 0 || memory.lastLabel !== signal.label);
35833
35860
  const confidenceTrigger = signal.isNewBest && (memory === void 0 || signal.confidence - memory.lastConfidence >= config.minConfDelta);
35861
+ const zoneTrigger = signal.zonesVisitedCount !== void 0 && memory?.lastZonesVisitedCount !== void 0 && signal.zonesVisitedCount > memory.lastZonesVisitedCount;
35834
35862
  const cropTrigger = signal.bboxArea !== void 0 && memory?.lastBboxArea !== void 0 && memory.lastBboxArea > 0 && signal.bboxArea >= memory.lastBboxArea * (1 + config.minCropAreaGrowth);
35835
- const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : cropTrigger ? "crop" : void 0;
35836
- const keptMemory = memory ?? {
35863
+ const reason = identityTrigger ? "identity" : confidenceTrigger ? "confidence" : zoneTrigger ? "zone" : cropTrigger ? "crop" : void 0;
35864
+ const keptMemory = memory === void 0 ? {
35837
35865
  lastConfidence: signal.confidence,
35838
- lastEmitAt: signal.now
35839
- };
35866
+ lastEmitAt: signal.now,
35867
+ ...signal.zonesVisitedCount !== void 0 ? { lastZonesVisitedCount: signal.zonesVisitedCount } : {}
35868
+ } : memory.lastZonesVisitedCount === void 0 && signal.zonesVisitedCount !== void 0 ? {
35869
+ ...memory,
35870
+ lastZonesVisitedCount: signal.zonesVisitedCount
35871
+ } : memory;
35840
35872
  if (reason === void 0) return {
35841
35873
  emit: false,
35842
35874
  memory: keptMemory
35843
35875
  };
35844
35876
  if (memory !== void 0 && signal.now - memory.lastEmitAt < config.minIntervalMs) return {
35845
35877
  emit: false,
35846
- memory
35878
+ suppressed: reason,
35879
+ memory: {
35880
+ ...keptMemory,
35881
+ suppressedSinceEmit: (keptMemory.suppressedSinceEmit ?? 0) + 1
35882
+ }
35847
35883
  };
35848
35884
  const nextLabel = signal.label ?? memory?.lastLabel;
35849
35885
  const nextBboxArea = signal.bboxArea ?? memory?.lastBboxArea;
35886
+ const nextZonesCount = signal.zonesVisitedCount ?? memory?.lastZonesVisitedCount;
35850
35887
  return {
35851
35888
  emit: true,
35852
35889
  reason,
@@ -35854,7 +35891,8 @@ function evaluateTrackLifecycleUpdate(signal, memory, config) {
35854
35891
  lastConfidence: memory === void 0 ? signal.confidence : Math.max(memory.lastConfidence, signal.confidence),
35855
35892
  lastEmitAt: signal.now,
35856
35893
  ...nextLabel !== void 0 ? { lastLabel: nextLabel } : {},
35857
- ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {}
35894
+ ...nextBboxArea !== void 0 ? { lastBboxArea: nextBboxArea } : {},
35895
+ ...nextZonesCount !== void 0 ? { lastZonesVisitedCount: nextZonesCount } : {}
35858
35896
  }
35859
35897
  };
35860
35898
  }
@@ -39465,6 +39503,10 @@ var TRACKS_COLUMNS = [
39465
39503
  name: "debug",
39466
39504
  type: "BOOLEAN"
39467
39505
  },
39506
+ {
39507
+ name: "debugNote",
39508
+ type: "TEXT"
39509
+ },
39468
39510
  {
39469
39511
  name: "favourited",
39470
39512
  type: "BOOLEAN"
@@ -39660,6 +39702,7 @@ function cloneTrack(t) {
39660
39702
  markForTrain: t.retrainStatus === "staging"
39661
39703
  } : {},
39662
39704
  ...t.debug !== void 0 ? { debug: t.debug } : {},
39705
+ ...t.debugNote !== void 0 ? { debugNote: t.debugNote } : {},
39663
39706
  ...t.favourited !== void 0 ? { favourited: t.favourited } : {},
39664
39707
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
39665
39708
  ...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
@@ -39896,7 +39939,8 @@ var TrackStore = class {
39896
39939
  * Cheap read of an active track's promotion-relevant fields WITHOUT the deep
39897
39940
  * clone `getActiveByTrack` does — the returned `positions` is the live
39898
39941
  * internal array (read-only; callers must not mutate). Feeds the per-frame
39899
- * stationary-promotion check, called at inference fps. Null if unknown/expired.
39942
+ * stationary-promotion check AND the lifecycle update gate, both called at
39943
+ * inference fps. Null if unknown/expired.
39900
39944
  */
39901
39945
  peekActive(trackId) {
39902
39946
  const t = this.active.get(trackId);
@@ -39904,6 +39948,7 @@ var TrackStore = class {
39904
39948
  return {
39905
39949
  firstSeen: t.firstSeen,
39906
39950
  positions: t.positions,
39951
+ zonesVisitedCount: t.zonesVisited.length,
39907
39952
  ...displayLabel(t) !== void 0 ? { label: displayLabel(t) } : {}
39908
39953
  };
39909
39954
  }
@@ -40048,12 +40093,14 @@ var TrackStore = class {
40048
40093
  const nextStatus = patch.markForTrain === void 0 ? void 0 : patch.markForTrain ? "staging" : "none";
40049
40094
  if (nextStatus !== void 0) data["retrainStatus"] = nextStatus;
40050
40095
  if (patch.debug !== void 0) data["debug"] = patch.debug;
40096
+ if (patch.debugNote !== void 0) data["debugNote"] = patch.debugNote;
40051
40097
  if (patch.favourited !== void 0) data["favourited"] = patch.favourited;
40052
40098
  if (Object.keys(data).length === 0) return;
40053
40099
  const active = this.active.get(trackId);
40054
40100
  if (active) {
40055
40101
  if (nextStatus !== void 0) active.retrainStatus = nextStatus;
40056
40102
  if (patch.debug !== void 0) active.debug = patch.debug;
40103
+ if (patch.debugNote !== void 0) active.debugNote = patch.debugNote;
40057
40104
  if (patch.favourited !== void 0) active.favourited = patch.favourited;
40058
40105
  }
40059
40106
  try {
@@ -40256,6 +40303,7 @@ var TrackStore = class {
40256
40303
  if (active) return {
40257
40304
  retrainStatus: active.retrainStatus ?? "none",
40258
40305
  debug: active.debug === true,
40306
+ debugNote: active.debugNote ?? "",
40259
40307
  favourited: active.favourited === true
40260
40308
  };
40261
40309
  const persisted = await this.getPersistedByTrackId(trackId);
@@ -40263,6 +40311,7 @@ var TrackStore = class {
40263
40311
  return {
40264
40312
  retrainStatus: persisted.retrainStatus ?? "none",
40265
40313
  debug: persisted.debug === true,
40314
+ debugNote: persisted.debugNote ?? "",
40266
40315
  favourited: persisted.favourited === true
40267
40316
  };
40268
40317
  }
@@ -41162,6 +41211,7 @@ var TrackStore = class {
41162
41211
  ...t.audioLabels !== void 0 ? { audioLabels: [...t.audioLabels] } : {},
41163
41212
  ...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
41164
41213
  ...t.debug !== void 0 ? { debug: t.debug } : {},
41214
+ ...t.debugNote !== void 0 ? { debugNote: t.debugNote } : {},
41165
41215
  ...t.favourited !== void 0 ? { favourited: t.favourited } : {},
41166
41216
  ...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
41167
41217
  ...t.hasEmbeddedFace !== void 0 ? { hasEmbeddedFace: t.hasEmbeddedFace } : {},
@@ -41220,6 +41270,7 @@ var TrackStore = class {
41220
41270
  const audioLabels = readAudioLabels(data["audioLabels"]);
41221
41271
  const retrainParsed = RetrainStatusSchema.safeParse(data["retrainStatus"]);
41222
41272
  const debug = data["debug"];
41273
+ const debugNote = data["debugNote"];
41223
41274
  const favourited = data["favourited"];
41224
41275
  const hasFace = data["hasFace"];
41225
41276
  /** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`.
@@ -41268,6 +41319,7 @@ var TrackStore = class {
41268
41319
  markForTrain: retrainParsed.data === "staging"
41269
41320
  } : {},
41270
41321
  ...typeof debug === "boolean" ? { debug } : {},
41322
+ ...typeof debugNote === "string" ? { debugNote } : {},
41271
41323
  ...typeof favourited === "boolean" ? { favourited } : {},
41272
41324
  ...typeof hasFace === "boolean" ? { hasFace } : {},
41273
41325
  ...typeof hasEmbeddedFace === "boolean" ? { hasEmbeddedFace } : {},
@@ -54790,6 +54842,47 @@ var TrackAlreadyTrainedError = class extends Error {
54790
54842
  }
54791
54843
  };
54792
54844
  /**
54845
+ * The `debugNote` half of the patch — `{}` when nothing should be written.
54846
+ *
54847
+ * The note is not an independent column: it is the operator's sentence about
54848
+ * why `debug` is on, so it exists exactly as long as the flag does. Three
54849
+ * cases, and the order matters:
54850
+ *
54851
+ * 1. **`debug: false` in the patch → the note is CLEARED**, whatever the
54852
+ * caller sent with it. A note surviving the flag is a leftover that reads
54853
+ * as a live request, and the invariant belongs here rather than in the
54854
+ * three surfaces that write this patch — one of which is a browser grid
54855
+ * that has no note UI at all and would never think to clear it.
54856
+ * 2. **No note in the patch → nothing is written.** This is what makes a
54857
+ * cancelled prompt free: the app turns debug on, the operator declines to
54858
+ * type, and the flag still lands. Refusing to write a note must never cost
54859
+ * the capture that was asked for.
54860
+ * 3. **A note, on a track whose debug is off and is not being turned on →
54861
+ * DROPPED.** Storing it would put the operator's words somewhere no reader
54862
+ * of a debug set will ever look. Dropped work is logged, per-camera: a
54863
+ * branch that silently discards a sentence is indistinguishable from one
54864
+ * that stored it.
54865
+ *
54866
+ * `''` is a WRITE, not an absence — clearing a note the operator no longer
54867
+ * stands behind is a real edit.
54868
+ */
54869
+ function resolveNotePatch(deps, input, current) {
54870
+ if (input.flags.debug === false) return { debugNote: "" };
54871
+ if (input.flags.debugNote === void 0) return {};
54872
+ if (!(input.flags.debug === true || current.debug)) {
54873
+ deps.logger.warn("setTrackFlags dropped a debug note — the track is not flagged for debug", {
54874
+ tags: { deviceId: input.deviceId },
54875
+ meta: {
54876
+ deviceId: input.deviceId,
54877
+ trackId: input.trackId,
54878
+ noteLength: input.flags.debugNote.length
54879
+ }
54880
+ });
54881
+ return {};
54882
+ }
54883
+ return { debugNote: input.flags.debugNote };
54884
+ }
54885
+ /**
54793
54886
  * Apply a partial flag patch and report the RESOLVED state of both flags.
54794
54887
  *
54795
54888
  * Existence is checked BEFORE the write: a patch against a track that no longer
@@ -54850,7 +54943,8 @@ async function applyTrackFlags(deps, input) {
54850
54943
  const patch = {
54851
54944
  ...input.flags.markForTrain !== void 0 ? { markForTrain: input.flags.markForTrain } : {},
54852
54945
  ...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {},
54853
- ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {}
54946
+ ...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {},
54947
+ ...resolveNotePatch(deps, input, current)
54854
54948
  };
54855
54949
  if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
54856
54950
  const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
@@ -54858,6 +54952,7 @@ async function applyTrackFlags(deps, input) {
54858
54952
  trackId: input.trackId,
54859
54953
  markForTrain: retrainStatus === "staging",
54860
54954
  debug: patch.debug ?? current.debug,
54955
+ debugNote: patch.debugNote ?? current.debugNote,
54861
54956
  favourited: patch.favourited ?? current.favourited,
54862
54957
  retrainStatus
54863
54958
  };
@@ -58587,6 +58682,13 @@ var ANALYTICS_COLLECTIONS = [
58587
58682
  kind: "never-orphaned",
58588
58683
  why: "declares `sourceTrackId`, which is a REFERENCE and not ownership — a parked object outliving the track that first saw it is the entire purpose of the registry. It has its own TTL sweep keyed on OBSERVED frame time, not wall clock. Classified by hand precisely because the derivation must not see it."
58589
58684
  }
58685
+ },
58686
+ {
58687
+ collection: STATIONARY_EVIDENCE_COLLECTION,
58688
+ classification: {
58689
+ kind: "never-orphaned",
58690
+ why: "the candidate half of the registry above, and it inherits that entry's argument for the same reason: `sourceTrackId` is the PROVENANCE of whichever track happened to be looking, never ownership. It cannot be `track-owned` on the behaviour, not merely on the name — a candidate is fed by a SUCCESSION of tracks (`StationaryEvidenceLedger.observe` re-binds under D280, and `retainTracks` drops a binding the moment the tracker forgets the track) while the row itself survives, because agreement accumulating ACROSS tracks is the whole measurement. Cascading it on the first contributor's eviction would delete evidence still being gathered by tracks alive right now. Its bounds are its own and stay inside D61's one-clock rule: a candidate untouched for EVIDENCE_WINDOW_MS (6h) is dropped by `expire`, on the next OBSERVATION of that device and against the frame timestamp — the same observed-time clock the registry uses, never wall clock and never a sweep; a device is capped at EVIDENCE_MAX_CANDIDATES_PER_DEVICE (512, least-recently-touched evicted); a promotion deletes the candidate the instant the registry entry is born, so the two tables cannot both hold the object; and `clearDevice` takes the rest when a camera is removed. Deliberately NOT `owner-indirection`: that kind enrols a collection into `auditableCollections()`, and the audit has a resolver only for `pipeline-analytics:media`."
58691
+ }
58590
58692
  }
58591
58693
  ];
58592
58694
  /** The collections the orphan audit actually scans — everything with an owner. */
@@ -71084,15 +71186,41 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71084
71186
  */
71085
71187
  maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest) {
71086
71188
  const memory = this.residents.lifecycleUpdateMem(t.trackId);
71189
+ /**
71190
+ * The signal's label reads the frame patch AND the track row: the face
71191
+ * recognizer writes a recognised identity to the ROW over the async
71192
+ * detail plane, never into a frame patch, and a patch-only read left the
71193
+ * identity trigger unable to fire for a face — every face name reached
71194
+ * the export surfaces only on the `end` event, ~30 s after the subject
71195
+ * left the frame (measured live 2026-09-05).
71196
+ */
71197
+ const peeked = this.trackStore?.peekActive(t.trackId);
71198
+ const label = resolveLifecycleUpdateLabel(t.labelPatch, peeked?.label);
71087
71199
  const decision = evaluateTrackLifecycleUpdate({
71088
71200
  isNewBest,
71089
71201
  confidence: t.confidence,
71090
71202
  now: timestamp,
71091
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71203
+ ...label !== void 0 ? { label } : {},
71204
+ ...peeked !== null && peeked !== void 0 ? { zonesVisitedCount: peeked.zonesVisitedCount } : {},
71092
71205
  bboxArea: t.bbox.w * t.bbox.h
71093
71206
  }, memory, DEFAULT_UPDATE_GATE_CONFIG);
71094
71207
  this.residents.setLifecycleUpdateMem(deviceId, t.trackId, decision.memory);
71095
- if (!decision.emit) return;
71208
+ if (!decision.emit) {
71209
+ /**
71210
+ * Dropped for cadence (debounce) — counted, never silent. The count
71211
+ * since the last emit rides the memory and this line; a throttle
71212
+ * with no ledger is how a 30 s delivery delay stayed invisible.
71213
+ */
71214
+ if (decision.suppressed !== void 0) this.ctx.logger.debug("lifecycle update suppressed (debounce)", {
71215
+ tags: { deviceId },
71216
+ meta: {
71217
+ trackId: t.trackId,
71218
+ reason: decision.suppressed,
71219
+ suppressedSinceEmit: decision.memory.suppressedSinceEmit ?? 0
71220
+ }
71221
+ });
71222
+ return;
71223
+ }
71096
71224
  const track = this.trackStore?.getActiveByTrack(t.trackId);
71097
71225
  const keyFrameMediaKey = this.residents.keyFrameKey(t.trackId);
71098
71226
  const payload = buildTrackLifecyclePayload({
@@ -71104,7 +71232,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71104
71232
  bestConfidence: t.confidence,
71105
71233
  firstSeen: track?.firstSeen ?? timestamp,
71106
71234
  lastSeen: track?.lastSeen ?? timestamp,
71107
- ...patchDisplayLabel(t.labelPatch) !== void 0 ? { label: patchDisplayLabel(t.labelPatch) } : {},
71235
+ ...label !== void 0 ? { label } : {},
71108
71236
  ...t.plateText !== void 0 ? { plateText: t.plateText } : {},
71109
71237
  ...track?.zonesVisited !== void 0 ? { zonesVisited: track.zonesVisited } : {},
71110
71238
  ...track?.totalDistance !== void 0 ? { totalDistance: track.totalDistance } : {},
@@ -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-MvTzBID6.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BL6jNwq1.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.177",
3
+ "version": "1.2.179",
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",