@camstack/addon-post-analysis 1.2.666 → 1.2.667

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (16) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{clip-model-registry-BIhYJRFt.js → clip-model-registry-D7cqhFK3.js} +731 -485
  3. package/dist/{clip-model-registry-Uc_C1x81.mjs → clip-model-registry-DpzRS8q3.mjs} +731 -485
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-FkeZoNXj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DmTIEp0m.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DAAkZRnK.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-znrgUrN1.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-BBhgVoNl.mjs → hostInit-ODebWXu_.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +449 -114
  12. package/dist/pipeline-analytics/index.mjs +449 -114
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CnYZAdvw.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Br6eWzp_.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-BIhYJRFt.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-D7cqhFK3.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -59754,6 +59754,140 @@ async function promoteWithStill(input) {
59754
59754
  });
59755
59755
  }
59756
59756
  //#endregion
59757
+ //#region src/pipeline-analytics/pipeline/key-frame-persist.ts
59758
+ /**
59759
+ * A box is written only when it can be USED — a degenerate rectangle, or one
59760
+ * measured on a degenerate frame, says nothing and would read back as a box at
59761
+ * the origin. Absent is the honest answer; the reader then pairs a snapshot,
59762
+ * exactly as it did before D510.
59763
+ *
59764
+ * Its result is spread into BOTH writes: the small companion is derived from
59765
+ * the very bytes of the native one, so it is the same scene at the same
59766
+ * instant and the box maps into it by ratio like any other scale of the frame.
59767
+ * A box on one row and not the other would make the summary's answer depend on
59768
+ * which row a reader happened to pick.
59769
+ */
59770
+ function isStatableSubject(subject) {
59771
+ if (subject === void 0) return false;
59772
+ return subject.w > 0 && subject.h > 0 && subject.frameWidth > 0 && subject.frameHeight > 0;
59773
+ }
59774
+ /**
59775
+ * Read, derive and store one track's key frame + its web companion.
59776
+ *
59777
+ * PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
59778
+ * parks a later frame. The `keyFrame` write lands FIRST and its media id (D482)
59779
+ * is what the outcome carries — a `keyFrameSmall` failure afterwards must not
59780
+ * re-arm a retry that would re-shoot a key frame the rebuild can already read.
59781
+ */
59782
+ async function persistTrackKeyFrame(deps, input) {
59783
+ const { deviceId, trackId, retry } = input;
59784
+ const drop = {
59785
+ tags: { deviceId },
59786
+ meta: {
59787
+ trackId,
59788
+ retry
59789
+ }
59790
+ };
59791
+ try {
59792
+ const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
59793
+ if (parcel === null || parcel.jpeg.byteLength === 0) {
59794
+ deps.logger.error("key-frame parcel missing — nothing to write, retry parks a later frame", { ...drop });
59795
+ return { kind: "parcel-missing" };
59796
+ }
59797
+ const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
59798
+ const keyFrameMediaId = await deps.media.putReplacing({
59799
+ deviceId,
59800
+ ownerKind: "track",
59801
+ ownerId: trackId,
59802
+ kind: "keyFrame",
59803
+ timestamp: parcel.timestamp,
59804
+ data: parcel.jpeg,
59805
+ ...subjectBox
59806
+ });
59807
+ try {
59808
+ const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
59809
+ await deps.media.putReplacing({
59810
+ deviceId,
59811
+ ownerKind: "track",
59812
+ ownerId: trackId,
59813
+ kind: "keyFrameSmall",
59814
+ timestamp: parcel.timestamp,
59815
+ data: keyFrameSmall,
59816
+ ...subjectBox
59817
+ });
59818
+ } catch (err) {
59819
+ deps.logger.warn("keyFrameSmall write failed — the native keyFrame is stored", {
59820
+ ...drop,
59821
+ meta: {
59822
+ ...drop.meta,
59823
+ nodeId: parcel.nodeId,
59824
+ error: require_clip_model_registry.errMsg(err)
59825
+ }
59826
+ });
59827
+ }
59828
+ deps.logger.debug("key frame written from the parked parcel", {
59829
+ ...drop,
59830
+ meta: {
59831
+ ...drop.meta,
59832
+ nodeId: parcel.nodeId,
59833
+ width: parcel.width,
59834
+ height: parcel.height,
59835
+ bytes: parcel.jpeg.byteLength,
59836
+ parkedTimestamp: parcel.timestamp
59837
+ }
59838
+ });
59839
+ return {
59840
+ kind: "landed",
59841
+ keyFrameMediaId
59842
+ };
59843
+ } catch (err) {
59844
+ const error = require_clip_model_registry.errMsg(err);
59845
+ deps.logger.warn("key-frame write failed", {
59846
+ ...drop,
59847
+ meta: {
59848
+ ...drop.meta,
59849
+ error
59850
+ }
59851
+ });
59852
+ return {
59853
+ kind: "error",
59854
+ error
59855
+ };
59856
+ }
59857
+ }
59858
+ /**
59859
+ * Write one parked frame under an OBJECT event (`object:<id>`), once.
59860
+ *
59861
+ * A package delivery has no track row owning a key frame, and the notification
59862
+ * ladder reads only `track:` and `object:` owners — so without this the
59863
+ * delivery ships text-only. `null` = no parcel to write (said at the caller).
59864
+ * `putReplacing` makes a repeat the same row, never a second one.
59865
+ */
59866
+ async function persistObjectFrame(deps, input) {
59867
+ const { deviceId, trackId, objectId, timestamp } = input;
59868
+ const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
59869
+ if (parcel === null || parcel.jpeg.byteLength === 0) {
59870
+ deps.logger.warn("object frame parcel missing — nothing to write", {
59871
+ tags: { deviceId },
59872
+ meta: {
59873
+ trackId,
59874
+ objectId
59875
+ }
59876
+ });
59877
+ return null;
59878
+ }
59879
+ const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
59880
+ return deps.media.putReplacing({
59881
+ deviceId,
59882
+ ownerKind: "object",
59883
+ ownerId: objectId,
59884
+ kind: "keyFrame",
59885
+ timestamp,
59886
+ data: parcel.jpeg,
59887
+ ...subjectBox
59888
+ });
59889
+ }
59890
+ //#endregion
59757
59891
  //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
59758
59892
  /**
59759
59893
  * How much of the PERSON box must sit inside a vehicle box to be counted.
@@ -71161,6 +71295,12 @@ Object.freeze({
71161
71295
  "idleStopMinutes": 30
71162
71296
  } } }
71163
71297
  },
71298
+ "media-player": {
71299
+ "channelDown": { "steps": 1 },
71300
+ "channelUp": { "steps": 1 },
71301
+ "volumeDown": { "steps": 1 },
71302
+ "volumeUp": { "steps": 1 }
71303
+ },
71164
71304
  "mqtt-broker": { "startEmbeddedBroker": {
71165
71305
  "port": 1883,
71166
71306
  "allowAnonymous": false
@@ -71655,6 +71795,15 @@ var WORD_LEVEL_FIELD = {
71655
71795
  */
71656
71796
  var BOOLEAN_LEVEL_FIELD = { "running-state": "running" };
71657
71797
  /**
71798
+ * The device-history series key of a level cap's boolean — `cap:<cap>.<field>`,
71799
+ * the shape of `capSeriesKey` (system builtin, not importable from an addon;
71800
+ * the wiring spec pins the literal). `null` for a cap with no boolean level.
71801
+ */
71802
+ function levelSeriesKey(capName) {
71803
+ const field = BOOLEAN_LEVEL_FIELD[capName];
71804
+ return field === void 0 ? null : `cap:${capName}.${field}`;
71805
+ }
71806
+ /**
71658
71807
  * Caps whose level is a WORD with NO change stamp: cap → field. `enum-sensor`
71659
71808
  * carries `{ value, lastFetchedAt }`, and `lastFetchedAt` moves on every
71660
71809
  * provider refresh, so it says nothing about when the word changed. Gated by
@@ -71681,6 +71830,7 @@ var WORD_LEVEL_INACTIVE = {
71681
71830
  * post-analysis runner, D28).
71682
71831
  */
71683
71832
  var ADVANCE_FIELD = require_clip_model_registry.CAP_ADVANCE_FIELDS;
71833
+ var DEFAULT_SEED_WAIT_MS = 2e3;
71684
71834
  /** Default drop-summary window: one line per device per ten minutes, at most. */
71685
71835
  var DEFAULT_DROP_SUMMARY_WINDOW_MS = 6e5;
71686
71836
  var SensorTransitionGate = class {
@@ -71692,6 +71842,14 @@ var SensorTransitionGate = class {
71692
71842
  lastStamps = /* @__PURE__ */ new Map();
71693
71843
  /** Drops since the window opened, keyed by `${deviceId}:${capName}:${reason}`. */
71694
71844
  drops = /* @__PURE__ */ new Map();
71845
+ /** Seed reads started, by key; a settled one stays so a seed is read once. */
71846
+ seeds = /* @__PURE__ */ new Map();
71847
+ seedsSettled = /* @__PURE__ */ new Set();
71848
+ /** Seeds whose wait ran out: decided as unseeded from then on. */
71849
+ seedsAbandoned = /* @__PURE__ */ new Set();
71850
+ readSeed;
71851
+ onSeedFailed;
71852
+ seedWaitMs;
71695
71853
  windowStartedAt;
71696
71854
  startedAtMs;
71697
71855
  now;
@@ -71703,6 +71861,55 @@ var SensorTransitionGate = class {
71703
71861
  this.freshnessMs = options.firstSightingFreshnessMs ?? require_clip_model_registry.DEFAULT_FIRST_SIGHTING_FRESHNESS_MS;
71704
71862
  this.summaryWindowMs = options.summaryWindowMs ?? DEFAULT_DROP_SUMMARY_WINDOW_MS;
71705
71863
  this.windowStartedAt = options.now();
71864
+ this.readSeed = options.readSeed;
71865
+ this.onSeedFailed = options.onSeedFailed;
71866
+ this.seedWaitMs = options.seedWaitMs ?? DEFAULT_SEED_WAIT_MS;
71867
+ }
71868
+ /**
71869
+ * {@link observe}, plus: a level cap's first boolean `false` whose persisted
71870
+ * prior is still being read waits for that read, bounded by `seedWaitMs`, and
71871
+ * is decided again. This is the only wait on the path, and it is on a read
71872
+ * that was started at the FIRST sighting of the device (a null at boot, long
71873
+ * before the boolean) — never one begun by the event itself unless the
71874
+ * boolean really is the first thing seen. A read that fails or times out
71875
+ * decides as unseeded, i.e. exactly as before the seed existed (D49).
71876
+ */
71877
+ async observeSettled(deviceId, capName, slice) {
71878
+ const first = this.observe(deviceId, capName, slice);
71879
+ if (first.changed || first.reason !== "seed-pending") return first;
71880
+ const key = `${deviceId}:${capName}`;
71881
+ const pending = this.seeds.get(key);
71882
+ if (pending !== void 0) {
71883
+ let timer;
71884
+ const timeout = new Promise((resolve) => {
71885
+ timer = setTimeout(resolve, this.seedWaitMs);
71886
+ });
71887
+ await Promise.race([pending, timeout]);
71888
+ if (timer !== void 0) clearTimeout(timer);
71889
+ if (!this.seedsSettled.has(key)) this.seedsAbandoned.add(key);
71890
+ }
71891
+ return this.observe(deviceId, capName, slice);
71892
+ }
71893
+ /** Start the one seed read of a level cap's key; never awaited here. */
71894
+ ensureSeed(deviceId, capName) {
71895
+ const read = this.readSeed;
71896
+ if (read === void 0) return;
71897
+ const key = `${deviceId}:${capName}`;
71898
+ if (this.seeds.has(key)) return;
71899
+ const settle = () => {
71900
+ this.seedsSettled.add(key);
71901
+ };
71902
+ const started = read(deviceId, capName).then((seed) => {
71903
+ if (seed !== null && !this.lastValues.has(key)) this.lastValues.set(key, seed.value);
71904
+ settle();
71905
+ }, (error) => {
71906
+ this.onSeedFailed?.(deviceId, capName, error);
71907
+ settle();
71908
+ });
71909
+ this.seeds.set(key, started);
71910
+ }
71911
+ seedPending(key) {
71912
+ return this.seeds.has(key) && !this.seedsSettled.has(key) && !this.seedsAbandoned.has(key);
71706
71913
  }
71707
71914
  /**
71708
71915
  * Feed one `DeviceStateChanged` slice for a cap `EVENT_KIND_BY_CAP` maps.
@@ -71714,6 +71921,7 @@ var SensorTransitionGate = class {
71714
71921
  * a boolean in either direction, or a word-level cap's word changing.
71715
71922
  */
71716
71923
  observe(deviceId, capName, slice) {
71924
+ if (BOOLEAN_LEVEL_FIELD[capName] !== void 0) this.ensureSeed(deviceId, capName);
71717
71925
  if (slice["lastChangedAt"] === 0) return this.drop(deviceId, capName, "cold-slice");
71718
71926
  const unstampedField = UNSTAMPED_WORD_FIELD[capName];
71719
71927
  if (unstampedField !== void 0) return this.observeUnstampedWord(deviceId, capName, slice, unstampedField);
@@ -71769,6 +71977,10 @@ var SensorTransitionGate = class {
71769
71977
  if (typeof raw !== "boolean") return this.drop(deviceId, capName, "non-boolean-value");
71770
71978
  const key = `${deviceId}:${capName}`;
71771
71979
  const prior = this.lastValues.get(key);
71980
+ if (prior === void 0 && !raw && this.seedPending(key)) return {
71981
+ changed: false,
71982
+ reason: "seed-pending"
71983
+ };
71772
71984
  this.lastValues.set(key, raw);
71773
71985
  if (prior === void 0) {
71774
71986
  if (!raw) return this.drop(deviceId, capName, "baseline-seeded-inactive");
@@ -85607,6 +85819,15 @@ function isLinkedStillId(id) {
85607
85819
  var NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS = 15e3;
85608
85820
  var NC_LINKED_WAKE_POLL_MS = 1e3;
85609
85821
  var NC_LINKED_WAKE_MIN_GAP_MS = 6e4;
85822
+ /**
85823
+ * D60 — a tile the freshness read calls stale is RE-POLLED, unforced, until the
85824
+ * wrapper hands back a frame from the event or this bound is spent. The wrapper
85825
+ * settles a soft miss with its old cached hit; one more ask, a moment later,
85826
+ * usually finds the refreshed one. The bound sits inside the dispatcher's still
85827
+ * deadline (`NC_STILL_WAIT_MS`, 3 s): the row waits for it through `pendingUntil`.
85828
+ */
85829
+ var NC_LINKED_STALE_POLL_MS = 500;
85830
+ var NC_LINKED_STALE_MAX_POLLS = 5;
85610
85831
  /** Per camera — the marker's own bound (`SENSOR_MARKER_SNAPSHOT_TIMEOUT_MS`). */
85611
85832
  var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
85612
85833
  /** The capture-free wrapper-state read, per camera. It runs IN PARALLEL with
@@ -85616,6 +85837,8 @@ var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
85616
85837
  * photograph). A read that misses the bound or fails is UNKNOWN and keeps the
85617
85838
  * tile (D49). */
85618
85839
  var SENSOR_LINKED_STATE_READ_TIMEOUT_MS = SENSOR_LINKED_CAPTURE_TIMEOUT_MS;
85840
+ /** The battery-slice read behind the unknown-state wake: cheap, and bounded. */
85841
+ var SENSOR_LINKED_BATTERY_READ_TIMEOUT_MS = 1e3;
85619
85842
  /** The one cache-only freshness read for the set — one hop, after the
85620
85843
  * captures. A read that misses it keeps every tile and says `freshness
85621
85844
  * unverified` (D49). */
@@ -85721,13 +85944,13 @@ var NcLinkedStillShelf = class {
85721
85944
  startedAt,
85722
85945
  options
85723
85946
  };
85724
- let results;
85947
+ let judged;
85725
85948
  try {
85726
- results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
85949
+ const results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
85950
+ judged = await this.repollStale(ctx, atMs, await this.judgeFreshness(stills.sensorId, results, atMs));
85727
85951
  } finally {
85728
85952
  this.pendingWakes.delete(setId);
85729
85953
  }
85730
- const judged = await this.judgeFreshness(stills.sensorId, results, atMs);
85731
85954
  for (const r of judged) this.logTile(stills.sensorId, r);
85732
85955
  const landed = judged.filter(isLandedTile);
85733
85956
  const composed = await this.compose(stills.sensorId, landed, atMs);
@@ -85774,6 +85997,16 @@ var NcLinkedStillShelf = class {
85774
85997
  if (verdict.reason === "waking" && ctx.options.wakeSleeping) return this.awaitWaking(cameraId, ctx);
85775
85998
  if (verdict.reason === "unreachable" && ctx.options.wakeSleeping) return excluded("wake-skipped:unreachable");
85776
85999
  if (verdict.reason !== null) return excluded(verdict.reason);
86000
+ if (verdict.unknown !== null && capture.outcome === "empty" && ctx.options.wakeSleeping && await this.isBatteryCamera(cameraId)) {
86001
+ this.deps.logger.info("linked camera state unknown and capture empty — waking as a battery camera", {
86002
+ tags: { deviceId: cameraId },
86003
+ meta: {
86004
+ sensorId: ctx.sensorId,
86005
+ stateUnknown: verdict.unknown
86006
+ }
86007
+ });
86008
+ return this.wakeOne(cameraId, ctx);
86009
+ }
85777
86010
  return {
85778
86011
  cameraId,
85779
86012
  ...capture,
@@ -85781,6 +86014,20 @@ var NcLinkedStillShelf = class {
85781
86014
  tookMs: took()
85782
86015
  };
85783
86016
  }
86017
+ /** Only a camera KNOWN to be a battery camera is woken on an unknown state. */
86018
+ async isBatteryCamera(cameraId) {
86019
+ const read = this.deps.isBatteryCamera;
86020
+ if (read === void 0) return false;
86021
+ try {
86022
+ return await withTimeout$2(read(cameraId), SENSOR_LINKED_BATTERY_READ_TIMEOUT_MS, "battery read") === true;
86023
+ } catch (err) {
86024
+ this.deps.logger.warn("linked camera battery read failed — not woken on an unknown state", {
86025
+ tags: { deviceId: cameraId },
86026
+ meta: { error: String(err) }
86027
+ });
86028
+ return false;
86029
+ }
86030
+ }
85784
86031
  /** D998: one forced capture, by the rule's leave, at most once per gap per camera. */
85785
86032
  async wakeOne(cameraId, ctx) {
85786
86033
  const took = () => this.deps.now() - ctx.startedAt;
@@ -85842,8 +86089,46 @@ var NcLinkedStillShelf = class {
85842
86089
  };
85843
86090
  }
85844
86091
  markPending(ctx) {
85845
- const until = ctx.startedAt + NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS + NC_LINKED_WAKE_POLL_MS;
85846
- this.pendingWakes.set(ctx.setId, Math.max(this.pendingWakes.get(ctx.setId) ?? 0, until));
86092
+ this.markPendingUntil(ctx.setId, ctx.startedAt + NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS + NC_LINKED_WAKE_POLL_MS);
86093
+ }
86094
+ markPendingUntil(setId, until) {
86095
+ this.pendingWakes.set(setId, Math.max(this.pendingWakes.get(setId) ?? 0, until));
86096
+ }
86097
+ /**
86098
+ * D60: a `stale` tile asks again (unforced — a poll never wakes) until its
86099
+ * frame is from the event or the bound is spent; the row's still-wait is held
86100
+ * open meanwhile. A camera that never freshens stays `stale`, said on its line.
86101
+ */
86102
+ async repollStale(ctx, atMs, judged) {
86103
+ let current = judged;
86104
+ if (!current.some((r) => r.outcome === "stale")) return current;
86105
+ this.markPendingUntil(ctx.setId, this.deps.now() + NC_LINKED_STALE_POLL_MS * 6);
86106
+ for (let i = 0; i < NC_LINKED_STALE_MAX_POLLS; i++) {
86107
+ const waiting = current.filter((r) => r.outcome === "stale").map((r) => r.cameraId);
86108
+ if (waiting.length === 0) break;
86109
+ await new Promise((resolve) => setTimeout(resolve, NC_LINKED_STALE_POLL_MS));
86110
+ const landedAgain = (await Promise.all(waiting.map(async (cameraId) => {
86111
+ const capture = await this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS);
86112
+ return capture.outcome === "landed" && capture.jpeg !== void 0 ? {
86113
+ cameraId,
86114
+ outcome: "landed",
86115
+ jpeg: capture.jpeg,
86116
+ tookMs: this.deps.now() - ctx.startedAt
86117
+ } : null;
86118
+ }))).filter((r) => r !== null);
86119
+ if (landedAgain.length === 0) continue;
86120
+ const rejudged = await this.judgeFreshness(ctx.sensorId, landedAgain, atMs);
86121
+ const fresh = new Map(rejudged.filter((r) => r.outcome === "landed").map((r) => [r.cameraId, r]));
86122
+ current = current.map((r) => fresh.get(r.cameraId) ?? r);
86123
+ for (const cameraId of fresh.keys()) this.deps.logger.info("linked camera frame was stale — a re-poll brought one from the event", {
86124
+ tags: { deviceId: cameraId },
86125
+ meta: {
86126
+ sensorId: ctx.sensorId,
86127
+ polls: i + 1
86128
+ }
86129
+ });
86130
+ }
86131
+ return current;
85847
86132
  }
85848
86133
  /** A wake that brought nothing back names itself, not the generic `empty`. */
85849
86134
  asWakeResult(capture) {
@@ -85912,9 +86197,17 @@ var NcLinkedStillShelf = class {
85912
86197
  unknown: null
85913
86198
  };
85914
86199
  } catch (err) {
86200
+ const timedOut = err instanceof LinkedReadTimeout;
86201
+ this.deps.logger.warn("linked camera snapshot state read failed — state unknown", {
86202
+ tags: { deviceId: cameraId },
86203
+ meta: {
86204
+ error: String(err),
86205
+ timedOut
86206
+ }
86207
+ });
85915
86208
  return {
85916
86209
  reason: null,
85917
- unknown: err instanceof LinkedReadTimeout ? "timeout" : "error"
86210
+ unknown: timedOut ? "timeout" : "error"
85918
86211
  };
85919
86212
  }
85920
86213
  }
@@ -107556,6 +107849,7 @@ var NotificationCenter = class {
107556
107849
  this.linkedStills = getSnapshot !== void 0 ? new NcLinkedStillShelf({
107557
107850
  getSnapshot,
107558
107851
  ...deps.getSnapshotState !== void 0 ? { getSnapshotState: deps.getSnapshotState } : {},
107852
+ ...deps.isBatteryCamera !== void 0 ? { isBatteryCamera: deps.isBatteryCamera } : {},
107559
107853
  ...deps.getFrameCapturedAt !== void 0 ? { getFrameCapturedAt: deps.getFrameCapturedAt } : {},
107560
107854
  isOnline: (id) => this.deviceDirectory.get(id)?.online,
107561
107855
  logger: this.logger.child("linked-stills"),
@@ -110327,6 +110621,7 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
110327
110621
  now: () => Date.now()
110328
110622
  }),
110329
110623
  ...buildLinkedSnapshotReads(api.snapshot),
110624
+ isBatteryCamera: async (deviceId) => await api.battery.getStatus.query({ deviceId }) !== null,
110330
110625
  identityIdsByName: async () => {
110331
110626
  const byName = /* @__PURE__ */ new Map();
110332
110627
  const identities = await stores.identityStore.listIdentities();
@@ -114524,6 +114819,12 @@ function deliveredIdempotencyKey(entryId) {
114524
114819
  function pickedUpIdempotencyKey(entryId) {
114525
114820
  return `pa-pkg-${entryId}-pickedup`;
114526
114821
  }
114822
+ /**
114823
+ * How long a delivery waits for its still to be written before it ships
114824
+ * without one. Under the notification still poll (`NC_STILL_WAIT_MS`, 3 s) so
114825
+ * a slow store never holds the alert longer than the dispatcher would.
114826
+ */
114827
+ var PACKAGE_FRAME_PERSIST_BOUND_MS = 2e3;
114527
114828
  var PackageDropDetector = class {
114528
114829
  deps;
114529
114830
  /** Per-device mirror of the last LOGGED value of the `packageDropEnabled`
@@ -114537,6 +114838,62 @@ var PackageDropDetector = class {
114537
114838
  * LLM gate (D931). `null` means the delivery is dropped (no, or no yes).
114538
114839
  * An empty object means the rule did not ask. A verdict is spread onto the row.
114539
114840
  */
114841
+ /**
114842
+ * The delivery's still, written once under `object:<rowId>` when the entry
114843
+ * had no key frame. Never throws and never waits past
114844
+ * {@link PACKAGE_FRAME_PERSIST_BOUND_MS}: a delivery without a picture still
114845
+ * ships, and says why (D391).
114846
+ */
114847
+ async persistDeliveryStill(entry, rowId, timestamp) {
114848
+ const persist = this.deps.persistDeliveryFrame;
114849
+ if (persist === void 0) return void 0;
114850
+ const tags = { deviceId: entry.deviceId };
114851
+ const meta = {
114852
+ entryId: entry.id,
114853
+ rowId
114854
+ };
114855
+ let timer;
114856
+ try {
114857
+ const bound = new Promise((resolve) => {
114858
+ timer = setTimeout(() => resolve("timeout"), PACKAGE_FRAME_PERSIST_BOUND_MS);
114859
+ });
114860
+ const written = await Promise.race([persist({
114861
+ deviceId: entry.deviceId,
114862
+ trackId: entry.sourceTrackId ?? entry.id,
114863
+ rowId,
114864
+ timestamp
114865
+ }), bound]);
114866
+ if (written === "timeout") {
114867
+ this.deps.logger.warn("package delivery still not written in time — shipping without it", {
114868
+ tags,
114869
+ meta: {
114870
+ ...meta,
114871
+ boundMs: PACKAGE_FRAME_PERSIST_BOUND_MS
114872
+ }
114873
+ });
114874
+ return;
114875
+ }
114876
+ if (written === null) {
114877
+ this.deps.logger.warn("package delivery has no frame to write — shipping without a still", {
114878
+ tags,
114879
+ meta
114880
+ });
114881
+ return;
114882
+ }
114883
+ return written;
114884
+ } catch (err) {
114885
+ this.deps.logger.warn("package delivery still write failed — shipping without it", {
114886
+ tags,
114887
+ meta: {
114888
+ ...meta,
114889
+ error: require_clip_model_registry.errMsg(err)
114890
+ }
114891
+ });
114892
+ return;
114893
+ } finally {
114894
+ if (timer !== void 0) clearTimeout(timer);
114895
+ }
114896
+ }
114540
114897
  async llmFields(entry, rules, hitZones, tags) {
114541
114898
  if (!rules.some((rule) => rule.confirmWithLlm === true && rule.zoneIds.some((id) => hitZones.includes(id)))) return {};
114542
114899
  const confirm = this.deps.confirmPackage;
@@ -114798,6 +115155,7 @@ var PackageDropDetector = class {
114798
115155
  });
114799
115156
  return;
114800
115157
  }
115158
+ const keyFrameMediaId = entry.keyFrameMediaId ?? await this.persistDeliveryStill(entry, rowId, rowTimestamp);
114801
115159
  const ev = {
114802
115160
  ...draft,
114803
115161
  id: rowId
@@ -114809,7 +115167,7 @@ var PackageDropDetector = class {
114809
115167
  eventId: String(rowId),
114810
115168
  className: entry.className,
114811
115169
  zoneIds: hitZones,
114812
- ...entry.keyFrameMediaId !== void 0 ? { keyFrameMediaId: entry.keyFrameMediaId } : {},
115170
+ ...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {},
114813
115171
  bbox: {
114814
115172
  x: entry.bbox.x,
114815
115173
  y: entry.bbox.y,
@@ -118372,6 +118730,7 @@ var BuildStores = class extends BuildStoresBase {
118372
118730
  resolveSettings: (deviceId) => this.resolveDevicePackageDropSettings(deviceId),
118373
118731
  resolveZoneMembershipMinOverlap: async (deviceId) => (await this.resolveDeviceTrackingSettings(deviceId)).zoneMembershipMinOverlap,
118374
118732
  confirmPackage: (input) => this.deps.confirmPackage(input),
118733
+ persistDeliveryFrame: (request) => this.deps.persistPackageDeliveryFrame(request),
118375
118734
  onLlmSilence: (edge) => {
118376
118735
  const deviceId = edge.phase === "silent" ? edge.note.deviceId : edge.deviceId;
118377
118736
  this.ctx.eventBus.emit({
@@ -120493,108 +120852,6 @@ async function ensureTrackHierarchyMedia(deps, input) {
120493
120852
  await Promise.all(tasks);
120494
120853
  }
120495
120854
  //#endregion
120496
- //#region src/pipeline-analytics/pipeline/key-frame-persist.ts
120497
- /**
120498
- * A box is written only when it can be USED — a degenerate rectangle, or one
120499
- * measured on a degenerate frame, says nothing and would read back as a box at
120500
- * the origin. Absent is the honest answer; the reader then pairs a snapshot,
120501
- * exactly as it did before D510.
120502
- *
120503
- * Its result is spread into BOTH writes: the small companion is derived from
120504
- * the very bytes of the native one, so it is the same scene at the same
120505
- * instant and the box maps into it by ratio like any other scale of the frame.
120506
- * A box on one row and not the other would make the summary's answer depend on
120507
- * which row a reader happened to pick.
120508
- */
120509
- function isStatableSubject(subject) {
120510
- if (subject === void 0) return false;
120511
- return subject.w > 0 && subject.h > 0 && subject.frameWidth > 0 && subject.frameHeight > 0;
120512
- }
120513
- /**
120514
- * Read, derive and store one track's key frame + its web companion.
120515
- *
120516
- * PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
120517
- * parks a later frame. The `keyFrame` write lands FIRST and its media id (D482)
120518
- * is what the outcome carries — a `keyFrameSmall` failure afterwards must not
120519
- * re-arm a retry that would re-shoot a key frame the rebuild can already read.
120520
- */
120521
- async function persistTrackKeyFrame(deps, input) {
120522
- const { deviceId, trackId, retry } = input;
120523
- const drop = {
120524
- tags: { deviceId },
120525
- meta: {
120526
- trackId,
120527
- retry
120528
- }
120529
- };
120530
- try {
120531
- const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
120532
- if (parcel === null || parcel.jpeg.byteLength === 0) {
120533
- deps.logger.error("key-frame parcel missing — nothing to write, retry parks a later frame", { ...drop });
120534
- return { kind: "parcel-missing" };
120535
- }
120536
- const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
120537
- const keyFrameMediaId = await deps.media.putReplacing({
120538
- deviceId,
120539
- ownerKind: "track",
120540
- ownerId: trackId,
120541
- kind: "keyFrame",
120542
- timestamp: parcel.timestamp,
120543
- data: parcel.jpeg,
120544
- ...subjectBox
120545
- });
120546
- try {
120547
- const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
120548
- await deps.media.putReplacing({
120549
- deviceId,
120550
- ownerKind: "track",
120551
- ownerId: trackId,
120552
- kind: "keyFrameSmall",
120553
- timestamp: parcel.timestamp,
120554
- data: keyFrameSmall,
120555
- ...subjectBox
120556
- });
120557
- } catch (err) {
120558
- deps.logger.warn("keyFrameSmall write failed — the native keyFrame is stored", {
120559
- ...drop,
120560
- meta: {
120561
- ...drop.meta,
120562
- nodeId: parcel.nodeId,
120563
- error: require_clip_model_registry.errMsg(err)
120564
- }
120565
- });
120566
- }
120567
- deps.logger.debug("key frame written from the parked parcel", {
120568
- ...drop,
120569
- meta: {
120570
- ...drop.meta,
120571
- nodeId: parcel.nodeId,
120572
- width: parcel.width,
120573
- height: parcel.height,
120574
- bytes: parcel.jpeg.byteLength,
120575
- parkedTimestamp: parcel.timestamp
120576
- }
120577
- });
120578
- return {
120579
- kind: "landed",
120580
- keyFrameMediaId
120581
- };
120582
- } catch (err) {
120583
- const error = require_clip_model_registry.errMsg(err);
120584
- deps.logger.warn("key-frame write failed", {
120585
- ...drop,
120586
- meta: {
120587
- ...drop.meta,
120588
- error
120589
- }
120590
- });
120591
- return {
120592
- kind: "error",
120593
- error
120594
- };
120595
- }
120596
- }
120597
- //#endregion
120598
120855
  //#region src/pipeline-analytics/pipeline/key-frame-retry.ts
120599
120856
  /** Retry-path attempts allowed per track. The operator's number. */
120600
120857
  var KEY_FRAME_MAX_RETRIES = 3;
@@ -122178,6 +122435,8 @@ var DEFAULT_ASSIST_MIN_SCORE = .3;
122178
122435
  * cannot page the whole table into memory to build a manifest.
122179
122436
  */
122180
122437
  var RETRAIN_EXPORT_MAX = 2e4;
122438
+ /** How far back a restart looks for a level cap's last persisted boolean. */
122439
+ var LEVEL_SEED_WINDOW_MS = 48 * 36e5;
122181
122440
  /**
122182
122441
  * Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
122183
122442
  * `PipelineAudioInferenceResult` on the bus, refines raw detections
@@ -122453,7 +122712,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122453
122712
  */
122454
122713
  sensorTransitions = new SensorTransitionGate({
122455
122714
  startedAtMs: Date.now(),
122456
- now: () => Date.now()
122715
+ now: () => Date.now(),
122716
+ readSeed: (deviceId, capName) => this.readPersistedLevel(deviceId, capName),
122717
+ onSeedFailed: (deviceId, capName, error) => this.ctx.logger.warn("level seed read failed — gate starts unseeded", {
122718
+ tags: { deviceId },
122719
+ meta: {
122720
+ capName,
122721
+ error: require_clip_model_registry.errMsg(error)
122722
+ }
122723
+ })
122457
122724
  });
122458
122725
  /** `camera-index-unknown` drops, summarised per window — see the file. */
122459
122726
  cameraIndexUnknownDrops = new CameraIndexUnknownDrops({ nowMs: Date.now() });
@@ -124265,6 +124532,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124265
124532
  this.packageDropDetector = value;
124266
124533
  },
124267
124534
  confirmPackage: (input) => this.confirmPackageDelivery(input),
124535
+ persistPackageDeliveryFrame: (request) => this.persistPackageDeliveryFrame(request),
124268
124536
  stationaryEvidence: () => this.stationaryEvidence,
124269
124537
  setStationaryEvidence: (value) => {
124270
124538
  this.stationaryEvidence = value;
@@ -128420,6 +128688,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128420
128688
  via: "frame"
128421
128689
  });
128422
128690
  }
128691
+ /**
128692
+ * #383: a delivery whose entry had no key frame writes the source track's
128693
+ * parked frame under the object row it just created — the one owner the
128694
+ * notification ladder reads for a package event.
128695
+ */
128696
+ async persistPackageDeliveryFrame(request) {
128697
+ const mediaStore = this.mediaStore;
128698
+ if (mediaStore === null) return null;
128699
+ return persistObjectFrame({
128700
+ fetchParkedKeyFrame: async (deviceId, trackId) => {
128701
+ const parcel = await this.mediaCapture.fetchParkedParcel(trackId, "keyFrame");
128702
+ if (parcel === null || parcel.info.deviceId !== deviceId) return null;
128703
+ const subject = parcel.info.subject;
128704
+ return {
128705
+ jpeg: parcel.jpeg,
128706
+ width: parcel.width,
128707
+ height: parcel.height,
128708
+ timestamp: parcel.timestamp,
128709
+ nodeId: parcel.info.nodeId,
128710
+ subject: {
128711
+ x: subject.bbox.x,
128712
+ y: subject.bbox.y,
128713
+ w: subject.bbox.w,
128714
+ h: subject.bbox.h,
128715
+ frameWidth: subject.frameWidth,
128716
+ frameHeight: subject.frameHeight
128717
+ }
128718
+ };
128719
+ },
128720
+ media: mediaStore,
128721
+ logger: this.ctx.logger
128722
+ }, {
128723
+ deviceId: request.deviceId,
128724
+ trackId: request.trackId,
128725
+ objectId: String(request.rowId),
128726
+ timestamp: request.timestamp
128727
+ });
128728
+ }
128423
128729
  async confirmPackageDelivery(input) {
128424
128730
  const crops = this.packageScene?.cropsFor(input.deviceId);
128425
128731
  return confirmPackageWithLlm({
@@ -129421,7 +129727,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129421
129727
  const store = this.sensorEventStore;
129422
129728
  const cache = this.linkedCamerasCache;
129423
129729
  if (store === null || cache === null) return;
129424
- if (!this.sensorSliceCarriedATransition(data)) return;
129730
+ if (!await this.sensorSliceCarriedATransition(data)) return;
129425
129731
  const cameras = await resolveSensorNotificationCameras(cache, data).catch((err) => {
129426
129732
  this.ctx.logger.debug("linked-camera resolution failed — evaluated per row", {
129427
129733
  tags: { deviceId: data.deviceId },
@@ -129589,7 +129895,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129589
129895
  * The drops are SUMMARISED, never logged one by one: this path carries every
129590
129896
  * camera's native `motion` slice at motion rate.
129591
129897
  */
129592
- sensorSliceCarriedATransition(data) {
129898
+ async sensorSliceCarriedATransition(data) {
129593
129899
  const slice = data.slice;
129594
129900
  if (typeof slice !== "object" || slice === null || Array.isArray(slice)) {
129595
129901
  this.ctx.logger.debug("sensor state change carried no slice — dropped", {
@@ -129598,7 +129904,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129598
129904
  });
129599
129905
  return false;
129600
129906
  }
129601
- const verdict = this.sensorTransitions.observe(data.deviceId, data.capName, { ...slice });
129907
+ const verdict = await this.sensorTransitions.observeSettled(data.deviceId, data.capName, { ...slice });
129602
129908
  this.flushSensorTransitionDrops();
129603
129909
  if (verdict.changed) {
129604
129910
  const entries = Object.entries(slice);
@@ -129616,6 +129922,35 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129616
129922
  return verdict.changed;
129617
129923
  }
129618
129924
  /**
129925
+ * The last persisted boolean of a level cap, from device-history — the
129926
+ * gate's seed after a restart. Off the event path (the gate starts it at the
129927
+ * first sighting). A cap that declares no history field, or a series with no
129928
+ * point, answers `null`.
129929
+ */
129930
+ async readPersistedLevel(deviceId, capName) {
129931
+ const key = levelSeriesKey(capName);
129932
+ if (key === null) return null;
129933
+ const to = Date.now();
129934
+ const result = await this.ctx.api.deviceHistory.query.query({
129935
+ deviceId,
129936
+ key,
129937
+ from: to - LEVEL_SEED_WINDOW_MS,
129938
+ to,
129939
+ resolution: "raw",
129940
+ maxPoints: 200
129941
+ });
129942
+ const latest = result.series.flatMap((s) => s.points).reduce((best, p) => best === null || p.t > best.t ? p : best, null);
129943
+ if (latest !== null && latest.last !== null) return {
129944
+ value: latest.last >= .5,
129945
+ since: latest.t
129946
+ };
129947
+ const held = result.series.flatMap((s) => s.before === null ? [] : [s.before]).reduce((best, b) => best === null || b.t > best.t ? b : best, null);
129948
+ return held !== null && held.value !== null ? {
129949
+ value: held.value >= .5,
129950
+ since: held.t
129951
+ } : null;
129952
+ }
129953
+ /**
129619
129954
  * One `info` line per (device, cap, reason) per window, or none.
129620
129955
  *
129621
129956
  * A branch that drops work says so — but this one drops ~0.6 events/s