@camstack/addon-pipeline 1.2.19 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +3 -2
  3. package/dist/chunk-DnnnRqeS.mjs +26 -0
  4. package/dist/detection-pipeline/index.js +2 -2
  5. package/dist/detection-pipeline/index.mjs +4 -3
  6. package/dist/{dist-DZBePRjP.mjs → dist-CsKaXmP8.mjs} +415 -126
  7. package/dist/{dist-DKqOqH5j.js → dist-DUHlZcWd.js} +420 -131
  8. package/dist/{model-download-service-Cp9f4dk6-WO-foD7W.mjs → model-download-service-Cp9f4dk6-CwUb5v3Y.mjs} +1 -26
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +230 -28
  12. package/dist/pipeline-runner/index.mjs +230 -28
  13. package/dist/recorder/index.js +2604 -458
  14. package/dist/recorder/index.mjs +2599 -459
  15. package/dist/{step-definitions-zDq8cP8M.js → step-definitions-Bo1hJQQw.js} +1 -1
  16. package/dist/{step-definitions-BwB6TBMB.mjs → step-definitions-C8A78dBi.mjs} +1 -1
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bwuv7iK_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs} +2 -2
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.mjs +26 -0
  20. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfnRAN5T.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSWUWZ8c.mjs} +1 -1
  21. package/dist/stream-broker/{hostInit-DC0aArn1.mjs → hostInit-CIwilJUZ.mjs} +2 -2
  22. package/dist/stream-broker/index.js +7374 -250
  23. package/dist/stream-broker/index.mjs +7371 -249
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C2hbIwzB.js → MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-BpJ-foJW.js → MotionZonesSettings-NcxxQN8r-YyB4uLdH.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BjLs9Z3w.js → PrivacyMaskSettings-APgPLF7p-DfqixEad.js} +1 -1
  28. package/embed-dist/assets/{index-Dv5H-32C.css → index-CAe6Dg-X.css} +1 -1
  29. package/embed-dist/assets/index-zVLLVdIS.js +343 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +5 -1
  32. package/dist/fmp4-keyframes-5s8T9Mng.js +0 -6711
  33. package/dist/fmp4-keyframes-CV0X1mGy.mjs +0 -6694
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDeEvhV5.mjs +0 -26
  35. package/embed-dist/assets/index-_BRQTmdE.js +0 -116
@@ -1,33 +1,8 @@
1
- import { createRequire } from "node:module";
2
1
  import * as fs from "node:fs";
3
2
  import { createReadStream, promises } from "node:fs";
4
3
  import * as path$1 from "node:path";
5
4
  import path from "node:path";
6
5
  import "node:crypto";
7
- //#region \0rolldown/runtime.js
8
- var __create = Object.create;
9
- var __defProp = Object.defineProperty;
10
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
- var __getOwnPropNames = Object.getOwnPropertyNames;
12
- var __getProtoOf = Object.getPrototypeOf;
13
- var __hasOwnProp = Object.prototype.hasOwnProperty;
14
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
15
- var __copyProps = (to, from, except, desc) => {
16
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
- key = keys[i];
18
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
- get: ((k) => from[k]).bind(null, key),
20
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
- });
22
- }
23
- return to;
24
- };
25
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
26
- value: mod,
27
- enumerable: true
28
- }) : target, mod));
29
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
- //#endregion
31
6
  //#region ../system/dist/model-download-service-Cp9f4dk6.mjs
32
7
  /**
33
8
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
@@ -353,4 +328,4 @@ function deleteModelFromDisk(modelsDir, entry, format) {
353
328
  return true;
354
329
  }
355
330
  //#endregion
356
- export { ensureModel as a, __commonJSMin as c, downloadFile as i, __require as l, createFileDataPlaneHandler as n, isModelDownloaded as o, deleteModelFromDisk as r, parseRangeHeader as s, contentTypeFor as t, __toESM as u };
331
+ export { ensureModel as a, downloadFile as i, createFileDataPlaneHandler as n, isModelDownloaded as o, deleteModelFromDisk as r, parseRangeHeader as s, contentTypeFor as t };
@@ -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-DKqOqH5j.js");
5
+ const require_dist = require("../dist-DUHlZcWd.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { A as motionDetectionCapability, K as hydrateSchema, T as evaluateZoneRules, V as BaseAddon, W as DeviceType } from "../dist-DZBePRjP.mjs";
1
+ import { A as motionDetectionCapability, E as evaluateZoneRules, K as hydrateSchema, V as BaseAddon, W as DeviceType } from "../dist-CsKaXmP8.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-DKqOqH5j.js");
6
+ const require_dist = require("../dist-DUHlZcWd.js");
7
7
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
10
- const require_step_definitions = require("../step-definitions-zDq8cP8M.js");
10
+ const require_step_definitions = require("../step-definitions-Bo1hJQQw.js");
11
11
  let node_child_process = require("node:child_process");
12
12
  let node_url = require("node:url");
13
13
  let sharp = require("sharp");
@@ -575,6 +575,22 @@ var PipelineTimingSampler = class {
575
575
  //#endregion
576
576
  //#region src/pipeline-runner/runner.ts
577
577
  /**
578
+ * Bound on one `processFrame` call. Chosen well above the slowest legitimate
579
+ * inference seen in production (8 617 ms, GPU contention on the hub) so the
580
+ * timeout only ever fires on a call that is genuinely never coming back.
581
+ */
582
+ var DEFAULT_INFERENCE_TIMEOUT_MS = 2e4;
583
+ /** Grace period before a permit-starved scheduler with queued work warns. */
584
+ var DEFAULT_INFERENCE_STALL_WARN_MS = 6e4;
585
+ /** Re-warn cadence while a stall persists, so a wedge stays visible. */
586
+ var INFERENCE_STALL_REWARN_MS = 6e4;
587
+ /**
588
+ * Sentinel distinguishing "the bound expired" from a legitimate `null`
589
+ * (`processFrame` returning no result). A unique symbol, so it can never
590
+ * collide with a value the executor produces.
591
+ */
592
+ var TIMED_OUT = Symbol("inference-timed-out");
593
+ /**
578
594
  * Clone a {@link DetectionQueueEntry} for the queue's copy-on-retain
579
595
  * boundary: the inner frame's pixel buffer is detached via
580
596
  * {@link ownFrame}, the `FrameHandle` is carried through unchanged (it's
@@ -657,6 +673,10 @@ var PipelineRunner = class {
657
673
  arrivalCounts = /* @__PURE__ */ new Map();
658
674
  lastGovernorRecomputeAt = Date.now();
659
675
  governorIntervalHandle = null;
676
+ /** When the scheduler first found itself with zero permits; null when healthy. */
677
+ inferenceStallSince = null;
678
+ /** Last stall warning, so a persistent wedge re-warns instead of going quiet. */
679
+ inferenceStallWarnedAt = null;
660
680
  constructor(config) {
661
681
  this.config = config;
662
682
  this.logger = config.logger;
@@ -1047,7 +1067,12 @@ var PipelineRunner = class {
1047
1067
  }
1048
1068
  tick() {
1049
1069
  this.drainMotionQueues();
1050
- if (this.semaphore.available <= 0) return;
1070
+ if (this.semaphore.available <= 0) {
1071
+ this.noteInferenceStall();
1072
+ return;
1073
+ }
1074
+ this.inferenceStallSince = null;
1075
+ this.inferenceStallWarnedAt = null;
1051
1076
  const picked = this.pickNextDetectionFrame();
1052
1077
  if (!picked) return;
1053
1078
  const { deviceId, entry, state } = picked;
@@ -1070,11 +1095,18 @@ var PipelineRunner = class {
1070
1095
  const pickedAt = Date.now();
1071
1096
  const { frame, handle } = entry;
1072
1097
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
1073
- const release = await this.semaphore.acquire();
1098
+ const rawRelease = await this.semaphore.acquire();
1099
+ let released = false;
1100
+ const release = () => {
1101
+ if (released) return;
1102
+ released = true;
1103
+ rawRelease();
1104
+ };
1074
1105
  const semAcquiredAt = Date.now();
1075
1106
  const executorHandle = state.detectionQueue.size <= HANDLE_PASSTHROUGH_MAX_QUEUE_DEPTH && this.semaphore.available > 0 ? handle : void 0;
1076
1107
  try {
1077
- const result = await this.config.processFrame(deviceId, frameInput, executorHandle);
1108
+ const result = await this.awaitInferenceBounded(deviceId, frameInput, executorHandle, release);
1109
+ if (result === TIMED_OUT) return;
1078
1110
  const inferDoneAt = Date.now();
1079
1111
  const inferenceMs = inferDoneAt - semAcquiredAt;
1080
1112
  state.inferenceTimes.push(inferenceMs);
@@ -1098,6 +1130,73 @@ var PipelineRunner = class {
1098
1130
  release();
1099
1131
  }
1100
1132
  }
1133
+ /**
1134
+ * Await `processFrame` with a hard bound. On expiry the permit is released
1135
+ * immediately (via the caller's release-once closure) and {@link TIMED_OUT}
1136
+ * is returned; the underlying call is left to settle into the void, since
1137
+ * the executor contract offers no cancellation.
1138
+ */
1139
+ async awaitInferenceBounded(deviceId, frameInput, executorHandle, release) {
1140
+ const timeoutMs = this.config.inferenceTimeoutMs ?? DEFAULT_INFERENCE_TIMEOUT_MS;
1141
+ let timer = null;
1142
+ const expiry = new Promise((resolve) => {
1143
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
1144
+ timer.unref?.();
1145
+ });
1146
+ try {
1147
+ const outcome = await Promise.race([this.config.processFrame(deviceId, frameInput, executorHandle), expiry]);
1148
+ if (outcome === TIMED_OUT) {
1149
+ release();
1150
+ this.logger?.warn("inference timed out — permit released, result discarded", {
1151
+ tags: { deviceId },
1152
+ meta: {
1153
+ timeoutMs,
1154
+ availablePermits: this.semaphore.available
1155
+ }
1156
+ });
1157
+ }
1158
+ return outcome;
1159
+ } finally {
1160
+ if (timer !== null) clearTimeout(timer);
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Called on every tick that finds zero available permits. Warns once the
1165
+ * starvation outlives `inferenceStallWarnMs` WITH queued work — queued work
1166
+ * is the discriminator: a runner with no permits and no backlog is simply
1167
+ * busy, while one with a backlog it cannot touch is wedged.
1168
+ */
1169
+ noteInferenceStall() {
1170
+ let queuedFrames = 0;
1171
+ for (const state of this.cameras.values()) queuedFrames += state.detectionQueue.size;
1172
+ if (queuedFrames === 0) {
1173
+ this.inferenceStallSince = null;
1174
+ this.inferenceStallWarnedAt = null;
1175
+ return;
1176
+ }
1177
+ const now = Date.now();
1178
+ if (this.inferenceStallSince === null) {
1179
+ this.inferenceStallSince = now;
1180
+ return;
1181
+ }
1182
+ const stalledMs = now - this.inferenceStallSince;
1183
+ if (stalledMs < (this.config.inferenceStallWarnMs ?? DEFAULT_INFERENCE_STALL_WARN_MS)) return;
1184
+ if (this.inferenceStallWarnedAt !== null && now - this.inferenceStallWarnedAt < INFERENCE_STALL_REWARN_MS) return;
1185
+ this.inferenceStallWarnedAt = now;
1186
+ for (const [deviceId, state] of this.cameras) {
1187
+ if (state.detectionQueue.size === 0) continue;
1188
+ this.logger?.warn("no inference permit available while frames are queued", {
1189
+ tags: { deviceId },
1190
+ meta: {
1191
+ stalledMs,
1192
+ queuedFrames: state.detectionQueue.size,
1193
+ droppedFrames: state.detectionQueue.droppedFrames,
1194
+ phase: state.phase,
1195
+ concurrency: this.semaphore.concurrency
1196
+ }
1197
+ });
1198
+ }
1199
+ }
1101
1200
  async notifyCallbacks(deviceId, frame, result, streamType, handle) {
1102
1201
  for (const callback of this.resultCallbacks) try {
1103
1202
  await callback(deviceId, frame, result, streamType, handle);
@@ -1237,7 +1336,24 @@ function forkDecodeWorker(_source) {
1237
1336
  //#endregion
1238
1337
  //#region src/session-decode/guarded-session-decode.ts
1239
1338
  /**
1240
- * Guards the in-flight restream-acquire against a detach/unsubscribe race.
1339
+ * 2s 5s 15s 30s, then 30s forever.
1340
+ *
1341
+ * Indefinite is deliberate. The failure this recovers from is "the broker has
1342
+ * no profile bound yet" or "the broker call failed right now" — both of which
1343
+ * clear on their own, often minutes later (a source re-dialing, a runner
1344
+ * restarting). A bounded retry would turn a slow recovery back into the
1345
+ * permanent outage this exists to prevent, and teardown already stops it: a
1346
+ * detach aborts the loop, so nothing retries for a camera nobody wants.
1347
+ */
1348
+ var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
1349
+ 2e3,
1350
+ 5e3,
1351
+ 15e3,
1352
+ 3e4
1353
+ ];
1354
+ /**
1355
+ * Guards the in-flight restream-acquire against a detach/unsubscribe race,
1356
+ * and retries an acquire that fails.
1241
1357
  *
1242
1358
  * `acquireSessionDecodeRestream` + forking the decode worker both take real
1243
1359
  * time (a broker round-trip, then a process fork). If the caller's teardown
@@ -1249,29 +1365,71 @@ function forkDecodeWorker(_source) {
1249
1365
  *
1250
1366
  * This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
1251
1367
  * run in the background; an `aborted` flag recorded by the (possibly
1252
- * already-called) teardown is checked right after the acquire resolves —
1253
- * if set, the restream is released immediately and the pump is NEVER
1368
+ * already-called) teardown is checked right after each acquire resolves —
1369
+ * if set, any restream is released immediately and the pump is NEVER
1254
1370
  * started (no orphan worker). Otherwise the pump starts and the teardown,
1255
1371
  * once called, stops the pump then releases the restream.
1372
+ *
1373
+ * **A failed acquire is retried, not swallowed.** The teardown handed back
1374
+ * here is stored by the caller (as `attachment.detectionUnsubscribe`) the
1375
+ * instant this returns, so a `return` on failure left that handle permanently
1376
+ * non-null with no pump behind it: `onEnded` could never fire because nothing
1377
+ * had ever started, the camera's phase stayed `active`, and not one frame
1378
+ * arrived again. The motion path was hardened against this shape by a frame
1379
+ * freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
1380
+ * path's half.
1256
1381
  */
1257
1382
  function startGuardedSessionDecode(deps) {
1258
1383
  let aborted = false;
1259
1384
  let stopPump = null;
1260
1385
  let release = null;
1386
+ let retryTimer = null;
1387
+ const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
1388
+ /** Resolves after the backoff for `attempt`, or immediately once aborted. */
1389
+ const waitBackoff = (attempt) => new Promise((resolve) => {
1390
+ const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
1391
+ retryTimer = setTimeout(() => {
1392
+ retryTimer = null;
1393
+ resolve();
1394
+ }, ms);
1395
+ retryTimer.unref?.();
1396
+ });
1261
1397
  (async () => {
1262
- const acquired = await deps.acquire();
1263
- if (!acquired) return;
1264
- if (aborted) {
1265
- acquired.release();
1398
+ for (let attempt = 0; !aborted; attempt++) {
1399
+ let acquired = null;
1400
+ try {
1401
+ acquired = await deps.acquire();
1402
+ } catch (err) {
1403
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
1404
+ attempt: attempt + 1,
1405
+ error: require_dist.errMsg(err)
1406
+ } });
1407
+ await waitBackoff(attempt);
1408
+ continue;
1409
+ }
1410
+ if (aborted) {
1411
+ if (acquired) acquired.release();
1412
+ return;
1413
+ }
1414
+ if (!acquired) {
1415
+ (attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
1416
+ await waitBackoff(attempt);
1417
+ continue;
1418
+ }
1419
+ if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
1420
+ release = acquired.release;
1421
+ stopPump = deps.startPump(acquired.source);
1266
1422
  return;
1267
1423
  }
1268
- release = acquired.release;
1269
- stopPump = deps.startPump(acquired.source);
1270
1424
  })().catch((err) => {
1271
1425
  deps.logger.warn("session-decode: guarded start failed", { meta: { error: require_dist.errMsg(err) } });
1272
1426
  });
1273
1427
  return () => {
1274
1428
  aborted = true;
1429
+ if (retryTimer !== null) {
1430
+ clearTimeout(retryTimer);
1431
+ retryTimer = null;
1432
+ }
1275
1433
  stopPump?.();
1276
1434
  if (release) release();
1277
1435
  };
@@ -2063,10 +2221,14 @@ var NativeCropRegistry = class {
2063
2221
  * Bind `request` to `handle`; evicts the oldest entry past the cap, releasing
2064
2222
  * its worker-side RAM lease so a lease never outlives its registry entry.
2065
2223
  */
2066
- register(handle, request) {
2224
+ register(handle, request, deviceId) {
2067
2225
  const k = this.key(handle);
2068
2226
  this.entries.delete(k);
2069
- this.entries.set(k, request);
2227
+ this.entries.set(k, {
2228
+ request,
2229
+ deviceId,
2230
+ registeredAt: Date.now()
2231
+ });
2070
2232
  while (this.entries.size > this.capacity) {
2071
2233
  const oldest = this.entries.keys().next().value;
2072
2234
  if (oldest === void 0) break;
@@ -2075,7 +2237,22 @@ var NativeCropRegistry = class {
2075
2237
  }
2076
2238
  /** The requester for `handle`, or `null` if never registered / already evicted. */
2077
2239
  get(handle) {
2078
- return this.entries.get(this.key(handle)) ?? null;
2240
+ const entry = this.entries.get(this.key(handle));
2241
+ if (entry === void 0) return null;
2242
+ return {
2243
+ request: entry.request,
2244
+ deviceId: entry.deviceId,
2245
+ ageMs: Date.now() - entry.registeredAt
2246
+ };
2247
+ }
2248
+ /**
2249
+ * The deviceId a handle was registered under, even AFTER its entry was
2250
+ * evicted — kept in a small parallel ring so a `no-registry-entry` miss can
2251
+ * still be attributed to a camera (a miss line without `deviceId` cannot
2252
+ * answer "why is 617 worse than 615?").
2253
+ */
2254
+ deviceIdFor(handle) {
2255
+ return this.evictedDeviceIds.get(this.key(handle)) ?? this.entries.get(this.key(handle))?.deviceId ?? null;
2079
2256
  }
2080
2257
  /**
2081
2258
  * Release the worker-side RAM native lease for `handle` and drop its entry —
@@ -2087,15 +2264,27 @@ var NativeCropRegistry = class {
2087
2264
  }
2088
2265
  /** Drop every entry, releasing each frame's worker-side lease (node/addon shutdown). */
2089
2266
  clear() {
2090
- for (const request of this.entries.values()) safeRelease(request);
2267
+ for (const entry of this.entries.values()) safeRelease(entry.request);
2091
2268
  this.entries.clear();
2269
+ this.evictedDeviceIds.clear();
2092
2270
  }
2093
- /** Release + drop one entry by its map key. */
2271
+ /** Release + drop one entry by its map key, remembering its camera. */
2094
2272
  releaseEntry(key) {
2095
- const request = this.entries.get(key);
2096
- if (!request) return;
2273
+ const entry = this.entries.get(key);
2274
+ if (!entry) return;
2097
2275
  this.entries.delete(key);
2098
- safeRelease(request);
2276
+ this.rememberEvicted(key, entry.deviceId);
2277
+ safeRelease(entry.request);
2278
+ }
2279
+ /** Bounded memory of evicted handles → deviceId, for miss attribution. */
2280
+ evictedDeviceIds = /* @__PURE__ */ new Map();
2281
+ rememberEvicted(key, deviceId) {
2282
+ this.evictedDeviceIds.set(key, deviceId);
2283
+ while (this.evictedDeviceIds.size > this.capacity) {
2284
+ const oldest = this.evictedDeviceIds.keys().next().value;
2285
+ if (oldest === void 0) break;
2286
+ this.evictedDeviceIds.delete(oldest);
2287
+ }
2099
2288
  }
2100
2289
  };
2101
2290
  //#endregion
@@ -3301,16 +3490,29 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3301
3490
  const isFullFrame = b.x <= .001 && b.y <= .001 && b.w >= .999 && b.h >= .999;
3302
3491
  const isUnboundedFullFrame = isFullFrame && input.maxWidth === void 0;
3303
3492
  let nativeResult = null;
3493
+ let missReason = null;
3304
3494
  if (!isUnboundedFullFrame) {
3305
- const requester = this.nativeCropRegistry.get(input.handle);
3306
- if (requester) {
3307
- const result = await requester(input.bbox, input.maxWidth);
3495
+ const found = this.nativeCropRegistry.get(input.handle);
3496
+ if (found) {
3497
+ const result = await found.request(input.bbox, input.maxWidth);
3308
3498
  if (result) nativeResult = {
3309
3499
  bytes: result.bytes,
3310
3500
  width: result.width,
3311
3501
  height: result.height
3312
3502
  };
3313
- }
3503
+ else missReason = "worker-lease-gone";
3504
+ } else missReason = "no-registry-entry";
3505
+ if (missReason !== null) this.ctx.logger.debug("native crop miss", {
3506
+ tags: { deviceId: found?.deviceId ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
3507
+ meta: {
3508
+ reason: missReason,
3509
+ handle: `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`,
3510
+ handleNodeId: input.handle.nodeId,
3511
+ handleAgeMs: found?.ageMs ?? null,
3512
+ registrySize: this.nativeCropRegistry.size,
3513
+ registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
3514
+ }
3515
+ });
3314
3516
  }
3315
3517
  const tier = resolveNativeCropSource(isFullFrame, nativeResult !== null);
3316
3518
  if (tier === "native" && nativeResult) return this.encodeNativeCropResult(nativeResult, tier, input.encodeJpeg);
@@ -3791,7 +3993,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
3791
3993
  onEnded: () => this.handleDetectionDecodeEnded(config.deviceId),
3792
3994
  onDecodedFrame: (frame, nativeCrop) => {
3793
3995
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
3794
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
3996
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
3795
3997
  this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
3796
3998
  }
3797
3999
  });
@@ -4304,7 +4506,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
4304
4506
  onDecodedFrame: (frame, nativeCrop) => {
4305
4507
  if (settled) return;
4306
4508
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
4307
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
4509
+ if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
4308
4510
  runner.enqueueOccupancyFrame(deviceId, frame, handle);
4309
4511
  count++;
4310
4512
  if (count >= frames) {