@camstack/addon-pipeline-orchestrator 1.2.292 → 1.2.293

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
@@ -5410,7 +5410,7 @@ var ZodIssueCode = {
5410
5410
  var ZodFirstPartyTypeKind;
5411
5411
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5412
5412
  //#endregion
5413
- //#region ../types/dist/sleep-C16NCZR2.mjs
5413
+ //#region ../types/dist/sleep-DAJ9Er57.mjs
5414
5414
  /**
5415
5415
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5416
5416
  * window to float samples (D455).
@@ -25033,170 +25033,17 @@ var pipelineExecutorCapability = {
25033
25033
  }
25034
25034
  };
25035
25035
  /**
25036
- * Wire shapes of the Load balancing surface (D748): the session placement
25037
- * policy, the atomic share write, the observed placement statistics, the
25038
- * hardware share suggestion and the stale device pins.
25039
- */
25040
- /**
25041
- * How a new detection session picks its node and device.
25042
- * - `least-loaded` — the historical rule: lowest `attached / weight`, a
25043
- * constant node order at a tie (D37). An idle node always ties at 0, so a
25044
- * weight only bites once a node already holds a session.
25045
- * - `share` — the share of PARALLEL sessions is held to the weights, and a
25046
- * full or drowning node yields to a free one (D748).
25047
- */
25048
- var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
25049
- /**
25050
- * `share` since D750: the percentages are held by default. `least-loaded` stays
25051
- * selectable.
25052
- */
25053
- var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
25054
- /** The global-settings key the policy is stored under (orchestrator `load` section). */
25055
- var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
25056
- /** Parse a stored value; anything unknown is the default. */
25057
- function parseSessionPlacementPolicy(value) {
25058
- const parsed = SessionPlacementPolicySchema.safeParse(value);
25059
- return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
25060
- }
25061
- var LoadBalancingSharesInputSchema = object({
25062
- /** nodeId → share (> 0). A node left out keeps its weight. */
25063
- nodes: record(string(), number().positive()).default({}),
25064
- /** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
25065
- devices: record(string(), record(string(), number().positive())).default({})
25066
- });
25067
- var PlacementStatsInputSchema = object({
25068
- /** Window the observed shares are averaged over (default 1 h, max 24 h). */
25069
- windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
25070
- var PlacementDeviceStatsSchema = object({
25071
- deviceKey: string(),
25072
- /** Sessions running on this device now. */
25073
- sessionsNow: number(),
25074
- /** Sessions started on this device inside the window. */
25075
- sessionsStarted: number(),
25076
- /** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
25077
- observedShare: number().nullable()
25078
- });
25079
- var PlacementNodeStatsSchema = object({
25080
- nodeId: string(),
25081
- sessionsNow: number(),
25082
- sessionsStarted: number(),
25083
- /** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
25084
- observedShare: number().nullable(),
25085
- /** Most sessions this node ran at once inside the window. */
25086
- peakSessions: number(),
25087
- /** Sessions a share election handed elsewhere because this node was full or drowning. */
25088
- yieldsAway: number(),
25089
- devices: array(PlacementDeviceStatsSchema).readonly()
25090
- });
25091
- /** A camera's device pin that the session path cannot honour. */
25092
- var StaleDevicePinSchema = object({
25093
- deviceId: number(),
25094
- deviceKey: string(),
25095
- /** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
25096
- reason: _enum(["disabled", "absent"])
25097
- });
25098
- var PlacementStatsSchema = object({
25099
- policy: SessionPlacementPolicySchema,
25100
- windowMs: number(),
25101
- /** How much of the window was actually observed (the hub keeps it in memory). */
25102
- observedForMs: number(),
25103
- sessionsNow: number(),
25104
- /** Most parallel sessions across the cluster inside the window. */
25105
- clusterPeakSessions: number(),
25106
- nodes: array(PlacementNodeStatsSchema).readonly(),
25107
- stalePins: array(StaleDevicePinSchema).readonly()
25108
- });
25109
- var SuggestSharesInputSchema = object({ headroom: number().min(.3).max(.5).default(.4) });
25110
- var SuggestedDeviceShareSchema = object({
25111
- deviceKey: string(),
25112
- /** Share of the node (0–100, rounded). */
25113
- share: number(),
25114
- modelId: string().nullable(),
25115
- /** Inferences per second at that model, from the measured table. */
25116
- fps: number().nullable(),
25117
- measured: boolean(),
25118
- /** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
25119
- suggestedMaxSessions: number().nullable()
25120
- });
25121
- var SuggestedNodeShareSchema = object({
25122
- nodeId: string(),
25123
- /** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
25124
- share: number().nullable(),
25125
- /** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
25126
- capacitySessions: number(),
25127
- devices: array(SuggestedDeviceShareSchema).readonly(),
25128
- skippedReason: string().nullable()
25129
- });
25130
- var SuggestSharesResultSchema = object({
25131
- headroom: number(),
25132
- /** Camera detection rate the capacity is counted in (the fastest camera's fps). */
25133
- cameraFps: number(),
25134
- /** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
25135
- iterations: number(),
25136
- nodes: array(SuggestedNodeShareSchema).readonly()
25137
- });
25138
- /**
25139
- * Where the node and device shares come from (D750).
25140
- * - `auto` — computed by the system from the cluster's measured capacity at
25141
- * the Auto tune models, and recomputed whenever a node or device joins,
25142
- * leaves, is switched on or off, or changes model.
25143
- * - `manual` — the operator's stored sliders. A node or device that cannot
25144
- * take sessions gives its share to the others in proportion; the stored
25145
- * values are never rewritten for that.
25146
- */
25147
- var ShareSourceSchema = _enum(["auto", "manual"]);
25148
- var DEFAULT_SHARE_SOURCE = "auto";
25149
- /** Headroom the automatic shares are computed at (Auto tune's default). */
25150
- var AUTO_SHARE_HEADROOM = .4;
25151
- /** The global-settings key the share source is stored under (orchestrator `load` section). */
25152
- var SHARE_SOURCE_SETTING_KEY = "shareSource";
25153
- /** Parse a stored value; anything unknown is the default. */
25154
- function parseShareSource(value) {
25155
- const parsed = ShareSourceSchema.safeParse(value);
25156
- return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
25157
- }
25158
- var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
25159
- object({
25160
- nodes: record(string(), number()),
25161
- devices: record(string(), record(string(), number()))
25162
- });
25163
- var EffectiveDeviceShareSchema = object({
25164
- deviceKey: string(),
25165
- /** Can take sessions now (enabled, available, can run the root). */
25166
- eligible: boolean(),
25167
- /** The share the source assigns, as a percentage of the node's devices. */
25168
- configuredPct: number(),
25169
- /** The share actually used, after an ineligible device's share went to the others. */
25170
- effectivePct: number()
25171
- });
25172
- var EffectiveNodeShareSchema = object({
25173
- nodeId: string(),
25174
- eligible: boolean(),
25175
- configuredPct: number(),
25176
- effectivePct: number(),
25177
- devices: array(EffectiveDeviceShareSchema).readonly()
25178
- });
25179
- var EffectiveSharesSchema = object({
25180
- source: ShareSourceSchema,
25181
- policy: SessionPlacementPolicySchema,
25182
- /** `auto` with no computed answer yet: the stored sliders are used meanwhile. */
25183
- autoPending: boolean(),
25184
- /** When the automatic shares were last computed (ms epoch); null = never. */
25185
- computedAt: number().nullable(),
25186
- /** What made the system recompute them last. */
25187
- trigger: string().nullable(),
25188
- nodes: array(EffectiveNodeShareSchema).readonly(),
25189
- /** The relative weights the election reads (before eligibility). */
25190
- nodeWeights: record(string(), number()),
25191
- deviceWeights: record(string(), record(string(), number()))
25192
- });
25193
- /**
25194
25036
  * The wire shapes of the Auto tune plan and apply (D745), used by the
25195
25037
  * `pipeline-orchestrator` cap. The recommender's own types
25196
- * (`recommend.ts`, `demand.ts`) are the TS face of the same data; these
25038
+ * (`recommend.ts`) are the TS face of the same data; these
25197
25039
  * schemas are what crosses the cap boundary.
25198
25040
  */
25199
25041
  var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
25042
+ /** The operator's range for it. */
25043
+ var MIN_SESSIONS_PER_DEVICE_RANGE = {
25044
+ min: 1,
25045
+ max: 8
25046
+ };
25200
25047
  var HardwareClassIdSchema = _enum([
25201
25048
  "intel-core-ultra",
25202
25049
  "intel-n-series",
@@ -25214,8 +25061,9 @@ var AcceleratorKindSchema = _enum([
25214
25061
  ]);
25215
25062
  var RecommendationReasonSchema = discriminatedUnion("kind", [
25216
25063
  object({
25217
- kind: literal("fits"),
25218
- utilisation: number()
25064
+ kind: literal("capacity"),
25065
+ sessions: number(),
25066
+ minSessions: number()
25219
25067
  }),
25220
25068
  object({
25221
25069
  kind: literal("tie-faster"),
@@ -25230,16 +25078,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
25230
25078
  }),
25231
25079
  object({
25232
25080
  kind: literal("no-fit-lightest"),
25233
- utilisation: number()
25081
+ sessions: number(),
25082
+ minSessions: number()
25234
25083
  }),
25235
25084
  object({
25236
25085
  kind: literal("derived-latency"),
25237
25086
  modelId: string()
25238
25087
  }),
25239
- object({
25240
- kind: literal("estimated-load"),
25241
- observedForMs: number().nullable()
25242
- }),
25243
25088
  object({ kind: literal("cpu-device") }),
25244
25089
  object({ kind: literal("coral-untouched") }),
25245
25090
  object({ kind: literal("device-disabled") }),
@@ -25255,28 +25100,13 @@ var RecommendationReasonSchema = discriminatedUnion("kind", [
25255
25100
  format: ModelFormatSchema$1
25256
25101
  })
25257
25102
  ]);
25258
- var DeviceDemandSchema = object({
25259
- nodeId: string(),
25260
- deviceKey: string(),
25261
- demandFps: number(),
25262
- maxCameraFps: number(),
25263
- alwaysOnCameras: number(),
25264
- onMotionSessions: number(),
25265
- onMotionSource: _enum([
25266
- "observed-24h",
25267
- "estimate",
25268
- "none"
25269
- ]),
25270
- observedForMs: number().nullable(),
25271
- /** How on-motion sessions were charged: per-device observation, or split by share (D748). */
25272
- placement: SessionPlacementPolicySchema
25273
- });
25274
25103
  var DeviceModelChoiceSchema = object({
25275
25104
  modelId: string(),
25276
25105
  p50Ms: number(),
25277
25106
  fps: number(),
25278
25107
  measured: boolean(),
25279
- utilisation: number(),
25108
+ /** Parallel sessions at the camera rate with the headroom kept free: ⌊fps × (1 − headroom) / camera fps⌋. */
25109
+ capacitySessions: number(),
25280
25110
  personRecall: number()
25281
25111
  });
25282
25112
  var DeviceRecommendationSchema = object({
@@ -25284,7 +25114,6 @@ var DeviceRecommendationSchema = object({
25284
25114
  accelerator: AcceleratorKindSchema.nullable(),
25285
25115
  enabled: boolean(),
25286
25116
  available: boolean(),
25287
- demand: DeviceDemandSchema.nullable(),
25288
25117
  chosen: DeviceModelChoiceSchema.nullable(),
25289
25118
  reasons: array(RecommendationReasonSchema).readonly()
25290
25119
  });
@@ -25345,20 +25174,20 @@ var AutoTuneOptionsSchema = object({
25345
25174
  /** Share of each accelerator's throughput kept free (0.3–0.5). */
25346
25175
  headroom: number().min(.3).max(.5).default(.4),
25347
25176
  /**
25348
- * What-if shares (D748): plan as if these node/device shares were stored
25349
- * and the policy were `share`. Absent = the stored shares under the stored
25350
- * policy. A what-if plan is for display; apply always re-plans from what
25351
- * is stored.
25177
+ * The least number of parallel sessions every device must sustain at the
25178
+ * camera rate with the headroom kept free (D755). The most accurate model
25179
+ * that reaches it wins.
25352
25180
  */
25353
- shares: LoadBalancingSharesInputSchema.optional()
25181
+ minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
25354
25182
  });
25355
25183
  var AutoTunePlanSchema = object({
25356
25184
  /** Hash of everything the apply will write — the apply refuses a different one. */
25357
25185
  planHash: string(),
25358
25186
  family: literal("yolov9"),
25359
25187
  headroom: number(),
25360
- /** Planned under what-if shares (D748): for display — apply refuses it as stale. */
25361
- whatIf: boolean(),
25188
+ minSessionsPerDevice: number(),
25189
+ /** The detection rate a session is counted at (the fastest camera running the pipeline root). */
25190
+ cameraFps: number(),
25362
25191
  generatedAt: number(),
25363
25192
  nodes: array(NodeRecommendationSchema).readonly(),
25364
25193
  nodeChanges: array(NodeDeviceChangeSchema).readonly(),
@@ -25368,7 +25197,8 @@ var AutoTunePlanSchema = object({
25368
25197
  });
25369
25198
  var AutoTuneApplyInputSchema = object({
25370
25199
  planHash: string(),
25371
- headroom: number().min(.3).max(.5).default(.4)
25200
+ headroom: number().min(.3).max(.5).default(.4),
25201
+ minSessionsPerDevice: number().int().min(MIN_SESSIONS_PER_DEVICE_RANGE.min).max(MIN_SESSIONS_PER_DEVICE_RANGE.max).default(2)
25372
25202
  });
25373
25203
  var AutoTunePhaseResultSchema = object({
25374
25204
  phase: _enum([
@@ -25400,6 +25230,206 @@ var AutoTuneApplyResultSchema = discriminatedUnion("kind", [
25400
25230
  reason: _enum(["apply-in-progress"])
25401
25231
  })
25402
25232
  ]);
25233
+ /**
25234
+ * Wire shapes of the Load balancing surface (D748): the session placement
25235
+ * policy, the atomic share write, the observed placement statistics, the
25236
+ * hardware share suggestion and the stale device pins.
25237
+ */
25238
+ /**
25239
+ * How a new detection session picks its node and device.
25240
+ * - `least-loaded` — the historical rule: lowest `attached / weight`, a
25241
+ * constant node order at a tie (D37). An idle node always ties at 0, so a
25242
+ * weight only bites once a node already holds a session.
25243
+ * - `share` — the share of PARALLEL sessions is held to the weights, and a
25244
+ * full or drowning node yields to a free one (D748).
25245
+ */
25246
+ var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
25247
+ /**
25248
+ * `share` since D750: the percentages are held by default. `least-loaded` stays
25249
+ * selectable.
25250
+ */
25251
+ var DEFAULT_SESSION_PLACEMENT_POLICY = "share";
25252
+ /** The global-settings key the policy is stored under (orchestrator `load` section). */
25253
+ var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
25254
+ /** Parse a stored value; anything unknown is the default. */
25255
+ function parseSessionPlacementPolicy(value) {
25256
+ const parsed = SessionPlacementPolicySchema.safeParse(value);
25257
+ return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
25258
+ }
25259
+ var LoadBalancingSharesInputSchema = object({
25260
+ /** nodeId → share (> 0). A node left out keeps its weight. */
25261
+ nodes: record(string(), number().positive()).default({}),
25262
+ /** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
25263
+ devices: record(string(), record(string(), number().positive())).default({})
25264
+ });
25265
+ var PlacementStatsInputSchema = object({
25266
+ /** Window the observed shares are averaged over (default 1 h, max 24 h). */
25267
+ windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
25268
+ var PlacementDeviceStatsSchema = object({
25269
+ deviceKey: string(),
25270
+ /** Sessions running on this device now. */
25271
+ sessionsNow: number(),
25272
+ /** Sessions started on this device inside the window. */
25273
+ sessionsStarted: number(),
25274
+ /** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
25275
+ observedShare: number().nullable()
25276
+ });
25277
+ var PlacementNodeStatsSchema = object({
25278
+ nodeId: string(),
25279
+ sessionsNow: number(),
25280
+ sessionsStarted: number(),
25281
+ /** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
25282
+ observedShare: number().nullable(),
25283
+ /** Most sessions this node ran at once inside the window. */
25284
+ peakSessions: number(),
25285
+ /** Sessions a share election handed elsewhere because this node was full or drowning. */
25286
+ yieldsAway: number(),
25287
+ devices: array(PlacementDeviceStatsSchema).readonly()
25288
+ });
25289
+ /** A camera's device pin that the session path cannot honour. */
25290
+ var StaleDevicePinSchema = object({
25291
+ deviceId: number(),
25292
+ deviceKey: string(),
25293
+ /** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
25294
+ reason: _enum(["disabled", "absent"])
25295
+ });
25296
+ var PlacementStatsSchema = object({
25297
+ policy: SessionPlacementPolicySchema,
25298
+ windowMs: number(),
25299
+ /** How much of the window was actually observed (the hub keeps it in memory). */
25300
+ observedForMs: number(),
25301
+ sessionsNow: number(),
25302
+ /** Most parallel sessions across the cluster inside the window. */
25303
+ clusterPeakSessions: number(),
25304
+ nodes: array(PlacementNodeStatsSchema).readonly(),
25305
+ stalePins: array(StaleDevicePinSchema).readonly()
25306
+ });
25307
+ /**
25308
+ * Where an automatic share's capacity comes from (D755).
25309
+ * - `hardware` — the static capacity of each device at its Auto tune model:
25310
+ * the measured latency table for the node's hardware class.
25311
+ * - `benchmark` — the node's own measured capacity (node benchmark results).
25312
+ * A node with no results falls back to `hardware`, and says so.
25313
+ */
25314
+ var CapacitySourceSchema = _enum(["hardware", "benchmark"]);
25315
+ /** Why a node under the `benchmark` source was sized from hardware instead. */
25316
+ var CapacityFallbackSchema = _enum(["no-benchmark-results", "partial-benchmark-results"]);
25317
+ var SuggestSharesInputSchema = object({
25318
+ headroom: number().min(.3).max(.5).default(.4),
25319
+ /** Default `hardware`. */
25320
+ source: CapacitySourceSchema.default("hardware")
25321
+ });
25322
+ var SuggestedDeviceShareSchema = object({
25323
+ deviceKey: string(),
25324
+ /** Share of the node (0–100, rounded). */
25325
+ share: number(),
25326
+ modelId: string().nullable(),
25327
+ /** Inferences per second at that model, from the measured table; null when sized by a benchmark. */
25328
+ fps: number().nullable(),
25329
+ /** False for a table cell derived from another chip; true for a measured cell or a benchmark. */
25330
+ measured: boolean(),
25331
+ /**
25332
+ * Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋ from the table,
25333
+ * or the benchmark's measured max sessions. Never enforced.
25334
+ */
25335
+ suggestedMaxSessions: number().nullable()
25336
+ });
25337
+ var SuggestedNodeShareSchema = object({
25338
+ nodeId: string(),
25339
+ /** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
25340
+ share: number().nullable(),
25341
+ /** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
25342
+ capacitySessions: number(),
25343
+ devices: array(SuggestedDeviceShareSchema).readonly(),
25344
+ skippedReason: string().nullable(),
25345
+ /** What this node's capacity was read from; null when the node is skipped. */
25346
+ capacitySource: CapacitySourceSchema.nullable(),
25347
+ /** Set when the `benchmark` source was asked for and this node used `hardware`. */
25348
+ capacityFallback: CapacityFallbackSchema.nullable()
25349
+ });
25350
+ var SuggestSharesResultSchema = object({
25351
+ headroom: number(),
25352
+ /** The source asked for; each node says what it actually used. */
25353
+ source: CapacitySourceSchema,
25354
+ /** Camera detection rate the capacity is counted in (the fastest camera's fps). */
25355
+ cameraFps: number(),
25356
+ /** The Auto tune capacity floor the models were picked at. */
25357
+ minSessionsPerDevice: number(),
25358
+ nodes: array(SuggestedNodeShareSchema).readonly()
25359
+ });
25360
+ /**
25361
+ * Where the node and device shares come from (D750, D755).
25362
+ * - `hardware` (default) — computed by the system from each node's static
25363
+ * capacity (its accelerators at their Auto tune models, from the measured
25364
+ * latency table), recomputed whenever a node or device joins, leaves, is
25365
+ * switched on or off, or changes model. No load input.
25366
+ * - `benchmark` — the same, from each node's measured capacity (node
25367
+ * benchmark results); a node without results falls back to `hardware`.
25368
+ * - `manual` — the operator's stored sliders.
25369
+ * Either way a node or device that cannot take sessions gives its share to
25370
+ * the others in proportion; the stored values are never rewritten for that.
25371
+ */
25372
+ var ShareSourceSchema = _enum([
25373
+ "hardware",
25374
+ "benchmark",
25375
+ "manual"
25376
+ ]);
25377
+ var DEFAULT_SHARE_SOURCE = "hardware";
25378
+ /** `hardware` or `benchmark`: the system computes the shares. */
25379
+ function isAutomaticShareSource(source) {
25380
+ return source !== "manual";
25381
+ }
25382
+ /** Headroom the automatic shares are computed at (Auto tune's default). */
25383
+ var AUTO_SHARE_HEADROOM = .4;
25384
+ /** The global-settings key the share source is stored under (orchestrator `load` section). */
25385
+ var SHARE_SOURCE_SETTING_KEY = "shareSource";
25386
+ /**
25387
+ * Parse a stored value; anything unknown is the default. `auto` (D750, before
25388
+ * D755 split it) reads as `hardware` — that is what it computed.
25389
+ */
25390
+ function parseShareSource(value) {
25391
+ const parsed = ShareSourceSchema.safeParse(value);
25392
+ return parsed.success ? parsed.data : DEFAULT_SHARE_SOURCE;
25393
+ }
25394
+ var SetShareSourceInputSchema = object({ source: ShareSourceSchema });
25395
+ object({
25396
+ nodes: record(string(), number()),
25397
+ devices: record(string(), record(string(), number()))
25398
+ });
25399
+ var EffectiveDeviceShareSchema = object({
25400
+ deviceKey: string(),
25401
+ /** Can take sessions now (enabled, available, can run the root). */
25402
+ eligible: boolean(),
25403
+ /** The share the source assigns, as a percentage of the node's devices. */
25404
+ configuredPct: number(),
25405
+ /** The share actually used, after an ineligible device's share went to the others. */
25406
+ effectivePct: number()
25407
+ });
25408
+ var EffectiveNodeShareSchema = object({
25409
+ nodeId: string(),
25410
+ eligible: boolean(),
25411
+ configuredPct: number(),
25412
+ effectivePct: number(),
25413
+ /** Automatic sources: what this node's share was computed from; null under `manual` or before the first answer. */
25414
+ capacitySource: CapacitySourceSchema.nullable(),
25415
+ /** Automatic `benchmark`: why this node used `hardware` instead. */
25416
+ capacityFallback: CapacityFallbackSchema.nullable(),
25417
+ devices: array(EffectiveDeviceShareSchema).readonly()
25418
+ });
25419
+ var EffectiveSharesSchema = object({
25420
+ source: ShareSourceSchema,
25421
+ policy: SessionPlacementPolicySchema,
25422
+ /** An automatic source with no computed answer yet: the stored sliders are used meanwhile. */
25423
+ autoPending: boolean(),
25424
+ /** When the automatic shares were last computed (ms epoch); null = never. */
25425
+ computedAt: number().nullable(),
25426
+ /** What made the system recompute them last. */
25427
+ trigger: string().nullable(),
25428
+ nodes: array(EffectiveNodeShareSchema).readonly(),
25429
+ /** The relative weights the election reads (before eligibility). */
25430
+ nodeWeights: record(string(), number()),
25431
+ deviceWeights: record(string(), record(string(), number()))
25432
+ });
25403
25433
  object({
25404
25434
  activeCameras: number(),
25405
25435
  throttledCameras: number(),
@@ -26807,8 +26837,10 @@ var CameraStepOverridePatchSchema = object({
26807
26837
  modelId: string().optional(),
26808
26838
  settings: record(string(), unknown()).readonly().optional()
26809
26839
  });
26840
+ var CameraStepSettingsSchema = record(string(), record(string(), unknown()).readonly());
26810
26841
  var CameraPipelineSettingsSchema = object({
26811
26842
  stepToggles: record(string(), boolean()).optional(),
26843
+ stepSettings: CameraStepSettingsSchema.optional(),
26812
26844
  stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
26813
26845
  pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
26814
26846
  });
@@ -27574,9 +27606,11 @@ var pipelineOrchestratorCapability = {
27574
27606
  auth: "admin"
27575
27607
  }),
27576
27608
  /**
27577
- * Auto tune (D745) — DRY RUN. Recommends ONE detector family (YOLOv9)
27578
- * for the whole cluster from each node's static hardware class, the
27579
- * measured latency table and the detection demand, and lists every write
27609
+ * Auto tune (D745, D755) — DRY RUN. Recommends ONE detector family
27610
+ * (YOLOv9) for the whole cluster from each node's static hardware class
27611
+ * and the measured latency table: per device, the most accurate model
27612
+ * that sustains at least `minSessionsPerDevice` parallel sessions at the
27613
+ * camera rate with the headroom kept free (no load input), and lists every write
27580
27614
  * the apply would make: the node model pins set, the enrichment pins and
27581
27615
  * model-bound thresholds removed (node level and per camera), and the
27582
27616
  * per-camera detector thresholds cleared. Writes nothing. Step
@@ -27615,10 +27649,12 @@ var pipelineOrchestratorCapability = {
27615
27649
  */
27616
27650
  getPlacementStats: method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
27617
27651
  /**
27618
- * Load balancing (D748) — shares proportional to each node's measured
27619
- * capacity at the models Auto tune would pick, iterated (≤ 3 rounds)
27620
- * until the shares stop moving; per device, an ADVISORY session cap.
27621
- * Writes nothing.
27652
+ * Load balancing (D748, D755) — shares proportional to each node's
27653
+ * capacity in parallel sessions: from the latency table at the models
27654
+ * Auto tune picks (`hardware`), or from node benchmark results
27655
+ * (`benchmark`, each node without results falling back to `hardware` and
27656
+ * saying so). The automatic shares are exactly this answer. Per device,
27657
+ * an ADVISORY session cap. Writes nothing.
27622
27658
  */
27623
27659
  suggestShares: method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
27624
27660
  /**
@@ -27629,9 +27665,11 @@ var pipelineOrchestratorCapability = {
27629
27665
  */
27630
27666
  getEffectiveShares: method(_void(), EffectiveSharesSchema, { auth: "admin" }),
27631
27667
  /**
27632
- * Load balancing (D750) — switch between automatic and manual shares.
27633
- * Going manual seeds the sliders with the current automatic shares (one
27634
- * atomic write), so nothing jumps; going automatic recomputes at once.
27668
+ * Load balancing (D750, D755) — switch the share source: `hardware`
27669
+ * (automatic, from static capacity), `benchmark` (automatic, from node
27670
+ * benchmark results) or `manual`. Going manual seeds the sliders with the
27671
+ * current automatic shares (one atomic write), so nothing jumps; going
27672
+ * to an automatic source recomputes at once.
27635
27673
  */
27636
27674
  setShareSource: method(SetShareSourceInputSchema, object({ success: literal(true) }), {
27637
27675
  kind: "mutation",
@@ -27666,6 +27704,22 @@ var pipelineOrchestratorCapability = {
27666
27704
  kind: "mutation",
27667
27705
  auth: "admin"
27668
27706
  }),
27707
+ /**
27708
+ * Write or clear one step's CAMERA-WIDE settings (D754) — the fields this
27709
+ * camera overrides on every accelerator it runs on. `settings` is the
27710
+ * whole field-level bag for that step (only the overridden keys; a
27711
+ * model-bound field travels with its binding key); `null` or `{}` clears
27712
+ * it. Merged at dispatch between the node base and the per-(node,device)
27713
+ * override, key by key.
27714
+ */
27715
+ setCameraStepSettings: method(object({
27716
+ deviceId: number(),
27717
+ addonId: string(),
27718
+ settings: record(string(), unknown()).nullable()
27719
+ }), object({ success: literal(true) }), {
27720
+ kind: "mutation",
27721
+ auth: "admin"
27722
+ }),
27669
27723
  /** Write or clear the wholesale per-agent pipeline override. Pass `pipeline: null` to drop the override. */
27670
27724
  setCameraPipelineForAgent: method(object({
27671
27725
  deviceId: number(),
@@ -43954,6 +44008,12 @@ Object.freeze({
43954
44008
  addonId: null,
43955
44009
  access: "create"
43956
44010
  },
44011
+ "pipelineOrchestrator.setCameraStepSettings": {
44012
+ capName: "pipeline-orchestrator",
44013
+ capScope: "system",
44014
+ addonId: null,
44015
+ access: "create"
44016
+ },
43957
44017
  "pipelineOrchestrator.setCameraStepToggle": {
43958
44018
  capName: "pipeline-orchestrator",
43959
44019
  capScope: "system",
@@ -49430,6 +49490,11 @@ Object.freeze({
49430
49490
  form: "single",
49431
49491
  optional: false
49432
49492
  }],
49493
+ "pipelineOrchestrator.setCameraStepSettings": [{
49494
+ name: "deviceId",
49495
+ form: "single",
49496
+ optional: false
49497
+ }],
49433
49498
  "pipelineOrchestrator.setCameraStepToggle": [{
49434
49499
  name: "deviceId",
49435
49500
  form: "single",
@@ -51720,137 +51785,22 @@ var DETECTOR_ACCURACY = {
51720
51785
  personRecall: .646
51721
51786
  }
51722
51787
  };
51723
- var DEFAULT_DEMAND_OPTIONS = {
51724
- onMotionConcurrency: .25,
51725
- onMotionMinConcurrent: 2
51726
- };
51727
- function deviceId(nodeId, deviceKey) {
51728
- return `${nodeId}\u0000${deviceKey}`;
51729
- }
51730
- function charges(load) {
51731
- return load.mode !== "disabled" && load.runsPipelineRoot && load.fps > 0;
51732
- }
51733
- /** The highest `count` rates, summed. */
51734
- function topRatesSum(rates, count) {
51735
- return rates.toSorted((a, b) => b - a).slice(0, Math.max(0, count)).reduce((sum, r) => sum + r, 0);
51736
- }
51737
- /** Concurrent on-motion sessions assumed cluster-wide without an observation. */
51738
- function estimatedConcurrentSessions(cameraCount, options) {
51739
- if (cameraCount <= 0) return 0;
51740
- const share = Math.ceil(options.onMotionConcurrency * cameraCount);
51741
- return Math.min(cameraCount, Math.max(share, Math.min(cameraCount, options.onMotionMinConcurrent)));
51742
- }
51743
- /**
51744
- * Demand on every eligible device of the cluster.
51745
- */
51746
- function estimateDeviceDemand(input) {
51747
- const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
51748
- const placement = input.shares ? "share" : "least-loaded";
51749
- const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
51750
- const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
51751
- const weightByNode = /* @__PURE__ */ new Map();
51752
- const eligibleCountByNode = /* @__PURE__ */ new Map();
51753
- for (const d of input.eligible) {
51754
- weightByNode.set(d.nodeId, (weightByNode.get(d.nodeId) ?? 0) + Math.max(d.weight, 0));
51755
- eligibleCountByNode.set(d.nodeId, (eligibleCountByNode.get(d.nodeId) ?? 0) + 1);
51756
- }
51757
- const pinnedFps = /* @__PURE__ */ new Map();
51758
- const alwaysOnFps = /* @__PURE__ */ new Map();
51759
- const alwaysOnCount = /* @__PURE__ */ new Map();
51760
- const maxFps = /* @__PURE__ */ new Map();
51761
- const onMotionRates = [];
51762
- const bump = (map, key, by) => {
51763
- map.set(key, (map.get(key) ?? 0) + by);
51764
- };
51765
- const noteMax = (key, fps) => {
51766
- maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
51767
- };
51768
- const pinNodeOf = (pin) => {
51769
- if (pin.nodeId !== null) return pin.nodeId;
51770
- const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
51771
- return holders.length === 1 ? holders[0].nodeId : null;
51772
- };
51773
- let alwaysOnTotal = 0;
51774
- for (const cam of input.cameras) {
51775
- if (!charges(cam)) continue;
51776
- const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
51777
- if (cam.devicePin !== null && pinNode !== null) {
51778
- const key = deviceId(pinNode, cam.devicePin.deviceKey);
51779
- if (!eligibleById.has(key)) continue;
51780
- bump(pinnedFps, key, cam.fps);
51781
- noteMax(key, cam.fps);
51782
- continue;
51783
- }
51784
- const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
51785
- if (standing !== null) {
51786
- if (cam.mode === "always-on") alwaysOnTotal += 1;
51787
- const nodeWeight = weightByNode.get(standing) ?? 0;
51788
- for (const d of input.eligible) {
51789
- if (d.nodeId !== standing || nodeWeight <= 0) continue;
51790
- const key = deviceId(d.nodeId, d.deviceKey);
51791
- const share = Math.max(d.weight, 0) / nodeWeight;
51792
- bump(alwaysOnFps, key, cam.fps * share);
51793
- bump(alwaysOnCount, key, share);
51794
- noteMax(key, cam.fps);
51795
- }
51796
- continue;
51797
- }
51798
- onMotionRates.push(cam.fps);
51799
- }
51800
- const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
51801
- const nodeCount = weightByNode.size;
51802
- const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
51803
- const clusterPeak = input.shares?.clusterPeak ?? null;
51804
- const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
51805
- const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
51806
- const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
51807
- return input.eligible.map((d) => {
51808
- const key = deviceId(d.nodeId, d.deviceKey);
51809
- const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
51810
- const alwaysOn = alwaysOnCount.get(key) ?? 0;
51811
- let onMotionSessions = 0;
51812
- let onMotionSource = "none";
51813
- if (onMotionRates.length > 0 && input.shares !== void 0) {
51814
- const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
51815
- const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
51816
- const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
51817
- onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
51818
- onMotionSource = clusterObserved ? "observed-24h" : "estimate";
51819
- } else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
51820
- onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
51821
- onMotionSource = "observed-24h";
51822
- } else {
51823
- const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
51824
- const share = nodeCount > 0 && nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight / nodeCount : 0;
51825
- onMotionSessions = Math.ceil(estimatedTotal * share);
51826
- onMotionSource = "estimate";
51827
- }
51828
- const demandFps = (pinnedFps.get(key) ?? 0) + (alwaysOnFps.get(key) ?? 0) + topRatesSum(onMotionRates, onMotionSessions);
51829
- return {
51830
- nodeId: d.nodeId,
51831
- deviceKey: d.deviceKey,
51832
- demandFps: Math.round(demandFps * 100) / 100,
51833
- maxCameraFps: Math.max(maxFps.get(key) ?? 0, fastestOnMotion),
51834
- alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
51835
- onMotionSessions,
51836
- onMotionSource,
51837
- observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
51838
- placement
51839
- };
51840
- }).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
51841
- }
51842
51788
  /** The family the cluster runs. */
51843
51789
  var AUTO_TUNE_FAMILY = "yolov9";
51790
+ /** Parallel sessions one device sustains at `fps` with `headroom` kept free. */
51791
+ function capacitySessionsOf(fps, headroom, cameraFps) {
51792
+ if (!(fps > 0) || !(cameraFps > 0)) return 0;
51793
+ return Math.max(0, Math.floor(fps * (1 - headroom) / cameraFps + 1e-9));
51794
+ }
51844
51795
  function round(value, digits = 3) {
51845
51796
  const f = 10 ** digits;
51846
51797
  return Math.round(value * f) / f;
51847
51798
  }
51848
- function utilisationOf(cell, demand) {
51849
- return cell.fps > 0 ? round((demand?.demandFps ?? 0) / cell.fps) : Number.POSITIVE_INFINITY;
51799
+ function sessionsOn(cell, sizing) {
51800
+ return cell === void 0 ? 0 : capacitySessionsOf(cell.fps, sizing.headroom, sizing.cameraFps);
51850
51801
  }
51851
- function fitsOn(cell, demand, headroom) {
51852
- const budget = cell.fps * (1 - headroom);
51853
- return budget >= (demand?.demandFps ?? 0) && budget >= (demand?.maxCameraFps ?? 0);
51802
+ function fitsOn(cell, sizing) {
51803
+ return sessionsOn(cell, sizing) >= sizing.minSessions;
51854
51804
  }
51855
51805
  function candidatesFor(contexts, formatsByModel) {
51856
51806
  return Object.keys(DETECTOR_ACCURACY).filter((id) => id.startsWith(AUTO_TUNE_FAMILY)).filter((id) => contexts.every((c) => c.column[id] !== void 0 && (formatsByModel[id] ?? []).includes(c.device.format))).map((id) => ({
@@ -51861,28 +51811,24 @@ function candidatesFor(contexts, formatsByModel) {
51861
51811
  function totalLatency(contexts, modelId) {
51862
51812
  return contexts.reduce((sum, c) => sum + (c.column[modelId]?.p50Ms ?? Number.POSITIVE_INFINITY), 0);
51863
51813
  }
51864
- function worstUtilisation(contexts, modelId) {
51865
- return contexts.reduce((worst, c) => {
51866
- const cell = c.column[modelId];
51867
- return Math.max(worst, cell ? utilisationOf(cell, c.demand) : Number.POSITIVE_INFINITY);
51868
- }, 0);
51814
+ /** The fewest sessions any device of the group sustains with this model. */
51815
+ function worstSessions(contexts, modelId, sizing) {
51816
+ return contexts.reduce((worst, c) => Math.min(worst, sessionsOn(c.column[modelId], sizing)), Number.POSITIVE_INFINITY);
51869
51817
  }
51870
51818
  /** One model for a group of devices that must agree (step 2–4 of the header). */
51871
- function pickForGroup(contexts, formatsByModel, headroom) {
51819
+ function pickForGroup(contexts, formatsByModel, sizing) {
51872
51820
  const candidates = candidatesFor(contexts, formatsByModel);
51873
51821
  if (candidates.length === 0) return null;
51874
- const fitting = candidates.filter((c) => contexts.every((ctx) => {
51875
- const cell = ctx.column[c.modelId];
51876
- return cell !== void 0 && fitsOn(cell, ctx.demand, headroom);
51877
- }));
51822
+ const fitting = candidates.filter((c) => contexts.every((ctx) => fitsOn(ctx.column[c.modelId], sizing)));
51878
51823
  if (fitting.length === 0) {
51879
- const lightest = candidates.toSorted((a, b) => worstUtilisation(contexts, a.modelId) - worstUtilisation(contexts, b.modelId))[0];
51824
+ const lightest = candidates.toSorted((a, b) => worstSessions(contexts, b.modelId, sizing) - worstSessions(contexts, a.modelId, sizing) || totalLatency(contexts, a.modelId) - totalLatency(contexts, b.modelId))[0];
51880
51825
  if (lightest === void 0) return null;
51881
51826
  return {
51882
51827
  modelId: lightest.modelId,
51883
51828
  reasons: [{
51884
51829
  kind: "no-fit-lightest",
51885
- utilisation: worstUtilisation(contexts, lightest.modelId)
51830
+ sessions: worstSessions(contexts, lightest.modelId, sizing),
51831
+ minSessions: sizing.minSessions
51886
51832
  }]
51887
51833
  };
51888
51834
  }
@@ -51892,10 +51838,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
51892
51838
  const reasons = [];
51893
51839
  const mostAccurate = candidates[0];
51894
51840
  if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
51895
- const limitedBy = contexts.find((ctx) => {
51896
- const cell = ctx.column[mostAccurate.modelId];
51897
- return cell === void 0 || !fitsOn(cell, ctx.demand, headroom);
51898
- })?.device.key ?? "";
51841
+ const limitedBy = contexts.find((ctx) => !fitsOn(ctx.column[mostAccurate.modelId], sizing))?.device.key ?? "";
51899
51842
  reasons.push({
51900
51843
  kind: "stepped-down",
51901
51844
  from: mostAccurate.modelId,
@@ -51913,7 +51856,7 @@ function pickForGroup(contexts, formatsByModel, headroom) {
51913
51856
  reasons
51914
51857
  };
51915
51858
  }
51916
- function choiceOn(ctx, modelId) {
51859
+ function choiceOn(ctx, modelId, sizing) {
51917
51860
  const cell = ctx.column[modelId];
51918
51861
  if (cell === void 0) return null;
51919
51862
  return {
@@ -51921,23 +51864,20 @@ function choiceOn(ctx, modelId) {
51921
51864
  p50Ms: cell.p50Ms,
51922
51865
  fps: cell.fps,
51923
51866
  measured: cell.measured,
51924
- utilisation: utilisationOf(cell, ctx.demand),
51867
+ capacitySessions: sessionsOn(cell, sizing),
51925
51868
  personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
51926
51869
  };
51927
51870
  }
51928
- function deviceReasonsFor(ctx, choice) {
51871
+ function deviceReasonsFor(choice, sizing) {
51929
51872
  const out = [{
51930
- kind: "fits",
51931
- utilisation: choice.utilisation
51873
+ kind: "capacity",
51874
+ sessions: choice.capacitySessions,
51875
+ minSessions: sizing.minSessions
51932
51876
  }];
51933
51877
  if (!choice.measured) out.push({
51934
51878
  kind: "derived-latency",
51935
51879
  modelId: choice.modelId
51936
51880
  });
51937
- if (ctx.demand?.onMotionSource === "estimate") out.push({
51938
- kind: "estimated-load",
51939
- observedForMs: ctx.demand.observedForMs
51940
- });
51941
51881
  return out;
51942
51882
  }
51943
51883
  function skippedNode(node, reason, hardwareClass) {
@@ -51953,7 +51893,6 @@ function skippedNode(node, reason, hardwareClass) {
51953
51893
  accelerator: node.hardware ? acceleratorKindOf(d.key, node.hardware) : null,
51954
51894
  enabled: d.enabled,
51955
51895
  available: d.available,
51956
- demand: null,
51957
51896
  chosen: null,
51958
51897
  reasons: []
51959
51898
  })),
@@ -51963,7 +51902,11 @@ function skippedNode(node, reason, hardwareClass) {
51963
51902
  /** Recommend one node. See the module header. */
51964
51903
  function recommendNode(input) {
51965
51904
  const { node } = input;
51966
- const headroom = input.headroom ?? .4;
51905
+ const sizing = {
51906
+ headroom: input.headroom ?? .4,
51907
+ cameraFps: input.cameraFps ?? 10,
51908
+ minSessions: input.minSessionsPerDevice ?? 2
51909
+ };
51967
51910
  const perf = input.perf ?? DETECTOR_PERF;
51968
51911
  if (!node.reachable) return skippedNode(node, { kind: "node-offline" }, null);
51969
51912
  if (node.hardware === null) return skippedNode(node, { kind: "no-hardware-facts" }, null);
@@ -51974,7 +51917,6 @@ function recommendNode(input) {
51974
51917
  kind: "no-measured-data",
51975
51918
  hardwareClass
51976
51919
  }, hardwareClass);
51977
- const demandByKey = new Map(input.demand.filter((d) => d.nodeId === node.nodeId).map((d) => [d.deviceKey, d]));
51978
51920
  const devices = node.devices.toSorted((a, b) => a.key.localeCompare(b.key));
51979
51921
  const contexts = /* @__PURE__ */ new Map();
51980
51922
  const early = /* @__PURE__ */ new Map();
@@ -52003,8 +51945,7 @@ function recommendNode(input) {
52003
51945
  contexts.set(device.key, {
52004
51946
  device,
52005
51947
  accelerator,
52006
- column,
52007
- demand: demandByKey.get(device.key) ?? null
51948
+ column
52008
51949
  });
52009
51950
  }
52010
51951
  const isCpu = (a) => a === "x86-cpu" || a === "arm-cpu";
@@ -52014,23 +51955,23 @@ function recommendNode(input) {
52014
51955
  const reasonsByKey = /* @__PURE__ */ new Map();
52015
51956
  const nodeReasons = [];
52016
51957
  let acceleratorModelId = null;
52017
- const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, headroom) : null;
51958
+ const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, sizing) : null;
52018
51959
  if (groupPick !== null) {
52019
51960
  acceleratorModelId = groupPick.modelId;
52020
51961
  nodeReasons.push(...groupPick.reasons);
52021
51962
  for (const ctx of accelerators) {
52022
- const choice = choiceOn(ctx, groupPick.modelId);
51963
+ const choice = choiceOn(ctx, groupPick.modelId, sizing);
52023
51964
  if (choice === null) continue;
52024
51965
  chosenByKey.set(ctx.device.key, choice);
52025
- reasonsByKey.set(ctx.device.key, deviceReasonsFor(ctx, choice));
51966
+ reasonsByKey.set(ctx.device.key, deviceReasonsFor(choice, sizing));
52026
51967
  }
52027
51968
  } else for (const ctx of accelerators) reasonsByKey.set(ctx.device.key, [{
52028
51969
  kind: "no-family-build",
52029
51970
  format: ctx.device.format
52030
51971
  }]);
52031
51972
  for (const ctx of cpus) {
52032
- const pick = pickForGroup([ctx], node.formatsByModel, headroom);
52033
- const choice = pick === null ? null : choiceOn(ctx, pick.modelId);
51973
+ const pick = pickForGroup([ctx], node.formatsByModel, sizing);
51974
+ const choice = pick === null ? null : choiceOn(ctx, pick.modelId, sizing);
52034
51975
  if (pick === null || choice === null) {
52035
51976
  reasonsByKey.set(ctx.device.key, [{
52036
51977
  kind: "no-family-build",
@@ -52042,7 +51983,7 @@ function recommendNode(input) {
52042
51983
  reasonsByKey.set(ctx.device.key, [
52043
51984
  { kind: "cpu-device" },
52044
51985
  ...pick.reasons,
52045
- ...deviceReasonsFor(ctx, choice)
51986
+ ...deviceReasonsFor(choice, sizing)
52046
51987
  ]);
52047
51988
  }
52048
51989
  return {
@@ -52057,7 +51998,6 @@ function recommendNode(input) {
52057
51998
  accelerator: acceleratorKindOf(device.key, hardware),
52058
51999
  enabled: device.enabled,
52059
52000
  available: device.available,
52060
- demand: demandByKey.get(device.key) ?? null,
52061
52001
  chosen: chosenByKey.get(device.key) ?? null,
52062
52002
  reasons: early.get(device.key) ?? reasonsByKey.get(device.key) ?? []
52063
52003
  })),
@@ -52104,6 +52044,18 @@ function electByShare(candidates) {
52104
52044
  reason: "share"
52105
52045
  };
52106
52046
  }
52047
+ /**
52048
+ * The shares a session election actually reads, and the view the Load
52049
+ * balancing tab shows (D750). Pure.
52050
+ *
52051
+ * The SOURCE decides the configured weights: the automatic capacity shares
52052
+ * (`hardware` or `benchmark`, D755), or the operator's stored sliders. Eligibility decides the effective ones: a
52053
+ * node or device that cannot take a session now (offline, switched off, no
52054
+ * detect capability, unable to run the root) contributes nothing, and the
52055
+ * others split its share in proportion to their own. That redistribution is
52056
+ * exactly what `electByShare` does over its candidate set, so the view and
52057
+ * the election cannot disagree; the stored values are never rewritten for it.
52058
+ */
52107
52059
  /** Weight a node or device has when its source names none (the balancer's own default). */
52108
52060
  var DEFAULT_WEIGHT = 1;
52109
52061
  function positive(value) {
@@ -52114,7 +52066,8 @@ function percentages(keys, weightOf, counts) {
52114
52066
  return new Map(keys.map((k) => [k, total > 0 && counts(k) ? Math.round(weightOf(k) / total * 1e3) / 10 : 0]));
52115
52067
  }
52116
52068
  function resolveEffectiveShares(input) {
52117
- const weights = input.source === "auto" && input.auto !== null && input.auto !== null ? input.auto : input.stored;
52069
+ const useAuto = isAutomaticShareSource(input.source) && input.auto !== null;
52070
+ const weights = useAuto && input.auto !== null ? input.auto : input.stored;
52118
52071
  const nodeWeights = {};
52119
52072
  for (const nodeId of input.nodeIds) nodeWeights[nodeId] = positive(weights.nodes[nodeId]);
52120
52073
  const eligibleNodes = new Set(input.eligibleNodes);
@@ -52136,18 +52089,21 @@ function resolveEffectiveShares(input) {
52136
52089
  configuredPct: devConfigured.get(deviceKey) ?? 0,
52137
52090
  effectivePct: devEffective.get(deviceKey) ?? 0
52138
52091
  }));
52092
+ const capacity = useAuto ? input.autoCapacity?.[nodeId] : void 0;
52139
52093
  return {
52140
52094
  nodeId,
52141
52095
  eligible: eligibleNodes.has(nodeId),
52142
52096
  configuredPct: configured.get(nodeId) ?? 0,
52143
52097
  effectivePct: effective.get(nodeId) ?? 0,
52098
+ capacitySource: capacity?.source ?? null,
52099
+ capacityFallback: capacity?.fallback ?? null,
52144
52100
  devices
52145
52101
  };
52146
52102
  });
52147
52103
  return {
52148
52104
  source: input.source,
52149
52105
  policy: input.policy ?? "share",
52150
- autoPending: input.source === "auto" && input.auto === null,
52106
+ autoPending: isAutomaticShareSource(input.source) && input.auto === null,
52151
52107
  computedAt: input.computedAt ?? null,
52152
52108
  trigger: input.trigger ?? null,
52153
52109
  nodes,
@@ -52156,6 +52112,137 @@ function resolveEffectiveShares(input) {
52156
52112
  };
52157
52113
  }
52158
52114
  /**
52115
+ * Automatic shares from CAPACITY (D755). Pure.
52116
+ *
52117
+ * A node's share is its capacity in parallel sessions over the cluster's; a
52118
+ * device's share inside its node follows its throughput. There is no load
52119
+ * input: nothing here depends on how many sessions ran, only on what each
52120
+ * node can carry.
52121
+ *
52122
+ * Two places a capacity can come from:
52123
+ * - `hardware` — each enabled accelerator at its Auto tune model, from the
52124
+ * measured latency table: ⌊fps × (1 − headroom) / camera fps⌋ sessions;
52125
+ * - `benchmark` — the node's own measured max sessions per device, read
52126
+ * through a {@link NodeCapacitySource}. A node whose results do not cover
52127
+ * every enabled device it would be sized on is sized from `hardware`
52128
+ * instead, and the answer says so per node — never a silent mix.
52129
+ */
52130
+ /** A source that has no results — the benchmark store before it exists. */
52131
+ var NO_MEASURED_CAPACITY = { readMeasuredCapacity: async () => /* @__PURE__ */ new Map() };
52132
+ /** Integer shares summing to 100 (largest remainder); ties broken by key. */
52133
+ function toPercentShares(weights) {
52134
+ const total = [...weights.values()].reduce((a, b) => a + b, 0);
52135
+ const out = /* @__PURE__ */ new Map();
52136
+ if (total <= 0) return out;
52137
+ const raw = [...weights].map(([k, w]) => ({
52138
+ k,
52139
+ exact: w / total * 100
52140
+ }));
52141
+ let assigned = 0;
52142
+ for (const r of raw) {
52143
+ out.set(r.k, Math.floor(r.exact));
52144
+ assigned += Math.floor(r.exact);
52145
+ }
52146
+ const remainder = (exact) => Math.round((exact - Math.floor(exact)) * 1e6);
52147
+ const byRemainder = raw.toSorted((a, b) => remainder(b.exact) - remainder(a.exact) || a.k.localeCompare(b.k));
52148
+ for (let i = 0; i < 100 - assigned; i++) {
52149
+ const r = byRemainder[i % byRemainder.length];
52150
+ if (r === void 0) break;
52151
+ out.set(r.k, (out.get(r.k) ?? 0) + 1);
52152
+ }
52153
+ return out;
52154
+ }
52155
+ function hardwareDevices(rec, headroom, cameraFps) {
52156
+ return rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
52157
+ const fps = d.chosen?.fps ?? 0;
52158
+ return {
52159
+ deviceKey: d.deviceKey,
52160
+ sessions: capacitySessionsOf(fps, headroom, cameraFps),
52161
+ weight: Math.max(fps, 0),
52162
+ modelId: d.chosen?.modelId ?? null,
52163
+ fps: d.chosen?.fps ?? null,
52164
+ measured: d.chosen?.measured ?? false
52165
+ };
52166
+ });
52167
+ }
52168
+ function sizeNode(rec, input) {
52169
+ const fromHardware = (fallback) => {
52170
+ const devices = rec.skipped ? [] : hardwareDevices(rec, input.headroom, input.cameraFps);
52171
+ const capacity = devices.reduce((sum, d) => sum + d.sessions, 0);
52172
+ return {
52173
+ rec,
52174
+ devices,
52175
+ capacitySource: rec.skipped ? null : "hardware",
52176
+ capacityFallback: rec.skipped ? null : fallback,
52177
+ skippedReason: rec.skipped ? rec.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity"
52178
+ };
52179
+ };
52180
+ if (input.source === "hardware") return fromHardware(null);
52181
+ if (!rec.reachable) return fromHardware(null);
52182
+ const results = input.measured.get(rec.nodeId);
52183
+ const byKey = new Map((results?.devices ?? []).map((d) => [d.deviceKey, d]));
52184
+ const usable = rec.devices.filter((d) => d.enabled && d.available);
52185
+ const mustCover = rec.devices.filter((d) => d.enabled && d.available && d.chosen !== null);
52186
+ const covered = usable.filter((d) => byKey.has(d.deviceKey));
52187
+ if (covered.length === 0) return fromHardware("no-benchmark-results");
52188
+ if (mustCover.some((d) => !byKey.has(d.deviceKey))) return fromHardware("partial-benchmark-results");
52189
+ const devices = covered.map((d) => {
52190
+ const row = byKey.get(d.deviceKey);
52191
+ const sessions = Math.max(0, Math.floor(row?.maxSessions ?? 0));
52192
+ return {
52193
+ deviceKey: d.deviceKey,
52194
+ sessions,
52195
+ weight: sessions,
52196
+ modelId: row?.modelId ?? null,
52197
+ fps: null,
52198
+ measured: true
52199
+ };
52200
+ });
52201
+ return {
52202
+ rec,
52203
+ devices,
52204
+ capacitySource: "benchmark",
52205
+ capacityFallback: null,
52206
+ skippedReason: devices.reduce((sum, d) => sum + d.sessions, 0) > 0 ? null : "no-capacity"
52207
+ };
52208
+ }
52209
+ /** Shares proportional to capacity — the automatic shares and the suggestion are this. */
52210
+ function computeCapacityShares(input) {
52211
+ const sized = input.recommendations.map((rec) => sizeNode(rec, input));
52212
+ const capacityByNode = /* @__PURE__ */ new Map();
52213
+ for (const node of sized) {
52214
+ const capacity = node.devices.reduce((sum, d) => sum + d.sessions, 0);
52215
+ if (node.skippedReason === null && capacity > 0) capacityByNode.set(node.rec.nodeId, capacity);
52216
+ }
52217
+ const nodePct = toPercentShares(capacityByNode);
52218
+ const nodes = sized.map((node) => {
52219
+ const devicePct = toPercentShares(new Map(node.devices.map((d) => [d.deviceKey, d.weight])));
52220
+ return {
52221
+ nodeId: node.rec.nodeId,
52222
+ share: nodePct.get(node.rec.nodeId) ?? null,
52223
+ capacitySessions: node.devices.reduce((sum, d) => sum + d.sessions, 0),
52224
+ skippedReason: node.skippedReason,
52225
+ capacitySource: node.capacitySource,
52226
+ capacityFallback: node.capacityFallback,
52227
+ devices: node.devices.map((d) => ({
52228
+ deviceKey: d.deviceKey,
52229
+ share: devicePct.get(d.deviceKey) ?? 0,
52230
+ modelId: d.modelId,
52231
+ fps: d.fps,
52232
+ measured: d.measured,
52233
+ suggestedMaxSessions: d.sessions
52234
+ }))
52235
+ };
52236
+ });
52237
+ return {
52238
+ headroom: input.headroom,
52239
+ source: input.source,
52240
+ cameraFps: input.cameraFps,
52241
+ minSessionsPerDevice: input.minSessionsPerDevice,
52242
+ nodes
52243
+ };
52244
+ }
52245
+ /**
52159
52246
  * THE detail-crop convention — the single derivation of the rectangle a
52160
52247
  * detail/enrichment step (clip-embedding, face-detection, plate-detection…)
52161
52248
  * is fed.
@@ -64627,6 +64714,7 @@ var StoredCameraPipelineForAgentSchema = object({
64627
64714
  });
64628
64715
  var StoredCameraPipelineSettingsSchema = object({
64629
64716
  stepToggles: record(string(), boolean()).optional(),
64717
+ stepSettings: record(string(), record(string(), unknown()).readonly()).optional(),
64630
64718
  stepOverridesByDevice: record(string(), record(string(), record(string(), StoredCameraStepOverridePatchSchema))).optional(),
64631
64719
  stepOverridesByAgent: record(string(), record(string(), LegacyCameraStepOverridePatchSchema)).optional(),
64632
64720
  pipelineByAgent: record(string(), StoredCameraPipelineForAgentSchema).optional(),
@@ -64946,6 +65034,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
64946
65034
  };
64947
65035
  });
64948
65036
  }
65037
+ /**
65038
+ * Write or clear one camera's CAMERA-WIDE settings for `addonId` (D754): the
65039
+ * field-level bag merged between the node base and the per-(node,device)
65040
+ * override on every accelerator. `null` or an empty bag deletes the step's
65041
+ * entry; an empty layer is pruned. Read-patch-write, immutable.
65042
+ */
65043
+ async writeCameraStepSettings(deviceId, addonId, settings) {
65044
+ await this.cameraSettingsState.update((prev) => {
65045
+ const key = String(deviceId);
65046
+ const camera = prev[key] ?? {};
65047
+ const nextLayer = { ...camera.stepSettings };
65048
+ if (settings === null || Object.keys(settings).length === 0) delete nextLayer[addonId];
65049
+ else nextLayer[addonId] = { ...settings };
65050
+ const { stepSettings: _drop, ...rest } = camera;
65051
+ const nextCamera = {
65052
+ ...rest,
65053
+ ...Object.keys(nextLayer).length > 0 ? { stepSettings: nextLayer } : {}
65054
+ };
65055
+ return {
65056
+ ...prev,
65057
+ [key]: nextCamera
65058
+ };
65059
+ });
65060
+ }
64949
65061
  /** Purge one device's entry through the durable handle (whole-blob round-trip). */
64950
65062
  async deleteCameraSettings(deviceId) {
64951
65063
  await this.cameraSettingsState.update((prev) => {
@@ -65512,13 +65624,14 @@ var PipelineSettingsStore = class PipelineSettingsStore {
65512
65624
  async readDeviceProvisioning(deviceId, nodeId, deviceKeys) {
65513
65625
  const [agentBlob, cameraMap] = await Promise.all([this.agentSettingsState.get(), this.readCameraSettingsMap()]);
65514
65626
  const devices = agentBlob[nodeId]?.inferenceDevices;
65515
- const byDevice = cameraMap[String(deviceId)]?.stepOverridesByDevice?.[nodeId];
65627
+ const camera = cameraMap[String(deviceId)];
65628
+ const byDevice = camera?.stepOverridesByDevice?.[nodeId];
65516
65629
  const out = /* @__PURE__ */ new Map();
65517
65630
  for (const deviceKey of deviceKeys) {
65518
- const override = deviceKey === "" ? void 0 : byDevice?.[deviceKey];
65631
+ const perDevice = deviceKey === "" ? void 0 : byDevice?.[deviceKey];
65519
65632
  out.set(deviceKey, {
65520
65633
  base: foldNodeStepSettings(devices, deviceKey),
65521
- override: override ? { ...override } : {}
65634
+ override: layerCameraStepSettings(camera?.stepSettings, perDevice)
65522
65635
  });
65523
65636
  }
65524
65637
  return out;
@@ -65582,6 +65695,30 @@ var PipelineSettingsStore = class PipelineSettingsStore {
65582
65695
  return `tpl_${Date.now().toString(36).slice(-6)}${rand}`;
65583
65696
  }
65584
65697
  };
65698
+ /**
65699
+ * The camera's override for one accelerator, with its CAMERA-WIDE settings
65700
+ * (D754) folded underneath: per step, `settings = { ...camera, ...perDevice }`
65701
+ * (a per-accelerator key wins), and the per-accelerator `modelId` kept as is —
65702
+ * the camera layer carries no model. The result is what
65703
+ * `applyDeviceProvisioning` merges over the node base, so the full order is
65704
+ * node ⊕ camera ⊕ camera-per-accelerator. Fresh objects; a camera with neither
65705
+ * layer yields `{}`.
65706
+ */
65707
+ function layerCameraStepSettings(cameraWide, perDevice) {
65708
+ const out = { ...perDevice };
65709
+ for (const [stepId, settings] of Object.entries(cameraWide ?? {})) {
65710
+ if (Object.keys(settings).length === 0) continue;
65711
+ const own = out[stepId];
65712
+ out[stepId] = {
65713
+ ...own,
65714
+ settings: {
65715
+ ...settings,
65716
+ ...own?.settings
65717
+ }
65718
+ };
65719
+ }
65720
+ return out;
65721
+ }
65585
65722
  //#endregion
65586
65723
  //#region src/pipeline-watchdog.ts
65587
65724
  var DEFAULT_WATCHDOG_THRESHOLDS = {
@@ -69204,7 +69341,6 @@ async function buildOrchestratorControllers(deps) {
69204
69341
  readGlobalSettings: () => globalSettings,
69205
69342
  onCensus: (entries) => {
69206
69343
  motionSliceGate?.noteCensus(entries);
69207
- deps.onRunnerCensus(entries.flatMap((e) => e.load === null ? [] : [e.load]));
69208
69344
  providerSliceGate?.refreshIfStale("runner-census");
69209
69345
  }
69210
69346
  });
@@ -70804,11 +70940,13 @@ function applyCameraAutoTune(blob, input) {
70804
70940
  //#endregion
70805
70941
  //#region src/auto-tune/auto-tune-service.ts
70806
70942
  /**
70807
- * Auto tune (D745): gather the inputs, compute the plan, and apply exactly
70808
- * the plan the operator confirmed.
70943
+ * Auto tune (D745, D755): gather the inputs, compute the plan, and apply
70944
+ * exactly the plan the operator confirmed.
70809
70945
  *
70810
70946
  * The plan is the recommender (`@camstack/types` auto-tune) over every
70811
- * runner node, plus the diff of the writes (`plan-diff.ts`) and the
70947
+ * runner node — sized by a capacity floor (`minSessionsPerDevice` parallel
70948
+ * sessions per device at the camera rate), never by observed or estimated
70949
+ * load — plus the diff of the writes (`plan-diff.ts`) and the
70812
70950
  * per-camera detector thresholds the hub's executor would clear. Its
70813
70951
  * `planHash` names everything the apply writes; the apply recomputes the plan
70814
70952
  * and refuses a different hash, so a configuration change between the two is
@@ -70825,6 +70963,9 @@ function applyCameraAutoTune(blob, input) {
70825
70963
  * A failure after phase 2 leaves "overrides cleared, previous node model",
70826
70964
  * which is a valid configuration (the defaults run); re-running converges.
70827
70965
  */
70966
+ function minSessionsOf(input) {
70967
+ return input.minSessionsPerDevice ?? 2;
70968
+ }
70828
70969
  function errMsg(err) {
70829
70970
  return err instanceof Error ? err.message : String(err);
70830
70971
  }
@@ -70853,29 +70994,6 @@ function boundKeysOf(catalogs) {
70853
70994
  }
70854
70995
  return Object.fromEntries([...byStep].map(([step, set]) => [step, [...set].toSorted()]));
70855
70996
  }
70856
- /** Bounded fixed point of the hardware share suggestion (D748). */
70857
- var SUGGEST_MAX_ROUNDS = 3;
70858
- /** Integer shares summing to 100 (largest remainder). */
70859
- function toPercent(weights) {
70860
- const total = [...weights.values()].reduce((a, b) => a + b, 0);
70861
- const out = /* @__PURE__ */ new Map();
70862
- if (total <= 0) return out;
70863
- const raw = [...weights].map(([k, w]) => ({
70864
- k,
70865
- exact: w / total * 100
70866
- }));
70867
- let assigned = 0;
70868
- for (const r of raw) {
70869
- out.set(r.k, Math.floor(r.exact));
70870
- assigned += Math.floor(r.exact);
70871
- }
70872
- const byRemainder = raw.toSorted((a, b) => b.exact - Math.floor(b.exact) - (a.exact - Math.floor(a.exact)) || a.k.localeCompare(b.k));
70873
- for (let i = 0; i < 100 - assigned; i++) {
70874
- const r = byRemainder[i % byRemainder.length];
70875
- out.set(r.k, (out.get(r.k) ?? 0) + 1);
70876
- }
70877
- return out;
70878
- }
70879
70997
  var AutoTuneService = class {
70880
70998
  deps;
70881
70999
  applying = false;
@@ -70883,87 +71001,38 @@ var AutoTuneService = class {
70883
71001
  this.deps = deps;
70884
71002
  }
70885
71003
  async plan(input) {
70886
- return (await this.computePlan(input.headroom, input.shares)).plan;
71004
+ return (await this.computePlan(input.headroom, minSessionsOf(input))).plan;
70887
71005
  }
70888
71006
  /**
70889
- * Shares proportional to each node's capacity at the models Auto tune
70890
- * would pick under those very shares (D748). Model and load depend on each
70891
- * other, so it iterates from equal shares until the rounded shares stop
70892
- * moving, at most {@link SUGGEST_MAX_ROUNDS} rounds. Per device it reports an
70893
- * ADVISORY session cap — never written, never enforced.
71007
+ * Shares proportional to each node's capacity in parallel sessions (D755):
71008
+ * from the latency table at the models Auto tune picks at its defaults
71009
+ * (`hardware`), or from node benchmark results (`benchmark`), a node
71010
+ * without results falling back to `hardware` and saying so. ONE pass, and
71011
+ * nothing but capacity goes in — the automatic shares are exactly this.
71012
+ * Per device it reports an ADVISORY session cap — never written, never
71013
+ * enforced.
70894
71014
  */
70895
71015
  async suggestShares(input) {
70896
- let shares = {
70897
- nodes: {},
70898
- devices: {}
70899
- };
70900
- let result = [];
70901
- let cameraFps = 10;
70902
- let rounds = 0;
70903
- for (let round = 1; round <= SUGGEST_MAX_ROUNDS; round++) {
70904
- rounds = round;
70905
- const rec = await this.recommend(input.headroom, shares);
70906
- cameraFps = rec.cameraFps;
70907
- result = this.sharesFrom(rec, input.headroom);
70908
- const next = {
70909
- nodes: Object.fromEntries(result.flatMap((n) => n.share !== null && n.share > 0 ? [[n.nodeId, n.share]] : [])),
70910
- devices: Object.fromEntries(result.map((n) => [n.nodeId, Object.fromEntries(n.devices.flatMap((d) => d.share > 0 ? [[d.deviceKey, d.share]] : []))]))
70911
- };
70912
- const settled = JSON.stringify(next) === JSON.stringify(shares);
70913
- shares = next;
70914
- if (settled) break;
71016
+ const source = input.source ?? "hardware";
71017
+ const minSessionsPerDevice = 2;
71018
+ const rec = await this.recommend(input.headroom, minSessionsPerDevice);
71019
+ let measured = /* @__PURE__ */ new Map();
71020
+ if (source === "benchmark") try {
71021
+ measured = await this.deps.measuredCapacity.readMeasuredCapacity(rec.recommendations.map((r) => r.nodeId));
71022
+ } catch (err) {
71023
+ this.deps.logger.warn("load balancing: benchmark results unreadable — sized from hardware", { meta: { error: errMsg(err) } });
70915
71024
  }
70916
- return {
71025
+ return computeCapacityShares({
71026
+ recommendations: rec.recommendations,
71027
+ source,
71028
+ measured,
70917
71029
  headroom: input.headroom,
70918
- cameraFps,
70919
- iterations: rounds,
70920
- nodes: [...result]
70921
- };
70922
- }
70923
- sharesFrom(rec, headroom) {
70924
- const capacityByNode = /* @__PURE__ */ new Map();
70925
- const perNode = rec.recommendations.map((r) => {
70926
- const devices = r.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
70927
- const fps = d.chosen?.fps ?? 0;
70928
- return {
70929
- d,
70930
- fps,
70931
- sessions: rec.cameraFps > 0 ? Math.floor(fps * (1 - headroom) / rec.cameraFps) : 0
70932
- };
70933
- });
70934
- const capacity = devices.reduce((sum, x) => sum + x.sessions, 0);
70935
- if (!r.skipped && capacity > 0) capacityByNode.set(r.nodeId, capacity);
70936
- return {
70937
- r,
70938
- devices,
70939
- capacity
70940
- };
70941
- });
70942
- const nodePct = toPercent(capacityByNode);
70943
- return perNode.map(({ r, devices, capacity }) => {
70944
- const devicePct = toPercent(new Map(devices.map((x) => [x.d.deviceKey, Math.max(x.fps, 0)])));
70945
- return {
70946
- nodeId: r.nodeId,
70947
- share: nodePct.get(r.nodeId) ?? null,
70948
- capacitySessions: capacity,
70949
- skippedReason: r.skipped ? r.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity",
70950
- devices: devices.map((x) => ({
70951
- deviceKey: x.d.deviceKey,
70952
- share: devicePct.get(x.d.deviceKey) ?? 0,
70953
- modelId: x.d.chosen?.modelId ?? null,
70954
- fps: x.d.chosen?.fps ?? null,
70955
- measured: x.d.chosen?.measured ?? false,
70956
- suggestedMaxSessions: x.sessions
70957
- }))
70958
- };
71030
+ cameraFps: rec.cameraFps,
71031
+ minSessionsPerDevice
70959
71032
  });
70960
71033
  }
70961
- /**
70962
- * Gather every node and recommend. `shares` (a what-if or the suggestion's
70963
- * round) forces share placement with those shares overlaid on the stored
70964
- * ones; absent, the STORED policy decides how on-motion load is charged.
70965
- */
70966
- async recommend(headroom, shares) {
71034
+ /** Gather every node and recommend. Nothing but hardware, catalog and the sizing goes in. */
71035
+ async recommend(headroom, minSessionsPerDevice) {
70967
71036
  const nodeIds = [...new Set(["hub", ...this.deps.runnerNodeIds()])].toSorted();
70968
71037
  const gathered = await Promise.all(nodeIds.map(async (nodeId) => {
70969
71038
  const [devices, hardware, catalog] = await Promise.all([
@@ -70978,8 +71047,6 @@ var AutoTuneService = class {
70978
71047
  catalog
70979
71048
  };
70980
71049
  }));
70981
- const effective = await this.deps.readEffectiveWeights();
70982
- const deviceShare = (nodeId, key, stored) => shares?.devices[nodeId]?.[key] ?? effective.devices[nodeId]?.[key] ?? stored;
70983
71050
  const nodes = gathered.map((g) => ({
70984
71051
  nodeId: g.nodeId,
70985
71052
  reachable: g.devices.reachable && g.catalog !== null,
@@ -70992,40 +71059,22 @@ var AutoTuneService = class {
70992
71059
  })),
70993
71060
  formatsByModel: formatsByModelOf(g.catalog)
70994
71061
  }));
70995
- const eligible = gathered.flatMap((g) => g.devices.reachable ? g.devices.devices.filter((d) => d.enabled && d.available).map((d) => ({
70996
- nodeId: g.nodeId,
70997
- deviceKey: d.key,
70998
- weight: deviceShare(g.nodeId, d.key, d.weight)
70999
- })) : []);
71000
- const cameras = await this.deps.cameraLoads();
71001
- const policy = shares ? "share" : this.deps.sessionPlacement();
71002
- const demand = estimateDeviceDemand({
71003
- cameras,
71004
- eligible,
71005
- peaks: this.deps.peaks(),
71006
- ...policy === "share" ? { shares: {
71007
- nodes: {
71008
- ...effective.nodes,
71009
- ...shares?.nodes
71010
- },
71011
- clusterPeak: this.deps.clusterPeak()
71012
- } } : {}
71013
- });
71014
- const recommendations = nodes.map((node) => recommendNode({
71015
- node,
71016
- demand,
71017
- headroom
71018
- }));
71019
- const cameraFps = cameras.reduce((m, c) => c.mode !== "disabled" && c.runsPipelineRoot ? Math.max(m, c.fps) : m, 0);
71062
+ const observedFps = await this.deps.cameraFps();
71063
+ const cameraFps = observedFps !== null && observedFps > 0 ? observedFps : 10;
71020
71064
  return {
71021
71065
  gathered,
71022
71066
  nodes,
71023
- recommendations,
71024
- cameraFps: cameraFps > 0 ? cameraFps : 10
71067
+ recommendations: nodes.map((node) => recommendNode({
71068
+ node,
71069
+ headroom,
71070
+ minSessionsPerDevice,
71071
+ cameraFps
71072
+ })),
71073
+ cameraFps
71025
71074
  };
71026
71075
  }
71027
- async computePlan(headroom, shares) {
71028
- const { gathered, nodes, recommendations } = await this.recommend(headroom, shares);
71076
+ async computePlan(headroom, minSessionsPerDevice) {
71077
+ const { gathered, nodes, recommendations, cameraFps } = await this.recommend(headroom, minSessionsPerDevice);
71029
71078
  const touchedNodes = new Set(recommendations.filter((r) => !r.skipped).map((r) => r.nodeId));
71030
71079
  const targets = /* @__PURE__ */ new Map();
71031
71080
  const formats = /* @__PURE__ */ new Map();
@@ -71085,9 +71134,10 @@ var AutoTuneService = class {
71085
71134
  return {
71086
71135
  plan: {
71087
71136
  planHash: (0, node_crypto.createHash)("sha256").update(canonical({
71088
- version: 1,
71137
+ version: 2,
71089
71138
  headroom,
71090
- whatIf: shares ?? null,
71139
+ minSessionsPerDevice,
71140
+ cameraFps,
71091
71141
  picks: recommendations.map((r) => ({
71092
71142
  nodeId: r.nodeId,
71093
71143
  skipped: r.skipped,
@@ -71099,7 +71149,8 @@ var AutoTuneService = class {
71099
71149
  })).digest("hex"),
71100
71150
  family: "yolov9",
71101
71151
  headroom,
71102
- whatIf: shares !== void 0,
71152
+ minSessionsPerDevice,
71153
+ cameraFps,
71103
71154
  generatedAt: this.deps.now(),
71104
71155
  nodes: recommendations,
71105
71156
  nodeChanges: [...nodeChanges],
@@ -71124,7 +71175,7 @@ var AutoTuneService = class {
71124
71175
  }
71125
71176
  }
71126
71177
  async applyLocked(input) {
71127
- const { plan, diffInput, formats } = await this.computePlan(input.headroom);
71178
+ const { plan, diffInput, formats } = await this.computePlan(input.headroom, minSessionsOf(input));
71128
71179
  if (plan.planHash !== input.planHash) {
71129
71180
  this.deps.logger.warn("auto tune: stale plan refused", { meta: {
71130
71181
  confirmed: input.planHash,
@@ -71267,83 +71318,6 @@ var AutoTuneService = class {
71267
71318
  }
71268
71319
  };
71269
71320
  //#endregion
71270
- //#region src/auto-tune/device-peak-tracker.ts
71271
- var BUCKET_MS = 3600 * 1e3;
71272
- var DevicePeakTracker = class DevicePeakTracker {
71273
- now;
71274
- series = /* @__PURE__ */ new Map();
71275
- cluster = null;
71276
- constructor(now = () => Date.now()) {
71277
- this.now = now;
71278
- }
71279
- static key(nodeId, deviceKey) {
71280
- return `${nodeId}\u0000${deviceKey}`;
71281
- }
71282
- /** Fold one runner census in. A runner that did not answer is simply absent. */
71283
- noteCensus(loads) {
71284
- const now = this.now();
71285
- const bucket = now - now % BUCKET_MS;
71286
- let clusterTotal = 0;
71287
- for (const load of loads) {
71288
- const rows = load.devices.length > 0 ? load.devices : [{
71289
- deviceKey: "",
71290
- attachedCameras: load.attachedCameras
71291
- }];
71292
- for (const device of rows) {
71293
- clusterTotal += device.attachedCameras;
71294
- const key = DevicePeakTracker.key(load.nodeId, device.deviceKey);
71295
- let s = this.series.get(key);
71296
- if (s === void 0) {
71297
- s = {
71298
- firstSeenAt: now,
71299
- buckets: /* @__PURE__ */ new Map()
71300
- };
71301
- this.series.set(key, s);
71302
- }
71303
- s.buckets.set(bucket, Math.max(s.buckets.get(bucket) ?? 0, device.attachedCameras));
71304
- for (const start of s.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) s.buckets.delete(start);
71305
- }
71306
- }
71307
- if (loads.length === 0) return;
71308
- const c = this.cluster ?? {
71309
- firstSeenAt: now,
71310
- buckets: /* @__PURE__ */ new Map()
71311
- };
71312
- this.cluster = c;
71313
- c.buckets.set(bucket, Math.max(c.buckets.get(bucket) ?? 0, clusterTotal));
71314
- for (const start of c.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) c.buckets.delete(start);
71315
- }
71316
- /** The cluster-wide parallel peak over the window, or null before any census. */
71317
- clusterPeak() {
71318
- const c = this.cluster;
71319
- if (c === null) return null;
71320
- const now = this.now();
71321
- let peak = 0;
71322
- for (const [start, value] of c.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
71323
- return {
71324
- peakSessions: peak,
71325
- observedForMs: now - c.firstSeenAt
71326
- };
71327
- }
71328
- /** Every observed device's peak over the window and how long it has been watched. */
71329
- peaks() {
71330
- const now = this.now();
71331
- const out = [];
71332
- for (const [key, s] of this.series) {
71333
- const [nodeId = "", deviceKey = ""] = key.split("\0");
71334
- let peak = 0;
71335
- for (const [start, value] of s.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
71336
- out.push({
71337
- nodeId,
71338
- deviceKey,
71339
- peakSessions: peak,
71340
- observedForMs: now - s.firstSeenAt
71341
- });
71342
- }
71343
- return out.toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
71344
- }
71345
- };
71346
- //#endregion
71347
71321
  //#region src/session-placement-stats.ts
71348
71322
  var RETAIN_MS = 1440 * 60 * 1e3;
71349
71323
  function keyOf(nodeId, deviceKey) {
@@ -71510,6 +71484,16 @@ function applyLoadBalancingShares(prev, write) {
71510
71484
  }
71511
71485
  //#endregion
71512
71486
  //#region src/load-balancing/effective-shares-service.ts
71487
+ /** Skips that mean "not readable yet", never "cannot take sessions". */
71488
+ var NOT_READY_REASONS = new Set(["node-offline", "no-hardware-facts"]);
71489
+ function capacityFrom(result) {
71490
+ const out = {};
71491
+ for (const node of result.nodes) if (node.capacitySource !== null) out[node.nodeId] = {
71492
+ source: node.capacitySource,
71493
+ fallback: node.capacityFallback
71494
+ };
71495
+ return out;
71496
+ }
71513
71497
  function weightsFrom(result) {
71514
71498
  const nodes = {};
71515
71499
  const devices = {};
@@ -71540,19 +71524,27 @@ var EffectiveSharesService = class EffectiveSharesService {
71540
71524
  now() {
71541
71525
  return this.deps.now?.() ?? Date.now();
71542
71526
  }
71543
- /** The last automatic answer; null = never computed. */
71527
+ /** The last automatic answer for the CURRENT source; null = none yet. */
71544
71528
  current() {
71545
- return this.answer;
71529
+ return this.usable();
71530
+ }
71531
+ /** The answer, when the source is automatic and the answer was computed for it. */
71532
+ usable() {
71533
+ const source = this.deps.readSource();
71534
+ if (!isAutomaticShareSource(source)) return null;
71535
+ return this.answer !== null && this.answer.source === source ? this.answer : null;
71546
71536
  }
71547
71537
  /** Node weights the election reads. */
71548
71538
  async nodeWeights() {
71549
- if (this.deps.readSource() === "auto" && this.answer !== null) return { ...this.answer.weights.nodes };
71539
+ const answer = this.usable();
71540
+ if (answer !== null) return { ...answer.weights.nodes };
71550
71541
  return { ...(await this.deps.readStored()).nodes };
71551
71542
  }
71552
71543
  /** The weights of a node's ELIGIBLE devices (`eligible` carries their stored weights). */
71553
71544
  deviceWeights(nodeId, eligible) {
71554
- if (this.deps.readSource() !== "auto" || this.answer === null) return { ...eligible };
71555
- const auto = this.answer.weights.devices[nodeId] ?? {};
71545
+ const answer = this.usable();
71546
+ if (answer === null) return { ...eligible };
71547
+ const auto = answer.weights.devices[nodeId] ?? {};
71556
71548
  const out = {};
71557
71549
  for (const [key, stored] of Object.entries(eligible)) {
71558
71550
  const share = auto[key];
@@ -71562,15 +71554,14 @@ var EffectiveSharesService = class EffectiveSharesService {
71562
71554
  }
71563
71555
  /** Both maps, as the Auto tune recommender charges load by them. */
71564
71556
  async effectiveWeights() {
71565
- if (this.deps.readSource() === "auto" && this.answer !== null) return this.answer.weights;
71566
- return this.deps.readStored();
71557
+ return this.usable()?.weights ?? this.deps.readStored();
71567
71558
  }
71568
71559
  /**
71569
- * Recompute now (auto only). Concurrent calls coalesce: one runs, and one
71570
- * trailing run picks up whatever was reported meanwhile.
71560
+ * Recompute now (automatic sources only). Concurrent calls coalesce: one
71561
+ * runs, and one trailing run picks up whatever was reported meanwhile.
71571
71562
  */
71572
71563
  async recompute(trigger) {
71573
- if (this.deps.readSource() !== "auto") return;
71564
+ if (!isAutomaticShareSource(this.deps.readSource())) return;
71574
71565
  if (this.inFlight !== null) {
71575
71566
  this.pendingTrigger = trigger;
71576
71567
  return this.inFlight;
@@ -71586,11 +71577,13 @@ var EffectiveSharesService = class EffectiveSharesService {
71586
71577
  }
71587
71578
  }
71588
71579
  async run(trigger) {
71580
+ const source = this.deps.readSource();
71581
+ if (!isAutomaticShareSource(source)) return;
71589
71582
  let signature;
71590
71583
  let result;
71591
71584
  try {
71592
71585
  signature = await this.deps.topologySignature();
71593
- result = await this.deps.suggest();
71586
+ result = await this.deps.suggest(source);
71594
71587
  } catch (err) {
71595
71588
  this.deps.logger.warn("load balancing: automatic shares not recomputed — last answer kept", { meta: {
71596
71589
  trigger,
@@ -71601,17 +71594,27 @@ var EffectiveSharesService = class EffectiveSharesService {
71601
71594
  }
71602
71595
  const before = this.answer?.weights ?? null;
71603
71596
  const after = weightsFrom(result);
71597
+ const notReady = result.nodes.filter((n) => n.share === null && NOT_READY_REASONS.has(n.skippedReason ?? "")).map((n) => n.nodeId).toSorted();
71598
+ const capacity = capacityFrom(result);
71604
71599
  this.answer = {
71605
71600
  weights: after,
71601
+ capacity,
71602
+ source,
71606
71603
  computedAt: this.now(),
71607
71604
  trigger,
71608
- signature
71605
+ signature,
71606
+ notReady
71609
71607
  };
71610
71608
  const nodeIds = [...new Set([...Object.keys(before?.nodes ?? {}), ...result.nodes.map((n) => n.nodeId)])];
71611
- for (const nodeId of nodeIds.toSorted()) this.deps.logger.info("load balancing: automatic shares recomputed", {
71609
+ const log = before !== null && JSON.stringify(before) === JSON.stringify(after) && trigger.startsWith("reconcile:not-ready") ? this.deps.logger.debug.bind(this.deps.logger) : this.deps.logger.info.bind(this.deps.logger);
71610
+ for (const nodeId of nodeIds.toSorted()) log("load balancing: automatic shares recomputed", {
71612
71611
  tags: { nodeId },
71613
71612
  meta: {
71614
71613
  trigger,
71614
+ source,
71615
+ capacitySource: capacity[nodeId]?.source ?? null,
71616
+ capacityFallback: capacity[nodeId]?.fallback ?? null,
71617
+ notReady: notReady.includes(nodeId),
71615
71618
  before: before?.nodes[nodeId] ?? null,
71616
71619
  after: after.nodes[nodeId] ?? null,
71617
71620
  devicesBefore: JSON.stringify(before?.devices[nodeId] ?? {}),
@@ -71619,10 +71622,17 @@ var EffectiveSharesService = class EffectiveSharesService {
71619
71622
  }
71620
71623
  });
71621
71624
  }
71622
- /** Recompute if the topology changed or the answer has grown old (auto only). */
71625
+ /**
71626
+ * Recompute if there is no answer for the current source, a node was not
71627
+ * readable yet, the topology changed or the answer has grown old
71628
+ * (automatic sources only).
71629
+ */
71623
71630
  async reconcile() {
71624
- if (this.deps.readSource() !== "auto") return;
71631
+ const source = this.deps.readSource();
71632
+ if (!isAutomaticShareSource(source)) return;
71625
71633
  if (this.answer === null) return this.recompute("reconcile:first");
71634
+ if (this.answer.source !== source) return this.recompute(`reconcile:source:${source}`);
71635
+ if (this.answer.notReady.length > 0) return this.recompute(`reconcile:not-ready:${this.answer.notReady.join(",")}`);
71626
71636
  if (this.now() - this.answer.computedAt > EffectiveSharesService.MAX_AGE_MS) return this.recompute("reconcile:age");
71627
71637
  let signature;
71628
71638
  try {
@@ -71762,7 +71772,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
71762
71772
  */
71763
71773
  clusterModelChoiceReady = false;
71764
71774
  /** The per-device 24 h peak of concurrent sessions, fed by every runner census (D745). */
71765
- devicePeaks = new DevicePeakTracker();
71766
71775
  /** What session placement did (D748): the Load balancing tab's "actual" column. */
71767
71776
  placementStats = new SessionPlacementStats();
71768
71777
  /** The stored session placement policy (D748, default `share` since D750). */
@@ -72048,7 +72057,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
72048
72057
  rebalance: (opts) => this.rebalance(opts),
72049
72058
  setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
72050
72059
  maintenancePaused: () => this.storageMigrationLeaseId !== null,
72051
- onRunnerCensus: (loads) => this.devicePeaks.noteCensus(loads),
72052
72060
  placementStats: this.placementStats,
72053
72061
  setTopology: (topology) => {
72054
72062
  this.topology = topology;
@@ -73171,11 +73179,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73171
73179
  readCameraSettings: () => store.readCameraSettingsMap(),
73172
73180
  rewriteAgentSettings: (transform) => store.rewriteAgentSettingsMap(transform),
73173
73181
  rewriteCameraSettings: (transform) => store.rewriteCameraSettingsMap(transform),
73174
- cameraLoads: () => this.autoTuneCameraLoads(),
73175
- peaks: () => this.devicePeaks.peaks(),
73176
- clusterPeak: () => this.devicePeaks.clusterPeak(),
73177
- sessionPlacement: () => this.readSessionPlacement(),
73178
- readEffectiveWeights: () => this.effectiveShares().effectiveWeights(),
73182
+ cameraFps: async () => this.autoTuneCameraFps(),
73183
+ measuredCapacity: NO_MEASURED_CAPACITY,
73179
73184
  detectorStore: async (input) => {
73180
73185
  const api = this.ctx.api;
73181
73186
  if (!api) throw new Error("ctx.api unavailable");
@@ -73200,28 +73205,18 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73200
73205
  });
73201
73206
  return this.autoTuneService;
73202
73207
  }
73203
- /** Every camera with detection started, as the recommender charges it (D745). */
73204
- async autoTuneCameraLoads() {
73205
- const standing = new Map(this.getPipelineAssignments().map((a) => [a.deviceId, a.agentNodeId]));
73206
- const loads = [];
73207
- for (const [deviceId, config] of this.detectionWiring.activeDetections) {
73208
- const nodePin = await this.readPipelinePin(deviceId);
73209
- const pinnedKey = await this.readPipelineDevicePin(deviceId);
73210
- const standingNode = config.detectionMode === "always-on" ? standing.get(deviceId) ?? null : null;
73211
- loads.push({
73212
- deviceId,
73213
- mode: config.detectionMode,
73214
- fps: config.detectionFps,
73215
- runsPipelineRoot: (config.detectionSources ?? ["pipeline"]).includes("pipeline"),
73216
- standingNodeId: standingNode,
73217
- nodePinId: nodePin,
73218
- devicePin: pinnedKey !== null ? {
73219
- nodeId: nodePin ?? standingNode,
73220
- deviceKey: pinnedKey
73221
- } : null
73222
- });
73208
+ /**
73209
+ * The rate one detection session is counted at (D755): the fastest camera
73210
+ * with detection started that runs the pipeline root; null when none does.
73211
+ */
73212
+ autoTuneCameraFps() {
73213
+ let fps = 0;
73214
+ for (const config of this.detectionWiring.activeDetections.values()) {
73215
+ if (config.detectionMode === "disabled") continue;
73216
+ if (!(config.detectionSources ?? ["pipeline"]).includes("pipeline")) continue;
73217
+ fps = Math.max(fps, config.detectionFps);
73223
73218
  }
73224
- return loads;
73219
+ return fps > 0 ? fps : null;
73225
73220
  }
73226
73221
  async planAutoTune(input) {
73227
73222
  return this.autoTune().plan(input);
@@ -73293,7 +73288,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73293
73288
  this.effectiveSharesService = new EffectiveSharesService({
73294
73289
  readSource: () => this.readShareSource(),
73295
73290
  readStored: async () => storedShareWeights(await store.readAgentSettingsMap()),
73296
- suggest: () => this.autoTune().suggestShares({ headroom: AUTO_SHARE_HEADROOM }),
73291
+ suggest: (source) => this.autoTune().suggestShares({
73292
+ headroom: AUTO_SHARE_HEADROOM,
73293
+ source
73294
+ }),
73297
73295
  topologySignature: async () => shareTopologySignature([...new Set(["hub", ...this.topology.collectRunnerNodeIds()])], await store.readAgentSettingsMap()),
73298
73296
  logger: this.ctx.logger
73299
73297
  });
@@ -73330,6 +73328,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73330
73328
  nodeIds,
73331
73329
  stored: storedShareWeights(await this.settingsStore.readAgentSettingsMap()),
73332
73330
  auto: auto?.weights ?? null,
73331
+ ...auto !== null ? { autoCapacity: auto.capacity } : {},
73333
73332
  eligibleNodes,
73334
73333
  eligibleDevices,
73335
73334
  computedAt: auto?.computedAt ?? null,
@@ -73338,7 +73337,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73338
73337
  }
73339
73338
  async setShareSource(input) {
73340
73339
  const previous = this.readShareSource();
73341
- if (input.source === "manual" && previous === "auto") {
73340
+ if (input.source === "manual" && isAutomaticShareSource(previous)) {
73342
73341
  const auto = this.effectiveShares().current();
73343
73342
  if (auto !== null) await this.setLoadBalancingShares(auto.weights);
73344
73343
  }
@@ -73347,7 +73346,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73347
73346
  from: previous,
73348
73347
  to: input.source
73349
73348
  } });
73350
- if (input.source === "auto") this.noteShareChange("source:auto");
73349
+ if (isAutomaticShareSource(input.source)) this.noteShareChange(`source:${input.source}`);
73351
73350
  return { success: true };
73352
73351
  }
73353
73352
  async getCameraSettings(input) {
@@ -73379,6 +73378,16 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
73379
73378
  await this.emitResolvedCameraUpdated(input.deviceId);
73380
73379
  return { success: true };
73381
73380
  }
73381
+ /**
73382
+ * D754 — write or clear one step's CAMERA-WIDE settings (field-level bag,
73383
+ * merged under the per-(node,device) override on every accelerator). No
73384
+ * deviceKey to validate: the layer is node-independent by construction.
73385
+ */
73386
+ async setCameraStepSettings(input) {
73387
+ await this.settingsStore.writeCameraStepSettings(input.deviceId, input.addonId, input.settings);
73388
+ await this.emitResolvedCameraUpdated(input.deviceId);
73389
+ return { success: true };
73390
+ }
73382
73391
  async setCameraPipelineForAgent(input) {
73383
73392
  const existing = (await this.settingsStore.readCameraSettingsMap())[String(input.deviceId)] ?? {};
73384
73393
  const nextByAgent = { ...existing.pipelineByAgent };