@camstack/addon-pipeline-orchestrator 1.2.1 → 1.2.3

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.mjs CHANGED
@@ -11581,7 +11581,8 @@ var PipelineStepInputSchema = lazy(() => object({
11581
11581
  modelId: string().optional(),
11582
11582
  enabled: boolean().default(true),
11583
11583
  children: array(PipelineStepInputSchema).optional(),
11584
- settings: record(string(), unknown()).optional()
11584
+ settings: record(string(), unknown()).optional(),
11585
+ jumpDeviceKey: string().optional()
11585
11586
  }));
11586
11587
  var ModelSubstitutionSchema = object({
11587
11588
  addonId: string(),
@@ -12072,8 +12073,24 @@ var NativeCropBboxSchema = object({
12072
12073
  });
12073
12074
  /** Result of a best-effort native-resolution crop (`getNativeCrop`). */
12074
12075
  var NativeCropResultSchema = object({
12075
- /** Packed rgb (24-bit) pixels of the crop. */
12076
- bytes: _instanceof(Uint8Array),
12076
+ /**
12077
+ * Packed rgb (24-bit) pixels of the crop. Present on the DEFAULT (raw) path —
12078
+ * same-node (in-process / UDS) callers get zero-copy RGB and encode locally.
12079
+ * OMITTED when the caller requested `encodeJpeg` (the cross-node compressed
12080
+ * path below), where shipping raw RGB is both an invariant violation and, for
12081
+ * a native full frame (~26 MB at 4K), larger than Moleculer's 10 MB
12082
+ * `maxPacketSize` → the packet is dropped and the call 60s-times-out. That is
12083
+ * the cross-node native-media miss: `bytes` is replaced by `jpeg`.
12084
+ */
12085
+ bytes: _instanceof(Uint8Array).optional(),
12086
+ /**
12087
+ * Base64 JPEG of the crop — the COMPRESSED cross-node payload. The OWNING node
12088
+ * (which holds the native surface) encodes it in-process, so a native 4K frame
12089
+ * ships as ~0.5–2 MB (well under `maxPacketSize`) and NO raw pixels ever cross
12090
+ * a process boundary (CLAUDE.md invariant #21). Present ONLY when the request
12091
+ * set `encodeJpeg: true`; `bytes` is then absent.
12092
+ */
12093
+ jpeg: string().optional(),
12077
12094
  width: number().int().positive(),
12078
12095
  height: number().int().positive(),
12079
12096
  /**
@@ -12219,6 +12236,20 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
12219
12236
  hubHostnameOverride: string().optional()
12220
12237
  })]).describe("Per-camera frame-source mode for the runner (P2c)");
12221
12238
  /**
12239
+ * One ENABLED inference device on the runner's node, as the step-tree
12240
+ * device-jump resolver sees it (phase 1). The orchestrator populates this
12241
+ * roster on the attach payload whenever the camera is elected onto a specific
12242
+ * `deviceKey` and the node has ≥2 enabled devices — it is the candidate set the
12243
+ * runner auto-jumps an enrichment step to when the elected device's format
12244
+ * cannot run that step's model. `weight`/`maxSessions` mirror the balancer's
12245
+ * per-device knobs so the auto choice is weighted-least-loaded.
12246
+ */
12247
+ var RunnerInferenceDeviceSchema = object({
12248
+ deviceKey: string(),
12249
+ weight: number().positive().default(1),
12250
+ maxSessions: number().int().positive().nullable().default(null)
12251
+ });
12252
+ /**
12222
12253
  * Camera assignment payload sent by `addon-pipeline-orchestrator` to a
12223
12254
  * specific runner instance via `attachCamera`. Carries everything the
12224
12255
  * runner needs to subscribe to the local broker and execute inference.
@@ -12336,7 +12367,16 @@ var RunnerCameraConfigSchema = object({
12336
12367
  * omitted ⇒ the runner's default device. The engine itself stays node-local —
12337
12368
  * this only selects WHICH device pool of that node runs the session.
12338
12369
  */
12339
- deviceKey: string().optional()
12370
+ deviceKey: string().optional(),
12371
+ /**
12372
+ * Step-tree device-jump roster (phase 1): the node's ENABLED inference
12373
+ * devices, populated by the orchestrator ONLY when `deviceKey` is set and the
12374
+ * node has ≥2 enabled devices. The runner uses it to AUTO-jump an enrichment
12375
+ * step whose model has no build for the elected device's format onto another
12376
+ * enabled device of the SAME node (weighted-least-loaded). Absent/single-entry
12377
+ * ⇒ no jump possible; the step runs on `deviceKey`.
12378
+ */
12379
+ inferenceDevices: array(RunnerInferenceDeviceSchema).readonly().optional()
12340
12380
  });
12341
12381
  /**
12342
12382
  * Per-device settings UI fields the runner cap owns. Co-located with
@@ -12516,7 +12556,18 @@ var RunnerLocalMetricsSchema = object({
12516
12556
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number()).readonly()), method(object({
12517
12557
  handle: FrameHandleSchema,
12518
12558
  bbox: NativeCropBboxSchema,
12519
- maxWidth: number().int().positive().optional()
12559
+ maxWidth: number().int().positive().optional(),
12560
+ /**
12561
+ * When `true`, the runner encodes the resolved crop to JPEG ON THE
12562
+ * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
12563
+ * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
12564
+ * agent) so the compressed payload fits Moleculer's `maxPacketSize` and
12565
+ * no raw pixels cross the process boundary. Same-node callers omit it
12566
+ * and keep the zero-copy raw `bytes` path. Additive + optional: a
12567
+ * pre-encode runner (version skew) ignores it and returns `bytes`, so
12568
+ * the caller falls back to encoding locally.
12569
+ */
12570
+ encodeJpeg: boolean().optional()
12520
12571
  }), NativeCropResultSchema.nullable()), method(object({
12521
12572
  deviceId: number(),
12522
12573
  frameHandle: FrameHandleSchema.optional(),
@@ -17428,7 +17479,8 @@ var MediaFileKindEnum = _enum([
17428
17479
  "faceCrop",
17429
17480
  "plateCrop",
17430
17481
  "keyFrame",
17431
- "keyFrameSmall"
17482
+ "keyFrameSmall",
17483
+ "thumbnailSmall"
17432
17484
  ]);
17433
17485
  var MediaFileSchema = object({
17434
17486
  key: string(),
@@ -17759,7 +17811,16 @@ var PipelineTemplateSchema = object({
17759
17811
  });
17760
17812
  var DeviceStepConfigSchema = object({
17761
17813
  modelId: string().optional(),
17762
- settings: record(string(), unknown()).optional()
17814
+ settings: record(string(), unknown()).optional(),
17815
+ /**
17816
+ * Step-tree device jump (same-node, phase 1) — OPTIONAL manual override.
17817
+ * When set, this step runs on the named enabled device of the SAME node
17818
+ * (`<backend>:<device>`) instead of the camera's elected device, bypassing
17819
+ * the runtime AUTO-jump. The orchestrator rejects a `jumpDeviceKey` pointing
17820
+ * at a disabled/absent device on that node at save time. Absent ⇒ the runner
17821
+ * auto-jumps only when the effective device's format cannot run the step.
17822
+ */
17823
+ jumpDeviceKey: string().optional()
17763
17824
  });
17764
17825
  var AgentPipelineSettingsSchema = object({
17765
17826
  maxCameras: number().int().nonnegative().nullable().default(null),
@@ -26572,6 +26633,25 @@ function defaultModelIdForDevice(deviceKey) {
26572
26633
  return "yolo26n";
26573
26634
  }
26574
26635
  /**
26636
+ * Step-tree device jump (phase 1): validate every `steps[step].jumpDeviceKey`
26637
+ * manual override in a to-be-saved `inferenceDevices` map. A jump target MUST be
26638
+ * an enabled∧available device on the SAME node and DIFFERENT from the owning
26639
+ * device. `enabledAvailableKeys` is the effective enabled∧available set (from
26640
+ * `mergeInferenceDevices(probe, submitted)`) so an absent/unplugged/disabled
26641
+ * target is rejected honestly (an operator can't route a step onto a dead pool).
26642
+ * Returns the FIRST human-readable error, or `null` when every override is
26643
+ * valid. Pure + deterministic.
26644
+ */
26645
+ function validateJumpTargets(inferenceDevices, enabledAvailableKeys) {
26646
+ for (const [deviceKey, entry] of Object.entries(inferenceDevices)) for (const [stepId, step] of Object.entries(entry.steps ?? {})) {
26647
+ const target = step.jumpDeviceKey;
26648
+ if (target === void 0) continue;
26649
+ if (target === deviceKey) return `step "${stepId}" on "${deviceKey}" has a jumpDeviceKey pointing at its own device`;
26650
+ if (!enabledAvailableKeys.has(target)) return `step "${stepId}" on "${deviceKey}" has a jumpDeviceKey "${target}" that is not an enabled, available device on this node`;
26651
+ }
26652
+ return null;
26653
+ }
26654
+ /**
26575
26655
  * Merge a node's live-probed inference devices with its stored per-device map.
26576
26656
  *
26577
26657
  * The default is **AUTO = all discovered ACCELERATORS enabled** (spec C2,
@@ -26640,6 +26720,23 @@ function eligibleInferenceDeviceWeights(probed, stored) {
26640
26720
  for (const d of mergeInferenceDevices(probed, stored)) if (d.enabled && d.available) out[d.key] = d.weight;
26641
26721
  return out;
26642
26722
  }
26723
+ /**
26724
+ * Step-tree device jump (phase 1): the attach-payload roster of a node's
26725
+ * enabled∧available inference devices with the balancer knobs (`weight`,
26726
+ * `maxSessions`) the runner uses to AUTO-jump an enrichment step off a device
26727
+ * whose format can't run it. Built from the SAME `eligible` (deviceKey→weight)
26728
+ * and `caps` (deviceKey→maxSessions) the dispatcher already computes, so the
26729
+ * roster the runner sees exactly matches the balancer's candidate set. Sorted
26730
+ * by key for determinism. Populated onto `RunnerCameraConfig.inferenceDevices`
26731
+ * ONLY when a `deviceKey` is elected and there are ≥2 entries.
26732
+ */
26733
+ function buildInferenceDeviceRoster(eligible, caps) {
26734
+ return Object.entries(eligible).map(([deviceKey, weight]) => ({
26735
+ deviceKey,
26736
+ weight: weight > 0 ? weight : 1,
26737
+ maxSessions: caps[deviceKey] ?? null
26738
+ })).toSorted((a, b) => a.deviceKey < b.deviceKey ? -1 : a.deviceKey > b.deviceKey ? 1 : 0);
26739
+ }
26643
26740
  //#endregion
26644
26741
  //#region src/orchestrator-types.ts
26645
26742
  var PHASE_MODE_VALUES = new Set([
@@ -28801,7 +28898,12 @@ function buildRunnerConfig(base, overrides) {
28801
28898
  }
28802
28899
  //#endregion
28803
28900
  //#region src/apply-device-provisioning.ts
28804
- /** Merge base then override for one step. Override keys win; undefined = inherit. */
28901
+ /**
28902
+ * Merge base then override for one step. Override keys win; undefined = inherit.
28903
+ * `jumpDeviceKey` (step-tree device jump, phase 1) is a per-(node,device) BASE
28904
+ * concept ONLY — the per-camera override never carries it (spec: per-camera jump
28905
+ * is out of scope), so it is taken from `base` verbatim.
28906
+ */
28805
28907
  function resolveStep(base, override) {
28806
28908
  if (!base && !override) return void 0;
28807
28909
  const modelId = override?.modelId ?? base?.modelId;
@@ -28809,9 +28911,11 @@ function resolveStep(base, override) {
28809
28911
  ...base?.settings,
28810
28912
  ...override?.settings
28811
28913
  } : void 0;
28914
+ const jumpDeviceKey = base?.jumpDeviceKey;
28812
28915
  return {
28813
28916
  ...modelId !== void 0 ? { modelId } : {},
28814
- ...settings ? { settings } : {}
28917
+ ...settings ? { settings } : {},
28918
+ ...jumpDeviceKey !== void 0 ? { jumpDeviceKey } : {}
28815
28919
  };
28816
28920
  }
28817
28921
  /**
@@ -28829,6 +28933,7 @@ function applyDeviceProvisioning(steps, base, override) {
28829
28933
  ...step,
28830
28934
  ...cfg?.modelId !== void 0 ? { modelId: cfg.modelId } : {},
28831
28935
  ...cfg?.settings ? { settings: cfg.settings } : {},
28936
+ ...cfg?.jumpDeviceKey !== void 0 ? { jumpDeviceKey: cfg.jumpDeviceKey } : {},
28832
28937
  ...children !== void 0 ? { children } : {}
28833
28938
  };
28834
28939
  });
@@ -29813,17 +29918,21 @@ var PlacementService = class {
29813
29918
  dispatchScore = decision.score;
29814
29919
  }
29815
29920
  const targetLoad = loads.find((load) => load.nodeId === targetNodeId);
29921
+ const eligibleDevices = await this.deps.getEligibleInferenceDevices(targetNodeId);
29922
+ const deviceCaps = await this.deps.getInferenceDeviceCaps(targetNodeId);
29816
29923
  const deviceKey = pickInferenceDevice({
29817
- eligible: await this.deps.getEligibleInferenceDevices(targetNodeId),
29924
+ eligible: eligibleDevices,
29818
29925
  loads: targetLoad?.devices ?? [],
29819
- caps: await this.deps.getInferenceDeviceCaps(targetNodeId),
29926
+ caps: deviceCaps,
29820
29927
  pinnedDeviceKey: await this.deps.readPipelineDevicePin(runnerConfig.deviceId)
29821
29928
  }) ?? void 0;
29822
29929
  const deviceBase = deviceKey ? await this.deps.settingsStore.buildNodeInferenceDeviceSteps(targetNodeId, deviceKey) : {};
29823
29930
  const deviceOverride = deviceKey ? await this.deps.settingsStore.readCameraDeviceOverride(runnerConfig.deviceId, targetNodeId, deviceKey) : {};
29931
+ const inferenceDevices = deviceKey && Object.keys(eligibleDevices).length >= 2 ? buildInferenceDeviceRoster(eligibleDevices, deviceCaps) : void 0;
29824
29932
  const attachConfig = {
29825
29933
  ...runnerConfig,
29826
29934
  ...deviceKey ? { deviceKey } : {},
29935
+ ...inferenceDevices ? { inferenceDevices } : {},
29827
29936
  ...runnerConfig.steps ? { steps: applyDeviceProvisioning(runnerConfig.steps, deviceBase, deviceOverride) } : {}
29828
29937
  };
29829
29938
  const dispatchGuard = this.deps.ledger.markDispatching(runnerConfig.deviceId);
@@ -31805,7 +31914,8 @@ var StoredAgentAddonConfigSchema = object({
31805
31914
  });
31806
31915
  var StoredDeviceStepConfigSchema = object({
31807
31916
  modelId: string().optional(),
31808
- settings: record(string(), unknown()).readonly().optional()
31917
+ settings: record(string(), unknown()).readonly().optional(),
31918
+ jumpDeviceKey: string().optional()
31809
31919
  });
31810
31920
  var StoredAgentPipelineSettingsSchema = object({
31811
31921
  addonDefaults: record(string(), StoredAgentAddonConfigSchema).optional(),
@@ -33266,10 +33376,12 @@ var SessionDispatchController = class {
33266
33376
  const deviceBase = deviceKey ? await this.deps.settingsStore.buildNodeInferenceDeviceSteps(targetNodeId, deviceKey) : {};
33267
33377
  const deviceOverride = deviceKey ? await this.deps.settingsStore.readCameraDeviceOverride(deviceId, targetNodeId, deviceKey) : {};
33268
33378
  const steps = applyDeviceProvisioning(pipelineConfig.steps, deviceBase, deviceOverride);
33379
+ const inferenceDevices = deviceKey && Object.keys(enabledDevices).length >= 2 ? buildInferenceDeviceRoster(enabledDevices, deviceCaps) : void 0;
33269
33380
  const zones = await this.deps.listZones(deviceId);
33270
33381
  const sessionConfig = {
33271
33382
  deviceId,
33272
33383
  ...deviceKey ? { deviceKey } : {},
33384
+ ...inferenceDevices ? { inferenceDevices } : {},
33273
33385
  motionCooldownMs: config.motionCooldownMs,
33274
33386
  motionFps: config.motionFps,
33275
33387
  detectionFps: config.detectionFps,
@@ -35316,6 +35428,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
35316
35428
  * pending so a node newly given more capacity picks up stranded cameras.
35317
35429
  */
35318
35430
  async setAgentInferenceDevices(input) {
35431
+ const { probed } = await this.probeNodeInferenceDevices(input.agentNodeId);
35432
+ const enabledAvailableKeys = new Set(mergeInferenceDevices(probed, input.inferenceDevices).filter((d) => d.enabled && d.available).map((d) => d.key));
35433
+ const jumpError = validateJumpTargets(input.inferenceDevices, enabledAvailableKeys);
35434
+ if (jumpError) throw new Error(`setAgentInferenceDevices: ${jumpError}`);
35319
35435
  const existing = (await this.settingsStore.readAgentSettingsMap())[input.agentNodeId];
35320
35436
  const next = existing ? {
35321
35437
  ...existing,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-D2fJDIWd.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-TbyFsMw6.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline-orchestrator",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
5
5
  "keywords": [
6
6
  "camstack",