@camstack/addon-post-analysis 1.2.173 → 1.2.175

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.
@@ -13146,7 +13146,26 @@ var DiscoveryCandidateSchema = object({
13146
13146
  * identity ahead of adoption. Rendering metadata (unit, precision)
13147
13147
  * flows live through the cap STATUS SLICE after adoption.
13148
13148
  */
13149
- sourceInfo: SourceInfoSchema.optional()
13149
+ sourceInfo: SourceInfoSchema.optional(),
13150
+ /**
13151
+ * Set when this candidate is a device the provider ALREADY owns.
13152
+ *
13153
+ * A scan cannot generally produce the identity a device was onboarded under
13154
+ * (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
13155
+ * comparison never matches and an owned device looks addable. Re-adopting one
13156
+ * overwrites its config with scan-derived values — that is how a Home Hub's
13157
+ * Baichuan port was overwritten with its ONVIF port, taking the hub and its
13158
+ * three child cameras offline for four hours.
13159
+ *
13160
+ * A provider that can recognise its own devices says so here. Absent means
13161
+ * "not recognised", which is not the same as "known to be new" — a provider
13162
+ * that cannot tell simply never sets it.
13163
+ */
13164
+ alreadyOnboarded: boolean().optional(),
13165
+ /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
13166
+ onboardedDeviceId: number().optional(),
13167
+ /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
13168
+ onboardedName: string().optional()
13150
13169
  });
13151
13170
  /**
13152
13171
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -13177,7 +13177,26 @@ var DiscoveryCandidateSchema = object({
13177
13177
  * identity ahead of adoption. Rendering metadata (unit, precision)
13178
13178
  * flows live through the cap STATUS SLICE after adoption.
13179
13179
  */
13180
- sourceInfo: SourceInfoSchema.optional()
13180
+ sourceInfo: SourceInfoSchema.optional(),
13181
+ /**
13182
+ * Set when this candidate is a device the provider ALREADY owns.
13183
+ *
13184
+ * A scan cannot generally produce the identity a device was onboarded under
13185
+ * (Reolink keys on `mac-<mac>`, learned at adopt time), so a stableId
13186
+ * comparison never matches and an owned device looks addable. Re-adopting one
13187
+ * overwrites its config with scan-derived values — that is how a Home Hub's
13188
+ * Baichuan port was overwritten with its ONVIF port, taking the hub and its
13189
+ * three child cameras offline for four hours.
13190
+ *
13191
+ * A provider that can recognise its own devices says so here. Absent means
13192
+ * "not recognised", which is not the same as "known to be new" — a provider
13193
+ * that cannot tell simply never sets it.
13194
+ */
13195
+ alreadyOnboarded: boolean().optional(),
13196
+ /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
13197
+ onboardedDeviceId: number().optional(),
13198
+ /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
13199
+ onboardedName: string().optional()
13181
13200
  });
13182
13201
  /**
13183
13202
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-LIFo4F1P.js");
5
+ const require_dist = require("../dist-BfdKgU7-.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-BXc3lu5t.mjs";
1
+ import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-B1cFmGC0.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.63",
6
+ version: "1.2.64",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.143",
21
+ version: "1.2.144",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.109",
36
+ version: "1.2.110",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.143",
39
+ version: "1.2.144",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.63",
48
+ version: "1.2.64",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.109",
84
+ version: "1.2.110",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-LIFo4F1P.js");
5
+ const require_dist = require("../dist-BfdKgU7-.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -45339,6 +45339,48 @@ async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
45339
45339
  return true;
45340
45340
  }
45341
45341
  //#endregion
45342
+ //#region src/pipeline-analytics/detail-refusal.ts
45343
+ /**
45344
+ * Recognises the runner's TYPED `not-attached` refusal from a
45345
+ * `pipelineRunner.runDetailSubtree` call.
45346
+ *
45347
+ * The refusal crosses the wire as an ERROR whose message carries a marker,
45348
+ * not as a result field: the cap output schema
45349
+ * (`@camstack/types` `pipeline-runner.cap.ts` → `z.object({ details
45350
+ * }).nullable()`) is parsed on the hub route and Zod strips unknown keys, so a
45351
+ * refusal field cannot survive without a types + codegen + server-train
45352
+ * change. Error messages DO survive the route (tRPC carries `error.message`;
45353
+ * see `cap-route-error-formatter.ts`).
45354
+ *
45355
+ * The marker constant is a TWIN of
45356
+ * `addon-pipeline/src/pipeline-runner/detail-park.ts`
45357
+ * `DETAIL_NOT_ATTACHED_MARKER` — addons never import each other (`ctx.api` is
45358
+ * the only cross-addon layer), so the value is duplicated here the same way
45359
+ * `composeDetailSteps` mirrors the pipeline catalog's chain. Keep the two
45360
+ * identical.
45361
+ *
45362
+ * Why the dispatcher cares: this refusal is about the camera's SESSION state
45363
+ * (the on-motion session closed and the runner's detail park grace has
45364
+ * lapsed), not about one frame — so the full-frame and crop fallback rungs are
45365
+ * structurally doomed and must be skipped, feeding the scheduler's bounded
45366
+ * give-up directly.
45367
+ */
45368
+ var DETAIL_NOT_ATTACHED_MARKER = "runDetailSubtree-refused:not-attached";
45369
+ /** Best-effort message extraction across transport error shapes. */
45370
+ function messageOf(err) {
45371
+ if (err instanceof Error) return err.message;
45372
+ if (typeof err === "string") return err;
45373
+ if (typeof err === "object" && err !== null) {
45374
+ const message = Reflect.get(err, "message");
45375
+ if (typeof message === "string") return message;
45376
+ }
45377
+ return "";
45378
+ }
45379
+ /** Did this `runDetailSubtree` error carry the runner's not-attached marker? */
45380
+ function isDetailNotAttachedRefusal(err) {
45381
+ return messageOf(err).includes(DETAIL_NOT_ATTACHED_MARKER);
45382
+ }
45383
+ //#endregion
45342
45384
  //#region src/pipeline-analytics/detail-scheduler.ts
45343
45385
  /** Default backoff/period when a step's cadence omits `minIntervalMs`. */
45344
45386
  var DEFAULT_MIN_INTERVAL_MS = 1e3;
@@ -45719,6 +45761,14 @@ var TrackDetailDispatcher = class {
45719
45761
  * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
45720
45762
  * demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
45721
45763
  * rung 1 missed — never a frame-rate stream, and never raw pixels.
45764
+ *
45765
+ * ONE outcome stops the ladder cold: the runner's typed `not-attached`
45766
+ * refusal (`detail-refusal.ts`). It names the camera's SESSION state, not
45767
+ * this frame — the session closed and the runner's detail park grace has
45768
+ * lapsed — so the two remaining rungs are structurally doomed (the retained
45769
+ * store is purged with the session; `captureCrop` is native-strict against
45770
+ * the same missing lease). Walking them anyway is what burned two extra
45771
+ * cross-node RPCs per refusal, ~600–900 times an hour fleet-wide.
45722
45772
  */
45723
45773
  async runOnce(deviceId, dev, req, frame) {
45724
45774
  const parent = {
@@ -45735,6 +45785,10 @@ var TrackDetailDispatcher = class {
45735
45785
  }, frame.nodeId);
45736
45786
  if (primary !== null) return primary.details;
45737
45787
  } catch (err) {
45788
+ if (isDetailNotAttachedRefusal(err)) {
45789
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45790
+ return null;
45791
+ }
45738
45792
  this.deps.logger.debug("detail primary call failed — trying frame fallback", {
45739
45793
  tags: { deviceId },
45740
45794
  meta: {
@@ -45767,6 +45821,10 @@ var TrackDetailDispatcher = class {
45767
45821
  if (derived !== null) return derived.details;
45768
45822
  }
45769
45823
  } catch (err) {
45824
+ if (isDetailNotAttachedRefusal(err)) {
45825
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45826
+ return null;
45827
+ }
45770
45828
  this.deps.logger.debug("detail full-frame fallback call failed", {
45771
45829
  tags: { deviceId },
45772
45830
  meta: {
@@ -45788,6 +45846,10 @@ var TrackDetailDispatcher = class {
45788
45846
  if (retry !== null) return retry.details;
45789
45847
  }
45790
45848
  } catch (err) {
45849
+ if (isDetailNotAttachedRefusal(err)) {
45850
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45851
+ return null;
45852
+ }
45791
45853
  this.deps.logger.debug("detail crop-fallback call failed", {
45792
45854
  tags: { deviceId },
45793
45855
  meta: {
@@ -45800,6 +45862,26 @@ var TrackDetailDispatcher = class {
45800
45862
  this.warnFailThrottled(deviceId, dev, req);
45801
45863
  return null;
45802
45864
  }
45865
+ /**
45866
+ * The typed refusal is EXPECTED for a track that outlives the runner's
45867
+ * detail park grace (a closing track's last dispatches), so it logs its own
45868
+ * line — the reason is knowable, unlike the every-rung-missed warn — and
45869
+ * shares the per-device throttle. Never silent: this branch drops the
45870
+ * track's enrichment for good once the give-up bound is reached.
45871
+ */
45872
+ warnNotAttachedThrottled(deviceId, dev, req) {
45873
+ const now = Date.now();
45874
+ if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
45875
+ dev.lastFailWarnAt = now;
45876
+ this.deps.logger.warn("detail dispatch refused: camera not attached (park grace lapsed) — skipping fallback rungs", {
45877
+ tags: { deviceId },
45878
+ meta: {
45879
+ trackId: req.trackId,
45880
+ stepId: req.stepId,
45881
+ reason: req.reason
45882
+ }
45883
+ });
45884
+ }
45803
45885
  warnFailThrottled(deviceId, dev, req) {
45804
45886
  const now = Date.now();
45805
45887
  if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-BXc3lu5t.mjs";
1
+ import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as boolean, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as isDeviceScopedCap, G as TimelapseRulePatchSchema, Gt as partialRecord, H as SceneMonitorSchema, Ht as literal, I as RECORDING_EXPORT_MAX_READ_BYTES, It as nodePin, J as VISIT_MERGE_GAP_MS, Jt as unknown, K as TimelapseRuleSchema, Kt as record, L as RetrainStatusSchema, Lt as sleep$1, M as NcSystemEventKindSchema, Mt as DeviceType, N as NcTaxonomySchema, Nt as createEvent, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as hydrateSchema, Q as audioMetricsCapability, Rt as _enum, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as number, V as SCENE_DIVERGED, Vt as discriminatedUnion, W as TimelapseRuleInputSchema, Wt as object, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Yt as EventCategory, Z as assertTimelapseCadences, _ as MediaFileKindEnum, _t as pickClusterStepModels, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, c as DETECTION_MACRO_CLASSES, ct as faceGalleryCapability, d as EVENT_KIND_BY_CAP, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isScheduleActive, g as MACRO_LABELS, h as LabelAttributionSchema, ht as notificationRulesCapability, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as CamProfileSchema, k as NcSnoozeInputSchema, kt as errMsg, lt as failureContributionCapability, m as FailureCounters, mt as kebabToCamel, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isSourceCap, q as TrackSourceSchema, qt as string, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as array } from "../dist-B1cFmGC0.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -45265,6 +45265,48 @@ async function copyFaceCropIfTrackHasNoThumbnail(store, input) {
45265
45265
  return true;
45266
45266
  }
45267
45267
  //#endregion
45268
+ //#region src/pipeline-analytics/detail-refusal.ts
45269
+ /**
45270
+ * Recognises the runner's TYPED `not-attached` refusal from a
45271
+ * `pipelineRunner.runDetailSubtree` call.
45272
+ *
45273
+ * The refusal crosses the wire as an ERROR whose message carries a marker,
45274
+ * not as a result field: the cap output schema
45275
+ * (`@camstack/types` `pipeline-runner.cap.ts` → `z.object({ details
45276
+ * }).nullable()`) is parsed on the hub route and Zod strips unknown keys, so a
45277
+ * refusal field cannot survive without a types + codegen + server-train
45278
+ * change. Error messages DO survive the route (tRPC carries `error.message`;
45279
+ * see `cap-route-error-formatter.ts`).
45280
+ *
45281
+ * The marker constant is a TWIN of
45282
+ * `addon-pipeline/src/pipeline-runner/detail-park.ts`
45283
+ * `DETAIL_NOT_ATTACHED_MARKER` — addons never import each other (`ctx.api` is
45284
+ * the only cross-addon layer), so the value is duplicated here the same way
45285
+ * `composeDetailSteps` mirrors the pipeline catalog's chain. Keep the two
45286
+ * identical.
45287
+ *
45288
+ * Why the dispatcher cares: this refusal is about the camera's SESSION state
45289
+ * (the on-motion session closed and the runner's detail park grace has
45290
+ * lapsed), not about one frame — so the full-frame and crop fallback rungs are
45291
+ * structurally doomed and must be skipped, feeding the scheduler's bounded
45292
+ * give-up directly.
45293
+ */
45294
+ var DETAIL_NOT_ATTACHED_MARKER = "runDetailSubtree-refused:not-attached";
45295
+ /** Best-effort message extraction across transport error shapes. */
45296
+ function messageOf(err) {
45297
+ if (err instanceof Error) return err.message;
45298
+ if (typeof err === "string") return err;
45299
+ if (typeof err === "object" && err !== null) {
45300
+ const message = Reflect.get(err, "message");
45301
+ if (typeof message === "string") return message;
45302
+ }
45303
+ return "";
45304
+ }
45305
+ /** Did this `runDetailSubtree` error carry the runner's not-attached marker? */
45306
+ function isDetailNotAttachedRefusal(err) {
45307
+ return messageOf(err).includes(DETAIL_NOT_ATTACHED_MARKER);
45308
+ }
45309
+ //#endregion
45268
45310
  //#region src/pipeline-analytics/detail-scheduler.ts
45269
45311
  /** Default backoff/period when a step's cadence omits `minIntervalMs`. */
45270
45312
  var DEFAULT_MIN_INTERVAL_MS = 1e3;
@@ -45645,6 +45687,14 @@ var TrackDetailDispatcher = class {
45645
45687
  * On D9/D18: the payload that crosses in rung 2 is a COMPRESSED JPEG fetched on
45646
45688
  * demand, per TRACK per dispatch (≤2 in flight per device, ~1 Hz) and only after
45647
45689
  * rung 1 missed — never a frame-rate stream, and never raw pixels.
45690
+ *
45691
+ * ONE outcome stops the ladder cold: the runner's typed `not-attached`
45692
+ * refusal (`detail-refusal.ts`). It names the camera's SESSION state, not
45693
+ * this frame — the session closed and the runner's detail park grace has
45694
+ * lapsed — so the two remaining rungs are structurally doomed (the retained
45695
+ * store is purged with the session; `captureCrop` is native-strict against
45696
+ * the same missing lease). Walking them anyway is what burned two extra
45697
+ * cross-node RPCs per refusal, ~600–900 times an hour fleet-wide.
45648
45698
  */
45649
45699
  async runOnce(deviceId, dev, req, frame) {
45650
45700
  const parent = {
@@ -45661,6 +45711,10 @@ var TrackDetailDispatcher = class {
45661
45711
  }, frame.nodeId);
45662
45712
  if (primary !== null) return primary.details;
45663
45713
  } catch (err) {
45714
+ if (isDetailNotAttachedRefusal(err)) {
45715
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45716
+ return null;
45717
+ }
45664
45718
  this.deps.logger.debug("detail primary call failed — trying frame fallback", {
45665
45719
  tags: { deviceId },
45666
45720
  meta: {
@@ -45693,6 +45747,10 @@ var TrackDetailDispatcher = class {
45693
45747
  if (derived !== null) return derived.details;
45694
45748
  }
45695
45749
  } catch (err) {
45750
+ if (isDetailNotAttachedRefusal(err)) {
45751
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45752
+ return null;
45753
+ }
45696
45754
  this.deps.logger.debug("detail full-frame fallback call failed", {
45697
45755
  tags: { deviceId },
45698
45756
  meta: {
@@ -45714,6 +45772,10 @@ var TrackDetailDispatcher = class {
45714
45772
  if (retry !== null) return retry.details;
45715
45773
  }
45716
45774
  } catch (err) {
45775
+ if (isDetailNotAttachedRefusal(err)) {
45776
+ this.warnNotAttachedThrottled(deviceId, dev, req);
45777
+ return null;
45778
+ }
45717
45779
  this.deps.logger.debug("detail crop-fallback call failed", {
45718
45780
  tags: { deviceId },
45719
45781
  meta: {
@@ -45726,6 +45788,26 @@ var TrackDetailDispatcher = class {
45726
45788
  this.warnFailThrottled(deviceId, dev, req);
45727
45789
  return null;
45728
45790
  }
45791
+ /**
45792
+ * The typed refusal is EXPECTED for a track that outlives the runner's
45793
+ * detail park grace (a closing track's last dispatches), so it logs its own
45794
+ * line — the reason is knowable, unlike the every-rung-missed warn — and
45795
+ * shares the per-device throttle. Never silent: this branch drops the
45796
+ * track's enrichment for good once the give-up bound is reached.
45797
+ */
45798
+ warnNotAttachedThrottled(deviceId, dev, req) {
45799
+ const now = Date.now();
45800
+ if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
45801
+ dev.lastFailWarnAt = now;
45802
+ this.deps.logger.warn("detail dispatch refused: camera not attached (park grace lapsed) — skipping fallback rungs", {
45803
+ tags: { deviceId },
45804
+ meta: {
45805
+ trackId: req.trackId,
45806
+ stepId: req.stepId,
45807
+ reason: req.reason
45808
+ }
45809
+ });
45810
+ }
45729
45811
  warnFailThrottled(deviceId, dev, req) {
45730
45812
  const now = Date.now();
45731
45813
  if (now - dev.lastFailWarnAt < FAIL_WARN_THROTTLE_MS) return;
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C2HBf1pQ.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CSd331cv.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.173",
3
+ "version": "1.2.175",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",