@camstack/addon-pipeline 1.2.309 → 1.2.311

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.
@@ -3885,6 +3885,24 @@ function isParkedDetailFresh(parked, nowMs) {
3885
3885
  function buildDetailNotAttachedError(deviceId) {
3886
3886
  return /* @__PURE__ */ new Error(`${DETAIL_NOT_ATTACHED_MARKER} — camera ${deviceId} is not attached and no detail state is parked within the grace window`);
3887
3887
  }
3888
+ /**
3889
+ * The wire marker for the typed handle-expired refusal (D688): the frame handle
3890
+ * a detail dispatch named is past the native lease age bound, so every later
3891
+ * ask for it is refused too. Twin constant in
3892
+ * `addon-post-analysis/src/pipeline-analytics/detail-refusal.ts` — keep in sync.
3893
+ * An error rather than a result field for the same reason as
3894
+ * {@link DETAIL_NOT_ATTACHED_MARKER}: the cap output schema strips unknown keys.
3895
+ */
3896
+ var DETAIL_HANDLE_EXPIRED_MARKER = "runDetailSubtree-refused:handle-expired";
3897
+ /**
3898
+ * The error the cap method throws when the dispatch's only pixel source was a
3899
+ * frame handle the lease registry refused as expired. The dispatcher recognises
3900
+ * the marker, retires the handle for that track and falls back to its parked
3901
+ * frame — it never names the same handle again.
3902
+ */
3903
+ function buildDetailHandleExpiredError(deviceId, handleAgeMs) {
3904
+ return /* @__PURE__ */ new Error(`${DETAIL_HANDLE_EXPIRED_MARKER} — camera ${deviceId}: the frame handle is past the native lease age bound (age ${handleAgeMs ?? "unknown"} ms)`);
3905
+ }
3888
3906
  //#endregion
3889
3907
  //#region src/pipeline-runner/face-parent-geometry.ts
3890
3908
  /** Is the centre of `face` inside `parent` (edges inclusive)? */
@@ -3970,7 +3988,11 @@ var FaceOutsideParentLog = class {
3970
3988
  //#region src/pipeline-runner/detail-subtree.ts
3971
3989
  /** Did the subtree run refuse because the camera is not attached (park lapsed)? */
3972
3990
  function isDetailNotAttachedRefusal(outcome) {
3973
- return "refusal" in outcome;
3991
+ return "refusal" in outcome && outcome.refusal === "not-attached";
3992
+ }
3993
+ /** Did the subtree run refuse because the named frame handle is past its lease age bound? */
3994
+ function isDetailHandleExpiredRefusal(outcome) {
3995
+ return "refusal" in outcome && outcome.refusal === "handle-expired";
3974
3996
  }
3975
3997
  /** Did the native-lease fetch report a miss (rather than return pixels)? */
3976
3998
  function isNativeCropMiss(fetch) {
@@ -4272,6 +4294,10 @@ async function runDetailSubtree(deps, input) {
4272
4294
  nativeWorkerReason: nativeMiss?.workerReason ?? null
4273
4295
  }
4274
4296
  });
4297
+ if (nativeMiss?.miss === "handle-expired") return {
4298
+ refusal: "handle-expired",
4299
+ handleAgeMs: nativeMiss.handleAgeMs
4300
+ };
4275
4301
  return null;
4276
4302
  }
4277
4303
  const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
@@ -5298,6 +5324,19 @@ async function resolveMotionFrameView(registry, ref) {
5298
5324
  /** The step id (`PipelineStepInput.addonId`) the governor gates. */
5299
5325
  var PACKAGE_DETECTION_STEP_ID = "package-detection";
5300
5326
  /**
5327
+ * The executor's frame, stamped `packageLooked` when the `package-detection`
5328
+ * root was among the steps actually DISPATCHED for it (the governor let it
5329
+ * through) and the frame was not shed. Post-analysis reads it as "an empty
5330
+ * doormat on this frame is an ABSENCE, not silence" (D687). Never mutates.
5331
+ */
5332
+ function withPackageLook(result, dispatched) {
5333
+ if (result.shed === true) return result;
5334
+ return dispatched.some((s) => s.addonId === "package-detection" && s.enabled !== false) ? {
5335
+ ...result,
5336
+ packageLooked: true
5337
+ } : result;
5338
+ }
5339
+ /**
5301
5340
  * Burst: 5 runs, 5s apart — 0s, 5s, 10s, 15s, 20s after the settle edge.
5302
5341
  *
5303
5342
  * Sized as a SAFETY NET when the operator raises `packageDropDwellSec` above
@@ -10058,6 +10097,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10058
10097
  recordFaceOutsideParent: (drop) => this.faceOutsideParentLog.record(drop)
10059
10098
  }, input);
10060
10099
  if (outcome !== null && isDetailNotAttachedRefusal(outcome)) throw buildDetailNotAttachedError(input.deviceId);
10100
+ if (outcome !== null && isDetailHandleExpiredRefusal(outcome)) throw buildDetailHandleExpiredError(input.deviceId, outcome.handleAgeMs);
10061
10101
  return outcome;
10062
10102
  }
10063
10103
  /**
@@ -11385,7 +11425,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
11385
11425
  return null;
11386
11426
  }
11387
11427
  if (result === null) return null;
11388
- return this.recoverSmallSubjects(deviceId, frame, result, dispatchSteps, camConfig?.deviceKey);
11428
+ const looked = withPackageLook(result, dispatchSteps);
11429
+ return this.recoverSmallSubjects(deviceId, frame, looked, dispatchSteps, camConfig?.deviceKey);
11389
11430
  }
11390
11431
  /**
11391
11432
  * The second detector pass at native resolution — `small-subject-recovery.ts`
@@ -3877,6 +3877,24 @@ function isParkedDetailFresh(parked, nowMs) {
3877
3877
  function buildDetailNotAttachedError(deviceId) {
3878
3878
  return /* @__PURE__ */ new Error(`${DETAIL_NOT_ATTACHED_MARKER} — camera ${deviceId} is not attached and no detail state is parked within the grace window`);
3879
3879
  }
3880
+ /**
3881
+ * The wire marker for the typed handle-expired refusal (D688): the frame handle
3882
+ * a detail dispatch named is past the native lease age bound, so every later
3883
+ * ask for it is refused too. Twin constant in
3884
+ * `addon-post-analysis/src/pipeline-analytics/detail-refusal.ts` — keep in sync.
3885
+ * An error rather than a result field for the same reason as
3886
+ * {@link DETAIL_NOT_ATTACHED_MARKER}: the cap output schema strips unknown keys.
3887
+ */
3888
+ var DETAIL_HANDLE_EXPIRED_MARKER = "runDetailSubtree-refused:handle-expired";
3889
+ /**
3890
+ * The error the cap method throws when the dispatch's only pixel source was a
3891
+ * frame handle the lease registry refused as expired. The dispatcher recognises
3892
+ * the marker, retires the handle for that track and falls back to its parked
3893
+ * frame — it never names the same handle again.
3894
+ */
3895
+ function buildDetailHandleExpiredError(deviceId, handleAgeMs) {
3896
+ return /* @__PURE__ */ new Error(`${DETAIL_HANDLE_EXPIRED_MARKER} — camera ${deviceId}: the frame handle is past the native lease age bound (age ${handleAgeMs ?? "unknown"} ms)`);
3897
+ }
3880
3898
  //#endregion
3881
3899
  //#region src/pipeline-runner/face-parent-geometry.ts
3882
3900
  /** Is the centre of `face` inside `parent` (edges inclusive)? */
@@ -3962,7 +3980,11 @@ var FaceOutsideParentLog = class {
3962
3980
  //#region src/pipeline-runner/detail-subtree.ts
3963
3981
  /** Did the subtree run refuse because the camera is not attached (park lapsed)? */
3964
3982
  function isDetailNotAttachedRefusal(outcome) {
3965
- return "refusal" in outcome;
3983
+ return "refusal" in outcome && outcome.refusal === "not-attached";
3984
+ }
3985
+ /** Did the subtree run refuse because the named frame handle is past its lease age bound? */
3986
+ function isDetailHandleExpiredRefusal(outcome) {
3987
+ return "refusal" in outcome && outcome.refusal === "handle-expired";
3966
3988
  }
3967
3989
  /** Did the native-lease fetch report a miss (rather than return pixels)? */
3968
3990
  function isNativeCropMiss(fetch) {
@@ -4264,6 +4286,10 @@ async function runDetailSubtree(deps, input) {
4264
4286
  nativeWorkerReason: nativeMiss?.workerReason ?? null
4265
4287
  }
4266
4288
  });
4289
+ if (nativeMiss?.miss === "handle-expired") return {
4290
+ refusal: "handle-expired",
4291
+ handleAgeMs: nativeMiss.handleAgeMs
4292
+ };
4267
4293
  return null;
4268
4294
  }
4269
4295
  const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
@@ -5290,6 +5316,19 @@ async function resolveMotionFrameView(registry, ref) {
5290
5316
  /** The step id (`PipelineStepInput.addonId`) the governor gates. */
5291
5317
  var PACKAGE_DETECTION_STEP_ID = "package-detection";
5292
5318
  /**
5319
+ * The executor's frame, stamped `packageLooked` when the `package-detection`
5320
+ * root was among the steps actually DISPATCHED for it (the governor let it
5321
+ * through) and the frame was not shed. Post-analysis reads it as "an empty
5322
+ * doormat on this frame is an ABSENCE, not silence" (D687). Never mutates.
5323
+ */
5324
+ function withPackageLook(result, dispatched) {
5325
+ if (result.shed === true) return result;
5326
+ return dispatched.some((s) => s.addonId === "package-detection" && s.enabled !== false) ? {
5327
+ ...result,
5328
+ packageLooked: true
5329
+ } : result;
5330
+ }
5331
+ /**
5293
5332
  * Burst: 5 runs, 5s apart — 0s, 5s, 10s, 15s, 20s after the settle edge.
5294
5333
  *
5295
5334
  * Sized as a SAFETY NET when the operator raises `packageDropDwellSec` above
@@ -10050,6 +10089,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10050
10089
  recordFaceOutsideParent: (drop) => this.faceOutsideParentLog.record(drop)
10051
10090
  }, input);
10052
10091
  if (outcome !== null && isDetailNotAttachedRefusal(outcome)) throw buildDetailNotAttachedError(input.deviceId);
10092
+ if (outcome !== null && isDetailHandleExpiredRefusal(outcome)) throw buildDetailHandleExpiredError(input.deviceId, outcome.handleAgeMs);
10053
10093
  return outcome;
10054
10094
  }
10055
10095
  /**
@@ -11377,7 +11417,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
11377
11417
  return null;
11378
11418
  }
11379
11419
  if (result === null) return null;
11380
- return this.recoverSmallSubjects(deviceId, frame, result, dispatchSteps, camConfig?.deviceKey);
11420
+ const looked = withPackageLook(result, dispatchSteps);
11421
+ return this.recoverSmallSubjects(deviceId, frame, looked, dispatchSteps, camConfig?.deviceKey);
11381
11422
  }
11382
11423
  /**
11383
11424
  * The second detector pass at native resolution — `small-subject-recovery.ts`
@@ -6777,6 +6777,30 @@ async function patchExport(state, id, patch) {
6777
6777
  return merged;
6778
6778
  });
6779
6779
  }
6780
+ /**
6781
+ * Patch a row UNLESS its current state says not to — the check and the write
6782
+ * are ONE serialized step, so a delete that lands between a separate read and
6783
+ * a separate patch can no longer be overwritten (review R5).
6784
+ */
6785
+ async function patchExportUnless(state, id, patch, refuse) {
6786
+ return serialize(state, id, async () => {
6787
+ const existing = await state.read(id);
6788
+ if (existing === null) return { kind: "missing" };
6789
+ if (refuse(existing)) return {
6790
+ kind: "refused",
6791
+ record: existing
6792
+ };
6793
+ const merged = require_dist.ExportRecordSchema.parse({
6794
+ ...existing,
6795
+ ...patch
6796
+ });
6797
+ await state.write(id, merged);
6798
+ return {
6799
+ kind: "patched",
6800
+ record: merged
6801
+ };
6802
+ });
6803
+ }
6780
6804
  /** Read one export row by id — ONE keyed row read. */
6781
6805
  async function getExportRecord(state, id) {
6782
6806
  return state.read(id);
@@ -6996,6 +7020,13 @@ function expectedOutSeconds(rec) {
6996
7020
  }
6997
7021
  return rangeSec / (rec.options.speed ?? 1);
6998
7022
  }
7023
+ /**
7024
+ * A row nobody will read: its caller deleted it, it expired, or it already
7025
+ * failed (a cancel finalises as `failed`). The engine renders none of these.
7026
+ */
7027
+ function isAbandonedExportState(state) {
7028
+ return state === "deleted" || state === "expired" || state === "failed";
7029
+ }
6999
7030
  var ExportEngine = class {
7000
7031
  deps;
7001
7032
  queue = [];
@@ -7056,16 +7087,37 @@ var ExportEngine = class {
7056
7087
  async render(id) {
7057
7088
  const existing = await getExportRecord(this.deps.state, id);
7058
7089
  if (!existing) return;
7090
+ if (isAbandonedExportState(existing.state)) {
7091
+ this.deps.logger.info("export render skipped: the export was abandoned before it ran", {
7092
+ tags: { deviceId: existing.deviceId },
7093
+ meta: {
7094
+ exportId: id,
7095
+ state: existing.state
7096
+ }
7097
+ });
7098
+ return;
7099
+ }
7059
7100
  const subject = require_dist.exportSubjectOf(existing);
7060
7101
  if (subject.kind === "clip" && subject.source !== ANALYTICS_CLIP_SOURCE) {
7061
7102
  await this.fetchClip(id, existing, subject);
7062
7103
  return;
7063
7104
  }
7064
- const rec = await patchExport(this.deps.state, id, {
7105
+ const claimed = await patchExportUnless(this.deps.state, id, {
7065
7106
  state: "rendering",
7066
7107
  progressPct: 0
7067
- });
7068
- if (!rec) return;
7108
+ }, (current) => isAbandonedExportState(current.state));
7109
+ if (claimed.kind === "missing") return;
7110
+ if (claimed.kind === "refused") {
7111
+ this.deps.logger.info("export render skipped: the export was abandoned before it ran", {
7112
+ tags: { deviceId: claimed.record.deviceId },
7113
+ meta: {
7114
+ exportId: id,
7115
+ state: claimed.record.state
7116
+ }
7117
+ });
7118
+ return;
7119
+ }
7120
+ const rec = claimed.record;
7069
7121
  const playlist = await this.deps.resolvePlaylist(rec);
7070
7122
  if (!playlist) {
7071
7123
  await this.fail(id, "no footage for range");
@@ -7699,6 +7751,19 @@ var ExportEngine = class {
7699
7751
  await this.fail(id, `render produced no video frames (${bytes} bytes, < ${EXPORT_MIN_PLAUSIBLE_BYTES})`);
7700
7752
  return;
7701
7753
  }
7754
+ const current = await getExportRecord(this.deps.state, id);
7755
+ if (current !== null && isAbandonedExportState(current.state)) {
7756
+ await this.deps.removeFile(outPath);
7757
+ this.deps.logger.info("export render discarded: the export was abandoned while it ran", {
7758
+ tags: { deviceId: current.deviceId },
7759
+ meta: {
7760
+ exportId: id,
7761
+ state: current.state,
7762
+ bytes
7763
+ }
7764
+ });
7765
+ return;
7766
+ }
7702
7767
  await patchExport(this.deps.state, id, {
7703
7768
  state: "ready",
7704
7769
  progressPct: 100,
@@ -7726,6 +7791,8 @@ var ExportEngine = class {
7726
7791
  exportId: id,
7727
7792
  error
7728
7793
  } });
7794
+ const current = await getExportRecord(this.deps.state, id);
7795
+ if (current !== null && current.state === "deleted") return;
7729
7796
  await patchExport(this.deps.state, id, {
7730
7797
  state: "failed",
7731
7798
  progressPct: null,
@@ -6769,6 +6769,30 @@ async function patchExport(state, id, patch) {
6769
6769
  return merged;
6770
6770
  });
6771
6771
  }
6772
+ /**
6773
+ * Patch a row UNLESS its current state says not to — the check and the write
6774
+ * are ONE serialized step, so a delete that lands between a separate read and
6775
+ * a separate patch can no longer be overwritten (review R5).
6776
+ */
6777
+ async function patchExportUnless(state, id, patch, refuse) {
6778
+ return serialize(state, id, async () => {
6779
+ const existing = await state.read(id);
6780
+ if (existing === null) return { kind: "missing" };
6781
+ if (refuse(existing)) return {
6782
+ kind: "refused",
6783
+ record: existing
6784
+ };
6785
+ const merged = ExportRecordSchema.parse({
6786
+ ...existing,
6787
+ ...patch
6788
+ });
6789
+ await state.write(id, merged);
6790
+ return {
6791
+ kind: "patched",
6792
+ record: merged
6793
+ };
6794
+ });
6795
+ }
6772
6796
  /** Read one export row by id — ONE keyed row read. */
6773
6797
  async function getExportRecord(state, id) {
6774
6798
  return state.read(id);
@@ -6988,6 +7012,13 @@ function expectedOutSeconds(rec) {
6988
7012
  }
6989
7013
  return rangeSec / (rec.options.speed ?? 1);
6990
7014
  }
7015
+ /**
7016
+ * A row nobody will read: its caller deleted it, it expired, or it already
7017
+ * failed (a cancel finalises as `failed`). The engine renders none of these.
7018
+ */
7019
+ function isAbandonedExportState(state) {
7020
+ return state === "deleted" || state === "expired" || state === "failed";
7021
+ }
6991
7022
  var ExportEngine = class {
6992
7023
  deps;
6993
7024
  queue = [];
@@ -7048,16 +7079,37 @@ var ExportEngine = class {
7048
7079
  async render(id) {
7049
7080
  const existing = await getExportRecord(this.deps.state, id);
7050
7081
  if (!existing) return;
7082
+ if (isAbandonedExportState(existing.state)) {
7083
+ this.deps.logger.info("export render skipped: the export was abandoned before it ran", {
7084
+ tags: { deviceId: existing.deviceId },
7085
+ meta: {
7086
+ exportId: id,
7087
+ state: existing.state
7088
+ }
7089
+ });
7090
+ return;
7091
+ }
7051
7092
  const subject = exportSubjectOf(existing);
7052
7093
  if (subject.kind === "clip" && subject.source !== ANALYTICS_CLIP_SOURCE) {
7053
7094
  await this.fetchClip(id, existing, subject);
7054
7095
  return;
7055
7096
  }
7056
- const rec = await patchExport(this.deps.state, id, {
7097
+ const claimed = await patchExportUnless(this.deps.state, id, {
7057
7098
  state: "rendering",
7058
7099
  progressPct: 0
7059
- });
7060
- if (!rec) return;
7100
+ }, (current) => isAbandonedExportState(current.state));
7101
+ if (claimed.kind === "missing") return;
7102
+ if (claimed.kind === "refused") {
7103
+ this.deps.logger.info("export render skipped: the export was abandoned before it ran", {
7104
+ tags: { deviceId: claimed.record.deviceId },
7105
+ meta: {
7106
+ exportId: id,
7107
+ state: claimed.record.state
7108
+ }
7109
+ });
7110
+ return;
7111
+ }
7112
+ const rec = claimed.record;
7061
7113
  const playlist = await this.deps.resolvePlaylist(rec);
7062
7114
  if (!playlist) {
7063
7115
  await this.fail(id, "no footage for range");
@@ -7691,6 +7743,19 @@ var ExportEngine = class {
7691
7743
  await this.fail(id, `render produced no video frames (${bytes} bytes, < ${EXPORT_MIN_PLAUSIBLE_BYTES})`);
7692
7744
  return;
7693
7745
  }
7746
+ const current = await getExportRecord(this.deps.state, id);
7747
+ if (current !== null && isAbandonedExportState(current.state)) {
7748
+ await this.deps.removeFile(outPath);
7749
+ this.deps.logger.info("export render discarded: the export was abandoned while it ran", {
7750
+ tags: { deviceId: current.deviceId },
7751
+ meta: {
7752
+ exportId: id,
7753
+ state: current.state,
7754
+ bytes
7755
+ }
7756
+ });
7757
+ return;
7758
+ }
7694
7759
  await patchExport(this.deps.state, id, {
7695
7760
  state: "ready",
7696
7761
  progressPct: 100,
@@ -7718,6 +7783,8 @@ var ExportEngine = class {
7718
7783
  exportId: id,
7719
7784
  error
7720
7785
  } });
7786
+ const current = await getExportRecord(this.deps.state, id);
7787
+ if (current !== null && current.state === "deleted") return;
7721
7788
  await patchExport(this.deps.state, id, {
7722
7789
  state: "failed",
7723
7790
  progressPct: null,
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.277",
21
+ version: "1.2.278",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.277",
39
+ version: "1.2.278",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -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_stream_broker_widgets-7H0Oxf23.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZh9jQi-.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-pipeline",
3
- "version": "1.2.309",
3
+ "version": "1.2.311",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",