@camstack/addon-pipeline 1.2.219 → 1.2.221

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 (40) hide show
  1. package/dist/audio-analyzer/index.js +661 -7
  2. package/dist/audio-analyzer/index.mjs +658 -5
  3. package/dist/chunk-emK7D4bc.js +35 -0
  4. package/dist/{default-detection-model-d7-Q_LMo.js → default-detection-model-B1NT1A38.js} +1 -1
  5. package/dist/{default-detection-model-Dsk1a5pY.mjs → default-detection-model-DkVFgubU.mjs} +1 -1
  6. package/dist/detection-pipeline/index.js +10 -9
  7. package/dist/detection-pipeline/index.mjs +4 -4
  8. package/dist/{dist-wtNhtVVo.js → dist-ATDfcxgL.js} +282 -39
  9. package/dist/{dist-CKK0CBUW.mjs → dist-CEFC3PkP.mjs} +265 -5
  10. package/dist/{lazy-sharp-zoQ07I43.js → lazy-sharp-BuwKlQLR.js} +2 -2
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-BduNi21Z.js → node-C5jnTgpv.js} +4 -3
  14. package/dist/{node-CrNBWFbA.mjs → node-DR3fHqUq.mjs} +1 -1
  15. package/dist/pipeline-runner/index.js +222 -157
  16. package/dist/pipeline-runner/index.mjs +218 -154
  17. package/dist/{process-memory-Dwl0MFPH.js → process-memory-DQbCPnMA.js} +3 -2
  18. package/dist/{process-memory-BRQOtDN1.mjs → process-memory-DqYj5IAb.mjs} +1 -1
  19. package/dist/recorder/index.js +344 -127
  20. package/dist/recorder/index.mjs +342 -126
  21. package/dist/{segment-demux-js-D2o9ShNL.mjs → segment-demux-js-CddDwD69.mjs} +1 -1
  22. package/dist/{segment-demux-js-CFOU3biG.js → segment-demux-js-o9KBAz9p.js} +4 -3
  23. package/dist/session-decode/decode-worker-child.js +155 -74
  24. package/dist/session-decode/decode-worker-child.mjs +154 -73
  25. package/dist/stream-broker/_stub.js +2 -2
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN_IoShh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs} +3 -3
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CyMYDKfM.mjs +26 -0
  28. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CKUGpDns.mjs} +1 -1
  29. package/dist/stream-broker/demux-worker-child.js +1 -1
  30. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  31. package/dist/stream-broker/{hostInit-n7c9pNlt.mjs → hostInit-C-VdcJS2.mjs} +3 -3
  32. package/dist/stream-broker/index.js +231 -112
  33. package/dist/stream-broker/index.mjs +224 -106
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/dist/worker-protocol-B2MfQLlu.js +184 -0
  36. package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
  37. package/package.json +1 -1
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7hqrrd88.mjs +0 -26
  39. package/dist/worker-protocol-Cx3Nqreq.mjs +0 -264
  40. package/dist/worker-protocol-iWTFnnUM.js +0 -299
@@ -2,19 +2,20 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-wtNhtVVo.js");
6
- const require_node = require("../node-BduNi21Z.js");
7
- const require_default_detection_model = require("../default-detection-model-d7-Q_LMo.js");
8
- const require_worker_protocol = require("../worker-protocol-iWTFnnUM.js");
5
+ const require_chunk = require("../chunk-emK7D4bc.js");
6
+ const require_dist = require("../dist-ATDfcxgL.js");
7
+ const require_node = require("../node-C5jnTgpv.js");
8
+ const require_default_detection_model = require("../default-detection-model-B1NT1A38.js");
9
+ const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
9
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
- const require_lazy_sharp = require("../lazy-sharp-zoQ07I43.js");
11
+ const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
11
12
  const require_restream_intent = require("../restream-intent-B1Difcnq.js");
12
13
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
13
14
  let node_os = require("node:os");
14
- node_os = require_dist.__toESM(node_os);
15
+ node_os = require_chunk.__toESM(node_os);
15
16
  let node_child_process = require("node:child_process");
16
17
  let sharp = require("sharp");
17
- sharp = require_dist.__toESM(sharp);
18
+ sharp = require_chunk.__toESM(sharp);
18
19
  let node_url = require("node:url");
19
20
  let node_perf_hooks = require("node:perf_hooks");
20
21
  let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
@@ -73,26 +74,23 @@ function wrapChild(child, onExit) {
73
74
  * signature (it forwards the same source in the subsequent `start` message);
74
75
  * the child entry itself is source-independent, so it is not consumed here.
75
76
  *
76
- * `leaseOverride` carries the operator's cluster-wide native-lease settings
77
- * (`pipeline-orchestrator` globals, read by `native-lease-settings-source.ts`)
78
- * into the child as environment variables — the child constructs its lease
79
- * store and demand gate at module load, before the first `start` message, so
80
- * the values have to be present at spawn. Only the knobs the operator actually
81
- * SET are emitted, which is what keeps the pre-settings env override
82
- * (`CAMSTACK_SESSION_NATIVE_LEASE_*`) working for the untouched ones; see
83
- * `native-lease-config.ts` for the full precedence.
77
+ * The fork carries NO configuration. Until D453 it spread a set of
78
+ * `*_SETTING` env vars in, because the child resolved its native-lease knobs
79
+ * at module load; the knobs now travel on the `start` message
80
+ * (`WorkerRequest.lease`), resolved in this process. That is what makes a
81
+ * decode worker forkable BEFORE anyone knows which camera it will serve — a
82
+ * child configured at spawn is a child bound to the configuration it was born
83
+ * with — and it is what let the child bundle drop the Zod schemas that cost
84
+ * every fork ~170 ms and ~100 MB of heap (D453).
84
85
  *
85
86
  * The inherited environment is SPREAD, never replaced: this child resolves
86
87
  * node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
87
88
  * agents, so a replaced env would silently break decoding off the hub.
88
89
  */
89
- function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
90
+ function forkDecodeWorker(source, costRegistry) {
90
91
  const child = (0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
91
92
  serialization: "advanced",
92
- env: {
93
- ...process.env,
94
- ...require_worker_protocol.nativeLeaseSettingEnv(leaseOverride)
95
- }
93
+ env: { ...process.env }
96
94
  });
97
95
  const costClaim = costRegistry?.claim({
98
96
  role: "decode",
@@ -103,6 +101,145 @@ function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
103
101
  return wrapChild(child, () => costClaim.release());
104
102
  }
105
103
  //#endregion
104
+ //#region src/session-decode/native-lease-config.ts
105
+ /**
106
+ * Resolution of the native-frame hold / tile knobs for one decode session —
107
+ * PARENT side. The result travels to the decode worker on its `start` message
108
+ * (`native-lease-knobs.ts` is the wire half, and the only half the child sees).
109
+ *
110
+ * ## Where the values come from
111
+ *
112
+ * The knobs are cluster-wide `pipeline-orchestrator` settings
113
+ * (`@camstack/types` → `pipeline/native-lease.ts`). The pipeline-runner reads
114
+ * them over the hub-routed `addon-settings` cap
115
+ * (`pipeline-runner/native-lease-settings-source.ts`) and resolves them HERE,
116
+ * once per session, at the moment the session starts — so a changed setting
117
+ * reaches the next session and leaves running ones alone, exactly as before.
118
+ *
119
+ * Until D453 the resolution ran inside the CHILD, from `*_SETTING` env vars the
120
+ * parent wrote at fork time. That bound a worker to the knobs it was forked
121
+ * with (a pre-forked worker cannot be), and it dragged the Zod schemas — the
122
+ * whole `@camstack/types` index — into the child bundle: ~130 ms and ~140 MB
123
+ * per fork, measured. Moving the resolution up one process removed both.
124
+ *
125
+ * ## Precedence — setting > env > default
126
+ *
127
+ * 1. **setting** — the operator set the knob in the orchestrator's global
128
+ * settings. Wins, because a value someone deliberately chose in the UI must
129
+ * not be silently overruled by a shell variable nobody can see from there.
130
+ * 2. **env** — {@link NATIVE_LEASE_ENV_VARS}, the pre-settings mechanism, kept
131
+ * as an emergency / dev override. It applies only while the corresponding
132
+ * setting is UNSET. "Unset" includes a knob sitting AT its shipped default:
133
+ * `addon-settings.getGlobalSettings` hydrates an unstored field with the
134
+ * schema default and offers no way to tell the two apart, so treating the
135
+ * default as a choice would have retired every env var on the day this
136
+ * shipped. Move the slider off the default and the setting takes over; move
137
+ * it back and the env var returns. See `readNativeLeaseOverride`.
138
+ * 3. **default** — {@link DEFAULT_NATIVE_LEASE_SETTINGS}, byte-for-byte the
139
+ * constants the worker shipped with.
140
+ *
141
+ * A value that fails its bounds at ANY level falls through to the next one
142
+ * rather than being clamped: clamping turns a typo into a number nobody chose,
143
+ * and this one governs how much RAM a busy camera holds.
144
+ *
145
+ * The GPU surface ring (`CAMSTACK_SESSION_NATIVE_CROP_RING`) is deliberately
146
+ * NOT here — see its docblock in `decode-worker-child.ts` for why it stays
147
+ * env-only.
148
+ */
149
+ /** Emergency / dev override env vars (pre-settings; lowest precedence but the default). */
150
+ var NATIVE_LEASE_ENV_VARS = {
151
+ holdFrames: "CAMSTACK_SESSION_NATIVE_LEASE_HOLD_FRAMES",
152
+ budgetMb: "CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB",
153
+ activityMs: "CAMSTACK_SESSION_NATIVE_LEASE_ACTIVITY_MS",
154
+ admission: "CAMSTACK_SESSION_NATIVE_LEASE_ADMISSION",
155
+ tileBudgetMb: "CAMSTACK_SESSION_NATIVE_TILE_BUDGET_MB",
156
+ sceneBudgetMb: "CAMSTACK_SESSION_NATIVE_SCENE_BUDGET_MB"
157
+ };
158
+ /**
159
+ * Validate one candidate through the knob's own Zod bound. `null` for absent,
160
+ * non-numeric, or out-of-bounds input — the caller then falls through to the
161
+ * next precedence level.
162
+ */
163
+ function parseKnob(knob, raw) {
164
+ if (raw === void 0) return null;
165
+ if (typeof raw === "string" && raw.trim() === "") return null;
166
+ const parsed = require_dist.NativeLeaseSettingsSchema.shape[knob].safeParse(Number(raw));
167
+ return parsed.success ? parsed.data : null;
168
+ }
169
+ /** {@link parseKnob} for the one knob whose value is a word, not a number. */
170
+ function parseAdmission(raw) {
171
+ if (raw === void 0) return null;
172
+ const parsed = require_dist.NativeLeaseAdmissionSchema.safeParse(raw.trim());
173
+ return parsed.success ? parsed.data : null;
174
+ }
175
+ /** Resolve one NUMERIC knob across the three precedence levels. */
176
+ function resolveKnob(knob, env, override) {
177
+ const fromSetting = parseKnob(knob, override[knob]);
178
+ if (fromSetting !== null) return {
179
+ value: fromSetting,
180
+ source: "setting"
181
+ };
182
+ const fromEnv = parseKnob(knob, env[NATIVE_LEASE_ENV_VARS[knob]]);
183
+ if (fromEnv !== null) return {
184
+ value: fromEnv,
185
+ source: "env"
186
+ };
187
+ return {
188
+ value: require_dist.DEFAULT_NATIVE_LEASE_SETTINGS[knob],
189
+ source: "default"
190
+ };
191
+ }
192
+ /** {@link resolveKnob} for `admission`. Same precedence, different parser. */
193
+ function resolveAdmission(env, override) {
194
+ const fromSetting = parseAdmission(override.admission);
195
+ if (fromSetting !== null) return {
196
+ value: fromSetting,
197
+ source: "setting"
198
+ };
199
+ const fromEnv = parseAdmission(env[NATIVE_LEASE_ENV_VARS.admission]);
200
+ if (fromEnv !== null) return {
201
+ value: fromEnv,
202
+ source: "env"
203
+ };
204
+ return {
205
+ value: require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.admission,
206
+ source: "default"
207
+ };
208
+ }
209
+ /**
210
+ * Resolve every lease knob for one session. Pure.
211
+ *
212
+ * `override` carries ONLY the knobs the operator actually SET (see
213
+ * `native-lease-settings-source.ts`); an empty object leaves the env override
214
+ * fully functional, which is the whole point of the distinction.
215
+ */
216
+ function resolveNativeLeaseKnobs(env, override) {
217
+ const holdFrames = resolveKnob("holdFrames", env, override);
218
+ const budgetMb = resolveKnob("budgetMb", env, override);
219
+ const activityMs = resolveKnob("activityMs", env, override);
220
+ const admission = resolveAdmission(env, override);
221
+ const tileBudgetMb = resolveKnob("tileBudgetMb", env, override);
222
+ const sceneBudgetMb = resolveKnob("sceneBudgetMb", env, override);
223
+ return {
224
+ values: {
225
+ holdFrames: holdFrames.value,
226
+ budgetMb: budgetMb.value,
227
+ activityMs: activityMs.value,
228
+ admission: admission.value,
229
+ tileBudgetMb: tileBudgetMb.value,
230
+ sceneBudgetMb: sceneBudgetMb.value
231
+ },
232
+ sources: {
233
+ holdFrames: holdFrames.source,
234
+ budgetMb: budgetMb.source,
235
+ activityMs: activityMs.source,
236
+ admission: admission.source,
237
+ tileBudgetMb: tileBudgetMb.source,
238
+ sceneBudgetMb: sceneBudgetMb.source
239
+ }
240
+ };
241
+ }
242
+ //#endregion
106
243
  //#region src/session-decode/guarded-session-decode.ts
107
244
  /**
108
245
  * 2s → 5s → 15s → 30s, then 30s forever.
@@ -1349,11 +1486,12 @@ var SessionDecodeSession = class {
1349
1486
  this.child.on("message", (m) => this.handleMessage(m));
1350
1487
  this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1351
1488
  }
1352
- start(source, opts) {
1489
+ start(source, opts, lease) {
1353
1490
  this.trySend({
1354
1491
  kind: "start",
1355
1492
  source,
1356
- opts
1493
+ opts,
1494
+ lease
1357
1495
  });
1358
1496
  }
1359
1497
  /**
@@ -1521,6 +1659,7 @@ var SessionDecodeSession = class {
1521
1659
  ...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
1522
1660
  });
1523
1661
  return;
1662
+ case "ready": return;
1524
1663
  case "error":
1525
1664
  this.settleError(raw.message, raw.frameId);
1526
1665
  return;
@@ -1772,7 +1911,7 @@ function createSessionDecodeCoordinator(deps) {
1772
1911
  const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
1773
1912
  return { async *generateVideoFrames(source, opts) {
1774
1913
  const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
1775
- session.start(source, opts);
1914
+ session.start(source, opts, deps.lease());
1776
1915
  try {
1777
1916
  while (true) {
1778
1917
  const outcome = await session.pullNext();
@@ -1839,6 +1978,7 @@ function startSessionDecodePump(deps) {
1839
1978
  let dialTiming = null;
1840
1979
  const iterator = createSessionDecodeCoordinator({
1841
1980
  fork: deps.fork,
1981
+ lease: deps.lease,
1842
1982
  logger: deps.logger,
1843
1983
  onDialTiming: (timing) => {
1844
1984
  dialTiming = timing;
@@ -6555,7 +6695,6 @@ function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
6555
6695
  if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
6556
6696
  return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(require_default_detection_model.localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
6557
6697
  }
6558
- var DETECTION_KEEPWARM_MS = 15e3;
6559
6698
  /**
6560
6699
  * Occupancy-burst safety timeout (both the dial path and the analyzer-tap
6561
6700
  * path — Task 10). Bounds worst-case burst latency when frames stop
@@ -6582,6 +6721,31 @@ var DETECTION_REDIAL_DELAY_MS = 2e3;
6582
6721
  */
6583
6722
  var DETECTION_REDIAL_GIVEUP_WINDOW_MS = 6e4;
6584
6723
  /**
6724
+ * ## There is no decode keep-warm any more — D453 removed it
6725
+ *
6726
+ * A `WarmDetachedDecode` used to hold a detection worker alive for 15 s across
6727
+ * a detach, hoping the next motion event would adopt it. It did not park: it
6728
+ * kept its broker restream lease and kept DECODING, because the child's loop is
6729
+ * `for await (... decoder.frames(demuxer.packets(...)))`, driven by the demuxer
6730
+ * and not by the parent's `pull` — `minIntervalMs` throttles only inside
6731
+ * `FrameSlot.publish`, after the decode. `framesDecoded` advanced in 122 of 122
6732
+ * park windows at full source rate, and a decode-worker child costs 40-80% of a
6733
+ * core.
6734
+ *
6735
+ * So the park cost about what running costs, and it bought a re-fork back in
6736
+ * 12% of cases: on this fleet, 272 of 440 park windows expired having adopted
6737
+ * nothing, while the two decode-worker children stood at 319% of the 522% of
6738
+ * CPU CamStack drew on the host. D282 put the arithmetic at ~25x net-negative
6739
+ * against the forks it avoided, and D453 made the fork itself ~150 ms and
6740
+ * ~100 MB cheaper, which only widens it.
6741
+ *
6742
+ * Do not reintroduce a keep-warm shaped like this one, and do not "fix" it with
6743
+ * a longer TTL: the measured p50 gap between two sessions of one camera is
6744
+ * 61 s, so a longer window scales the waste and not the hit rate. If a warm
6745
+ * process is wanted, it must be a process that holds NO stream and decodes
6746
+ * NOTHING — see D453 for what that costs and why it was not taken either.
6747
+ */
6748
+ /**
6585
6749
  * Pure decision helper: returns `true` when a dynamic frame-diff analyzer
6586
6750
  * subscription should be opened for an onboard-motion camera.
6587
6751
  *
@@ -6907,12 +7071,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
6907
7071
  retention = null;
6908
7072
  attached = /* @__PURE__ */ new Map();
6909
7073
  /**
6910
- * Detection decode workers parked across a detach, keyed by deviceId — see
6911
- * {@link WarmDetachedDecode}. Bounded by construction: at most one entry per
6912
- * camera, each self-expiring after {@link DETECTION_KEEPWARM_MS}.
6913
- */
6914
- warmDetachedDetection = /* @__PURE__ */ new Map();
6915
- /**
6916
7074
  * Detail step state parked across a SOFT detach, keyed by deviceId — see
6917
7075
  * `detail-park.ts` for the fault this closes. Bounded by construction: at
6918
7076
  * most one entry per camera, each self-expiring after
@@ -7271,14 +7429,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7271
7429
  this.runner = null;
7272
7430
  }
7273
7431
  for (const attachment of this.attached.values()) {
7274
- if (attachment.detectionKeepWarmTimer !== null) {
7275
- clearTimeout(attachment.detectionKeepWarmTimer);
7276
- attachment.detectionKeepWarmTimer = null;
7277
- }
7278
7432
  attachment.motionUnsubscribe?.();
7279
7433
  attachment.detectionUnsubscribe?.();
7280
7434
  }
7281
- for (const deviceId of [...this.warmDetachedDetection.keys()]) this.dropWarmDetectionDecode(deviceId, "shutdown");
7282
7435
  for (const deviceId of [...this.parkedDetail.keys()]) this.dropParkedDetail(deviceId);
7283
7436
  this.attached.clear();
7284
7437
  this.lastMotionFrameAt.clear();
@@ -7989,7 +8142,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7989
8142
  config,
7990
8143
  motionUnsubscribe: null,
7991
8144
  detectionUnsubscribe: null,
7992
- detectionKeepWarmTimer: null,
7993
8145
  detailSteps: deriveDetailSteps(config.steps)
7994
8146
  };
7995
8147
  this.attached.set(config.deviceId, attachment);
@@ -8645,12 +8797,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8645
8797
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
8646
8798
  } });
8647
8799
  /**
8648
- * Fork a decode worker carrying the operator's lease knobs. Used in place of
8649
- * a bare `forkDecodeWorker` reference at every session-decode spawn site: the
8650
- * knobs are read at spawn, so a changed setting reaches NEW sessions and
8800
+ * Fork a decode worker. Used in place of a bare `forkDecodeWorker` reference
8801
+ * at every session-decode spawn site so the cost registry is never forgotten.
8802
+ *
8803
+ * The fork carries NO configuration: since D453 the lease knobs travel on the
8804
+ * `start` message ({@link resolveSessionLeaseKnobs}), not in the child's
8805
+ * environment. A worker is therefore forkable before anyone knows which
8806
+ * camera it will serve.
8807
+ */
8808
+ forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.costRegistry);
8809
+ /**
8810
+ * The lease knobs for the session that is about to start — `setting > env >
8811
+ * default`, resolved HERE because this process is the one that carries the
8812
+ * Zod schemas (the child no longer does; D453).
8813
+ *
8814
+ * Read per session, so a changed operator setting reaches NEW sessions and
8651
8815
  * leaves running ones alone (a live session would have to be re-dialled).
8652
8816
  */
8653
- forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current(), this.costRegistry);
8817
+ resolveSessionLeaseKnobs = () => resolveNativeLeaseKnobs(process.env, this.nativeLease.current());
8654
8818
  /**
8655
8819
  * Refresh the lease knobs before a decode worker is spawned. Awaited on the
8656
8820
  * `acquire` leg of `startGuardedSessionDecode` (which always completes before
@@ -8855,19 +9019,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8855
9019
  */
8856
9020
  handleDetectionDecodeEnded(deviceId) {
8857
9021
  const attachment = this.attached.get(deviceId);
8858
- if (!attachment) {
8859
- this.dropWarmDetectionDecode(deviceId, "worker-died-while-parked");
8860
- return;
8861
- }
9022
+ if (!attachment) return;
8862
9023
  const log = this.ctx.logger.withTags({ deviceId });
8863
9024
  attachment.detectionUnsubscribe?.();
8864
9025
  attachment.detectionUnsubscribe = null;
8865
- if (attachment.detectionKeepWarmTimer !== null) {
8866
- clearTimeout(attachment.detectionKeepWarmTimer);
8867
- attachment.detectionKeepWarmTimer = null;
8868
- log.warn("detection decode worker died while kept warm — next open re-forks cold");
8869
- return;
8870
- }
8871
9026
  const now = Date.now();
8872
9027
  const last = this.lastDecodeUnexpectedEndAt.get(deviceId) ?? 0;
8873
9028
  this.lastDecodeUnexpectedEndAt.set(deviceId, now);
@@ -8892,13 +9047,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8892
9047
  /**
8893
9048
  * Tear a camera down.
8894
9049
  *
8895
- * `keepDecodeWarm` routes the DETECTION decode through the same keep-warm
8896
- * grace the phase gate uses instead of killing it — see
8897
- * {@link WarmDetachedDecode}. Set only by the {@link detachCamera} cap entry
8898
- * point (the orchestrator's motion-cooldown teardown); every internal detach
8899
- * — an attach replacing a stale config, a decode that died twice, shutdown —
8900
- * is a hard one, because in each of those cases the worker is either wrong or
8901
- * already gone.
9050
+ * `softDetach` is the {@link detachCamera} cap entry point (the
9051
+ * orchestrator's motion-cooldown teardown). It keeps the DETAIL step tree
9052
+ * parked — a config slice, no process, no CPU — so a track outliving this
9053
+ * session can still resolve its enrichment steps. It does NOT keep the decode
9054
+ * worker: since D453 every detach releases it, because a held worker went on
9055
+ * decoding at full rate for the whole window (see the note above
9056
+ * {@link AttachedCamera}). Every internal detach — an attach replacing a
9057
+ * stale config, a decode that died twice, shutdown — is hard on both counts.
8902
9058
  */
8903
9059
  /**
8904
9060
  * Park the detaching camera's detail-relevant config slice for the grace
@@ -8945,18 +9101,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8945
9101
  }
8946
9102
  return parked.state;
8947
9103
  }
8948
- detachInternal(deviceId, keepDecodeWarm = false) {
9104
+ detachInternal(deviceId, softDetach = false) {
8949
9105
  const attachment = this.attached.get(deviceId);
8950
9106
  if (!attachment) return;
8951
9107
  this.clearOnboardAnalyzer(deviceId);
8952
- if (attachment.detectionKeepWarmTimer !== null) {
8953
- clearTimeout(attachment.detectionKeepWarmTimer);
8954
- attachment.detectionKeepWarmTimer = null;
8955
- }
8956
9108
  attachment.motionUnsubscribe?.();
8957
- if (keepDecodeWarm && attachment.detectionUnsubscribe !== null) this.parkWarmDetectionDecode(deviceId, attachment.config, attachment.detectionUnsubscribe);
8958
- else attachment.detectionUnsubscribe?.();
8959
- if (keepDecodeWarm) this.parkDetailState(deviceId, attachment.config);
9109
+ attachment.detectionUnsubscribe?.();
9110
+ if (softDetach) this.parkDetailState(deviceId, attachment.config);
8960
9111
  else this.dropParkedDetail(deviceId);
8961
9112
  this.lastMotionFrameAt.delete(deviceId);
8962
9113
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
@@ -8981,83 +9132,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8981
9132
  this.runner?.unregisterCamera(deviceId);
8982
9133
  this.ctx?.logger.info("detachCamera", {
8983
9134
  tags: { deviceId },
8984
- meta: { decodeKeptWarm: this.warmDetachedDetection.has(deviceId) }
8985
- });
8986
- }
8987
- /**
8988
- * The decode-relevant identity of a camera's detection decode. Two configs
8989
- * that agree on all of it can share one worker; anything else must re-fork.
8990
- * Kept in ONE place so a future decode-shaping option cannot be added to the
8991
- * pump without the adoption check noticing.
8992
- */
8993
- detectionDecodeIdentity(config) {
8994
- return {
8995
- streamId: config.detectionStreamId,
8996
- fps: config.detectionFps,
8997
- hostname: resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]),
8998
- ownerNodeId: resolveSessionDecodeOwnerNodeId(config)
8999
- };
9000
- }
9001
- /**
9002
- * Hold a detached camera's detection decode worker for
9003
- * {@link DETECTION_KEEPWARM_MS} so the next motion event reuses it.
9004
- * Replacing an existing park releases the older worker first — two parked
9005
- * workers for one camera would be two live decodes and two broker leases.
9006
- */
9007
- parkWarmDetectionDecode(deviceId, config, unsubscribe) {
9008
- this.dropWarmDetectionDecode(deviceId, "replaced");
9009
- const timer = setTimeout(() => {
9010
- this.warmDetachedDetection.delete(deviceId);
9011
- unsubscribe();
9012
- this.ctx?.logger.info("session-decode: warm detection decode expired — worker released", {
9013
- tags: { deviceId },
9014
- meta: { keepWarmMs: DETECTION_KEEPWARM_MS }
9015
- });
9016
- }, DETECTION_KEEPWARM_MS);
9017
- timer.unref?.();
9018
- this.warmDetachedDetection.set(deviceId, {
9019
- unsubscribe,
9020
- timer,
9021
- ...this.detectionDecodeIdentity(config)
9022
- });
9023
- }
9024
- /**
9025
- * Adopt a parked decode worker for a camera that is attaching again, or
9026
- * `null` when there is nothing to adopt.
9027
- *
9028
- * A parked worker whose identity no longer matches is DROPPED here rather
9029
- * than left to expire: the caller is about to fork a replacement, and letting
9030
- * both run would double this camera's decode cost for the rest of the window.
9031
- */
9032
- adoptWarmDetectionDecode(deviceId, config) {
9033
- const warm = this.warmDetachedDetection.get(deviceId);
9034
- if (!warm) return null;
9035
- const wanted = this.detectionDecodeIdentity(config);
9036
- if (warm.streamId !== wanted.streamId || warm.fps !== wanted.fps || warm.hostname !== wanted.hostname || warm.ownerNodeId !== wanted.ownerNodeId) {
9037
- this.dropWarmDetectionDecode(deviceId, "config-changed");
9038
- return null;
9039
- }
9040
- clearTimeout(warm.timer);
9041
- this.warmDetachedDetection.delete(deviceId);
9042
- this.ctx?.logger.info("session-decode: reusing the warm detection decode worker (no re-fork)", {
9043
- tags: { deviceId },
9044
- meta: {
9045
- streamId: warm.streamId,
9046
- fps: warm.fps
9047
- }
9048
- });
9049
- return warm.unsubscribe;
9050
- }
9051
- /** Release a parked worker now. `reason` names why in the log. */
9052
- dropWarmDetectionDecode(deviceId, reason) {
9053
- const warm = this.warmDetachedDetection.get(deviceId);
9054
- if (!warm) return;
9055
- clearTimeout(warm.timer);
9056
- this.warmDetachedDetection.delete(deviceId);
9057
- warm.unsubscribe();
9058
- this.ctx?.logger.info("session-decode: warm detection decode released", {
9059
- tags: { deviceId },
9060
- meta: { reason }
9135
+ meta: { detailParked: this.parkedDetail.has(deviceId) }
9061
9136
  });
9062
9137
  }
9063
9138
  /**
@@ -9368,6 +9443,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9368
9443
  let reservation = null;
9369
9444
  return startSessionDecodePump({
9370
9445
  fork: this.forkDecodeWorkerWithLease,
9446
+ lease: this.resolveSessionLeaseKnobs,
9371
9447
  logger: log,
9372
9448
  source,
9373
9449
  opts: {
@@ -9526,6 +9602,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9526
9602
  } });
9527
9603
  return startSessionDecodePump({
9528
9604
  fork: this.forkDecodeWorkerWithLease,
9605
+ lease: this.resolveSessionLeaseKnobs,
9529
9606
  logger: log,
9530
9607
  source,
9531
9608
  opts: {
@@ -9572,16 +9649,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9572
9649
  const attachment = this.attached.get(deviceId);
9573
9650
  if (!attachment) return;
9574
9651
  if (action === "subscribe") {
9575
- if (attachment.detectionKeepWarmTimer !== null) {
9576
- clearTimeout(attachment.detectionKeepWarmTimer);
9577
- attachment.detectionKeepWarmTimer = null;
9578
- }
9579
9652
  if (attachment.detectionUnsubscribe !== null) return;
9580
- const adopted = this.adoptWarmDetectionDecode(deviceId, attachment.config);
9581
- if (adopted) {
9582
- attachment.detectionUnsubscribe = adopted;
9583
- return;
9584
- }
9585
9653
  this.subscribeDetectionFrames(attachment.config).then((unsub) => {
9586
9654
  if (!this.attached.has(deviceId)) {
9587
9655
  unsub?.();
@@ -9590,13 +9658,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
9590
9658
  attachment.detectionUnsubscribe = unsub;
9591
9659
  });
9592
9660
  } else {
9593
- if (attachment.detectionKeepWarmTimer !== null) return;
9594
9661
  if (attachment.detectionUnsubscribe === null) return;
9595
- attachment.detectionKeepWarmTimer = setTimeout(() => {
9596
- attachment.detectionKeepWarmTimer = null;
9597
- attachment.detectionUnsubscribe?.();
9598
- attachment.detectionUnsubscribe = null;
9599
- }, DETECTION_KEEPWARM_MS);
9662
+ attachment.detectionUnsubscribe();
9663
+ attachment.detectionUnsubscribe = null;
9600
9664
  }
9601
9665
  }
9602
9666
  /**
@@ -10348,6 +10412,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10348
10412
  const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
10349
10413
  return startSessionDecodePump({
10350
10414
  fork: this.forkDecodeWorkerWithLease,
10415
+ lease: this.resolveSessionLeaseKnobs,
10351
10416
  logger: log,
10352
10417
  source,
10353
10418
  opts: {