@camstack/addon-post-analysis 1.2.654 → 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.
@@ -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-CFQBxtE2.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
@@ -68515,6 +68510,7 @@ function wireSubscriptions(deps) {
68515
68510
  deps.onClusterTopology(nodes);
68516
68511
  }));
68517
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()));
68518
68514
  }
68519
68515
  unsubs.push(deps.eventBus.subscribe({ category: require_clip_model_registry.EventCategory.DeviceBindingsChanged }, (ev) => {
68520
68516
  deps.onBindingsChanged(ev.data);
@@ -69076,6 +69072,12 @@ async function composeEventKindsForDevices(deviceIds, composeOne, onError) {
69076
69072
  * nearest one is normally the same detection; the delta is reported so a
69077
69073
  * reviewer can see how far it was.
69078
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
+ *
69079
69081
  * Every OTHER track alive on the camera at that instant contributes its own
69080
69082
  * nearest position the same way — a frame with three people is three boxes,
69081
69083
  * and a recording still (no track of its own) gets everything the pipeline
@@ -69130,6 +69132,18 @@ function toRetrainClassTiers(className, label) {
69130
69132
  label: kept
69131
69133
  } : { macroClass };
69132
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
+ }
69133
69147
  /** A pixel box as a clamped normalised one, or `null` when nothing is left. */
69134
69148
  function normalise(box, frameWidth, frameHeight) {
69135
69149
  if (frameWidth === void 0 || frameHeight === void 0) return null;
@@ -69187,6 +69201,22 @@ function boxFor(track, bbox, basis, deltaMs, score) {
69187
69201
  deltaMs
69188
69202
  };
69189
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
+ }
69190
69220
  /** The own track's box: the recorded subject box first, the nearest position second. */
69191
69221
  function ownBox(input, tolerance) {
69192
69222
  const track = input.ownTrack;
@@ -69209,10 +69239,13 @@ function pipelineBoxesForFrame(input) {
69209
69239
  const out = [];
69210
69240
  const own = ownBox(input, tolerance);
69211
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);
69212
69244
  const at = input.frame.capturedAt;
69213
69245
  if (at === void 0) return out;
69214
69246
  const ownId = input.frame.trackId;
69215
69247
  const seen = new Set(ownId !== void 0 ? [ownId] : []);
69248
+ if (input.verdictSubject !== void 0) seen.add(input.verdictSubject.trackId);
69216
69249
  for (const track of input.neighbours) {
69217
69250
  if (seen.has(track.trackId) || track.deviceId !== input.frame.deviceId) continue;
69218
69251
  seen.add(track.trackId);
@@ -82462,10 +82495,18 @@ function parseActions(raw) {
82462
82495
  for (const entry of raw) {
82463
82496
  const parsed = require_clip_model_registry.NcRuleActionSchema.safeParse(entry);
82464
82497
  if (!parsed.success) return null;
82465
- out.push(parsed.data);
82498
+ out.push(require_clip_model_registry.normalizeNcAction(parsed.data));
82466
82499
  }
82467
82500
  return out;
82468
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
+ }
82469
82510
  function rowToSequence(id, body) {
82470
82511
  const actions = parseActions(body.actions);
82471
82512
  if (actions === null) return null;
@@ -82504,6 +82545,7 @@ var NcSequenceStore = class NcSequenceStore {
82504
82545
  try {
82505
82546
  const { records } = await this.kind.query({ limit: 1e4 });
82506
82547
  const next = /* @__PURE__ */ new Map();
82548
+ const legacyRows = [];
82507
82549
  let skipped = 0;
82508
82550
  for (const row of records) {
82509
82551
  const sequence = rowToSequence(row.id, row.data);
@@ -82512,9 +82554,11 @@ var NcSequenceStore = class NcSequenceStore {
82512
82554
  continue;
82513
82555
  }
82514
82556
  next.set(sequence.id, sequence);
82557
+ if (hasLegacyTarget(row.data.actions)) legacyRows.push(row.id);
82515
82558
  }
82516
82559
  this.byId.clear();
82517
82560
  for (const [id, seq] of next) this.byId.set(id, seq);
82561
+ await this.rewriteLegacyRows(legacyRows, records);
82518
82562
  if (skipped > 0) this.logger.warn("sequence rows skipped as malformed — bindings to them will miss", { meta: { skipped } });
82519
82563
  } catch (err) {
82520
82564
  this.logger.warn("sequence load failed — keeping the sequences already in memory", { meta: {
@@ -82523,6 +82567,28 @@ var NcSequenceStore = class NcSequenceStore {
82523
82567
  } });
82524
82568
  }
82525
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
+ }
82526
82592
  list() {
82527
82593
  return [...this.byId.values()].toSorted((a, b) => a.name.localeCompare(b.name));
82528
82594
  }
@@ -82542,7 +82608,7 @@ var NcSequenceStore = class NcSequenceStore {
82542
82608
  id: (0, node_crypto.randomUUID)(),
82543
82609
  name: input.name,
82544
82610
  ...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
82545
- actions: [...input.actions]
82611
+ actions: input.actions.map(require_clip_model_registry.normalizeNcAction)
82546
82612
  };
82547
82613
  await this.persist(sequence, now, now);
82548
82614
  this.byId.set(sequence.id, sequence);
@@ -82558,7 +82624,7 @@ var NcSequenceStore = class NcSequenceStore {
82558
82624
  id: sequence.id,
82559
82625
  name: sequence.name,
82560
82626
  ...sequence.minDelaySec !== void 0 ? { minDelaySec: sequence.minDelaySec } : {},
82561
- actions: [...sequence.actions]
82627
+ actions: sequence.actions.map(require_clip_model_registry.normalizeNcAction)
82562
82628
  };
82563
82629
  await this.persist(stored, now, now);
82564
82630
  this.byId.set(stored.id, stored);
@@ -82571,7 +82637,7 @@ var NcSequenceStore = class NcSequenceStore {
82571
82637
  id: sequenceId,
82572
82638
  name: input.name,
82573
82639
  ...input.minDelaySec !== void 0 ? { minDelaySec: input.minDelaySec } : {},
82574
- actions: [...input.actions]
82640
+ actions: input.actions.map(require_clip_model_registry.normalizeNcAction)
82575
82641
  };
82576
82642
  const createdAt = await this.readCreatedAt(sequenceId, now);
82577
82643
  await this.persist(sequence, createdAt, now);
@@ -82639,7 +82705,11 @@ var NC_DETECTION_VERDICTS_COLUMNS = [
82639
82705
  var NcDetectionVerdictRecordSchema = require_clip_model_registry.object({
82640
82706
  trackId: require_clip_model_registry.string().min(1),
82641
82707
  deviceId: require_clip_model_registry.number().int(),
82642
- verdict: require_clip_model_registry._enum(["correct", "wrong"]),
82708
+ verdict: require_clip_model_registry._enum([
82709
+ "correct",
82710
+ "wrong",
82711
+ "wrong-label"
82712
+ ]),
82643
82713
  /** The class the operator said it really was. Only with `wrong`. */
82644
82714
  suggestedClass: require_clip_model_registry.string().min(1).optional(),
82645
82715
  /** Whose notification it was (the token's frozen owner), NOT who tapped. */
@@ -82674,6 +82744,16 @@ var NcDetectionVerdictLedger = class {
82674
82744
  logger: deps.logger
82675
82745
  });
82676
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
+ }
82677
82757
  /** Write-through; throws on a failed write so the tap reports it. */
82678
82758
  async record(row) {
82679
82759
  await this.ledger.put(NcDetectionVerdictRecordSchema.parse(row));
@@ -82807,10 +82887,33 @@ function pruneHookBindings(bindings, field, isDead, notes) {
82807
82887
  kept.push(binding);
82808
82888
  continue;
82809
82889
  }
82810
- if (binding.action.kind === "cap" && isDead(binding.action.deviceId)) {
82811
- changed = true;
82812
- notes.removed(field, deviceSubjects([binding.action.deviceId]), "binding-removed");
82813
- 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
+ }
82814
82917
  }
82815
82918
  kept.push(binding);
82816
82919
  }
@@ -82966,7 +83069,11 @@ function referencedDeviceIds(snapshot) {
82966
83069
  add(c.systemEvent?.deviceIds);
82967
83070
  if (c.deviceState !== void 0) ids.add(c.deviceState.deviceId);
82968
83071
  if (c.scene?.deviceId !== void 0) ids.add(c.scene.deviceId);
82969
- 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
+ }
82970
83077
  }
82971
83078
  for (const rule of snapshot.timelapse) add(rule.deviceIds);
82972
83079
  for (const rule of snapshot.summary) add(rule.deviceIds);
@@ -83637,6 +83744,8 @@ var NcDeviceStateCache = class {
83637
83744
  //#region src/notification-center/detection-verdict-buttons.ts
83638
83745
  var VERDICT_CORRECT_ID = "detection-verdict-correct";
83639
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";
83640
83749
  /** `detection-verdict-wrong:<class>` — Wrong, and what it really was. */
83641
83750
  var VERDICT_WRONG_AS_PREFIX = "detection-verdict-wrong:";
83642
83751
  /** The macro classes offered as "it was really…". Short on purpose. */
@@ -83653,6 +83762,15 @@ function verdictButtonRefusal(subject) {
83653
83762
  if (subject.ownerUserId === void 0) return "unowned-target";
83654
83763
  return null;
83655
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
+ }
83656
83774
  /** The classes worth offering for a subject of `className`. */
83657
83775
  function verdictClassChoices(className) {
83658
83776
  return VERDICT_CLASS_CHOICES.filter((c) => c !== className).slice(0, VERDICT_MAX_CLASS_CHOICES);
@@ -83664,13 +83782,17 @@ function verdictClassChoices(className) {
83664
83782
  * when the channel declared room for a fourth button.
83665
83783
  */
83666
83784
  function verdictButtonSpecs(input) {
83667
- const base = [{
83785
+ const plain = [{
83668
83786
  actionId: VERDICT_CORRECT_ID,
83669
83787
  verdict: "correct"
83670
83788
  }, {
83671
83789
  actionId: VERDICT_WRONG_ID,
83672
83790
  verdict: "wrong"
83673
83791
  }];
83792
+ const base = subjectHasLabel(input.className, input.label) ? [...plain, {
83793
+ actionId: VERDICT_WRONG_LABEL_ID,
83794
+ verdict: "wrong-label"
83795
+ }] : plain;
83674
83796
  if (input.maxActions === void 0 || input.maxActions <= 3) return base;
83675
83797
  const room = input.maxActions - base.length;
83676
83798
  const extra = verdictClassChoices(input.className).slice(0, room).map((suggestedClass) => ({
@@ -87401,6 +87523,8 @@ var NcDispatcher = class {
87401
87523
  ...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
87402
87524
  ...entry.payload.subject.className !== void 0 ? { className: entry.payload.subject.className } : {},
87403
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 } : {},
87404
87528
  eventAtMs: entry.payload.subject.timestamp,
87405
87529
  ...target.maxActions !== void 0 ? { maxActions: target.maxActions } : {},
87406
87530
  ...entry.payload.subject.systemEvent !== void 0 ? { systemEvent: entry.payload.subject.systemEvent } : {}
@@ -91764,7 +91888,7 @@ var NcRuleActionRunner = class {
91764
91888
  if (sequence.throttle) this.lastRunAt.set(sequence.id, now);
91765
91889
  let stepsRun = 0;
91766
91890
  for (const [index, action] of sequence.actions.entries()) try {
91767
- await this.step(action, input.origin);
91891
+ await this.step(action, input.origin, sequence.id);
91768
91892
  stepsRun += 1;
91769
91893
  } catch (err) {
91770
91894
  this.deps.logger.warn("rule action failed — stopping the sequence", {
@@ -91804,20 +91928,28 @@ var NcRuleActionRunner = class {
91804
91928
  stepsRun
91805
91929
  };
91806
91930
  }
91807
- async step(action, origin) {
91931
+ async step(action, origin, owner) {
91808
91932
  if (action.kind === "wait") {
91809
91933
  await this.sleep(action.seconds * 1e3);
91810
91934
  return;
91811
91935
  }
91812
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`);
91813
- await this.deps.actuate({
91814
- 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,
91815
91941
  cap: action.cap,
91816
91942
  method: action.method,
91817
91943
  args: action.args ?? {},
91818
91944
  ...origin !== void 0 ? { origin } : {}
91819
91945
  });
91820
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
+ }
91821
91953
  };
91822
91954
  /** Force-enable a shared sequence for a button tap (always runs, still throttled). */
91823
91955
  function asForcedSequence(sequence) {
@@ -100205,6 +100337,12 @@ function rowRefusalId(systemEvent) {
100205
100337
  const value = systemEvent?.data["refusalId"];
100206
100338
  return typeof value === "string" && value.length > 0 ? value : void 0;
100207
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
+ };
100208
100346
  /**
100209
100347
  * The tap-through buttons for ONE delivery, each with its own single-use
100210
100348
  * token.
@@ -100241,6 +100379,7 @@ async function mintButtons(host, input) {
100241
100379
  const verdictOwner = input.ownerUserId;
100242
100380
  const verdictButtons = verdictRefusal !== null || verdictTrackId === void 0 || verdictOwner === void 0 ? [] : verdictButtonSpecs({
100243
100381
  className: input.className,
100382
+ label: input.label,
100244
100383
  maxActions: input.maxActions
100245
100384
  }).map((spec) => {
100246
100385
  const id = (0, node_crypto.randomUUID)();
@@ -100258,6 +100397,11 @@ async function mintButtons(host, input) {
100258
100397
  ...spec.suggestedClass !== void 0 ? { suggestedClass: spec.suggestedClass } : {},
100259
100398
  ...input.maxActions === 0 ? { viaLink: true } : {},
100260
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
+ } } : {},
100261
100405
  deviceId: input.deviceId,
100262
100406
  actionId: spec.actionId,
100263
100407
  targetId: input.targetId,
@@ -100266,7 +100410,7 @@ async function mintButtons(host, input) {
100266
100410
  });
100267
100411
  return {
100268
100412
  id: spec.actionId,
100269
- 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({
100270
100414
  key: "button.verdict.wrongAs",
100271
100415
  vars: { class: spec.suggestedClass }
100272
100416
  }),
@@ -100725,6 +100869,18 @@ var VERDICT_FRAME_REFUSAL_COPY = {
100725
100869
  "still-busy": "The track is gone and the recorder is busy — nothing was changed, try again later",
100726
100870
  failed: "The track is gone and its frame could not be saved — nothing was changed"
100727
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
+ }
100728
100884
  /**
100729
100885
  * The track is gone (aged out, or discarded as a still object): the camera's
100730
100886
  * RECORDING still covers the instant, so the verdict lands a frame there
@@ -100747,7 +100903,8 @@ async function landFrameFromRecording(host, grant) {
100747
100903
  }
100748
100904
  const landed = await addFrame({
100749
100905
  deviceId: grant.deviceId,
100750
- epochMs: grant.eventAtMs
100906
+ epochMs: grant.eventAtMs,
100907
+ ...grant.subject !== void 0 ? { subject: verdictSubjectOf(grant, grant.subject, host.now()) } : {}
100751
100908
  });
100752
100909
  if (!landed.ok) {
100753
100910
  host.logger.warn("detection verdict dropped — the track is gone and the recording refused", {
@@ -101708,6 +101865,7 @@ function buildEntries(host, rule, subject, kind, matchedOn, userTargets = [], or
101708
101865
  ...alarmSource?.eventId !== void 0 ? { eventId: alarmSource.eventId } : hasEventMedia ? { eventId: subject.recordId } : {},
101709
101866
  className: subject.classNames[0] ?? subject.sensorKind ?? "event",
101710
101867
  ...subject.label !== void 0 ? { label: subject.label } : {},
101868
+ ...subject.bbox !== void 0 ? { bbox: subject.bbox } : {},
101711
101869
  ...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {},
101712
101870
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
101713
101871
  zones: renderedZones,
@@ -106028,6 +106186,8 @@ var NotificationCenter = class {
106028
106186
  zoneOwners;
106029
106187
  /** Runs a matched rule's action sequences. Null when no actuator is wired. */
106030
106188
  actionRunner;
106189
+ /** Domain action targets read from the registry (D983); null without a directory. */
106190
+ actionTargets;
106031
106191
  /** Grants behind the tap-through buttons. In-process and single-use — read
106032
106192
  * `action-token.ts` before assuming anything about what a tap proves. */
106033
106193
  actionTokens = new NcActionTokenRegistry();
@@ -106435,9 +106595,15 @@ var NotificationCenter = class {
106435
106595
  });
106436
106596
  this.livenessHoldoff = new DeviceLivenessHoldoff((event) => this.consumeEvent(event), { delayMs: deps.livenessHoldoffMs ?? (process.env.VITEST ? 0 : DEVICE_LIVENESS_HOLDOFF_MS) });
106437
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;
106438
106600
  this.actionRunner = deps.actuate !== void 0 && deps.isCapDeviceScoped !== void 0 ? new NcRuleActionRunner({
106439
106601
  logger: this.logger.child("actions"),
106440
106602
  actuate: (input) => this.actuateWithAttribution(input),
106603
+ ...targets !== null ? { resolveTarget: (target, actionId, owner) => targets.resolve(target, actionId, {}, {
106604
+ actionId,
106605
+ owner
106606
+ }) } : {},
106441
106607
  isDeviceScoped: deps.isCapDeviceScoped,
106442
106608
  ...deps.now !== void 0 ? { now: deps.now } : {}
106443
106609
  }) : null;
@@ -107227,6 +107393,21 @@ var NotificationCenter = class {
107227
107393
  }
107228
107394
  };
107229
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
+ }
107230
107411
  observeOccupancy(deviceId, snapshot, evidence = "scene") {
107231
107412
  return observeOccupancy(this.occupancyWatchHost(), deviceId, snapshot, evidence);
107232
107413
  }
@@ -107402,6 +107583,10 @@ var NotificationCenter = class {
107402
107583
  }
107403
107584
  };
107404
107585
  }
107586
+ /** The device registry changed: a cached domain action target is stale (D983). */
107587
+ invalidateActionTargets() {
107588
+ this.actionTargets?.invalidate();
107589
+ }
107405
107590
  onSystemEvent(event) {
107406
107591
  return onSystemEvent(this.systemEventsHost(), event);
107407
107592
  }
@@ -108875,7 +109060,9 @@ async function addVerdictFrameFromRecording(api, logger, input) {
108875
109060
  }
108876
109061
  const result = await api.addRetrainFrameFromRecording({
108877
109062
  collectionId,
108878
- ...input
109063
+ deviceId: input.deviceId,
109064
+ epochMs: input.epochMs,
109065
+ ...input.subject !== void 0 ? { verdictSubject: input.subject } : {}
108879
109066
  });
108880
109067
  if (result.kind === "added") return { ok: true };
108881
109068
  return {
@@ -109162,6 +109349,20 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109162
109349
  });
109163
109350
  },
109164
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
+ },
109165
109366
  generateVision: async (request) => {
109166
109367
  const result = await api.llm.generateVision.mutate({
109167
109368
  ...request.profileId !== void 0 ? { profileId: request.profileId } : {},
@@ -113787,7 +113988,7 @@ var RetrainCollectionService = class {
113787
113988
  frameId: recordingFrameId(input.deviceId, input.epochMs),
113788
113989
  deviceId: input.deviceId
113789
113990
  });
113790
- 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);
113791
113992
  if (stored.kind === "refused") {
113792
113993
  this.deps.logger.info("retrain collection add refused", {
113793
113994
  tags,
@@ -113799,11 +114000,15 @@ var RetrainCollectionService = class {
113799
114000
  });
113800
114001
  return stored;
113801
114002
  }
113802
- if (stored.reused) await this.deps.collections.addFrame({
113803
- collectionId: input.collectionId,
113804
- frameId: stored.frame.frameId,
113805
- deviceId: input.deviceId
113806
- });
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
+ }
113807
114012
  this.deps.logger.info("retrain frame added to a collection from the recording", {
113808
114013
  tags,
113809
114014
  meta: {
@@ -113813,7 +114018,7 @@ var RetrainCollectionService = class {
113813
114018
  reused: stored.reused
113814
114019
  }
113815
114020
  });
113816
- const [wire] = await this.deps.retrain.toWireFrames([stored.frame]);
114021
+ const [wire] = await this.deps.retrain.toWireFrames([storedFrame]);
113817
114022
  if (wire === void 0) throw new Error("retrain: a stored still produced no wire frame");
113818
114023
  return {
113819
114024
  kind: "added",
@@ -113864,7 +114069,7 @@ var RetrainCollectionService = class {
113864
114069
  const ids = await this.deps.collections.listFrameIds(input.collectionId);
113865
114070
  return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
113866
114071
  }
113867
- async storeStill(deviceId, epochMs, withRow) {
114072
+ async storeStill(deviceId, epochMs, withRow, verdictSubject) {
113868
114073
  const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
113869
114074
  if (existing !== null) return {
113870
114075
  kind: "stored",
@@ -113883,7 +114088,8 @@ var RetrainCollectionService = class {
113883
114088
  const stored = await this.deps.frames.storeRecordingStill({
113884
114089
  deviceId,
113885
114090
  epochMs,
113886
- bytes: still.jpeg
114091
+ bytes: still.jpeg,
114092
+ ...verdictSubject !== void 0 ? { verdictSubject } : {}
113887
114093
  }, withRow);
113888
114094
  return stored.kind === "copied" ? {
113889
114095
  kind: "stored",
@@ -114669,6 +114875,14 @@ function toWireFrame(f) {
114669
114875
  dismissed: f.dismissed ?? []
114670
114876
  };
114671
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
+ }
114672
114886
  /**
114673
114887
  * What the operator did with the proposals on a frame (`retrain-review.ts`).
114674
114888
  *
@@ -114729,7 +114943,11 @@ var RetrainService = class {
114729
114943
  const tracks = [];
114730
114944
  for (const deviceId of devices) tracks.push(...await this.deps.tracks.listStaging(deviceId, limit));
114731
114945
  const ids = tracks.map((t) => t.trackId);
114732
- 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
+ ]);
114733
114951
  return tracks.map((t) => ({
114734
114952
  trackId: t.trackId,
114735
114953
  deviceId: t.deviceId,
@@ -114738,7 +114956,8 @@ var RetrainService = class {
114738
114956
  firstSeen: t.firstSeen,
114739
114957
  lastSeen: t.lastSeen,
114740
114958
  frameCount: frameCounts.get(t.trackId) ?? 0,
114741
- annotationCount: annotationCounts.get(t.trackId) ?? 0
114959
+ annotationCount: annotationCounts.get(t.trackId) ?? 0,
114960
+ ...verdicts.has(t.trackId) ? { verdict: verdicts.get(t.trackId) } : {}
114742
114961
  }));
114743
114962
  }
114744
114963
  /**
@@ -114788,9 +115007,14 @@ var RetrainService = class {
114788
115007
  const ownTracks = /* @__PURE__ */ new Map();
114789
115008
  const subjectBoxes = /* @__PURE__ */ new Map();
114790
115009
  const annotationsByFrame = await this.annotationsByFrame(copies);
115010
+ const verdicts = await this.verdictsOf(copies);
114791
115011
  const out = [];
114792
115012
  for (const copy of copies) {
114793
- const wire = toWireFrame(copy);
115013
+ const verdict = verdictOfFrame(copy, verdicts);
115014
+ const wire = {
115015
+ ...toWireFrame(copy),
115016
+ ...verdict !== void 0 ? { verdict } : {}
115017
+ };
114794
115018
  const collectionIds = collections?.get(copy.frameId);
114795
115019
  const withCollections = {
114796
115020
  ...collections !== null ? {
@@ -114821,7 +115045,8 @@ var RetrainService = class {
114821
115045
  },
114822
115046
  ownTrack: trackId !== "" ? ownTracks.get(trackId) ?? null : null,
114823
115047
  ownSubjectBox: trackId !== "" ? subjectBoxes.get(trackId)?.get(copy.sourceMediaId) ?? null : null,
114824
- neighbours
115048
+ neighbours,
115049
+ ...copy.verdictSubject !== void 0 ? { verdictSubject: copy.verdictSubject } : {}
114825
115050
  });
114826
115051
  out.push({
114827
115052
  ...withCollections,
@@ -114831,6 +115056,22 @@ var RetrainService = class {
114831
115056
  }
114832
115057
  return out;
114833
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
+ }
114834
115075
  /** The URLs of a frame's pixels, when a data plane is served for them. */
114835
115076
  frameUrls(frameId) {
114836
115077
  const frameUrl = this.deps.frameUrl;
@@ -116947,6 +117188,7 @@ var BuildStores = class extends BuildStoresBase {
116947
117188
  subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
116948
117189
  },
116949
117190
  memberships: retrainCollectionStore,
117191
+ verdicts: { forTracks: async (trackIds) => this.notificationCenter?.detectionVerdictsOf(trackIds) ?? /* @__PURE__ */ new Map() },
116950
117192
  frameUrl: (frameId, size) => retrainFramePath(this.ctx.id, frameId, size)
116951
117193
  });
116952
117194
  this.retrain = retrain;
@@ -122658,7 +122900,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122658
122900
  onBindingsChanged: (data) => this.handleBindingsChangedEvent(data),
122659
122901
  onDeviceUnregistered: (deviceId) => this.handleDeviceUnregisteredEvent(deviceId),
122660
122902
  onClusterTopology: (nodes) => this.nodeLiveness.applySnapshot(nodes, Date.now()),
122661
- onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event)
122903
+ onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event),
122904
+ onDeviceRegistryChange: () => this.notificationCenter?.invalidateActionTargets()
122662
122905
  });
122663
122906
  await step("startTrackingTapes", () => this.startTrackingTapes(this.isPostProcessingNode));
122664
122907
  if (this.isPostProcessingNode) await step("startEmbeddingDispatcher", () => this.startEmbeddingDispatcher(logger, transport));