@camstack/addon-post-analysis 1.2.653 → 1.2.655

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 +15 -0
  2. package/dist/{clip-model-registry-Bz1uQvQ8.js → clip-model-registry-C-en5vSD.js} +68371 -66865
  3. package/dist/{clip-model-registry-BrazZJvr.mjs → clip-model-registry-_b3KHYFm.mjs} +68490 -67020
  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-D6DnF6Vk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CF-Mts9j.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-C7OJ7wek.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-DXQUFp7P.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-DtrcY1yz.mjs → hostInit-BSfz5s98.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +478 -81
  12. package/dist/pipeline-analytics/index.mjs +477 -80
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +1 -1
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BKMr4IqL.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-CtNyIYsM.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-Bz1uQvQ8.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-C-en5vSD.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");
@@ -895,7 +895,9 @@ function describeGrant(grant) {
895
895
  return grant.action === "disarm" ? "Alarm disarmed" : `Alarm armed${grant.mode !== void 0 ? ` (${grant.mode})` : ""}`;
896
896
  case "addon-update": return `${grant.packages.length} addon update(s) on ${grant.nodeId}`;
897
897
  case "server-update": return `Server update on ${grant.nodeId}`;
898
- case "detection-verdict": return grant.verdict === "correct" ? "Marked correct — kept for retraining" : grant.suggestedClass === void 0 ? "Marked wrong — kept for retraining" : `Marked wrong, it was a ${grant.suggestedClass} — kept for retraining`;
898
+ case "detection-verdict":
899
+ if (grant.verdict === "wrong-label") return "Marked label wrong, class right — kept for retraining";
900
+ return grant.verdict === "correct" ? "Marked correct — kept for retraining" : grant.suggestedClass === void 0 ? "Marked wrong — kept for retraining" : `Marked wrong, it was a ${grant.suggestedClass} — kept for retraining`;
899
901
  }
900
902
  }
901
903
  /** The audit line's `meta`, per family. A snooze that logged a `sequence` it
@@ -969,7 +971,7 @@ var FAILURE_COPY = {
969
971
  /** The question a link-only channel lands on, answered by a POST form. */
970
972
  function confirmPage(url, grant) {
971
973
  const action = `${url.pathname}?exp=${encodeURIComponent(url.searchParams.get("exp") ?? "")}&sig=${encodeURIComponent(url.searchParams.get("sig") ?? "")}&confirm=1`;
972
- return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee}main{max-width:24rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 1rem}button{font:inherit;padding:.6rem 1.4rem;border-radius:.5rem;border:0;background:#3b82f6;color:#fff}</style><main><h1>Confirm</h1><p>${grant.verdict === "correct" ? "Mark this detection as correct?" : grant.suggestedClass === void 0 ? "Mark this detection as wrong?" : `Mark this detection as wrong, it was a ${grant.suggestedClass}?`}</p><form method="post" action="${action}"><button type="submit">Confirm</button></form></main>`;
974
+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee}main{max-width:24rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 1rem}button{font:inherit;padding:.6rem 1.4rem;border-radius:.5rem;border:0;background:#3b82f6;color:#fff}</style><main><h1>Confirm</h1><p>${grant.verdict === "wrong-label" ? "Mark the label of this detection as wrong (the class is right)?" : grant.verdict === "correct" ? "Mark this detection as correct?" : grant.suggestedClass === void 0 ? "Mark this detection as wrong?" : `Mark this detection as wrong, it was a ${grant.suggestedClass}?`}</p><form method="post" action="${action}"><button type="submit">Confirm</button></form></main>`;
973
975
  }
974
976
  function page(title, detail) {
975
977
  return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee}main{max-width:24rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 .5rem}p{margin:0;color:#aaa}</style><main><h1>${title}</h1><p>${detail}</p></main>`;
@@ -2953,7 +2955,6 @@ var NIGHT = "night";
2953
2955
  var IR = "ir";
2954
2956
  var DAWN = "dawn";
2955
2957
  var DUSK = "dusk";
2956
- var DEG = Math.PI / 180;
2957
2958
  /**
2958
2959
  * Mean channel spread below which a frame is judged infrared.
2959
2960
  *
@@ -2975,38 +2976,6 @@ var IR_CHROMA_THRESHOLD = 6;
2975
2976
  * scene reports `unknown` — the honest answer.
2976
2977
  */
2977
2978
  var TWILIGHT_BAND_MS = 2400 * 1e3;
2978
- /** Julian day number for a JS Date (UTC). */
2979
- function toJulian(date) {
2980
- return date.getTime() / 864e5 + 2440587.5;
2981
- }
2982
- /** Julian day → epoch ms (UTC). */
2983
- function fromJulian(j) {
2984
- return (j - 2440587.5) * 864e5;
2985
- }
2986
- /**
2987
- * Sunrise/sunset (UTC epoch ms) for a location and date via the standard NOAA
2988
- * sunrise equation. Returns `null` for the polar edge cases (sun never
2989
- * rises/sets that day) so the caller can degrade to the clock split.
2990
- */
2991
- function sunTimes(lat, lng, date) {
2992
- const jDate = toJulian(date);
2993
- const jStar = Math.round(jDate - 2451545 + 8e-4) - lng / 360;
2994
- const m = (357.5291 + .98560028 * jStar) % 360;
2995
- const mRad = m * DEG;
2996
- const lambda = (m + (1.9148 * Math.sin(mRad) + .02 * Math.sin(2 * mRad) + 3e-4 * Math.sin(3 * mRad)) + 180 + 102.9372) % 360 * DEG;
2997
- const jTransit = 2451545 + jStar + .0053 * Math.sin(mRad) - .0069 * Math.sin(2 * lambda);
2998
- const declination = Math.asin(Math.sin(lambda) * Math.sin(23.44 * DEG));
2999
- const latRad = lat * DEG;
3000
- const cosOmega = (Math.sin(-.833 * DEG) - Math.sin(latRad) * Math.sin(declination)) / (Math.cos(latRad) * Math.cos(declination));
3001
- if (cosOmega > 1 || cosOmega < -1) return null;
3002
- const omega = Math.acos(cosOmega) / DEG;
3003
- const jRise = jTransit - omega / 360;
3004
- const jSet = jTransit + omega / 360;
3005
- return {
3006
- rise: fromJulian(jRise),
3007
- set: fromJulian(jSet)
3008
- };
3009
- }
3010
2979
  /** Coarse UTC clock split — day between 06:00 and 20:00 UTC. Deterministic
3011
2980
  * last resort when no coordinates let us compute real sun-times. */
3012
2981
  function clockCondition(now) {
@@ -3047,7 +3016,7 @@ function resolveConditionDetailed(input) {
3047
3016
  source: "ir-chroma"
3048
3017
  };
3049
3018
  if (input.coordinates) {
3050
- const times = sunTimes(input.coordinates.lat, input.coordinates.lng, input.now);
3019
+ const times = require_clip_model_registry.sunTimes(input.coordinates.lat, input.coordinates.lng, input.now);
3051
3020
  if (times) {
3052
3021
  const t = input.now.getTime();
3053
3022
  if (Math.abs(t - times.rise) <= TWILIGHT_BAND_MS) return {
@@ -9289,6 +9258,7 @@ var en_default = {
9289
9258
  "button.verdict.correct": "Correct",
9290
9259
  "button.verdict.wrong": "Wrong",
9291
9260
  "button.verdict.wrongAs": "Wrong: it is {{class}}",
9261
+ "button.verdict.wrongLabel": "Wrong label, class OK",
9292
9262
  "action.server-update.title": "Server update",
9293
9263
  "action.server-update.started": "Updating server on {{nodeId}}…",
9294
9264
  "action.server-update.restarting": "Server update staged on {{nodeId}} — the node is restarting. You will hear when it comes back.",
@@ -9694,6 +9664,7 @@ var it_default = {
9694
9664
  "button.verdict.correct": "Corretto",
9695
9665
  "button.verdict.wrong": "Sbagliato",
9696
9666
  "button.verdict.wrongAs": "Sbagliato: è {{class}}",
9667
+ "button.verdict.wrongLabel": "Etichetta sbagliata, classe giusta",
9697
9668
  "action.server-update.title": "Aggiornamento server",
9698
9669
  "action.server-update.started": "Aggiornamento server su {{nodeId}}…",
9699
9670
  "action.server-update.restarting": "Aggiornamento server predisposto su {{nodeId}} — il nodo si riavvia. Ti avviseremo quando torna.",
@@ -39079,6 +39050,11 @@ function rowIsDurableRetrainCopy(data) {
39079
39050
  * audit resolves ownership from INDEX rows, but a future filesystem-side tool
39080
39051
  * must be able to tell the two corpora apart by path alone. */
39081
39052
  var RETRAIN_PATH_PREFIX = "retrain";
39053
+ /** The stored verdict subject, validated; anything unreadable reads as absent. */
39054
+ function narrowVerdictSubject(value) {
39055
+ const parsed = require_clip_model_registry.RetrainVerdictSubjectSchema.safeParse(value);
39056
+ return parsed.success ? { verdictSubject: parsed.data } : {};
39057
+ }
39082
39058
  /** Ids per `IN (…)` page in {@link RetrainFrameStore.getMany}. */
39083
39059
  var GET_MANY_PAGE = 500;
39084
39060
  /**
@@ -39162,7 +39138,8 @@ var RetrainFrameStore = class {
39162
39138
  complete: data["complete"] === true || data["complete"] === 1,
39163
39139
  ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {},
39164
39140
  revision: finiteNumber(data["revision"]) ?? 0,
39165
- dismissed: narrowDismissed(data["dismissed"])
39141
+ dismissed: narrowDismissed(data["dismissed"]),
39142
+ ...narrowVerdictSubject(data["verdictSubject"])
39166
39143
  };
39167
39144
  }
39168
39145
  /** The copies with these ids, in the order given; unknown ids are skipped. */
@@ -39515,7 +39492,8 @@ var RetrainFrameStore = class {
39515
39492
  capturedAt: input.epochMs,
39516
39493
  ...chromaSpread !== void 0 ? { chromaSpread } : {},
39517
39494
  complete: false,
39518
- epochMs: input.epochMs
39495
+ epochMs: input.epochMs,
39496
+ ...input.verdictSubject !== void 0 ? { verdictSubject: input.verdictSubject } : {}
39519
39497
  };
39520
39498
  const path = framePath(frame);
39521
39499
  const persist = async () => {
@@ -39539,7 +39517,8 @@ var RetrainFrameStore = class {
39539
39517
  chromaSpread: frame.chromaSpread ?? null,
39540
39518
  complete: false,
39541
39519
  origin: "recording",
39542
- epochMs: input.epochMs
39520
+ epochMs: input.epochMs,
39521
+ ...input.verdictSubject !== void 0 ? { verdictSubject: input.verdictSubject } : {}
39543
39522
  });
39544
39523
  await this.ledger.transact([row, ...withRow]);
39545
39524
  };
@@ -39576,6 +39555,22 @@ var RetrainFrameStore = class {
39576
39555
  };
39577
39556
  }
39578
39557
  /**
39558
+ * Record the verdict subject on a still that already existed (the moment was
39559
+ * in the dataset before the operator tapped). The latest answer wins, like
39560
+ * the verdict ledger.
39561
+ *
39562
+ * @returns the updated frame, or `null` when no such copy exists.
39563
+ */
39564
+ async setVerdictSubject(frameId, verdictSubject) {
39565
+ const existing = await this.get(frameId);
39566
+ if (existing === null) return null;
39567
+ await this.ledger.update(frameId, { verdictSubject });
39568
+ return {
39569
+ ...existing,
39570
+ verdictSubject
39571
+ };
39572
+ }
39573
+ /**
39579
39574
  * The operator's "every subject in this frame is boxed" (D785 context: an
39580
39575
  * unboxed subject is a false positive to a detector evaluation). Only the
39581
39576
  * operator sets it; nothing derives it from the annotation count, because a
@@ -54723,6 +54718,12 @@ var FrameState = class {
54723
54718
  * per 5 minutes, not one per governor round (the wedge re-sights all
54724
54719
  * night; ~300 identical lines would bury the log it exists to serve). */
54725
54720
  packageOversizeLogAt = /* @__PURE__ */ new Map();
54721
+ /**
54722
+ * The newest frame the package path held per camera — the picture a parcel
54723
+ * promoted from a raw look is stored from (`package-still-capture.ts`). The
54724
+ * scene path concludes without a frame of its own, so it reads this.
54725
+ */
54726
+ packageLookFrames = /* @__PURE__ */ new Map();
54726
54727
  /** Reasons logged on the previous package look — episode helper for drops. */
54727
54728
  packageLookDropReasons = /* @__PURE__ */ new Map();
54728
54729
  };
@@ -59027,6 +59028,89 @@ function sweepOne(deps, deviceId) {
59027
59028
  if (outcome.departed.length > 0 || outcome.departedWithoutActivity.length > 0 || outcome.expired.length > 0) deps.onOutcome(deviceId, outcome, deps.now);
59028
59029
  }
59029
59030
  //#endregion
59031
+ //#region src/pipeline-analytics/pipeline/package/package-still-capture.ts
59032
+ /** A held look frame older than this is not the parcel's picture any more. */
59033
+ var PACKAGE_LOOK_FRAME_MAX_AGE_MS = 15e3;
59034
+ async function jpegOf(reply) {
59035
+ if (reply.tier === "ram-fullframe") return "preview-only";
59036
+ if (reply.jpegBytes !== void 0 && reply.jpegBytes.byteLength > 0) return reply.jpegBytes;
59037
+ if (reply.bytes !== void 0 && reply.bytes.length === reply.width * reply.height * 3) return encodeRgbCropToJpeg(reply.bytes, reply.width, reply.height);
59038
+ return "empty-frame";
59039
+ }
59040
+ /**
59041
+ * The entry carrying its still's `keyFrameMediaId`, or the entry unchanged when
59042
+ * it already had one or none could be stored (said, with the reason).
59043
+ */
59044
+ async function withParcelStill(input) {
59045
+ const { entry, logger } = input;
59046
+ if (entry.keyFrameMediaId !== void 0) return entry;
59047
+ const miss = (reason, detail) => {
59048
+ logger.warn("package still NOT stored — the delivery ships without a picture of the parcel", {
59049
+ tags: { deviceId: entry.deviceId },
59050
+ meta: {
59051
+ entryId: entry.id,
59052
+ reason,
59053
+ ...detail !== void 0 ? { error: detail } : {}
59054
+ }
59055
+ });
59056
+ return entry;
59057
+ };
59058
+ if (input.handle === null) return miss("no-frame-handle");
59059
+ if (input.writer === null) return miss("no-media-store");
59060
+ try {
59061
+ const reply = await input.query({
59062
+ handle: input.handle,
59063
+ bbox: {
59064
+ x: 0,
59065
+ y: 0,
59066
+ w: entry.frameWidth,
59067
+ h: entry.frameHeight
59068
+ },
59069
+ acceptJpegBytes: true,
59070
+ encodeJpeg: true,
59071
+ pin: require_clip_model_registry.nodePin(input.handle.nodeId)
59072
+ });
59073
+ if (reply === null) return miss("native-miss");
59074
+ const jpeg = await jpegOf(reply);
59075
+ if (typeof jpeg === "string") return miss(jpeg);
59076
+ const keyFrameMediaId = await input.writer.putReplacing({
59077
+ deviceId: entry.deviceId,
59078
+ ownerKind: "track",
59079
+ ownerId: entry.id,
59080
+ kind: "keyFrame",
59081
+ timestamp: input.timestamp,
59082
+ data: Buffer.from(jpeg),
59083
+ subjectBox: {
59084
+ x: entry.bbox.x,
59085
+ y: entry.bbox.y,
59086
+ w: entry.bbox.w,
59087
+ h: entry.bbox.h,
59088
+ frameWidth: entry.frameWidth,
59089
+ frameHeight: entry.frameHeight
59090
+ }
59091
+ });
59092
+ return {
59093
+ ...entry,
59094
+ keyFrameMediaId
59095
+ };
59096
+ } catch (err) {
59097
+ return miss("write-failed", require_clip_model_registry.errMsg(err));
59098
+ }
59099
+ }
59100
+ /**
59101
+ * One promotion, in the order that matters: the still is stored FIRST, then the
59102
+ * detector emits `delivered` — the notification centre reads the still when the
59103
+ * event arrives, so a picture written after it is a picture nobody sees.
59104
+ */
59105
+ async function promoteWithStill(input) {
59106
+ const entry = await withParcelStill(input);
59107
+ await input.detector.onStationaryChange({
59108
+ phase: "appeared",
59109
+ entry,
59110
+ timestamp: input.entry.becameStationaryAt
59111
+ });
59112
+ }
59113
+ //#endregion
59030
59114
  //#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
59031
59115
  /**
59032
59116
  * How much of the PERSON box must sit inside a vehicle box to be counted.
@@ -68426,6 +68510,7 @@ function wireSubscriptions(deps) {
68426
68510
  deps.onClusterTopology(nodes);
68427
68511
  }));
68428
68512
  for (const category of NC_SYSTEM_EVENT_CATEGORIES) unsubs.push(deps.eventBus.subscribe({ category }, (ev) => deps.onSystemEvent(ev)));
68513
+ for (const category of require_clip_model_registry.ACTION_TARGET_INVALIDATION_EVENTS) unsubs.push(deps.eventBus.subscribe({ category }, () => deps.onDeviceRegistryChange()));
68429
68514
  }
68430
68515
  unsubs.push(deps.eventBus.subscribe({ category: require_clip_model_registry.EventCategory.DeviceBindingsChanged }, (ev) => {
68431
68516
  deps.onBindingsChanged(ev.data);
@@ -68987,6 +69072,12 @@ async function composeEventKindsForDevices(deviceIds, composeOne, onError) {
68987
69072
  * nearest one is normally the same detection; the delta is reported so a
68988
69073
  * reviewer can see how far it was.
68989
69074
  *
69075
+ * 0. **The subject a notification verdict froze** (a recording still landed by
69076
+ * a verdict whose track was already gone). It is the one subject the
69077
+ * operator answered about, so it comes FIRST and is the own box; the
69078
+ * neighbours below stay secondary. Its box is normalised against the full
69079
+ * frame already, so it is used as is — never re-scaled.
69080
+ *
68990
69081
  * Every OTHER track alive on the camera at that instant contributes its own
68991
69082
  * nearest position the same way — a frame with three people is three boxes,
68992
69083
  * and a recording still (no track of its own) gets everything the pipeline
@@ -69041,6 +69132,18 @@ function toRetrainClassTiers(className, label) {
69041
69132
  label: kept
69042
69133
  } : { macroClass };
69043
69134
  }
69135
+ /** A verdict on the wire: the operator's class split into tiers once, here. */
69136
+ function toRetrainVerdict(input) {
69137
+ const tiers = input.suggestedClass !== void 0 ? toRetrainClassTiers(input.suggestedClass, void 0) : null;
69138
+ return {
69139
+ verdict: input.verdict,
69140
+ trackId: input.trackId,
69141
+ ...input.suggestedClass !== void 0 ? { suggestedClass: input.suggestedClass } : {},
69142
+ ...tiers !== null ? { suggestedMacroClass: tiers.macroClass } : {},
69143
+ ...tiers?.label !== void 0 ? { suggestedLabel: tiers.label } : {},
69144
+ ...input.at !== void 0 ? { at: input.at } : {}
69145
+ };
69146
+ }
69044
69147
  /** A pixel box as a clamped normalised one, or `null` when nothing is left. */
69045
69148
  function normalise(box, frameWidth, frameHeight) {
69046
69149
  if (frameWidth === void 0 || frameHeight === void 0) return null;
@@ -69098,6 +69201,22 @@ function boxFor(track, bbox, basis, deltaMs, score) {
69098
69201
  deltaMs
69099
69202
  };
69100
69203
  }
69204
+ /** The box a notification verdict froze, or `null` when the event carried none. */
69205
+ function boxOfVerdict(subject) {
69206
+ const box = subject.bbox;
69207
+ if (box === void 0) return null;
69208
+ const clamped = normalise(box, 1, 1);
69209
+ if (clamped === null) return null;
69210
+ const tiers = toRetrainClassTiers(subject.className, subject.label);
69211
+ return {
69212
+ bbox: clamped,
69213
+ macroClass: tiers.macroClass,
69214
+ ...tiers.label !== void 0 ? { label: tiers.label } : {},
69215
+ trackId: subject.trackId,
69216
+ basis: "verdict",
69217
+ deltaMs: 0
69218
+ };
69219
+ }
69101
69220
  /** The own track's box: the recorded subject box first, the nearest position second. */
69102
69221
  function ownBox(input, tolerance) {
69103
69222
  const track = input.ownTrack;
@@ -69120,10 +69239,13 @@ function pipelineBoxesForFrame(input) {
69120
69239
  const out = [];
69121
69240
  const own = ownBox(input, tolerance);
69122
69241
  if (own !== null) out.push(own);
69242
+ const verdictBox = input.verdictSubject !== void 0 ? boxOfVerdict(input.verdictSubject) : null;
69243
+ if (verdictBox !== null) out.push(verdictBox);
69123
69244
  const at = input.frame.capturedAt;
69124
69245
  if (at === void 0) return out;
69125
69246
  const ownId = input.frame.trackId;
69126
69247
  const seen = new Set(ownId !== void 0 ? [ownId] : []);
69248
+ if (input.verdictSubject !== void 0) seen.add(input.verdictSubject.trackId);
69127
69249
  for (const track of input.neighbours) {
69128
69250
  if (seen.has(track.trackId) || track.deviceId !== input.frame.deviceId) continue;
69129
69251
  seen.add(track.trackId);
@@ -70516,7 +70638,12 @@ Object.freeze({
70516
70638
  "listSupportedEntityKinds"
70517
70639
  ],
70518
70640
  "device-extension": ["listSlots", "listSources"],
70519
- "device-provider": ["discoverDevices", "getDevices"],
70641
+ "device-provider": [
70642
+ "classifyDiscoveryHits",
70643
+ "discoverDevices",
70644
+ "getDevices",
70645
+ "getDiscoveryModes"
70646
+ ],
70520
70647
  "device-removal-hook": ["listDeviceData"],
70521
70648
  "failure-contribution": ["list"],
70522
70649
  "llm": [
@@ -82368,10 +82495,18 @@ function parseActions(raw) {
82368
82495
  for (const entry of raw) {
82369
82496
  const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
82370
82497
  if (!parsed.success) return null;
82371
- out.push(parsed.data);
82498
+ out.push(require_clip_model_registry.normalizeNcAction(parsed.data));
82372
82499
  }
82373
82500
  return out;
82374
82501
  }
82502
+ /** Does a stored row still carry a pre-D983 cap step (`deviceId`, no `target`)? */
82503
+ function hasLegacyTarget(raw) {
82504
+ if (!Array.isArray(raw)) return false;
82505
+ return raw.some((entry) => {
82506
+ const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
82507
+ return parsed.success && parsed.data.kind === "cap" && parsed.data.target === void 0;
82508
+ });
82509
+ }
82375
82510
  function rowToSequence(id, body) {
82376
82511
  const actions = parseActions(body.actions);
82377
82512
  if (actions === null) return null;
@@ -82410,6 +82545,7 @@ var NcSequenceStore = class NcSequenceStore {
82410
82545
  try {
82411
82546
  const { records } = await this.kind.query({ limit: 1e4 });
82412
82547
  const next = /* @__PURE__ */ new Map();
82548
+ const legacyRows = [];
82413
82549
  let skipped = 0;
82414
82550
  for (const row of records) {
82415
82551
  const sequence = rowToSequence(row.id, row.data);
@@ -82418,9 +82554,11 @@ var NcSequenceStore = class NcSequenceStore {
82418
82554
  continue;
82419
82555
  }
82420
82556
  next.set(sequence.id, sequence);
82557
+ if (hasLegacyTarget(row.data.actions)) legacyRows.push(row.id);
82421
82558
  }
82422
82559
  this.byId.clear();
82423
82560
  for (const [id, seq] of next) this.byId.set(id, seq);
82561
+ await this.rewriteLegacyRows(legacyRows, records);
82424
82562
  if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
82425
82563
  } catch (err) {
82426
82564
  this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
@@ -82429,6 +82567,28 @@ var NcSequenceStore = class NcSequenceStore {
82429
82567
  } });
82430
82568
  }
82431
82569
  }
82570
+ /**
82571
+ * One-shot, idempotent (D983): a row whose steps still carry `deviceId` is
82572
+ * stored again in the target shape. A failed write leaves the row as it was —
82573
+ * it is still READ as the new shape — and is on the record.
82574
+ */
82575
+ async rewriteLegacyRows(ids, records) {
82576
+ for (const id of ids) {
82577
+ const sequence = this.byId.get(id) ?? null;
82578
+ const row = records.find((r) => r.id === id);
82579
+ if (sequence === null || row === void 0) continue;
82580
+ try {
82581
+ const createdAt = row.data.createdAt;
82582
+ await this.persist(sequence, typeof createdAt === "number" && Number.isFinite(createdAt) ? createdAt : this.now(), this.now());
82583
+ this.logger.info("notification sequence rewritten to the target shape", { meta: { sequenceId: id } });
82584
+ } catch (err) {
82585
+ this.logger.warn("notification sequence could not be rewritten — it is still read as the target shape", { meta: {
82586
+ sequenceId: id,
82587
+ error: String(err)
82588
+ } });
82589
+ }
82590
+ }
82591
+ }
82432
82592
  list() {
82433
82593
  return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
82434
82594
  }
@@ -82448,7 +82608,7 @@ var NcSequenceStore = class NcSequenceStore {
82448
82608
  id: (0, node_crypto.randomUUID)(),
82449
82609
  name: input.name,
82450
82610
  ...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
82451
- actions: [...input.actions]
82611
+ actions: input.actions.map(require_clip_model_registry.normalizeNcAction)
82452
82612
  };
82453
82613
  await this.persist(sequence, now, now);
82454
82614
  this.byId.set(sequence.id, sequence);
@@ -82464,7 +82624,7 @@ var NcSequenceStore = class NcSequenceStore {
82464
82624
  id: sequence.id,
82465
82625
  name: sequence.name,
82466
82626
  ...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
82467
- actions: [...sequence.actions]
82627
+ actions: sequence.actions.map(require_clip_model_registry.normalizeNcAction)
82468
82628
  };
82469
82629
  await this.persist(stored, now, now);
82470
82630
  this.byId.set(stored.id, stored);
@@ -82477,7 +82637,7 @@ var NcSequenceStore = class NcSequenceStore {
82477
82637
  id: sequenceId,
82478
82638
  name: input.name,
82479
82639
  ...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
82480
- actions: [...input.actions]
82640
+ actions: input.actions.map(require_clip_model_registry.normalizeNcAction)
82481
82641
  };
82482
82642
  const createdAt = await this.readCreatedAt(sequenceId, now);
82483
82643
  await this.persist(sequence, createdAt, now);
@@ -82545,7 +82705,11 @@ var NC_DETECTION_VERDICTS_COLUMNS = [
82545
82705
  var NcDetectionVerdictRecordSchema = require_clip_model_registry.object({
82546
82706
  trackId: require_clip_model_registry.string().min(1),
82547
82707
  deviceId: require_clip_model_registry.number().int(),
82548
- verdict: require_clip_model_registry._enum(["correct", "wrong"]),
82708
+ verdict: require_clip_model_registry._enum([
82709
+ "correct",
82710
+ "wrong",
82711
+ "wrong-label"
82712
+ ]),
82549
82713
  /** The class the operator said it really was. Only with `wrong`. */
82550
82714
  suggestedClass: require_clip_model_registry.string().min(1).optional(),
82551
82715
  /** Whose notification it was (the token's frozen owner), NOT who tapped. */
@@ -82580,6 +82744,16 @@ var NcDetectionVerdictLedger = class {
82580
82744
  logger: deps.logger
82581
82745
  });
82582
82746
  }
82747
+ /** The answers recorded for these tracks (a track nobody answered is absent).
82748
+ * Reads the in-memory mirror: no I/O, so a listing never waits on it. */
82749
+ forTracks(trackIds) {
82750
+ const out = /* @__PURE__ */ new Map();
82751
+ for (const id of trackIds) {
82752
+ const row = this.ledger.get(id);
82753
+ if (row !== void 0) out.set(id, row);
82754
+ }
82755
+ return out;
82756
+ }
82583
82757
  /** Write-through; throws on a failed write so the tap reports it. */
82584
82758
  async record(row) {
82585
82759
  await this.ledger.put(NcDetectionVerdictRecordSchema.parse(row));
@@ -82713,10 +82887,33 @@ function pruneHookBindings(bindings, field, isDead, notes) {
82713
82887
  kept.push(binding);
82714
82888
  continue;
82715
82889
  }
82716
- if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
82717
- changed = true;
82718
- notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
82719
- continue;
82890
+ if (binding.action.kind === "cap") {
82891
+ const { target } = require_clip_model_registry.normalizeNcCapTarget(binding.action);
82892
+ if (target.kind === "devices") {
82893
+ const dead = target.ids.filter(isDead);
82894
+ if (dead.length === target.ids.length) {
82895
+ changed = true;
82896
+ notes.removed(field, deviceSubjects(dead), "binding-removed");
82897
+ continue;
82898
+ }
82899
+ if (dead.length > 0) {
82900
+ changed = true;
82901
+ notes.removed(field, deviceSubjects(dead), "binding-removed");
82902
+ const alive = target.ids.filter((id) => !isDead(id));
82903
+ kept.push({
82904
+ ...binding,
82905
+ action: require_clip_model_registry.normalizeNcAction({
82906
+ ...binding.action,
82907
+ deviceId: void 0,
82908
+ target: {
82909
+ kind: "devices",
82910
+ ids: alive
82911
+ }
82912
+ })
82913
+ });
82914
+ continue;
82915
+ }
82916
+ }
82720
82917
  }
82721
82918
  kept.push(binding);
82722
82919
  }
@@ -82872,7 +83069,11 @@ function referencedDeviceIds(snapshot) {
82872
83069
  add(c.systemEvent?.deviceIds);
82873
83070
  if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
82874
83071
  if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
82875
- for (const binding of [...rule.actions?.onTrigger ?? [], ...rule.actions?.onTriggerOff ?? []]) if (binding.kind === "do" && binding.action.kind === "cap") ids.add(binding.action.deviceId);
83072
+ for (const binding of [...rule.actions?.onTrigger ?? [], ...rule.actions?.onTriggerOff ?? []]) {
83073
+ if (binding.kind !== "do" || binding.action.kind !== "cap") continue;
83074
+ const { target } = require_clip_model_registry.normalizeNcCapTarget(binding.action);
83075
+ if (target.kind === "devices") add(target.ids);
83076
+ }
82876
83077
  }
82877
83078
  for (const rule of snapshot.timelapse) add(rule.deviceIds);
82878
83079
  for (const rule of snapshot.summary) add(rule.deviceIds);
@@ -83543,6 +83744,8 @@ var NcDeviceStateCache = class {
83543
83744
  //#region src/notification-center/detection-verdict-buttons.ts
83544
83745
  var VERDICT_CORRECT_ID = "detection-verdict-correct";
83545
83746
  var VERDICT_WRONG_ID = "detection-verdict-wrong";
83747
+ /** Wrong LABEL, right class — only for a subject that has a label. */
83748
+ var VERDICT_WRONG_LABEL_ID = "detection-verdict-wrong-label";
83546
83749
  /** `detection-verdict-wrong:<class>` — Wrong, and what it really was. */
83547
83750
  var VERDICT_WRONG_AS_PREFIX = "detection-verdict-wrong:";
83548
83751
  /** The macro classes offered as "it was really…". Short on purpose. */
@@ -83559,6 +83762,15 @@ function verdictButtonRefusal(subject) {
83559
83762
  if (subject.ownerUserId === void 0) return "unowned-target";
83560
83763
  return null;
83561
83764
  }
83765
+ /**
83766
+ * Does the notified subject carry a label below its macro class — a sub-class
83767
+ * (`dog` under `animal`), a recognised name, a plate? The same tier split the
83768
+ * retrain annotations use, so "has a label" means one thing in both places.
83769
+ */
83770
+ function subjectHasLabel(className, label) {
83771
+ if (className === void 0) return false;
83772
+ return toRetrainClassTiers(className, label).label !== void 0;
83773
+ }
83562
83774
  /** The classes worth offering for a subject of `className`. */
83563
83775
  function verdictClassChoices(className) {
83564
83776
  return VERDICT_CLASS_CHOICES.filter((c) => c !== className).slice(0, VERDICT_MAX_CLASS_CHOICES);
@@ -83570,13 +83782,17 @@ function verdictClassChoices(className) {
83570
83782
  * when the channel declared room for a fourth button.
83571
83783
  */
83572
83784
  function verdictButtonSpecs(input) {
83573
- const base = [{
83785
+ const plain = [{
83574
83786
  actionId: VERDICT_CORRECT_ID,
83575
83787
  verdict: "correct"
83576
83788
  }, {
83577
83789
  actionId: VERDICT_WRONG_ID,
83578
83790
  verdict: "wrong"
83579
83791
  }];
83792
+ const base = subjectHasLabel(input.className, input.label) ? [...plain, {
83793
+ actionId: VERDICT_WRONG_LABEL_ID,
83794
+ verdict: "wrong-label"
83795
+ }] : plain;
83580
83796
  if (input.maxActions === void 0 || input.maxActions <= 3) return base;
83581
83797
  const room = input.maxActions - base.length;
83582
83798
  const extra = verdictClassChoices(input.className).slice(0, room).map((suggestedClass) => ({
@@ -87307,6 +87523,8 @@ var NcDispatcher = class {
87307
87523
  ...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
87308
87524
  ...entry.payload.subject.className !== void 0 ? { className: entry.payload.subject.className } : {},
87309
87525
  ...entry.payload.subject.trackId !== void 0 ? { trackId: entry.payload.subject.trackId } : {},
87526
+ ...entry.payload.subject.label !== void 0 ? { label: entry.payload.subject.label } : {},
87527
+ ...entry.payload.subject.bbox !== void 0 ? { bbox: entry.payload.subject.bbox } : {},
87310
87528
  eventAtMs: entry.payload.subject.timestamp,
87311
87529
  ...target.maxActions !== void 0 ? { maxActions: target.maxActions } : {},
87312
87530
  ...entry.payload.subject.systemEvent !== void 0 ? { systemEvent: entry.payload.subject.systemEvent } : {}
@@ -91670,7 +91888,7 @@ var NcRuleActionRunner = class {
91670
91888
  if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
91671
91889
  let stepsRun = 0;
91672
91890
  for (const [index, action] of sequence.actions.entries()) try {
91673
- await this.step(action, input.origin);
91891
+ await this.step(action, input.origin, sequence.id);
91674
91892
  stepsRun += 1;
91675
91893
  } catch (err) {
91676
91894
  this.deps.logger.warn("rule action failed — stopping the sequence", {
@@ -91710,20 +91928,28 @@ var NcRuleActionRunner = class {
91710
91928
  stepsRun
91711
91929
  };
91712
91930
  }
91713
- async step(action, origin) {
91931
+ async step(action, origin, owner) {
91714
91932
  if (action.kind === "wait") {
91715
91933
  await this.sleep(action.seconds * 1e3);
91716
91934
  return;
91717
91935
  }
91718
91936
  if (!this.deps.isDeviceScoped(action.cap)) throw new Error(`rule action refused: "${action.cap}" is not a device capability — only devices can be actuated`);
91719
- await this.deps.actuate({
91720
- deviceId: action.deviceId,
91937
+ const { target, actionId } = require_clip_model_registry.normalizeNcCapTarget(action);
91938
+ const deviceIds = await this.devicesOf(target, actionId, owner);
91939
+ for (const deviceId of deviceIds) await this.deps.actuate({
91940
+ deviceId,
91721
91941
  cap: action.cap,
91722
91942
  method: action.method,
91723
91943
  args: action.args ?? {},
91724
91944
  ...origin !== void 0 ? { origin } : {}
91725
91945
  });
91726
91946
  }
91947
+ async devicesOf(target, actionId, owner) {
91948
+ if (target.kind === "devices") return target.ids;
91949
+ if (target.kind === "var") throw new Error("rule action refused: a sequence has no variables to resolve a target from");
91950
+ if (this.deps.resolveTarget === void 0) throw new Error("rule action refused: this node cannot resolve a domain target");
91951
+ return (await this.deps.resolveTarget(target, actionId, owner)).deviceIds;
91952
+ }
91727
91953
  };
91728
91954
  /** Force-enable a shared sequence for a button tap (always runs, still throttled). */
91729
91955
  function asForcedSequence(sequence) {
@@ -97314,6 +97540,15 @@ var NcSystemStateLedger = class {
97314
97540
  closeLegacyStreamKeys() {
97315
97541
  this.closeWhere("legacy-stream-keys", (row) => row.key.startsWith("stream:") && !DEVICE_STREAM_KEY.test(row.key), "legacy-stream-key");
97316
97542
  }
97543
+ /**
97544
+ * Is this device switched off RIGHT NOW (an open `device-disabled` row)?
97545
+ * D62: the switch outranks the device's liveness and its streams — a device
97546
+ * somebody turned off is reported off, never as offline. `false` while the
97547
+ * store has never answered (unknown is never "disabled").
97548
+ */
97549
+ isDeviceDisabled(deviceId) {
97550
+ return (this.openRows() ?? []).some((row) => row.kind === "device-disabled" && row.deviceId === deviceId);
97551
+ }
97317
97552
  /** The open rows, or `null` while the store has never answered. */
97318
97553
  openRows() {
97319
97554
  return this.deps.rows.answered() ? this.deps.rows.all() : null;
@@ -97820,6 +98055,7 @@ function readRaw(section, ports, nowMs) {
97820
98055
  reason: UNREAD_STATE
97821
98056
  };
97822
98057
  const items = [];
98058
+ const switchedOff = new Set(state.flatMap((row) => row.kind === "device-disabled" && row.deviceId !== void 0 ? [row.deviceId] : []));
97823
98059
  if (section === "device-state" || section === "nodes") {
97824
98060
  const liveness = ports.liveness();
97825
98061
  if (liveness === null) return {
@@ -97829,10 +98065,14 @@ function readRaw(section, ports, nowMs) {
97829
98065
  for (const row of liveness) {
97830
98066
  if (row.state !== "offline" || nowMs - row.updatedAt < ports.livenessHoldoffMs) continue;
97831
98067
  const item = livenessItem(row, ports.lookup);
97832
- if (item !== null && item.section === section) items.push(item);
98068
+ if (item === null || item.section !== section) continue;
98069
+ const id = item.subject.deviceId;
98070
+ if (id !== void 0 && switchedOff.has(id)) continue;
98071
+ items.push(item);
97833
98072
  }
97834
98073
  }
97835
98074
  for (const row of state) {
98075
+ if (row.kind === "stream-offline" && row.deviceId !== void 0 && switchedOff.has(row.deviceId)) continue;
97836
98076
  const item = stateItem(row, ports.lookup, ports.nodes);
97837
98077
  if (item.section === section) items.push(item);
97838
98078
  }
@@ -100097,6 +100337,12 @@ function rowRefusalId(systemEvent) {
100097
100337
  const value = systemEvent?.data["refusalId"];
100098
100338
  return typeof value === "string" && value.length > 0 ? value : void 0;
100099
100339
  }
100340
+ /** The text-catalog key of each verdict button. */
100341
+ var VERDICT_LABEL_KEY = {
100342
+ correct: "button.verdict.correct",
100343
+ wrong: "button.verdict.wrong",
100344
+ "wrong-label": "button.verdict.wrongLabel"
100345
+ };
100100
100346
  /**
100101
100347
  * The tap-through buttons for ONE delivery, each with its own single-use
100102
100348
  * token.
@@ -100133,6 +100379,7 @@ async function mintButtons(host, input) {
100133
100379
  const verdictOwner = input.ownerUserId;
100134
100380
  const verdictButtons = verdictRefusal !== null || verdictTrackId === void 0 || verdictOwner === void 0 ? [] : verdictButtonSpecs({
100135
100381
  className: input.className,
100382
+ label: input.label,
100136
100383
  maxActions: input.maxActions
100137
100384
  }).map((spec) => {
100138
100385
  const id = (0, node_crypto.randomUUID)();
@@ -100150,6 +100397,11 @@ async function mintButtons(host, input) {
100150
100397
  ...spec.suggestedClass !== void 0 ? { suggestedClass: spec.suggestedClass } : {},
100151
100398
  ...input.maxActions === 0 ? { viaLink: true } : {},
100152
100399
  ...input.eventAtMs !== void 0 ? { eventAtMs: input.eventAtMs } : {},
100400
+ ...input.className !== void 0 ? { subject: {
100401
+ className: input.className,
100402
+ ...input.label !== void 0 ? { label: input.label } : {},
100403
+ ...input.bbox !== void 0 ? { bbox: input.bbox } : {}
100404
+ } } : {},
100153
100405
  deviceId: input.deviceId,
100154
100406
  actionId: spec.actionId,
100155
100407
  targetId: input.targetId,
@@ -100158,7 +100410,7 @@ async function mintButtons(host, input) {
100158
100410
  });
100159
100411
  return {
100160
100412
  id: spec.actionId,
100161
- label: spec.suggestedClass === void 0 ? host.texts.text({ key: spec.verdict === "correct" ? "button.verdict.correct" : "button.verdict.wrong" }) : host.texts.text({
100413
+ label: spec.suggestedClass === void 0 ? host.texts.text({ key: VERDICT_LABEL_KEY[spec.verdict] }) : host.texts.text({
100162
100414
  key: "button.verdict.wrongAs",
100163
100415
  vars: { class: spec.suggestedClass }
100164
100416
  }),
@@ -100617,6 +100869,18 @@ var VERDICT_FRAME_REFUSAL_COPY = {
100617
100869
  "still-busy": "The track is gone and the recorder is busy — nothing was changed, try again later",
100618
100870
  failed: "The track is gone and its frame could not be saved — nothing was changed"
100619
100871
  };
100872
+ /** The grant's frozen subject, in the shape the retrain still stores it in. */
100873
+ function verdictSubjectOf(grant, subject, at) {
100874
+ return {
100875
+ trackId: grant.trackId,
100876
+ className: subject.className,
100877
+ ...subject.label !== void 0 ? { label: subject.label } : {},
100878
+ ...subject.bbox !== void 0 ? { bbox: subject.bbox } : {},
100879
+ verdict: grant.verdict,
100880
+ ...grant.suggestedClass !== void 0 ? { suggestedClass: grant.suggestedClass } : {},
100881
+ at
100882
+ };
100883
+ }
100620
100884
  /**
100621
100885
  * The track is gone (aged out, or discarded as a still object): the camera's
100622
100886
  * RECORDING still covers the instant, so the verdict lands a frame there
@@ -100639,7 +100903,8 @@ async function landFrameFromRecording(host, grant) {
100639
100903
  }
100640
100904
  const landed = await addFrame({
100641
100905
  deviceId: grant.deviceId,
100642
- epochMs: grant.eventAtMs
100906
+ epochMs: grant.eventAtMs,
100907
+ ...grant.subject !== void 0 ? { subject: verdictSubjectOf(grant, grant.subject, host.now()) } : {}
100643
100908
  });
100644
100909
  if (!landed.ok) {
100645
100910
  host.logger.warn("detection verdict dropped — the track is gone and the recording refused", {
@@ -101600,6 +101865,7 @@ function buildEntries(host, rule, subject, kind, matchedOn, userTargets = [], or
101600
101865
  ...alarmSource?.eventId !== void 0 ? { eventId: alarmSource.eventId } : hasEventMedia ? { eventId: subject.recordId } : {},
101601
101866
  className: subject.classNames[0] ?? subject.sensorKind ?? "event",
101602
101867
  ...subject.label !== void 0 ? { label: subject.label } : {},
101868
+ ...subject.bbox !== void 0 ? { bbox: subject.bbox } : {},
101603
101869
  ...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {},
101604
101870
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
101605
101871
  zones: renderedZones,
@@ -101997,16 +102263,37 @@ function onSystemEvent(host, event) {
101997
102263
  return;
101998
102264
  }
101999
102265
  if (isDeviceLivenessKind(observation.kind)) {
102266
+ if (dropSwitchedOff(host, system)) return;
102000
102267
  holdoff.arm(observation.subject, incoming);
102001
102268
  return;
102002
102269
  }
102003
102270
  } else if (system !== void 0 && (system.kind.startsWith("device-") || system.kind.startsWith("stream-"))) {
102004
102271
  if (host.lastSystemLiveness.get(system.subject) === system.kind) return;
102005
102272
  host.lastSystemLiveness.set(system.subject, system.kind);
102273
+ if (dropSwitchedOff(host, system)) return;
102006
102274
  }
102007
102275
  host.consumeEvent(incoming);
102008
102276
  }
102009
102277
  /**
102278
+ * D62: a device the operator switched off is reported off, never offline. Its
102279
+ * `device-offline` / `stream-offline` is dropped (and said) while an open
102280
+ * `device-disabled` row exists — the broker of a disabled device stopping is
102281
+ * the switch working, not an outage. The memory of the flip is already kept.
102282
+ */
102283
+ function dropSwitchedOff(host, system) {
102284
+ if (system === void 0 || system.deviceId === void 0) return false;
102285
+ if (system.kind !== "device-offline" && system.kind !== "stream-offline") return false;
102286
+ if (!host.stateSummary.ledger.isDeviceDisabled(system.deviceId)) return false;
102287
+ host.logger.debug("offline not notified — the device is switched off (D62)", {
102288
+ tags: { deviceId: system.deviceId },
102289
+ meta: {
102290
+ kind: system.kind,
102291
+ subject: system.subject
102292
+ }
102293
+ });
102294
+ return true;
102295
+ }
102296
+ /**
102010
102297
  * Arm a node liveness flip. An OFFLINE of a node that announced an update is
102011
102298
  * held for the whole planned-update window and worded as such if it is still
102012
102299
  * sent; the node's return inside the window is logged and sends nothing (the
@@ -105899,6 +106186,8 @@ var NotificationCenter = class {
105899
106186
  zoneOwners;
105900
106187
  /** Runs a matched rule's action sequences. Null when no actuator is wired. */
105901
106188
  actionRunner;
106189
+ /** Domain action targets read from the registry (D983); null without a directory. */
106190
+ actionTargets;
105902
106191
  /** Grants behind the tap-through buttons. In-process and single-use — read
105903
106192
  * `action-token.ts` before assuming anything about what a tap proves. */
105904
106193
  actionTokens = new NcActionTokenRegistry();
@@ -106306,9 +106595,15 @@ var NotificationCenter = class {
106306
106595
  });
106307
106596
  this.livenessHoldoff = new DeviceLivenessHoldoff((event) => this.consumeEvent(event), { delayMs: deps.livenessHoldoffMs ?? (process.env.VITEST ? 0 : DEVICE_LIVENESS_HOLDOFF_MS) });
106308
106597
  this.nodeLivenessHoldoff = new DeviceLivenessHoldoff((event, timing) => this.consumeNodeLiveness(event, timing), { delayMs: deps.nodeLivenessHoldoffMs ?? (process.env.VITEST ? 0 : NODE_LIVENESS_HOLDOFF_MS) });
106598
+ const targets = deps.actionTargetDirectory !== void 0 ? new require_clip_model_registry.ActionTargetResolver(deps.actionTargetDirectory, this.logger.child("actions")) : null;
106599
+ this.actionTargets = targets;
106309
106600
  this.actionRunner = deps.actuate !== void 0 && deps.isCapDeviceScoped !== void 0 ? new NcRuleActionRunner({
106310
106601
  logger: this.logger.child("actions"),
106311
106602
  actuate: (input) => this.actuateWithAttribution(input),
106603
+ ...targets !== null ? { resolveTarget: (target, actionId, owner) => targets.resolve(target, actionId, {}, {
106604
+ actionId,
106605
+ owner
106606
+ }) } : {},
106312
106607
  isDeviceScoped: deps.isCapDeviceScoped,
106313
106608
  ...deps.now !== void 0 ? { now: deps.now } : {}
106314
106609
  }) : null;
@@ -107098,6 +107393,21 @@ var NotificationCenter = class {
107098
107393
  }
107099
107394
  };
107100
107395
  }
107396
+ /**
107397
+ * What the operator answered to the "was this detection right?" buttons, for
107398
+ * the retrain page: by track, the latest answer wins. A track nobody answered
107399
+ * is absent.
107400
+ */
107401
+ detectionVerdictsOf(trackIds) {
107402
+ const out = /* @__PURE__ */ new Map();
107403
+ for (const [trackId, row] of this.verdicts.forTracks(trackIds)) out.set(trackId, toRetrainVerdict({
107404
+ verdict: row.verdict,
107405
+ trackId,
107406
+ at: row.at,
107407
+ suggestedClass: row.suggestedClass
107408
+ }));
107409
+ return out;
107410
+ }
107101
107411
  observeOccupancy(deviceId, snapshot, evidence = "scene") {
107102
107412
  return observeOccupancy(this.occupancyWatchHost(), deviceId, snapshot, evidence);
107103
107413
  }
@@ -107273,6 +107583,10 @@ var NotificationCenter = class {
107273
107583
  }
107274
107584
  };
107275
107585
  }
107586
+ /** The device registry changed: a cached domain action target is stale (D983). */
107587
+ invalidateActionTargets() {
107588
+ this.actionTargets?.invalidate();
107589
+ }
107276
107590
  onSystemEvent(event) {
107277
107591
  return onSystemEvent(this.systemEventsHost(), event);
107278
107592
  }
@@ -108746,7 +109060,9 @@ async function addVerdictFrameFromRecording(api, logger, input) {
108746
109060
  }
108747
109061
  const result = await api.addRetrainFrameFromRecording({
108748
109062
  collectionId,
108749
- ...input
109063
+ deviceId: input.deviceId,
109064
+ epochMs: input.epochMs,
109065
+ ...input.subject !== void 0 ? { verdictSubject: input.subject } : {}
108750
109066
  });
108751
109067
  if (result.kind === "added") return { ok: true };
108752
109068
  return {
@@ -109033,6 +109349,20 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109033
109349
  });
109034
109350
  },
109035
109351
  isCapDeviceScoped: require_clip_model_registry.isDeviceScopedCap,
109352
+ actionTargetDirectory: {
109353
+ listDeviceIdsByCaps: (capNames) => api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...capNames] }),
109354
+ listDevices: async (deviceIds) => {
109355
+ return (await api.deviceManager.listAll.query({
109356
+ deviceIds: [...deviceIds],
109357
+ projection: "slim"
109358
+ })).map((d) => ({
109359
+ id: d.id,
109360
+ type: d.type,
109361
+ location: d.location,
109362
+ disabled: d.disabled
109363
+ }));
109364
+ }
109365
+ },
109036
109366
  generateVision: async (request) => {
109037
109367
  const result = await api.llm.generateVision.mutate({
109038
109368
  ...request.profileId !== void 0 ? { profileId: request.profileId } : {},
@@ -113197,7 +113527,10 @@ var PackageDropDetector = class {
113197
113527
  }
113198
113528
  if (outcome.kind === "yes") {
113199
113529
  this.reportLlmSilence(this.llmSilence.noteAnswered(entry.deviceId), tags);
113200
- return { packageLlmVerdict: outcome.verdict };
113530
+ return {
113531
+ packageLlmVerdict: outcome.verdict,
113532
+ confirmElapsedMs: latencyMs
113533
+ };
113201
113534
  }
113202
113535
  this.dropUncertain(entry, tags, outcome.reason, latencyMs);
113203
113536
  return null;
@@ -113389,10 +113722,12 @@ var PackageDropDetector = class {
113389
113722
  }
113390
113723
  const llm = await this.llmFields(entry, rules, hitZones, tags);
113391
113724
  if (llm === null) return;
113725
+ const { confirmElapsedMs, ...llmRowFields } = llm;
113726
+ const rowTimestamp = timestamp + (confirmElapsedMs ?? 0);
113392
113727
  const draft = {
113393
113728
  kind: "object",
113394
113729
  deviceId: entry.deviceId,
113395
- timestamp,
113730
+ timestamp: rowTimestamp,
113396
113731
  source: "pipeline",
113397
113732
  idempotencyKey,
113398
113733
  trackId: entry.sourceTrackId ?? entry.id,
@@ -113411,7 +113746,7 @@ var PackageDropDetector = class {
113411
113746
  frameHeight: entry.frameHeight,
113412
113747
  ...entry.keyFrameMediaId !== void 0 ? { mediaId: entry.keyFrameMediaId } : {},
113413
113748
  importance: PACKAGE_IMPORTANCE,
113414
- ...llm
113749
+ ...llmRowFields
113415
113750
  };
113416
113751
  const rowId = await this.deps.events.insertObject(draft);
113417
113752
  if (rowId === null) {
@@ -113442,7 +113777,7 @@ var PackageDropDetector = class {
113442
113777
  w: entry.bbox.w,
113443
113778
  h: entry.bbox.h
113444
113779
  },
113445
- timestamp
113780
+ timestamp: rowTimestamp
113446
113781
  });
113447
113782
  this.deps.logger.info("package delivered — all 5 gates passed", {
113448
113783
  tags: { deviceId: entry.deviceId },
@@ -113453,6 +113788,7 @@ var PackageDropDetector = class {
113453
113788
  zoneIds: hitZones,
113454
113789
  dwellMs,
113455
113790
  areaFrac: Number(minAreaFrac.toFixed(5)),
113791
+ ...confirmElapsedMs !== void 0 ? { footageAnchorShiftMs: confirmElapsedMs } : {},
113456
113792
  ...entry.confidence !== void 0 ? { confidence: entry.confidence } : {}
113457
113793
  }
113458
113794
  });
@@ -113652,7 +113988,7 @@ var RetrainCollectionService = class {
113652
113988
  frameId: recordingFrameId(input.deviceId, input.epochMs),
113653
113989
  deviceId: input.deviceId
113654
113990
  });
113655
- const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp]);
113991
+ const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp], input.verdictSubject);
113656
113992
  if (stored.kind === "refused") {
113657
113993
  this.deps.logger.info("retrain collection add refused", {
113658
113994
  tags,
@@ -113664,11 +114000,15 @@ var RetrainCollectionService = class {
113664
114000
  });
113665
114001
  return stored;
113666
114002
  }
113667
- if (stored.reused) await this.deps.collections.addFrame({
113668
- collectionId: input.collectionId,
113669
- frameId: stored.frame.frameId,
113670
- deviceId: input.deviceId
113671
- });
114003
+ let storedFrame = stored.frame;
114004
+ if (stored.reused) {
114005
+ await this.deps.collections.addFrame({
114006
+ collectionId: input.collectionId,
114007
+ frameId: stored.frame.frameId,
114008
+ deviceId: input.deviceId
114009
+ });
114010
+ if (input.verdictSubject !== void 0) storedFrame = await this.deps.frames.setVerdictSubject(stored.frame.frameId, input.verdictSubject) ?? stored.frame;
114011
+ }
113672
114012
  this.deps.logger.info("retrain frame added to a collection from the recording", {
113673
114013
  tags,
113674
114014
  meta: {
@@ -113678,7 +114018,7 @@ var RetrainCollectionService = class {
113678
114018
  reused: stored.reused
113679
114019
  }
113680
114020
  });
113681
- const [wire] = await this.deps.retrain.toWireFrames([stored.frame]);
114021
+ const [wire] = await this.deps.retrain.toWireFrames([storedFrame]);
113682
114022
  if (wire === void 0) throw new Error("retrain: a stored still produced no wire frame");
113683
114023
  return {
113684
114024
  kind: "added",
@@ -113729,7 +114069,7 @@ var RetrainCollectionService = class {
113729
114069
  const ids = await this.deps.collections.listFrameIds(input.collectionId);
113730
114070
  return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
113731
114071
  }
113732
- async storeStill(deviceId, epochMs, withRow) {
114072
+ async storeStill(deviceId, epochMs, withRow, verdictSubject) {
113733
114073
  const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
113734
114074
  if (existing !== null) return {
113735
114075
  kind: "stored",
@@ -113748,7 +114088,8 @@ var RetrainCollectionService = class {
113748
114088
  const stored = await this.deps.frames.storeRecordingStill({
113749
114089
  deviceId,
113750
114090
  epochMs,
113751
- bytes: still.jpeg
114091
+ bytes: still.jpeg,
114092
+ ...verdictSubject !== void 0 ? { verdictSubject } : {}
113752
114093
  }, withRow);
113753
114094
  return stored.kind === "copied" ? {
113754
114095
  kind: "stored",
@@ -114534,6 +114875,14 @@ function toWireFrame(f) {
114534
114875
  dismissed: f.dismissed ?? []
114535
114876
  };
114536
114877
  }
114878
+ /** The answer on a frame: a track copy asks the ledger, a still carries the
114879
+ * subject the notification froze. */
114880
+ function verdictOfFrame(copy, verdicts) {
114881
+ if (copy.origin === "track") return verdicts.get(copy.trackId);
114882
+ const s = copy.verdictSubject;
114883
+ if (s === void 0) return void 0;
114884
+ return toRetrainVerdict(s);
114885
+ }
114537
114886
  /**
114538
114887
  * What the operator did with the proposals on a frame (`retrain-review.ts`).
114539
114888
  *
@@ -114594,7 +114943,11 @@ var RetrainService = class {
114594
114943
  const tracks = [];
114595
114944
  for (const deviceId of devices) tracks.push(...await this.deps.tracks.listStaging(deviceId, limit));
114596
114945
  const ids = tracks.map((t) => t.trackId);
114597
- const [frameCounts, annotationCounts] = await Promise.all([this.deps.frames.countsByTrack(ids), this.deps.annotations.countsByTrack(ids)]);
114946
+ const [frameCounts, annotationCounts, verdicts] = await Promise.all([
114947
+ this.deps.frames.countsByTrack(ids),
114948
+ this.deps.annotations.countsByTrack(ids),
114949
+ this.verdictsOfTracks(ids)
114950
+ ]);
114598
114951
  return tracks.map((t) => ({
114599
114952
  trackId: t.trackId,
114600
114953
  deviceId: t.deviceId,
@@ -114603,7 +114956,8 @@ var RetrainService = class {
114603
114956
  firstSeen: t.firstSeen,
114604
114957
  lastSeen: t.lastSeen,
114605
114958
  frameCount: frameCounts.get(t.trackId) ?? 0,
114606
- annotationCount: annotationCounts.get(t.trackId) ?? 0
114959
+ annotationCount: annotationCounts.get(t.trackId) ?? 0,
114960
+ ...verdicts.has(t.trackId) ? { verdict: verdicts.get(t.trackId) } : {}
114607
114961
  }));
114608
114962
  }
114609
114963
  /**
@@ -114653,9 +115007,14 @@ var RetrainService = class {
114653
115007
  const ownTracks = /* @__PURE__ */ new Map();
114654
115008
  const subjectBoxes = /* @__PURE__ */ new Map();
114655
115009
  const annotationsByFrame = await this.annotationsByFrame(copies);
115010
+ const verdicts = await this.verdictsOf(copies);
114656
115011
  const out = [];
114657
115012
  for (const copy of copies) {
114658
- const wire = toWireFrame(copy);
115013
+ const verdict = verdictOfFrame(copy, verdicts);
115014
+ const wire = {
115015
+ ...toWireFrame(copy),
115016
+ ...verdict !== void 0 ? { verdict } : {}
115017
+ };
114659
115018
  const collectionIds = collections?.get(copy.frameId);
114660
115019
  const withCollections = {
114661
115020
  ...collections !== null ? {
@@ -114686,7 +115045,8 @@ var RetrainService = class {
114686
115045
  },
114687
115046
  ownTrack: trackId !== "" ? ownTracks.get(trackId) ?? null : null,
114688
115047
  ownSubjectBox: trackId !== "" ? subjectBoxes.get(trackId)?.get(copy.sourceMediaId) ?? null : null,
114689
- neighbours
115048
+ neighbours,
115049
+ ...copy.verdictSubject !== void 0 ? { verdictSubject: copy.verdictSubject } : {}
114690
115050
  });
114691
115051
  out.push({
114692
115052
  ...withCollections,
@@ -114696,6 +115056,22 @@ var RetrainService = class {
114696
115056
  }
114697
115057
  return out;
114698
115058
  }
115059
+ /** The verdicts of the tracks these frames were copied from. */
115060
+ async verdictsOf(copies) {
115061
+ return this.verdictsOfTracks(copies.flatMap((c) => c.origin === "track" ? [c.trackId] : []));
115062
+ }
115063
+ async verdictsOfTracks(trackIds) {
115064
+ const port = this.deps.verdicts;
115065
+ const ids = [...new Set(trackIds)];
115066
+ if (port === void 0 || ids.length === 0) return /* @__PURE__ */ new Map();
115067
+ return port.forTracks(ids).catch((err) => {
115068
+ this.deps.logger.warn("retrain frames: verdicts unreadable", { meta: {
115069
+ tracks: ids.length,
115070
+ error: String(err)
115071
+ } });
115072
+ return /* @__PURE__ */ new Map();
115073
+ });
115074
+ }
114699
115075
  /** The URLs of a frame's pixels, when a data plane is served for them. */
114700
115076
  frameUrls(frameId) {
114701
115077
  const frameUrl = this.deps.frameUrl;
@@ -116812,6 +117188,7 @@ var BuildStores = class extends BuildStoresBase {
116812
117188
  subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
116813
117189
  },
116814
117190
  memberships: retrainCollectionStore,
117191
+ verdicts: { forTracks: async (trackIds) => this.notificationCenter?.detectionVerdictsOf(trackIds) ?? /* @__PURE__ */ new Map() },
116815
117192
  frameUrl: (frameId, size) => retrainFramePath(this.ctx.id, frameId, size)
116816
117193
  });
116817
117194
  this.retrain = retrain;
@@ -122523,7 +122900,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122523
122900
  onBindingsChanged: (data) => this.handleBindingsChangedEvent(data),
122524
122901
  onDeviceUnregistered: (deviceId) => this.handleDeviceUnregisteredEvent(deviceId),
122525
122902
  onClusterTopology: (nodes) => this.nodeLiveness.applySnapshot(nodes, Date.now()),
122526
- onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event)
122903
+ onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event),
122904
+ onDeviceRegistryChange: () => this.notificationCenter?.invalidateActionTargets()
122527
122905
  });
122528
122906
  await step("startTrackingTapes", () => this.startTrackingTapes(this.isPostProcessingNode));
122529
122907
  if (this.isPostProcessingNode) await step("startEmbeddingDispatcher", () => this.startEmbeddingDispatcher(logger, transport));
@@ -123984,6 +124362,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
123984
124362
  this.frameState.lastNonPackageActivityAt.delete(data.deviceId);
123985
124363
  this.frameState.packageCollectors.forgetDevice(data.deviceId);
123986
124364
  this.frameState.packageOversizeLogAt.delete(data.deviceId);
124365
+ this.frameState.packageLookFrames.delete(data.deviceId);
123987
124366
  this.frameState.packageLookDropReasons.delete(data.deviceId);
123988
124367
  this.overlayState.clearDevice(data.deviceId);
123989
124368
  this.frameState.overlaySynthesisWarnAt.delete(data.deviceId);
@@ -124007,6 +124386,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124007
124386
  this.frameState.lastNonPackageActivityAt.delete(deviceId);
124008
124387
  this.frameState.packageCollectors.forgetDevice(deviceId);
124009
124388
  this.frameState.packageOversizeLogAt.delete(deviceId);
124389
+ this.frameState.packageLookFrames.delete(deviceId);
124010
124390
  this.frameState.packageLookDropReasons.delete(deviceId);
124011
124391
  this.overlayState.clearDevice(deviceId);
124012
124392
  this.frameState.overlaySynthesisWarnAt.delete(deviceId);
@@ -126989,6 +127369,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
126989
127369
  runPackageStillness(input) {
126990
127370
  if (this.packageDropDetector === null) return;
126991
127371
  const tags = { deviceId: input.deviceId };
127372
+ if (input.frameHandle !== null) this.frameState.packageLookFrames.set(input.deviceId, {
127373
+ handle: input.frameHandle,
127374
+ at: input.timestamp
127375
+ });
126992
127376
  for (const t of input.tracked) {
126993
127377
  if (input.packageClasses.has(t.className)) continue;
126994
127378
  if (!isDelivererInPackageZone(t.zones, input.packageZoneIds)) continue;
@@ -127222,10 +127606,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127222
127606
  ...entry.confidence !== void 0 ? { confidence: entry.confidence } : {}
127223
127607
  }
127224
127608
  });
127225
- this.packageDropDetector.onStationaryChange({
127226
- phase: "appeared",
127609
+ const lookFrame = this.frameState.packageLookFrames.get(input.deviceId);
127610
+ promoteWithStill({
127227
127611
  entry,
127228
- timestamp: entry.becameStationaryAt
127612
+ handle: lookFrame !== void 0 && input.timestamp - lookFrame.at <= PACKAGE_LOOK_FRAME_MAX_AGE_MS ? lookFrame.handle : null,
127613
+ timestamp: lookFrame?.at ?? input.timestamp,
127614
+ query: (request) => this.queryPackageSceneCrop(request),
127615
+ writer: this.mediaStore,
127616
+ logger: this.ctx.logger,
127617
+ detector: this.packageDropDetector
127618
+ }).catch((err) => {
127619
+ this.ctx.logger.warn("package promotion failed", {
127620
+ tags,
127621
+ meta: {
127622
+ entryId: entry.id,
127623
+ error: err instanceof Error ? err.message : String(err)
127624
+ }
127625
+ });
127229
127626
  });
127230
127627
  }
127231
127628
  for (const entry of outcome.departed) {