@camstack/types 1.2.317 → 1.2.318

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
@@ -27242,6 +27242,107 @@ var pipelineExecutorCapability = {
27242
27242
  }
27243
27243
  };
27244
27244
  //#endregion
27245
+ //#region src/pipeline/load-balancing/schemas.ts
27246
+ /**
27247
+ * Wire shapes of the Load balancing surface (D748): the session placement
27248
+ * policy, the atomic share write, the observed placement statistics, the
27249
+ * hardware share suggestion and the stale device pins.
27250
+ */
27251
+ /**
27252
+ * How a new detection session picks its node and device.
27253
+ * - `least-loaded` — the historical rule: lowest `attached / weight`, a
27254
+ * constant node order at a tie (D37). An idle node always ties at 0, so a
27255
+ * weight only bites once a node already holds a session.
27256
+ * - `share` — the share of PARALLEL sessions is held to the weights, and a
27257
+ * full or drowning node yields to a free one (D748).
27258
+ */
27259
+ var SessionPlacementPolicySchema = zod.z.enum(["least-loaded", "share"]);
27260
+ var DEFAULT_SESSION_PLACEMENT_POLICY = "least-loaded";
27261
+ /** The global-settings key the policy is stored under (orchestrator `load` section). */
27262
+ var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
27263
+ /** Parse a stored value; anything unknown is the default (nothing changes on deploy). */
27264
+ function parseSessionPlacementPolicy(value) {
27265
+ const parsed = SessionPlacementPolicySchema.safeParse(value);
27266
+ return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
27267
+ }
27268
+ var LoadBalancingSharesInputSchema = zod.z.object({
27269
+ /** nodeId → share (> 0). A node left out keeps its weight. */
27270
+ nodes: zod.z.record(zod.z.string(), zod.z.number().positive()).default({}),
27271
+ /** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
27272
+ devices: zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.number().positive())).default({})
27273
+ });
27274
+ var PlacementStatsInputSchema = zod.z.object({
27275
+ /** Window the observed shares are averaged over (default 1 h, max 24 h). */
27276
+ windowMs: zod.z.number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
27277
+ var PlacementDeviceStatsSchema = zod.z.object({
27278
+ deviceKey: zod.z.string(),
27279
+ /** Sessions running on this device now. */
27280
+ sessionsNow: zod.z.number(),
27281
+ /** Sessions started on this device inside the window. */
27282
+ sessionsStarted: zod.z.number(),
27283
+ /** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
27284
+ observedShare: zod.z.number().nullable()
27285
+ });
27286
+ var PlacementNodeStatsSchema = zod.z.object({
27287
+ nodeId: zod.z.string(),
27288
+ sessionsNow: zod.z.number(),
27289
+ sessionsStarted: zod.z.number(),
27290
+ /** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
27291
+ observedShare: zod.z.number().nullable(),
27292
+ /** Most sessions this node ran at once inside the window. */
27293
+ peakSessions: zod.z.number(),
27294
+ /** Sessions a share election handed elsewhere because this node was full or drowning. */
27295
+ yieldsAway: zod.z.number(),
27296
+ devices: zod.z.array(PlacementDeviceStatsSchema).readonly()
27297
+ });
27298
+ /** A camera's device pin that the session path cannot honour. */
27299
+ var StaleDevicePinSchema = zod.z.object({
27300
+ deviceId: zod.z.number(),
27301
+ deviceKey: zod.z.string(),
27302
+ /** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
27303
+ reason: zod.z.enum(["disabled", "absent"])
27304
+ });
27305
+ var PlacementStatsSchema = zod.z.object({
27306
+ policy: SessionPlacementPolicySchema,
27307
+ windowMs: zod.z.number(),
27308
+ /** How much of the window was actually observed (the hub keeps it in memory). */
27309
+ observedForMs: zod.z.number(),
27310
+ sessionsNow: zod.z.number(),
27311
+ /** Most parallel sessions across the cluster inside the window. */
27312
+ clusterPeakSessions: zod.z.number(),
27313
+ nodes: zod.z.array(PlacementNodeStatsSchema).readonly(),
27314
+ stalePins: zod.z.array(StaleDevicePinSchema).readonly()
27315
+ });
27316
+ var SuggestSharesInputSchema = zod.z.object({ headroom: zod.z.number().min(.3).max(.5).default(.4) });
27317
+ var SuggestedDeviceShareSchema = zod.z.object({
27318
+ deviceKey: zod.z.string(),
27319
+ /** Share of the node (0–100, rounded). */
27320
+ share: zod.z.number(),
27321
+ modelId: zod.z.string().nullable(),
27322
+ /** Inferences per second at that model, from the measured table. */
27323
+ fps: zod.z.number().nullable(),
27324
+ measured: zod.z.boolean(),
27325
+ /** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
27326
+ suggestedMaxSessions: zod.z.number().nullable()
27327
+ });
27328
+ var SuggestedNodeShareSchema = zod.z.object({
27329
+ nodeId: zod.z.string(),
27330
+ /** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
27331
+ share: zod.z.number().nullable(),
27332
+ /** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
27333
+ capacitySessions: zod.z.number(),
27334
+ devices: zod.z.array(SuggestedDeviceShareSchema).readonly(),
27335
+ skippedReason: zod.z.string().nullable()
27336
+ });
27337
+ var SuggestSharesResultSchema = zod.z.object({
27338
+ headroom: zod.z.number(),
27339
+ /** Camera detection rate the capacity is counted in (the fastest camera's fps). */
27340
+ cameraFps: zod.z.number(),
27341
+ /** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
27342
+ iterations: zod.z.number(),
27343
+ nodes: zod.z.array(SuggestedNodeShareSchema).readonly()
27344
+ });
27345
+ //#endregion
27245
27346
  //#region src/pipeline/best-settings/plan-schemas.ts
27246
27347
  /**
27247
27348
  * The wire shapes of the Best settings plan and apply (D745), used by the
@@ -27320,7 +27421,9 @@ var DeviceDemandSchema = zod.z.object({
27320
27421
  "estimate",
27321
27422
  "none"
27322
27423
  ]),
27323
- observedForMs: zod.z.number().nullable()
27424
+ observedForMs: zod.z.number().nullable(),
27425
+ /** How on-motion sessions were charged: per-device observation, or split by share (D748). */
27426
+ placement: SessionPlacementPolicySchema
27324
27427
  });
27325
27428
  var DeviceModelChoiceSchema = zod.z.object({
27326
27429
  modelId: zod.z.string(),
@@ -27392,14 +27495,24 @@ var BestSettingsWarningSchema = zod.z.discriminatedUnion("kind", [
27392
27495
  deviceKey: zod.z.string()
27393
27496
  })
27394
27497
  ]);
27395
- var BestSettingsOptionsSchema = zod.z.object({
27396
- /** Share of each accelerator's throughput kept free (0.3–0.5). */
27397
- headroom: zod.z.number().min(.3).max(.5).default(.4) });
27498
+ var BestSettingsOptionsSchema = zod.z.object({
27499
+ /** Share of each accelerator's throughput kept free (0.3–0.5). */
27500
+ headroom: zod.z.number().min(.3).max(.5).default(.4),
27501
+ /**
27502
+ * What-if shares (D748): plan as if these node/device shares were stored
27503
+ * and the policy were `share`. Absent = the stored shares under the stored
27504
+ * policy. A what-if plan is for display; apply always re-plans from what
27505
+ * is stored.
27506
+ */
27507
+ shares: LoadBalancingSharesInputSchema.optional()
27508
+ });
27398
27509
  var BestSettingsPlanSchema = zod.z.object({
27399
27510
  /** Hash of everything the apply will write — the apply refuses a different one. */
27400
27511
  planHash: zod.z.string(),
27401
27512
  family: zod.z.literal("yolov9"),
27402
27513
  headroom: zod.z.number(),
27514
+ /** Planned under what-if shares (D748): for display — apply refuses it as stale. */
27515
+ whatIf: zod.z.boolean(),
27403
27516
  generatedAt: zod.z.number(),
27404
27517
  nodes: zod.z.array(NodeRecommendationSchema).readonly(),
27405
27518
  nodeChanges: zod.z.array(NodeDeviceChangeSchema).readonly(),
@@ -29055,7 +29168,6 @@ var CameraStepOverridePatchSchema = zod.z.object({
29055
29168
  settings: zod.z.record(zod.z.string(), zod.z.unknown()).readonly().optional()
29056
29169
  });
29057
29170
  var CameraPipelineSettingsSchema = zod.z.object({
29058
- pinnedAgentNodeId: zod.z.string().optional(),
29059
29171
  stepToggles: zod.z.record(zod.z.string(), zod.z.boolean()).optional(),
29060
29172
  stepOverridesByDevice: zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), zod.z.record(zod.z.string(), CameraStepOverridePatchSchema))).optional(),
29061
29173
  pipelineByAgent: zod.z.record(zod.z.string(), CameraPipelineForAgentSchema).optional()
@@ -29749,18 +29861,6 @@ var pipelineOrchestratorCapability = {
29749
29861
  auth: "admin"
29750
29862
  }),
29751
29863
  /**
29752
- * Set a node's detection WEIGHT (relative share for the quota balancer).
29753
- * `null` clears it (back to the implicit weight 1). Unpinned cameras
29754
- * distribute proportionally to weight; `maxCameras` still clamps on top.
29755
- */
29756
- setAgentDetectWeight: require_sleep.method(zod.z.object({
29757
- agentNodeId: zod.z.string(),
29758
- detectWeight: zod.z.number().positive().nullable()
29759
- }), zod.z.object({ success: zod.z.literal(true) }), {
29760
- kind: "mutation",
29761
- auth: "admin"
29762
- }),
29763
- /**
29764
29864
  * Set one node's placement CAPABILITIES — the per-node record that
29765
29865
  * replaced the four flat orchestrator lists (`enabledNodes`,
29766
29866
  * `enabledDecoderNodes`, `enabledAudioNodes`, `remoteSourcingNodes`).
@@ -29900,6 +30000,33 @@ var pipelineOrchestratorCapability = {
29900
30000
  kind: "mutation",
29901
30001
  auth: "admin"
29902
30002
  }),
30003
+ /**
30004
+ * Load balancing (D748) — write node shares (`detectWeight`) and per-node
30005
+ * device shares (inference-device `weight`) in ONE atomic read-modify-write
30006
+ * of the agent settings — the only writer of node shares (the per-node
30007
+ * `setAgentDetectWeight` it replaced raced its siblings, one RMW each).
30008
+ * A node or device left out keeps its weight. Never changes the
30009
+ * `sessionPlacement` policy.
30010
+ */
30011
+ setLoadBalancingShares: require_sleep.method(LoadBalancingSharesInputSchema, zod.z.object({ success: zod.z.literal(true) }), {
30012
+ kind: "mutation",
30013
+ auth: "admin"
30014
+ }),
30015
+ /**
30016
+ * Load balancing (D748) — what placement actually did: per node and
30017
+ * device, sessions now, sessions started and the time-weighted share of
30018
+ * PARALLEL sessions over `windowMs`; the cluster's parallel peak; yields
30019
+ * a share election made; and every device pin the session path cannot
30020
+ * honour. In memory on the hub — a restart starts a new window.
30021
+ */
30022
+ getPlacementStats: require_sleep.method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
30023
+ /**
30024
+ * Load balancing (D748) — shares proportional to each node's measured
30025
+ * capacity at the models Best settings would pick, iterated (≤ 3 rounds)
30026
+ * until the shares stop moving; per device, an ADVISORY session cap.
30027
+ * Writes nothing.
30028
+ */
30029
+ suggestShares: require_sleep.method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
29903
30030
  /** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
29904
30031
  getCameraSettings: require_sleep.method(zod.z.object({ deviceId: zod.z.number() }), CameraPipelineSettingsSchema.nullable()),
29905
30032
  /** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
@@ -56360,6 +56487,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
56360
56487
  addonId: null,
56361
56488
  access: "view"
56362
56489
  },
56490
+ "pipelineOrchestrator.getPlacementStats": {
56491
+ capName: "pipeline-orchestrator",
56492
+ capScope: "system",
56493
+ addonId: null,
56494
+ access: "view"
56495
+ },
56363
56496
  "pipelineOrchestrator.getReconcileFromDiskStatus": {
56364
56497
  capName: "pipeline-orchestrator",
56365
56498
  capScope: "system",
@@ -56450,12 +56583,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
56450
56583
  addonId: null,
56451
56584
  access: "create"
56452
56585
  },
56453
- "pipelineOrchestrator.setAgentDetectWeight": {
56454
- capName: "pipeline-orchestrator",
56455
- capScope: "system",
56456
- addonId: null,
56457
- access: "create"
56458
- },
56459
56586
  "pipelineOrchestrator.setAgentInferenceDevices": {
56460
56587
  capName: "pipeline-orchestrator",
56461
56588
  capScope: "system",
@@ -56510,12 +56637,24 @@ var METHOD_ACCESS_MAP = Object.freeze({
56510
56637
  addonId: null,
56511
56638
  access: "create"
56512
56639
  },
56640
+ "pipelineOrchestrator.setLoadBalancingShares": {
56641
+ capName: "pipeline-orchestrator",
56642
+ capScope: "system",
56643
+ addonId: null,
56644
+ access: "create"
56645
+ },
56513
56646
  "pipelineOrchestrator.setPipelineDevicePin": {
56514
56647
  capName: "pipeline-orchestrator",
56515
56648
  capScope: "system",
56516
56649
  addonId: null,
56517
56650
  access: "create"
56518
56651
  },
56652
+ "pipelineOrchestrator.suggestShares": {
56653
+ capName: "pipeline-orchestrator",
56654
+ capScope: "system",
56655
+ addonId: null,
56656
+ access: "view"
56657
+ },
56519
56658
  "pipelineOrchestrator.unassignAudio": {
56520
56659
  capName: "pipeline-orchestrator",
56521
56660
  capScope: "system",
@@ -67270,7 +67409,6 @@ function createSystemProxy(api) {
67270
67409
  listAgentSettings: (input) => dispatch("pipelineOrchestrator", "listAgentSettings", "query", input),
67271
67410
  removeAgentSettings: (input) => dispatch("pipelineOrchestrator", "removeAgentSettings", "mutation", input),
67272
67411
  setAgentMaxCameras: (input) => dispatch("pipelineOrchestrator", "setAgentMaxCameras", "mutation", input),
67273
- setAgentDetectWeight: (input) => dispatch("pipelineOrchestrator", "setAgentDetectWeight", "mutation", input),
67274
67412
  setAgentCapabilities: (input) => dispatch("pipelineOrchestrator", "setAgentCapabilities", "mutation", input),
67275
67413
  setAgentDecodeLimits: (input) => dispatch("pipelineOrchestrator", "setAgentDecodeLimits", "mutation", input),
67276
67414
  setAgentReachableHost: (input) => dispatch("pipelineOrchestrator", "setAgentReachableHost", "mutation", input),
@@ -67279,6 +67417,9 @@ function createSystemProxy(api) {
67279
67417
  resetNodePipelineDefaults: (input) => dispatch("pipelineOrchestrator", "resetNodePipelineDefaults", "mutation", input),
67280
67418
  planBestSettings: (input) => dispatch("pipelineOrchestrator", "planBestSettings", "query", input),
67281
67419
  applyBestSettings: (input) => dispatch("pipelineOrchestrator", "applyBestSettings", "mutation", input),
67420
+ setLoadBalancingShares: (input) => dispatch("pipelineOrchestrator", "setLoadBalancingShares", "mutation", input),
67421
+ getPlacementStats: (input) => dispatch("pipelineOrchestrator", "getPlacementStats", "query", input),
67422
+ suggestShares: (input) => dispatch("pipelineOrchestrator", "suggestShares", "query", input),
67282
67423
  getCameraStatuses: (input) => dispatch("pipelineOrchestrator", "getCameraStatuses", "query", input),
67283
67424
  setCameraSensors: (input) => dispatch("pipelineOrchestrator", "setCameraSensors", "mutation", input),
67284
67425
  reconcileFromDisk: (input) => dispatch("pipelineOrchestrator", "reconcileFromDisk", "mutation", input),
@@ -72767,6 +72908,7 @@ function estimatedConcurrentSessions(cameraCount, options) {
72767
72908
  */
72768
72909
  function estimateDeviceDemand(input) {
72769
72910
  const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
72911
+ const placement = input.shares ? "share" : "least-loaded";
72770
72912
  const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
72771
72913
  const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
72772
72914
  const weightByNode = /* @__PURE__ */ new Map();
@@ -72786,19 +72928,28 @@ function estimateDeviceDemand(input) {
72786
72928
  const noteMax = (key, fps) => {
72787
72929
  maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
72788
72930
  };
72931
+ const pinNodeOf = (pin) => {
72932
+ if (pin.nodeId !== null) return pin.nodeId;
72933
+ const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
72934
+ return holders.length === 1 ? holders[0].nodeId : null;
72935
+ };
72936
+ let alwaysOnTotal = 0;
72789
72937
  for (const cam of input.cameras) {
72790
72938
  if (!charges(cam)) continue;
72791
- if (cam.devicePin !== null) {
72792
- const key = deviceId(cam.devicePin.nodeId, cam.devicePin.deviceKey);
72939
+ const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
72940
+ if (cam.devicePin !== null && pinNode !== null) {
72941
+ const key = deviceId(pinNode, cam.devicePin.deviceKey);
72793
72942
  if (!eligibleById.has(key)) continue;
72794
72943
  bump(pinnedFps, key, cam.fps);
72795
72944
  noteMax(key, cam.fps);
72796
72945
  continue;
72797
72946
  }
72798
- if (cam.mode === "always-on" && cam.standingNodeId !== null) {
72799
- const nodeWeight = weightByNode.get(cam.standingNodeId) ?? 0;
72947
+ const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
72948
+ if (standing !== null) {
72949
+ if (cam.mode === "always-on") alwaysOnTotal += 1;
72950
+ const nodeWeight = weightByNode.get(standing) ?? 0;
72800
72951
  for (const d of input.eligible) {
72801
- if (d.nodeId !== cam.standingNodeId || nodeWeight <= 0) continue;
72952
+ if (d.nodeId !== standing || nodeWeight <= 0) continue;
72802
72953
  const key = deviceId(d.nodeId, d.deviceKey);
72803
72954
  const share = Math.max(d.weight, 0) / nodeWeight;
72804
72955
  bump(alwaysOnFps, key, cam.fps * share);
@@ -72812,13 +72963,23 @@ function estimateDeviceDemand(input) {
72812
72963
  const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
72813
72964
  const nodeCount = weightByNode.size;
72814
72965
  const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
72966
+ const clusterPeak = input.shares?.clusterPeak ?? null;
72967
+ const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
72968
+ const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
72969
+ const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
72815
72970
  return input.eligible.map((d) => {
72816
72971
  const key = deviceId(d.nodeId, d.deviceKey);
72817
72972
  const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
72818
72973
  const alwaysOn = alwaysOnCount.get(key) ?? 0;
72819
72974
  let onMotionSessions = 0;
72820
72975
  let onMotionSource = "none";
72821
- if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
72976
+ if (onMotionRates.length > 0 && input.shares !== void 0) {
72977
+ const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
72978
+ const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
72979
+ const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
72980
+ onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
72981
+ onMotionSource = clusterObserved ? "observed-24h" : "estimate";
72982
+ } else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
72822
72983
  onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
72823
72984
  onMotionSource = "observed-24h";
72824
72985
  } else {
@@ -72836,7 +72997,8 @@ function estimateDeviceDemand(input) {
72836
72997
  alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
72837
72998
  onMotionSessions,
72838
72999
  onMotionSource,
72839
- observedForMs: peak?.observedForMs ?? null
73000
+ observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
73001
+ placement
72840
73002
  };
72841
73003
  }).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
72842
73004
  }
@@ -73072,6 +73234,48 @@ function recommendNode(input) {
73072
73234
  };
73073
73235
  }
73074
73236
  //#endregion
73237
+ //#region src/pipeline/load-balancing/share-election.ts
73238
+ /** Deficits equal up to float noise (0.7 × 2 − 1 is 0.3999…, not 0.4). */
73239
+ var DEFICIT_EPSILON = 1e-9;
73240
+ function byDeficit(a, b) {
73241
+ const diff = b.deficit - a.deficit;
73242
+ return (Math.abs(diff) < DEFICIT_EPSILON ? 0 : diff) || b.candidate.share - a.candidate.share || a.candidate.key.localeCompare(b.candidate.key);
73243
+ }
73244
+ function spareOf(c) {
73245
+ return c.spare ?? Number.POSITIVE_INFINITY;
73246
+ }
73247
+ function electByShare(candidates) {
73248
+ if (candidates.length === 0) return { kind: "none" };
73249
+ const shareSum = candidates.reduce((s, c) => s + Math.max(c.share, 0), 0);
73250
+ const running = candidates.reduce((s, c) => s + Math.max(c.running, 0), 0);
73251
+ const scored = candidates.map((candidate) => ({
73252
+ candidate,
73253
+ deficit: (shareSum > 0 ? Math.max(candidate.share, 0) / shareSum : 1 / candidates.length) * (running + 1) - candidate.running
73254
+ })).toSorted(byDeficit);
73255
+ const turn = scored[0].candidate;
73256
+ if (!turn.capped && !turn.saturated) return {
73257
+ kind: "elected",
73258
+ key: turn.key,
73259
+ reason: "share"
73260
+ };
73261
+ const free = scored.filter((s) => !s.candidate.capped && !s.candidate.saturated).toSorted((a, b) => spareOf(b.candidate) - spareOf(a.candidate) || byDeficit(a, b));
73262
+ const yieldReason = turn.capped ? "at-cap" : "saturated";
73263
+ if (free.length > 0) return {
73264
+ kind: "elected",
73265
+ key: free[0].candidate.key,
73266
+ reason: "yield",
73267
+ turnKey: turn.key,
73268
+ yieldReason
73269
+ };
73270
+ const uncapped = scored.find((s) => !s.candidate.capped);
73271
+ if (uncapped === void 0) return { kind: "none" };
73272
+ return {
73273
+ kind: "elected",
73274
+ key: uncapped.candidate.key,
73275
+ reason: "share"
73276
+ };
73277
+ }
73278
+ //#endregion
73075
73279
  //#region src/pipeline/root-face-crop.ts
73076
73280
  /** Side of the square, as a multiple of the face box's longer side. */
73077
73281
  var ROOT_FACE_REFINE_SCALE = 1.6;
@@ -75591,6 +75795,7 @@ exports.DEFAULT_POOL_MEMORY_POLICY = DEFAULT_POOL_MEMORY_POLICY;
75591
75795
  exports.DEFAULT_RECORDING_PROFILES = DEFAULT_RECORDING_PROFILES;
75592
75796
  exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
75593
75797
  exports.DEFAULT_RUNTIME_STATE_DURABILITY = require_sleep.DEFAULT_RUNTIME_STATE_DURABILITY;
75798
+ exports.DEFAULT_SESSION_PLACEMENT_POLICY = DEFAULT_SESSION_PLACEMENT_POLICY;
75594
75799
  exports.DEFAULT_TIMELAPSE_PREVIEW_TEXT = DEFAULT_TIMELAPSE_PREVIEW_TEXT;
75595
75800
  exports.DEFAULT_TOKEN_EXPIRY = DEFAULT_TOKEN_EXPIRY;
75596
75801
  exports.DERIVED_CAMERA_SILENCED_CAP_NAMES = DERIVED_CAMERA_SILENCED_CAP_NAMES;
@@ -75887,6 +76092,7 @@ exports.LlmRuntimeStatusSchema = LlmRuntimeStatusSchema;
75887
76092
  exports.LlmTimeoutDefaults = LlmTimeoutDefaults;
75888
76093
  exports.LlmUsageRollupSchema = LlmUsageRollupSchema;
75889
76094
  exports.LlmUsageSchema = LlmUsageSchema;
76095
+ exports.LoadBalancingSharesInputSchema = LoadBalancingSharesInputSchema;
75890
76096
  exports.LoadContributionSchema = LoadContributionSchema;
75891
76097
  exports.LocateSegmentResultSchema = LocateSegmentResultSchema;
75892
76098
  exports.LocationDescriptionAssignmentSchema = LocationDescriptionAssignmentSchema;
@@ -76319,6 +76525,10 @@ exports.PipelineStepInputSchema = PipelineStepInputSchema;
76319
76525
  exports.PipelineValidationIssueSchema = PipelineValidationIssueSchema;
76320
76526
  exports.PipelineValidationResultSchema = PipelineValidationResultSchema;
76321
76527
  exports.PlaceholderReasonSchema = PlaceholderReasonSchema;
76528
+ exports.PlacementDeviceStatsSchema = PlacementDeviceStatsSchema;
76529
+ exports.PlacementNodeStatsSchema = PlacementNodeStatsSchema;
76530
+ exports.PlacementStatsInputSchema = PlacementStatsInputSchema;
76531
+ exports.PlacementStatsSchema = PlacementStatsSchema;
76322
76532
  exports.PolygonPointSchema = PolygonPointSchema;
76323
76533
  exports.PoolMemoryWatchdog = PoolMemoryWatchdog;
76324
76534
  exports.PowerMeterStatusSchema = PowerMeterStatusSchema;
@@ -76481,6 +76691,7 @@ exports.SCENE_RESET_RECAPTURES = SCENE_RESET_RECAPTURES;
76481
76691
  exports.SCOPE_PRESETS = SCOPE_PRESETS;
76482
76692
  exports.SENSOR_FEATURES = SENSOR_FEATURES;
76483
76693
  exports.SENSOR_MAP = SENSOR_MAP;
76694
+ exports.SESSION_PLACEMENT_SETTING_KEY = SESSION_PLACEMENT_SETTING_KEY;
76484
76695
  exports.SHARE_VIEW_KINDS = require_sleep.SHARE_VIEW_KINDS;
76485
76696
  exports.SINGLE_HUB_ADDRESS_ROLES = SINGLE_HUB_ADDRESS_ROLES;
76486
76697
  exports.SOURCE_CAPS = require_composition.SOURCE_CAPS;
@@ -76526,6 +76737,7 @@ exports.ServerUpdateActionResultSchema = ServerUpdateActionResultSchema;
76526
76737
  exports.ServerUpdateCheckResultSchema = ServerUpdateCheckResultSchema;
76527
76738
  exports.ServerUpdateOutcomeSchema = ServerUpdateOutcomeSchema;
76528
76739
  exports.ServerUpdateStateSchema = ServerUpdateStateSchema;
76740
+ exports.SessionPlacementPolicySchema = SessionPlacementPolicySchema;
76529
76741
  exports.SessionTriggerSchema = SessionTriggerSchema;
76530
76742
  exports.SetCameraSensorsInputSchema = SetCameraSensorsInputSchema;
76531
76743
  exports.SetLoggingSettingsInputSchema = SetLoggingSettingsInputSchema;
@@ -76548,6 +76760,7 @@ exports.SnapshotImageSchema = SnapshotImageSchema;
76548
76760
  exports.SourceInfoSchema = SourceInfoSchema;
76549
76761
  exports.SpatialDetectionSchema = SpatialDetectionSchema;
76550
76762
  exports.SsoBridgeClaimsSchema = SsoBridgeClaimsSchema;
76763
+ exports.StaleDevicePinSchema = StaleDevicePinSchema;
76551
76764
  exports.StartEmbeddedInputSchema = StartEmbeddedInputSchema;
76552
76765
  exports.StationaryObjectSchema = StationaryObjectSchema;
76553
76766
  exports.StorageAbortUploadInputSchema = AbortUploadInputSchema;
@@ -76611,6 +76824,10 @@ exports.SubscribeAudioChunksInputSchema = require_sleep.SubscribeAudioChunksInpu
76611
76824
  exports.SubscribeAudioChunksResultSchema = require_sleep.SubscribeAudioChunksResultSchema;
76612
76825
  exports.SubscribeFramesInputSchema = require_sleep.SubscribeFramesInputSchema;
76613
76826
  exports.SubscribeFramesResultSchema = require_sleep.SubscribeFramesResultSchema;
76827
+ exports.SuggestSharesInputSchema = SuggestSharesInputSchema;
76828
+ exports.SuggestSharesResultSchema = SuggestSharesResultSchema;
76829
+ exports.SuggestedDeviceShareSchema = SuggestedDeviceShareSchema;
76830
+ exports.SuggestedNodeShareSchema = SuggestedNodeShareSchema;
76614
76831
  exports.SuppliedRootOutputSchema = SuppliedRootOutputSchema;
76615
76832
  exports.SwitchStatusSchema = SwitchStatusSchema;
76616
76833
  exports.SystemMetricsSchema = SystemMetricsSchema;
@@ -76966,6 +77183,7 @@ exports.doorbellFeatureSources = doorbellFeatureSources;
76966
77183
  exports.droppedConditionsForKind = droppedConditionsForKind;
76967
77184
  exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
76968
77185
  exports.egressTransportFromRequest = egressTransportFromRequest;
77186
+ exports.electByShare = electByShare;
76969
77187
  exports.embeddingEncoderCapability = embeddingEncoderCapability;
76970
77188
  exports.embeddingModelFailedError = embeddingModelFailedError;
76971
77189
  exports.embeddingModelFailedOf = embeddingModelFailedOf;
@@ -77216,6 +77434,7 @@ exports.parseOccupancyScope = parseOccupancyScope;
77216
77434
  exports.parseProcStatus = parseProcStatus;
77217
77435
  exports.parseProfileBrokerId = require_sleep.parseProfileBrokerId;
77218
77436
  exports.parseRuleSection = parseRuleSection;
77437
+ exports.parseSessionPlacementPolicy = parseSessionPlacementPolicy;
77219
77438
  exports.parseStreamParamsFormPatch = parseStreamParamsFormPatch;
77220
77439
  exports.patchAudio = patchAudio;
77221
77440
  exports.petFeederCapability = petFeederCapability;