@camstack/addon-pipeline 1.2.218 → 1.2.220

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 (38) hide show
  1. package/dist/audio-analyzer/index.js +49 -5
  2. package/dist/audio-analyzer/index.mjs +46 -3
  3. package/dist/chunk-emK7D4bc.js +35 -0
  4. package/dist/{default-detection-model-AJyhoTLF.mjs → default-detection-model-DBNEc3iw.mjs} +1 -1
  5. package/dist/{default-detection-model-Bf45EQ98.js → default-detection-model-x6oNMBDH.js} +1 -1
  6. package/dist/detection-pipeline/index.js +10 -9
  7. package/dist/detection-pipeline/index.mjs +4 -4
  8. package/dist/{dist-BbvtiBNc.js → dist-ByPsWlTa.js} +57 -38
  9. package/dist/{dist-Dl6GntEh.mjs → dist-ClihWGOZ.mjs} +57 -3
  10. package/dist/{lazy-sharp-L9Lcr62v.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-CbCVHEvJ.js → node-CNPnaux4.js} +4 -3
  14. package/dist/{node-CEyw52D2.mjs → node-DiaTmz84.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-DBxdiCSF.js → process-memory-9KqRtUHt.js} +3 -2
  18. package/dist/{process-memory-Du00R6-e.mjs → process-memory-D4t8hpJd.mjs} +1 -1
  19. package/dist/recorder/index.js +334 -123
  20. package/dist/recorder/index.mjs +332 -122
  21. package/dist/{segment-demux-js-B278aBtB.mjs → segment-demux-js-CFFXVae4.mjs} +1 -1
  22. package/dist/{segment-demux-js-D7bjzY8M.js → segment-demux-js-CZefXDLx.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 +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BnmbfnAd.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CtdpN7Sz.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-yJ2CyixO.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7hqrrd88.mjs} +1 -1
  28. package/dist/stream-broker/demux-worker-child.js +1 -1
  29. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  30. package/dist/stream-broker/{hostInit-MIKQqs86.mjs → hostInit-McoUiljf.mjs} +3 -3
  31. package/dist/stream-broker/index.js +24 -25
  32. package/dist/stream-broker/index.mjs +17 -19
  33. package/dist/stream-broker/remoteEntry.js +1 -1
  34. package/dist/worker-protocol-B2MfQLlu.js +184 -0
  35. package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
  36. package/package.json +1 -1
  37. package/dist/worker-protocol-BEbD5adq.mjs +0 -264
  38. package/dist/worker-protocol-C4rh5uiS.js +0 -299
@@ -1,7 +1,7 @@
1
- import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-Dl6GntEh.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-CEyw52D2.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-AJyhoTLF.mjs";
4
- import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-BEbD5adq.mjs";
1
+ import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, b as NativeLeaseSettingsSchema, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride, y as NativeLeaseAdmissionSchema } from "../dist-ClihWGOZ.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DiaTmz84.mjs";
3
+ import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-DBNEc3iw.mjs";
4
+ import { t as isWorkerReply } from "../worker-protocol-C_W-P_g-.mjs";
5
5
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
6
6
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
7
7
  import { a as preRollRequestOf, o as withDetectionIntent, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DWe2kInY.mjs";
@@ -67,26 +67,23 @@ function wrapChild(child, onExit) {
67
67
  * signature (it forwards the same source in the subsequent `start` message);
68
68
  * the child entry itself is source-independent, so it is not consumed here.
69
69
  *
70
- * `leaseOverride` carries the operator's cluster-wide native-lease settings
71
- * (`pipeline-orchestrator` globals, read by `native-lease-settings-source.ts`)
72
- * into the child as environment variables — the child constructs its lease
73
- * store and demand gate at module load, before the first `start` message, so
74
- * the values have to be present at spawn. Only the knobs the operator actually
75
- * SET are emitted, which is what keeps the pre-settings env override
76
- * (`CAMSTACK_SESSION_NATIVE_LEASE_*`) working for the untouched ones; see
77
- * `native-lease-config.ts` for the full precedence.
70
+ * The fork carries NO configuration. Until D453 it spread a set of
71
+ * `*_SETTING` env vars in, because the child resolved its native-lease knobs
72
+ * at module load; the knobs now travel on the `start` message
73
+ * (`WorkerRequest.lease`), resolved in this process. That is what makes a
74
+ * decode worker forkable BEFORE anyone knows which camera it will serve — a
75
+ * child configured at spawn is a child bound to the configuration it was born
76
+ * with — and it is what let the child bundle drop the Zod schemas that cost
77
+ * every fork ~170 ms and ~100 MB of heap (D453).
78
78
  *
79
79
  * The inherited environment is SPREAD, never replaced: this child resolves
80
80
  * node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
81
81
  * agents, so a replaced env would silently break decoding off the hub.
82
82
  */
83
- function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
83
+ function forkDecodeWorker(source, costRegistry) {
84
84
  const child = fork(CHILD_ENTRY_PATH, [], {
85
85
  serialization: "advanced",
86
- env: {
87
- ...process.env,
88
- ...nativeLeaseSettingEnv(leaseOverride)
89
- }
86
+ env: { ...process.env }
90
87
  });
91
88
  const costClaim = costRegistry?.claim({
92
89
  role: "decode",
@@ -97,6 +94,145 @@ function forkDecodeWorker(source, leaseOverride = {}, costRegistry) {
97
94
  return wrapChild(child, () => costClaim.release());
98
95
  }
99
96
  //#endregion
97
+ //#region src/session-decode/native-lease-config.ts
98
+ /**
99
+ * Resolution of the native-frame hold / tile knobs for one decode session —
100
+ * PARENT side. The result travels to the decode worker on its `start` message
101
+ * (`native-lease-knobs.ts` is the wire half, and the only half the child sees).
102
+ *
103
+ * ## Where the values come from
104
+ *
105
+ * The knobs are cluster-wide `pipeline-orchestrator` settings
106
+ * (`@camstack/types` → `pipeline/native-lease.ts`). The pipeline-runner reads
107
+ * them over the hub-routed `addon-settings` cap
108
+ * (`pipeline-runner/native-lease-settings-source.ts`) and resolves them HERE,
109
+ * once per session, at the moment the session starts — so a changed setting
110
+ * reaches the next session and leaves running ones alone, exactly as before.
111
+ *
112
+ * Until D453 the resolution ran inside the CHILD, from `*_SETTING` env vars the
113
+ * parent wrote at fork time. That bound a worker to the knobs it was forked
114
+ * with (a pre-forked worker cannot be), and it dragged the Zod schemas — the
115
+ * whole `@camstack/types` index — into the child bundle: ~130 ms and ~140 MB
116
+ * per fork, measured. Moving the resolution up one process removed both.
117
+ *
118
+ * ## Precedence — setting > env > default
119
+ *
120
+ * 1. **setting** — the operator set the knob in the orchestrator's global
121
+ * settings. Wins, because a value someone deliberately chose in the UI must
122
+ * not be silently overruled by a shell variable nobody can see from there.
123
+ * 2. **env** — {@link NATIVE_LEASE_ENV_VARS}, the pre-settings mechanism, kept
124
+ * as an emergency / dev override. It applies only while the corresponding
125
+ * setting is UNSET. "Unset" includes a knob sitting AT its shipped default:
126
+ * `addon-settings.getGlobalSettings` hydrates an unstored field with the
127
+ * schema default and offers no way to tell the two apart, so treating the
128
+ * default as a choice would have retired every env var on the day this
129
+ * shipped. Move the slider off the default and the setting takes over; move
130
+ * it back and the env var returns. See `readNativeLeaseOverride`.
131
+ * 3. **default** — {@link DEFAULT_NATIVE_LEASE_SETTINGS}, byte-for-byte the
132
+ * constants the worker shipped with.
133
+ *
134
+ * A value that fails its bounds at ANY level falls through to the next one
135
+ * rather than being clamped: clamping turns a typo into a number nobody chose,
136
+ * and this one governs how much RAM a busy camera holds.
137
+ *
138
+ * The GPU surface ring (`CAMSTACK_SESSION_NATIVE_CROP_RING`) is deliberately
139
+ * NOT here — see its docblock in `decode-worker-child.ts` for why it stays
140
+ * env-only.
141
+ */
142
+ /** Emergency / dev override env vars (pre-settings; lowest precedence but the default). */
143
+ var NATIVE_LEASE_ENV_VARS = {
144
+ holdFrames: "CAMSTACK_SESSION_NATIVE_LEASE_HOLD_FRAMES",
145
+ budgetMb: "CAMSTACK_SESSION_NATIVE_LEASE_BUDGET_MB",
146
+ activityMs: "CAMSTACK_SESSION_NATIVE_LEASE_ACTIVITY_MS",
147
+ admission: "CAMSTACK_SESSION_NATIVE_LEASE_ADMISSION",
148
+ tileBudgetMb: "CAMSTACK_SESSION_NATIVE_TILE_BUDGET_MB",
149
+ sceneBudgetMb: "CAMSTACK_SESSION_NATIVE_SCENE_BUDGET_MB"
150
+ };
151
+ /**
152
+ * Validate one candidate through the knob's own Zod bound. `null` for absent,
153
+ * non-numeric, or out-of-bounds input — the caller then falls through to the
154
+ * next precedence level.
155
+ */
156
+ function parseKnob(knob, raw) {
157
+ if (raw === void 0) return null;
158
+ if (typeof raw === "string" && raw.trim() === "") return null;
159
+ const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(Number(raw));
160
+ return parsed.success ? parsed.data : null;
161
+ }
162
+ /** {@link parseKnob} for the one knob whose value is a word, not a number. */
163
+ function parseAdmission(raw) {
164
+ if (raw === void 0) return null;
165
+ const parsed = NativeLeaseAdmissionSchema.safeParse(raw.trim());
166
+ return parsed.success ? parsed.data : null;
167
+ }
168
+ /** Resolve one NUMERIC knob across the three precedence levels. */
169
+ function resolveKnob(knob, env, override) {
170
+ const fromSetting = parseKnob(knob, override[knob]);
171
+ if (fromSetting !== null) return {
172
+ value: fromSetting,
173
+ source: "setting"
174
+ };
175
+ const fromEnv = parseKnob(knob, env[NATIVE_LEASE_ENV_VARS[knob]]);
176
+ if (fromEnv !== null) return {
177
+ value: fromEnv,
178
+ source: "env"
179
+ };
180
+ return {
181
+ value: DEFAULT_NATIVE_LEASE_SETTINGS[knob],
182
+ source: "default"
183
+ };
184
+ }
185
+ /** {@link resolveKnob} for `admission`. Same precedence, different parser. */
186
+ function resolveAdmission(env, override) {
187
+ const fromSetting = parseAdmission(override.admission);
188
+ if (fromSetting !== null) return {
189
+ value: fromSetting,
190
+ source: "setting"
191
+ };
192
+ const fromEnv = parseAdmission(env[NATIVE_LEASE_ENV_VARS.admission]);
193
+ if (fromEnv !== null) return {
194
+ value: fromEnv,
195
+ source: "env"
196
+ };
197
+ return {
198
+ value: DEFAULT_NATIVE_LEASE_SETTINGS.admission,
199
+ source: "default"
200
+ };
201
+ }
202
+ /**
203
+ * Resolve every lease knob for one session. Pure.
204
+ *
205
+ * `override` carries ONLY the knobs the operator actually SET (see
206
+ * `native-lease-settings-source.ts`); an empty object leaves the env override
207
+ * fully functional, which is the whole point of the distinction.
208
+ */
209
+ function resolveNativeLeaseKnobs(env, override) {
210
+ const holdFrames = resolveKnob("holdFrames", env, override);
211
+ const budgetMb = resolveKnob("budgetMb", env, override);
212
+ const activityMs = resolveKnob("activityMs", env, override);
213
+ const admission = resolveAdmission(env, override);
214
+ const tileBudgetMb = resolveKnob("tileBudgetMb", env, override);
215
+ const sceneBudgetMb = resolveKnob("sceneBudgetMb", env, override);
216
+ return {
217
+ values: {
218
+ holdFrames: holdFrames.value,
219
+ budgetMb: budgetMb.value,
220
+ activityMs: activityMs.value,
221
+ admission: admission.value,
222
+ tileBudgetMb: tileBudgetMb.value,
223
+ sceneBudgetMb: sceneBudgetMb.value
224
+ },
225
+ sources: {
226
+ holdFrames: holdFrames.source,
227
+ budgetMb: budgetMb.source,
228
+ activityMs: activityMs.source,
229
+ admission: admission.source,
230
+ tileBudgetMb: tileBudgetMb.source,
231
+ sceneBudgetMb: sceneBudgetMb.source
232
+ }
233
+ };
234
+ }
235
+ //#endregion
100
236
  //#region src/session-decode/guarded-session-decode.ts
101
237
  /**
102
238
  * 2s → 5s → 15s → 30s, then 30s forever.
@@ -1343,11 +1479,12 @@ var SessionDecodeSession = class {
1343
1479
  this.child.on("message", (m) => this.handleMessage(m));
1344
1480
  this.child.on("exit", (m) => this.handleExit(toWorkerExitInfo(m)));
1345
1481
  }
1346
- start(source, opts) {
1482
+ start(source, opts, lease) {
1347
1483
  this.trySend({
1348
1484
  kind: "start",
1349
1485
  source,
1350
- opts
1486
+ opts,
1487
+ lease
1351
1488
  });
1352
1489
  }
1353
1490
  /**
@@ -1515,6 +1652,7 @@ var SessionDecodeSession = class {
1515
1652
  ...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
1516
1653
  });
1517
1654
  return;
1655
+ case "ready": return;
1518
1656
  case "error":
1519
1657
  this.settleError(raw.message, raw.frameId);
1520
1658
  return;
@@ -1766,7 +1904,7 @@ function createSessionDecodeCoordinator(deps) {
1766
1904
  const firstFrameMaxWaitMs = Math.max(deps.firstFrameMaxWaitMs ?? DEFAULT_FIRST_FRAME_MAX_WAIT_MS, firstFrameTimeoutMs);
1767
1905
  return { async *generateVideoFrames(source, opts) {
1768
1906
  const session = new SessionDecodeSession(deps.fork(source), deps.logger, source.deviceId, firstFrameTimeoutMs, frameTimeoutMs, firstFrameMaxWaitMs, deps.onDialTiming, deps.onPreRollReport);
1769
- session.start(source, opts);
1907
+ session.start(source, opts, deps.lease());
1770
1908
  try {
1771
1909
  while (true) {
1772
1910
  const outcome = await session.pullNext();
@@ -1833,6 +1971,7 @@ function startSessionDecodePump(deps) {
1833
1971
  let dialTiming = null;
1834
1972
  const iterator = createSessionDecodeCoordinator({
1835
1973
  fork: deps.fork,
1974
+ lease: deps.lease,
1836
1975
  logger: deps.logger,
1837
1976
  onDialTiming: (timing) => {
1838
1977
  dialTiming = timing;
@@ -6548,7 +6687,6 @@ function writeRetainedDetectionFrame(store, deviceId, frame, retention) {
6548
6687
  if (!isContinuousRetainEnabled()) return store.allocateHandle(deviceId, frame) ?? void 0;
6549
6688
  return store.write(deviceId, frame, frame.frameRef ? createRetainedContentFallback(localFrameRegistry, frame.frameRef) : void 0) ?? void 0;
6550
6689
  }
6551
- var DETECTION_KEEPWARM_MS = 15e3;
6552
6690
  /**
6553
6691
  * Occupancy-burst safety timeout (both the dial path and the analyzer-tap
6554
6692
  * path — Task 10). Bounds worst-case burst latency when frames stop
@@ -6575,6 +6713,31 @@ var DETECTION_REDIAL_DELAY_MS = 2e3;
6575
6713
  */
6576
6714
  var DETECTION_REDIAL_GIVEUP_WINDOW_MS = 6e4;
6577
6715
  /**
6716
+ * ## There is no decode keep-warm any more — D453 removed it
6717
+ *
6718
+ * A `WarmDetachedDecode` used to hold a detection worker alive for 15 s across
6719
+ * a detach, hoping the next motion event would adopt it. It did not park: it
6720
+ * kept its broker restream lease and kept DECODING, because the child's loop is
6721
+ * `for await (... decoder.frames(demuxer.packets(...)))`, driven by the demuxer
6722
+ * and not by the parent's `pull` — `minIntervalMs` throttles only inside
6723
+ * `FrameSlot.publish`, after the decode. `framesDecoded` advanced in 122 of 122
6724
+ * park windows at full source rate, and a decode-worker child costs 40-80% of a
6725
+ * core.
6726
+ *
6727
+ * So the park cost about what running costs, and it bought a re-fork back in
6728
+ * 12% of cases: on this fleet, 272 of 440 park windows expired having adopted
6729
+ * nothing, while the two decode-worker children stood at 319% of the 522% of
6730
+ * CPU CamStack drew on the host. D282 put the arithmetic at ~25x net-negative
6731
+ * against the forks it avoided, and D453 made the fork itself ~150 ms and
6732
+ * ~100 MB cheaper, which only widens it.
6733
+ *
6734
+ * Do not reintroduce a keep-warm shaped like this one, and do not "fix" it with
6735
+ * a longer TTL: the measured p50 gap between two sessions of one camera is
6736
+ * 61 s, so a longer window scales the waste and not the hit rate. If a warm
6737
+ * process is wanted, it must be a process that holds NO stream and decodes
6738
+ * NOTHING — see D453 for what that costs and why it was not taken either.
6739
+ */
6740
+ /**
6578
6741
  * Pure decision helper: returns `true` when a dynamic frame-diff analyzer
6579
6742
  * subscription should be opened for an onboard-motion camera.
6580
6743
  *
@@ -6900,12 +7063,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
6900
7063
  retention = null;
6901
7064
  attached = /* @__PURE__ */ new Map();
6902
7065
  /**
6903
- * Detection decode workers parked across a detach, keyed by deviceId — see
6904
- * {@link WarmDetachedDecode}. Bounded by construction: at most one entry per
6905
- * camera, each self-expiring after {@link DETECTION_KEEPWARM_MS}.
6906
- */
6907
- warmDetachedDetection = /* @__PURE__ */ new Map();
6908
- /**
6909
7066
  * Detail step state parked across a SOFT detach, keyed by deviceId — see
6910
7067
  * `detail-park.ts` for the fault this closes. Bounded by construction: at
6911
7068
  * most one entry per camera, each self-expiring after
@@ -7264,14 +7421,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7264
7421
  this.runner = null;
7265
7422
  }
7266
7423
  for (const attachment of this.attached.values()) {
7267
- if (attachment.detectionKeepWarmTimer !== null) {
7268
- clearTimeout(attachment.detectionKeepWarmTimer);
7269
- attachment.detectionKeepWarmTimer = null;
7270
- }
7271
7424
  attachment.motionUnsubscribe?.();
7272
7425
  attachment.detectionUnsubscribe?.();
7273
7426
  }
7274
- for (const deviceId of [...this.warmDetachedDetection.keys()]) this.dropWarmDetectionDecode(deviceId, "shutdown");
7275
7427
  for (const deviceId of [...this.parkedDetail.keys()]) this.dropParkedDetail(deviceId);
7276
7428
  this.attached.clear();
7277
7429
  this.lastMotionFrameAt.clear();
@@ -7982,7 +8134,6 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7982
8134
  config,
7983
8135
  motionUnsubscribe: null,
7984
8136
  detectionUnsubscribe: null,
7985
- detectionKeepWarmTimer: null,
7986
8137
  detailSteps: deriveDetailSteps(config.steps)
7987
8138
  };
7988
8139
  this.attached.set(config.deviceId, attachment);
@@ -8638,12 +8789,24 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8638
8789
  warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
8639
8790
  } });
8640
8791
  /**
8641
- * Fork a decode worker carrying the operator's lease knobs. Used in place of
8642
- * a bare `forkDecodeWorker` reference at every session-decode spawn site: the
8643
- * knobs are read at spawn, so a changed setting reaches NEW sessions and
8792
+ * Fork a decode worker. Used in place of a bare `forkDecodeWorker` reference
8793
+ * at every session-decode spawn site so the cost registry is never forgotten.
8794
+ *
8795
+ * The fork carries NO configuration: since D453 the lease knobs travel on the
8796
+ * `start` message ({@link resolveSessionLeaseKnobs}), not in the child's
8797
+ * environment. A worker is therefore forkable before anyone knows which
8798
+ * camera it will serve.
8799
+ */
8800
+ forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.costRegistry);
8801
+ /**
8802
+ * The lease knobs for the session that is about to start — `setting > env >
8803
+ * default`, resolved HERE because this process is the one that carries the
8804
+ * Zod schemas (the child no longer does; D453).
8805
+ *
8806
+ * Read per session, so a changed operator setting reaches NEW sessions and
8644
8807
  * leaves running ones alone (a live session would have to be re-dialled).
8645
8808
  */
8646
- forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.nativeLease.current(), this.costRegistry);
8809
+ resolveSessionLeaseKnobs = () => resolveNativeLeaseKnobs(process.env, this.nativeLease.current());
8647
8810
  /**
8648
8811
  * Refresh the lease knobs before a decode worker is spawned. Awaited on the
8649
8812
  * `acquire` leg of `startGuardedSessionDecode` (which always completes before
@@ -8848,19 +9011,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8848
9011
  */
8849
9012
  handleDetectionDecodeEnded(deviceId) {
8850
9013
  const attachment = this.attached.get(deviceId);
8851
- if (!attachment) {
8852
- this.dropWarmDetectionDecode(deviceId, "worker-died-while-parked");
8853
- return;
8854
- }
9014
+ if (!attachment) return;
8855
9015
  const log = this.ctx.logger.withTags({ deviceId });
8856
9016
  attachment.detectionUnsubscribe?.();
8857
9017
  attachment.detectionUnsubscribe = null;
8858
- if (attachment.detectionKeepWarmTimer !== null) {
8859
- clearTimeout(attachment.detectionKeepWarmTimer);
8860
- attachment.detectionKeepWarmTimer = null;
8861
- log.warn("detection decode worker died while kept warm — next open re-forks cold");
8862
- return;
8863
- }
8864
9018
  const now = Date.now();
8865
9019
  const last = this.lastDecodeUnexpectedEndAt.get(deviceId) ?? 0;
8866
9020
  this.lastDecodeUnexpectedEndAt.set(deviceId, now);
@@ -8885,13 +9039,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8885
9039
  /**
8886
9040
  * Tear a camera down.
8887
9041
  *
8888
- * `keepDecodeWarm` routes the DETECTION decode through the same keep-warm
8889
- * grace the phase gate uses instead of killing it — see
8890
- * {@link WarmDetachedDecode}. Set only by the {@link detachCamera} cap entry
8891
- * point (the orchestrator's motion-cooldown teardown); every internal detach
8892
- * — an attach replacing a stale config, a decode that died twice, shutdown —
8893
- * is a hard one, because in each of those cases the worker is either wrong or
8894
- * already gone.
9042
+ * `softDetach` is the {@link detachCamera} cap entry point (the
9043
+ * orchestrator's motion-cooldown teardown). It keeps the DETAIL step tree
9044
+ * parked — a config slice, no process, no CPU — so a track outliving this
9045
+ * session can still resolve its enrichment steps. It does NOT keep the decode
9046
+ * worker: since D453 every detach releases it, because a held worker went on
9047
+ * decoding at full rate for the whole window (see the note above
9048
+ * {@link AttachedCamera}). Every internal detach — an attach replacing a
9049
+ * stale config, a decode that died twice, shutdown — is hard on both counts.
8895
9050
  */
8896
9051
  /**
8897
9052
  * Park the detaching camera's detail-relevant config slice for the grace
@@ -8938,18 +9093,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8938
9093
  }
8939
9094
  return parked.state;
8940
9095
  }
8941
- detachInternal(deviceId, keepDecodeWarm = false) {
9096
+ detachInternal(deviceId, softDetach = false) {
8942
9097
  const attachment = this.attached.get(deviceId);
8943
9098
  if (!attachment) return;
8944
9099
  this.clearOnboardAnalyzer(deviceId);
8945
- if (attachment.detectionKeepWarmTimer !== null) {
8946
- clearTimeout(attachment.detectionKeepWarmTimer);
8947
- attachment.detectionKeepWarmTimer = null;
8948
- }
8949
9100
  attachment.motionUnsubscribe?.();
8950
- if (keepDecodeWarm && attachment.detectionUnsubscribe !== null) this.parkWarmDetectionDecode(deviceId, attachment.config, attachment.detectionUnsubscribe);
8951
- else attachment.detectionUnsubscribe?.();
8952
- if (keepDecodeWarm) this.parkDetailState(deviceId, attachment.config);
9101
+ attachment.detectionUnsubscribe?.();
9102
+ if (softDetach) this.parkDetailState(deviceId, attachment.config);
8953
9103
  else this.dropParkedDetail(deviceId);
8954
9104
  this.lastMotionFrameAt.delete(deviceId);
8955
9105
  this.lastDecodeUnexpectedEndAt.delete(deviceId);
@@ -8974,83 +9124,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8974
9124
  this.runner?.unregisterCamera(deviceId);
8975
9125
  this.ctx?.logger.info("detachCamera", {
8976
9126
  tags: { deviceId },
8977
- meta: { decodeKeptWarm: this.warmDetachedDetection.has(deviceId) }
8978
- });
8979
- }
8980
- /**
8981
- * The decode-relevant identity of a camera's detection decode. Two configs
8982
- * that agree on all of it can share one worker; anything else must re-fork.
8983
- * Kept in ONE place so a future decode-shaping option cannot be added to the
8984
- * pump without the adoption check noticing.
8985
- */
8986
- detectionDecodeIdentity(config) {
8987
- return {
8988
- streamId: config.detectionStreamId,
8989
- fps: config.detectionFps,
8990
- hostname: resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]),
8991
- ownerNodeId: resolveSessionDecodeOwnerNodeId(config)
8992
- };
8993
- }
8994
- /**
8995
- * Hold a detached camera's detection decode worker for
8996
- * {@link DETECTION_KEEPWARM_MS} so the next motion event reuses it.
8997
- * Replacing an existing park releases the older worker first — two parked
8998
- * workers for one camera would be two live decodes and two broker leases.
8999
- */
9000
- parkWarmDetectionDecode(deviceId, config, unsubscribe) {
9001
- this.dropWarmDetectionDecode(deviceId, "replaced");
9002
- const timer = setTimeout(() => {
9003
- this.warmDetachedDetection.delete(deviceId);
9004
- unsubscribe();
9005
- this.ctx?.logger.info("session-decode: warm detection decode expired — worker released", {
9006
- tags: { deviceId },
9007
- meta: { keepWarmMs: DETECTION_KEEPWARM_MS }
9008
- });
9009
- }, DETECTION_KEEPWARM_MS);
9010
- timer.unref?.();
9011
- this.warmDetachedDetection.set(deviceId, {
9012
- unsubscribe,
9013
- timer,
9014
- ...this.detectionDecodeIdentity(config)
9015
- });
9016
- }
9017
- /**
9018
- * Adopt a parked decode worker for a camera that is attaching again, or
9019
- * `null` when there is nothing to adopt.
9020
- *
9021
- * A parked worker whose identity no longer matches is DROPPED here rather
9022
- * than left to expire: the caller is about to fork a replacement, and letting
9023
- * both run would double this camera's decode cost for the rest of the window.
9024
- */
9025
- adoptWarmDetectionDecode(deviceId, config) {
9026
- const warm = this.warmDetachedDetection.get(deviceId);
9027
- if (!warm) return null;
9028
- const wanted = this.detectionDecodeIdentity(config);
9029
- if (warm.streamId !== wanted.streamId || warm.fps !== wanted.fps || warm.hostname !== wanted.hostname || warm.ownerNodeId !== wanted.ownerNodeId) {
9030
- this.dropWarmDetectionDecode(deviceId, "config-changed");
9031
- return null;
9032
- }
9033
- clearTimeout(warm.timer);
9034
- this.warmDetachedDetection.delete(deviceId);
9035
- this.ctx?.logger.info("session-decode: reusing the warm detection decode worker (no re-fork)", {
9036
- tags: { deviceId },
9037
- meta: {
9038
- streamId: warm.streamId,
9039
- fps: warm.fps
9040
- }
9041
- });
9042
- return warm.unsubscribe;
9043
- }
9044
- /** Release a parked worker now. `reason` names why in the log. */
9045
- dropWarmDetectionDecode(deviceId, reason) {
9046
- const warm = this.warmDetachedDetection.get(deviceId);
9047
- if (!warm) return;
9048
- clearTimeout(warm.timer);
9049
- this.warmDetachedDetection.delete(deviceId);
9050
- warm.unsubscribe();
9051
- this.ctx?.logger.info("session-decode: warm detection decode released", {
9052
- tags: { deviceId },
9053
- meta: { reason }
9127
+ meta: { detailParked: this.parkedDetail.has(deviceId) }
9054
9128
  });
9055
9129
  }
9056
9130
  /**
@@ -9361,6 +9435,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9361
9435
  let reservation = null;
9362
9436
  return startSessionDecodePump({
9363
9437
  fork: this.forkDecodeWorkerWithLease,
9438
+ lease: this.resolveSessionLeaseKnobs,
9364
9439
  logger: log,
9365
9440
  source,
9366
9441
  opts: {
@@ -9519,6 +9594,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9519
9594
  } });
9520
9595
  return startSessionDecodePump({
9521
9596
  fork: this.forkDecodeWorkerWithLease,
9597
+ lease: this.resolveSessionLeaseKnobs,
9522
9598
  logger: log,
9523
9599
  source,
9524
9600
  opts: {
@@ -9565,16 +9641,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9565
9641
  const attachment = this.attached.get(deviceId);
9566
9642
  if (!attachment) return;
9567
9643
  if (action === "subscribe") {
9568
- if (attachment.detectionKeepWarmTimer !== null) {
9569
- clearTimeout(attachment.detectionKeepWarmTimer);
9570
- attachment.detectionKeepWarmTimer = null;
9571
- }
9572
9644
  if (attachment.detectionUnsubscribe !== null) return;
9573
- const adopted = this.adoptWarmDetectionDecode(deviceId, attachment.config);
9574
- if (adopted) {
9575
- attachment.detectionUnsubscribe = adopted;
9576
- return;
9577
- }
9578
9645
  this.subscribeDetectionFrames(attachment.config).then((unsub) => {
9579
9646
  if (!this.attached.has(deviceId)) {
9580
9647
  unsub?.();
@@ -9583,13 +9650,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
9583
9650
  attachment.detectionUnsubscribe = unsub;
9584
9651
  });
9585
9652
  } else {
9586
- if (attachment.detectionKeepWarmTimer !== null) return;
9587
9653
  if (attachment.detectionUnsubscribe === null) return;
9588
- attachment.detectionKeepWarmTimer = setTimeout(() => {
9589
- attachment.detectionKeepWarmTimer = null;
9590
- attachment.detectionUnsubscribe?.();
9591
- attachment.detectionUnsubscribe = null;
9592
- }, DETECTION_KEEPWARM_MS);
9654
+ attachment.detectionUnsubscribe();
9655
+ attachment.detectionUnsubscribe = null;
9593
9656
  }
9594
9657
  }
9595
9658
  /**
@@ -10341,6 +10404,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10341
10404
  const useLocalFrameRef = this.executorCoLocated() && process.env["CAMSTACK_DISABLE_LOCAL_FRAME_REF"] !== "1";
10342
10405
  return startSessionDecodePump({
10343
10406
  fork: this.forkDecodeWorkerWithLease,
10407
+ lease: this.resolveSessionLeaseKnobs,
10344
10408
  logger: log,
10345
10409
  source,
10346
10410
  opts: {
@@ -1,6 +1,7 @@
1
- const require_dist = require("./dist-BbvtiBNc.js");
1
+ const require_chunk = require("./chunk-emK7D4bc.js");
2
+ const require_dist = require("./dist-ByPsWlTa.js");
2
3
  let node_fs = require("node:fs");
3
- node_fs = require_dist.__toESM(node_fs);
4
+ node_fs = require_chunk.__toESM(node_fs);
4
5
  let node_child_process = require("node:child_process");
5
6
  //#region src/shared/node-topology-platform.ts
6
7
  /**
@@ -1,4 +1,4 @@
1
- import { mt as parseProcStatus } from "./dist-Dl6GntEh.mjs";
1
+ import { mt as parseProcStatus } from "./dist-ClihWGOZ.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts