@camstack/addon-pipeline-orchestrator 1.2.295 → 1.2.297

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.
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ node_fs = __toESM(node_fs);
30
30
  let node_path = require("node:path");
31
31
  node_path = __toESM(node_path);
32
32
  let node_url = require("node:url");
33
- //#region ../types/dist/event-category-BGmz_oxW.mjs
33
+ //#region ../types/dist/event-category-B_0XEcLF.mjs
34
34
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
35
35
  EventCategory["SystemBoot"] = "system.boot";
36
36
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -531,15 +531,6 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
531
531
  EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
532
532
  EventCategory["AgentBackupActivated"] = "agent.backup.activated";
533
533
  EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
534
- /**
535
- * Per-agent hwaccel preference changed (user override set, cleared, or
536
- * re-probed). Observability event — decoders pull the current pref at
537
- * `createSession`, so running sessions keep their current backend until
538
- * they rotate naturally (camera add/remove, stream restart). Future
539
- * work can wire a listener in stream-broker that force-rotates live
540
- * sessions; for now this event powers logs + admin-UI toast feedback.
541
- */
542
- EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
543
534
  EventCategory["MotionAnalysis"] = "detection.motion-analysis";
544
535
  /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
545
536
  EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
@@ -5410,7 +5401,7 @@ var ZodIssueCode = {
5410
5401
  var ZodFirstPartyTypeKind;
5411
5402
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5412
5403
  //#endregion
5413
- //#region ../types/dist/sleep-DAJ9Er57.mjs
5404
+ //#region ../types/dist/sleep-BNeXenvr.mjs
5414
5405
  /**
5415
5406
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5416
5407
  * window to float samples (D455).
@@ -6809,32 +6800,12 @@ object({
6809
6800
  audioCodec: string().optional(),
6810
6801
  metadata: record(string(), unknown()).readonly().optional()
6811
6802
  });
6812
- var EncodedPacketSchema = object({
6813
- type: _enum(["video", "audio"]),
6814
- data: _instanceof(Uint8Array),
6815
- pts: number(),
6816
- dts: number(),
6817
- keyframe: boolean(),
6818
- codec: string()
6819
- });
6820
- var DecodedFrameSchema = object({
6821
- data: _instanceof(Uint8Array),
6822
- width: number(),
6823
- height: number(),
6824
- format: _enum([
6825
- "jpeg",
6826
- "rgb",
6827
- "bgr",
6828
- "yuv420",
6829
- "gray"
6830
- ]),
6831
- timestamp: number()
6832
- });
6833
6803
  /**
6834
6804
  * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
6835
6805
  * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
6836
- * interface in `interfaces/frame-handle.ts` field-for-field so a decoder cap
6837
- * method (`pullHandles`) can carry handles over tRPC / Moleculer.
6806
+ * interface in `interfaces/frame-handle.ts` field-for-field so a cap method
6807
+ * (the runner's native-crop and pipeline-executor inputs) can carry handles
6808
+ * over tRPC / Moleculer.
6838
6809
  *
6839
6810
  * The `satisfies` assertion below pins this schema to the interface: if a
6840
6811
  * field is added to / removed from `FrameHandle` without a matching schema
@@ -6860,50 +6831,6 @@ var FrameHandleSchema = object({
6860
6831
  slotCount: number().int().positive()
6861
6832
  });
6862
6833
  /**
6863
- * Pixel format a frame-handle subscriber can request from the broker
6864
- * (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
6865
- * `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
6866
- * `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
6867
- * payload, and no decoded-frame consumer requests it over the shm plane.
6868
- */
6869
- var FrameHandleFormatSchema = _enum([
6870
- "rgb",
6871
- "bgr",
6872
- "yuv420",
6873
- "gray"
6874
- ]);
6875
- /**
6876
- * Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
6877
- * broker for a `FrameHandle` stream of one `format`; the broker maintains one
6878
- * shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
6879
- * subscriber (motion) and an `rgb` subscriber (detection) each read the format
6880
- * they asked for with no broker-side conversion.
6881
- */
6882
- var SubscribeFramesInputSchema = object({
6883
- brokerId: string(),
6884
- format: FrameHandleFormatSchema,
6885
- /**
6886
- * Optional reader-side cadence hint in frames per second. The broker does
6887
- * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
6888
- * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
6889
- * the consumer can pace its own `pullFrameHandles` polling.
6890
- */
6891
- maxFps: number().positive().optional(),
6892
- /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
6893
- tag: string().optional()
6894
- });
6895
- /**
6896
- * Result of `stream-broker.subscribeFrames`. The consumer then polls
6897
- * `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
6898
- * `FrameHandle` to a `FrameRingReader`.
6899
- */
6900
- var SubscribeFramesResultSchema = object({
6901
- /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
6902
- subscriptionId: string(),
6903
- /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6904
- maxFps: number().nonnegative()
6905
- });
6906
- /**
6907
6834
  * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
6908
6835
  * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
6909
6836
  * sample bytes plus the track parameters a consumer needs to interpret them.
@@ -6911,8 +6838,8 @@ var SubscribeFramesResultSchema = object({
6911
6838
  * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
6912
6839
  * Float32 PCM), so unlike video frames they ship their bytes INLINE over
6913
6840
  * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
6914
- * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6915
- * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6841
+ * (the wire-serialisable supertype of `Buffer`); a `Buffer` is assignable to
6842
+ * it.
6916
6843
  */
6917
6844
  var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6918
6845
  var DecodedAudioChunkSchema = object({
@@ -6930,11 +6857,10 @@ var DecodedAudioChunkSchema = object({
6930
6857
  format: AudioChunkFormatSchema.optional()
6931
6858
  });
6932
6859
  /**
6933
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
6934
- * handle-free, ship-bytes-inline analogue of `subscribeFrames`: a consumer
6935
- * asks the broker for the decoded audio-chunk stream of one broker; the
6936
- * broker keeps a per-subscription bounded FIFO queue the consumer drains via
6937
- * `pullAudioChunks`.
6860
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). Audio ships
6861
+ * its bytes inline: a consumer asks the broker for the decoded audio-chunk
6862
+ * stream of one broker; the broker keeps a per-subscription bounded FIFO queue
6863
+ * the consumer drains via `pullAudioChunks`.
6938
6864
  */
6939
6865
  var SubscribeAudioChunksInputSchema = object({
6940
6866
  brokerId: string(),
@@ -6965,9 +6891,7 @@ var BrokerStatsSchema = object({
6965
6891
  "stopped"
6966
6892
  ]),
6967
6893
  inputFps: number(),
6968
- decodeFps: number(),
6969
6894
  encodedSubscribers: number(),
6970
- decodedSubscribers: number(),
6971
6895
  uptimeMs: number(),
6972
6896
  bitrateKbps: number(),
6973
6897
  idrIntervalMs: number(),
@@ -6980,14 +6904,6 @@ var BrokerStatsSchema = object({
6980
6904
  preBufferMs: number(),
6981
6905
  preBufferPackets: number(),
6982
6906
  /**
6983
- * Moleculer node id of the decoder provider currently servicing this
6984
- * stream's decoded subscribers. `null` until the deferred decoder is
6985
- * created (no decoded clients yet, or codec not detected). Surfaces the
6986
- * runtime decoder placement so the UI can show "Decoder: <agent>" without
6987
- * a separate cap call per broker.
6988
- */
6989
- decoderNodeId: string().nullable(),
6990
- /**
6991
6907
  * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
6992
6908
  * when the stream has no audio track or the broker is in cold start.
6993
6909
  * `supported = false` means the codec was detected but the local
@@ -11325,70 +11241,6 @@ var StorageLocationDeclarationSchema = object({
11325
11241
  "backup"
11326
11242
  ]).optional()
11327
11243
  });
11328
- var DecoderStatsSchema = object({
11329
- inputFps: number(),
11330
- outputFps: number(),
11331
- avgDecodeTimeMs: number(),
11332
- droppedFrames: number(),
11333
- /**
11334
- * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
11335
- * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
11336
- * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
11337
- * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
11338
- */
11339
- lagMs: number().optional(),
11340
- effectiveFps: number().optional(),
11341
- adaptiveFps: number().optional()
11342
- });
11343
- var DecoderSessionConfigSchema = object({
11344
- codec: string(),
11345
- maxFps: number().default(0),
11346
- outputFormat: _enum([
11347
- "jpeg",
11348
- "rgb",
11349
- "bgr",
11350
- "yuv420",
11351
- "gray"
11352
- ]).default("jpeg"),
11353
- scale: number().default(1),
11354
- width: number().optional(),
11355
- height: number().optional(),
11356
- /**
11357
- * Identifier of the camera this decoder session serves. Optional
11358
- * because the cap is generic (any caller could request decode), but
11359
- * stream-broker passes it so decoder logs include `deviceId` for
11360
- * per-camera filtering when diagnosing failures (e.g. node-av
11361
- * sendPacket errors on a single hung camera).
11362
- */
11363
- deviceId: number().int().nonnegative().optional(),
11364
- /**
11365
- * Free-form tag for log scoping. Stream-broker uses
11366
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
11367
- * on every line so `grep tag=broker:5/high` filters one camera
11368
- * profile cleanly.
11369
- */
11370
- tag: string().optional(),
11371
- /**
11372
- * Where the session delivers decoded frames (Phase 5 / D9):
11373
- *
11374
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
11375
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
11376
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
11377
- * into an OS shared-memory ring and drained as zero-pixel
11378
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
11379
- * other — `pullFrames` returns nothing for an `'shm'` session and
11380
- * `pullHandles` returns nothing for a `'callback'` session.
11381
- */
11382
- frameSink: _enum(["callback", "shm"]).default("callback"),
11383
- /**
11384
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
11385
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
11386
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
11387
- * stream-broker's `streamingDebug` gate — off by default so production logs
11388
- * stay quiet and the emit path pays zero per-frame cost when disabled.
11389
- */
11390
- debug: boolean().optional()
11391
- });
11392
11244
  /**
11393
11245
  * Per-component log CHANNELS — the gate a hot path consults, and the registry
11394
11246
  * an addon declares its channels in.
@@ -13592,13 +13444,6 @@ var BrokerRtspClientSchema = object({
13592
13444
  lastRtpAt: number(),
13593
13445
  bytesSent: number()
13594
13446
  });
13595
- var BrokerDecodedClientSchema = object({
13596
- tag: string(),
13597
- subscribedAt: number(),
13598
- maxFps: number(),
13599
- framesDelivered: number(),
13600
- framesDropped: number()
13601
- });
13602
13447
  var BrokerAudioClientSchema = object({
13603
13448
  tag: string(),
13604
13449
  subscribedAt: number(),
@@ -13690,7 +13535,6 @@ var BrokerEncodedClientSchema = object({
13690
13535
  });
13691
13536
  var BrokerClientsSchema = object({
13692
13537
  rtsp: array(BrokerRtspClientSchema).readonly(),
13693
- decoded: array(BrokerDecodedClientSchema).readonly(),
13694
13538
  audio: array(BrokerAudioClientSchema).readonly(),
13695
13539
  encoded: array(BrokerEncodedClientSchema).readonly(),
13696
13540
  pipeClients: number(),
@@ -14059,11 +13903,7 @@ method(object({
14059
13903
  auth: "admin"
14060
13904
  }), method(object({ brokerId: string() }), BrokerClientsSchema), method(object({
14061
13905
  brokerId: string(),
14062
- channel: _enum([
14063
- "rtsp",
14064
- "decoded",
14065
- "audio"
14066
- ]),
13906
+ channel: _enum(["rtsp", "audio"]),
14067
13907
  handle: string()
14068
13908
  }), object({ killed: boolean() }), {
14069
13909
  kind: "mutation",
@@ -14098,10 +13938,7 @@ method(object({
14098
13938
  }), method(SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, { kind: "mutation" }), method(object({
14099
13939
  subscriptionId: string(),
14100
13940
  maxCount: number().int().positive().default(8)
14101
- }), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(SubscribeFramesInputSchema, SubscribeFramesResultSchema, { kind: "mutation" }), method(object({
14102
- subscriptionId: string(),
14103
- maxCount: number().int().positive().default(4)
14104
- }), array(FrameHandleSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
13941
+ }), array(DecodedAudioChunkSchema).readonly()), method(object({ subscriptionId: string() }), object({ released: boolean() }), { kind: "mutation" }), method(object({
14105
13942
  brokerId: string(),
14106
13943
  seconds: number().min(0).max(30)
14107
13944
  }), _void(), {
@@ -15258,54 +15095,12 @@ id: union([string(), number()]) }), {
15258
15095
  kind: "mutation",
15259
15096
  auth: "admin"
15260
15097
  });
15261
- /**
15262
- * shm ring usage stats for a `frameSink: 'shm'` decoder session —
15263
- * exposed via `decoder.getShmStats` so downstream consumers can
15264
- * observe ring pressure (slot count, byte budget, hit/miss ratio).
15265
- */
15266
- var ShmRingStatsSchema = object({
15267
- sessionId: string(),
15268
- slotCount: number().int(),
15269
- slotByteLength: number().int(),
15270
- segmentBytes: number().int(),
15271
- budgetMb: number().int(),
15272
- framesWritten: number().int(),
15273
- getFrameHits: number().int(),
15274
- getFrameMisses: number().int()
15275
- });
15276
- method(object({ codec: string() }), boolean()), method(_void(), object({
15098
+ method(_void(), object({
15277
15099
  id: string(),
15278
15100
  name: string(),
15279
- isPullMode: boolean().optional(),
15280
- priority: number().optional(),
15281
15101
  hwaccel: string().optional(),
15282
15102
  probedBestHwaccel: string().optional()
15283
- })), method(DecoderSessionConfigSchema, object({
15284
- sessionId: string(),
15285
- nodeId: string()
15286
- })), method(object({ sessionId: string() }), _void()), method(object({
15287
- sessionId: string(),
15288
- packet: EncodedPacketSchema
15289
- }), _void()), method(object({
15290
- sessionId: string(),
15291
- url: string()
15292
- }), _void()), method(object({
15293
- sessionId: string(),
15294
- maxCount: number().default(1),
15295
- waitMs: number().optional()
15296
- }), array(DecodedFrameSchema)), method(object({
15297
- sessionId: string(),
15298
- maxCount: number().default(1),
15299
- waitMs: number().optional()
15300
- }), array(FrameHandleSchema)), method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()), method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()), method(object({
15301
- sessionId: string(),
15302
- config: DecoderSessionConfigSchema.partial()
15303
- }), _void()), method(object({ sessionId: string() }), DecoderStatsSchema), method(_void(), array(object({
15304
- sessionId: string(),
15305
- codec: string(),
15306
- outputFormat: string(),
15307
- createdAtMs: number()
15308
- })).readonly()), method(_void(), object({ backend: string() }), {
15103
+ })), method(_void(), object({ backend: string() }), {
15309
15104
  kind: "mutation",
15310
15105
  auth: "admin"
15311
15106
  });
@@ -22375,8 +22170,8 @@ function interpretStoredClusterModel(raw) {
22375
22170
  var CLUSTER_STEP_SETTING_FIELDS = [{
22376
22171
  stepId: "face-embedding",
22377
22172
  key: "minLandmarkFaceSize",
22378
- label: "Min face size for recognition (detection px)",
22379
- description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
22173
+ label: "Min face size to embed (detection-frame px)",
22174
+ description: "Faces smaller than this in the small frame the detector sees are not embedded, so they can never be recognised. Below it the face cannot be aligned reliably and a bad embedding matches everyone. Same value on every node. 0 = no limit.",
22380
22175
  type: "slider",
22381
22176
  min: 0,
22382
22177
  max: 64,
@@ -26508,14 +26303,15 @@ var RunnerLocalLoadSchema = object({
26508
26303
  * Per-inference-device live load (multi-device C4). One entry per deviceKey
26509
26304
  * this runner currently has attached cameras on, so the orchestrator's second
26510
26305
  * `balance()` pass (over a node's devices) weights on real per-pool session
26511
- * counts. Empty on single-device / pre-multi-device runners. `queueDepthTotal`
26512
- * per device is 0 until the pool backlog gets a public accessor (follow-up).
26306
+ * counts. Empty on single-device / pre-multi-device runners. There is no
26307
+ * per-device queue depth: the inference pool's backlog has no accessor, and
26308
+ * a field that always read 0 was removed rather than shown as a measurement
26309
+ * (D762).
26513
26310
  */
26514
26311
  devices: array(object({
26515
26312
  deviceKey: string(),
26516
26313
  backend: string(),
26517
- attachedCameras: number(),
26518
- queueDepthTotal: number()
26314
+ attachedCameras: number()
26519
26315
  })).default([]),
26520
26316
  /** Hardware capability flags reported by this node. */
26521
26317
  hardware: object({
@@ -26838,7 +26634,10 @@ var PipelineAssignmentSchema = object({
26838
26634
  });
26839
26635
  /**
26840
26636
  * Per-agent load summary surfaced to the load balancer + dashboards.
26841
- * Aggregated from each runner's `getLocalLoad` cap call.
26637
+ * Aggregated from each runner's `getLocalLoad` cap call. In the
26638
+ * `getAgentLoad` report, a single-device node's default-pool sessions (no
26639
+ * `deviceKey`) are credited to its one eligible device in `load.devices` —
26640
+ * the default pool IS that device (D745); the balancer's census stays raw.
26842
26641
  */
26843
26642
  var AgentLoadSummarySchema = object({
26844
26643
  nodeId: string(),
@@ -26927,8 +26726,14 @@ var CameraBrokerStatusSchema = object({
26927
26726
  width: number(),
26928
26727
  height: number(),
26929
26728
  subscribers: number(),
26930
- inFps: number(),
26931
- outFps: number()
26729
+ /**
26730
+ * Encoded packets/s the broker receives from the camera. null when the
26731
+ * broker's stats read failed — unknown, never 0 (a 0 here reads as a stall).
26732
+ * There is no output rate: decoding runs in the session decode workers, not
26733
+ * in the broker, so the broker's own decoded-frame rate is 0 on every live
26734
+ * camera and is not served (D758).
26735
+ */
26736
+ inFps: number().nullable()
26932
26737
  })).readonly(),
26933
26738
  webrtcSessions: number(),
26934
26739
  rtspRestream: boolean()
@@ -30017,8 +29822,6 @@ var WebrtcStreamChoiceSchema = object({
30017
29822
  ]).nullable(),
30018
29823
  /** Input FPS from the camera (null for adaptive). */
30019
29824
  inputFps: number().nullable(),
30020
- /** Decoded FPS (null for adaptive). */
30021
- decodeFps: number().nullable(),
30022
29825
  /** Bitrate in kbps (null for adaptive). */
30023
29826
  bitrateKbps: number().nullable()
30024
29827
  });
@@ -30597,6 +30400,59 @@ var AddonAutoUpdateSchema = ChannelWithInheritSchema;
30597
30400
  var RestartAddonResultSchema = unknown();
30598
30401
  var InstallPackageResultSchema = unknown();
30599
30402
  var ReloadPackagesResultSchema = unknown();
30403
+ /**
30404
+ * What an uninstall did on ONE node.
30405
+ * - `uninstalled` — removed: runner(s) stopped, caps gone, code deleted.
30406
+ * - `not-installed` — the node did not have it (nothing to do).
30407
+ * - `refused` — protected / bootstrap package; nothing touched.
30408
+ * - `failed` — attempted and did not complete; `error` says why.
30409
+ * - `pending-reconnect` — the node is OFFLINE: its record was pruned so the
30410
+ * back-fill never returns the package, and the stale-addon sweep removes the
30411
+ * copy when the node registers again (only once the hub no longer has it).
30412
+ */
30413
+ var UninstallNodeOutcomeSchema = _enum([
30414
+ "uninstalled",
30415
+ "not-installed",
30416
+ "refused",
30417
+ "failed",
30418
+ "pending-reconnect"
30419
+ ]);
30420
+ var UninstallNodeResultSchema = object({
30421
+ nodeId: string(),
30422
+ outcome: UninstallNodeOutcomeSchema,
30423
+ error: string().optional(),
30424
+ addonIds: array(string()).readonly(),
30425
+ runnersStopped: array(string()).readonly(),
30426
+ warnings: array(string()).readonly()
30427
+ });
30428
+ var UninstallPackageResultSchema = object({
30429
+ /** Every targeted node ended `uninstalled`, `not-installed` or `pending-reconnect`. */
30430
+ success: boolean(),
30431
+ packageName: string(),
30432
+ results: array(UninstallNodeResultSchema).readonly()
30433
+ });
30434
+ /**
30435
+ * `unreachable` lists online agents whose status read FAILED — absence from
30436
+ * `installations` is only "not installed" for a node that answered (D315).
30437
+ */
30438
+ var PackageInstallationsSchema = object({
30439
+ installations: array(object({
30440
+ packageName: string(),
30441
+ nodeId: string(),
30442
+ nodeName: string(),
30443
+ isHub: boolean(),
30444
+ isOnline: boolean(),
30445
+ version: string().nullable(),
30446
+ addonIds: array(string()).readonly(),
30447
+ source: _enum(["live", "recorded"]),
30448
+ /** The hub installs this package too. `false` on an agent row = an orphan. */
30449
+ onHub: boolean()
30450
+ })).readonly(),
30451
+ unreachable: array(object({
30452
+ nodeId: string(),
30453
+ error: string()
30454
+ })).readonly()
30455
+ });
30600
30456
  var BulkUpdateItemStatusSchema = _enum([
30601
30457
  "queued",
30602
30458
  "updating",
@@ -30679,10 +30535,14 @@ method(_void(), array(AddonListItemSchema).readonly()), method(object({
30679
30535
  }), method(object({ packageName: string() }), InstallPackageResultSchema, {
30680
30536
  kind: "mutation",
30681
30537
  auth: "admin"
30682
- }), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({ packageName: string() }), unknown(), {
30538
+ }), method(_void(), boolean()), method(_void(), array(unknown()).readonly()), method(object({
30539
+ packageName: string().min(1),
30540
+ nodeId: string().optional(),
30541
+ everywhere: boolean().optional()
30542
+ }), UninstallPackageResultSchema, {
30683
30543
  kind: "mutation",
30684
30544
  auth: "admin"
30685
- }), method(_void(), ReloadPackagesResultSchema, {
30545
+ }), method(object({ packageName: string().optional() }), PackageInstallationsSchema, { auth: "admin" }), method(_void(), ReloadPackagesResultSchema, {
30686
30546
  kind: "mutation",
30687
30547
  auth: "admin"
30688
30548
  }), method(object({ query: string().optional() }), array(SearchResultSchema)), method(object({ nodeId: string().optional() }), array(PackageUpdateSchema).readonly(), { auth: "admin" }), method(object({
@@ -39082,6 +38942,12 @@ Object.freeze({
39082
38942
  addonId: null,
39083
38943
  access: "view"
39084
38944
  },
38945
+ "addons.listInstallations": {
38946
+ capName: "addons",
38947
+ capScope: "system",
38948
+ addonId: null,
38949
+ access: "view"
38950
+ },
39085
38951
  "addons.listJobs": {
39086
38952
  capName: "addons",
39087
38953
  capScope: "system",
@@ -40264,90 +40130,18 @@ Object.freeze({
40264
40130
  addonId: null,
40265
40131
  access: "create"
40266
40132
  },
40267
- "decoder.createSession": {
40268
- capName: "decoder",
40269
- capScope: "system",
40270
- addonId: null,
40271
- access: "view"
40272
- },
40273
- "decoder.destroySession": {
40274
- capName: "decoder",
40275
- capScope: "system",
40276
- addonId: null,
40277
- access: "delete"
40278
- },
40279
- "decoder.getFrame": {
40280
- capName: "decoder",
40281
- capScope: "system",
40282
- addonId: null,
40283
- access: "view"
40284
- },
40285
40133
  "decoder.getInfo": {
40286
40134
  capName: "decoder",
40287
40135
  capScope: "system",
40288
40136
  addonId: null,
40289
40137
  access: "view"
40290
40138
  },
40291
- "decoder.getShmStats": {
40292
- capName: "decoder",
40293
- capScope: "system",
40294
- addonId: null,
40295
- access: "view"
40296
- },
40297
- "decoder.getStats": {
40298
- capName: "decoder",
40299
- capScope: "system",
40300
- addonId: null,
40301
- access: "view"
40302
- },
40303
- "decoder.listActiveSessions": {
40304
- capName: "decoder",
40305
- capScope: "system",
40306
- addonId: null,
40307
- access: "view"
40308
- },
40309
- "decoder.openStream": {
40310
- capName: "decoder",
40311
- capScope: "system",
40312
- addonId: null,
40313
- access: "view"
40314
- },
40315
- "decoder.pullFrames": {
40316
- capName: "decoder",
40317
- capScope: "system",
40318
- addonId: null,
40319
- access: "view"
40320
- },
40321
- "decoder.pullHandles": {
40322
- capName: "decoder",
40323
- capScope: "system",
40324
- addonId: null,
40325
- access: "view"
40326
- },
40327
- "decoder.pushPacket": {
40328
- capName: "decoder",
40329
- capScope: "system",
40330
- addonId: null,
40331
- access: "view"
40332
- },
40333
40139
  "decoder.reprobeHwaccel": {
40334
40140
  capName: "decoder",
40335
40141
  capScope: "system",
40336
40142
  addonId: null,
40337
40143
  access: "create"
40338
40144
  },
40339
- "decoder.supportsCodec": {
40340
- capName: "decoder",
40341
- capScope: "system",
40342
- addonId: null,
40343
- access: "view"
40344
- },
40345
- "decoder.updateConfig": {
40346
- capName: "decoder",
40347
- capScope: "system",
40348
- addonId: null,
40349
- access: "view"
40350
- },
40351
40145
  "detectionPipeline.applyDeviceSettingsPatch": {
40352
40146
  capName: "detection-pipeline",
40353
40147
  capScope: "device",
@@ -45394,12 +45188,6 @@ Object.freeze({
45394
45188
  addonId: null,
45395
45189
  access: "view"
45396
45190
  },
45397
- "streamBroker.pullFrameHandles": {
45398
- capName: "stream-broker",
45399
- capScope: "system",
45400
- addonId: null,
45401
- access: "view"
45402
- },
45403
45191
  "streamBroker.regenerateRtspToken": {
45404
45192
  capName: "stream-broker",
45405
45193
  capScope: "system",
@@ -45466,12 +45254,6 @@ Object.freeze({
45466
45254
  addonId: null,
45467
45255
  access: "create"
45468
45256
  },
45469
- "streamBroker.subscribeFrames": {
45470
- capName: "stream-broker",
45471
- capScope: "system",
45472
- addonId: null,
45473
- access: "create"
45474
- },
45475
45257
  "streamBroker.unassignProfile": {
45476
45258
  capName: "stream-broker",
45477
45259
  capScope: "system",
@@ -45484,12 +45266,6 @@ Object.freeze({
45484
45266
  addonId: null,
45485
45267
  access: "create"
45486
45268
  },
45487
- "streamBroker.unsubscribeFrames": {
45488
- capName: "stream-broker",
45489
- capScope: "system",
45490
- addonId: null,
45491
- access: "create"
45492
- },
45493
45269
  "streamCatalog.getCatalog": {
45494
45270
  capName: "stream-catalog",
45495
45271
  capScope: "device",
@@ -48031,11 +47807,6 @@ Object.freeze({
48031
47807
  form: "single",
48032
47808
  optional: false
48033
47809
  }],
48034
- "decoder.createSession": [{
48035
- name: "deviceId",
48036
- form: "single",
48037
- optional: true
48038
- }],
48039
47810
  "detectionPipeline.applyDeviceSettingsPatch": [{
48040
47811
  name: "deviceId",
48041
47812
  form: "single",
@@ -54752,6 +54523,40 @@ var LoadShedController = class LoadShedController {
54752
54523
  }
54753
54524
  };
54754
54525
  //#endregion
54526
+ //#region src/default-pool-attribution.ts
54527
+ /** The backend prefix of a `<backend>:<device>` key (bare `cpu` → `cpu`). */
54528
+ function backendOf(deviceKey) {
54529
+ const colon = deviceKey.indexOf(":");
54530
+ return colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
54531
+ }
54532
+ /**
54533
+ * `load` with its unattributed sessions credited to `eligibleKeys[0]` when that
54534
+ * is the node's ONLY eligible device; `load` itself (same reference) otherwise.
54535
+ * `eligibleKeys` null = the eligible set is not known.
54536
+ */
54537
+ function attributeDefaultPoolSessions(load, eligibleKeys) {
54538
+ if (eligibleKeys === null || eligibleKeys.length !== 1) return load;
54539
+ const onlyKey = eligibleKeys[0];
54540
+ if (onlyKey === void 0) return load;
54541
+ const attributed = load.devices.reduce((sum, d) => sum + d.attachedCameras, 0);
54542
+ const unattributed = load.attachedCameras - attributed;
54543
+ if (unattributed <= 0) return load;
54544
+ const existing = load.devices.find((d) => d.deviceKey === onlyKey);
54545
+ const credited = existing ? {
54546
+ ...existing,
54547
+ attachedCameras: existing.attachedCameras + unattributed
54548
+ } : {
54549
+ deviceKey: onlyKey,
54550
+ backend: backendOf(onlyKey),
54551
+ attachedCameras: unattributed
54552
+ };
54553
+ const devices = [...load.devices.filter((d) => d.deviceKey !== onlyKey), credited].toSorted((a, b) => a.deviceKey.localeCompare(b.deviceKey));
54554
+ return {
54555
+ ...load,
54556
+ devices
54557
+ };
54558
+ }
54559
+ //#endregion
54755
54560
  //#region src/dispatch-timing.ts
54756
54561
  /**
54757
54562
  * How long the placement census waits for ONE runner's `getLocalCameras`
@@ -54818,6 +54623,25 @@ var AgentLoadService = class AgentLoadService {
54818
54623
  getAgentLoadSummaries() {
54819
54624
  return [...this.cachedAgentLoad.values()];
54820
54625
  }
54626
+ /**
54627
+ * The cached summaries as the load REPORT (`getAgentLoad`) carries them: a
54628
+ * single-device node's default-pool sessions credited to its one device
54629
+ * (`default-pool-attribution.ts`). The balancer's census
54630
+ * ({@link collectAgentLoad}) stays the runner's raw answer.
54631
+ */
54632
+ async reportedAgentLoad() {
54633
+ const resolve = this.deps.eligibleDeviceKeys;
54634
+ const summaries = this.getAgentLoadSummaries();
54635
+ if (resolve === void 0) return summaries;
54636
+ return Promise.all(summaries.map(async (summary) => {
54637
+ const eligible = await resolve(summary.nodeId).catch(() => null);
54638
+ const load = attributeDefaultPoolSessions(summary.load, eligible);
54639
+ return load === summary.load ? summary : {
54640
+ ...summary,
54641
+ load
54642
+ };
54643
+ }));
54644
+ }
54821
54645
  cachedAgentLoadNodeIds() {
54822
54646
  return [...this.cachedAgentLoad.keys()];
54823
54647
  }
@@ -57265,8 +57089,7 @@ function mapBroker(brokerResult) {
57265
57089
  width: p.width,
57266
57090
  height: p.height,
57267
57091
  subscribers: p.subscribers,
57268
- inFps: p.inFps,
57269
- outFps: p.outFps
57092
+ inFps: p.inFps
57270
57093
  })),
57271
57094
  webrtcSessions: brokerResult.webrtcSessions,
57272
57095
  rtspRestream: brokerResult.rtspRestream
@@ -57653,11 +57476,9 @@ var CameraStatusService = class {
57653
57476
  * `allSlotsFetch` (fetched once by the caller) to avoid a second
57654
57477
  * round-trip. Per-slot `getBrokerStats` + `listClients` are fetched in
57655
57478
  * parallel. WebRTC sessions are counted by filtering `encoded[]` entries
57656
- * whose `attribution.kind` is one of the `WEBRTC_KINDS`. Also captures the
57657
- * broker's actual decode-session node into `liveDecoder` (T6) — the first
57658
- * slot that reports one wins.
57479
+ * whose `attribution.kind` is one of the `WEBRTC_KINDS`.
57659
57480
  */
57660
- buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, sink) {
57481
+ buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, sink) {
57661
57482
  if (!api || !allSlotsFetch) return Promise.resolve(null);
57662
57483
  return this.boundedStage(allSlotsFetch.then(async (slots) => {
57663
57484
  const deviceSlots = slots.filter((s) => s.deviceId === deviceId);
@@ -57674,19 +57495,16 @@ var CameraStatusService = class {
57674
57495
  clients
57675
57496
  };
57676
57497
  })), pass.rtspEntries(api, sourceNodeId).catch(() => null)]);
57677
- const profileDetails = statsAndClients.map(({ slot, stats, clients }) => ({
57678
- profile: slot.profile,
57679
- status: slot.status,
57680
- codec: stats?.codec ?? slot.codec ?? "",
57681
- width: slot.resolution?.width ?? 0,
57682
- height: slot.resolution?.height ?? 0,
57683
- subscribers: clients?.encodedSubscribers ?? 0,
57684
- inFps: stats?.inputFps ?? 0,
57685
- outFps: stats?.decodeFps ?? 0
57686
- }));
57687
- for (const { stats } of statsAndClients) if (liveDecoder.nodeId === null && typeof stats?.decoderNodeId === "string") liveDecoder.nodeId = stats.decoderNodeId;
57688
57498
  return {
57689
- profiles: profileDetails,
57499
+ profiles: statsAndClients.map(({ slot, stats, clients }) => ({
57500
+ profile: slot.profile,
57501
+ status: slot.status,
57502
+ codec: stats?.codec ?? slot.codec ?? "",
57503
+ width: slot.resolution?.width ?? 0,
57504
+ height: slot.resolution?.height ?? 0,
57505
+ subscribers: clients?.encodedSubscribers ?? 0,
57506
+ inFps: stats?.inputFps ?? null
57507
+ })),
57690
57508
  webrtcSessions: statsAndClients.reduce((total, { clients }) => {
57691
57509
  if (!clients) return total;
57692
57510
  return total + clients.encoded.filter((c) => WEBRTC_KINDS.has(c.attribution.kind)).length;
@@ -57880,10 +57698,9 @@ var CameraStatusService = class {
57880
57698
  const api = this.deps.api();
57881
57699
  const degradations = { entries: [] };
57882
57700
  const { detectionNodeId, sourceNodeId, pinned, detectionReason, audioNodeId, audioPinned } = this.buildAssignmentContext(deviceId);
57883
- const liveDecoder = { nodeId: null };
57884
57701
  const allSlotsFetch = api ? pass.profileSlots(api, sourceNodeId) : null;
57885
57702
  const sourceFetch = this.buildSourceStage(api, allSlotsFetch, deviceId, degradations);
57886
- const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, liveDecoder, degradations);
57703
+ const brokerFetch = this.buildBrokerStage(api, allSlotsFetch, deviceId, sourceNodeId, pass, degradations);
57887
57704
  const decoderFetch = this.buildDecoderStage(detectionNodeId);
57888
57705
  const motionResult = this.buildMotionStage(deviceId);
57889
57706
  const detectionFetch = this.buildDetectionStage(api, detectionNodeId, deviceId, pass, degradations);
@@ -57898,11 +57715,10 @@ var CameraStatusService = class {
57898
57715
  switchesFetch
57899
57716
  ]);
57900
57717
  const audioResult = this.buildAudioStage(audioNodeId, switchedOff);
57901
- const liveDecoderNodeId = brokerResult !== null ? liveDecoder.nodeId : null;
57902
- const decoderNodeId = liveDecoderNodeId ?? detectionNodeId;
57718
+ const decoderNodeId = detectionNodeId;
57903
57719
  const reasons = {
57904
57720
  detection: detectionReason,
57905
- decoder: liveDecoderNodeId !== null ? "session" : decoderNodeId !== null ? "co-located" : void 0,
57721
+ decoder: decoderNodeId !== null ? "co-located" : void 0,
57906
57722
  audio: audioPinned ? "manual" : void 0
57907
57723
  };
57908
57724
  return composeCameraStatus({
@@ -60139,7 +59955,7 @@ function deviceAsLoad(deviceKey, load) {
60139
59955
  activeCameras: 0,
60140
59956
  avgInferenceFps: 0,
60141
59957
  avgInferenceTimeMs: 0,
60142
- queueDepthTotal: load?.queueDepthTotal ?? 0,
59958
+ queueDepthTotal: 0,
60143
59959
  devices: [],
60144
59960
  hardware: { hasGpu: false }
60145
59961
  };
@@ -61119,8 +60935,7 @@ function tallyLoad(load, nodeDelta, perDevice) {
61119
60935
  }), ...[...perDevice ?? /* @__PURE__ */ new Map()].filter(([deviceKey, delta]) => !known.has(deviceKey) && delta > 0).map(([deviceKey, delta]) => ({
61120
60936
  deviceKey,
61121
60937
  backend: deviceKey.split(":")[0] ?? deviceKey,
61122
- attachedCameras: delta,
61123
- queueDepthTotal: 0
60938
+ attachedCameras: delta
61124
60939
  }))];
61125
60940
  return {
61126
60941
  ...load,
@@ -69108,6 +68923,10 @@ async function buildOrchestratorControllers(deps) {
69108
68923
  onCensus: (entries) => {
69109
68924
  motionSliceGate?.noteCensus(entries);
69110
68925
  providerSliceGate?.refreshIfStale("runner-census");
68926
+ },
68927
+ eligibleDeviceKeys: async (nodeId) => {
68928
+ const read = await deps.getEligibleInferenceDevicesIfCached(nodeId);
68929
+ return read.kind === "eligible" ? Object.keys(read.eligible) : null;
69111
68930
  }
69112
68931
  });
69113
68932
  /**
@@ -69966,12 +69785,12 @@ function buildGlobalSettingsSections(options) {
69966
69785
  id: CLUSTER_MODEL_SECTION_ID,
69967
69786
  title: "Enrichment models",
69968
69787
  tab: "pipeline",
69969
- description: "The encoders that turn a crop into a VECTOR, and the admission knobs those encoders share. One value each for the whole cluster, because each writes into a single shared index. Changing a model does not corrupt what is already stored (every vector carries the model that produced it) but it does STRAND it until a re-embed pass has run. Step knobs such as the face-size floor apply to every node for the same reason.",
69788
+ description: "The face and search encoders, one each for the whole cluster: each fills a single shared index, and vectors from two models cannot be compared. Changing one keeps what is stored but hides it from matching and search until it is re-embedded.",
69970
69789
  fields: [...CLUSTER_MODEL_SCOPED_STEPS.map((step) => ({
69971
69790
  key: clusterModelSettingKey(step.stepId),
69972
69791
  type: "select",
69973
69792
  label: step.label,
69974
- description: `Applies to every node. A node whose engine ships no build of the chosen model REFUSES this step and says so in its log, rather than quietly running a different encoder into ${step.indexName}. Changing this strands every vector already in ${step.indexName} until it is re-embedded.`,
69793
+ description: `Used on every node. A node that has no build of this model skips the step (and logs it) rather than use another model. Changing it hides what is already in ${step.indexName} until it is re-embedded.`,
69975
69794
  options: step.options.map((o) => ({
69976
69795
  value: o.id,
69977
69796
  label: o.label
@@ -69993,12 +69812,12 @@ function buildGlobalSettingsSections(options) {
69993
69812
  id: DETAIL_CROP_SECTION_ID,
69994
69813
  title: "Enrichment crop",
69995
69814
  tab: "pipeline",
69996
- description: "How the crop fed to enrichment steps (semantic-search embedding, face, plate) is cut out of the frame. ONE value for the whole cluster: the semantic-search index is a single feature space, and vectors cut with different conventions cannot be compared, so a per-node value would silently corrupt search results. Changing either knob invalidates the stored index — follow it with a rebuild (Camera → Semantic search index → Rebuild), which now cuts through this same setting.",
69815
+ description: "How the crop handed to the search, face and plate models is cut from the frame. One value for the whole cluster, because search results are only comparable when every crop is cut the same way. After changing it, rebuild the search index (Camera → Semantic search index → Rebuild).",
69997
69816
  fields: [{
69998
69817
  key: DETAIL_CROP_PADDING_KEY,
69999
69818
  type: "slider",
70000
69819
  label: "Crop margin",
70001
- description: "Fraction of the detection box added on EACH side before cutting. 0 is the pixel-tight box; 0.15 is the shipped default; 0.2 gives a 1.4x window. CLIP is trained on natural images WITH surroundings, so a tight crop removes exactly the context it is strongest on — but the right value is empirical, which is why it is a knob.",
69820
+ description: "Extra margin added on each side of the detection box, as a fraction of its size. 0 = tight box, 0.15 = default. Search works better with some surroundings in the crop.",
70002
69821
  min: DETAIL_CROP_PADDING_FIELD.min,
70003
69822
  max: DETAIL_CROP_PADDING_FIELD.max,
70004
69823
  step: DETAIL_CROP_PADDING_FIELD.step,
@@ -70008,7 +69827,7 @@ function buildGlobalSettingsSections(options) {
70008
69827
  key: DETAIL_CROP_SQUARE_KEY,
70009
69828
  type: "boolean",
70010
69829
  label: "Square the crop",
70011
- description: "Grow the shorter side so the crop is square before it reaches the model. CLIP's input is square, so a tall subject resized straight to NxN is squashed into a shape the model never saw in training. Off by default because the stored index was built unsquared.",
69830
+ description: "Widen the crop to a square so a tall subject is not squashed when the model resizes it. Off by default because the existing index was built without it.",
70012
69831
  default: DEFAULT_DETAIL_CROP_CONVENTION.square
70013
69832
  }]
70014
69833
  },
@@ -70016,13 +69835,13 @@ function buildGlobalSettingsSections(options) {
70016
69835
  id: NATIVE_LEASE_SECTION_ID,
70017
69836
  title: "Native frame lease",
70018
69837
  tab: "pipeline",
70019
- description: "How a decode worker keeps native pixels available for a LATE crop (post-analysis snapshot, face/plate detail) instead of falling back to the ≤640 detection frame. Two things are kept and they cost very differently: a HELD FRAME is a full native raster (~24.9 MB at 4K on the default pinned-RGB24 path, ~6.2 MB at 1080p) and lives only until its own detection result arrives; a TILE is one subject cut from that frame at native resolution and JPEG-encoded (~60-120 KB at 4K), and lives for tens of seconds. A frame on which nothing was detected produces no tiles and costs nothing. Takes effect on the NEXT decode session for a camera, not on sessions already running.",
69838
+ description: "How each camera keeps full-resolution pixels so a crop taken seconds later (snapshot, face, plate) is sharp instead of cut from the small detection frame. A held frame is a whole picture (~25 MB at 4K) kept until its detections arrive; a tile is one subject, or one whole-scene copy, saved as JPEG for tens of seconds. Applies from the next session of each camera.",
70020
69839
  fields: [
70021
69840
  {
70022
69841
  key: NATIVE_LEASE_HOLD_KEY,
70023
69842
  type: "slider",
70024
69843
  label: "Frames held at once",
70025
- description: "How many delivered frames a worker keeps alive while waiting for their detection results. A frame is freed as soon as its own result comes back and its subject tiles have been cut, so the steady state is inference latency × delivered fps — 1 to 4 frames in practice. This number is only the bound above which the OLDEST held frame is dropped, which is what stops a runner that has stopped answering from pinning RAM. Raising it does not improve crop hit rate; it buys tolerance for a slow runner, and holdOverflow on the metrics line is what tells you that you need it.",
69844
+ description: "Most frames kept per camera while their detections are pending; past it the oldest is dropped. 1 to 4 are in use normally. Raise it only if a slow node drops frames (holdOverflow in the logs) — it does not make crops sharper.",
70026
69845
  min: NATIVE_LEASE_HOLD_FIELD.min,
70027
69846
  max: NATIVE_LEASE_HOLD_FIELD.max,
70028
69847
  step: NATIVE_LEASE_HOLD_FIELD.step,
@@ -70034,7 +69853,7 @@ function buildGlobalSettingsSections(options) {
70034
69853
  key: NATIVE_LEASE_TILE_BUDGET_KEY,
70035
69854
  type: "slider",
70036
69855
  label: "Subject tile RAM",
70037
- description: "RAM per decode worker for the compressed SUBJECT TILES — the native-resolution crops taken at the moment a frame's detections are known, and kept long after the frame itself is gone. This is what serves a crop that arrives seconds late, which measurement says is the ordinary case (2 to 10 seconds on this cluster). At ~60-120 KB a tile, 64 MB is many hundreds of subjects. 0 turns tiles OFF and restores the old behaviour, where a late crop had nothing to fall back to but the ≤640 detection frame.",
69856
+ description: "RAM per camera for subject tiles: full-resolution crops of each detected subject, kept so a crop asked for seconds later is still sharp. A tile is ~60-120 KB. 0 = off (late crops come from the small detection frame).",
70038
69857
  min: NATIVE_LEASE_TILE_BUDGET_FIELD.min,
70039
69858
  max: NATIVE_LEASE_TILE_BUDGET_FIELD.max,
70040
69859
  step: NATIVE_LEASE_TILE_BUDGET_FIELD.step,
@@ -70046,7 +69865,7 @@ function buildGlobalSettingsSections(options) {
70046
69865
  key: NATIVE_LEASE_SCENE_BUDGET_KEY,
70047
69866
  type: "slider",
70048
69867
  label: "Scene tile RAM",
70049
- description: "RAM per decode worker for the SCENE TILES — one JPEG copy of the WHOLE native frame, taken at the same instant as the subject tiles above, and only on frames that detected something. A subject tile cannot answer a request for the whole frame (containment is strict, so that a crop never quietly comes from a different part of the scene), which left the native key frame, the detail plane's full-frame rung and the gallery fallback with nothing to fall back to once the held raster was released — measured at 23.3% of key-frame captures lost, 95.7% of them for exactly that reason. A scene tile is ~1.5-2.5 MB at 4K against ~24 MB for the raster it came from, so this budget buys tens of seconds of coverage where holding the rasters for ten would cost around half a gigabyte per camera. 0 turns scene tiles OFF and restores that gap.",
69868
+ description: "RAM per camera for scene tiles: a full-resolution JPEG of the whole picture (~2 MB at 4K) on frames where something was detected, for key frames and whole-frame crops. 0 = off (those then fall back to the small detection frame).",
70050
69869
  min: NATIVE_LEASE_SCENE_BUDGET_FIELD.min,
70051
69870
  max: NATIVE_LEASE_SCENE_BUDGET_FIELD.max,
70052
69871
  step: NATIVE_LEASE_SCENE_BUDGET_FIELD.step,
@@ -70058,7 +69877,7 @@ function buildGlobalSettingsSections(options) {
70058
69877
  key: NATIVE_LEASE_BUDGET_KEY,
70059
69878
  type: "slider",
70060
69879
  label: "Lease RAM ceiling",
70061
- description: "Hard RAM ceiling for held frames, PER decode worker (one worker per camera per plane). This is a safety ceiling, not the working size — the hold count above is what reclaims frames now, and the ceiling is the number above which something is wrong. Lower it on a small host to bound the worst case. 0 DISABLES the lease entirely and falls the worker back to the tiny GPU surface ring, which misses roughly 85% of late crops — that is the behaviour the lease exists to replace, so 0 is a diagnostic setting, not a tuning one.",
69880
+ description: "Safety ceiling on the RAM held frames may use per camera; the frame count above is what normally limits it. Lower it on a small host. 0 turns full-resolution crops off almost entirely — for diagnosis only.",
70062
69881
  min: NATIVE_LEASE_BUDGET_FIELD.min,
70063
69882
  max: NATIVE_LEASE_BUDGET_FIELD.max,
70064
69883
  step: NATIVE_LEASE_BUDGET_FIELD.step,
@@ -70070,7 +69889,7 @@ function buildGlobalSettingsSections(options) {
70070
69889
  key: NATIVE_LEASE_ACTIVITY_KEY,
70071
69890
  type: "slider",
70072
69891
  label: "Demand window",
70073
- description: "Retention only runs within this long of the last native-crop request (or of the stream dialling). A camera nothing is cropping therefore retains nothing and pays no GPU→RAM download at all; the first crop after the window closes misses once, falls back, and re-opens the window for the retry. 0 means ALWAYS ON — it turns the gate off, it does not turn retention off — which is the legacy behaviour that saturated an N100 with 24 native-4K downloads per second on a camera with zero crop demand. Leave it non-zero unless you are reproducing that.",
69892
+ description: "Keep full-resolution pixels only within this long of the last crop request, so a camera nobody crops costs nothing; the first crop after a quiet spell falls back once. 0 = always keep (heavy on small hosts).",
70074
69893
  min: NATIVE_LEASE_ACTIVITY_FIELD.min,
70075
69894
  max: NATIVE_LEASE_ACTIVITY_FIELD.max,
70076
69895
  step: NATIVE_LEASE_ACTIVITY_FIELD.step,
@@ -70083,7 +69902,7 @@ function buildGlobalSettingsSections(options) {
70083
69902
  key: NATIVE_LEASE_ADMISSION_KEY,
70084
69903
  type: "select",
70085
69904
  label: "What to retain",
70086
- description: "WHICH delivered frames get a native copy. A late crop always names one specific frame, and the only way a frame id leaves the runner is on an inference result — so 'Only frames sent to inference' keeps exactly the set a crop can ever ask for and drops the rest, which at the governor's usual 5-in-25 admit rate is around 80% less retained RAM and 80% less GPU→RAM download. 'Every delivered frame' is the older, unconditional behaviour; choose it if a crop ever has to reach a frame the pipeline never inferred. The decode worker counts any crop that names a non-inferred frame on its metrics line (leaseUnmarkedCrops), so a wrong choice here is visible rather than silent.",
69905
+ description: "Which frames get a full-resolution copy. 'Only frames sent to inference' keeps the frames a crop can actually ask for (about 80% less RAM); 'Every delivered frame' keeps all of them.",
70087
69906
  options: NATIVE_LEASE_ADMISSION_FIELD.options,
70088
69907
  default: NATIVE_LEASE_ADMISSION_FIELD.default
70089
69908
  }
@@ -70096,7 +69915,8 @@ function buildGlobalSettingsSections(options) {
70096
69915
  {
70097
69916
  key: "onDisconnect",
70098
69917
  type: "select",
70099
- label: "On runner disconnect",
69918
+ label: "When a node disconnects",
69919
+ description: "Move its unpinned cameras to other nodes, or leave them stopped.",
70100
69920
  default: DEFAULT_FAILOVER_POLICY.onDisconnect,
70101
69921
  options: [{
70102
69922
  value: "migrate",
@@ -70109,7 +69929,8 @@ function buildGlobalSettingsSections(options) {
70109
69929
  {
70110
69930
  key: "pinnedOnDisconnect",
70111
69931
  type: "select",
70112
- label: "Pinned cameras on disconnect",
69932
+ label: "Its pinned cameras",
69933
+ description: "Keep the pin and wait for the node, or drop the pin and move them.",
70113
69934
  default: DEFAULT_FAILOVER_POLICY.pinnedOnDisconnect,
70114
69935
  options: [{
70115
69936
  value: "leave-pinned",
@@ -70122,7 +69943,8 @@ function buildGlobalSettingsSections(options) {
70122
69943
  {
70123
69944
  key: "onReconnect",
70124
69945
  type: "select",
70125
- label: "On runner reconnect",
69946
+ label: "When it comes back",
69947
+ description: "Put its cameras back where they were, or rebalance the whole cluster.",
70126
69948
  default: DEFAULT_FAILOVER_POLICY.onReconnect,
70127
69949
  options: [{
70128
69950
  value: "restore",
@@ -72123,7 +71945,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
72123
71945
  async getAgentLoad() {
72124
71946
  await this.loadService.refreshDecodeHwaccels();
72125
71947
  await this.loadService.collectAgentLoad();
72126
- return this.loadService.getAgentLoadSummaries();
71948
+ return this.loadService.reportedAgentLoad();
72127
71949
  }
72128
71950
  async getGlobalMetrics() {
72129
71951
  const summaries = await this.getAgentLoad();