@camstack/addon-post-analysis 1.2.364 → 1.2.365

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.
@@ -1,4 +1,4 @@
1
- import { $ as NC_ALARM_DEFAULT_PROFILE_ID, $r as storageOccupancyCapability, $t as RECORDING_EXPORT_MAX_READ_BYTES, A as EMBEDDING_MODEL_FAILED, Ai as array, An as applyBlockingEdit, Ar as occupancyScope, At as NcConditionsSchema, B as LABEL_SUPPLY_MACRO, Bi as string, Bn as consumableLowPercentOf, Br as resolveModeDelays, Bt as NcSharedSequenceSchema, C as DEFAULT_DETAIL_CROP_CONVENTION, Ci as hydrateSchema, Cn as TrackRelationSchema, Cr as isSystemEventSummaryRule, Ct as NC_TEMPLATE_VARS, D as DETECTION_PIPELINE_CAP_NAME, Di as sleep$1, Dn as addonWidgetsSourceCapability, Dr as mayWriteToLocation, Dt as NcAlarmSensorsSchema, E as DETECTION_MACRO_CLASSES, En as activateAlarmProfile, Er as linkedStepOf, Et as NcAlarmSensorsInputSchema, F as EVENT_OWNER_TYPES, Fi as number, Fn as audioMetricsCapability, Fr as readMotionTriggeredBy, Ft as NcRulePatchSchema, G as MAX_ARCHIVED_DEBUG_NOTES, Gi as EventCategory, Gn as deriveDetailCropRect, Gr as ruleBelongsToAlarm$1, Gt as NcSystemEventKindSchema, H as ListTrackRelationsInputSchema, Hi as tuple, Hn as cosineSimilarity$1, Ht as NcSnoozeSchema, I as EXPORT_DENSE_MAX_RANGES, Ii as object, In as audioModeOf, Ir as readTimelapseGeneratedAt, It as NcRuleSchema, J as MAX_TRACK_PURITIES, Jn as describeSystemEventDeliveryProblem, Jr as ruleKindOf, Jt as NcTemplatePreviewSchema, K as MAX_BIRTH_DECISION_RECORDS, Kn as derivePostTriggeredAction, Kr as ruleDeviceCompatCapQueries, Kt as NcTaxonomySchema, L as FIRST_LEVEL_MACRO_CLASSES, Li as partialRecord, Ln as buildEventKindDescriptor, Lr as renameAlarmProfile, Lt as NcRuleTargetSchema, M as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Mi as discriminatedUnion, Mn as applyNonBlockingEdit, Mr as pipelineAnalyticsCapability, Mt as NcRuleActionSchema, N as EVENT_DENSITY_BATCH_MAX, Ni as literal, Nn as applySecureOnArmEdit, Nr as plateGalleryCapability, Nt as NcRuleActionsSchema, O as DeclaredDevices, Oi as ZodObject, On as alarmDeviceSecureOnArm, Or as memoizePinnedFfmpeg, Ot as NcAlarmSettingsPatchSchema, P as EVENT_KIND_BY_CAP, Pi as looseObject, Pn as assertTimelapseCadences, Pr as readDeviceStateFrom, Pt as NcRuleInputSchema, Q as MotionTriggeredBySchema, Qn as embeddingRefusalOf, Qr as servedTemplateVars, R as FULL_IMAGE_BBOX, Ri as record, Rn as buildViewerLink, Rr as resolveClipSearchMinScore, Rt as NcRuleUpdateInputSchema, S as CoverStateSchema, Si as defineCustomActions, Sn as TrackRelationKindSchema, Sr as isSystemDelivery, St as NC_TAXONOMY, T as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ti as nodePin, Tn as TrackSourceSchema, Tr as kebabToCamel, Tt as NcAlarmPostTriggeredActionSchema, U as ListTrackRelationsResultSchema, Ui as union, Un as createAlarmProfile, Ur as rootSupplyServes, Ut as NcSnoozeSuppressedSchema, V as LabelAttributionSchema, Vi as templateLiteral, Vn as coreBlockIdFromAddonId, Vr as resolveModeNotifyPolicy, Vt as NcSnoozeInputSchema, W as LockStateSchema, Wi as unknown, Wn as deleteAlarmProfile, Wr as ruleAutoTriggersAlarm, Wt as NcStateSummarySectionIdSchema, X as MOTION_CLOSE_AFTER_MS, Xr as secureActuationFor, Xt as OPS_LOG_DEFAULT_LIMIT, Y as MAX_TRACK_RELATIONS, Yn as describeSystemEventFamilyProblem, Yr as sceneMonitorCapability, Yt as NcTemplateVarDescriptorSchema, Z as MediaFileKindEnum, Zr as sensorRaisedOf, Zt as OpsLogEntrySchema, _ as BaseDevice, _i as errMsg, _n as TRACK_RELATIONS_DEFAULT_LIMIT, _r as isLabelSupplyStepId, _t as NC_GROUPING_MIN_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as systemEventSummaryScheduleOf, an as SCENE_DEFAULT_ANCHOR_THRESHOLD, ar as faceGalleryCapability, at as NC_ALARM_TRIGGERED_MAX_SEC, b as ClearTrackRelationInputSchema, bi as createEvent, bn as TimelapseRuleSchema, br as isRootSuppliableStepId, bt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, c as resolveClusterModelPin, ci as templateVarsFor, cn as SCENE_DEFAULT_QUIET_SECONDS, cr as gifFromMp4Args, ct as NC_CLIP_POST_ROLL_DEFAULT_SEC, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as zoneAnalyticsCapability, dn as SCENE_RESET_RECAPTURES, dr as isAudioClassName, dt as NC_CONDITION_CATALOG, ei as subKindsOf, en as RECORDING_SOURCE_CAMSTACK_ADDON, et as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, fn as SceneMonitorSchema, fr as isDefaultAlarmProfileId, ft as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as DeviceType, gn as TIMELAPSE_DENSE_FLOOR_SEC, gr as isKnownTemplateVar, gt as NC_GROUPING_MAX_SEC, h as ArchivedDebugNoteSchema, hi as isSourceCap, hn as SetTrackRelationInputSchema, hr as isEventOwnerType, ht as NC_GROUPING_DEFAULT_SEC, ii as systemEventFilterApplies, in as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, ir as evictionPolicyOfLocation, it as NC_ALARM_SYSTEM_EVENT_KINDS, j as EMBEDDING_REQUIREMENTS_FAILED, ji as boolean, jn as applyModeDelaysEdits, jt as NcLegacyRuleActionSequenceSchema, k as DetectionSourceSchema, ki as _enum, kn as alarmPanelCapability, kr as notificationRulesCapability, kt as NcConditionDescriptorSchema, l as APPEARANCE_BIN_COUNT, li as vectorDimFromBase64, ln as SCENE_DEFAULT_UNCOVERED_POLICY, lr as isAlarmControllableCap, lt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, m as AlarmArmModeSchema, mi as evaluateSensorEdge, mn as SetTrackPurityResultSchema, mr as isEventKindLabelKey, mt as NC_DEFAULT_SNOOZE_MINUTES, ni as systemEventDeliveryProblems, nn as RetrainStatusSchema, nr as ensureAlarmProfiles, nt as NC_ALARM_DELAY_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, oi as systemEventSummarySplitOf, on as SCENE_DEFAULT_CHECK_INTERVAL_SEC, or as failureContributionCapability, ot as NC_AUDIO_CONFIRM_HITS_DEFAULT, p as AUDIO_MACRO_LABELS, pi as canonicalHash, pn as SetTrackPurityInputSchema, pr as isDetectionMacroClass, pt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, q as MAX_BIRTH_LATENCY_PROXY_MS, qn as deriveRecordingMode, qr as ruleGroupingApplies, qt as NcTemplatePreviewInputSchema, ri as systemEventFamilyProblems, rn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, rr as eventKindLabel, rt as NC_ALARM_NOTIFY_FIELDS, s as readClusterModelPinStrict, si as templateCarriesSummary, sn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, sr as gifDeriveSpeed, st as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as syncActiveAlarmProfileSettings, tn as RebuildObjectEmbeddingsRefusalSchema, tr as encodeVectorBase64, tt as NC_ALARM_DELAY_FIELDS, u as APPEARANCE_BLOCKS, ui as viewerLinkBaseOf, un as SCENE_DIVERGED, ur as isAppLinkBase, ut as NC_CLIP_RETENTION_MAX_SEC, v as BirthDecisionRecordSchema, vi as BaseAddon, vn as TimelapseRuleInputSchema, vr as isMediaOwnerType, vt as NC_SENSOR_TRANSITION_KINDS, w as DEFAULT_EVENT_COLOR, wi as isDeviceScopedCap, wn as TrackSideSchema, wr as isVectorIndexDimConflict, wt as NcAlarmConfigSchema, x as ClearTrackRelationResultSchema, xi as customAction, xn as TrackPuritySchema, xr as isScheduleActive, xt as NC_TANK_SYSTEM_EVENT_KINDS, y as COCO_TO_MACRO, yi as CamProfileSchema, yn as TimelapseRulePatchSchema, yr as isRetiredClearKey, yt as NC_SNOOZE_MAX_MINUTES, z as FailureCounters, zi as strictObject, zn as cloneAlarmSettings, zr as resolveLocationMode, zt as NcScheduleSchema } from "../clip-model-registry-B69Mktce.mjs";
1
+ import { $ as NC_ALARM_DEFAULT_PROFILE_ID, $r as sceneMonitorCapability, $t as RECORDING_EXPORT_MAX_READ_BYTES, A as EMBEDDING_MODEL_FAILED, Ai as nodePin, An as alarmDeviceSecureOnArm, Ar as mayWriteToLocation, At as NcConditionsSchema, B as LABEL_SUPPLY_MACRO, Bi as number, Bn as buildViewerLink, Br as renameAlarmProfile, Bt as NcSharedSequenceSchema, C as DEFAULT_DETAIL_CROP_CONVENTION, Ci as BaseAddon, Cn as TrackPuritySchema, Cr as isRootSuppliableStepId, Ct as NC_TEMPLATE_VARS, D as DETECTION_PIPELINE_CAP_NAME, Di as defineCustomActions, Dn as TrackSourceSchema, Dr as isVectorIndexDimConflict, Dt as NcAlarmSensorsSchema, E as DETECTION_MACRO_CLASSES, Ei as customAction, En as TrackSideSchema, Er as isSystemEventSummaryRule, Et as NcAlarmSensorsInputSchema, F as EVENT_OWNER_TYPES, Fi as array, Fn as applySecureOnArmEdit, Fr as pipelineAnalyticsCapability, Ft as NcRulePatchSchema, G as MAX_ARCHIVED_DEBUG_NOTES, Gi as string, Gn as cosineSimilarity$1, Gt as NcSystemEventKindSchema, H as ListTrackRelationsInputSchema, Hi as partialRecord, Hn as cloneAlarmSettings, Hr as resolveLocationMode, Ht as NcSnoozeSchema, I as EXPORT_DENSE_MAX_RANGES, Ii as boolean, In as assertTimelapseCadences, Ir as plateGalleryCapability, It as NcRuleSchema, J as MAX_TRACK_PURITIES, Ji as union, Jn as deriveDetailCropRect, Jr as ruleAutoTriggersAlarm, Jt as NcTemplatePreviewSchema, K as MAX_BIRTH_DECISION_RECORDS, Ki as templateLiteral, Kn as createAlarmProfile, Kr as retrainRevisionConflictMessage, Kt as NcTaxonomySchema, L as FIRST_LEVEL_MACRO_CLASSES, Li as discriminatedUnion, Ln as audioMetricsCapability, Lr as readDeviceStateFrom, Lt as NcRuleTargetSchema, M as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Mi as sleep$1, Mn as applyBlockingEdit, Mr as notificationRulesCapability, Mt as NcRuleActionSchema, N as EVENT_DENSITY_BATCH_MAX, Ni as ZodObject, Nn as applyModeDelaysEdits, Nr as occupancyScope, Nt as NcRuleActionsSchema, O as DeclaredDevices, Oi as hydrateSchema, On as activateAlarmProfile, Or as kebabToCamel, Ot as NcAlarmSettingsPatchSchema, P as EVENT_KIND_BY_CAP, Pi as _enum, Pn as applyNonBlockingEdit, Pt as NcRuleInputSchema, Q as MotionTriggeredBySchema, Qn as describeSystemEventFamilyProblem, Qr as ruleKindOf, R as FULL_IMAGE_BBOX, Ri as literal, Rn as audioModeOf, Rr as readMotionTriggeredBy, Rt as NcRuleUpdateInputSchema, S as CoverStateSchema, Si as errMsg, Sn as TimelapseRuleSchema, Sr as isRetiredClearKey, St as NC_TAXONOMY, T as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Ti as createEvent, Tn as TrackRelationSchema, Tr as isSystemDelivery, Tt as NcAlarmPostTriggeredActionSchema, U as ListTrackRelationsResultSchema, Ui as record, Un as consumableLowPercentOf, Ur as resolveModeDelays, Ut as NcSnoozeSuppressedSchema, V as LabelAttributionSchema, Vi as object, Vn as classifyRetrainReview, Vr as resolveClipSearchMinScore, Vt as NcSnoozeInputSchema, W as LockStateSchema, Wi as strictObject, Wn as coreBlockIdFromAddonId, Wr as resolveModeNotifyPolicy, Wt as NcStateSummarySectionIdSchema, X as MOTION_CLOSE_AFTER_MS, Xi as EventCategory, Xn as deriveRecordingMode, Xr as ruleDeviceCompatCapQueries, Xt as OPS_LOG_DEFAULT_LIMIT, Y as MAX_TRACK_RELATIONS, Yi as unknown, Yn as derivePostTriggeredAction, Yr as ruleBelongsToAlarm$1, Yt as NcTemplateVarDescriptorSchema, Z as MediaFileKindEnum, Zn as describeSystemEventDeliveryProblem, Zr as ruleGroupingApplies, Zt as OpsLogEntrySchema, _ as BaseDevice, _i as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, _n as SetTrackRelationInputSchema, _r as isEventKindLabelKey, _t as NC_GROUPING_MIN_SEC, a as CLIP_EMBEDDING_STEP_ID, ai as summarizeReview, an as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ar as ensureAlarmProfiles, at as NC_ALARM_TRIGGERED_MAX_SEC, b as ClearTrackRelationInputSchema, bi as isSourceCap, bn as TimelapseRuleInputSchema, br as isLabelSupplyStepId, bt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, c as resolveClusterModelPin, ci as systemEventFamilyProblems, cn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, cr as faceGalleryCapability, ct as NC_CLIP_POST_ROLL_DEFAULT_SEC, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as systemEventSummarySplitOf, dn as SCENE_DEFAULT_UNCOVERED_POLICY, dr as gifFromMp4Args, dt as NC_CONDITION_CATALOG, ei as secureActuationFor, en as RECORDING_SOURCE_CAMSTACK_ADDON, et as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as templateCarriesSummary, fn as SCENE_DIVERGED, fr as isAlarmControllableCap, ft as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as zoneAnalyticsCapability, gn as SetTrackPurityResultSchema, gr as isDetectionMacroClass, gt as NC_GROUPING_MAX_SEC, h as ArchivedDebugNoteSchema, hi as viewerLinkBaseOf, hn as SetTrackPurityInputSchema, hr as isDefaultAlarmProfileId, ht as NC_GROUPING_DEFAULT_SEC, ii as subKindsOf, in as RetrainStatusSchema, ir as encodeVectorBase64, it as NC_ALARM_SYSTEM_EVENT_KINDS, j as EMBEDDING_REQUIREMENTS_FAILED, jn as alarmPanelCapability, jr as memoizePinnedFfmpeg, jt as NcLegacyRuleActionSequenceSchema, k as DetectionSourceSchema, ki as isDeviceScopedCap, kn as addonWidgetsSourceCapability, kr as linkedStepOf, kt as NcConditionDescriptorSchema, l as APPEARANCE_BIN_COUNT, li as systemEventFilterApplies, ln as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, lr as failureContributionCapability, lt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, m as AlarmArmModeSchema, mi as vectorDimFromBase64, mn as SceneMonitorSchema, mr as isAudioClassName, mt as NC_DEFAULT_SNOOZE_MINUTES, ni as servedTemplateVars, nn as RetrainDismissedSchema, nt as NC_ALARM_DELAY_MAX_SEC, o as FACE_EMBEDDING_STEP_ID, oi as syncActiveAlarmProfileSettings, on as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, or as eventKindLabel, ot as NC_AUDIO_CONFIRM_HITS_DEFAULT, p as AUDIO_MACRO_LABELS, pi as templateVarsFor, pn as SCENE_RESET_RECAPTURES, pr as isAppLinkBase, pt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, q as MAX_BIRTH_LATENCY_PROXY_MS, qi as tuple, qn as deleteAlarmProfile, qr as rootSupplyServes, qt as NcTemplatePreviewInputSchema, ri as storageOccupancyCapability, rn as RetrainOriginSchema, rt as NC_ALARM_NOTIFY_FIELDS, s as readClusterModelPinStrict, si as systemEventDeliveryProblems, sn as SCENE_DEFAULT_ANCHOR_THRESHOLD, sr as evictionPolicyOfLocation, st as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as sensorRaisedOf, tn as RebuildObjectEmbeddingsRefusalSchema, tr as embeddingRefusalOf, tt as NC_ALARM_DELAY_FIELDS, u as APPEARANCE_BLOCKS, ui as systemEventSummaryScheduleOf, un as SCENE_DEFAULT_QUIET_SECONDS, ur as gifDeriveSpeed, ut as NC_CLIP_RETENTION_MAX_SEC, v as BirthDecisionRecordSchema, vi as canonicalHash, vn as TIMELAPSE_DENSE_FLOOR_SEC, vr as isEventOwnerType, vt as NC_SENSOR_TRANSITION_KINDS, w as DEFAULT_EVENT_COLOR, wi as CamProfileSchema, wn as TrackRelationKindSchema, wr as isScheduleActive, wt as NcAlarmConfigSchema, x as ClearTrackRelationResultSchema, xi as DeviceType, xn as TimelapseRulePatchSchema, xr as isMediaOwnerType, xt as NC_TANK_SYSTEM_EVENT_KINDS, y as COCO_TO_MACRO, yi as evaluateSensorEdge, yn as TRACK_RELATIONS_DEFAULT_LIMIT, yr as isKnownTemplateVar, yt as NC_SNOOZE_MAX_MINUTES, z as FailureCounters, zi as looseObject, zn as buildEventKindDescriptor, zr as readTimelapseGeneratedAt, zt as NcScheduleSchema } from "../clip-model-registry-BRXTB6Qa.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -48379,6 +48379,7 @@ function startAlarmPanelConvergence(ports) {
48379
48379
  var NC_ARTIFACT_READ_PREFIX_REF = NC_ARTIFACT_READ_PREFIX;
48380
48380
  var TRAINING_EXPORT_PREFIX_REF = "training-export";
48381
48381
  var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
48382
+ var RETRAIN_FRAME_PREFIX_REF = "retrain-frame";
48382
48383
  var DEBUG_EXPORT_PREFIX_REF = "debug-export";
48383
48384
  var TAPE_EXPORT_PREFIX_REF = "tape-export";
48384
48385
  /**
@@ -48404,6 +48405,13 @@ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
48404
48405
  /** Every frame behind a marked selection — a corpus, not an item. */
48405
48406
  [TRAINING_EXPORT_PREFIX_REF]: "admin",
48406
48407
  [RETRAIN_EXPORT_PREFIX_REF]: "admin",
48408
+ /**
48409
+ * One copied retrain frame by its id. `admin`, not `authenticated`: the item
48410
+ * is single, but the cap that served the same bytes (`getRetrainFrameImage`)
48411
+ * is `auth: 'admin'` and the surface is admin curation — widening the bytes
48412
+ * beyond the method they replace would be a policy change nobody asked for.
48413
+ */
48414
+ [RETRAIN_FRAME_PREFIX_REF]: "admin",
48407
48415
  /** Diagnostic bundle: logs, config and frames. */
48408
48416
  [DEBUG_EXPORT_PREFIX_REF]: "admin",
48409
48417
  /** A promoted tracking tape and its ground truth (D681) — a corpus, not an item. */
@@ -70911,6 +70919,67 @@ var TrackStore = class {
70911
70919
  }
70912
70920
  };
70913
70921
  //#endregion
70922
+ //#region src/pipeline-analytics/retrain/retrain-origin.ts
70923
+ /**
70924
+ * Reading the two JSON columns the review rests on — an annotation's `origin`
70925
+ * and a frame's `dismissed` set — back into typed values.
70926
+ *
70927
+ * Parsed with the cap's own schemas rather than cast: a column written by
70928
+ * something that bypassed the guard reads as "no origin" / "nothing dismissed",
70929
+ * which is the safe direction (a missing snapshot makes a verdict approximate;
70930
+ * it never invents one).
70931
+ */
70932
+ function parseJsonIfText(value) {
70933
+ if (typeof value !== "string") return value;
70934
+ try {
70935
+ return JSON.parse(value);
70936
+ } catch {
70937
+ return null;
70938
+ }
70939
+ }
70940
+ /** The stored origin snapshot, or `null` (legacy row / unreadable value). */
70941
+ function narrowOrigin(value) {
70942
+ const parsed = RetrainOriginSchema.safeParse(parseJsonIfText(value));
70943
+ return parsed.success ? parsed.data : null;
70944
+ }
70945
+ /** The stored dismissed set, or an empty list. */
70946
+ function narrowDismissed(value) {
70947
+ const parsed = RetrainDismissedSchema.array().safeParse(parseJsonIfText(value));
70948
+ return parsed.success ? parsed.data : [];
70949
+ }
70950
+ /**
70951
+ * The stable name of a pipeline box inside its frame. One live track draws one
70952
+ * box on a frame, and the track id survives a re-derivation, which is what lets
70953
+ * a dismissal made today still match tomorrow's list.
70954
+ */
70955
+ function pipelineOriginRef(trackId) {
70956
+ return `pipeline:${trackId}`;
70957
+ }
70958
+ /** A re-derived pipeline box as the snapshot an annotation or dismissal keeps. */
70959
+ function pipelineBoxOrigin(box) {
70960
+ return {
70961
+ source: "pipeline",
70962
+ ref: pipelineOriginRef(box.trackId),
70963
+ bbox: box.bbox,
70964
+ macroClass: box.macroClass,
70965
+ ...box.label !== void 0 ? { label: box.label } : {},
70966
+ ...box.score !== void 0 ? { score: box.score } : {},
70967
+ ...box.modelId !== void 0 ? { modelId: box.modelId } : {}
70968
+ };
70969
+ }
70970
+ /** The n-th proposal of a detector (`assist`) or vision-LLM (`llm`) run. */
70971
+ function proposalOrigin(source, index, facts) {
70972
+ return {
70973
+ source,
70974
+ ref: `${source}:${String(index)}`,
70975
+ bbox: facts.bbox,
70976
+ macroClass: facts.macroClass,
70977
+ ...facts.label !== void 0 ? { label: facts.label } : {},
70978
+ ...facts.score !== void 0 ? { score: facts.score } : {},
70979
+ ...facts.modelId !== void 0 ? { modelId: facts.modelId } : {}
70980
+ };
70981
+ }
70982
+ //#endregion
70914
70983
  //#region src/pipeline-analytics/retrain/retrain-frame-store.ts
70915
70984
  /**
70916
70985
  * RetrainFrameStore — the COPIES the retrain dataset owns.
@@ -71080,6 +71149,22 @@ var RETRAIN_FRAME_COLUMNS = [
71080
71149
  {
71081
71150
  name: "epochMs",
71082
71151
  type: "INTEGER"
71152
+ }),
71153
+ (
71154
+ /** Bumped by every annotation save; the compare-and-set a save is guarded
71155
+ * by. The column default makes a pre-revision row read `0`. */
71156
+ {
71157
+ name: "revision",
71158
+ type: "INTEGER",
71159
+ notNull: true,
71160
+ defaultValue: 0
71161
+ }),
71162
+ (
71163
+ /** The proposals the operator deleted (`RetrainDismissed[]`), so a deleted
71164
+ * pipeline box is not offered again. NULL = none. */
71165
+ {
71166
+ name: "dismissed",
71167
+ type: "JSON"
71083
71168
  })
71084
71169
  ];
71085
71170
  var RETRAIN_FRAME_INDEXES = [
@@ -71193,7 +71278,9 @@ var RetrainFrameStore = class {
71193
71278
  ...finiteNumber(data["capturedAt"]) !== void 0 ? { capturedAt: finiteNumber(data["capturedAt"]) } : {},
71194
71279
  ...finiteNumber(data["chromaSpread"]) !== void 0 ? { chromaSpread: finiteNumber(data["chromaSpread"]) } : {},
71195
71280
  complete: data["complete"] === true || data["complete"] === 1,
71196
- ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {}
71281
+ ...finiteNumber(data["epochMs"]) !== void 0 ? { epochMs: finiteNumber(data["epochMs"]) } : {},
71282
+ revision: finiteNumber(data["revision"]) ?? 0,
71283
+ dismissed: narrowDismissed(data["dismissed"])
71197
71284
  };
71198
71285
  }
71199
71286
  /** The copies with these ids, in the order given; unknown ids are skipped. */
@@ -80715,7 +80802,7 @@ async function ensureTrackHierarchyMedia(deps, input) {
80715
80802
  }
80716
80803
  //#endregion
80717
80804
  //#region src/pipeline-analytics/event-media-handler.ts
80718
- var CACHE_CONTROL = "public, max-age=31536000, immutable";
80805
+ var CACHE_CONTROL$1 = "public, max-age=31536000, immutable";
80719
80806
  /** Default / clamp bounds for the `thumb` variant edge (px). Mirrors
80720
80807
  * `shared/frame/square-thumb.ts`; kept here so query parsing stays pure. */
80721
80808
  var THUMB_DEFAULT_SIZE = 160;
@@ -80900,14 +80987,14 @@ function createEventMediaHandler(deps) {
80900
80987
  if (req.headers["if-none-match"] === etag) {
80901
80988
  res.writeHead(304, {
80902
80989
  etag,
80903
- "cache-control": CACHE_CONTROL,
80990
+ "cache-control": CACHE_CONTROL$1,
80904
80991
  "server-timing": serverTiming
80905
80992
  }).end();
80906
80993
  return;
80907
80994
  }
80908
80995
  res.writeHead(200, {
80909
80996
  "content-type": "image/jpeg",
80910
- "cache-control": CACHE_CONTROL,
80997
+ "cache-control": CACHE_CONTROL$1,
80911
80998
  etag,
80912
80999
  "content-length": String(media.bytes.byteLength),
80913
81000
  "server-timing": serverTiming
@@ -82837,6 +82924,13 @@ var RETRAIN_ANNOTATION_COLUMNS = [
82837
82924
  name: "assistScore",
82838
82925
  type: "REAL"
82839
82926
  },
82927
+ (
82928
+ /** The proposal the box started from (`RetrainOrigin`), snapshotted at
82929
+ * accept time. NULL on a row saved before origins existed. */
82930
+ {
82931
+ name: "origin",
82932
+ type: "JSON"
82933
+ }),
82840
82934
  {
82841
82935
  name: "exportedInBatch",
82842
82936
  type: "TEXT"
@@ -82920,6 +83014,7 @@ var RetrainAnnotationStore = class {
82920
83014
  source: narrowSource(data["source"]),
82921
83015
  assistModelId: optionalString(data["assistModelId"]),
82922
83016
  assistScore: typeof score === "number" ? score : void 0,
83017
+ origin: narrowOrigin(data["origin"]),
82923
83018
  exportedInBatch: optionalString(data["exportedInBatch"]),
82924
83019
  createdAt: Number(data["createdAt"] ?? 0)
82925
83020
  };
@@ -82937,12 +83032,14 @@ var RetrainAnnotationStore = class {
82937
83032
  source: a.source,
82938
83033
  assistModelId: a.assistModelId ?? null,
82939
83034
  assistScore: a.assistScore ?? null,
83035
+ origin: a.origin ?? null,
82940
83036
  exportedInBatch: a.exportedInBatch ?? null,
82941
83037
  createdAt: a.createdAt
82942
83038
  };
82943
83039
  }
82944
83040
  /**
82945
- * Replace EVERY annotation on one frame with the supplied set.
83041
+ * Replace EVERY annotation on one frame with the supplied set — atomically,
83042
+ * and only if the frame is still at the revision the caller saw.
82946
83043
  *
82947
83044
  * Whole-frame replacement rather than per-box upsert, deliberately: the unit
82948
83045
  * of ground truth is the frame, and "the operator deleted a box" and "the
@@ -82950,38 +83047,113 @@ var RetrainAnnotationStore = class {
82950
83047
  * patch API would let a frame keep a box the operator removed on a surface
82951
83048
  * that only knew about the boxes it sent.
82952
83049
  *
83050
+ * The old rows' deletion, the new rows, the frame's `dismissed` set and its
83051
+ * bumped `revision` are ONE `settingsStore.transact`, guarded by
83052
+ * `commit.fromRevision`: a crash leaves the frame as it was, a concurrent
83053
+ * save leaves one winner and one `conflict` — never a blend, never a frame
83054
+ * blanked between a delete and an insert.
83055
+ *
82953
83056
  * Returns the persisted rows so the caller renders ids it can address.
82954
83057
  */
82955
- async replaceForFrame(mediaKey, drafts) {
82956
- const existing = await this.listByFrame(mediaKey);
83058
+ async replaceForFrame(mediaKey, drafts, commit) {
82957
83059
  const createdAt = this.now();
82958
83060
  const rows = drafts.map((d) => ({
82959
83061
  ...d,
83062
+ origin: d.origin ?? null,
82960
83063
  id: this.newId(),
82961
83064
  createdAt,
82962
83065
  mediaKey
82963
83066
  }));
82964
- for (const row of rows) await this.store.set.mutate({
82965
- collection: RETRAIN_ANNOTATIONS_COLLECTION,
82966
- key: row.id,
82967
- value: this.toRecord(row)
83067
+ const nextRevision = commit.fromRevision + 1;
83068
+ const result = await this.store.transact.mutate({
83069
+ guard: {
83070
+ collection: RETRAIN_FRAMES_COLLECTION,
83071
+ key: mediaKey,
83072
+ column: "revision",
83073
+ equals: commit.fromRevision
83074
+ },
83075
+ ops: [
83076
+ {
83077
+ op: "deleteWhere",
83078
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
83079
+ filter: { where: { mediaKey } }
83080
+ },
83081
+ {
83082
+ op: "insertMany",
83083
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
83084
+ records: rows.map((row) => ({
83085
+ id: row.id,
83086
+ data: this.toRecord(row)
83087
+ }))
83088
+ },
83089
+ {
83090
+ op: "update",
83091
+ collection: RETRAIN_FRAMES_COLLECTION,
83092
+ id: mediaKey,
83093
+ data: {
83094
+ revision: nextRevision,
83095
+ ...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
83096
+ }
83097
+ }
83098
+ ]
82968
83099
  });
82969
- for (const old of existing) try {
82970
- await this.store.delete.mutate({
83100
+ if (!result.applied) {
83101
+ const current = result.current;
83102
+ return {
83103
+ kind: "conflict",
83104
+ currentRevision: typeof current === "number" ? current : null
83105
+ };
83106
+ }
83107
+ return {
83108
+ kind: "saved",
83109
+ rows,
83110
+ revision: nextRevision
83111
+ };
83112
+ }
83113
+ /**
83114
+ * Set `origin` on rows that have none, in one guarded transaction with a
83115
+ * revision bump. Rows that already carry an origin, or are not on this frame,
83116
+ * are left untouched and not counted. No eligible row ⇒ no write at all.
83117
+ */
83118
+ async setOrigins(mediaKey, origins, commit) {
83119
+ const eligible = (await this.listByFrame(mediaKey)).filter((row) => !row.origin && origins.has(row.id));
83120
+ if (eligible.length === 0) return {
83121
+ kind: "updated",
83122
+ updated: 0,
83123
+ revision: commit.fromRevision
83124
+ };
83125
+ const nextRevision = commit.fromRevision + 1;
83126
+ const result = await this.store.transact.mutate({
83127
+ guard: {
83128
+ collection: RETRAIN_FRAMES_COLLECTION,
83129
+ key: mediaKey,
83130
+ column: "revision",
83131
+ equals: commit.fromRevision
83132
+ },
83133
+ ops: [...eligible.map((row) => ({
83134
+ op: "update",
82971
83135
  collection: RETRAIN_ANNOTATIONS_COLLECTION,
82972
- key: old.id
82973
- });
82974
- } catch (err) {
82975
- this.logger.warn("retrain annotation replace: stale row delete failed", {
82976
- tags: { deviceId: old.deviceId },
82977
- meta: {
82978
- annotationId: old.id,
82979
- mediaKey,
82980
- error: String(err)
82981
- }
82982
- });
83136
+ id: row.id,
83137
+ data: { origin: origins.get(row.id) ?? null }
83138
+ })), {
83139
+ op: "update",
83140
+ collection: RETRAIN_FRAMES_COLLECTION,
83141
+ id: mediaKey,
83142
+ data: { revision: nextRevision }
83143
+ }]
83144
+ });
83145
+ if (!result.applied) {
83146
+ const current = result.current;
83147
+ return {
83148
+ kind: "conflict",
83149
+ currentRevision: typeof current === "number" ? current : null
83150
+ };
82983
83151
  }
82984
- return rows;
83152
+ return {
83153
+ kind: "updated",
83154
+ updated: eligible.length,
83155
+ revision: nextRevision
83156
+ };
82985
83157
  }
82986
83158
  async listByFrame(mediaKey) {
82987
83159
  return this.queryWhere({ mediaKey }, `frame ${mediaKey}`);
@@ -84101,7 +84273,14 @@ var RetrainLlmAssistService = class {
84101
84273
  profileId: profile.id,
84102
84274
  model: answer.model,
84103
84275
  leftHouse: leavesHouse,
84104
- proposals: parsed.objects.map((o) => ({
84276
+ proposals: parsed.objects.map((o, index) => ({
84277
+ origin: proposalOrigin("llm", index, {
84278
+ bbox: o.bbox,
84279
+ macroClass: o.macroClass,
84280
+ ...o.label !== void 0 ? { label: o.label } : {},
84281
+ score: o.confidence,
84282
+ modelId: answer.model
84283
+ }),
84105
84284
  bbox: o.bbox,
84106
84285
  macroClass: o.macroClass,
84107
84286
  ...o.label !== void 0 ? { label: o.label } : {},
@@ -84446,6 +84625,87 @@ async function serveRetrainArchive(_req, res, writeArchive) {
84446
84625
  }
84447
84626
  }
84448
84627
  //#endregion
84628
+ //#region src/pipeline-analytics/retrain/retrain-frame-handler.ts
84629
+ var RETRAIN_FRAME_PREFIX = "retrain-frame";
84630
+ /** Longest side of a `size=thumb` response. */
84631
+ var RETRAIN_THUMB_MAX_SIDE = 480;
84632
+ var CACHE_CONTROL = "private, max-age=31536000, immutable";
84633
+ /** The path `getRetrainFrameUrl`-style callers compose for a frame. */
84634
+ function retrainFramePath$1(addonId, frameId, size) {
84635
+ return `/addon/${addonId}/${RETRAIN_FRAME_PREFIX}/${encodeURIComponent(frameId)}?size=${size}`;
84636
+ }
84637
+ /** `{ frameId, size }` from the request URL (path after the prefix), or `null`. */
84638
+ function parseRetrainFrameRequest(url) {
84639
+ const queryAt = url.indexOf("?");
84640
+ const rawPath = queryAt === -1 ? url : url.slice(0, queryAt);
84641
+ const query = queryAt === -1 ? "" : url.slice(queryAt + 1);
84642
+ const segment = rawPath.replace(/^\/+/, "");
84643
+ if (segment === "" || segment.includes("/")) return null;
84644
+ let frameId;
84645
+ try {
84646
+ frameId = decodeURIComponent(segment);
84647
+ } catch {
84648
+ return null;
84649
+ }
84650
+ if (frameId === "") return null;
84651
+ const size = new URLSearchParams(query).get("size");
84652
+ if (size !== null && size !== "thumb" && size !== "full") return null;
84653
+ return {
84654
+ frameId,
84655
+ size: size ?? "full"
84656
+ };
84657
+ }
84658
+ function createRetrainFrameHandler(deps) {
84659
+ return async (req, res) => {
84660
+ const method = req.method ?? "GET";
84661
+ if (method !== "GET" && method !== "HEAD") {
84662
+ res.writeHead(405, { allow: "GET, HEAD" });
84663
+ res.end();
84664
+ return;
84665
+ }
84666
+ const request = parseRetrainFrameRequest(req.url ?? "");
84667
+ if (request === null) {
84668
+ res.writeHead(404);
84669
+ res.end();
84670
+ return;
84671
+ }
84672
+ const etag = `"${request.frameId}:${request.size}"`;
84673
+ if (req.headers["if-none-match"] === etag) {
84674
+ res.writeHead(304, {
84675
+ etag,
84676
+ "cache-control": CACHE_CONTROL
84677
+ });
84678
+ res.end();
84679
+ return;
84680
+ }
84681
+ try {
84682
+ const stored = await deps.readFrame(request.frameId);
84683
+ if (stored === null) {
84684
+ res.writeHead(404);
84685
+ res.end();
84686
+ return;
84687
+ }
84688
+ const body = request.size === "thumb" ? await deps.resize(stored, RETRAIN_THUMB_MAX_SIDE) : stored;
84689
+ res.writeHead(200, {
84690
+ "content-type": "image/jpeg",
84691
+ "content-length": String(body.byteLength),
84692
+ "cache-control": CACHE_CONTROL,
84693
+ etag
84694
+ });
84695
+ if (method === "HEAD") res.end();
84696
+ else res.end(body);
84697
+ } catch (err) {
84698
+ deps.logger.warn("retrain frame read failed", { meta: {
84699
+ frameId: request.frameId,
84700
+ size: request.size,
84701
+ error: String(err)
84702
+ } });
84703
+ res.writeHead(500, { "content-type": "text/plain" });
84704
+ res.end("retrain frame read failed");
84705
+ }
84706
+ };
84707
+ }
84708
+ //#endregion
84449
84709
  //#region src/pipeline-analytics/retrain/retrain-crop-cut.ts
84450
84710
  /**
84451
84711
  * The `crops` layout's cutter: one subject crop from a retrain frame copy.
@@ -84480,6 +84740,15 @@ function retrainCropWindow(input) {
84480
84740
  height: bottom - top
84481
84741
  };
84482
84742
  }
84743
+ /** A frame re-encoded to a JPEG whose longest side is at most `maxSide` (grid thumbnails). */
84744
+ async function resizeRetrainJpeg(bytes, maxSide) {
84745
+ return sharp(bytes).resize({
84746
+ width: maxSide,
84747
+ height: maxSide,
84748
+ fit: "inside",
84749
+ withoutEnlargement: true
84750
+ }).jpeg({ quality: 80 }).toBuffer();
84751
+ }
84483
84752
  async function cutRetrainCrop(input) {
84484
84753
  const window = retrainCropWindow(input);
84485
84754
  if (window === null) return null;
@@ -85582,9 +85851,27 @@ async function restageRetrainTrack(deps, input) {
85582
85851
  }
85583
85852
  //#endregion
85584
85853
  //#region src/pipeline-analytics/retrain/retrain-service.ts
85854
+ /**
85855
+ * RetrainService — the bodies behind the eleven `pipelineAnalytics.*Retrain*`
85856
+ * cap methods.
85857
+ *
85858
+ * It exists as a service rather than as eleven more methods on a 7,500-line
85859
+ * addon class for the reason `analytics-query-facade.ts` and `track-flags.ts`
85860
+ * already exist: the interesting parts (the copy ordering, the assist plan, the
85861
+ * lifecycle preconditions) are decidable from injected contracts alone, and a
85862
+ * test that had to boot the addon to reach them would not be written.
85863
+ *
85864
+ * Everything durable lives in two collections and one blob prefix:
85865
+ * `pipeline-analytics:retrain-frames` (the copies),
85866
+ * `pipeline-analytics:retrain-annotations` (the ground truth), and
85867
+ * `retrain/<deviceId>/<trackId>/…` (the pixels). No age sweep and no owner
85868
+ * cascade walks any of them — that is the whole point of copying.
85869
+ */
85585
85870
  /** Default page size for the staging worklist. Staging is bounded at 500 per
85586
85871
  * camera (D81), so this is a page, not a ceiling. */
85587
85872
  var DEFAULT_STAGING_LIMIT = 100;
85873
+ /** Tries a last-write-wins save gets when it keeps losing the revision race. */
85874
+ var LAST_WRITE_WINS_ATTEMPTS = 3;
85588
85875
  /** A macro class that failed validation — the write is refused, never coerced. */
85589
85876
  var RetrainAnnotationError = class extends Error {};
85590
85877
  /** A store frame on the wire. A recording still carries no track and no
@@ -85605,7 +85892,31 @@ function toWireFrame(f) {
85605
85892
  height: f.height,
85606
85893
  copiedAt: f.copiedAt,
85607
85894
  ...f.capturedAt !== void 0 ? { capturedAt: f.capturedAt } : {},
85608
- complete: f.complete
85895
+ complete: f.complete,
85896
+ revision: f.revision ?? 0,
85897
+ dismissed: f.dismissed ?? []
85898
+ };
85899
+ }
85900
+ /**
85901
+ * What the operator did with the proposals on a frame (`retrain-review.ts`).
85902
+ *
85903
+ * A frame whose rows predate origins is judged against the pipeline boxes
85904
+ * re-derived now, and a proposal nobody kept is reported `removed` — both
85905
+ * flagged approximate. A frame with snapshots is judged on them alone.
85906
+ */
85907
+ function reviewOfFrame(annotations, pipelineBoxes, dismissed) {
85908
+ const legacy = annotations.some((a) => !a.origin);
85909
+ const review = classifyRetrainReview(annotations.map((a) => ({
85910
+ id: a.id,
85911
+ bbox: a.bbox,
85912
+ macroClass: a.macroClass,
85913
+ ...a.label !== void 0 ? { label: a.label } : {},
85914
+ kind: a.kind,
85915
+ origin: a.origin ?? null
85916
+ })), pipelineBoxes.map(pipelineBoxOrigin), dismissed, { inferRemoved: legacy });
85917
+ return {
85918
+ boxes: review.boxes,
85919
+ summary: summarizeReview(review)
85609
85920
  };
85610
85921
  }
85611
85922
  function toWireAnnotation(a) {
@@ -85622,6 +85933,7 @@ function toWireAnnotation(a) {
85622
85933
  source: a.source,
85623
85934
  ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
85624
85935
  ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
85936
+ origin: a.origin ?? null,
85625
85937
  ...a.exportedInBatch !== void 0 ? { exportedInBatch: a.exportedInBatch } : {},
85626
85938
  createdAt: a.createdAt
85627
85939
  };
@@ -85703,16 +86015,24 @@ var RetrainService = class {
85703
86015
  const evidence = this.deps.pipelineEvidence;
85704
86016
  const ownTracks = /* @__PURE__ */ new Map();
85705
86017
  const subjectBoxes = /* @__PURE__ */ new Map();
86018
+ const annotationsByFrame = await this.annotationsByFrame(copies);
85706
86019
  const out = [];
85707
86020
  for (const copy of copies) {
85708
86021
  const wire = toWireFrame(copy);
85709
86022
  const collectionIds = collections?.get(copy.frameId);
85710
- const withCollections = collections !== null ? {
85711
- ...wire,
85712
- collectionIds: collectionIds ?? []
85713
- } : wire;
86023
+ const withCollections = {
86024
+ ...collections !== null ? {
86025
+ ...wire,
86026
+ collectionIds: collectionIds ?? []
86027
+ } : wire,
86028
+ ...this.frameUrls(copy.frameId)
86029
+ };
86030
+ const reviewOf = (boxes) => reviewOfFrame(annotationsByFrame.get(copy.frameId) ?? [], boxes, copy.dismissed ?? []);
85714
86031
  if (evidence === void 0) {
85715
- out.push(withCollections);
86032
+ out.push({
86033
+ ...withCollections,
86034
+ review: reviewOf([])
86035
+ });
85716
86036
  continue;
85717
86037
  }
85718
86038
  const trackId = copy.origin === "track" ? copy.trackId : "";
@@ -85733,11 +86053,32 @@ var RetrainService = class {
85733
86053
  });
85734
86054
  out.push({
85735
86055
  ...withCollections,
85736
- pipelineBoxes
86056
+ pipelineBoxes,
86057
+ review: reviewOf(pipelineBoxes)
85737
86058
  });
85738
86059
  }
85739
86060
  return out;
85740
86061
  }
86062
+ /** The URLs of a frame's pixels, when a data plane is served for them. */
86063
+ frameUrls(frameId) {
86064
+ const frameUrl = this.deps.frameUrl;
86065
+ if (frameUrl === void 0) return {};
86066
+ return {
86067
+ imageUrl: frameUrl(frameId, "full"),
86068
+ thumbnailUrl: frameUrl(frameId, "thumb")
86069
+ };
86070
+ }
86071
+ /** The saved annotations of these frames, grouped by frame. */
86072
+ async annotationsByFrame(copies) {
86073
+ const grouped = /* @__PURE__ */ new Map();
86074
+ if (copies.length === 0) return grouped;
86075
+ for (const a of await this.deps.annotations.listByFrames(copies.map((c) => c.frameId))) {
86076
+ const list = grouped.get(a.mediaKey);
86077
+ if (list === void 0) grouped.set(a.mediaKey, [a]);
86078
+ else list.push(a);
86079
+ }
86080
+ return grouped;
86081
+ }
85741
86082
  /**
85742
86083
  * The frame a frame-scoped call names, or a refusal. The frame is the
85743
86084
  * authority: its camera must be the caller's, and a `trackId`, when the
@@ -85824,7 +86165,10 @@ var RetrainService = class {
85824
86165
  sourceKind: kindOf.get(mediaId) ?? "keyFrame",
85825
86166
  ...capturedAtOf.has(mediaId) ? { capturedAt: capturedAtOf.get(mediaId) } : {}
85826
86167
  });
85827
- if (result.kind === "copied") copied.push(toWireFrame(result.frame));
86168
+ if (result.kind === "copied") copied.push({
86169
+ ...toWireFrame(result.frame),
86170
+ ...this.frameUrls(result.frame.frameId)
86171
+ });
85828
86172
  else refused.push({
85829
86173
  sourceMediaId: result.sourceMediaId,
85830
86174
  reason: result.reason
@@ -85851,7 +86195,10 @@ var RetrainService = class {
85851
86195
  complete: input.complete
85852
86196
  }
85853
86197
  });
85854
- return toWireFrame(updated);
86198
+ return {
86199
+ ...toWireFrame(updated),
86200
+ ...this.frameUrls(updated.frameId)
86201
+ };
85855
86202
  }
85856
86203
  /** Un-select: annotations first, then the copy. Never the other way round —
85857
86204
  * an annotation whose frame is gone is unopenable and unfixable. The frame's
@@ -85992,9 +86339,16 @@ var RetrainService = class {
85992
86339
  modelId: result.modelId,
85993
86340
  stepId: plan.stepId,
85994
86341
  minScore: plan.minScore,
85995
- proposals: mapping.proposals.map((p) => {
86342
+ proposals: mapping.proposals.map((p, index) => {
85996
86343
  const draft = proposalToDraft(p, owner);
85997
86344
  return {
86345
+ origin: proposalOrigin("assist", index, {
86346
+ bbox: draft.bbox,
86347
+ macroClass: draft.macroClass,
86348
+ ...draft.label !== void 0 ? { label: draft.label } : {},
86349
+ ...draft.assistScore !== void 0 ? { score: draft.assistScore } : {},
86350
+ ...draft.assistModelId !== void 0 ? { modelId: draft.assistModelId } : {}
86351
+ }),
85998
86352
  bbox: draft.bbox,
85999
86353
  macroClass: draft.macroClass,
86000
86354
  ...draft.label !== void 0 ? { label: draft.label } : {},
@@ -86041,9 +86395,12 @@ var RetrainService = class {
86041
86395
  kind: a.kind,
86042
86396
  source: a.source,
86043
86397
  ...a.assistModelId !== void 0 ? { assistModelId: a.assistModelId } : {},
86044
- ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {}
86398
+ ...a.assistScore !== void 0 ? { assistScore: a.assistScore } : {},
86399
+ origin: a.origin ?? null
86045
86400
  }));
86046
- const saved = await this.deps.annotations.replaceForFrame(input.frameId, drafts);
86401
+ const keptRefs = new Set(drafts.flatMap((d) => d.origin ? [d.origin.ref] : []));
86402
+ const dismissed = input.dismissed !== void 0 ? input.dismissed.filter((d) => !keptRefs.has(d.ref)) : void 0;
86403
+ const saved = await this.replaceGuarded(frame, input, drafts, dismissed);
86047
86404
  const errors = saved.filter((a) => a.kind === "model_error").length;
86048
86405
  this.deps.logger.info("retrain annotations saved", {
86049
86406
  tags: { deviceId: input.deviceId },
@@ -86052,12 +86409,99 @@ var RetrainService = class {
86052
86409
  trackId: frame.trackId,
86053
86410
  frameId: input.frameId,
86054
86411
  subjects: saved.length - errors,
86055
- modelErrors: errors
86412
+ modelErrors: errors,
86413
+ dismissed: dismissed?.length
86056
86414
  }
86057
86415
  });
86058
86416
  return saved.map(toWireAnnotation);
86059
86417
  }
86060
86418
  /**
86419
+ * The guarded replace. A caller that names the revision it saw gets exactly
86420
+ * one attempt: a different current revision is a conflict, never a retry —
86421
+ * the point is that it must look again. A caller that names none asked for
86422
+ * last-write-wins, so a race lost between our read and the transaction is
86423
+ * retried on the fresh revision, a bounded number of times.
86424
+ */
86425
+ async replaceGuarded(frame, input, drafts, dismissed) {
86426
+ return await this.underRevision(frame, input, async (revision) => {
86427
+ const result = await this.deps.annotations.replaceForFrame(input.frameId, drafts, {
86428
+ fromRevision: revision,
86429
+ ...dismissed !== void 0 ? { dismissed } : {}
86430
+ });
86431
+ return result.kind === "saved" ? {
86432
+ kind: "done",
86433
+ value: result.rows
86434
+ } : {
86435
+ kind: "conflict",
86436
+ currentRevision: result.currentRevision
86437
+ };
86438
+ });
86439
+ }
86440
+ /**
86441
+ * Run one revision-guarded write. `attempt` is handed the revision to guard
86442
+ * on and reports `done` or `conflict` (the frame moved; `null` = it is gone).
86443
+ */
86444
+ async underRevision(frame, input, attempt) {
86445
+ let revision = frame.revision ?? 0;
86446
+ for (let tries = 0; tries < LAST_WRITE_WINS_ATTEMPTS; tries += 1) {
86447
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== revision) this.refuseStale(input, revision, input.expectedRevision);
86448
+ const result = await attempt(revision);
86449
+ if (result.kind === "done") return result.value;
86450
+ if (result.currentRevision === null) throw new RetrainAnnotationError(`retrain frame ${input.frameId} vanished`);
86451
+ revision = result.currentRevision;
86452
+ if (input.expectedRevision !== void 0) this.refuseStale(input, revision, input.expectedRevision);
86453
+ }
86454
+ throw new RetrainAnnotationError(`retrain frame ${input.frameId} kept changing under the save — try again`);
86455
+ }
86456
+ /**
86457
+ * Give rows saved before origins existed the origin a caller reconstructed
86458
+ * for them — and touch nothing else (ids, creation time and the export stamp
86459
+ * of the row stay). Only rows that have NO origin yet are written; a row that
86460
+ * already has one is skipped, because a snapshot taken at accept time is
86461
+ * evidence and a reconstruction must never overwrite it.
86462
+ */
86463
+ async backfillOrigins(input) {
86464
+ const frame = await this.requireFrame(input, "backfill-origins");
86465
+ const wanted = new Map(input.origins.map((o) => [o.annotationId, o.origin]));
86466
+ const result = await this.underRevision(frame, input, async (revision) => {
86467
+ const outcome = await this.deps.annotations.setOrigins(input.frameId, wanted, { fromRevision: revision });
86468
+ return outcome.kind === "updated" ? {
86469
+ kind: "done",
86470
+ value: outcome
86471
+ } : {
86472
+ kind: "conflict",
86473
+ currentRevision: outcome.currentRevision
86474
+ };
86475
+ });
86476
+ this.deps.logger.info("retrain origins backfilled", {
86477
+ tags: { deviceId: input.deviceId },
86478
+ meta: {
86479
+ frameId: input.frameId,
86480
+ updated: result.updated,
86481
+ skipped: input.origins.length - result.updated
86482
+ }
86483
+ });
86484
+ return {
86485
+ updated: result.updated,
86486
+ skipped: input.origins.length - result.updated,
86487
+ revision: result.revision
86488
+ };
86489
+ }
86490
+ refuseStale(input, current, expected) {
86491
+ this.deps.logger.warn("retrain annotations save refused — the frame moved on", {
86492
+ tags: { deviceId: input.deviceId },
86493
+ meta: {
86494
+ frameId: input.frameId,
86495
+ currentRevision: current,
86496
+ expectedRevision: expected
86497
+ }
86498
+ });
86499
+ throw new RetrainAnnotationError(retrainRevisionConflictMessage(input.frameId, {
86500
+ currentRevision: current,
86501
+ expectedRevision: expected
86502
+ }));
86503
+ }
86504
+ /**
86061
86505
  * Paginated corpus list for the Frames grid. Index reads only — copies,
86062
86506
  * annotation counts/classes, collection membership; no JPEG.
86063
86507
  */
@@ -111256,6 +111700,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
111256
111700
  trainingExportDataPlane = null;
111257
111701
  /** Handle for the retrain-export data-plane listener (dispose on shutdown). */
111258
111702
  retrainExportDataPlane = null;
111703
+ /** Handle for the retrain-frame data-plane listener (dispose on shutdown). */
111704
+ retrainFrameDataPlane = null;
111259
111705
  /** Handle for the debug-export data-plane listener (dispose on shutdown). */
111260
111706
  debugExportDataPlane = null;
111261
111707
  /** Handle for the tape-export data-plane listener (dispose on shutdown). */
@@ -113353,7 +113799,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
113353
113799
  }),
113354
113800
  subjectBoxes: (trackId, deviceId) => mediaStore.listSubjectBoxesByOwner("track", trackId, deviceId)
113355
113801
  },
113356
- memberships: retrainCollectionStore
113802
+ memberships: retrainCollectionStore,
113803
+ frameUrl: (frameId, size) => retrainFramePath$1(this.ctx.id, frameId, size)
113357
113804
  });
113358
113805
  this.retrain = retrain;
113359
113806
  const retrainStills = new RecordedStillService(buildRecordedStillPorts({
@@ -114457,6 +114904,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114457
114904
  await this.serveNcArtifactPlane();
114458
114905
  await this.serveTrainingExportPlane();
114459
114906
  await this.serveRetrainExportPlane();
114907
+ await this.serveRetrainFramePlane();
114460
114908
  await this.serveDebugExportPlane();
114461
114909
  await this.serveTapeExportPlane();
114462
114910
  this.ctx.logger.info("event-media data-plane served", { meta: {
@@ -114608,6 +115056,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
114608
115056
  async saveRetrainAnnotations(input) {
114609
115057
  return this.requireRetrain().saveAnnotations(input);
114610
115058
  }
115059
+ async backfillRetrainOrigins(input) {
115060
+ return this.requireRetrain().backfillOrigins(input);
115061
+ }
114611
115062
  async completeRetrainTrack(input) {
114612
115063
  return this.requireRetrain().complete(input);
114613
115064
  }
@@ -115088,6 +115539,31 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115088
115539
  }
115089
115540
  }
115090
115541
  /**
115542
+ * The retrain-frame data plane: one copied frame's JPEG (`?size=thumb|full`).
115543
+ * Admin-only like the cap that used to serve it as base64 (D613).
115544
+ */
115545
+ async serveRetrainFramePlane() {
115546
+ try {
115547
+ const handler = createRetrainFrameHandler({
115548
+ readFrame: async (frameId) => {
115549
+ const frames = this.retrainFrames;
115550
+ if (frames === null) return null;
115551
+ const frame = await frames.get(frameId);
115552
+ return frame === null ? null : frames.readFrame(frame);
115553
+ },
115554
+ resize: resizeRetrainJpeg,
115555
+ logger: this.ctx.logger
115556
+ });
115557
+ this.retrainFrameDataPlane = await this.ctx.dataPlane?.serve({
115558
+ prefix: RETRAIN_FRAME_PREFIX,
115559
+ access: planeAccess("retrain-frame"),
115560
+ handler
115561
+ }) ?? null;
115562
+ } catch (err) {
115563
+ this.ctx.logger.warn("retrain-frame data-plane failed to serve", { meta: { error: errMsg(err) } });
115564
+ }
115565
+ }
115566
+ /**
115091
115567
  * Plan the annotated export — INDEX READS ONLY, no blob touched.
115092
115568
  *
115093
115569
  * The zone used for the `zoneCrop` derivation is the bounding box of the
@@ -115869,6 +116345,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
115869
116345
  this.trainingExportDataPlane = null;
115870
116346
  await this.retrainExportDataPlane?.dispose();
115871
116347
  this.retrainExportDataPlane = null;
116348
+ await this.retrainFrameDataPlane?.dispose();
116349
+ this.retrainFrameDataPlane = null;
115872
116350
  await this.debugExportDataPlane?.dispose();
115873
116351
  this.debugExportDataPlane = null;
115874
116352
  await this.tapeExportDataPlane?.dispose();