@camstack/addon-pipeline 1.2.221 → 1.2.223

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 (28) hide show
  1. package/dist/audio-analyzer/index.js +55 -16
  2. package/dist/audio-analyzer/index.mjs +55 -16
  3. package/dist/{default-detection-model-B1NT1A38.js → default-detection-model-B7kn9xHR.js} +1 -1
  4. package/dist/{default-detection-model-DkVFgubU.mjs → default-detection-model-BSX8cVHY.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-CEFC3PkP.mjs → dist-BNzfbpF6.mjs} +31 -2
  8. package/dist/{dist-ATDfcxgL.js → dist-DZsjVFey.js} +31 -2
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-C5jnTgpv.js → node-BhQ8dDQC.js} +1 -1
  12. package/dist/{node-DR3fHqUq.mjs → node-uYIx2aZF.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +139 -20
  14. package/dist/pipeline-runner/index.mjs +139 -20
  15. package/dist/{process-memory-DQbCPnMA.js → process-memory-BozKb5-g.js} +1 -1
  16. package/dist/{process-memory-DqYj5IAb.mjs → process-memory-CkoLpOV-.mjs} +1 -1
  17. package/dist/recorder/index.js +90 -27
  18. package/dist/recorder/index.mjs +90 -27
  19. package/dist/{segment-demux-js-o9KBAz9p.js → segment-demux-js-Cvszh55p.js} +1 -1
  20. package/dist/{segment-demux-js-CddDwD69.mjs → segment-demux-js-Dh1dn_AT.mjs} +1 -1
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BZG3prqU.mjs} +3 -3
  22. package/dist/stream-broker/demux-worker-child.js +1 -1
  23. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  24. package/dist/stream-broker/{hostInit-C-VdcJS2.mjs → hostInit-Cyo0CFo3.mjs} +3 -3
  25. package/dist/stream-broker/index.js +96 -54
  26. package/dist/stream-broker/index.mjs +96 -54
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/package.json +1 -1
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-ATDfcxgL.js");
7
- const require_node = require("../node-C5jnTgpv.js");
8
- const require_default_detection_model = require("../default-detection-model-B1NT1A38.js");
6
+ const require_dist = require("../dist-DZsjVFey.js");
7
+ const require_node = require("../node-BhQ8dDQC.js");
8
+ const require_default_detection_model = require("../default-detection-model-B7kn9xHR.js");
9
9
  const require_worker_protocol = require("../worker-protocol-B2MfQLlu.js");
10
10
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
11
11
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
@@ -4782,9 +4782,23 @@ function shouldRetainDetectionFrame(input) {
4782
4782
  if (input.continuousRetain) return true;
4783
4783
  return input.hasDetection;
4784
4784
  }
4785
+ /**
4786
+ * The ONE place the cross-node crop wire format is decided (D462).
4787
+ *
4788
+ * `jpeg` arrives as the bytes the encoder produced. Whether they leave as
4789
+ * those bytes or as a base64 string is not this function's preference — it is
4790
+ * the CALLER'S declaration, `acceptBytes`, and an absent declaration means the
4791
+ * old wire. A pre-D462 consumer does `Buffer.from(reply.jpeg, 'base64')`, so
4792
+ * reading a missing flag as consent would hand it a `Uint8Array` to parse as
4793
+ * base64 and it would decode to garbage rather than fail.
4794
+ *
4795
+ * Base64 cost ~400 MB/min of hub-main relay traffic and, on the decoded half
4796
+ * of that, a multi-megabyte string in old space per call.
4797
+ */
4785
4798
  function encodeCrossNodeCrop(input) {
4786
- if (!input.jpeg) return null;
4787
- return { jpeg: input.jpeg };
4799
+ if (!input.jpeg || input.jpeg.byteLength === 0) return null;
4800
+ if (input.acceptBytes === true) return { jpegBytes: input.jpeg };
4801
+ return { jpeg: Buffer.from(input.jpeg).toString("base64") };
4788
4802
  }
4789
4803
  function handleKey(handle) {
4790
4804
  return `${handle.shmId}#${handle.slot}#${handle.seq}`;
@@ -6491,6 +6505,37 @@ function isUsable(bbox) {
6491
6505
  return bbox.width > 0 && bbox.height > 0;
6492
6506
  }
6493
6507
  //#endregion
6508
+ //#region src/pipeline-runner/runtime-labels.ts
6509
+ /**
6510
+ * `null` means the read FAILED; `[]` means nothing is resident. Both omit the
6511
+ * fields, but they are different facts and the caller logs the failure.
6512
+ *
6513
+ * The `runtime` pool is named first because it is the one serving cameras; the
6514
+ * rest follow in arrival order. `python` is deliberately NOT printed: every
6515
+ * engine available on this cluster is python, and a token that cannot vary
6516
+ * costs a column and tells the reader nothing.
6517
+ */
6518
+ function buildRuntimeLabels(engines) {
6519
+ if (engines === null || engines.length === 0) return {};
6520
+ const ordered = [...engines.filter((e) => e.kind === "runtime"), ...engines.filter((e) => e.kind !== "runtime")];
6521
+ const devices = [...new Set(ordered.map((e) => e.device).filter((d) => !!d))];
6522
+ const backends = [...new Set(ordered.map((e) => e.backend))];
6523
+ const backend = backends.length === 1 ? backends[0] : "mixed";
6524
+ const runtime = devices.length === 0 ? backend : `${backend}/${devices.join("+")}`;
6525
+ const primary = ordered[0];
6526
+ if (primary === void 0) return {};
6527
+ const models = [];
6528
+ for (const engine of ordered) for (const model of engine.modelsLoaded) {
6529
+ if (models.includes(model)) continue;
6530
+ if (engine === primary || primary.modelsLoaded.includes(model)) models.push(model);
6531
+ else models.push(engine.device === void 0 ? model : `${model}@${engine.device}`);
6532
+ }
6533
+ return {
6534
+ pipelineRuntime: runtime,
6535
+ pipelineModels: models
6536
+ };
6537
+ }
6538
+ //#endregion
6494
6539
  //#region src/pipeline-runner/track-frame-park.ts
6495
6540
  /** Park one frame for one track. Never throws; a refusal is an answer. */
6496
6541
  async function parkTrackFrame(deps, input) {
@@ -6644,6 +6689,14 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
6644
6689
  var MAX_BOUND_FRAME_CUTTERS = 256;
6645
6690
  /** How often a camera reports its undispatched cuts. */
6646
6691
  var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
6692
+ /**
6693
+ * How often the `pipeline stats` runtime labels are re-read (D463). Slow on
6694
+ * purpose: this answers "which engine is this node on", which changes when an
6695
+ * operator re-selects one or lazy provisioning completes — not per frame.
6696
+ */
6697
+ var RUNTIME_INFO_REFRESH_MS = 3e5;
6698
+ /** Sentinel for "no api" so it is one transition, not a per-refresh line. */
6699
+ var NO_API_FAILURE = "no addon api";
6647
6700
  /** How often a camera reports parcels that left without a release (D422). */
6648
6701
  var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
6649
6702
  /**
@@ -7080,6 +7133,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7080
7133
  parkedDetail = /* @__PURE__ */ new Map();
7081
7134
  nodeId = "unknown";
7082
7135
  metricsSnapshotTimer = null;
7136
+ runtimeInfoTimer = null;
7137
+ /** Last runtime-label failure, so only a CHANGE is logged (D463). */
7138
+ lastRuntimeInfoFailure = null;
7083
7139
  unsubMotionEvents = null;
7084
7140
  /** Devices we have positively observed reporting `battery.sleeping: true`.
7085
7141
  * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
@@ -7331,6 +7387,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7331
7387
  workerFootprintMb: decodeWorkerFootprintMb(this.nativeLease.current())
7332
7388
  });
7333
7389
  this.runner.timingSampler.setLogger(this.ctx.logger.child("timing"));
7390
+ this.refreshRuntimeInfo();
7391
+ this.runtimeInfoTimer = setInterval(() => void this.refreshRuntimeInfo(), RUNTIME_INFO_REFRESH_MS);
7392
+ this.runtimeInfoTimer.unref?.();
7334
7393
  this.runner.onDetectionStreamChange((deviceId, action) => {
7335
7394
  this.handleDetectionStreamChange(deviceId, action);
7336
7395
  });
@@ -7397,6 +7456,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7397
7456
  };
7398
7457
  }
7399
7458
  async onShutdown() {
7459
+ if (this.runtimeInfoTimer !== null) {
7460
+ clearInterval(this.runtimeInfoTimer);
7461
+ this.runtimeInfoTimer = null;
7462
+ }
7400
7463
  if (this.metricsSnapshotTimer) {
7401
7464
  clearInterval(this.metricsSnapshotTimer);
7402
7465
  this.metricsSnapshotTimer = null;
@@ -8204,7 +8267,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8204
8267
  const resolved = await this.resolveNativeCropPixels(input);
8205
8268
  if (resolved.tier === "native" && resolved.pixels !== null) {
8206
8269
  this.frameLazy.noteHit(resolveNativeCropHitKind(resolved.pixels.source), deviceId);
8207
- return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
8270
+ return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg, input.acceptJpegBytes);
8208
8271
  }
8209
8272
  this.frameLazy.noteMiss("native", deviceId);
8210
8273
  if (resolved.tier !== "ram-fullframe") return null;
@@ -8230,7 +8293,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8230
8293
  bytes: crop.bytes,
8231
8294
  width: crop.width,
8232
8295
  height: crop.height
8233
- }, resolved.tier, input.encodeJpeg);
8296
+ }, resolved.tier, input.encodeJpeg, input.acceptJpegBytes);
8234
8297
  }
8235
8298
  /**
8236
8299
  * PARK a frame for a track (D422) — the hub judged it relevant, and this node
@@ -8253,8 +8316,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8253
8316
  store,
8254
8317
  resolvePixels: (request) => this.resolveNativeCropPixels(request),
8255
8318
  encodeJpeg: async (pixels) => {
8256
- const encoded = await this.encodeNativeCropResult(pixels, "native", true);
8257
- return encoded?.jpeg !== void 0 ? Buffer.from(encoded.jpeg, "base64") : null;
8319
+ const encoded = await this.encodeNativeCropResult(pixels, "native", true, true);
8320
+ return encoded?.jpegBytes !== void 0 ? Buffer.from(encoded.jpegBytes) : null;
8258
8321
  },
8259
8322
  logger: this.ctx.logger
8260
8323
  }, input);
@@ -8277,7 +8340,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8277
8340
  return null;
8278
8341
  }
8279
8342
  return {
8280
- jpeg: parcel.jpeg.toString("base64"),
8343
+ ...input.acceptJpegBytes === true ? { jpegBytes: new Uint8Array(parcel.jpeg) } : { jpeg: parcel.jpeg.toString("base64") },
8281
8344
  width: parcel.width,
8282
8345
  height: parcel.height,
8283
8346
  timestamp: parcel.timestamp,
@@ -8657,12 +8720,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8657
8720
  * boundary (CLAUDE.md invariant #21). On an encode failure the crop is a miss
8658
8721
  * (`null`) — the caller retries — never a silent raw-RGB cross-node fallback.
8659
8722
  */
8660
- async encodeNativeCropResult(crop, tier, encodeJpeg) {
8723
+ async encodeNativeCropResult(crop, tier, encodeJpeg, acceptJpegBytes) {
8661
8724
  if (crop.bytes.byteLength >= 2 && crop.bytes[0] === 255 && crop.bytes[1] === 216) {
8662
- const encoded = encodeCrossNodeCrop({ jpeg: Buffer.from(crop.bytes).toString("base64") });
8725
+ const encoded = encodeCrossNodeCrop({
8726
+ jpeg: crop.bytes,
8727
+ acceptBytes: acceptJpegBytes
8728
+ });
8663
8729
  if (!encoded) return null;
8664
8730
  return {
8665
- jpeg: encoded.jpeg,
8731
+ ...encoded,
8666
8732
  width: crop.width,
8667
8733
  height: crop.height,
8668
8734
  tier
@@ -8676,16 +8742,17 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8676
8742
  };
8677
8743
  try {
8678
8744
  const encoded = encodeCrossNodeCrop({
8679
- jpeg: (await (await require_lazy_sharp.getSharp())(Buffer.from(crop.bytes), { raw: {
8745
+ jpeg: await (await require_lazy_sharp.getSharp())(Buffer.from(crop.bytes), { raw: {
8680
8746
  width: crop.width,
8681
8747
  height: crop.height,
8682
8748
  channels: 3
8683
- } }).jpeg({ quality: 90 }).toBuffer()).toString("base64"),
8684
- bytes: crop.bytes
8749
+ } }).jpeg({ quality: 90 }).toBuffer(),
8750
+ bytes: crop.bytes,
8751
+ acceptBytes: acceptJpegBytes
8685
8752
  });
8686
8753
  if (!encoded) return null;
8687
8754
  return {
8688
- jpeg: encoded.jpeg,
8755
+ ...encoded,
8689
8756
  width: crop.width,
8690
8757
  height: crop.height,
8691
8758
  tier
@@ -8742,6 +8809,58 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8742
8809
  }, input);
8743
8810
  }
8744
8811
  /**
8812
+ * Re-read the labels the `pipeline stats` lines carry (D463).
8813
+ *
8814
+ * Each half is read INDEPENDENTLY and a failure omits only its own field:
8815
+ * an engine that will not answer must not also erase the model list. Both
8816
+ * reads are best-effort by design — this is a label on a telemetry line, and
8817
+ * it must never be able to take the runner down or to log a storm. A failure
8818
+ * is logged once per refresh at `debug`, deliberately: the ABSENCE of the
8819
+ * field in the line is the signal, and the hub writes `info`.
8820
+ */
8821
+ async refreshRuntimeInfo() {
8822
+ const api = this.ctxIfReady?.api;
8823
+ const sampler = this.runner?.timingSampler;
8824
+ if (!api || sampler === void 0 || sampler === null) {
8825
+ this.noteRuntimeInfoFailure(NO_API_FAILURE);
8826
+ return;
8827
+ }
8828
+ let engines = null;
8829
+ let failure = null;
8830
+ try {
8831
+ engines = (await api.pipelineExecutor.listLoadedEngines.query()).map((r) => ({
8832
+ backend: r.engine.backend,
8833
+ ...r.engine.device !== void 0 ? { device: r.engine.device } : {},
8834
+ kind: r.kind,
8835
+ modelsLoaded: r.modelsLoaded
8836
+ }));
8837
+ } catch (err) {
8838
+ failure = require_dist.errMsg(err);
8839
+ }
8840
+ this.noteRuntimeInfoFailure(failure);
8841
+ sampler.runtimeInfo = {
8842
+ ...sampler.runtimeInfo,
8843
+ ...buildRuntimeLabels(engines)
8844
+ };
8845
+ }
8846
+ /**
8847
+ * Log a runtime-label failure at its TRANSITION, and at `warn`.
8848
+ *
8849
+ * This branch first shipped at `debug`, on the argument that the absent field
8850
+ * in the line was signal enough. It was not: within the hour `little-unraid`
8851
+ * was the one node still logging `pipelineRuntime=null` and there was no way
8852
+ * to tell "this node has no engine" from "the read threw", because the hub
8853
+ * writes `info` and the agent forwards to it. That is D438's blind
8854
+ * instrument for the third time. Logging the transition rather than every
8855
+ * refresh is what keeps it from becoming a storm.
8856
+ */
8857
+ noteRuntimeInfoFailure(failure) {
8858
+ if (failure === this.lastRuntimeInfoFailure) return;
8859
+ if (failure === null) this.ctxIfReady?.logger.info("runtime labels: readable again");
8860
+ else this.ctxIfReady?.logger.warn("runtime labels unreadable — this node cannot name its engines", { meta: { error: failure } });
8861
+ this.lastRuntimeInfoFailure = failure;
8862
+ }
8863
+ /**
8745
8864
  * The model format this node dispatches on when no `deviceKey` is given —
8746
8865
  * its selected engine's format. `null` on any failure, which the caller
8747
8866
  * turns into a refusal rather than a guess: a model pin that cannot be
@@ -8970,13 +9089,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
8970
9089
  handleAgeMs: resolved.handleAgeMs,
8971
9090
  ...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
8972
9091
  };
8973
- const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
8974
- if (!encoded?.jpeg) return {
9092
+ const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true, true);
9093
+ if (!encoded?.jpegBytes) return {
8975
9094
  miss: "jpeg-encode-failed",
8976
9095
  handleAgeMs: resolved.handleAgeMs
8977
9096
  };
8978
9097
  return {
8979
- jpeg: Buffer.from(encoded.jpeg, "base64"),
9098
+ jpeg: Buffer.from(encoded.jpegBytes),
8980
9099
  width: encoded.width,
8981
9100
  height: encoded.height,
8982
9101
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
@@ -1,6 +1,6 @@
1
- import { H as defineCustomActions, Lt as errMsg, W as deriveDetailCropRect, Wt as BaseAddon, Yt as createEvent, _ as FailureCounters, at as loadContributionCapability, b as NativeLeaseAdmissionSchema, cn as boolean, dn as literal, et as failureContributionCapability, fn as number, gn as string, m as DEVICE_BACKEND_TO_FORMAT, on as _enum, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as object, sn as array, tn as nodePin, u as DEFAULT_DETAIL_CROP_CONVENTION, un as lazy, vn as EventCategory, vt as pickDetailCropConvention, x as NativeLeaseSettingsSchema, xt as pipelineRunnerCapability, yt as pickNativeLeaseOverride, z as customAction } from "../dist-CEFC3PkP.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DR3fHqUq.mjs";
3
- import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-DkVFgubU.mjs";
1
+ import { H as defineCustomActions, Lt as errMsg, W as deriveDetailCropRect, Wt as BaseAddon, Yt as createEvent, _ as FailureCounters, at as loadContributionCapability, b as NativeLeaseAdmissionSchema, cn as boolean, dn as literal, et as failureContributionCapability, fn as number, gn as string, m as DEVICE_BACKEND_TO_FORMAT, on as _enum, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as object, sn as array, tn as nodePin, u as DEFAULT_DETAIL_CROP_CONVENTION, un as lazy, vn as EventCategory, vt as pickDetailCropConvention, x as NativeLeaseSettingsSchema, xt as pipelineRunnerCapability, yt as pickNativeLeaseOverride, z as customAction } from "../dist-BNzfbpF6.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-uYIx2aZF.mjs";
3
+ import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-BSX8cVHY.mjs";
4
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";
@@ -4774,9 +4774,23 @@ function shouldRetainDetectionFrame(input) {
4774
4774
  if (input.continuousRetain) return true;
4775
4775
  return input.hasDetection;
4776
4776
  }
4777
+ /**
4778
+ * The ONE place the cross-node crop wire format is decided (D462).
4779
+ *
4780
+ * `jpeg` arrives as the bytes the encoder produced. Whether they leave as
4781
+ * those bytes or as a base64 string is not this function's preference — it is
4782
+ * the CALLER'S declaration, `acceptBytes`, and an absent declaration means the
4783
+ * old wire. A pre-D462 consumer does `Buffer.from(reply.jpeg, 'base64')`, so
4784
+ * reading a missing flag as consent would hand it a `Uint8Array` to parse as
4785
+ * base64 and it would decode to garbage rather than fail.
4786
+ *
4787
+ * Base64 cost ~400 MB/min of hub-main relay traffic and, on the decoded half
4788
+ * of that, a multi-megabyte string in old space per call.
4789
+ */
4777
4790
  function encodeCrossNodeCrop(input) {
4778
- if (!input.jpeg) return null;
4779
- return { jpeg: input.jpeg };
4791
+ if (!input.jpeg || input.jpeg.byteLength === 0) return null;
4792
+ if (input.acceptBytes === true) return { jpegBytes: input.jpeg };
4793
+ return { jpeg: Buffer.from(input.jpeg).toString("base64") };
4780
4794
  }
4781
4795
  function handleKey(handle) {
4782
4796
  return `${handle.shmId}#${handle.slot}#${handle.seq}`;
@@ -6483,6 +6497,37 @@ function isUsable(bbox) {
6483
6497
  return bbox.width > 0 && bbox.height > 0;
6484
6498
  }
6485
6499
  //#endregion
6500
+ //#region src/pipeline-runner/runtime-labels.ts
6501
+ /**
6502
+ * `null` means the read FAILED; `[]` means nothing is resident. Both omit the
6503
+ * fields, but they are different facts and the caller logs the failure.
6504
+ *
6505
+ * The `runtime` pool is named first because it is the one serving cameras; the
6506
+ * rest follow in arrival order. `python` is deliberately NOT printed: every
6507
+ * engine available on this cluster is python, and a token that cannot vary
6508
+ * costs a column and tells the reader nothing.
6509
+ */
6510
+ function buildRuntimeLabels(engines) {
6511
+ if (engines === null || engines.length === 0) return {};
6512
+ const ordered = [...engines.filter((e) => e.kind === "runtime"), ...engines.filter((e) => e.kind !== "runtime")];
6513
+ const devices = [...new Set(ordered.map((e) => e.device).filter((d) => !!d))];
6514
+ const backends = [...new Set(ordered.map((e) => e.backend))];
6515
+ const backend = backends.length === 1 ? backends[0] : "mixed";
6516
+ const runtime = devices.length === 0 ? backend : `${backend}/${devices.join("+")}`;
6517
+ const primary = ordered[0];
6518
+ if (primary === void 0) return {};
6519
+ const models = [];
6520
+ for (const engine of ordered) for (const model of engine.modelsLoaded) {
6521
+ if (models.includes(model)) continue;
6522
+ if (engine === primary || primary.modelsLoaded.includes(model)) models.push(model);
6523
+ else models.push(engine.device === void 0 ? model : `${model}@${engine.device}`);
6524
+ }
6525
+ return {
6526
+ pipelineRuntime: runtime,
6527
+ pipelineModels: models
6528
+ };
6529
+ }
6530
+ //#endregion
6486
6531
  //#region src/pipeline-runner/track-frame-park.ts
6487
6532
  /** Park one frame for one track. Never throws; a refusal is an answer. */
6488
6533
  async function parkTrackFrame(deps, input) {
@@ -6636,6 +6681,14 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
6636
6681
  var MAX_BOUND_FRAME_CUTTERS = 256;
6637
6682
  /** How often a camera reports its undispatched cuts. */
6638
6683
  var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
6684
+ /**
6685
+ * How often the `pipeline stats` runtime labels are re-read (D463). Slow on
6686
+ * purpose: this answers "which engine is this node on", which changes when an
6687
+ * operator re-selects one or lazy provisioning completes — not per frame.
6688
+ */
6689
+ var RUNTIME_INFO_REFRESH_MS = 3e5;
6690
+ /** Sentinel for "no api" so it is one transition, not a per-refresh line. */
6691
+ var NO_API_FAILURE = "no addon api";
6639
6692
  /** How often a camera reports parcels that left without a release (D422). */
6640
6693
  var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
6641
6694
  /**
@@ -7072,6 +7125,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7072
7125
  parkedDetail = /* @__PURE__ */ new Map();
7073
7126
  nodeId = "unknown";
7074
7127
  metricsSnapshotTimer = null;
7128
+ runtimeInfoTimer = null;
7129
+ /** Last runtime-label failure, so only a CHANGE is logged (D463). */
7130
+ lastRuntimeInfoFailure = null;
7075
7131
  unsubMotionEvents = null;
7076
7132
  /** Devices we have positively observed reporting `battery.sleeping: true`.
7077
7133
  * Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
@@ -7323,6 +7379,9 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7323
7379
  workerFootprintMb: decodeWorkerFootprintMb(this.nativeLease.current())
7324
7380
  });
7325
7381
  this.runner.timingSampler.setLogger(this.ctx.logger.child("timing"));
7382
+ this.refreshRuntimeInfo();
7383
+ this.runtimeInfoTimer = setInterval(() => void this.refreshRuntimeInfo(), RUNTIME_INFO_REFRESH_MS);
7384
+ this.runtimeInfoTimer.unref?.();
7326
7385
  this.runner.onDetectionStreamChange((deviceId, action) => {
7327
7386
  this.handleDetectionStreamChange(deviceId, action);
7328
7387
  });
@@ -7389,6 +7448,10 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7389
7448
  };
7390
7449
  }
7391
7450
  async onShutdown() {
7451
+ if (this.runtimeInfoTimer !== null) {
7452
+ clearInterval(this.runtimeInfoTimer);
7453
+ this.runtimeInfoTimer = null;
7454
+ }
7392
7455
  if (this.metricsSnapshotTimer) {
7393
7456
  clearInterval(this.metricsSnapshotTimer);
7394
7457
  this.metricsSnapshotTimer = null;
@@ -8196,7 +8259,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8196
8259
  const resolved = await this.resolveNativeCropPixels(input);
8197
8260
  if (resolved.tier === "native" && resolved.pixels !== null) {
8198
8261
  this.frameLazy.noteHit(resolveNativeCropHitKind(resolved.pixels.source), deviceId);
8199
- return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg);
8262
+ return this.encodeNativeCropResult(resolved.pixels, resolved.tier, input.encodeJpeg, input.acceptJpegBytes);
8200
8263
  }
8201
8264
  this.frameLazy.noteMiss("native", deviceId);
8202
8265
  if (resolved.tier !== "ram-fullframe") return null;
@@ -8222,7 +8285,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8222
8285
  bytes: crop.bytes,
8223
8286
  width: crop.width,
8224
8287
  height: crop.height
8225
- }, resolved.tier, input.encodeJpeg);
8288
+ }, resolved.tier, input.encodeJpeg, input.acceptJpegBytes);
8226
8289
  }
8227
8290
  /**
8228
8291
  * PARK a frame for a track (D422) — the hub judged it relevant, and this node
@@ -8245,8 +8308,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8245
8308
  store,
8246
8309
  resolvePixels: (request) => this.resolveNativeCropPixels(request),
8247
8310
  encodeJpeg: async (pixels) => {
8248
- const encoded = await this.encodeNativeCropResult(pixels, "native", true);
8249
- return encoded?.jpeg !== void 0 ? Buffer.from(encoded.jpeg, "base64") : null;
8311
+ const encoded = await this.encodeNativeCropResult(pixels, "native", true, true);
8312
+ return encoded?.jpegBytes !== void 0 ? Buffer.from(encoded.jpegBytes) : null;
8250
8313
  },
8251
8314
  logger: this.ctx.logger
8252
8315
  }, input);
@@ -8269,7 +8332,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8269
8332
  return null;
8270
8333
  }
8271
8334
  return {
8272
- jpeg: parcel.jpeg.toString("base64"),
8335
+ ...input.acceptJpegBytes === true ? { jpegBytes: new Uint8Array(parcel.jpeg) } : { jpeg: parcel.jpeg.toString("base64") },
8273
8336
  width: parcel.width,
8274
8337
  height: parcel.height,
8275
8338
  timestamp: parcel.timestamp,
@@ -8649,12 +8712,15 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8649
8712
  * boundary (CLAUDE.md invariant #21). On an encode failure the crop is a miss
8650
8713
  * (`null`) — the caller retries — never a silent raw-RGB cross-node fallback.
8651
8714
  */
8652
- async encodeNativeCropResult(crop, tier, encodeJpeg) {
8715
+ async encodeNativeCropResult(crop, tier, encodeJpeg, acceptJpegBytes) {
8653
8716
  if (crop.bytes.byteLength >= 2 && crop.bytes[0] === 255 && crop.bytes[1] === 216) {
8654
- const encoded = encodeCrossNodeCrop({ jpeg: Buffer.from(crop.bytes).toString("base64") });
8717
+ const encoded = encodeCrossNodeCrop({
8718
+ jpeg: crop.bytes,
8719
+ acceptBytes: acceptJpegBytes
8720
+ });
8655
8721
  if (!encoded) return null;
8656
8722
  return {
8657
- jpeg: encoded.jpeg,
8723
+ ...encoded,
8658
8724
  width: crop.width,
8659
8725
  height: crop.height,
8660
8726
  tier
@@ -8668,16 +8734,17 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8668
8734
  };
8669
8735
  try {
8670
8736
  const encoded = encodeCrossNodeCrop({
8671
- jpeg: (await (await getSharp())(Buffer.from(crop.bytes), { raw: {
8737
+ jpeg: await (await getSharp())(Buffer.from(crop.bytes), { raw: {
8672
8738
  width: crop.width,
8673
8739
  height: crop.height,
8674
8740
  channels: 3
8675
- } }).jpeg({ quality: 90 }).toBuffer()).toString("base64"),
8676
- bytes: crop.bytes
8741
+ } }).jpeg({ quality: 90 }).toBuffer(),
8742
+ bytes: crop.bytes,
8743
+ acceptBytes: acceptJpegBytes
8677
8744
  });
8678
8745
  if (!encoded) return null;
8679
8746
  return {
8680
- jpeg: encoded.jpeg,
8747
+ ...encoded,
8681
8748
  width: crop.width,
8682
8749
  height: crop.height,
8683
8750
  tier
@@ -8734,6 +8801,58 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8734
8801
  }, input);
8735
8802
  }
8736
8803
  /**
8804
+ * Re-read the labels the `pipeline stats` lines carry (D463).
8805
+ *
8806
+ * Each half is read INDEPENDENTLY and a failure omits only its own field:
8807
+ * an engine that will not answer must not also erase the model list. Both
8808
+ * reads are best-effort by design — this is a label on a telemetry line, and
8809
+ * it must never be able to take the runner down or to log a storm. A failure
8810
+ * is logged once per refresh at `debug`, deliberately: the ABSENCE of the
8811
+ * field in the line is the signal, and the hub writes `info`.
8812
+ */
8813
+ async refreshRuntimeInfo() {
8814
+ const api = this.ctxIfReady?.api;
8815
+ const sampler = this.runner?.timingSampler;
8816
+ if (!api || sampler === void 0 || sampler === null) {
8817
+ this.noteRuntimeInfoFailure(NO_API_FAILURE);
8818
+ return;
8819
+ }
8820
+ let engines = null;
8821
+ let failure = null;
8822
+ try {
8823
+ engines = (await api.pipelineExecutor.listLoadedEngines.query()).map((r) => ({
8824
+ backend: r.engine.backend,
8825
+ ...r.engine.device !== void 0 ? { device: r.engine.device } : {},
8826
+ kind: r.kind,
8827
+ modelsLoaded: r.modelsLoaded
8828
+ }));
8829
+ } catch (err) {
8830
+ failure = errMsg(err);
8831
+ }
8832
+ this.noteRuntimeInfoFailure(failure);
8833
+ sampler.runtimeInfo = {
8834
+ ...sampler.runtimeInfo,
8835
+ ...buildRuntimeLabels(engines)
8836
+ };
8837
+ }
8838
+ /**
8839
+ * Log a runtime-label failure at its TRANSITION, and at `warn`.
8840
+ *
8841
+ * This branch first shipped at `debug`, on the argument that the absent field
8842
+ * in the line was signal enough. It was not: within the hour `little-unraid`
8843
+ * was the one node still logging `pipelineRuntime=null` and there was no way
8844
+ * to tell "this node has no engine" from "the read threw", because the hub
8845
+ * writes `info` and the agent forwards to it. That is D438's blind
8846
+ * instrument for the third time. Logging the transition rather than every
8847
+ * refresh is what keeps it from becoming a storm.
8848
+ */
8849
+ noteRuntimeInfoFailure(failure) {
8850
+ if (failure === this.lastRuntimeInfoFailure) return;
8851
+ if (failure === null) this.ctxIfReady?.logger.info("runtime labels: readable again");
8852
+ else this.ctxIfReady?.logger.warn("runtime labels unreadable — this node cannot name its engines", { meta: { error: failure } });
8853
+ this.lastRuntimeInfoFailure = failure;
8854
+ }
8855
+ /**
8737
8856
  * The model format this node dispatches on when no `deviceKey` is given —
8738
8857
  * its selected engine's format. `null` on any failure, which the caller
8739
8858
  * turns into a refusal rather than a guess: a model pin that cannot be
@@ -8962,13 +9081,13 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8962
9081
  handleAgeMs: resolved.handleAgeMs,
8963
9082
  ...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
8964
9083
  };
8965
- const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
8966
- if (!encoded?.jpeg) return {
9084
+ const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true, true);
9085
+ if (!encoded?.jpegBytes) return {
8967
9086
  miss: "jpeg-encode-failed",
8968
9087
  handleAgeMs: resolved.handleAgeMs
8969
9088
  };
8970
9089
  return {
8971
- jpeg: Buffer.from(encoded.jpeg, "base64"),
9090
+ jpeg: Buffer.from(encoded.jpegBytes),
8972
9091
  width: encoded.width,
8973
9092
  height: encoded.height,
8974
9093
  frameSpace: denormalizeRect(paddedBbox, handle.width, handle.height)
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-ATDfcxgL.js");
2
+ const require_dist = require("./dist-DZsjVFey.js");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { ht as parseProcStatus } from "./dist-CEFC3PkP.mjs";
1
+ import { ht as parseProcStatus } from "./dist-BNzfbpF6.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