@camstack/addon-pipeline 1.2.73 → 1.2.75

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 (36) hide show
  1. package/dist/{addon-utils-CQs-AjTJ.js → addon-utils-a_36HH_N.js} +4 -4
  2. package/dist/audio-analyzer/index.js +5 -6
  3. package/dist/audio-analyzer/index.mjs +3 -3
  4. package/dist/detection-pipeline/index.js +9 -10
  5. package/dist/detection-pipeline/index.mjs +5 -5
  6. package/dist/{dist-BAbvf81B.js → dist-6ZJCV0jY.js} +1901 -28
  7. package/dist/{dist-Dpr92hOt.mjs → dist-T_8jMw7d.mjs} +1866 -28
  8. package/dist/{event-loop-stall-monitor-BhKoybot.js → event-loop-stall-monitor-Cjof8vA1.js} +1 -1
  9. package/dist/{event-loop-stall-monitor-B7Wd2jo-.mjs → event-loop-stall-monitor-XeGmL4xr.mjs} +1 -1
  10. package/dist/lazy-sharp-6oymT_yf.mjs +341 -0
  11. package/dist/lazy-sharp-DvwPafeP.js +360 -0
  12. package/dist/motion-wasm/index.js +3 -3
  13. package/dist/motion-wasm/index.mjs +3 -3
  14. package/dist/pipeline-runner/index.js +109 -21
  15. package/dist/pipeline-runner/index.mjs +107 -18
  16. package/dist/recorder/index.js +138 -32
  17. package/dist/recorder/index.mjs +137 -30
  18. package/dist/session-decode/decode-worker-child.js +311 -126
  19. package/dist/session-decode/decode-worker-child.mjs +311 -123
  20. package/dist/stream-broker/_stub.js +94 -94
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cvh5La_s.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-H2OTmgR6.mjs} +2 -2
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.mjs +26 -0
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfsLsn2e.mjs +26 -0
  24. package/dist/stream-broker/{hostInit-Ci7epN77.mjs → hostInit-4XDN_Nt0.mjs} +2 -2
  25. package/dist/stream-broker/index.js +12 -13
  26. package/dist/stream-broker/index.mjs +3 -3
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-BFq9AIDa.js → worker-protocol-B0cbTXyw.js} +1 -1
  29. package/dist/{worker-protocol-C1FtUWvH.mjs → worker-protocol-SqED7Y9i.mjs} +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-emK7D4bc.js +0 -35
  32. package/dist/lazy-sharp-B-mb8uWx.mjs +0 -20
  33. package/dist/lazy-sharp-CcYuXtsa.js +0 -25
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C_plRUtD.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CExO0piw.mjs +0 -26
  36. package/dist/{addon-utils-CZ2xo67g.mjs → addon-utils-A2S9D7pu.mjs} +2 -2
@@ -0,0 +1,360 @@
1
+ const require_dist = require("./dist-6ZJCV0jY.js");
2
+ let node_url = require("node:url");
3
+ let node_module = require("node:module");
4
+ let node_path = require("node:path");
5
+ node_path = require_dist.__toESM(node_path);
6
+ //#region src/shared/host-external-import.ts
7
+ /**
8
+ * Resolving a HOST-EXTERNAL package (`sharp`, `node-av`, …) from a process that
9
+ * does NOT have the framework ESM resolver hook registered.
10
+ *
11
+ * ## The problem this exists for
12
+ *
13
+ * An addon bundle leaves the packages in `HOST_EXTERNAL_SPECIFIERS`
14
+ * (`@camstack/system` → `kernel/host-externals.ts`) unbundled and expects the
15
+ * HOST to provide them. What provides them is a module-customization hook
16
+ * (`framework-resolver-hook.mjs`) that `registerFrameworkResolver` installs in
17
+ * the **addon-runner process**, re-anchoring those bare specifiers at
18
+ * `<frameworkDir>/node_modules`.
19
+ *
20
+ * A worker started with a plain `child_process.fork` — the session decode
21
+ * worker (`session-decode/fork-decode-worker.ts`) is the one in this package —
22
+ * does NOT inherit that hook. In it, `import('sharp')` and `import('node-av')`
23
+ * throw `Cannot find package 'X' imported from /data/addons/@camstack/…`,
24
+ * because the addon directory has no `node_modules` to walk up into.
25
+ *
26
+ * ## Why an explicit path and not the hook
27
+ *
28
+ * Registering the framework resolver in the child was considered and rejected:
29
+ *
30
+ * - it would need the child to reach INTO another package's build output
31
+ * (`<frameworkDir>/node_modules/@camstack/system/dist/…/framework-resolver-hook.mjs`)
32
+ * plus an `--import` shim, i.e. a hard coupling to a layout nothing checks;
33
+ * - a registered hook makes **every** `import()` in that process re-enter the
34
+ * loader and block the main thread on `Atomics.wait` until the hooks worker
35
+ * answers — the exact cost `shared/lazy-sharp.ts` was written to remove from
36
+ * the frame path. Paying it per decode worker to load two modules is the
37
+ * wrong trade;
38
+ * - the explicit-path fallback is already the shipped answer for `node-av` in
39
+ * this same worker, and had been for months. A second mechanism for `sharp`
40
+ * would mean two ways to fail.
41
+ *
42
+ * So: try the bare specifier (works on the hub, where the fork inherits the
43
+ * hook, and in dev, where workspace symlinks resolve), then resolve the package
44
+ * explicitly out of the module roots an agent is known to lay out.
45
+ *
46
+ * ## The anchors, in order
47
+ *
48
+ * - **DOCKER / single-copy node** — the framework overlay
49
+ * `CAMSTACK_FRAMEWORK_DIR` (`<frameworkDir>/node_modules/<pkg>`). The
50
+ * launcher points this at the ACTIVE closure in data-root mode and forked
51
+ * children inherit it (`server/backend/src/launcher.ts`).
52
+ * - **ELECTRON agent** — the app bundle beside `CAMSTACK_BUNDLED_ADDONS_DIR`
53
+ * (`<resources>/addons`): `<resources>/node_modules/<pkg>` and the
54
+ * `app.asar.unpacked` mirror.
55
+ * - **the process cwd** — always tried last, and it is NOT redundant:
56
+ * `CAMSTACK_FRAMEWORK_DIR` is only exported on some boots (the Docker
57
+ * entrypoint sets it in its fallback branch; the launcher only in data-root
58
+ * mode), while the launcher ALWAYS extends `NODE_PATH` with the framework and
59
+ * hub `node_modules`. A CJS-style resolution honours `NODE_PATH`; an ESM
60
+ * `import()` of a bare specifier does not. This is the same escape hatch
61
+ * `stream-broker/webrtc/session.ts` uses to find the framework's `werift`.
62
+ *
63
+ * An anchor that cannot resolve the specifier is skipped, so an empty result
64
+ * leaves the bare specifier as the only path, exactly as before.
65
+ *
66
+ * Resolution goes through `createRequire(...).resolve(specifier)` rather than a
67
+ * hardcoded relative entry (`dist/index.js`, …): that honours the package's own
68
+ * `exports`/`main`, so it keeps working across a dependency's file-layout
69
+ * changes, handles SUBPATH specifiers (`node-av/constants`) with no mapping
70
+ * table, and consults `NODE_PATH`. It also verifies the file exists, which is
71
+ * what the previous `fs.existsSync` check did by hand.
72
+ */
73
+ /**
74
+ * The anchor filename resolution walks up from. It never has to EXIST —
75
+ * `createRequire` only needs a path to anchor at — but naming it after camstack
76
+ * keeps a stack trace readable if one ever escapes.
77
+ */
78
+ var ANCHOR_FILE = "__camstack_host_external_anchor__.cjs";
79
+ /** Node error codes that mean "the module is not there", not "it failed to load". */
80
+ var RESOLUTION_ERROR_CODES = [
81
+ "ERR_MODULE_NOT_FOUND",
82
+ "MODULE_NOT_FOUND",
83
+ "ERR_PACKAGE_PATH_NOT_EXPORTED",
84
+ "ERR_UNSUPPORTED_DIR_IMPORT"
85
+ ];
86
+ /** `err.code` when the error carries a string one. No cast: `in` narrows. */
87
+ function errorCode(err) {
88
+ if (typeof err !== "object" || err === null) return null;
89
+ if (!("code" in err)) return null;
90
+ const code = err.code;
91
+ return typeof code === "string" ? code : null;
92
+ }
93
+ /**
94
+ * True when `err` says the module could not be FOUND (as opposed to found and
95
+ * failed to load — a broken native binary, a throwing top level).
96
+ *
97
+ * The distinction is load-bearing for callers that memoize failure: a
98
+ * resolution failure is deterministic for the life of the process (the env and
99
+ * the framework layout do not change under a running worker), so retrying it
100
+ * per frame can only re-pay the loader round-trip and re-log. A LOAD failure
101
+ * can plausibly be a deploy blip and stays retryable.
102
+ *
103
+ * The message check is a belt for the same braces: Node has moved these codes
104
+ * before, and a miss here degrades to "retry forever", which is the bug.
105
+ */
106
+ function isModuleResolutionError(err) {
107
+ const code = errorCode(err);
108
+ if (code !== null && RESOLUTION_ERROR_CODES.includes(code)) return true;
109
+ if (!(err instanceof Error)) return false;
110
+ return err.message.includes("Cannot find package") || err.message.includes("Cannot find module");
111
+ }
112
+ /**
113
+ * The files a resolution is anchored at, in the order they are tried. Each is a
114
+ * path INSIDE a candidate `node_modules` (so `<dir>/node_modules/<pkg>` is on
115
+ * the walk), except the cwd anchor, which walks up from the cwd itself.
116
+ *
117
+ * The anchor files never have to exist — `createRequire` only needs a path.
118
+ */
119
+ function hostExternalAnchors(env, cwd) {
120
+ const anchors = [];
121
+ const frameworkDir = env["CAMSTACK_FRAMEWORK_DIR"];
122
+ if (frameworkDir !== void 0 && frameworkDir.length > 0) anchors.push(node_path.join(frameworkDir, "node_modules", ANCHOR_FILE));
123
+ const bundledAddons = env["CAMSTACK_BUNDLED_ADDONS_DIR"];
124
+ if (bundledAddons !== void 0 && bundledAddons.length > 0) {
125
+ const resourcesRoot = node_path.dirname(bundledAddons);
126
+ anchors.push(node_path.join(resourcesRoot, "node_modules", ANCHOR_FILE));
127
+ anchors.push(node_path.join(resourcesRoot, "app.asar.unpacked", "node_modules", ANCHOR_FILE));
128
+ }
129
+ anchors.push(node_path.join(cwd, ANCHOR_FILE));
130
+ return anchors;
131
+ }
132
+ /** `createRequire(anchor).resolve(specifier)`, or `null` when it is not there. */
133
+ function resolveFromAnchor(anchorPath, specifier) {
134
+ try {
135
+ return (0, node_module.createRequire)(anchorPath).resolve(specifier);
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Importable `file:` URLs for `specifier`, in the order a caller should try them
142
+ * after the bare specifier has already failed. Deduplicated (two anchors
143
+ * routinely resolve to the SAME file — a NODE_PATH hit, or a symlink), and empty
144
+ * when nothing resolves it, which leaves the bare specifier the only path.
145
+ */
146
+ function hostExternalEntryUrls(specifier, options = {}) {
147
+ const env = options.env ?? process.env;
148
+ const cwd = options.cwd ?? process.cwd();
149
+ const resolveEntry = options.resolve ?? resolveFromAnchor;
150
+ const urls = [];
151
+ for (const anchor of hostExternalAnchors(env, cwd)) {
152
+ const entry = resolveEntry(anchor, specifier);
153
+ if (entry === null || entry.length === 0) continue;
154
+ const url = (0, node_url.pathToFileURL)(entry).href;
155
+ if (!urls.includes(url)) urls.push(url);
156
+ }
157
+ return urls;
158
+ }
159
+ //#endregion
160
+ //#region src/shared/lazy-sharp.ts
161
+ /**
162
+ * lazy-sharp — resolve `sharp` ONCE, not once per frame, and give up ONCE when
163
+ * it cannot be resolved at all.
164
+ *
165
+ * ## What this fixes (2026-08-02)
166
+ *
167
+ * `await import('sharp')` sat directly in the frame path, in a spot whose own
168
+ * comment reads "This is the production hot path (camera frames from broker)",
169
+ * and in six other places along the same path. At ~13 fps per camera across the
170
+ * fleet that is thousands of dynamic imports a minute.
171
+ *
172
+ * Node caches the MODULE after the first import, so this looks free. It is not:
173
+ * the addon runner registers module-customization hooks (the framework
174
+ * resolver), and with hooks registered every `import()` re-enters the loader and
175
+ * resolves the specifier through the hooks worker — **synchronously**. A CPU
176
+ * profile of the live detection runner caught it exactly:
177
+ *
178
+ * makeSyncRequest@hooks:631
179
+ * resolveSync@hooks:860
180
+ * #resolveAndMaybeBlockOnLoaderThread@loader:715
181
+ * …
182
+ * import@loader:626
183
+ * runPipeline@index.js:5565
184
+ *
185
+ * `#resolveAndMaybeBlockOnLoaderThread` is the name of the problem:
186
+ * `makeSyncRequest` blocks the MAIN THREAD on `Atomics.wait` until the hooks
187
+ * worker answers. That is a synchronous block on the frame path, which is the
188
+ * shape the 22-88s event-loop freezes had.
189
+ *
190
+ * ## What it fixes a second time (2026-08-14)
191
+ *
192
+ * A memo that CLEARS on failure re-pays that entire cost on every call once the
193
+ * module genuinely is not there. The subject-tile tier hit exactly that: in the
194
+ * decode worker (a plain `child_process.fork`, no resolver hook) `import('sharp')`
195
+ * cannot resolve, and because the memo was dropped on failure, every subject of
196
+ * every frame re-entered the loader and logged a WARN — 3 800 WARN/min
197
+ * cluster-wide, with a synchronous loader round-trip on the frame path behind
198
+ * each one, and not one tile ever cut.
199
+ *
200
+ * So this module now does two more things:
201
+ *
202
+ * 1. **Falls back to an explicit path.** Same mechanism `node-av` has used in
203
+ * the same worker for months, extracted into
204
+ * `shared/host-external-import.ts` — read its docblock for the layouts and
205
+ * for why the framework resolver is NOT registered in the child instead.
206
+ * 2. **Distinguishes the two failure kinds.**
207
+ * - **Resolution** failure (nothing, anywhere, resolves `sharp`): the answer
208
+ * cannot change while this process lives, so it is memoized as a sticky
209
+ * {@link SharpUnavailableError}. Later calls reject IMMEDIATELY, with no
210
+ * loader re-entry, and exactly ONE warning is emitted for the process
211
+ * naming every candidate tried and the cause.
212
+ * - **Load** failure (found, but the native binary or the module body threw):
213
+ * the memo is cleared, so a later call retries. A partially-installed
214
+ * native dep during a deploy must not poison the encoder for the whole
215
+ * process lifetime — that would turn a blip into a camera that never
216
+ * produces media again. This is the original behaviour, kept on purpose,
217
+ * and it is safe to keep only because it is now the NARROW case.
218
+ *
219
+ * ## Why a memo and not a static import
220
+ *
221
+ * `sharp` is a heavy native module. The dynamic import was deliberate — nodes
222
+ * that never encode a frame should not pay to load it, and a node whose native
223
+ * build is missing must fail at first use rather than at boot. Both properties
224
+ * survive: the module is still loaded on FIRST use, and a failure still surfaces
225
+ * at that call. Only the repeat resolutions are removed.
226
+ *
227
+ * The promise itself is cached (not just the resolved value), so N concurrent
228
+ * first-callers share one import instead of racing into N.
229
+ */
230
+ /** The bare specifier. Also the first entry of a failure's candidate list. */
231
+ var SHARP_SPECIFIER = "sharp";
232
+ /**
233
+ * `sharp` cannot be resolved in this process, and nothing about that can change
234
+ * while it runs. Carries the candidates tried so the log answers "where did you
235
+ * look" without a second round of diagnosis.
236
+ */
237
+ var SharpUnavailableError = class extends Error {
238
+ attempts;
239
+ constructor(attempts, cause) {
240
+ super(`sharp could not be resolved in this process — tried ${attempts.join(", ")}; cause: ${describeError(cause)}`, { cause });
241
+ this.name = "SharpUnavailableError";
242
+ this.attempts = attempts;
243
+ }
244
+ };
245
+ function describeError(err) {
246
+ return err instanceof Error ? err.message : String(err);
247
+ }
248
+ /**
249
+ * The bare specifier stays a LITERAL `import('sharp')` so the bundler keeps
250
+ * treating it as the declared external it is; only the fallback candidates go
251
+ * through a variable specifier (typed `any` by TS, so the declared return type
252
+ * applies without a cast — the same shape `importNodeAv` uses).
253
+ */
254
+ async function importSharpModule(specifier) {
255
+ if (specifier === SHARP_SPECIFIER) return import("sharp");
256
+ return await import(specifier);
257
+ }
258
+ /**
259
+ * Build a loader. Exported for tests; production uses the module-level
260
+ * {@link getSharp}, which is one of these.
261
+ */
262
+ function createSharpLoader(deps = {}) {
263
+ const importModule = deps.importModule ?? importSharpModule;
264
+ const fallbackUrls = deps.fallbackUrls ?? (() => hostExternalEntryUrls(SHARP_SPECIFIER));
265
+ const warn = deps.warn;
266
+ let pending = null;
267
+ let unavailable = null;
268
+ let warned = false;
269
+ /** Bare first, then each fallback candidate, in order. */
270
+ const attempt = async () => {
271
+ const attempts = [SHARP_SPECIFIER];
272
+ let lastError;
273
+ try {
274
+ return (await importModule(SHARP_SPECIFIER)).default;
275
+ } catch (bareError) {
276
+ if (!isModuleResolutionError(bareError)) {
277
+ pending = null;
278
+ throw bareError;
279
+ }
280
+ lastError = bareError;
281
+ }
282
+ for (const url of fallbackUrls()) {
283
+ attempts.push(url);
284
+ try {
285
+ return (await importModule(url)).default;
286
+ } catch (candidateError) {
287
+ if (!isModuleResolutionError(candidateError)) {
288
+ pending = null;
289
+ throw candidateError;
290
+ }
291
+ lastError = candidateError;
292
+ }
293
+ }
294
+ const failure = new SharpUnavailableError(attempts, lastError);
295
+ unavailable = failure;
296
+ pending = null;
297
+ if (!warned) {
298
+ warned = true;
299
+ if (warn) warn(failure.message);
300
+ else process.stderr.write(`${failure.message}\n`);
301
+ }
302
+ throw failure;
303
+ };
304
+ return {
305
+ load: () => {
306
+ if (unavailable !== null) return Promise.reject(unavailable);
307
+ if (pending === null) pending = attempt();
308
+ return pending;
309
+ },
310
+ reset: () => {
311
+ pending = null;
312
+ unavailable = null;
313
+ warned = false;
314
+ }
315
+ };
316
+ }
317
+ /** Where the process-wide loader's single diagnostic goes. */
318
+ var warnSink = null;
319
+ /**
320
+ * Route the one-off "sharp is unavailable" diagnostic somewhere it will be seen.
321
+ * Without a sink it goes to stderr, which the decode worker's parent inherits
322
+ * but does NOT forward to the scoped logger — so the worker installs one at
323
+ * boot (`decode-worker-child.ts`) to get the line into `addons.getLogs` with its
324
+ * `[dev:<id>]` prefix.
325
+ */
326
+ function setSharpWarnSink(sink) {
327
+ warnSink = sink;
328
+ }
329
+ var defaultLoader = createSharpLoader({ warn: (message) => {
330
+ if (warnSink) warnSink(message);
331
+ else process.stderr.write(`${message}\n`);
332
+ } });
333
+ /**
334
+ * The `sharp` factory, resolved once per process.
335
+ *
336
+ * Rejects with {@link SharpUnavailableError} — immediately, and without touching
337
+ * the module loader — once resolution has been proven impossible here.
338
+ */
339
+ function getSharp() {
340
+ return defaultLoader.load();
341
+ }
342
+ //#endregion
343
+ Object.defineProperty(exports, "getSharp", {
344
+ enumerable: true,
345
+ get: function() {
346
+ return getSharp;
347
+ }
348
+ });
349
+ Object.defineProperty(exports, "hostExternalEntryUrls", {
350
+ enumerable: true,
351
+ get: function() {
352
+ return hostExternalEntryUrls;
353
+ }
354
+ });
355
+ Object.defineProperty(exports, "setSharpWarnSink", {
356
+ enumerable: true,
357
+ get: function() {
358
+ return setSharpWarnSink;
359
+ }
360
+ });
@@ -2,10 +2,10 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-BAbvf81B.js");
6
- const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
7
- let node_fs = require("node:fs");
5
+ const require_dist = require("../dist-6ZJCV0jY.js");
6
+ const require_lazy_sharp = require("../lazy-sharp-DvwPafeP.js");
8
7
  let node_path = require("node:path");
8
+ let node_fs = require("node:fs");
9
9
  //#region src/motion-wasm/wasm-motion-detector.ts
10
10
  /**
11
11
  * WasmMotionDetector — loads the AssemblyScript WASM module and provides
@@ -1,7 +1,7 @@
1
- import { H as motionDetectionCapability, R as evaluateZoneRules, ft as hydrateSchema, st as BaseAddon, ut as DeviceType } from "../dist-Dpr92hOt.mjs";
2
- import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
3
- import { readFileSync } from "node:fs";
1
+ import { H as motionDetectionCapability, R as evaluateZoneRules, ft as hydrateSchema, st as BaseAddon, ut as DeviceType } from "../dist-T_8jMw7d.mjs";
2
+ import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
4
3
  import { join } from "node:path";
4
+ import { readFileSync } from "node:fs";
5
5
  //#region src/motion-wasm/wasm-motion-detector.ts
6
6
  /**
7
7
  * WasmMotionDetector — loads the AssemblyScript WASM module and provides
@@ -2,20 +2,19 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-BAbvf81B.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-BhKoybot.js");
8
- const require_worker_protocol = require("../worker-protocol-BFq9AIDa.js");
5
+ const require_dist = require("../dist-6ZJCV0jY.js");
6
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Cjof8vA1.js");
7
+ const require_worker_protocol = require("../worker-protocol-B0cbTXyw.js");
9
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
9
+ const require_lazy_sharp = require("../lazy-sharp-DvwPafeP.js");
11
10
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
12
11
  let sharp = require("sharp");
13
- sharp = require_chunk.__toESM(sharp);
12
+ sharp = require_dist.__toESM(sharp);
14
13
  let node_child_process = require("node:child_process");
15
14
  let node_url = require("node:url");
16
15
  let node_perf_hooks = require("node:perf_hooks");
17
16
  let node_os = require("node:os");
18
- node_os = require_chunk.__toESM(node_os);
17
+ node_os = require_dist.__toESM(node_os);
19
18
  //#region src/session-decode/fork-decode-worker.ts
20
19
  /** Absolute path to the built decode-worker child entry (a vite build entry). */
21
20
  var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
@@ -3836,6 +3835,18 @@ function isUsable(bbox) {
3836
3835
  }
3837
3836
  //#endregion
3838
3837
  //#region src/pipeline-runner/index.ts
3838
+ /**
3839
+ * Bound on the frames whose cutter is bound but whose result has not come back.
3840
+ * A few per camera in flight; the ceiling only catches results that never
3841
+ * arrive (shed, inference timeout, detach mid-flight).
3842
+ */
3843
+ var MAX_BOUND_FRAME_CUTTERS = 256;
3844
+ /** How often a camera reports its undispatched cuts. */
3845
+ var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
3846
+ /** The per-frame binding key — the same identity the retention handle carries. */
3847
+ function frameCutterKey(handle) {
3848
+ return `${handle.shmId}#${handle.slot}#${handle.seq}`;
3849
+ }
3839
3850
  var DEFAULT_CONFIG = {
3840
3851
  maxQueueDepth: 30,
3841
3852
  maxConcurrentInferences: 16,
@@ -4151,6 +4162,20 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
4151
4162
  */
4152
4163
  nativeCropRegistry = new NativeCropRegistry();
4153
4164
  /**
4165
+ * Per-frame cutter bindings — handle key → the requester that can cut this
4166
+ * frame's subject tiles and end its hold.
4167
+ *
4168
+ * Deliberately NOT `nativeCropRegistry`. That one is a node-wide LRU capped
4169
+ * at 256 and shared by every camera to serve LATE cross-process crops; on the
4170
+ * live hub it sits pinned at its cap, so an entry's lifetime depends on how
4171
+ * busy the OTHER cameras are. Ending a frame's hold is a per-frame lifecycle
4172
+ * event and cannot be allowed to depend on that.
4173
+ */
4174
+ frameCutters = /* @__PURE__ */ new Map();
4175
+ /** Per-camera tally of cuts that could not be dispatched, and why. */
4176
+ tileCutDrops = /* @__PURE__ */ new Map();
4177
+ tileCutDropLoggedAt = /* @__PURE__ */ new Map();
4178
+ /**
4154
4179
  * Node-level RAM store of retained rgb detection frames, keyed by the
4155
4180
  * `FrameHandle` that rides the inference-result event (shm-ring removal
4156
4181
  * 2026-07-16 — replaces the `csr.` shm retention ring). Backs the
@@ -5158,23 +5183,80 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5158
5183
  * by an encode.
5159
5184
  */
5160
5185
  cutSubjectTiles(deviceId, result, handle) {
5161
- if (!handle) return;
5162
- const found = this.nativeCropRegistry.get(handle);
5163
- if (!found) return;
5186
+ const outcome = this.dispatchSubjectTiles(result, handle);
5187
+ if (outcome === "sent") return;
5188
+ const tally = this.tileCutDrops.get(deviceId) ?? {
5189
+ handleless: 0,
5190
+ unbound: 0,
5191
+ threw: 0
5192
+ };
5193
+ tally[outcome] += 1;
5194
+ this.tileCutDrops.set(deviceId, tally);
5195
+ const now = Date.now();
5196
+ if (now - (this.tileCutDropLoggedAt.get(deviceId) ?? 0) < TILE_CUT_DROP_LOG_INTERVAL_MS) return;
5197
+ this.tileCutDropLoggedAt.set(deviceId, now);
5198
+ this.ctx.logger.warn("subject tile cut NOT dispatched — the frame hold will not be released", {
5199
+ tags: { deviceId },
5200
+ meta: {
5201
+ ...tally,
5202
+ lastReason: outcome,
5203
+ boundFrames: this.frameCutters.size
5204
+ }
5205
+ });
5206
+ }
5207
+ /**
5208
+ * Send the cut for one finished frame. Returns what happened, so the caller
5209
+ * can account for it.
5210
+ *
5211
+ * The cutter is looked up in {@link frameCutters} — the addon's OWN per-frame
5212
+ * binding — and NOT in `nativeCropRegistry`. That distinction is the bug this
5213
+ * method exists to not have again: the registry is a node-wide LRU capped at
5214
+ * 256 entries, shared by every camera, and it exists to serve LATE
5215
+ * cross-process crops. Measured on the live hub during the incident it sits
5216
+ * PINNED at `registrySize=256 registryCap=256`, so an entry's lifetime is a
5217
+ * function of how many other cameras are busy. Ending a frame's hold is a
5218
+ * per-frame lifecycle event and must never be subject to another camera's
5219
+ * eviction pressure.
5220
+ */
5221
+ dispatchSubjectTiles(result, handle) {
5222
+ if (!handle) return "handleless";
5223
+ const key = frameCutterKey(handle);
5224
+ const cutter = this.frameCutters.get(key);
5225
+ this.frameCutters.delete(key);
5226
+ if (!cutter) return "unbound";
5164
5227
  const boxes = planTileCuts(result.detections ?? [], {
5165
5228
  width: result.width,
5166
5229
  height: result.height
5167
5230
  });
5168
5231
  try {
5169
- found.request.cutTiles(boxes);
5170
- } catch (err) {
5171
- this.ctx.logger.debug("subject tile cut could not be dispatched", {
5172
- tags: { deviceId },
5173
- meta: {
5174
- subjects: boxes.length,
5175
- error: err instanceof Error ? err.message : String(err)
5176
- }
5177
- });
5232
+ cutter.cutTiles(boxes);
5233
+ return "sent";
5234
+ } catch {
5235
+ return "threw";
5236
+ }
5237
+ }
5238
+ /**
5239
+ * Bind a frame's cutter at DELIVERY, so its result can end its hold.
5240
+ *
5241
+ * Bounded by {@link MAX_BOUND_FRAME_CUTTERS}: a frame whose result never
5242
+ * comes back (a shed, a timed-out inference, a camera detaching mid-flight)
5243
+ * would otherwise leave its binding forever. The oldest is dropped and its
5244
+ * hold released directly — the worker's own frame bound would have reclaimed
5245
+ * it anyway, but releasing here is what keeps `holdOverflow` meaning "the
5246
+ * runner stopped answering" rather than "the runner forgot".
5247
+ */
5248
+ bindFrameCutter(handle, cutter) {
5249
+ const key = frameCutterKey(handle);
5250
+ this.frameCutters.delete(key);
5251
+ this.frameCutters.set(key, cutter);
5252
+ while (this.frameCutters.size > MAX_BOUND_FRAME_CUTTERS) {
5253
+ const oldest = this.frameCutters.keys().next().value;
5254
+ if (oldest === void 0) break;
5255
+ const stale = this.frameCutters.get(oldest);
5256
+ this.frameCutters.delete(oldest);
5257
+ try {
5258
+ stale?.release();
5259
+ } catch {}
5178
5260
  }
5179
5261
  }
5180
5262
  /**
@@ -5973,7 +6055,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5973
6055
  reservation = null;
5974
6056
  if (!current) return;
5975
6057
  const handle = current.detection ? retainedFrames?.write(config.deviceId, frame) ?? void 0 : void 0;
5976
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
6058
+ if (handle && nativeCrop) {
6059
+ this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
6060
+ this.bindFrameCutter(handle, nativeCrop);
6061
+ }
5977
6062
  if (this.enqueueSharedDetectionFrame(config.deviceId, frame, handle, current)) nativeCrop?.markInferred();
5978
6063
  }
5979
6064
  });
@@ -6497,7 +6582,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6497
6582
  onDecodedFrame: (frame, nativeCrop) => {
6498
6583
  if (settled) return;
6499
6584
  const handle = retainedFrames?.write(config.deviceId, frame) ?? void 0;
6500
- if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
6585
+ if (handle && nativeCrop) {
6586
+ this.nativeCropRegistry.register(handle, nativeCrop, config.deviceId);
6587
+ this.bindFrameCutter(handle, nativeCrop);
6588
+ }
6501
6589
  if (runner.enqueueOccupancyFrame(deviceId, frame, handle)) nativeCrop?.markInferred();
6502
6590
  count++;
6503
6591
  if (count >= frames) {