@camstack/addon-pipeline 1.2.302 → 1.2.304

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.
@@ -41,14 +41,18 @@ var KILL_SIGNALS = {
41
41
  * parameter rather than a second `child.on('exit')` in the caller because
42
42
  * every listener added out here is one somebody can forget to remove.
43
43
  */
44
- function wrapChild(child, onExit) {
44
+ function wrapChild(child, onExit, killedByRunner = () => false) {
45
45
  let spawned = false;
46
46
  let exitDelivered = false;
47
47
  const exitListeners = [];
48
- const deliverExit = (info) => {
48
+ const deliverExit = (exit) => {
49
49
  if (exitDelivered) return;
50
50
  exitDelivered = true;
51
51
  onExit();
52
+ const info = killedByRunner() ? {
53
+ ...exit,
54
+ killedByRunner: true
55
+ } : exit;
52
56
  for (const listener of exitListeners) listener(info);
53
57
  };
54
58
  child.on("spawn", () => {
@@ -102,19 +106,179 @@ function wrapChild(child, onExit) {
102
106
  * node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
103
107
  * agents, so a replaced env would silently break decoding off the hub.
104
108
  */
105
- function forkDecodeWorker(source, costRegistry) {
106
- const child = (0, node_child_process.fork)(CHILD_ENTRY_PATH, [], {
109
+ function forkDecodeWorker(source, deps) {
110
+ return forkDecodeWorkerAt(CHILD_ENTRY_PATH, source, deps ?? {});
111
+ }
112
+ /**
113
+ * {@link forkDecodeWorker} with the entry named — the one seam a process-level
114
+ * test needs to fork a stub through the REAL fork, claim and live-worker path.
115
+ * Production forks only {@link CHILD_ENTRY_PATH}.
116
+ */
117
+ function forkDecodeWorkerAt(entryPath, source, deps) {
118
+ const child = (0, node_child_process.fork)(entryPath, [], {
107
119
  serialization: "advanced",
108
120
  env: { ...process.env }
109
121
  });
110
- const costClaim = costRegistry?.claim({
122
+ const costClaim = deps.costRegistry?.claim({
111
123
  role: "decode",
112
124
  deviceId: source.deviceId,
113
125
  unit: `${source.deviceId}/decode`,
114
126
  pid: child.pid
115
127
  }) ?? require_node.NO_COST_CLAIM;
116
- return wrapChild(child, () => costClaim.release());
128
+ let killedByRunner = false;
129
+ const untrack = deps.liveWorkers?.add({
130
+ deviceId: source.deviceId,
131
+ pid: child.pid,
132
+ killNow: () => {
133
+ killedByRunner = true;
134
+ child.kill("SIGKILL");
135
+ }
136
+ }) ?? (() => void 0);
137
+ return wrapChild(child, () => {
138
+ costClaim.release();
139
+ untrack();
140
+ }, () => killedByRunner);
141
+ }
142
+ //#endregion
143
+ //#region src/session-decode/live-decode-workers.ts
144
+ var KILL_MESSAGE = {
145
+ shutdown: "session-decode: worker killed at shutdown",
146
+ "forked-after-shutdown": "session-decode: worker forked after shutdown began — killed at once",
147
+ "process-exit": "session-decode: worker killed at process exit"
148
+ };
149
+ /** Send the signal. Never throws: a failure is the outcome. */
150
+ function signal(worker) {
151
+ try {
152
+ worker.killNow();
153
+ return {
154
+ worker,
155
+ error: null
156
+ };
157
+ } catch (err) {
158
+ return {
159
+ worker,
160
+ error: require_dist.errMsg(err)
161
+ };
162
+ }
163
+ }
164
+ /**
165
+ * One line per outcome, each in its own swallowing try. The logger may throw:
166
+ * the `'exit'` hook runs while the transport goes down, and the runner's
167
+ * write path has no guard. A line that fails must not cost another line.
168
+ */
169
+ function logOutcomes(outcomes, reason, logger) {
170
+ for (const { worker, error } of outcomes) try {
171
+ if (error === null) logger.info(KILL_MESSAGE[reason], {
172
+ tags: { deviceId: worker.deviceId },
173
+ meta: {
174
+ pid: worker.pid ?? null,
175
+ signal: "SIGKILL",
176
+ reason
177
+ }
178
+ });
179
+ else logger.warn("session-decode: worker kill failed — it may outlive the runner", {
180
+ tags: { deviceId: worker.deviceId },
181
+ meta: {
182
+ pid: worker.pid ?? null,
183
+ reason,
184
+ error
185
+ }
186
+ });
187
+ } catch {}
117
188
  }
189
+ var LiveDecodeWorkers = class {
190
+ live = /* @__PURE__ */ new Set();
191
+ /** Set by {@link close}: from then on a worker is killed as it is added. */
192
+ closedLogger = null;
193
+ /** Track a forked worker; call the returned function from its real exit. */
194
+ add(worker) {
195
+ if (this.closedLogger !== null) {
196
+ const outcome = signal(worker);
197
+ logOutcomes([outcome], "forked-after-shutdown", this.closedLogger);
198
+ if (outcome.error === null) return () => void 0;
199
+ }
200
+ this.live.add(worker);
201
+ return () => {
202
+ this.live.delete(worker);
203
+ };
204
+ }
205
+ get size() {
206
+ return this.live.size;
207
+ }
208
+ /**
209
+ * SIGKILL every tracked worker now, then one log line per worker. SIGKILL
210
+ * rather than SIGTERM: the runner is about to exit and cannot wait for a
211
+ * fallback, and a worker blocked in a native call never runs a SIGTERM
212
+ * handler (D652).
213
+ *
214
+ * EVERY worker is signalled before ANY line is written: a logger that throws
215
+ * must not stop the kills. A signalled worker is forgotten (a second pass
216
+ * logs nothing twice); one whose kill threw stays tracked, so the exit hook
217
+ * or a later pass tries it again. Returns how many it signalled.
218
+ */
219
+ killAll(reason, logger) {
220
+ const outcomes = [...this.live].map(signal);
221
+ for (const outcome of outcomes) if (outcome.error === null) this.live.delete(outcome.worker);
222
+ logOutcomes(outcomes, reason, logger);
223
+ return outcomes.filter((outcome) => outcome.error === null).length;
224
+ }
225
+ /** Kill everything tracked, and kill every later `add` at once. */
226
+ close(reason, logger) {
227
+ this.closedLogger = logger;
228
+ return this.killAll(reason, logger);
229
+ }
230
+ };
231
+ /**
232
+ * The runner's decode workers: every fork is registered in the set, and
233
+ * {@link shutdown} and any process exit kill whatever is still alive.
234
+ */
235
+ var RunnerDecodeWorkers = class {
236
+ deps;
237
+ live = new LiveDecodeWorkers();
238
+ exitHookArmed = false;
239
+ shutDown = false;
240
+ constructor(deps) {
241
+ this.deps = deps;
242
+ }
243
+ /** Fork one worker, registered and cost-claimed. */
244
+ fork = (source) => {
245
+ this.armExitHook();
246
+ return this.deps.fork(source, {
247
+ costRegistry: this.deps.costRegistry,
248
+ liveWorkers: this.live
249
+ });
250
+ };
251
+ /**
252
+ * The addon is shutting down: kill every worker, and every later fork at
253
+ * once. Synchronous, so it is done before the runner's `process.exit`.
254
+ */
255
+ shutdown() {
256
+ this.shutDown = true;
257
+ const killed = this.live.close("shutdown", this.deps.logger);
258
+ if (this.live.size === 0) this.disarmExitHook();
259
+ return killed;
260
+ }
261
+ /**
262
+ * The process is exiting without having shut this addon down (an uncaught
263
+ * error, or an `onShutdown` that never reached {@link shutdown}). The log
264
+ * line is attempted but will probably not land: the transport goes down
265
+ * with the process.
266
+ */
267
+ onProcessExit = () => {
268
+ this.exitHookArmed = false;
269
+ this.live.close("process-exit", this.deps.logger);
270
+ };
271
+ armExitHook() {
272
+ if (this.exitHookArmed || this.shutDown) return;
273
+ this.exitHookArmed = true;
274
+ this.deps.process.once("exit", this.onProcessExit);
275
+ }
276
+ disarmExitHook() {
277
+ if (!this.exitHookArmed) return;
278
+ this.exitHookArmed = false;
279
+ this.deps.process.removeListener("exit", this.onProcessExit);
280
+ }
281
+ };
118
282
  //#endregion
119
283
  //#region src/session-decode/native-lease-config.ts
120
284
  /**
@@ -1722,6 +1886,11 @@ var SessionDecodeSession = class {
1722
1886
  /** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
1723
1887
  killSent = false;
1724
1888
  /**
1889
+ * A SIGKILL already went out — an `'immediate'` teardown from the pump
1890
+ * (D666) and the generator's later `finally` both escalate; one kill is enough.
1891
+ */
1892
+ sigkillSent = false;
1893
+ /**
1725
1894
  * Images yielded and not yet closed that a CONSUMER still holds — queued,
1726
1895
  * in flight, or being read. Each dies with the worker and somebody was going
1727
1896
  * to read it first. This is what a drain waits on.
@@ -1933,7 +2102,7 @@ var SessionDecodeSession = class {
1933
2102
  }
1934
2103
  /** The drain (or a SIGTERM still waiting on its fallback) gives way to SIGKILL now. */
1935
2104
  escalateToImmediate() {
1936
- if (this.exited) return;
2105
+ if (this.exited || this.sigkillSent) return;
1937
2106
  if (this.drainTimer !== null) {
1938
2107
  clearTimeout(this.drainTimer);
1939
2108
  this.drainTimer = null;
@@ -2120,7 +2289,14 @@ var SessionDecodeSession = class {
2120
2289
  }
2121
2290
  handleExit(info) {
2122
2291
  if (!this.exited) this.hooks.onWorkerExit?.();
2123
- if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
2292
+ if (!this.killSent && !this.exited && info?.killedByRunner === true) this.logger.debug("session-decode worker ended by the runner shutdown kill", {
2293
+ tags: { deviceId: this.deviceId },
2294
+ meta: {
2295
+ code: info.code,
2296
+ signal: info.signal
2297
+ }
2298
+ });
2299
+ else if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
2124
2300
  tags: { deviceId: this.deviceId },
2125
2301
  meta: {
2126
2302
  code: info?.code ?? null,
@@ -2312,11 +2488,15 @@ var SessionDecodeSession = class {
2312
2488
  tryKill(signal) {
2313
2489
  try {
2314
2490
  this.child.kill(signal);
2491
+ if (signal === "SIGKILL") this.sigkillSent = true;
2315
2492
  } catch (err) {
2316
- this.logger.warn("session-decode-coordinator: kill() on child failed", { meta: {
2317
- signal,
2318
- err: toError(err).message
2319
- } });
2493
+ this.logger.warn("session-decode-coordinator: kill() on child failed", {
2494
+ tags: { deviceId: this.deviceId },
2495
+ meta: {
2496
+ signal,
2497
+ err: toError(err).message
2498
+ }
2499
+ });
2320
2500
  }
2321
2501
  }
2322
2502
  };
@@ -2586,6 +2766,7 @@ function startSessionDecodePump(deps) {
2586
2766
  stopped = true;
2587
2767
  teardownMode = mode;
2588
2768
  releaseSpawnSlotOnce();
2769
+ if (mode === "immediate") escalateSession?.("immediate");
2589
2770
  iterator.return().catch(() => {});
2590
2771
  };
2591
2772
  }
@@ -8009,6 +8190,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8009
8190
  */
8010
8191
  costRegistry = new require_node.ChildCostRegistry();
8011
8192
  /**
8193
+ * Every decode worker this process forked, from ANY spawn site (live
8194
+ * sessions, occupancy bursts), until its exit. The first thing `onShutdown`
8195
+ * does is kill them, a fork after that is killed at once, and any process
8196
+ * exit kills what is left (D666). The logger is only unready before
8197
+ * `initialize()`, when no worker can have been forked.
8198
+ */
8199
+ decodeWorkers = new RunnerDecodeWorkers({
8200
+ fork: forkDecodeWorker,
8201
+ costRegistry: this.costRegistry,
8202
+ logger: {
8203
+ info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
8204
+ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
8205
+ },
8206
+ process
8207
+ });
8208
+ /**
8012
8209
  * Per-camera inference-deadline counters reported through
8013
8210
  * `failure-contribution`. Bounded, cumulative, and read (never drained) by
8014
8211
  * the provider. Constructed eagerly: created on the first timeout it would
@@ -8458,6 +8655,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8458
8655
  };
8459
8656
  }
8460
8657
  async onShutdown() {
8658
+ this.decodeWorkers.shutdown();
8461
8659
  if (this.runtimeInfoTimer !== null) {
8462
8660
  clearInterval(this.runtimeInfoTimer);
8463
8661
  this.runtimeInfoTimer = null;
@@ -10008,7 +10206,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10008
10206
  * environment. A worker is therefore forkable before anyone knows which
10009
10207
  * camera it will serve.
10010
10208
  */
10011
- forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.costRegistry);
10209
+ forkDecodeWorkerWithLease = (source) => this.decodeWorkers.fork(source);
10012
10210
  /**
10013
10211
  * The lease knobs for the session that is about to start — `setting > env >
10014
10212
  * default`, resolved HERE because this process is the one that carries the
@@ -34,14 +34,18 @@ var KILL_SIGNALS = {
34
34
  * parameter rather than a second `child.on('exit')` in the caller because
35
35
  * every listener added out here is one somebody can forget to remove.
36
36
  */
37
- function wrapChild(child, onExit) {
37
+ function wrapChild(child, onExit, killedByRunner = () => false) {
38
38
  let spawned = false;
39
39
  let exitDelivered = false;
40
40
  const exitListeners = [];
41
- const deliverExit = (info) => {
41
+ const deliverExit = (exit) => {
42
42
  if (exitDelivered) return;
43
43
  exitDelivered = true;
44
44
  onExit();
45
+ const info = killedByRunner() ? {
46
+ ...exit,
47
+ killedByRunner: true
48
+ } : exit;
45
49
  for (const listener of exitListeners) listener(info);
46
50
  };
47
51
  child.on("spawn", () => {
@@ -95,19 +99,179 @@ function wrapChild(child, onExit) {
95
99
  * node-av through `CAMSTACK_FRAMEWORK_DIR` / `CAMSTACK_BUNDLED_ADDONS_DIR` on
96
100
  * agents, so a replaced env would silently break decoding off the hub.
97
101
  */
98
- function forkDecodeWorker(source, costRegistry) {
99
- const child = fork(CHILD_ENTRY_PATH, [], {
102
+ function forkDecodeWorker(source, deps) {
103
+ return forkDecodeWorkerAt(CHILD_ENTRY_PATH, source, deps ?? {});
104
+ }
105
+ /**
106
+ * {@link forkDecodeWorker} with the entry named — the one seam a process-level
107
+ * test needs to fork a stub through the REAL fork, claim and live-worker path.
108
+ * Production forks only {@link CHILD_ENTRY_PATH}.
109
+ */
110
+ function forkDecodeWorkerAt(entryPath, source, deps) {
111
+ const child = fork(entryPath, [], {
100
112
  serialization: "advanced",
101
113
  env: { ...process.env }
102
114
  });
103
- const costClaim = costRegistry?.claim({
115
+ const costClaim = deps.costRegistry?.claim({
104
116
  role: "decode",
105
117
  deviceId: source.deviceId,
106
118
  unit: `${source.deviceId}/decode`,
107
119
  pid: child.pid
108
120
  }) ?? NO_COST_CLAIM;
109
- return wrapChild(child, () => costClaim.release());
121
+ let killedByRunner = false;
122
+ const untrack = deps.liveWorkers?.add({
123
+ deviceId: source.deviceId,
124
+ pid: child.pid,
125
+ killNow: () => {
126
+ killedByRunner = true;
127
+ child.kill("SIGKILL");
128
+ }
129
+ }) ?? (() => void 0);
130
+ return wrapChild(child, () => {
131
+ costClaim.release();
132
+ untrack();
133
+ }, () => killedByRunner);
134
+ }
135
+ //#endregion
136
+ //#region src/session-decode/live-decode-workers.ts
137
+ var KILL_MESSAGE = {
138
+ shutdown: "session-decode: worker killed at shutdown",
139
+ "forked-after-shutdown": "session-decode: worker forked after shutdown began — killed at once",
140
+ "process-exit": "session-decode: worker killed at process exit"
141
+ };
142
+ /** Send the signal. Never throws: a failure is the outcome. */
143
+ function signal(worker) {
144
+ try {
145
+ worker.killNow();
146
+ return {
147
+ worker,
148
+ error: null
149
+ };
150
+ } catch (err) {
151
+ return {
152
+ worker,
153
+ error: errMsg(err)
154
+ };
155
+ }
156
+ }
157
+ /**
158
+ * One line per outcome, each in its own swallowing try. The logger may throw:
159
+ * the `'exit'` hook runs while the transport goes down, and the runner's
160
+ * write path has no guard. A line that fails must not cost another line.
161
+ */
162
+ function logOutcomes(outcomes, reason, logger) {
163
+ for (const { worker, error } of outcomes) try {
164
+ if (error === null) logger.info(KILL_MESSAGE[reason], {
165
+ tags: { deviceId: worker.deviceId },
166
+ meta: {
167
+ pid: worker.pid ?? null,
168
+ signal: "SIGKILL",
169
+ reason
170
+ }
171
+ });
172
+ else logger.warn("session-decode: worker kill failed — it may outlive the runner", {
173
+ tags: { deviceId: worker.deviceId },
174
+ meta: {
175
+ pid: worker.pid ?? null,
176
+ reason,
177
+ error
178
+ }
179
+ });
180
+ } catch {}
110
181
  }
182
+ var LiveDecodeWorkers = class {
183
+ live = /* @__PURE__ */ new Set();
184
+ /** Set by {@link close}: from then on a worker is killed as it is added. */
185
+ closedLogger = null;
186
+ /** Track a forked worker; call the returned function from its real exit. */
187
+ add(worker) {
188
+ if (this.closedLogger !== null) {
189
+ const outcome = signal(worker);
190
+ logOutcomes([outcome], "forked-after-shutdown", this.closedLogger);
191
+ if (outcome.error === null) return () => void 0;
192
+ }
193
+ this.live.add(worker);
194
+ return () => {
195
+ this.live.delete(worker);
196
+ };
197
+ }
198
+ get size() {
199
+ return this.live.size;
200
+ }
201
+ /**
202
+ * SIGKILL every tracked worker now, then one log line per worker. SIGKILL
203
+ * rather than SIGTERM: the runner is about to exit and cannot wait for a
204
+ * fallback, and a worker blocked in a native call never runs a SIGTERM
205
+ * handler (D652).
206
+ *
207
+ * EVERY worker is signalled before ANY line is written: a logger that throws
208
+ * must not stop the kills. A signalled worker is forgotten (a second pass
209
+ * logs nothing twice); one whose kill threw stays tracked, so the exit hook
210
+ * or a later pass tries it again. Returns how many it signalled.
211
+ */
212
+ killAll(reason, logger) {
213
+ const outcomes = [...this.live].map(signal);
214
+ for (const outcome of outcomes) if (outcome.error === null) this.live.delete(outcome.worker);
215
+ logOutcomes(outcomes, reason, logger);
216
+ return outcomes.filter((outcome) => outcome.error === null).length;
217
+ }
218
+ /** Kill everything tracked, and kill every later `add` at once. */
219
+ close(reason, logger) {
220
+ this.closedLogger = logger;
221
+ return this.killAll(reason, logger);
222
+ }
223
+ };
224
+ /**
225
+ * The runner's decode workers: every fork is registered in the set, and
226
+ * {@link shutdown} and any process exit kill whatever is still alive.
227
+ */
228
+ var RunnerDecodeWorkers = class {
229
+ deps;
230
+ live = new LiveDecodeWorkers();
231
+ exitHookArmed = false;
232
+ shutDown = false;
233
+ constructor(deps) {
234
+ this.deps = deps;
235
+ }
236
+ /** Fork one worker, registered and cost-claimed. */
237
+ fork = (source) => {
238
+ this.armExitHook();
239
+ return this.deps.fork(source, {
240
+ costRegistry: this.deps.costRegistry,
241
+ liveWorkers: this.live
242
+ });
243
+ };
244
+ /**
245
+ * The addon is shutting down: kill every worker, and every later fork at
246
+ * once. Synchronous, so it is done before the runner's `process.exit`.
247
+ */
248
+ shutdown() {
249
+ this.shutDown = true;
250
+ const killed = this.live.close("shutdown", this.deps.logger);
251
+ if (this.live.size === 0) this.disarmExitHook();
252
+ return killed;
253
+ }
254
+ /**
255
+ * The process is exiting without having shut this addon down (an uncaught
256
+ * error, or an `onShutdown` that never reached {@link shutdown}). The log
257
+ * line is attempted but will probably not land: the transport goes down
258
+ * with the process.
259
+ */
260
+ onProcessExit = () => {
261
+ this.exitHookArmed = false;
262
+ this.live.close("process-exit", this.deps.logger);
263
+ };
264
+ armExitHook() {
265
+ if (this.exitHookArmed || this.shutDown) return;
266
+ this.exitHookArmed = true;
267
+ this.deps.process.once("exit", this.onProcessExit);
268
+ }
269
+ disarmExitHook() {
270
+ if (!this.exitHookArmed) return;
271
+ this.exitHookArmed = false;
272
+ this.deps.process.removeListener("exit", this.onProcessExit);
273
+ }
274
+ };
111
275
  //#endregion
112
276
  //#region src/session-decode/native-lease-config.ts
113
277
  /**
@@ -1715,6 +1879,11 @@ var SessionDecodeSession = class {
1715
1879
  /** Set when `kill()` is sent, so handleExit can tell an intentional stop from a death. */
1716
1880
  killSent = false;
1717
1881
  /**
1882
+ * A SIGKILL already went out — an `'immediate'` teardown from the pump
1883
+ * (D666) and the generator's later `finally` both escalate; one kill is enough.
1884
+ */
1885
+ sigkillSent = false;
1886
+ /**
1718
1887
  * Images yielded and not yet closed that a CONSUMER still holds — queued,
1719
1888
  * in flight, or being read. Each dies with the worker and somebody was going
1720
1889
  * to read it first. This is what a drain waits on.
@@ -1926,7 +2095,7 @@ var SessionDecodeSession = class {
1926
2095
  }
1927
2096
  /** The drain (or a SIGTERM still waiting on its fallback) gives way to SIGKILL now. */
1928
2097
  escalateToImmediate() {
1929
- if (this.exited) return;
2098
+ if (this.exited || this.sigkillSent) return;
1930
2099
  if (this.drainTimer !== null) {
1931
2100
  clearTimeout(this.drainTimer);
1932
2101
  this.drainTimer = null;
@@ -2113,7 +2282,14 @@ var SessionDecodeSession = class {
2113
2282
  }
2114
2283
  handleExit(info) {
2115
2284
  if (!this.exited) this.hooks.onWorkerExit?.();
2116
- if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
2285
+ if (!this.killSent && !this.exited && info?.killedByRunner === true) this.logger.debug("session-decode worker ended by the runner shutdown kill", {
2286
+ tags: { deviceId: this.deviceId },
2287
+ meta: {
2288
+ code: info.code,
2289
+ signal: info.signal
2290
+ }
2291
+ });
2292
+ else if (!this.killSent && !this.exited) this.logger.warn(this.stopRequested ? "session-decode worker exited unexpectedly while draining" : "session-decode worker exited unexpectedly", {
2117
2293
  tags: { deviceId: this.deviceId },
2118
2294
  meta: {
2119
2295
  code: info?.code ?? null,
@@ -2305,11 +2481,15 @@ var SessionDecodeSession = class {
2305
2481
  tryKill(signal) {
2306
2482
  try {
2307
2483
  this.child.kill(signal);
2484
+ if (signal === "SIGKILL") this.sigkillSent = true;
2308
2485
  } catch (err) {
2309
- this.logger.warn("session-decode-coordinator: kill() on child failed", { meta: {
2310
- signal,
2311
- err: toError(err).message
2312
- } });
2486
+ this.logger.warn("session-decode-coordinator: kill() on child failed", {
2487
+ tags: { deviceId: this.deviceId },
2488
+ meta: {
2489
+ signal,
2490
+ err: toError(err).message
2491
+ }
2492
+ });
2313
2493
  }
2314
2494
  }
2315
2495
  };
@@ -2579,6 +2759,7 @@ function startSessionDecodePump(deps) {
2579
2759
  stopped = true;
2580
2760
  teardownMode = mode;
2581
2761
  releaseSpawnSlotOnce();
2762
+ if (mode === "immediate") escalateSession?.("immediate");
2582
2763
  iterator.return().catch(() => {});
2583
2764
  };
2584
2765
  }
@@ -8001,6 +8182,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8001
8182
  */
8002
8183
  costRegistry = new ChildCostRegistry();
8003
8184
  /**
8185
+ * Every decode worker this process forked, from ANY spawn site (live
8186
+ * sessions, occupancy bursts), until its exit. The first thing `onShutdown`
8187
+ * does is kill them, a fork after that is killed at once, and any process
8188
+ * exit kills what is left (D666). The logger is only unready before
8189
+ * `initialize()`, when no worker can have been forked.
8190
+ */
8191
+ decodeWorkers = new RunnerDecodeWorkers({
8192
+ fork: forkDecodeWorker,
8193
+ costRegistry: this.costRegistry,
8194
+ logger: {
8195
+ info: (message, extras) => this.ctxIfReady?.logger.info(message, extras),
8196
+ warn: (message, extras) => this.ctxIfReady?.logger.warn(message, extras)
8197
+ },
8198
+ process
8199
+ });
8200
+ /**
8004
8201
  * Per-camera inference-deadline counters reported through
8005
8202
  * `failure-contribution`. Bounded, cumulative, and read (never drained) by
8006
8203
  * the provider. Constructed eagerly: created on the first timeout it would
@@ -8450,6 +8647,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8450
8647
  };
8451
8648
  }
8452
8649
  async onShutdown() {
8650
+ this.decodeWorkers.shutdown();
8453
8651
  if (this.runtimeInfoTimer !== null) {
8454
8652
  clearInterval(this.runtimeInfoTimer);
8455
8653
  this.runtimeInfoTimer = null;
@@ -10000,7 +10198,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10000
10198
  * environment. A worker is therefore forkable before anyone knows which
10001
10199
  * camera it will serve.
10002
10200
  */
10003
- forkDecodeWorkerWithLease = (source) => forkDecodeWorker(source, this.costRegistry);
10201
+ forkDecodeWorkerWithLease = (source) => this.decodeWorkers.fork(source);
10004
10202
  /**
10005
10203
  * The lease knobs for the session that is about to start — `setting > env >
10006
10204
  * default`, resolved HERE because this process is the one that carries the