@camstack/addon-pipeline-orchestrator 1.2.289 → 1.2.291
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/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-B17_Um7q.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CJCGMfMC.mjs} +2 -2
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-bgphbyCd.mjs +26 -0
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CvQcqdZe.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-pEikYuiA.mjs} +1 -1
- package/dist/{hostInit-Bd-NWr7j.mjs → hostInit-BpNmAa0Q.mjs} +2 -2
- package/dist/index.js +2791 -177
- package/dist/index.mjs +2792 -178
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-3MI4PP9B.mjs +0 -26
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -8048,7 +8048,7 @@ import { errMsg } from '@camstack/types'
|
|
|
8048
8048
|
* Extract a human-readable message from an unknown error value.
|
|
8049
8049
|
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
8050
8050
|
*/
|
|
8051
|
-
function errMsg$
|
|
8051
|
+
function errMsg$2(err) {
|
|
8052
8052
|
if (err instanceof Error) return err.message;
|
|
8053
8053
|
if (typeof err === "string") return err;
|
|
8054
8054
|
return String(err);
|
|
@@ -11551,7 +11551,53 @@ var ModelFormatEntrySchema = object({
|
|
|
11551
11551
|
/** Multi-file payload (directory members or sibling files). */
|
|
11552
11552
|
files: array(string()).readonly().optional(),
|
|
11553
11553
|
/** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
|
|
11554
|
-
runtimes: array(_enum(["python"])).readonly().optional()
|
|
11554
|
+
runtimes: array(_enum(["python"])).readonly().optional(),
|
|
11555
|
+
/**
|
|
11556
|
+
* Content revision of the artefact, for one hosted OFF the CamStack mirror.
|
|
11557
|
+
* A format on the mirror leaves it unset: its revision (the sha256 of its
|
|
11558
|
+
* weight file) comes from the downloader's generated table, kept in step
|
|
11559
|
+
* with the mirror by `scripts/check-hosted-model-revisions.ts` (D744). A
|
|
11560
|
+
* node records the revision it downloaded next to the file
|
|
11561
|
+
* (`<file>.revision`); a copy with no record or a different one is stale
|
|
11562
|
+
* unless its weight file hashes to the revision, and a stale copy is
|
|
11563
|
+
* downloaded again, the old one serving until the new one is complete. No
|
|
11564
|
+
* revision at all ⇒ a file on disk is current, whatever it holds (D743).
|
|
11565
|
+
*/
|
|
11566
|
+
revision: string().min(1).optional()
|
|
11567
|
+
});
|
|
11568
|
+
/**
|
|
11569
|
+
* What a node holds for one (model, format) (D746). `current` is a complete
|
|
11570
|
+
* copy at its catalog revision — exactly what `downloaded: true` means.
|
|
11571
|
+
* `stale` is bytes on disk that are not (yet) proven current under D743/D744:
|
|
11572
|
+
* no or a different revision record, or a missing sibling. `missing` is no
|
|
11573
|
+
* copy. `downloaded` alone folds `stale` into "not downloaded", which hid
|
|
11574
|
+
* reclaimable bytes and a copy about to be replaced.
|
|
11575
|
+
*/
|
|
11576
|
+
var ModelOnDiskStateSchema = _enum([
|
|
11577
|
+
"current",
|
|
11578
|
+
"stale",
|
|
11579
|
+
"missing"
|
|
11580
|
+
]);
|
|
11581
|
+
/**
|
|
11582
|
+
* The numeric precision of one format's BUILD (D746) — reported only when the
|
|
11583
|
+
* build states it (file-name token, an INT8 variant, an EdgeTPU compile, a
|
|
11584
|
+
* CamStack Core ML export). Absent means "not stated", never "fp32".
|
|
11585
|
+
*/
|
|
11586
|
+
var ModelFormatPrecisionSchema = _enum([
|
|
11587
|
+
"fp32",
|
|
11588
|
+
"fp16",
|
|
11589
|
+
"int8"
|
|
11590
|
+
]);
|
|
11591
|
+
/**
|
|
11592
|
+
* One format of one model as a node's `pipelineExecutor.getSchema` reports it.
|
|
11593
|
+
* `onDisk` / `precision` are optional for version skew: a runner that predates
|
|
11594
|
+
* them omits them, and a reader falls back to `downloaded`.
|
|
11595
|
+
*/
|
|
11596
|
+
var PipelineModelFormatStatusSchema = object({
|
|
11597
|
+
downloaded: boolean(),
|
|
11598
|
+
sizeMB: number(),
|
|
11599
|
+
onDisk: ModelOnDiskStateSchema.optional(),
|
|
11600
|
+
precision: ModelFormatPrecisionSchema.optional()
|
|
11555
11601
|
});
|
|
11556
11602
|
/**
|
|
11557
11603
|
* Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
|
|
@@ -24297,7 +24343,7 @@ var FrameRefSchema = object({
|
|
|
24297
24343
|
timestamp: number(),
|
|
24298
24344
|
capturedAt: number().optional()
|
|
24299
24345
|
});
|
|
24300
|
-
var ModelFormatSchema$
|
|
24346
|
+
var ModelFormatSchema$2 = _enum([
|
|
24301
24347
|
"onnx",
|
|
24302
24348
|
"coreml",
|
|
24303
24349
|
"openvino",
|
|
@@ -24315,7 +24361,7 @@ var PipelineSlotSchema = _enum([
|
|
|
24315
24361
|
var PipelineEngineChoiceSchema = object({
|
|
24316
24362
|
runtime: literal("python"),
|
|
24317
24363
|
backend: string(),
|
|
24318
|
-
format: ModelFormatSchema$
|
|
24364
|
+
format: ModelFormatSchema$2,
|
|
24319
24365
|
device: string().optional()
|
|
24320
24366
|
});
|
|
24321
24367
|
/**
|
|
@@ -24372,10 +24418,7 @@ var PipelineTemplateSchema$1 = object({
|
|
|
24372
24418
|
var PipelineModelOptionSchema = object({
|
|
24373
24419
|
id: string(),
|
|
24374
24420
|
name: string(),
|
|
24375
|
-
formats: record(string(),
|
|
24376
|
-
downloaded: boolean(),
|
|
24377
|
-
sizeMB: number()
|
|
24378
|
-
})),
|
|
24421
|
+
formats: record(string(), PipelineModelFormatStatusSchema),
|
|
24379
24422
|
group: ModelVariantGroupSchema.optional(),
|
|
24380
24423
|
legacy: boolean().optional(),
|
|
24381
24424
|
provider: ModelProviderIdSchema.optional(),
|
|
@@ -24590,6 +24633,32 @@ var pipelineExecutorCapability = {
|
|
|
24590
24633
|
auth: "admin"
|
|
24591
24634
|
}),
|
|
24592
24635
|
/**
|
|
24636
|
+
* Best settings (D745): the MODEL-BOUND thresholds (`bindsToModel` keys of
|
|
24637
|
+
* the object-detection root, and their binding keys) stored in THIS node's
|
|
24638
|
+
* per-camera detector settings — and, unless `dryRun`, remove them so the
|
|
24639
|
+
* running model's own defaults apply. Every other key of a camera's store
|
|
24640
|
+
* is kept. `nodeId` is the ROUTING key. `deviceIds` absent ⇒ every camera
|
|
24641
|
+
* with a store. A camera whose store could not be read is `refused`, never
|
|
24642
|
+
* reported as clean.
|
|
24643
|
+
*/
|
|
24644
|
+
clearModelBoundDeviceOverrides: method(object({
|
|
24645
|
+
nodeId: string(),
|
|
24646
|
+
deviceIds: array(number()).readonly().optional(),
|
|
24647
|
+
dryRun: boolean()
|
|
24648
|
+
}), object({
|
|
24649
|
+
entries: array(object({
|
|
24650
|
+
deviceId: number(),
|
|
24651
|
+
keys: array(string()).readonly()
|
|
24652
|
+
})).readonly(),
|
|
24653
|
+
refused: array(object({
|
|
24654
|
+
deviceId: number(),
|
|
24655
|
+
reason: string()
|
|
24656
|
+
})).readonly()
|
|
24657
|
+
}), {
|
|
24658
|
+
kind: "mutation",
|
|
24659
|
+
auth: "admin"
|
|
24660
|
+
}),
|
|
24661
|
+
/**
|
|
24593
24662
|
* Which of THIS node's inference devices the executor currently refuses,
|
|
24594
24663
|
* and why. `nodeId` is the ROUTING key (stripped by the generated router).
|
|
24595
24664
|
*
|
|
@@ -24684,12 +24753,12 @@ var pipelineExecutorCapability = {
|
|
|
24684
24753
|
downloadModel: method(object({
|
|
24685
24754
|
addonId: string(),
|
|
24686
24755
|
modelId: string(),
|
|
24687
|
-
format: ModelFormatSchema$
|
|
24756
|
+
format: ModelFormatSchema$2
|
|
24688
24757
|
}), DownloadModelResultSchema, { kind: "mutation" }),
|
|
24689
24758
|
deleteModel: method(object({
|
|
24690
24759
|
addonId: string(),
|
|
24691
24760
|
modelId: string(),
|
|
24692
|
-
format: ModelFormatSchema$
|
|
24761
|
+
format: ModelFormatSchema$2
|
|
24693
24762
|
}), object({ success: literal(true) }), { kind: "mutation" }),
|
|
24694
24763
|
/**
|
|
24695
24764
|
* Stateless single-frame execution. Callers (runner, benchmark) pass
|
|
@@ -24935,6 +25004,315 @@ var pipelineExecutorCapability = {
|
|
|
24935
25004
|
getDetectionConfigSchema: method(_void(), ConfigUISchemaNullableBridge)
|
|
24936
25005
|
}
|
|
24937
25006
|
};
|
|
25007
|
+
/**
|
|
25008
|
+
* Wire shapes of the Load balancing surface (D748): the session placement
|
|
25009
|
+
* policy, the atomic share write, the observed placement statistics, the
|
|
25010
|
+
* hardware share suggestion and the stale device pins.
|
|
25011
|
+
*/
|
|
25012
|
+
/**
|
|
25013
|
+
* How a new detection session picks its node and device.
|
|
25014
|
+
* - `least-loaded` — the historical rule: lowest `attached / weight`, a
|
|
25015
|
+
* constant node order at a tie (D37). An idle node always ties at 0, so a
|
|
25016
|
+
* weight only bites once a node already holds a session.
|
|
25017
|
+
* - `share` — the share of PARALLEL sessions is held to the weights, and a
|
|
25018
|
+
* full or drowning node yields to a free one (D748).
|
|
25019
|
+
*/
|
|
25020
|
+
var SessionPlacementPolicySchema = _enum(["least-loaded", "share"]);
|
|
25021
|
+
var DEFAULT_SESSION_PLACEMENT_POLICY = "least-loaded";
|
|
25022
|
+
/** The global-settings key the policy is stored under (orchestrator `load` section). */
|
|
25023
|
+
var SESSION_PLACEMENT_SETTING_KEY = "sessionPlacement";
|
|
25024
|
+
/** Parse a stored value; anything unknown is the default (nothing changes on deploy). */
|
|
25025
|
+
function parseSessionPlacementPolicy(value) {
|
|
25026
|
+
const parsed = SessionPlacementPolicySchema.safeParse(value);
|
|
25027
|
+
return parsed.success ? parsed.data : DEFAULT_SESSION_PLACEMENT_POLICY;
|
|
25028
|
+
}
|
|
25029
|
+
var LoadBalancingSharesInputSchema = object({
|
|
25030
|
+
/** nodeId → share (> 0). A node left out keeps its weight. */
|
|
25031
|
+
nodes: record(string(), number().positive()).default({}),
|
|
25032
|
+
/** nodeId → deviceKey → share (> 0). A device left out keeps its weight. */
|
|
25033
|
+
devices: record(string(), record(string(), number().positive())).default({})
|
|
25034
|
+
});
|
|
25035
|
+
var PlacementStatsInputSchema = object({
|
|
25036
|
+
/** Window the observed shares are averaged over (default 1 h, max 24 h). */
|
|
25037
|
+
windowMs: number().int().min(6e4).max(1440 * 60 * 1e3).default(3600 * 1e3) });
|
|
25038
|
+
var PlacementDeviceStatsSchema = object({
|
|
25039
|
+
deviceKey: string(),
|
|
25040
|
+
/** Sessions running on this device now. */
|
|
25041
|
+
sessionsNow: number(),
|
|
25042
|
+
/** Sessions started on this device inside the window. */
|
|
25043
|
+
sessionsStarted: number(),
|
|
25044
|
+
/** Time-weighted share of the node's parallel sessions over the window (0–1); null = no session ran. */
|
|
25045
|
+
observedShare: number().nullable()
|
|
25046
|
+
});
|
|
25047
|
+
var PlacementNodeStatsSchema = object({
|
|
25048
|
+
nodeId: string(),
|
|
25049
|
+
sessionsNow: number(),
|
|
25050
|
+
sessionsStarted: number(),
|
|
25051
|
+
/** Time-weighted share of the cluster's parallel sessions over the window (0–1); null = none ran. */
|
|
25052
|
+
observedShare: number().nullable(),
|
|
25053
|
+
/** Most sessions this node ran at once inside the window. */
|
|
25054
|
+
peakSessions: number(),
|
|
25055
|
+
/** Sessions a share election handed elsewhere because this node was full or drowning. */
|
|
25056
|
+
yieldsAway: number(),
|
|
25057
|
+
devices: array(PlacementDeviceStatsSchema).readonly()
|
|
25058
|
+
});
|
|
25059
|
+
/** A camera's device pin that the session path cannot honour. */
|
|
25060
|
+
var StaleDevicePinSchema = object({
|
|
25061
|
+
deviceId: number(),
|
|
25062
|
+
deviceKey: string(),
|
|
25063
|
+
/** `disabled` — the key exists on some node but is not enabled anywhere; `absent` — no node has it. */
|
|
25064
|
+
reason: _enum(["disabled", "absent"])
|
|
25065
|
+
});
|
|
25066
|
+
var PlacementStatsSchema = object({
|
|
25067
|
+
policy: SessionPlacementPolicySchema,
|
|
25068
|
+
windowMs: number(),
|
|
25069
|
+
/** How much of the window was actually observed (the hub keeps it in memory). */
|
|
25070
|
+
observedForMs: number(),
|
|
25071
|
+
sessionsNow: number(),
|
|
25072
|
+
/** Most parallel sessions across the cluster inside the window. */
|
|
25073
|
+
clusterPeakSessions: number(),
|
|
25074
|
+
nodes: array(PlacementNodeStatsSchema).readonly(),
|
|
25075
|
+
stalePins: array(StaleDevicePinSchema).readonly()
|
|
25076
|
+
});
|
|
25077
|
+
var SuggestSharesInputSchema = object({ headroom: number().min(.3).max(.5).default(.4) });
|
|
25078
|
+
var SuggestedDeviceShareSchema = object({
|
|
25079
|
+
deviceKey: string(),
|
|
25080
|
+
/** Share of the node (0–100, rounded). */
|
|
25081
|
+
share: number(),
|
|
25082
|
+
modelId: string().nullable(),
|
|
25083
|
+
/** Inferences per second at that model, from the measured table. */
|
|
25084
|
+
fps: number().nullable(),
|
|
25085
|
+
measured: boolean(),
|
|
25086
|
+
/** Advisory session cap: ⌊fps × (1 − headroom) / camera fps⌋. Never enforced. */
|
|
25087
|
+
suggestedMaxSessions: number().nullable()
|
|
25088
|
+
});
|
|
25089
|
+
var SuggestedNodeShareSchema = object({
|
|
25090
|
+
nodeId: string(),
|
|
25091
|
+
/** Share of the cluster (0–100, rounded); null = node skipped (offline, unknown hardware). */
|
|
25092
|
+
share: number().nullable(),
|
|
25093
|
+
/** Parallel sessions the node sustains at the suggested models with the headroom kept free. */
|
|
25094
|
+
capacitySessions: number(),
|
|
25095
|
+
devices: array(SuggestedDeviceShareSchema).readonly(),
|
|
25096
|
+
skippedReason: string().nullable()
|
|
25097
|
+
});
|
|
25098
|
+
var SuggestSharesResultSchema = object({
|
|
25099
|
+
headroom: number(),
|
|
25100
|
+
/** Camera detection rate the capacity is counted in (the fastest camera's fps). */
|
|
25101
|
+
cameraFps: number(),
|
|
25102
|
+
/** How many share → model → capacity rounds ran before the shares stopped moving (≤ 3). */
|
|
25103
|
+
iterations: number(),
|
|
25104
|
+
nodes: array(SuggestedNodeShareSchema).readonly()
|
|
25105
|
+
});
|
|
25106
|
+
/**
|
|
25107
|
+
* The wire shapes of the Best settings plan and apply (D745), used by the
|
|
25108
|
+
* `pipeline-orchestrator` cap. The recommender's own types
|
|
25109
|
+
* (`recommend.ts`, `demand.ts`) are the TS face of the same data; these
|
|
25110
|
+
* schemas are what crosses the cap boundary.
|
|
25111
|
+
*/
|
|
25112
|
+
var ModelFormatSchema$1 = _enum(MODEL_FORMATS);
|
|
25113
|
+
var HardwareClassIdSchema = _enum([
|
|
25114
|
+
"intel-core-ultra",
|
|
25115
|
+
"intel-n-series",
|
|
25116
|
+
"apple-m-base",
|
|
25117
|
+
"apple-m-pro",
|
|
25118
|
+
"unknown"
|
|
25119
|
+
]);
|
|
25120
|
+
var AcceleratorKindSchema = _enum([
|
|
25121
|
+
"intel-npu",
|
|
25122
|
+
"intel-igpu",
|
|
25123
|
+
"x86-cpu",
|
|
25124
|
+
"arm-cpu",
|
|
25125
|
+
"apple-ane",
|
|
25126
|
+
"coral"
|
|
25127
|
+
]);
|
|
25128
|
+
var RecommendationReasonSchema = discriminatedUnion("kind", [
|
|
25129
|
+
object({
|
|
25130
|
+
kind: literal("fits"),
|
|
25131
|
+
utilisation: number()
|
|
25132
|
+
}),
|
|
25133
|
+
object({
|
|
25134
|
+
kind: literal("tie-faster"),
|
|
25135
|
+
over: string(),
|
|
25136
|
+
recallDelta: number()
|
|
25137
|
+
}),
|
|
25138
|
+
object({
|
|
25139
|
+
kind: literal("stepped-down"),
|
|
25140
|
+
from: string(),
|
|
25141
|
+
to: string(),
|
|
25142
|
+
limitedBy: string()
|
|
25143
|
+
}),
|
|
25144
|
+
object({
|
|
25145
|
+
kind: literal("no-fit-lightest"),
|
|
25146
|
+
utilisation: number()
|
|
25147
|
+
}),
|
|
25148
|
+
object({
|
|
25149
|
+
kind: literal("derived-latency"),
|
|
25150
|
+
modelId: string()
|
|
25151
|
+
}),
|
|
25152
|
+
object({
|
|
25153
|
+
kind: literal("estimated-load"),
|
|
25154
|
+
observedForMs: number().nullable()
|
|
25155
|
+
}),
|
|
25156
|
+
object({ kind: literal("cpu-device") }),
|
|
25157
|
+
object({ kind: literal("coral-untouched") }),
|
|
25158
|
+
object({ kind: literal("device-disabled") }),
|
|
25159
|
+
object({ kind: literal("device-unavailable") }),
|
|
25160
|
+
object({ kind: literal("node-offline") }),
|
|
25161
|
+
object({ kind: literal("no-hardware-facts") }),
|
|
25162
|
+
object({
|
|
25163
|
+
kind: literal("no-measured-data"),
|
|
25164
|
+
hardwareClass: HardwareClassIdSchema
|
|
25165
|
+
}),
|
|
25166
|
+
object({
|
|
25167
|
+
kind: literal("no-family-build"),
|
|
25168
|
+
format: ModelFormatSchema$1
|
|
25169
|
+
})
|
|
25170
|
+
]);
|
|
25171
|
+
var DeviceDemandSchema = object({
|
|
25172
|
+
nodeId: string(),
|
|
25173
|
+
deviceKey: string(),
|
|
25174
|
+
demandFps: number(),
|
|
25175
|
+
maxCameraFps: number(),
|
|
25176
|
+
alwaysOnCameras: number(),
|
|
25177
|
+
onMotionSessions: number(),
|
|
25178
|
+
onMotionSource: _enum([
|
|
25179
|
+
"observed-24h",
|
|
25180
|
+
"estimate",
|
|
25181
|
+
"none"
|
|
25182
|
+
]),
|
|
25183
|
+
observedForMs: number().nullable(),
|
|
25184
|
+
/** How on-motion sessions were charged: per-device observation, or split by share (D748). */
|
|
25185
|
+
placement: SessionPlacementPolicySchema
|
|
25186
|
+
});
|
|
25187
|
+
var DeviceModelChoiceSchema = object({
|
|
25188
|
+
modelId: string(),
|
|
25189
|
+
p50Ms: number(),
|
|
25190
|
+
fps: number(),
|
|
25191
|
+
measured: boolean(),
|
|
25192
|
+
utilisation: number(),
|
|
25193
|
+
personRecall: number()
|
|
25194
|
+
});
|
|
25195
|
+
var DeviceRecommendationSchema = object({
|
|
25196
|
+
deviceKey: string(),
|
|
25197
|
+
accelerator: AcceleratorKindSchema.nullable(),
|
|
25198
|
+
enabled: boolean(),
|
|
25199
|
+
available: boolean(),
|
|
25200
|
+
demand: DeviceDemandSchema.nullable(),
|
|
25201
|
+
chosen: DeviceModelChoiceSchema.nullable(),
|
|
25202
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
25203
|
+
});
|
|
25204
|
+
var NodeRecommendationSchema = object({
|
|
25205
|
+
nodeId: string(),
|
|
25206
|
+
reachable: boolean(),
|
|
25207
|
+
hardwareClass: HardwareClassIdSchema.nullable(),
|
|
25208
|
+
cpuModel: string().nullable(),
|
|
25209
|
+
skipped: boolean(),
|
|
25210
|
+
acceleratorModelId: string().nullable(),
|
|
25211
|
+
devices: array(DeviceRecommendationSchema).readonly(),
|
|
25212
|
+
reasons: array(RecommendationReasonSchema).readonly()
|
|
25213
|
+
});
|
|
25214
|
+
var ModelPinChangeSchema = object({
|
|
25215
|
+
stepId: string(),
|
|
25216
|
+
before: string().nullable(),
|
|
25217
|
+
after: string().nullable()
|
|
25218
|
+
});
|
|
25219
|
+
var NodeDeviceChangeSchema = object({
|
|
25220
|
+
nodeId: string(),
|
|
25221
|
+
deviceKey: string(),
|
|
25222
|
+
madeExplicit: boolean(),
|
|
25223
|
+
modelPins: array(ModelPinChangeSchema).readonly(),
|
|
25224
|
+
thresholds: array(object({
|
|
25225
|
+
stepId: string(),
|
|
25226
|
+
keys: array(string()).readonly()
|
|
25227
|
+
})).readonly()
|
|
25228
|
+
});
|
|
25229
|
+
var CameraOverrideClearSchema = object({
|
|
25230
|
+
deviceId: number(),
|
|
25231
|
+
source: _enum(["step-override", "wholesale"]),
|
|
25232
|
+
nodeId: string(),
|
|
25233
|
+
deviceKey: string().nullable(),
|
|
25234
|
+
stepId: string(),
|
|
25235
|
+
modelIdBefore: string().nullable(),
|
|
25236
|
+
keys: array(string()).readonly()
|
|
25237
|
+
});
|
|
25238
|
+
var DetectorStoreClearSchema = object({
|
|
25239
|
+
deviceId: number(),
|
|
25240
|
+
keys: array(string()).readonly()
|
|
25241
|
+
});
|
|
25242
|
+
var BestSettingsWarningSchema = discriminatedUnion("kind", [
|
|
25243
|
+
object({
|
|
25244
|
+
kind: literal("detector-store-cluster-wide"),
|
|
25245
|
+
skippedNodes: array(string()).readonly()
|
|
25246
|
+
}),
|
|
25247
|
+
object({
|
|
25248
|
+
kind: literal("detector-store-unreadable"),
|
|
25249
|
+
error: string()
|
|
25250
|
+
}),
|
|
25251
|
+
object({
|
|
25252
|
+
kind: literal("coral-enabled"),
|
|
25253
|
+
nodeId: string(),
|
|
25254
|
+
deviceKey: string()
|
|
25255
|
+
})
|
|
25256
|
+
]);
|
|
25257
|
+
var BestSettingsOptionsSchema = object({
|
|
25258
|
+
/** Share of each accelerator's throughput kept free (0.3–0.5). */
|
|
25259
|
+
headroom: number().min(.3).max(.5).default(.4),
|
|
25260
|
+
/**
|
|
25261
|
+
* What-if shares (D748): plan as if these node/device shares were stored
|
|
25262
|
+
* and the policy were `share`. Absent = the stored shares under the stored
|
|
25263
|
+
* policy. A what-if plan is for display; apply always re-plans from what
|
|
25264
|
+
* is stored.
|
|
25265
|
+
*/
|
|
25266
|
+
shares: LoadBalancingSharesInputSchema.optional()
|
|
25267
|
+
});
|
|
25268
|
+
var BestSettingsPlanSchema = object({
|
|
25269
|
+
/** Hash of everything the apply will write — the apply refuses a different one. */
|
|
25270
|
+
planHash: string(),
|
|
25271
|
+
family: literal("yolov9"),
|
|
25272
|
+
headroom: number(),
|
|
25273
|
+
/** Planned under what-if shares (D748): for display — apply refuses it as stale. */
|
|
25274
|
+
whatIf: boolean(),
|
|
25275
|
+
generatedAt: number(),
|
|
25276
|
+
nodes: array(NodeRecommendationSchema).readonly(),
|
|
25277
|
+
nodeChanges: array(NodeDeviceChangeSchema).readonly(),
|
|
25278
|
+
cameraClears: array(CameraOverrideClearSchema).readonly(),
|
|
25279
|
+
detectorStoreClears: array(DetectorStoreClearSchema).readonly(),
|
|
25280
|
+
warnings: array(BestSettingsWarningSchema).readonly()
|
|
25281
|
+
});
|
|
25282
|
+
var BestSettingsApplyInputSchema = object({
|
|
25283
|
+
planHash: string(),
|
|
25284
|
+
headroom: number().min(.3).max(.5).default(.4)
|
|
25285
|
+
});
|
|
25286
|
+
var BestSettingsPhaseResultSchema = object({
|
|
25287
|
+
phase: _enum([
|
|
25288
|
+
"download",
|
|
25289
|
+
"camera-overrides",
|
|
25290
|
+
"detector-thresholds",
|
|
25291
|
+
"node-models",
|
|
25292
|
+
"redispatch"
|
|
25293
|
+
]),
|
|
25294
|
+
ok: boolean(),
|
|
25295
|
+
detail: string()
|
|
25296
|
+
});
|
|
25297
|
+
var BestSettingsApplyResultSchema = discriminatedUnion("kind", [
|
|
25298
|
+
object({
|
|
25299
|
+
kind: literal("applied"),
|
|
25300
|
+
planHash: string(),
|
|
25301
|
+
phases: array(BestSettingsPhaseResultSchema).readonly()
|
|
25302
|
+
}),
|
|
25303
|
+
object({
|
|
25304
|
+
kind: literal("aborted"),
|
|
25305
|
+
phases: array(BestSettingsPhaseResultSchema).readonly()
|
|
25306
|
+
}),
|
|
25307
|
+
object({
|
|
25308
|
+
kind: literal("stale"),
|
|
25309
|
+
plan: BestSettingsPlanSchema
|
|
25310
|
+
}),
|
|
25311
|
+
object({
|
|
25312
|
+
kind: literal("refused"),
|
|
25313
|
+
reason: _enum(["apply-in-progress"])
|
|
25314
|
+
})
|
|
25315
|
+
]);
|
|
24938
25316
|
object({
|
|
24939
25317
|
activeCameras: number(),
|
|
24940
25318
|
throttledCameras: number(),
|
|
@@ -26343,7 +26721,6 @@ var CameraStepOverridePatchSchema = object({
|
|
|
26343
26721
|
settings: record(string(), unknown()).readonly().optional()
|
|
26344
26722
|
});
|
|
26345
26723
|
var CameraPipelineSettingsSchema = object({
|
|
26346
|
-
pinnedAgentNodeId: string().optional(),
|
|
26347
26724
|
stepToggles: record(string(), boolean()).optional(),
|
|
26348
26725
|
stepOverridesByDevice: record(string(), record(string(), record(string(), CameraStepOverridePatchSchema))).optional(),
|
|
26349
26726
|
pipelineByAgent: record(string(), CameraPipelineForAgentSchema).optional()
|
|
@@ -26991,18 +27368,6 @@ var pipelineOrchestratorCapability = {
|
|
|
26991
27368
|
auth: "admin"
|
|
26992
27369
|
}),
|
|
26993
27370
|
/**
|
|
26994
|
-
* Set a node's detection WEIGHT (relative share for the quota balancer).
|
|
26995
|
-
* `null` clears it (back to the implicit weight 1). Unpinned cameras
|
|
26996
|
-
* distribute proportionally to weight; `maxCameras` still clamps on top.
|
|
26997
|
-
*/
|
|
26998
|
-
setAgentDetectWeight: method(object({
|
|
26999
|
-
agentNodeId: string(),
|
|
27000
|
-
detectWeight: number().positive().nullable()
|
|
27001
|
-
}), object({ success: literal(true) }), {
|
|
27002
|
-
kind: "mutation",
|
|
27003
|
-
auth: "admin"
|
|
27004
|
-
}),
|
|
27005
|
-
/**
|
|
27006
27371
|
* Set one node's placement CAPABILITIES — the per-node record that
|
|
27007
27372
|
* replaced the four flat orchestrator lists (`enabledNodes`,
|
|
27008
27373
|
* `enabledDecoderNodes`, `enabledAudioNodes`, `remoteSourcingNodes`).
|
|
@@ -27121,6 +27486,54 @@ var pipelineOrchestratorCapability = {
|
|
|
27121
27486
|
kind: "mutation",
|
|
27122
27487
|
auth: "admin"
|
|
27123
27488
|
}),
|
|
27489
|
+
/**
|
|
27490
|
+
* Best settings (D745) — DRY RUN. Recommends ONE detector family (YOLOv9)
|
|
27491
|
+
* for the whole cluster from each node's static hardware class, the
|
|
27492
|
+
* measured latency table and the detection demand, and lists every write
|
|
27493
|
+
* the apply would make: the node model pins set, the enrichment pins and
|
|
27494
|
+
* model-bound thresholds removed (node level and per camera), and the
|
|
27495
|
+
* per-camera detector thresholds cleared. Writes nothing. Step
|
|
27496
|
+
* enablement, device enablement, pins, recording, notifications, zones and
|
|
27497
|
+
* rules are never part of it. `planHash` names exactly this diff.
|
|
27498
|
+
*/
|
|
27499
|
+
planBestSettings: method(BestSettingsOptionsSchema, BestSettingsPlanSchema, { auth: "admin" }),
|
|
27500
|
+
/**
|
|
27501
|
+
* Best settings (D745) — APPLY the plan the operator confirmed. Recomputes
|
|
27502
|
+
* the plan and refuses (`stale`, with the fresh plan) when its hash is not
|
|
27503
|
+
* `planHash`, so what is written is exactly the confirmed diff. Offline
|
|
27504
|
+
* nodes are skipped; there is no undo.
|
|
27505
|
+
*/
|
|
27506
|
+
applyBestSettings: method(BestSettingsApplyInputSchema, BestSettingsApplyResultSchema, {
|
|
27507
|
+
kind: "mutation",
|
|
27508
|
+
auth: "admin"
|
|
27509
|
+
}),
|
|
27510
|
+
/**
|
|
27511
|
+
* Load balancing (D748) — write node shares (`detectWeight`) and per-node
|
|
27512
|
+
* device shares (inference-device `weight`) in ONE atomic read-modify-write
|
|
27513
|
+
* of the agent settings — the only writer of node shares (the per-node
|
|
27514
|
+
* `setAgentDetectWeight` it replaced raced its siblings, one RMW each).
|
|
27515
|
+
* A node or device left out keeps its weight. Never changes the
|
|
27516
|
+
* `sessionPlacement` policy.
|
|
27517
|
+
*/
|
|
27518
|
+
setLoadBalancingShares: method(LoadBalancingSharesInputSchema, object({ success: literal(true) }), {
|
|
27519
|
+
kind: "mutation",
|
|
27520
|
+
auth: "admin"
|
|
27521
|
+
}),
|
|
27522
|
+
/**
|
|
27523
|
+
* Load balancing (D748) — what placement actually did: per node and
|
|
27524
|
+
* device, sessions now, sessions started and the time-weighted share of
|
|
27525
|
+
* PARALLEL sessions over `windowMs`; the cluster's parallel peak; yields
|
|
27526
|
+
* a share election made; and every device pin the session path cannot
|
|
27527
|
+
* honour. In memory on the hub — a restart starts a new window.
|
|
27528
|
+
*/
|
|
27529
|
+
getPlacementStats: method(PlacementStatsInputSchema, PlacementStatsSchema, { auth: "admin" }),
|
|
27530
|
+
/**
|
|
27531
|
+
* Load balancing (D748) — shares proportional to each node's measured
|
|
27532
|
+
* capacity at the models Best settings would pick, iterated (≤ 3 rounds)
|
|
27533
|
+
* until the shares stop moving; per device, an ADVISORY session cap.
|
|
27534
|
+
* Writes nothing.
|
|
27535
|
+
*/
|
|
27536
|
+
suggestShares: method(SuggestSharesInputSchema, SuggestSharesResultSchema, { auth: "admin" }),
|
|
27124
27537
|
/** Read one camera's settings. Null when never touched (inherits agent defaults fully). */
|
|
27125
27538
|
getCameraSettings: method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()),
|
|
27126
27539
|
/** Set or clear the 3-state toggle for one (camera, addonId). Pass `enabled: null` to clear and revert to agent default. */
|
|
@@ -42922,6 +43335,12 @@ Object.freeze({
|
|
|
42922
43335
|
addonId: null,
|
|
42923
43336
|
access: "delete"
|
|
42924
43337
|
},
|
|
43338
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": {
|
|
43339
|
+
capName: "pipeline-executor",
|
|
43340
|
+
capScope: "system",
|
|
43341
|
+
addonId: null,
|
|
43342
|
+
access: "delete"
|
|
43343
|
+
},
|
|
42925
43344
|
"pipelineExecutor.deleteModel": {
|
|
42926
43345
|
capName: "pipeline-executor",
|
|
42927
43346
|
capScope: "system",
|
|
@@ -43138,6 +43557,12 @@ Object.freeze({
|
|
|
43138
43557
|
addonId: null,
|
|
43139
43558
|
access: "view"
|
|
43140
43559
|
},
|
|
43560
|
+
"pipelineOrchestrator.applyBestSettings": {
|
|
43561
|
+
capName: "pipeline-orchestrator",
|
|
43562
|
+
capScope: "system",
|
|
43563
|
+
addonId: null,
|
|
43564
|
+
access: "create"
|
|
43565
|
+
},
|
|
43141
43566
|
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
43142
43567
|
capName: "pipeline-orchestrator",
|
|
43143
43568
|
capScope: "system",
|
|
@@ -43288,6 +43713,12 @@ Object.freeze({
|
|
|
43288
43713
|
addonId: null,
|
|
43289
43714
|
access: "view"
|
|
43290
43715
|
},
|
|
43716
|
+
"pipelineOrchestrator.getPlacementStats": {
|
|
43717
|
+
capName: "pipeline-orchestrator",
|
|
43718
|
+
capScope: "system",
|
|
43719
|
+
addonId: null,
|
|
43720
|
+
access: "view"
|
|
43721
|
+
},
|
|
43291
43722
|
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
43292
43723
|
capName: "pipeline-orchestrator",
|
|
43293
43724
|
capScope: "system",
|
|
@@ -43312,6 +43743,12 @@ Object.freeze({
|
|
|
43312
43743
|
addonId: null,
|
|
43313
43744
|
access: "create"
|
|
43314
43745
|
},
|
|
43746
|
+
"pipelineOrchestrator.planBestSettings": {
|
|
43747
|
+
capName: "pipeline-orchestrator",
|
|
43748
|
+
capScope: "system",
|
|
43749
|
+
addonId: null,
|
|
43750
|
+
access: "view"
|
|
43751
|
+
},
|
|
43315
43752
|
"pipelineOrchestrator.rebalance": {
|
|
43316
43753
|
capName: "pipeline-orchestrator",
|
|
43317
43754
|
capScope: "system",
|
|
@@ -43372,12 +43809,6 @@ Object.freeze({
|
|
|
43372
43809
|
addonId: null,
|
|
43373
43810
|
access: "create"
|
|
43374
43811
|
},
|
|
43375
|
-
"pipelineOrchestrator.setAgentDetectWeight": {
|
|
43376
|
-
capName: "pipeline-orchestrator",
|
|
43377
|
-
capScope: "system",
|
|
43378
|
-
addonId: null,
|
|
43379
|
-
access: "create"
|
|
43380
|
-
},
|
|
43381
43812
|
"pipelineOrchestrator.setAgentInferenceDevices": {
|
|
43382
43813
|
capName: "pipeline-orchestrator",
|
|
43383
43814
|
capScope: "system",
|
|
@@ -43432,12 +43863,24 @@ Object.freeze({
|
|
|
43432
43863
|
addonId: null,
|
|
43433
43864
|
access: "create"
|
|
43434
43865
|
},
|
|
43866
|
+
"pipelineOrchestrator.setLoadBalancingShares": {
|
|
43867
|
+
capName: "pipeline-orchestrator",
|
|
43868
|
+
capScope: "system",
|
|
43869
|
+
addonId: null,
|
|
43870
|
+
access: "create"
|
|
43871
|
+
},
|
|
43435
43872
|
"pipelineOrchestrator.setPipelineDevicePin": {
|
|
43436
43873
|
capName: "pipeline-orchestrator",
|
|
43437
43874
|
capScope: "system",
|
|
43438
43875
|
addonId: null,
|
|
43439
43876
|
access: "create"
|
|
43440
43877
|
},
|
|
43878
|
+
"pipelineOrchestrator.suggestShares": {
|
|
43879
|
+
capName: "pipeline-orchestrator",
|
|
43880
|
+
capScope: "system",
|
|
43881
|
+
addonId: null,
|
|
43882
|
+
access: "view"
|
|
43883
|
+
},
|
|
43441
43884
|
"pipelineOrchestrator.unassignAudio": {
|
|
43442
43885
|
capName: "pipeline-orchestrator",
|
|
43443
43886
|
capScope: "system",
|
|
@@ -48762,6 +49205,11 @@ Object.freeze({
|
|
|
48762
49205
|
form: "single",
|
|
48763
49206
|
optional: false
|
|
48764
49207
|
}],
|
|
49208
|
+
"pipelineExecutor.clearModelBoundDeviceOverrides": [{
|
|
49209
|
+
name: "deviceIds",
|
|
49210
|
+
form: "array",
|
|
49211
|
+
optional: true
|
|
49212
|
+
}],
|
|
48765
49213
|
"pipelineExecutor.runPipeline": [{
|
|
48766
49214
|
name: "deviceId",
|
|
48767
49215
|
form: "single",
|
|
@@ -50656,6 +51104,891 @@ var REMOVED_MODEL_IDS = {
|
|
|
50656
51104
|
function isRemovedModelId(stepId, modelId) {
|
|
50657
51105
|
return REMOVED_MODEL_IDS[stepId]?.includes(modelId) ?? false;
|
|
50658
51106
|
}
|
|
51107
|
+
/** Suffix of the sibling key that names the model a stored value was made for. */
|
|
51108
|
+
var MODEL_BINDING_SUFFIX = "ModelId";
|
|
51109
|
+
/** `minConfidencePerson` → `minConfidencePersonModelId`. */
|
|
51110
|
+
function modelBindingKey(key) {
|
|
51111
|
+
return `${key}${MODEL_BINDING_SUFFIX}`;
|
|
51112
|
+
}
|
|
51113
|
+
/**
|
|
51114
|
+
* The keys of every `bindsToModel` field of a step's config schema, groups
|
|
51115
|
+
* flattened (D738). One definition for the runner (which applies the binding)
|
|
51116
|
+
* and the orchestrator's Best settings (which clears those overrides, D745).
|
|
51117
|
+
*/
|
|
51118
|
+
function collectModelBoundKeys(fields) {
|
|
51119
|
+
return fields.flatMap((field) => {
|
|
51120
|
+
if (field.type === "group") return collectModelBoundKeys(field.fields);
|
|
51121
|
+
return "key" in field && "bindsToModel" in field && field.bindsToModel === true ? [field.key] : [];
|
|
51122
|
+
});
|
|
51123
|
+
}
|
|
51124
|
+
var APPLE_CHIP = /^Apple M(\d+)(?:\s+(Pro|Max|Ultra))?\b/i;
|
|
51125
|
+
var INTEL_CORE_ULTRA = /Core\(TM\)\s+Ultra|Core Ultra/i;
|
|
51126
|
+
var INTEL_N_SERIES = /Intel\(R\)\s+N\d{2,3}\b|\bIntel\s+N\d{2,3}\b/i;
|
|
51127
|
+
function parseAppleChip(cpuModel) {
|
|
51128
|
+
const match = APPLE_CHIP.exec(cpuModel.trim());
|
|
51129
|
+
if (match === null) return null;
|
|
51130
|
+
const generation = Number(match[1]);
|
|
51131
|
+
const variantRaw = (match[2] ?? "base").toLowerCase();
|
|
51132
|
+
return {
|
|
51133
|
+
generation,
|
|
51134
|
+
variant: variantRaw === "pro" || variantRaw === "max" || variantRaw === "ultra" ? variantRaw : "base"
|
|
51135
|
+
};
|
|
51136
|
+
}
|
|
51137
|
+
/**
|
|
51138
|
+
* Classify a node. Apple base chips (M1–M4) map to the measured M2 column, the
|
|
51139
|
+
* Pro/Max/Ultra chips to the measured M3 Pro column — the M2 was measured at
|
|
51140
|
+
* 1.0–1.2× the M3 Pro on the Neural Engine, so the generation inside a variant
|
|
51141
|
+
* matters less than the column's own error.
|
|
51142
|
+
*/
|
|
51143
|
+
function classifyInferenceHardware(hw) {
|
|
51144
|
+
const cpuModel = hw.cpuModel;
|
|
51145
|
+
const apple = parseAppleChip(cpuModel);
|
|
51146
|
+
if (apple !== null) return {
|
|
51147
|
+
hardwareClass: apple.variant === "base" ? "apple-m-base" : "apple-m-pro",
|
|
51148
|
+
cpuModel,
|
|
51149
|
+
apple
|
|
51150
|
+
};
|
|
51151
|
+
if (INTEL_CORE_ULTRA.test(cpuModel)) return {
|
|
51152
|
+
hardwareClass: "intel-core-ultra",
|
|
51153
|
+
cpuModel
|
|
51154
|
+
};
|
|
51155
|
+
if (INTEL_N_SERIES.test(cpuModel)) return {
|
|
51156
|
+
hardwareClass: "intel-n-series",
|
|
51157
|
+
cpuModel
|
|
51158
|
+
};
|
|
51159
|
+
return {
|
|
51160
|
+
hardwareClass: "unknown",
|
|
51161
|
+
cpuModel
|
|
51162
|
+
};
|
|
51163
|
+
}
|
|
51164
|
+
/**
|
|
51165
|
+
* The accelerator kind of one of a node's device keys (`openvino:npu`,
|
|
51166
|
+
* `openvino:gpu`, `coreml:all`, `edgetpu:usb`, `cpu`, `onnx:cpu`, …). `null`
|
|
51167
|
+
* for a key this table has no column for.
|
|
51168
|
+
*/
|
|
51169
|
+
function acceleratorKindOf(deviceKey, hw) {
|
|
51170
|
+
const [backend = "", device = ""] = deviceKey.split(":");
|
|
51171
|
+
if (backend === "edgetpu") return "coral";
|
|
51172
|
+
if (backend === "coreml") return device === "cpu" ? "arm-cpu" : "apple-ane";
|
|
51173
|
+
if (backend === "openvino" && device === "npu") return "intel-npu";
|
|
51174
|
+
if (backend === "openvino" && device === "gpu") return "intel-igpu";
|
|
51175
|
+
if (backend === "cpu" || device === "cpu") return hw.arch === "arm64" ? "arm-cpu" : "x86-cpu";
|
|
51176
|
+
return null;
|
|
51177
|
+
}
|
|
51178
|
+
var DETECTOR_PERF = {
|
|
51179
|
+
"intel-core-ultra": {
|
|
51180
|
+
"intel-npu": {
|
|
51181
|
+
"yolov9t-320": {
|
|
51182
|
+
p50Ms: 2.9,
|
|
51183
|
+
fps: 383,
|
|
51184
|
+
measured: true
|
|
51185
|
+
},
|
|
51186
|
+
"yolov9t-320-int8": {
|
|
51187
|
+
p50Ms: 3.8,
|
|
51188
|
+
fps: 333,
|
|
51189
|
+
measured: true
|
|
51190
|
+
},
|
|
51191
|
+
"yolov9t-640": {
|
|
51192
|
+
p50Ms: 7.4,
|
|
51193
|
+
fps: 139,
|
|
51194
|
+
measured: true
|
|
51195
|
+
},
|
|
51196
|
+
"yolov9t-640-int8": {
|
|
51197
|
+
p50Ms: 7.5,
|
|
51198
|
+
fps: 144,
|
|
51199
|
+
measured: true
|
|
51200
|
+
},
|
|
51201
|
+
"yolov9s-320": {
|
|
51202
|
+
p50Ms: 4.4,
|
|
51203
|
+
fps: 241,
|
|
51204
|
+
measured: true
|
|
51205
|
+
},
|
|
51206
|
+
"yolov9s-320-int8": {
|
|
51207
|
+
p50Ms: 4.1,
|
|
51208
|
+
fps: 264,
|
|
51209
|
+
measured: true
|
|
51210
|
+
},
|
|
51211
|
+
"yolov9s-640": {
|
|
51212
|
+
p50Ms: 12.7,
|
|
51213
|
+
fps: 82,
|
|
51214
|
+
measured: true
|
|
51215
|
+
},
|
|
51216
|
+
"yolov9s-640-int8": {
|
|
51217
|
+
p50Ms: 10.7,
|
|
51218
|
+
fps: 99,
|
|
51219
|
+
measured: true
|
|
51220
|
+
},
|
|
51221
|
+
"yolov9m-320": {
|
|
51222
|
+
p50Ms: 8.4,
|
|
51223
|
+
fps: 123,
|
|
51224
|
+
measured: true
|
|
51225
|
+
},
|
|
51226
|
+
"yolov9m-320-int8": {
|
|
51227
|
+
p50Ms: 6.5,
|
|
51228
|
+
fps: 165,
|
|
51229
|
+
measured: true
|
|
51230
|
+
},
|
|
51231
|
+
"yolov9m-640": {
|
|
51232
|
+
p50Ms: 29.5,
|
|
51233
|
+
fps: 32,
|
|
51234
|
+
measured: true
|
|
51235
|
+
},
|
|
51236
|
+
"yolov9m-640-int8": {
|
|
51237
|
+
p50Ms: 19.5,
|
|
51238
|
+
fps: 52,
|
|
51239
|
+
measured: true
|
|
51240
|
+
}
|
|
51241
|
+
},
|
|
51242
|
+
"intel-igpu": {
|
|
51243
|
+
"yolov9t-320": {
|
|
51244
|
+
p50Ms: 2.6,
|
|
51245
|
+
fps: 405,
|
|
51246
|
+
measured: true
|
|
51247
|
+
},
|
|
51248
|
+
"yolov9t-320-int8": {
|
|
51249
|
+
p50Ms: 2.8,
|
|
51250
|
+
fps: 324,
|
|
51251
|
+
measured: true
|
|
51252
|
+
},
|
|
51253
|
+
"yolov9t-640": {
|
|
51254
|
+
p50Ms: 6.5,
|
|
51255
|
+
fps: 157,
|
|
51256
|
+
measured: true
|
|
51257
|
+
},
|
|
51258
|
+
"yolov9t-640-int8": {
|
|
51259
|
+
p50Ms: 5.6,
|
|
51260
|
+
fps: 185,
|
|
51261
|
+
measured: true
|
|
51262
|
+
},
|
|
51263
|
+
"yolov9s-320": {
|
|
51264
|
+
p50Ms: 4.6,
|
|
51265
|
+
fps: 200,
|
|
51266
|
+
measured: true
|
|
51267
|
+
},
|
|
51268
|
+
"yolov9s-320-int8": {
|
|
51269
|
+
p50Ms: 4.1,
|
|
51270
|
+
fps: 239,
|
|
51271
|
+
measured: true
|
|
51272
|
+
},
|
|
51273
|
+
"yolov9s-640": {
|
|
51274
|
+
p50Ms: 15.2,
|
|
51275
|
+
fps: 69,
|
|
51276
|
+
measured: true
|
|
51277
|
+
},
|
|
51278
|
+
"yolov9s-640-int8": {
|
|
51279
|
+
p50Ms: 10.5,
|
|
51280
|
+
fps: 99,
|
|
51281
|
+
measured: true
|
|
51282
|
+
},
|
|
51283
|
+
"yolov9m-320": {
|
|
51284
|
+
p50Ms: 11.4,
|
|
51285
|
+
fps: 88,
|
|
51286
|
+
measured: true
|
|
51287
|
+
},
|
|
51288
|
+
"yolov9m-320-int8": {
|
|
51289
|
+
p50Ms: 7,
|
|
51290
|
+
fps: 127,
|
|
51291
|
+
measured: true
|
|
51292
|
+
},
|
|
51293
|
+
"yolov9m-640": {
|
|
51294
|
+
p50Ms: 40,
|
|
51295
|
+
fps: 25,
|
|
51296
|
+
measured: true
|
|
51297
|
+
},
|
|
51298
|
+
"yolov9m-640-int8": {
|
|
51299
|
+
p50Ms: 23.5,
|
|
51300
|
+
fps: 44,
|
|
51301
|
+
measured: true
|
|
51302
|
+
}
|
|
51303
|
+
},
|
|
51304
|
+
"x86-cpu": {
|
|
51305
|
+
"yolov9t-320": {
|
|
51306
|
+
p50Ms: 6.9,
|
|
51307
|
+
fps: 145,
|
|
51308
|
+
measured: true
|
|
51309
|
+
},
|
|
51310
|
+
"yolov9t-320-int8": {
|
|
51311
|
+
p50Ms: 2.7,
|
|
51312
|
+
fps: 405,
|
|
51313
|
+
measured: true
|
|
51314
|
+
},
|
|
51315
|
+
"yolov9t-640": {
|
|
51316
|
+
p50Ms: 23,
|
|
51317
|
+
fps: 44,
|
|
51318
|
+
measured: true
|
|
51319
|
+
},
|
|
51320
|
+
"yolov9t-640-int8": {
|
|
51321
|
+
p50Ms: 8.8,
|
|
51322
|
+
fps: 124,
|
|
51323
|
+
measured: true
|
|
51324
|
+
},
|
|
51325
|
+
"yolov9s-320": {
|
|
51326
|
+
p50Ms: 15.8,
|
|
51327
|
+
fps: 63,
|
|
51328
|
+
measured: true
|
|
51329
|
+
},
|
|
51330
|
+
"yolov9s-320-int8": {
|
|
51331
|
+
p50Ms: 6.5,
|
|
51332
|
+
fps: 180,
|
|
51333
|
+
measured: true
|
|
51334
|
+
},
|
|
51335
|
+
"yolov9s-640": {
|
|
51336
|
+
p50Ms: 58.2,
|
|
51337
|
+
fps: 17,
|
|
51338
|
+
measured: true
|
|
51339
|
+
},
|
|
51340
|
+
"yolov9s-640-int8": {
|
|
51341
|
+
p50Ms: 20.1,
|
|
51342
|
+
fps: 52,
|
|
51343
|
+
measured: true
|
|
51344
|
+
},
|
|
51345
|
+
"yolov9m-320": {
|
|
51346
|
+
p50Ms: 40,
|
|
51347
|
+
fps: 25,
|
|
51348
|
+
measured: true
|
|
51349
|
+
},
|
|
51350
|
+
"yolov9m-320-int8": {
|
|
51351
|
+
p50Ms: 14.2,
|
|
51352
|
+
fps: 74,
|
|
51353
|
+
measured: true
|
|
51354
|
+
},
|
|
51355
|
+
"yolov9m-640": {
|
|
51356
|
+
p50Ms: 155.8,
|
|
51357
|
+
fps: 6,
|
|
51358
|
+
measured: true
|
|
51359
|
+
},
|
|
51360
|
+
"yolov9m-640-int8": {
|
|
51361
|
+
p50Ms: 52.4,
|
|
51362
|
+
fps: 19,
|
|
51363
|
+
measured: true
|
|
51364
|
+
}
|
|
51365
|
+
}
|
|
51366
|
+
},
|
|
51367
|
+
"intel-n-series": {
|
|
51368
|
+
"intel-igpu": {
|
|
51369
|
+
"yolov9t-320": {
|
|
51370
|
+
p50Ms: 15,
|
|
51371
|
+
fps: 48,
|
|
51372
|
+
measured: true
|
|
51373
|
+
},
|
|
51374
|
+
"yolov9t-320-int8": {
|
|
51375
|
+
p50Ms: 14.1,
|
|
51376
|
+
fps: 67,
|
|
51377
|
+
measured: true
|
|
51378
|
+
},
|
|
51379
|
+
"yolov9t-640": {
|
|
51380
|
+
p50Ms: 38.5,
|
|
51381
|
+
fps: 26,
|
|
51382
|
+
measured: true
|
|
51383
|
+
},
|
|
51384
|
+
"yolov9t-640-int8": {
|
|
51385
|
+
p50Ms: 33.1,
|
|
51386
|
+
fps: 33,
|
|
51387
|
+
measured: true
|
|
51388
|
+
},
|
|
51389
|
+
"yolov9s-320": {
|
|
51390
|
+
p50Ms: 41.4,
|
|
51391
|
+
fps: 24,
|
|
51392
|
+
measured: true
|
|
51393
|
+
},
|
|
51394
|
+
"yolov9s-320-int8": {
|
|
51395
|
+
p50Ms: 23.3,
|
|
51396
|
+
fps: 34,
|
|
51397
|
+
measured: true
|
|
51398
|
+
},
|
|
51399
|
+
"yolov9s-640": {
|
|
51400
|
+
p50Ms: 127.4,
|
|
51401
|
+
fps: 8,
|
|
51402
|
+
measured: true
|
|
51403
|
+
},
|
|
51404
|
+
"yolov9s-640-int8": {
|
|
51405
|
+
p50Ms: 92.9,
|
|
51406
|
+
fps: 11,
|
|
51407
|
+
measured: true
|
|
51408
|
+
},
|
|
51409
|
+
"yolov9m-320": {
|
|
51410
|
+
p50Ms: 63.5,
|
|
51411
|
+
fps: 16,
|
|
51412
|
+
measured: true
|
|
51413
|
+
},
|
|
51414
|
+
"yolov9m-320-int8": {
|
|
51415
|
+
p50Ms: 39.4,
|
|
51416
|
+
fps: 26,
|
|
51417
|
+
measured: true
|
|
51418
|
+
},
|
|
51419
|
+
"yolov9m-640": {
|
|
51420
|
+
p50Ms: 215.5,
|
|
51421
|
+
fps: 5,
|
|
51422
|
+
measured: true
|
|
51423
|
+
},
|
|
51424
|
+
"yolov9m-640-int8": {
|
|
51425
|
+
p50Ms: 139.1,
|
|
51426
|
+
fps: 7,
|
|
51427
|
+
measured: true
|
|
51428
|
+
}
|
|
51429
|
+
},
|
|
51430
|
+
"x86-cpu": {
|
|
51431
|
+
"yolov9t-320": {
|
|
51432
|
+
p50Ms: 86.2,
|
|
51433
|
+
fps: 12,
|
|
51434
|
+
measured: true
|
|
51435
|
+
},
|
|
51436
|
+
"yolov9t-320-int8": {
|
|
51437
|
+
p50Ms: 52.7,
|
|
51438
|
+
fps: 20,
|
|
51439
|
+
measured: true
|
|
51440
|
+
},
|
|
51441
|
+
"yolov9t-640": {
|
|
51442
|
+
p50Ms: 322.1,
|
|
51443
|
+
fps: 3,
|
|
51444
|
+
measured: true
|
|
51445
|
+
},
|
|
51446
|
+
"yolov9t-640-int8": {
|
|
51447
|
+
p50Ms: 205.6,
|
|
51448
|
+
fps: 5,
|
|
51449
|
+
measured: true
|
|
51450
|
+
},
|
|
51451
|
+
"yolov9s-320": {
|
|
51452
|
+
p50Ms: 164.4,
|
|
51453
|
+
fps: 6,
|
|
51454
|
+
measured: true
|
|
51455
|
+
},
|
|
51456
|
+
"yolov9s-320-int8": {
|
|
51457
|
+
p50Ms: 86.9,
|
|
51458
|
+
fps: 12,
|
|
51459
|
+
measured: true
|
|
51460
|
+
},
|
|
51461
|
+
"yolov9s-640": {
|
|
51462
|
+
p50Ms: 341.3,
|
|
51463
|
+
fps: 3,
|
|
51464
|
+
measured: true
|
|
51465
|
+
},
|
|
51466
|
+
"yolov9s-640-int8": {
|
|
51467
|
+
p50Ms: 185.3,
|
|
51468
|
+
fps: 5,
|
|
51469
|
+
measured: true
|
|
51470
|
+
},
|
|
51471
|
+
"yolov9m-320": {
|
|
51472
|
+
p50Ms: 166.8,
|
|
51473
|
+
fps: 6,
|
|
51474
|
+
measured: true
|
|
51475
|
+
},
|
|
51476
|
+
"yolov9m-320-int8": {
|
|
51477
|
+
p50Ms: 226.3,
|
|
51478
|
+
fps: 4,
|
|
51479
|
+
measured: true
|
|
51480
|
+
},
|
|
51481
|
+
"yolov9m-640": {
|
|
51482
|
+
p50Ms: 663.5,
|
|
51483
|
+
fps: 2,
|
|
51484
|
+
measured: true
|
|
51485
|
+
},
|
|
51486
|
+
"yolov9m-640-int8": {
|
|
51487
|
+
p50Ms: 324.4,
|
|
51488
|
+
fps: 3,
|
|
51489
|
+
measured: true
|
|
51490
|
+
}
|
|
51491
|
+
}
|
|
51492
|
+
},
|
|
51493
|
+
"apple-m-pro": { "apple-ane": {
|
|
51494
|
+
"yolov9t-320": {
|
|
51495
|
+
p50Ms: 1.1,
|
|
51496
|
+
fps: 952,
|
|
51497
|
+
measured: true
|
|
51498
|
+
},
|
|
51499
|
+
"yolov9t-640": {
|
|
51500
|
+
p50Ms: 3,
|
|
51501
|
+
fps: 333,
|
|
51502
|
+
measured: true
|
|
51503
|
+
},
|
|
51504
|
+
"yolov9s-320": {
|
|
51505
|
+
p50Ms: 1.5,
|
|
51506
|
+
fps: 667,
|
|
51507
|
+
measured: true
|
|
51508
|
+
},
|
|
51509
|
+
"yolov9s-640": {
|
|
51510
|
+
p50Ms: 5.1,
|
|
51511
|
+
fps: 196,
|
|
51512
|
+
measured: true
|
|
51513
|
+
},
|
|
51514
|
+
"yolov9m-320": {
|
|
51515
|
+
p50Ms: 3.2,
|
|
51516
|
+
fps: 312,
|
|
51517
|
+
measured: true
|
|
51518
|
+
},
|
|
51519
|
+
"yolov9m-640": {
|
|
51520
|
+
p50Ms: 10.2,
|
|
51521
|
+
fps: 98,
|
|
51522
|
+
measured: true
|
|
51523
|
+
}
|
|
51524
|
+
} },
|
|
51525
|
+
"apple-m-base": { "apple-ane": {
|
|
51526
|
+
"yolov9t-320": {
|
|
51527
|
+
p50Ms: 1.32,
|
|
51528
|
+
fps: 758,
|
|
51529
|
+
measured: false
|
|
51530
|
+
},
|
|
51531
|
+
"yolov9t-640": {
|
|
51532
|
+
p50Ms: 3.45,
|
|
51533
|
+
fps: 290,
|
|
51534
|
+
measured: true
|
|
51535
|
+
},
|
|
51536
|
+
"yolov9s-320": {
|
|
51537
|
+
p50Ms: 1.8,
|
|
51538
|
+
fps: 556,
|
|
51539
|
+
measured: false
|
|
51540
|
+
},
|
|
51541
|
+
"yolov9s-640": {
|
|
51542
|
+
p50Ms: 5.46,
|
|
51543
|
+
fps: 183,
|
|
51544
|
+
measured: true
|
|
51545
|
+
},
|
|
51546
|
+
"yolov9m-320": {
|
|
51547
|
+
p50Ms: 3.79,
|
|
51548
|
+
fps: 264,
|
|
51549
|
+
measured: true
|
|
51550
|
+
},
|
|
51551
|
+
"yolov9m-640": {
|
|
51552
|
+
p50Ms: 10.34,
|
|
51553
|
+
fps: 97,
|
|
51554
|
+
measured: true
|
|
51555
|
+
}
|
|
51556
|
+
} }
|
|
51557
|
+
};
|
|
51558
|
+
var DETECTOR_ACCURACY = {
|
|
51559
|
+
"yolov9m-320": {
|
|
51560
|
+
personPrecision: .979,
|
|
51561
|
+
personRecall: .649
|
|
51562
|
+
},
|
|
51563
|
+
"yolov9m-320-int8": {
|
|
51564
|
+
personPrecision: .974,
|
|
51565
|
+
personRecall: .684
|
|
51566
|
+
},
|
|
51567
|
+
"yolov9m-640": {
|
|
51568
|
+
personPrecision: .976,
|
|
51569
|
+
personRecall: .854
|
|
51570
|
+
},
|
|
51571
|
+
"yolov9m-640-int8": {
|
|
51572
|
+
personPrecision: .976,
|
|
51573
|
+
personRecall: .847
|
|
51574
|
+
},
|
|
51575
|
+
"yolov9s-320": {
|
|
51576
|
+
personPrecision: .958,
|
|
51577
|
+
personRecall: .479
|
|
51578
|
+
},
|
|
51579
|
+
"yolov9s-320-int8": {
|
|
51580
|
+
personPrecision: .969,
|
|
51581
|
+
personRecall: .438
|
|
51582
|
+
},
|
|
51583
|
+
"yolov9s-640": {
|
|
51584
|
+
personPrecision: .986,
|
|
51585
|
+
personRecall: .753
|
|
51586
|
+
},
|
|
51587
|
+
"yolov9s-640-int8": {
|
|
51588
|
+
personPrecision: .963,
|
|
51589
|
+
personRecall: .823
|
|
51590
|
+
},
|
|
51591
|
+
"yolov9t-320": {
|
|
51592
|
+
personPrecision: .946,
|
|
51593
|
+
personRecall: .368
|
|
51594
|
+
},
|
|
51595
|
+
"yolov9t-320-int8": {
|
|
51596
|
+
personPrecision: .943,
|
|
51597
|
+
personRecall: .347
|
|
51598
|
+
},
|
|
51599
|
+
"yolov9t-640": {
|
|
51600
|
+
personPrecision: .979,
|
|
51601
|
+
personRecall: .646
|
|
51602
|
+
},
|
|
51603
|
+
"yolov9t-640-int8": {
|
|
51604
|
+
personPrecision: .979,
|
|
51605
|
+
personRecall: .646
|
|
51606
|
+
}
|
|
51607
|
+
};
|
|
51608
|
+
var DEFAULT_DEMAND_OPTIONS = {
|
|
51609
|
+
onMotionConcurrency: .25,
|
|
51610
|
+
onMotionMinConcurrent: 2
|
|
51611
|
+
};
|
|
51612
|
+
function deviceId(nodeId, deviceKey) {
|
|
51613
|
+
return `${nodeId}\u0000${deviceKey}`;
|
|
51614
|
+
}
|
|
51615
|
+
function charges(load) {
|
|
51616
|
+
return load.mode !== "disabled" && load.runsPipelineRoot && load.fps > 0;
|
|
51617
|
+
}
|
|
51618
|
+
/** The highest `count` rates, summed. */
|
|
51619
|
+
function topRatesSum(rates, count) {
|
|
51620
|
+
return rates.toSorted((a, b) => b - a).slice(0, Math.max(0, count)).reduce((sum, r) => sum + r, 0);
|
|
51621
|
+
}
|
|
51622
|
+
/** Concurrent on-motion sessions assumed cluster-wide without an observation. */
|
|
51623
|
+
function estimatedConcurrentSessions(cameraCount, options) {
|
|
51624
|
+
if (cameraCount <= 0) return 0;
|
|
51625
|
+
const share = Math.ceil(options.onMotionConcurrency * cameraCount);
|
|
51626
|
+
return Math.min(cameraCount, Math.max(share, Math.min(cameraCount, options.onMotionMinConcurrent)));
|
|
51627
|
+
}
|
|
51628
|
+
/**
|
|
51629
|
+
* Demand on every eligible device of the cluster.
|
|
51630
|
+
*/
|
|
51631
|
+
function estimateDeviceDemand(input) {
|
|
51632
|
+
const options = input.options ?? DEFAULT_DEMAND_OPTIONS;
|
|
51633
|
+
const placement = input.shares ? "share" : "least-loaded";
|
|
51634
|
+
const eligibleById = new Map(input.eligible.map((d) => [deviceId(d.nodeId, d.deviceKey), d]));
|
|
51635
|
+
const peakById = new Map(input.peaks.map((p) => [deviceId(p.nodeId, p.deviceKey), p]));
|
|
51636
|
+
const weightByNode = /* @__PURE__ */ new Map();
|
|
51637
|
+
const eligibleCountByNode = /* @__PURE__ */ new Map();
|
|
51638
|
+
for (const d of input.eligible) {
|
|
51639
|
+
weightByNode.set(d.nodeId, (weightByNode.get(d.nodeId) ?? 0) + Math.max(d.weight, 0));
|
|
51640
|
+
eligibleCountByNode.set(d.nodeId, (eligibleCountByNode.get(d.nodeId) ?? 0) + 1);
|
|
51641
|
+
}
|
|
51642
|
+
const pinnedFps = /* @__PURE__ */ new Map();
|
|
51643
|
+
const alwaysOnFps = /* @__PURE__ */ new Map();
|
|
51644
|
+
const alwaysOnCount = /* @__PURE__ */ new Map();
|
|
51645
|
+
const maxFps = /* @__PURE__ */ new Map();
|
|
51646
|
+
const onMotionRates = [];
|
|
51647
|
+
const bump = (map, key, by) => {
|
|
51648
|
+
map.set(key, (map.get(key) ?? 0) + by);
|
|
51649
|
+
};
|
|
51650
|
+
const noteMax = (key, fps) => {
|
|
51651
|
+
maxFps.set(key, Math.max(maxFps.get(key) ?? 0, fps));
|
|
51652
|
+
};
|
|
51653
|
+
const pinNodeOf = (pin) => {
|
|
51654
|
+
if (pin.nodeId !== null) return pin.nodeId;
|
|
51655
|
+
const holders = input.eligible.filter((d) => d.deviceKey === pin.deviceKey);
|
|
51656
|
+
return holders.length === 1 ? holders[0].nodeId : null;
|
|
51657
|
+
};
|
|
51658
|
+
let alwaysOnTotal = 0;
|
|
51659
|
+
for (const cam of input.cameras) {
|
|
51660
|
+
if (!charges(cam)) continue;
|
|
51661
|
+
const pinNode = cam.devicePin !== null ? pinNodeOf(cam.devicePin) : null;
|
|
51662
|
+
if (cam.devicePin !== null && pinNode !== null) {
|
|
51663
|
+
const key = deviceId(pinNode, cam.devicePin.deviceKey);
|
|
51664
|
+
if (!eligibleById.has(key)) continue;
|
|
51665
|
+
bump(pinnedFps, key, cam.fps);
|
|
51666
|
+
noteMax(key, cam.fps);
|
|
51667
|
+
continue;
|
|
51668
|
+
}
|
|
51669
|
+
const standing = cam.mode === "always-on" && cam.standingNodeId !== null ? cam.standingNodeId : cam.nodePinId;
|
|
51670
|
+
if (standing !== null) {
|
|
51671
|
+
if (cam.mode === "always-on") alwaysOnTotal += 1;
|
|
51672
|
+
const nodeWeight = weightByNode.get(standing) ?? 0;
|
|
51673
|
+
for (const d of input.eligible) {
|
|
51674
|
+
if (d.nodeId !== standing || nodeWeight <= 0) continue;
|
|
51675
|
+
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51676
|
+
const share = Math.max(d.weight, 0) / nodeWeight;
|
|
51677
|
+
bump(alwaysOnFps, key, cam.fps * share);
|
|
51678
|
+
bump(alwaysOnCount, key, share);
|
|
51679
|
+
noteMax(key, cam.fps);
|
|
51680
|
+
}
|
|
51681
|
+
continue;
|
|
51682
|
+
}
|
|
51683
|
+
onMotionRates.push(cam.fps);
|
|
51684
|
+
}
|
|
51685
|
+
const fastestOnMotion = onMotionRates.reduce((m, r) => Math.max(m, r), 0);
|
|
51686
|
+
const nodeCount = weightByNode.size;
|
|
51687
|
+
const estimatedTotal = estimatedConcurrentSessions(onMotionRates.length, options);
|
|
51688
|
+
const clusterPeak = input.shares?.clusterPeak ?? null;
|
|
51689
|
+
const clusterObserved = clusterPeak !== null && clusterPeak.observedForMs >= 864e5;
|
|
51690
|
+
const clusterSessions = clusterObserved ? Math.max(0, clusterPeak.peakSessions - alwaysOnTotal) : estimatedTotal;
|
|
51691
|
+
const shareSum = [...weightByNode.keys()].reduce((sum, nodeId) => sum + Math.max(input.shares?.nodes[nodeId] ?? 0, 0), 0);
|
|
51692
|
+
return input.eligible.map((d) => {
|
|
51693
|
+
const key = deviceId(d.nodeId, d.deviceKey);
|
|
51694
|
+
const peak = peakById.get(key) ?? (eligibleCountByNode.get(d.nodeId) === 1 ? peakById.get(deviceId(d.nodeId, "")) : void 0);
|
|
51695
|
+
const alwaysOn = alwaysOnCount.get(key) ?? 0;
|
|
51696
|
+
let onMotionSessions = 0;
|
|
51697
|
+
let onMotionSource = "none";
|
|
51698
|
+
if (onMotionRates.length > 0 && input.shares !== void 0) {
|
|
51699
|
+
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51700
|
+
const nodeShare = shareSum > 0 ? Math.max(input.shares.nodes[d.nodeId] ?? 0, 0) / shareSum : 0;
|
|
51701
|
+
const deviceShare = nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight : 0;
|
|
51702
|
+
onMotionSessions = Math.max(0, Math.ceil(clusterSessions * nodeShare * deviceShare - 1e-9));
|
|
51703
|
+
onMotionSource = clusterObserved ? "observed-24h" : "estimate";
|
|
51704
|
+
} else if (onMotionRates.length > 0) if (peak !== void 0 && peak.observedForMs >= 864e5) {
|
|
51705
|
+
onMotionSessions = Math.max(0, Math.round(peak.peakSessions - alwaysOn));
|
|
51706
|
+
onMotionSource = "observed-24h";
|
|
51707
|
+
} else {
|
|
51708
|
+
const nodeWeight = weightByNode.get(d.nodeId) ?? 0;
|
|
51709
|
+
const share = nodeCount > 0 && nodeWeight > 0 ? Math.max(d.weight, 0) / nodeWeight / nodeCount : 0;
|
|
51710
|
+
onMotionSessions = Math.ceil(estimatedTotal * share);
|
|
51711
|
+
onMotionSource = "estimate";
|
|
51712
|
+
}
|
|
51713
|
+
const demandFps = (pinnedFps.get(key) ?? 0) + (alwaysOnFps.get(key) ?? 0) + topRatesSum(onMotionRates, onMotionSessions);
|
|
51714
|
+
return {
|
|
51715
|
+
nodeId: d.nodeId,
|
|
51716
|
+
deviceKey: d.deviceKey,
|
|
51717
|
+
demandFps: Math.round(demandFps * 100) / 100,
|
|
51718
|
+
maxCameraFps: Math.max(maxFps.get(key) ?? 0, fastestOnMotion),
|
|
51719
|
+
alwaysOnCameras: Math.round(alwaysOn * 100) / 100,
|
|
51720
|
+
onMotionSessions,
|
|
51721
|
+
onMotionSource,
|
|
51722
|
+
observedForMs: input.shares !== void 0 ? clusterPeak?.observedForMs ?? null : peak?.observedForMs ?? null,
|
|
51723
|
+
placement
|
|
51724
|
+
};
|
|
51725
|
+
}).toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
51726
|
+
}
|
|
51727
|
+
/** The family the cluster runs. */
|
|
51728
|
+
var BEST_SETTINGS_FAMILY = "yolov9";
|
|
51729
|
+
function round(value, digits = 3) {
|
|
51730
|
+
const f = 10 ** digits;
|
|
51731
|
+
return Math.round(value * f) / f;
|
|
51732
|
+
}
|
|
51733
|
+
function utilisationOf(cell, demand) {
|
|
51734
|
+
return cell.fps > 0 ? round((demand?.demandFps ?? 0) / cell.fps) : Number.POSITIVE_INFINITY;
|
|
51735
|
+
}
|
|
51736
|
+
function fitsOn(cell, demand, headroom) {
|
|
51737
|
+
const budget = cell.fps * (1 - headroom);
|
|
51738
|
+
return budget >= (demand?.demandFps ?? 0) && budget >= (demand?.maxCameraFps ?? 0);
|
|
51739
|
+
}
|
|
51740
|
+
function candidatesFor(contexts, formatsByModel) {
|
|
51741
|
+
return Object.keys(DETECTOR_ACCURACY).filter((id) => id.startsWith(BEST_SETTINGS_FAMILY)).filter((id) => contexts.every((c) => c.column[id] !== void 0 && (formatsByModel[id] ?? []).includes(c.device.format))).map((id) => ({
|
|
51742
|
+
modelId: id,
|
|
51743
|
+
personRecall: DETECTOR_ACCURACY[id]?.personRecall ?? 0
|
|
51744
|
+
})).toSorted((a, b) => b.personRecall - a.personRecall || a.modelId.localeCompare(b.modelId));
|
|
51745
|
+
}
|
|
51746
|
+
function totalLatency(contexts, modelId) {
|
|
51747
|
+
return contexts.reduce((sum, c) => sum + (c.column[modelId]?.p50Ms ?? Number.POSITIVE_INFINITY), 0);
|
|
51748
|
+
}
|
|
51749
|
+
function worstUtilisation(contexts, modelId) {
|
|
51750
|
+
return contexts.reduce((worst, c) => {
|
|
51751
|
+
const cell = c.column[modelId];
|
|
51752
|
+
return Math.max(worst, cell ? utilisationOf(cell, c.demand) : Number.POSITIVE_INFINITY);
|
|
51753
|
+
}, 0);
|
|
51754
|
+
}
|
|
51755
|
+
/** One model for a group of devices that must agree (step 2–4 of the header). */
|
|
51756
|
+
function pickForGroup(contexts, formatsByModel, headroom) {
|
|
51757
|
+
const candidates = candidatesFor(contexts, formatsByModel);
|
|
51758
|
+
if (candidates.length === 0) return null;
|
|
51759
|
+
const fitting = candidates.filter((c) => contexts.every((ctx) => {
|
|
51760
|
+
const cell = ctx.column[c.modelId];
|
|
51761
|
+
return cell !== void 0 && fitsOn(cell, ctx.demand, headroom);
|
|
51762
|
+
}));
|
|
51763
|
+
if (fitting.length === 0) {
|
|
51764
|
+
const lightest = candidates.toSorted((a, b) => worstUtilisation(contexts, a.modelId) - worstUtilisation(contexts, b.modelId))[0];
|
|
51765
|
+
if (lightest === void 0) return null;
|
|
51766
|
+
return {
|
|
51767
|
+
modelId: lightest.modelId,
|
|
51768
|
+
reasons: [{
|
|
51769
|
+
kind: "no-fit-lightest",
|
|
51770
|
+
utilisation: worstUtilisation(contexts, lightest.modelId)
|
|
51771
|
+
}]
|
|
51772
|
+
};
|
|
51773
|
+
}
|
|
51774
|
+
const best = fitting[0];
|
|
51775
|
+
if (best === void 0) return null;
|
|
51776
|
+
const chosen = fitting.filter((c) => best.personRecall - c.personRecall <= .02).toSorted((a, b) => totalLatency(contexts, a.modelId) - totalLatency(contexts, b.modelId) || b.personRecall - a.personRecall)[0] ?? best;
|
|
51777
|
+
const reasons = [];
|
|
51778
|
+
const mostAccurate = candidates[0];
|
|
51779
|
+
if (mostAccurate !== void 0 && mostAccurate.modelId !== best.modelId) {
|
|
51780
|
+
const limitedBy = contexts.find((ctx) => {
|
|
51781
|
+
const cell = ctx.column[mostAccurate.modelId];
|
|
51782
|
+
return cell === void 0 || !fitsOn(cell, ctx.demand, headroom);
|
|
51783
|
+
})?.device.key ?? "";
|
|
51784
|
+
reasons.push({
|
|
51785
|
+
kind: "stepped-down",
|
|
51786
|
+
from: mostAccurate.modelId,
|
|
51787
|
+
to: best.modelId,
|
|
51788
|
+
limitedBy
|
|
51789
|
+
});
|
|
51790
|
+
}
|
|
51791
|
+
if (chosen.modelId !== best.modelId) reasons.push({
|
|
51792
|
+
kind: "tie-faster",
|
|
51793
|
+
over: best.modelId,
|
|
51794
|
+
recallDelta: round(best.personRecall - chosen.personRecall)
|
|
51795
|
+
});
|
|
51796
|
+
return {
|
|
51797
|
+
modelId: chosen.modelId,
|
|
51798
|
+
reasons
|
|
51799
|
+
};
|
|
51800
|
+
}
|
|
51801
|
+
function choiceOn(ctx, modelId) {
|
|
51802
|
+
const cell = ctx.column[modelId];
|
|
51803
|
+
if (cell === void 0) return null;
|
|
51804
|
+
return {
|
|
51805
|
+
modelId,
|
|
51806
|
+
p50Ms: cell.p50Ms,
|
|
51807
|
+
fps: cell.fps,
|
|
51808
|
+
measured: cell.measured,
|
|
51809
|
+
utilisation: utilisationOf(cell, ctx.demand),
|
|
51810
|
+
personRecall: DETECTOR_ACCURACY[modelId]?.personRecall ?? 0
|
|
51811
|
+
};
|
|
51812
|
+
}
|
|
51813
|
+
function deviceReasonsFor(ctx, choice) {
|
|
51814
|
+
const out = [{
|
|
51815
|
+
kind: "fits",
|
|
51816
|
+
utilisation: choice.utilisation
|
|
51817
|
+
}];
|
|
51818
|
+
if (!choice.measured) out.push({
|
|
51819
|
+
kind: "derived-latency",
|
|
51820
|
+
modelId: choice.modelId
|
|
51821
|
+
});
|
|
51822
|
+
if (ctx.demand?.onMotionSource === "estimate") out.push({
|
|
51823
|
+
kind: "estimated-load",
|
|
51824
|
+
observedForMs: ctx.demand.observedForMs
|
|
51825
|
+
});
|
|
51826
|
+
return out;
|
|
51827
|
+
}
|
|
51828
|
+
function skippedNode(node, reason, hardwareClass) {
|
|
51829
|
+
return {
|
|
51830
|
+
nodeId: node.nodeId,
|
|
51831
|
+
reachable: node.reachable,
|
|
51832
|
+
hardwareClass,
|
|
51833
|
+
cpuModel: node.hardware?.cpuModel ?? null,
|
|
51834
|
+
skipped: true,
|
|
51835
|
+
acceleratorModelId: null,
|
|
51836
|
+
devices: node.devices.map((d) => ({
|
|
51837
|
+
deviceKey: d.key,
|
|
51838
|
+
accelerator: node.hardware ? acceleratorKindOf(d.key, node.hardware) : null,
|
|
51839
|
+
enabled: d.enabled,
|
|
51840
|
+
available: d.available,
|
|
51841
|
+
demand: null,
|
|
51842
|
+
chosen: null,
|
|
51843
|
+
reasons: []
|
|
51844
|
+
})),
|
|
51845
|
+
reasons: [reason]
|
|
51846
|
+
};
|
|
51847
|
+
}
|
|
51848
|
+
/** Recommend one node. See the module header. */
|
|
51849
|
+
function recommendNode(input) {
|
|
51850
|
+
const { node } = input;
|
|
51851
|
+
const headroom = input.headroom ?? .4;
|
|
51852
|
+
const perf = input.perf ?? DETECTOR_PERF;
|
|
51853
|
+
if (!node.reachable) return skippedNode(node, { kind: "node-offline" }, null);
|
|
51854
|
+
if (node.hardware === null) return skippedNode(node, { kind: "no-hardware-facts" }, null);
|
|
51855
|
+
const hardware = node.hardware;
|
|
51856
|
+
const { hardwareClass, cpuModel } = classifyInferenceHardware(hardware);
|
|
51857
|
+
const classTable = perf[hardwareClass];
|
|
51858
|
+
if (hardwareClass === "unknown" || classTable === void 0) return skippedNode(node, {
|
|
51859
|
+
kind: "no-measured-data",
|
|
51860
|
+
hardwareClass
|
|
51861
|
+
}, hardwareClass);
|
|
51862
|
+
const demandByKey = new Map(input.demand.filter((d) => d.nodeId === node.nodeId).map((d) => [d.deviceKey, d]));
|
|
51863
|
+
const devices = node.devices.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
51864
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
51865
|
+
const early = /* @__PURE__ */ new Map();
|
|
51866
|
+
for (const device of devices) {
|
|
51867
|
+
const accelerator = acceleratorKindOf(device.key, hardware);
|
|
51868
|
+
if (accelerator === "coral") {
|
|
51869
|
+
early.set(device.key, device.enabled ? [{ kind: "coral-untouched" }] : []);
|
|
51870
|
+
continue;
|
|
51871
|
+
}
|
|
51872
|
+
if (!device.enabled) {
|
|
51873
|
+
early.set(device.key, [{ kind: "device-disabled" }]);
|
|
51874
|
+
continue;
|
|
51875
|
+
}
|
|
51876
|
+
if (!device.available) {
|
|
51877
|
+
early.set(device.key, [{ kind: "device-unavailable" }]);
|
|
51878
|
+
continue;
|
|
51879
|
+
}
|
|
51880
|
+
const column = accelerator === null ? void 0 : classTable[accelerator];
|
|
51881
|
+
if (accelerator === null || column === void 0) {
|
|
51882
|
+
early.set(device.key, [{
|
|
51883
|
+
kind: "no-family-build",
|
|
51884
|
+
format: device.format
|
|
51885
|
+
}]);
|
|
51886
|
+
continue;
|
|
51887
|
+
}
|
|
51888
|
+
contexts.set(device.key, {
|
|
51889
|
+
device,
|
|
51890
|
+
accelerator,
|
|
51891
|
+
column,
|
|
51892
|
+
demand: demandByKey.get(device.key) ?? null
|
|
51893
|
+
});
|
|
51894
|
+
}
|
|
51895
|
+
const isCpu = (a) => a === "x86-cpu" || a === "arm-cpu";
|
|
51896
|
+
const accelerators = [...contexts.values()].filter((c) => !isCpu(c.accelerator));
|
|
51897
|
+
const cpus = [...contexts.values()].filter((c) => isCpu(c.accelerator));
|
|
51898
|
+
const chosenByKey = /* @__PURE__ */ new Map();
|
|
51899
|
+
const reasonsByKey = /* @__PURE__ */ new Map();
|
|
51900
|
+
const nodeReasons = [];
|
|
51901
|
+
let acceleratorModelId = null;
|
|
51902
|
+
const groupPick = accelerators.length > 0 ? pickForGroup(accelerators, node.formatsByModel, headroom) : null;
|
|
51903
|
+
if (groupPick !== null) {
|
|
51904
|
+
acceleratorModelId = groupPick.modelId;
|
|
51905
|
+
nodeReasons.push(...groupPick.reasons);
|
|
51906
|
+
for (const ctx of accelerators) {
|
|
51907
|
+
const choice = choiceOn(ctx, groupPick.modelId);
|
|
51908
|
+
if (choice === null) continue;
|
|
51909
|
+
chosenByKey.set(ctx.device.key, choice);
|
|
51910
|
+
reasonsByKey.set(ctx.device.key, deviceReasonsFor(ctx, choice));
|
|
51911
|
+
}
|
|
51912
|
+
} else for (const ctx of accelerators) reasonsByKey.set(ctx.device.key, [{
|
|
51913
|
+
kind: "no-family-build",
|
|
51914
|
+
format: ctx.device.format
|
|
51915
|
+
}]);
|
|
51916
|
+
for (const ctx of cpus) {
|
|
51917
|
+
const pick = pickForGroup([ctx], node.formatsByModel, headroom);
|
|
51918
|
+
const choice = pick === null ? null : choiceOn(ctx, pick.modelId);
|
|
51919
|
+
if (pick === null || choice === null) {
|
|
51920
|
+
reasonsByKey.set(ctx.device.key, [{
|
|
51921
|
+
kind: "no-family-build",
|
|
51922
|
+
format: ctx.device.format
|
|
51923
|
+
}]);
|
|
51924
|
+
continue;
|
|
51925
|
+
}
|
|
51926
|
+
chosenByKey.set(ctx.device.key, choice);
|
|
51927
|
+
reasonsByKey.set(ctx.device.key, [
|
|
51928
|
+
{ kind: "cpu-device" },
|
|
51929
|
+
...pick.reasons,
|
|
51930
|
+
...deviceReasonsFor(ctx, choice)
|
|
51931
|
+
]);
|
|
51932
|
+
}
|
|
51933
|
+
return {
|
|
51934
|
+
nodeId: node.nodeId,
|
|
51935
|
+
reachable: true,
|
|
51936
|
+
hardwareClass,
|
|
51937
|
+
cpuModel,
|
|
51938
|
+
skipped: chosenByKey.size === 0,
|
|
51939
|
+
acceleratorModelId,
|
|
51940
|
+
devices: devices.map((device) => ({
|
|
51941
|
+
deviceKey: device.key,
|
|
51942
|
+
accelerator: acceleratorKindOf(device.key, hardware),
|
|
51943
|
+
enabled: device.enabled,
|
|
51944
|
+
available: device.available,
|
|
51945
|
+
demand: demandByKey.get(device.key) ?? null,
|
|
51946
|
+
chosen: chosenByKey.get(device.key) ?? null,
|
|
51947
|
+
reasons: early.get(device.key) ?? reasonsByKey.get(device.key) ?? []
|
|
51948
|
+
})),
|
|
51949
|
+
reasons: nodeReasons
|
|
51950
|
+
};
|
|
51951
|
+
}
|
|
51952
|
+
/** Deficits equal up to float noise (0.7 × 2 − 1 is 0.3999…, not 0.4). */
|
|
51953
|
+
var DEFICIT_EPSILON = 1e-9;
|
|
51954
|
+
function byDeficit(a, b) {
|
|
51955
|
+
const diff = b.deficit - a.deficit;
|
|
51956
|
+
return (Math.abs(diff) < DEFICIT_EPSILON ? 0 : diff) || b.candidate.share - a.candidate.share || a.candidate.key.localeCompare(b.candidate.key);
|
|
51957
|
+
}
|
|
51958
|
+
function spareOf(c) {
|
|
51959
|
+
return c.spare ?? Number.POSITIVE_INFINITY;
|
|
51960
|
+
}
|
|
51961
|
+
function electByShare(candidates) {
|
|
51962
|
+
if (candidates.length === 0) return { kind: "none" };
|
|
51963
|
+
const shareSum = candidates.reduce((s, c) => s + Math.max(c.share, 0), 0);
|
|
51964
|
+
const running = candidates.reduce((s, c) => s + Math.max(c.running, 0), 0);
|
|
51965
|
+
const scored = candidates.map((candidate) => ({
|
|
51966
|
+
candidate,
|
|
51967
|
+
deficit: (shareSum > 0 ? Math.max(candidate.share, 0) / shareSum : 1 / candidates.length) * (running + 1) - candidate.running
|
|
51968
|
+
})).toSorted(byDeficit);
|
|
51969
|
+
const turn = scored[0].candidate;
|
|
51970
|
+
if (!turn.capped && !turn.saturated) return {
|
|
51971
|
+
kind: "elected",
|
|
51972
|
+
key: turn.key,
|
|
51973
|
+
reason: "share"
|
|
51974
|
+
};
|
|
51975
|
+
const free = scored.filter((s) => !s.candidate.capped && !s.candidate.saturated).toSorted((a, b) => spareOf(b.candidate) - spareOf(a.candidate) || byDeficit(a, b));
|
|
51976
|
+
const yieldReason = turn.capped ? "at-cap" : "saturated";
|
|
51977
|
+
if (free.length > 0) return {
|
|
51978
|
+
kind: "elected",
|
|
51979
|
+
key: free[0].candidate.key,
|
|
51980
|
+
reason: "yield",
|
|
51981
|
+
turnKey: turn.key,
|
|
51982
|
+
yieldReason
|
|
51983
|
+
};
|
|
51984
|
+
const uncapped = scored.find((s) => !s.candidate.capped);
|
|
51985
|
+
if (uncapped === void 0) return { kind: "none" };
|
|
51986
|
+
return {
|
|
51987
|
+
kind: "elected",
|
|
51988
|
+
key: uncapped.candidate.key,
|
|
51989
|
+
reason: "share"
|
|
51990
|
+
};
|
|
51991
|
+
}
|
|
50659
51992
|
/**
|
|
50660
51993
|
* THE detail-crop convention — the single derivation of the rectangle a
|
|
50661
51994
|
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
@@ -51430,7 +52763,7 @@ var AudioSubscriptionController = class {
|
|
|
51430
52763
|
} catch (err) {
|
|
51431
52764
|
this.deps.logger.error("audio re-attach failed", {
|
|
51432
52765
|
tags: { deviceId },
|
|
51433
|
-
meta: { error: errMsg$
|
|
52766
|
+
meta: { error: errMsg$2(err) }
|
|
51434
52767
|
});
|
|
51435
52768
|
}
|
|
51436
52769
|
}
|
|
@@ -51541,7 +52874,7 @@ var AudioSubscriptionController = class {
|
|
|
51541
52874
|
this.closeMotionAudioWindow(deviceId, "motion-cooldown-elapsed").catch((err) => {
|
|
51542
52875
|
this.deps.logger.warn("lazy audio: cooldown teardown failed", {
|
|
51543
52876
|
tags: { deviceId },
|
|
51544
|
-
meta: { error: errMsg$
|
|
52877
|
+
meta: { error: errMsg$2(err) }
|
|
51545
52878
|
});
|
|
51546
52879
|
});
|
|
51547
52880
|
}, cooldownMs);
|
|
@@ -51560,7 +52893,7 @@ var AudioSubscriptionController = class {
|
|
|
51560
52893
|
this.closeMotionAudioWindow(deviceId, "quiet-window-elapsed").catch((err) => {
|
|
51561
52894
|
this.deps.logger.warn("lazy audio: quiet-window teardown failed", {
|
|
51562
52895
|
tags: { deviceId },
|
|
51563
|
-
meta: { error: errMsg$
|
|
52896
|
+
meta: { error: errMsg$2(err) }
|
|
51564
52897
|
});
|
|
51565
52898
|
});
|
|
51566
52899
|
}, windowMs);
|
|
@@ -51762,7 +53095,7 @@ var AudioSubscriptionController = class {
|
|
|
51762
53095
|
meta: {
|
|
51763
53096
|
audioNodeId,
|
|
51764
53097
|
brokerId: audioBrokerId,
|
|
51765
|
-
error: errMsg$
|
|
53098
|
+
error: errMsg$2(err)
|
|
51766
53099
|
}
|
|
51767
53100
|
});
|
|
51768
53101
|
});
|
|
@@ -51837,7 +53170,7 @@ async function readOrRefuse(deps, deviceId) {
|
|
|
51837
53170
|
try {
|
|
51838
53171
|
device = await deps.readDevice(deviceId);
|
|
51839
53172
|
} catch (err) {
|
|
51840
|
-
return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$
|
|
53173
|
+
return refuse(deps, deviceId, `could not read device ${deviceId}: ${errMsg$2(err)}`);
|
|
51841
53174
|
}
|
|
51842
53175
|
if (device === null) return refuse(deps, deviceId, `no device ${deviceId}`);
|
|
51843
53176
|
return device;
|
|
@@ -53167,7 +54500,7 @@ var LoadShedController = class LoadShedController {
|
|
|
53167
54500
|
if (await this.tryClusterRelocate(deviceId).catch((err) => {
|
|
53168
54501
|
this.deps.logger.warn("Load management: cluster relocate failed", {
|
|
53169
54502
|
tags: { deviceId },
|
|
53170
|
-
meta: { error: errMsg$
|
|
54503
|
+
meta: { error: errMsg$2(err) }
|
|
53171
54504
|
});
|
|
53172
54505
|
return false;
|
|
53173
54506
|
})) {
|
|
@@ -53181,7 +54514,7 @@ var LoadShedController = class LoadShedController {
|
|
|
53181
54514
|
this.deps.api()?.pipelineRunner.detachCamera.mutate({ deviceId }).catch((err) => {
|
|
53182
54515
|
this.deps.logger.warn("Load management: detachCamera failed", {
|
|
53183
54516
|
tags: { deviceId },
|
|
53184
|
-
meta: { error: errMsg$
|
|
54517
|
+
meta: { error: errMsg$2(err) }
|
|
53185
54518
|
});
|
|
53186
54519
|
});
|
|
53187
54520
|
this.ensureLoadShedResumeTimer();
|
|
@@ -53213,7 +54546,7 @@ var LoadShedController = class LoadShedController {
|
|
|
53213
54546
|
tags: { deviceId },
|
|
53214
54547
|
meta: {
|
|
53215
54548
|
from: currentNode,
|
|
53216
|
-
error: errMsg$
|
|
54549
|
+
error: errMsg$2(err)
|
|
53217
54550
|
}
|
|
53218
54551
|
});
|
|
53219
54552
|
});
|
|
@@ -53256,7 +54589,7 @@ var LoadShedController = class LoadShedController {
|
|
|
53256
54589
|
this.deps.redispatchSingleCamera(deviceId).catch((err) => {
|
|
53257
54590
|
this.deps.logger.warn("Load management: auto-resume dispatch failed", {
|
|
53258
54591
|
tags: { deviceId },
|
|
53259
|
-
meta: { error: errMsg$
|
|
54592
|
+
meta: { error: errMsg$2(err) }
|
|
53260
54593
|
});
|
|
53261
54594
|
});
|
|
53262
54595
|
}
|
|
@@ -53421,7 +54754,7 @@ var AgentLoadService = class AgentLoadService {
|
|
|
53421
54754
|
ms,
|
|
53422
54755
|
outcome: "failed",
|
|
53423
54756
|
load: null,
|
|
53424
|
-
error: errMsg$
|
|
54757
|
+
error: errMsg$2(outcome.error)
|
|
53425
54758
|
};
|
|
53426
54759
|
return {
|
|
53427
54760
|
nodeId,
|
|
@@ -53888,7 +55221,7 @@ function buildCameraSensorsPorts(api, logger) {
|
|
|
53888
55221
|
} catch (err) {
|
|
53889
55222
|
return {
|
|
53890
55223
|
ok: false,
|
|
53891
|
-
error: errMsg$
|
|
55224
|
+
error: errMsg$2(err)
|
|
53892
55225
|
};
|
|
53893
55226
|
}
|
|
53894
55227
|
};
|
|
@@ -54152,7 +55485,7 @@ var CameraSensorsController = class {
|
|
|
54152
55485
|
async #readFleet() {
|
|
54153
55486
|
const read = await this.#deps.listCameraIds().catch((err) => ({
|
|
54154
55487
|
kind: "unreadable",
|
|
54155
|
-
error: errMsg$
|
|
55488
|
+
error: errMsg$2(err)
|
|
54156
55489
|
}));
|
|
54157
55490
|
if (read.kind === "unreadable") {
|
|
54158
55491
|
this.#deps.logger.warn("camera-sensors: camera list unreadable — mappings stay unloaded", { meta: { error: read.error } });
|
|
@@ -54179,7 +55512,7 @@ var CameraSensorsController = class {
|
|
|
54179
55512
|
} catch (err) {
|
|
54180
55513
|
this.#deps.logger.error("camera-sensors: refresh failed — nothing applied", {
|
|
54181
55514
|
tags: { deviceId: cameraId },
|
|
54182
|
-
meta: { error: errMsg$
|
|
55515
|
+
meta: { error: errMsg$2(err) }
|
|
54183
55516
|
});
|
|
54184
55517
|
}
|
|
54185
55518
|
}
|
|
@@ -54272,7 +55605,7 @@ async function readCameraSensorsSetting(read) {
|
|
|
54272
55605
|
} catch (err) {
|
|
54273
55606
|
return {
|
|
54274
55607
|
state: "unreadable",
|
|
54275
|
-
reason: `settings read failed — ${errMsg$
|
|
55608
|
+
reason: `settings read failed — ${errMsg$2(err)}`
|
|
54276
55609
|
};
|
|
54277
55610
|
}
|
|
54278
55611
|
}
|
|
@@ -54582,7 +55915,7 @@ var CameraMotionSlice = class {
|
|
|
54582
55915
|
st.chain = st.chain.then(job).catch((err) => {
|
|
54583
55916
|
this.#deps.logger.error("camera motion slice: write job failed", {
|
|
54584
55917
|
tags: { deviceId },
|
|
54585
|
-
meta: { error: errMsg$
|
|
55918
|
+
meta: { error: errMsg$2(err) }
|
|
54586
55919
|
});
|
|
54587
55920
|
});
|
|
54588
55921
|
}
|
|
@@ -54646,7 +55979,7 @@ var CameraMotionSlice = class {
|
|
|
54646
55979
|
meta: {
|
|
54647
55980
|
detected: slice.detected,
|
|
54648
55981
|
reason,
|
|
54649
|
-
error: errMsg$
|
|
55982
|
+
error: errMsg$2(err)
|
|
54650
55983
|
}
|
|
54651
55984
|
});
|
|
54652
55985
|
return;
|
|
@@ -54676,7 +56009,7 @@ var CameraMotionSlice = class {
|
|
|
54676
56009
|
async #republishAll() {
|
|
54677
56010
|
const listed = await this.#deps.listCameraIds().catch((err) => ({
|
|
54678
56011
|
kind: "unreadable",
|
|
54679
|
-
error: errMsg$
|
|
56012
|
+
error: errMsg$2(err)
|
|
54680
56013
|
}));
|
|
54681
56014
|
if (!this.#open || this.#stopped) return;
|
|
54682
56015
|
if (listed.kind === "unreadable") this.#deps.logger.warn("camera motion slice: camera list unreadable — only cameras with motion since boot are re-published", { meta: { error: listed.error } });
|
|
@@ -54798,7 +56131,7 @@ function hubMotionSliceStore(api) {
|
|
|
54798
56131
|
} catch (err) {
|
|
54799
56132
|
return {
|
|
54800
56133
|
kind: "unreadable",
|
|
54801
|
-
error: errMsg$
|
|
56134
|
+
error: errMsg$2(err)
|
|
54802
56135
|
};
|
|
54803
56136
|
}
|
|
54804
56137
|
}
|
|
@@ -56670,7 +58003,7 @@ function warnSampled(deps, deviceId, source, message, meta) {
|
|
|
56670
58003
|
function warnUnreachable(deps, deviceId, source, err) {
|
|
56671
58004
|
warnSampled(deps, deviceId, source, UNREACHABLE_MESSAGE, {
|
|
56672
58005
|
source,
|
|
56673
|
-
error: errMsg$
|
|
58006
|
+
error: errMsg$2(err)
|
|
56674
58007
|
});
|
|
56675
58008
|
}
|
|
56676
58009
|
/**
|
|
@@ -57944,7 +59277,7 @@ var ONBOARD_DETECTION_SOURCE = "onboard";
|
|
|
57944
59277
|
* configs, and it reads them the same way the runner does.
|
|
57945
59278
|
*/
|
|
57946
59279
|
var DETECTION_SOURCES_WHEN_ABSENT = ["pipeline"];
|
|
57947
|
-
function errMsg(err) {
|
|
59280
|
+
function errMsg$1(err) {
|
|
57948
59281
|
return err instanceof Error ? err.message : String(err);
|
|
57949
59282
|
}
|
|
57950
59283
|
/**
|
|
@@ -57963,7 +59296,7 @@ async function reconcileOnboardForwarding(input, deps) {
|
|
|
57963
59296
|
log.warn("onboard forwarding not reconciled — could not tell whether the camera has the cap", { meta: {
|
|
57964
59297
|
wantsOnboard,
|
|
57965
59298
|
detectionSources: [...detectionSources],
|
|
57966
|
-
error: errMsg(err)
|
|
59299
|
+
error: errMsg$1(err)
|
|
57967
59300
|
} });
|
|
57968
59301
|
return {
|
|
57969
59302
|
kind: "read-failed",
|
|
@@ -57981,7 +59314,7 @@ async function reconcileOnboardForwarding(input, deps) {
|
|
|
57981
59314
|
log.warn("onboard forwarding not reconciled — the vendor flag could not be read", { meta: {
|
|
57982
59315
|
wantsOnboard,
|
|
57983
59316
|
detectionSources: [...detectionSources],
|
|
57984
|
-
error: errMsg(err)
|
|
59317
|
+
error: errMsg$1(err)
|
|
57985
59318
|
} });
|
|
57986
59319
|
return {
|
|
57987
59320
|
kind: "read-failed",
|
|
@@ -57999,7 +59332,7 @@ async function reconcileOnboardForwarding(input, deps) {
|
|
|
57999
59332
|
was: current,
|
|
58000
59333
|
wanted: wantsOnboard,
|
|
58001
59334
|
detectionSources: [...detectionSources],
|
|
58002
|
-
error: errMsg(err)
|
|
59335
|
+
error: errMsg$1(err)
|
|
58003
59336
|
} });
|
|
58004
59337
|
return {
|
|
58005
59338
|
kind: "write-failed",
|
|
@@ -58195,7 +59528,7 @@ var DetectionWiringController = class {
|
|
|
58195
59528
|
try {
|
|
58196
59529
|
raw = await this.deps.ctx().settings.readDeviceStore(deviceId);
|
|
58197
59530
|
} catch (err) {
|
|
58198
|
-
const msg = errMsg$
|
|
59531
|
+
const msg = errMsg$2(err);
|
|
58199
59532
|
this.deps.logger.error("resolveDeviceDetectionSettings failed", {
|
|
58200
59533
|
tags: { deviceId },
|
|
58201
59534
|
meta: { error: msg }
|
|
@@ -58226,7 +59559,7 @@ var DetectionWiringController = class {
|
|
|
58226
59559
|
motionDetectionEnabled
|
|
58227
59560
|
});
|
|
58228
59561
|
} catch (err) {
|
|
58229
|
-
const msg = errMsg$
|
|
59562
|
+
const msg = errMsg$2(err);
|
|
58230
59563
|
this.deps.logger.error("resolveDeviceDetectionSettings narrowing failed", {
|
|
58231
59564
|
tags: { deviceId },
|
|
58232
59565
|
meta: { error: msg }
|
|
@@ -58261,7 +59594,7 @@ var DetectionWiringController = class {
|
|
|
58261
59594
|
} catch (err) {
|
|
58262
59595
|
this.deps.logger.debug("fetchAssignedProfiles: slot read failed (transient) — treating as unknown", {
|
|
58263
59596
|
tags: { deviceId },
|
|
58264
|
-
meta: { error: errMsg$
|
|
59597
|
+
meta: { error: errMsg$2(err) }
|
|
58265
59598
|
});
|
|
58266
59599
|
return null;
|
|
58267
59600
|
}
|
|
@@ -58337,7 +59670,7 @@ var DetectionWiringController = class {
|
|
|
58337
59670
|
if (config.motionSources.includes("analyzer")) try {
|
|
58338
59671
|
await this.deps.session.ensureMotionWatch(deviceId, config);
|
|
58339
59672
|
} catch (err) {
|
|
58340
|
-
log.error("ensureMotionWatch failed", { meta: { error: errMsg$
|
|
59673
|
+
log.error("ensureMotionWatch failed", { meta: { error: errMsg$2(err) } });
|
|
58341
59674
|
}
|
|
58342
59675
|
log.info("detection armed (session-time) — watching for motion", { meta: { motion: config.motionSources.join("+") } });
|
|
58343
59676
|
} else {
|
|
@@ -58358,14 +59691,14 @@ var DetectionWiringController = class {
|
|
|
58358
59691
|
try {
|
|
58359
59692
|
await this.deps.placement.dispatchCamera(runnerConfig);
|
|
58360
59693
|
} catch (err) {
|
|
58361
|
-
const msg = errMsg$
|
|
59694
|
+
const msg = errMsg$2(err);
|
|
58362
59695
|
log.error("dispatchCamera failed", { meta: { error: msg } });
|
|
58363
59696
|
}
|
|
58364
59697
|
}
|
|
58365
59698
|
try {
|
|
58366
59699
|
await this.deps.audio.resubscribe(deviceId, config);
|
|
58367
59700
|
} catch (err) {
|
|
58368
|
-
const msg = errMsg$
|
|
59701
|
+
const msg = errMsg$2(err);
|
|
58369
59702
|
log.error("subscribeAudioStream failed", { meta: { error: msg } });
|
|
58370
59703
|
}
|
|
58371
59704
|
if (config.detectionMode !== "on-motion") log.info("Detection dispatched", { meta: {
|
|
@@ -58381,7 +59714,7 @@ var DetectionWiringController = class {
|
|
|
58381
59714
|
try {
|
|
58382
59715
|
await this.deps.placement.releaseCamera({ deviceId });
|
|
58383
59716
|
} catch (err) {
|
|
58384
|
-
const msg = errMsg$
|
|
59717
|
+
const msg = errMsg$2(err);
|
|
58385
59718
|
this.deps.logger.error("releaseCamera failed", {
|
|
58386
59719
|
tags: { deviceId },
|
|
58387
59720
|
meta: { error: msg }
|
|
@@ -58407,7 +59740,7 @@ var DetectionWiringController = class {
|
|
|
58407
59740
|
} catch (err) {
|
|
58408
59741
|
this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
|
|
58409
59742
|
tags: { deviceId },
|
|
58410
|
-
meta: { error: errMsg$
|
|
59743
|
+
meta: { error: errMsg$2(err) }
|
|
58411
59744
|
});
|
|
58412
59745
|
}
|
|
58413
59746
|
this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
|
|
@@ -58431,7 +59764,7 @@ var DetectionWiringController = class {
|
|
|
58431
59764
|
} catch (err) {
|
|
58432
59765
|
this.deps.logger.debug("device existence check failed — proceeding as before", {
|
|
58433
59766
|
tags: { deviceId },
|
|
58434
|
-
meta: { error: errMsg$
|
|
59767
|
+
meta: { error: errMsg$2(err) }
|
|
58435
59768
|
});
|
|
58436
59769
|
return false;
|
|
58437
59770
|
}
|
|
@@ -58548,7 +59881,7 @@ var DetectionWiringController = class {
|
|
|
58548
59881
|
await this.deps.attachOn(assignment.agentNodeId, nextConfig);
|
|
58549
59882
|
log.info("pipeline.camera-updated: hot-reloaded", { tags: { nodeId: assignment.agentNodeId } });
|
|
58550
59883
|
} catch (err) {
|
|
58551
|
-
const msg = errMsg$
|
|
59884
|
+
const msg = errMsg$2(err);
|
|
58552
59885
|
log.error("pipeline.camera-updated: attachOn failed", {
|
|
58553
59886
|
tags: { nodeId: assignment.agentNodeId },
|
|
58554
59887
|
meta: { error: msg }
|
|
@@ -58624,7 +59957,7 @@ var DetectionWiringController = class {
|
|
|
58624
59957
|
this.deps.audio.resubscribe(deviceId, audioCfg).catch((err) => {
|
|
58625
59958
|
this.deps.logger.warn("watchdog audio re-subscribe failed", {
|
|
58626
59959
|
tags: { deviceId },
|
|
58627
|
-
meta: { error: errMsg$
|
|
59960
|
+
meta: { error: errMsg$2(err) }
|
|
58628
59961
|
});
|
|
58629
59962
|
});
|
|
58630
59963
|
}
|
|
@@ -58718,6 +60051,35 @@ function pickInferenceDevice(input) {
|
|
|
58718
60051
|
const loadByKey = new Map((input.loads ?? []).map((l) => [l.deviceKey, l]));
|
|
58719
60052
|
const healthyKeys = eligibleKeys.filter((k) => !isDeviceSaturated(loadByKey.get(k)));
|
|
58720
60053
|
const consideredKeys = pin !== null || healthyKeys.length === 0 || healthyKeys.length === eligibleKeys.length ? eligibleKeys : healthyKeys;
|
|
60054
|
+
if (input.policy === "share" && pin === null) {
|
|
60055
|
+
const election = electByShare(eligibleKeys.map((key) => {
|
|
60056
|
+
const load = loadByKey.get(key);
|
|
60057
|
+
const attached = load?.attachedCameras ?? 0;
|
|
60058
|
+
const cap = input.caps?.[key];
|
|
60059
|
+
const hasCap = cap !== null && cap !== void 0 && cap > 0;
|
|
60060
|
+
return {
|
|
60061
|
+
key,
|
|
60062
|
+
share: input.eligible[key] ?? 1,
|
|
60063
|
+
running: attached,
|
|
60064
|
+
capped: hasCap && attached >= cap,
|
|
60065
|
+
saturated: isDeviceSaturated(load),
|
|
60066
|
+
...hasCap ? { spare: Math.max(0, cap - attached) } : {}
|
|
60067
|
+
};
|
|
60068
|
+
}));
|
|
60069
|
+
if (election.kind === "none") return {
|
|
60070
|
+
kind: "default-pool",
|
|
60071
|
+
reason: "all-devices-at-cap",
|
|
60072
|
+
eligibleKeys
|
|
60073
|
+
};
|
|
60074
|
+
return {
|
|
60075
|
+
kind: "selected",
|
|
60076
|
+
deviceKey: election.key,
|
|
60077
|
+
reason: "share",
|
|
60078
|
+
tieBreak: null,
|
|
60079
|
+
eligibleKeys,
|
|
60080
|
+
tiedKeys: [election.key]
|
|
60081
|
+
};
|
|
60082
|
+
}
|
|
58721
60083
|
const balancerInput = {
|
|
58722
60084
|
nodes: consideredKeys.map((k) => deviceAsLoad(k, loadByKey.get(k))),
|
|
58723
60085
|
preferredAgent: pin,
|
|
@@ -58823,7 +60185,7 @@ var DeviceConfigContributions = class {
|
|
|
58823
60185
|
issue: configIssue
|
|
58824
60186
|
} });
|
|
58825
60187
|
this.deps.topology.refreshNodeCapabilities().then(() => this.deps.redispatchAllActiveCameras("ingest-owner-changed")).catch((err) => {
|
|
58826
|
-
this.deps.ctx().logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg$
|
|
60188
|
+
this.deps.ctx().logger.warn("ingest-owner-changed re-dispatch failed", { meta: { error: errMsg$2(err) } });
|
|
58827
60189
|
});
|
|
58828
60190
|
}
|
|
58829
60191
|
this.deps.clearOnSettingsChange();
|
|
@@ -59277,10 +60639,25 @@ function rootRefusal(input, droppedRoots) {
|
|
|
59277
60639
|
droppedRoots
|
|
59278
60640
|
};
|
|
59279
60641
|
}
|
|
60642
|
+
/**
|
|
60643
|
+
* Whose provisioning an attach applies. The elected device when there is one.
|
|
60644
|
+
* With NO elected device the runner's default pool serves the camera; when the
|
|
60645
|
+
* node has exactly one eligible device the balancer returns
|
|
60646
|
+
* `single-eligible-device` and that default pool IS that device (a Mac's
|
|
60647
|
+
* `coreml:all`, an N100 with only its iGPU on), so its model pin and the
|
|
60648
|
+
* camera's override stored under its key apply (D745 — before, the empty key
|
|
60649
|
+
* was read, which no store writes, and the pin was silently never run).
|
|
60650
|
+
* Otherwise `''`: the node's folded settings with no hardware fields.
|
|
60651
|
+
*/
|
|
60652
|
+
function provisioningKeyFor(input) {
|
|
60653
|
+
if (input.deviceKey !== void 0) return input.deviceKey;
|
|
60654
|
+
const eligibleKeys = Object.keys(input.eligible);
|
|
60655
|
+
return eligibleKeys.length === 1 ? eligibleKeys[0] ?? "" : "";
|
|
60656
|
+
}
|
|
59280
60657
|
/** Steps 3 of the module header: provision the resolved structure. */
|
|
59281
60658
|
async function provisionAttachPipeline(input, roster, resolved, store) {
|
|
59282
60659
|
const { deviceId, nodeId, deviceKey } = input;
|
|
59283
|
-
const electedKey =
|
|
60660
|
+
const electedKey = provisioningKeyFor(input);
|
|
59284
60661
|
const rosterKeys = (roster ?? []).map((d) => d.deviceKey);
|
|
59285
60662
|
const sources = await store.readDeviceProvisioning(deviceId, nodeId, [...new Set([electedKey, ...rosterKeys])]);
|
|
59286
60663
|
const elected = sources.get(electedKey);
|
|
@@ -59888,7 +61265,7 @@ var PlacementService = class {
|
|
|
59888
61265
|
async releaseCamera(input) {
|
|
59889
61266
|
const current = this.deps.ledger.getAssignment(input.deviceId);
|
|
59890
61267
|
if (current) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
59891
|
-
const msg = errMsg$
|
|
61268
|
+
const msg = errMsg$2(err);
|
|
59892
61269
|
this.deps.ctx().logger.debug("releaseCamera detach failed", {
|
|
59893
61270
|
tags: { deviceId: input.deviceId },
|
|
59894
61271
|
meta: { error: msg }
|
|
@@ -59916,7 +61293,7 @@ var PlacementService = class {
|
|
|
59916
61293
|
pipelineNodeId: input.agentNodeId,
|
|
59917
61294
|
[PREFERRED_AGENT_SETTING]: null
|
|
59918
61295
|
}).catch((err) => {
|
|
59919
|
-
const msg = errMsg$
|
|
61296
|
+
const msg = errMsg$2(err);
|
|
59920
61297
|
ctx.logger.warn("assignPipeline: failed to persist pin", {
|
|
59921
61298
|
tags: { deviceId: input.deviceId },
|
|
59922
61299
|
meta: { error: msg }
|
|
@@ -59948,7 +61325,7 @@ var PlacementService = class {
|
|
|
59948
61325
|
}
|
|
59949
61326
|
const current = this.deps.ledger.getAssignment(input.deviceId);
|
|
59950
61327
|
if (current && current.agentNodeId !== input.agentNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
59951
|
-
const msg = errMsg$
|
|
61328
|
+
const msg = errMsg$2(err);
|
|
59952
61329
|
ctx.logger.debug("assignPipeline detach-old failed", {
|
|
59953
61330
|
tags: { deviceId: input.deviceId },
|
|
59954
61331
|
meta: { error: msg }
|
|
@@ -59973,7 +61350,7 @@ var PlacementService = class {
|
|
|
59973
61350
|
await ctx.settings?.writeDeviceStore(input.deviceId, { pipelineDeviceKey: clearing ? "auto" : input.deviceKey }).catch((err) => {
|
|
59974
61351
|
ctx.logger.warn("setPipelineDevicePin: failed to persist device pin", {
|
|
59975
61352
|
tags: { deviceId: input.deviceId },
|
|
59976
|
-
meta: { error: errMsg$
|
|
61353
|
+
meta: { error: errMsg$2(err) }
|
|
59977
61354
|
});
|
|
59978
61355
|
});
|
|
59979
61356
|
const sessionNode = this.deps.activeSessionNode(input.deviceId);
|
|
@@ -59996,7 +61373,7 @@ var PlacementService = class {
|
|
|
59996
61373
|
[PREFERRED_AGENT_SETTING]: null,
|
|
59997
61374
|
pipelineNodeId: "auto"
|
|
59998
61375
|
}).catch((err) => {
|
|
59999
|
-
const msg = errMsg$
|
|
61376
|
+
const msg = errMsg$2(err);
|
|
60000
61377
|
ctx.logger.warn("unassignPipeline: failed to clear pin", {
|
|
60001
61378
|
tags: { deviceId: input.deviceId },
|
|
60002
61379
|
meta: { error: msg }
|
|
@@ -60017,7 +61394,7 @@ var PlacementService = class {
|
|
|
60017
61394
|
else {
|
|
60018
61395
|
const targetNodeId = decision.agentNodeId;
|
|
60019
61396
|
if (current && current.agentNodeId !== targetNodeId) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
60020
|
-
const msg = errMsg$
|
|
61397
|
+
const msg = errMsg$2(err);
|
|
60021
61398
|
ctx.logger.debug("unassignPipeline detach-old failed", {
|
|
60022
61399
|
tags: { deviceId: input.deviceId },
|
|
60023
61400
|
meta: { error: msg }
|
|
@@ -60037,7 +61414,7 @@ var PlacementService = class {
|
|
|
60037
61414
|
}
|
|
60038
61415
|
}
|
|
60039
61416
|
if (current) await this.detachOn(current.agentNodeId, input.deviceId).catch((err) => {
|
|
60040
|
-
const msg = errMsg$
|
|
61417
|
+
const msg = errMsg$2(err);
|
|
60041
61418
|
ctx.logger.debug("unassignPipeline detach failed", {
|
|
60042
61419
|
tags: { deviceId: input.deviceId },
|
|
60043
61420
|
meta: { error: msg }
|
|
@@ -60056,7 +61433,7 @@ var PlacementService = class {
|
|
|
60056
61433
|
async runAutoRebalance() {
|
|
60057
61434
|
if (!this.deps.ctxIfReady()) return;
|
|
60058
61435
|
await this.deps.reconcilePlacementFromRunners().catch((err) => {
|
|
60059
|
-
this.deps.ctx().logger.debug("auto-rebalance: attachment reconcile failed", { meta: { error: errMsg$
|
|
61436
|
+
this.deps.ctx().logger.debug("auto-rebalance: attachment reconcile failed", { meta: { error: errMsg$2(err) } });
|
|
60060
61437
|
});
|
|
60061
61438
|
if (this.autoRebalanceInFlight) return;
|
|
60062
61439
|
if (this.deps.topology.enabledNodes.length < 2) return;
|
|
@@ -60065,7 +61442,7 @@ var PlacementService = class {
|
|
|
60065
61442
|
const { migrated } = await this.rebalance({ minImprovement: AUTO_REBALANCE_MIN_IMPROVEMENT });
|
|
60066
61443
|
if (migrated > 0) this.deps.ctx().logger.info("auto-rebalance: corrected placement drift", { meta: { migrated } });
|
|
60067
61444
|
} catch (err) {
|
|
60068
|
-
this.deps.ctx().logger.debug("auto-rebalance pass failed", { meta: { error: errMsg$
|
|
61445
|
+
this.deps.ctx().logger.debug("auto-rebalance pass failed", { meta: { error: errMsg$2(err) } });
|
|
60069
61446
|
} finally {
|
|
60070
61447
|
this.autoRebalanceInFlight = false;
|
|
60071
61448
|
}
|
|
@@ -60137,7 +61514,7 @@ var PlacementService = class {
|
|
|
60137
61514
|
const targetConfig = withAttachPipeline(config, placement.pipeline);
|
|
60138
61515
|
if (current) {
|
|
60139
61516
|
await this.detachOn(current.agentNodeId, deviceId).catch((err) => {
|
|
60140
|
-
const msg = errMsg$
|
|
61517
|
+
const msg = errMsg$2(err);
|
|
60141
61518
|
ctx.logger.debug("rebalance detach-old failed", {
|
|
60142
61519
|
tags: { deviceId },
|
|
60143
61520
|
meta: { error: msg }
|
|
@@ -60542,7 +61919,7 @@ var NodeLifecycleHandler = class {
|
|
|
60542
61919
|
nodeId: decision.agentNodeId
|
|
60543
61920
|
} });
|
|
60544
61921
|
} catch (err) {
|
|
60545
|
-
const msg = errMsg$
|
|
61922
|
+
const msg = errMsg$2(err);
|
|
60546
61923
|
this.deps.logger.error("Failover: attach failed", {
|
|
60547
61924
|
tags: {
|
|
60548
61925
|
deviceId,
|
|
@@ -60572,7 +61949,7 @@ var NodeLifecycleHandler = class {
|
|
|
60572
61949
|
this.deps.schedulePendingRetry();
|
|
60573
61950
|
if (failoverPolicy.onReconnect === "rebalance") {
|
|
60574
61951
|
await this.deps.rebalance().catch((err) => {
|
|
60575
|
-
const msg = errMsg$
|
|
61952
|
+
const msg = errMsg$2(err);
|
|
60576
61953
|
this.deps.logger.error("rebalance after reconnect failed", { meta: { error: msg } });
|
|
60577
61954
|
});
|
|
60578
61955
|
return;
|
|
@@ -60598,7 +61975,7 @@ var NodeLifecycleHandler = class {
|
|
|
60598
61975
|
this.deps.ledger.recordAssignment(deviceId, nodeId, "failover", false);
|
|
60599
61976
|
restored++;
|
|
60600
61977
|
} catch (err) {
|
|
60601
|
-
const msg = errMsg$
|
|
61978
|
+
const msg = errMsg$2(err);
|
|
60602
61979
|
this.deps.logger.error("restore attach failed", {
|
|
60603
61980
|
tags: {
|
|
60604
61981
|
deviceId,
|
|
@@ -60692,7 +62069,7 @@ var NodeLifecycleHandler = class {
|
|
|
60692
62069
|
deviceId,
|
|
60693
62070
|
nodeId
|
|
60694
62071
|
},
|
|
60695
|
-
meta: { error: errMsg$
|
|
62072
|
+
meta: { error: errMsg$2(err) }
|
|
60696
62073
|
});
|
|
60697
62074
|
}
|
|
60698
62075
|
this.deps.logger.info("readiness: detection-pipeline reseeded agent defaults + redispatched cameras", {
|
|
@@ -60706,7 +62083,7 @@ var NodeLifecycleHandler = class {
|
|
|
60706
62083
|
this.reattachStaleEmpties(nodeId).catch((err) => {
|
|
60707
62084
|
this.deps.logger.debug("readiness deferred sweep failed", {
|
|
60708
62085
|
tags: { nodeId },
|
|
60709
|
-
meta: { error: errMsg$
|
|
62086
|
+
meta: { error: errMsg$2(err) }
|
|
60710
62087
|
});
|
|
60711
62088
|
});
|
|
60712
62089
|
}, 3e3);
|
|
@@ -60717,7 +62094,7 @@ var NodeLifecycleHandler = class {
|
|
|
60717
62094
|
tags: { nodeId },
|
|
60718
62095
|
meta: {
|
|
60719
62096
|
epoch: t.epoch,
|
|
60720
|
-
error: errMsg$
|
|
62097
|
+
error: errMsg$2(err)
|
|
60721
62098
|
}
|
|
60722
62099
|
});
|
|
60723
62100
|
}
|
|
@@ -60796,7 +62173,7 @@ var NodeLifecycleHandler = class {
|
|
|
60796
62173
|
deviceId,
|
|
60797
62174
|
nodeId
|
|
60798
62175
|
},
|
|
60799
|
-
meta: { error: errMsg$
|
|
62176
|
+
meta: { error: errMsg$2(err) }
|
|
60800
62177
|
});
|
|
60801
62178
|
}
|
|
60802
62179
|
}
|
|
@@ -61118,7 +62495,7 @@ var NodeStressController = class NodeStressController {
|
|
|
61118
62495
|
if (this.timer) return;
|
|
61119
62496
|
this.timer = setInterval(() => {
|
|
61120
62497
|
this.sweep().catch((err) => {
|
|
61121
|
-
this.deps.logger.warn("node-stress sweep failed", { meta: { error: errMsg$
|
|
62498
|
+
this.deps.logger.warn("node-stress sweep failed", { meta: { error: errMsg$2(err) } });
|
|
61122
62499
|
});
|
|
61123
62500
|
}, NodeStressController.SWEEP_INTERVAL_MS);
|
|
61124
62501
|
}
|
|
@@ -61291,7 +62668,7 @@ var NodeStressController = class NodeStressController {
|
|
|
61291
62668
|
meta: {
|
|
61292
62669
|
from: plan.fromNodeId,
|
|
61293
62670
|
to: plan.toNodeId,
|
|
61294
|
-
error: errMsg$
|
|
62671
|
+
error: errMsg$2(err)
|
|
61295
62672
|
}
|
|
61296
62673
|
});
|
|
61297
62674
|
} finally {
|
|
@@ -61988,7 +63365,7 @@ var NodeTopologyService = class {
|
|
|
61988
63365
|
try {
|
|
61989
63366
|
blob = await this.deps.readAgentSettingsMap();
|
|
61990
63367
|
} catch (err) {
|
|
61991
|
-
this.deps.logger.warn("refreshNodeCapabilities: agent settings read failed", { meta: { error: errMsg$
|
|
63368
|
+
this.deps.logger.warn("refreshNodeCapabilities: agent settings read failed", { meta: { error: errMsg$2(err) } });
|
|
61992
63369
|
}
|
|
61993
63370
|
const nodeIds = new Set(["hub"]);
|
|
61994
63371
|
for (const nodeId of Object.keys(blob)) if (typeof nodeId === "string" && nodeId.length > 0 && !nodeId.includes("/")) nodeIds.add(nodeId);
|
|
@@ -62162,7 +63539,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62162
63539
|
const unsubInferenceResult = deps.eventBus.subscribe({ category: EventCategory.PipelineInferenceResult }, (event) => {
|
|
62163
63540
|
if (!isEvent(event, EventCategory.PipelineInferenceResult)) return;
|
|
62164
63541
|
deps.handleInferenceResult(event.data).catch((err) => {
|
|
62165
|
-
const msg = errMsg$
|
|
63542
|
+
const msg = errMsg$2(err);
|
|
62166
63543
|
deps.logger.error("PipelineInferenceResult post-processing failed", {
|
|
62167
63544
|
tags: { deviceId: event.data.deviceId },
|
|
62168
63545
|
meta: { error: msg }
|
|
@@ -62181,7 +63558,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62181
63558
|
deps.logger.info("received onProfileSlotsChanged", { tags: { deviceId } });
|
|
62182
63559
|
deps.handleDeviceRegistered(deviceId).catch((err) => {
|
|
62183
63560
|
if (isTornDown) return;
|
|
62184
|
-
const msg = errMsg$
|
|
63561
|
+
const msg = errMsg$2(err);
|
|
62185
63562
|
deps.logger.error("handleDeviceRegistered failed", {
|
|
62186
63563
|
tags: { deviceId },
|
|
62187
63564
|
meta: { error: msg }
|
|
@@ -62193,7 +63570,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62193
63570
|
const unsubDeviceUnregistered = deps.eventBus.subscribe({ category: EventCategory.DeviceUnregistered }, (event) => {
|
|
62194
63571
|
const { deviceId } = event.data;
|
|
62195
63572
|
deps.handleDeviceUnregistered(deviceId).catch((err) => {
|
|
62196
|
-
const msg = errMsg$
|
|
63573
|
+
const msg = errMsg$2(err);
|
|
62197
63574
|
deps.logger.error("handleDeviceUnregistered failed", {
|
|
62198
63575
|
tags: { deviceId },
|
|
62199
63576
|
meta: { error: msg }
|
|
@@ -62206,7 +63583,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62206
63583
|
deps.cameraSensors.requestCamera(deviceId, "settings-updated");
|
|
62207
63584
|
deps.handleSettingsChanged(deviceId).catch((err) => {
|
|
62208
63585
|
if (isTornDown) return;
|
|
62209
|
-
const msg = errMsg$
|
|
63586
|
+
const msg = errMsg$2(err);
|
|
62210
63587
|
deps.logger.error("handleSettingsChanged failed", {
|
|
62211
63588
|
tags: { deviceId },
|
|
62212
63589
|
meta: { error: msg }
|
|
@@ -62216,7 +63593,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62216
63593
|
const unsubCameraUpdated = deps.eventBus.subscribe({ category: EventCategory.PipelineCameraUpdated }, (event) => {
|
|
62217
63594
|
if (!isEvent(event, EventCategory.PipelineCameraUpdated)) return;
|
|
62218
63595
|
deps.handlePipelineCameraUpdated(event.data.deviceId, event.data.config).catch((err) => {
|
|
62219
|
-
const msg = errMsg$
|
|
63596
|
+
const msg = errMsg$2(err);
|
|
62220
63597
|
deps.logger.error("handlePipelineCameraUpdated failed", {
|
|
62221
63598
|
tags: { deviceId: event.data.deviceId },
|
|
62222
63599
|
meta: { error: msg }
|
|
@@ -62229,7 +63606,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62229
63606
|
if (!(reason === "wrapper-activated" || reason === "wrapper-deactivated") || !(capName === "detection-pipeline" || capName === "audio-analysis" || capName === "motion-detection")) return;
|
|
62230
63607
|
deps.handleSettingsChanged(deviceId).catch((err) => {
|
|
62231
63608
|
if (isTornDown) return;
|
|
62232
|
-
const msg = errMsg$
|
|
63609
|
+
const msg = errMsg$2(err);
|
|
62233
63610
|
deps.logger.error("binding-changed re-dispatch failed", {
|
|
62234
63611
|
tags: { deviceId },
|
|
62235
63612
|
meta: {
|
|
@@ -62257,7 +63634,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62257
63634
|
tags: { deviceId },
|
|
62258
63635
|
meta: {
|
|
62259
63636
|
detected,
|
|
62260
|
-
error: errMsg$
|
|
63637
|
+
error: errMsg$2(err)
|
|
62261
63638
|
}
|
|
62262
63639
|
});
|
|
62263
63640
|
});
|
|
@@ -62304,7 +63681,7 @@ function wireOrchestratorSubscriptions(deps) {
|
|
|
62304
63681
|
tags: { deviceId },
|
|
62305
63682
|
meta: {
|
|
62306
63683
|
detected,
|
|
62307
|
-
error: errMsg$
|
|
63684
|
+
error: errMsg$2(err)
|
|
62308
63685
|
}
|
|
62309
63686
|
});
|
|
62310
63687
|
});
|
|
@@ -62570,7 +63947,7 @@ async function reattachAfterPackageZoneChange(deps, deviceId) {
|
|
|
62570
63947
|
deviceId,
|
|
62571
63948
|
nodeId
|
|
62572
63949
|
},
|
|
62573
|
-
meta: { error: errMsg$
|
|
63950
|
+
meta: { error: errMsg$2(err) }
|
|
62574
63951
|
});
|
|
62575
63952
|
}
|
|
62576
63953
|
}
|
|
@@ -62851,7 +64228,7 @@ async function resetRemovedModelPins(deps, trigger) {
|
|
|
62851
64228
|
} catch (err) {
|
|
62852
64229
|
deps.logger.warn("removed-model pin reset skipped — custom model registry unreadable", { meta: {
|
|
62853
64230
|
trigger,
|
|
62854
|
-
error: errMsg$
|
|
64231
|
+
error: errMsg$2(err)
|
|
62855
64232
|
} });
|
|
62856
64233
|
return [];
|
|
62857
64234
|
}
|
|
@@ -62870,7 +64247,7 @@ async function resetRemovedModelPins(deps, trigger) {
|
|
|
62870
64247
|
} catch (err) {
|
|
62871
64248
|
deps.logger.warn("removed-model pin reset failed", { meta: {
|
|
62872
64249
|
trigger,
|
|
62873
|
-
error: errMsg$
|
|
64250
|
+
error: errMsg$2(err)
|
|
62874
64251
|
} });
|
|
62875
64252
|
return [];
|
|
62876
64253
|
}
|
|
@@ -63026,7 +64403,6 @@ var StoredCameraPipelineForAgentSchema = object({
|
|
|
63026
64403
|
}).nullable()
|
|
63027
64404
|
});
|
|
63028
64405
|
var StoredCameraPipelineSettingsSchema = object({
|
|
63029
|
-
pinnedAgentNodeId: string().optional(),
|
|
63030
64406
|
stepToggles: record(string(), boolean()).optional(),
|
|
63031
64407
|
stepOverridesByDevice: record(string(), record(string(), record(string(), StoredCameraStepOverridePatchSchema))).optional(),
|
|
63032
64408
|
stepOverridesByAgent: record(string(), record(string(), LegacyCameraStepOverridePatchSchema)).optional(),
|
|
@@ -63229,6 +64605,21 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
63229
64605
|
});
|
|
63230
64606
|
}
|
|
63231
64607
|
/**
|
|
64608
|
+
* Rewrite the whole agent-settings map in ONE read-modify-write under
|
|
64609
|
+
* {@link blobWriteLock} — atomic across every node of the blob (Best
|
|
64610
|
+
* settings, D745, writes several nodes' devices at once). `transform` must
|
|
64611
|
+
* be pure: it may run on a fresh read.
|
|
64612
|
+
*/
|
|
64613
|
+
async rewriteAgentSettingsMap(transform) {
|
|
64614
|
+
await this.blobWriteLock.run(PipelineSettingsStore.AGENT_SETTINGS_LOCK_KEY, async () => {
|
|
64615
|
+
await this.agentSettingsState.set(transform(await this.readAgentSettingsMap()));
|
|
64616
|
+
});
|
|
64617
|
+
}
|
|
64618
|
+
/** Rewrite the whole camera-settings map in one durable `update()` (D745). */
|
|
64619
|
+
async rewriteCameraSettingsMap(transform) {
|
|
64620
|
+
await this.cameraSettingsState.update(transform);
|
|
64621
|
+
}
|
|
64622
|
+
/**
|
|
63232
64623
|
* Drop one agent's persisted settings entry. Returns whether an entry
|
|
63233
64624
|
* existed (`false` also when the store is already disposed — the locked
|
|
63234
64625
|
* section becomes a no-op then). Serialized via {@link blobWriteLock} for
|
|
@@ -63485,6 +64876,10 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
63485
64876
|
* epochs and operator resets — a bound, not a budget.
|
|
63486
64877
|
*/
|
|
63487
64878
|
static CATALOG_FENCE_REREADS = 3;
|
|
64879
|
+
/** The node's catalog, read through the cache (awaits the node when cold). */
|
|
64880
|
+
async readCatalogForNode(nodeId) {
|
|
64881
|
+
return this.getCatalogForAgent(nodeId);
|
|
64882
|
+
}
|
|
63488
64883
|
async getCatalogForAgent(nodeId) {
|
|
63489
64884
|
const api = this.deps.api();
|
|
63490
64885
|
if (!api) return null;
|
|
@@ -63502,7 +64897,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
63502
64897
|
this.catalogReadFailing.add(nodeId);
|
|
63503
64898
|
this.deps.logger.warn("node catalog read failed — placements on it wait or stay pending", {
|
|
63504
64899
|
tags: { nodeId },
|
|
63505
|
-
meta: { error: errMsg$
|
|
64900
|
+
meta: { error: errMsg$2(err) }
|
|
63506
64901
|
});
|
|
63507
64902
|
}
|
|
63508
64903
|
return null;
|
|
@@ -63589,7 +64984,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
63589
64984
|
tags: { deviceId },
|
|
63590
64985
|
meta: {
|
|
63591
64986
|
capName: "detection-pipeline",
|
|
63592
|
-
error: errMsg$
|
|
64987
|
+
error: errMsg$2(err)
|
|
63593
64988
|
}
|
|
63594
64989
|
});
|
|
63595
64990
|
return true;
|
|
@@ -64422,7 +65817,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64422
65817
|
try {
|
|
64423
65818
|
slots = await api.streamBroker.listAllProfileSlots.query();
|
|
64424
65819
|
} catch (err) {
|
|
64425
|
-
this.deps.logger.debug("dispatch reconcile skipped — stream-broker not ready", { meta: { error: errMsg$
|
|
65820
|
+
this.deps.logger.debug("dispatch reconcile skipped — stream-broker not ready", { meta: { error: errMsg$2(err) } });
|
|
64426
65821
|
return;
|
|
64427
65822
|
}
|
|
64428
65823
|
const desired = desiredDeviceIdsFromSlots(slots);
|
|
@@ -64449,7 +65844,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64449
65844
|
} catch (err) {
|
|
64450
65845
|
this.deps.logger.warn("dispatch reconcile: handleDeviceRegistered failed", {
|
|
64451
65846
|
tags: { deviceId },
|
|
64452
|
-
meta: { error: errMsg$
|
|
65847
|
+
meta: { error: errMsg$2(err) }
|
|
64453
65848
|
});
|
|
64454
65849
|
}
|
|
64455
65850
|
await this.reconcilePlacementFromRunners();
|
|
@@ -64477,7 +65872,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64477
65872
|
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
64478
65873
|
return new Set(devices.map((device) => device.id));
|
|
64479
65874
|
} catch (err) {
|
|
64480
|
-
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg$
|
|
65875
|
+
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg$2(err) } });
|
|
64481
65876
|
return null;
|
|
64482
65877
|
}
|
|
64483
65878
|
}
|
|
@@ -64503,7 +65898,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64503
65898
|
} catch (err) {
|
|
64504
65899
|
this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
|
|
64505
65900
|
tags: { deviceId },
|
|
64506
|
-
meta: { error: errMsg$
|
|
65901
|
+
meta: { error: errMsg$2(err) }
|
|
64507
65902
|
});
|
|
64508
65903
|
}
|
|
64509
65904
|
}
|
|
@@ -64560,7 +65955,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64560
65955
|
this.deps.logger.debug("placement reconcile skipped node — getLocalCameras failed", {
|
|
64561
65956
|
tags: { nodeId },
|
|
64562
65957
|
meta: {
|
|
64563
|
-
error: outcome.kind === "rejected" ? errMsg$
|
|
65958
|
+
error: outcome.kind === "rejected" ? errMsg$2(outcome.error) : "budget exceeded",
|
|
64564
65959
|
ms
|
|
64565
65960
|
}
|
|
64566
65961
|
});
|
|
@@ -64590,7 +65985,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64590
65985
|
nodeId,
|
|
64591
65986
|
deviceId
|
|
64592
65987
|
},
|
|
64593
|
-
meta: { error: errMsg$
|
|
65988
|
+
meta: { error: errMsg$2(err) }
|
|
64594
65989
|
});
|
|
64595
65990
|
})));
|
|
64596
65991
|
}
|
|
@@ -64632,7 +66027,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64632
66027
|
this.deps.handleDeviceRegistered(deviceId).catch((err) => {
|
|
64633
66028
|
this.deps.logger.debug("slot-read retry: handleDeviceRegistered failed", {
|
|
64634
66029
|
tags: { deviceId },
|
|
64635
|
-
meta: { error: errMsg$
|
|
66030
|
+
meta: { error: errMsg$2(err) }
|
|
64636
66031
|
});
|
|
64637
66032
|
});
|
|
64638
66033
|
}, delay);
|
|
@@ -64710,7 +66105,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64710
66105
|
} catch (err) {
|
|
64711
66106
|
this.deps.logger.warn("pending retry: dispatchCamera failed", {
|
|
64712
66107
|
tags: { deviceId },
|
|
64713
|
-
meta: { error: errMsg$
|
|
66108
|
+
meta: { error: errMsg$2(err) }
|
|
64714
66109
|
});
|
|
64715
66110
|
stillPending++;
|
|
64716
66111
|
}
|
|
@@ -64788,7 +66183,7 @@ var ReconcileController = class ReconcileController {
|
|
|
64788
66183
|
await this.deps.detachOn(assignment.agentNodeId, deviceId).catch((err) => {
|
|
64789
66184
|
this.deps.logger.debug("remote-health exhausted-detach failed", {
|
|
64790
66185
|
tags: { deviceId },
|
|
64791
|
-
meta: { error: errMsg$
|
|
66186
|
+
meta: { error: errMsg$2(err) }
|
|
64792
66187
|
});
|
|
64793
66188
|
});
|
|
64794
66189
|
return;
|
|
@@ -64811,14 +66206,14 @@ var ReconcileController = class ReconcileController {
|
|
|
64811
66206
|
await this.deps.detachOn(assignment.agentNodeId, deviceId).catch((err) => {
|
|
64812
66207
|
this.deps.logger.warn("remote-health detach failed", {
|
|
64813
66208
|
tags: { deviceId },
|
|
64814
|
-
meta: { error: errMsg$
|
|
66209
|
+
meta: { error: errMsg$2(err) }
|
|
64815
66210
|
});
|
|
64816
66211
|
});
|
|
64817
66212
|
this.deps.ledger.dropAssignment(deviceId, "silent");
|
|
64818
66213
|
await this.deps.redispatchCamera(cfg).catch((err) => {
|
|
64819
66214
|
this.deps.logger.warn("remote-health re-dispatch failed", {
|
|
64820
66215
|
tags: { deviceId },
|
|
64821
|
-
meta: { error: errMsg$
|
|
66216
|
+
meta: { error: errMsg$2(err) }
|
|
64822
66217
|
});
|
|
64823
66218
|
});
|
|
64824
66219
|
}
|
|
@@ -64993,7 +66388,7 @@ var OccupancyRecheckController = class {
|
|
|
64993
66388
|
tags: { deviceId },
|
|
64994
66389
|
meta: {
|
|
64995
66390
|
deviceId,
|
|
64996
|
-
error: errMsg$
|
|
66391
|
+
error: errMsg$2(err)
|
|
64997
66392
|
}
|
|
64998
66393
|
});
|
|
64999
66394
|
});
|
|
@@ -65127,7 +66522,7 @@ var OccupancyRecheckController = class {
|
|
|
65127
66522
|
tags: { deviceId },
|
|
65128
66523
|
meta: {
|
|
65129
66524
|
deviceId,
|
|
65130
|
-
error: errMsg$
|
|
66525
|
+
error: errMsg$2(err)
|
|
65131
66526
|
}
|
|
65132
66527
|
});
|
|
65133
66528
|
});
|
|
@@ -65182,7 +66577,7 @@ var OccupancyRecheckController = class {
|
|
|
65182
66577
|
meta: {
|
|
65183
66578
|
deviceId,
|
|
65184
66579
|
nodeId,
|
|
65185
|
-
error: errMsg$
|
|
66580
|
+
error: errMsg$2(err),
|
|
65186
66581
|
kind
|
|
65187
66582
|
}
|
|
65188
66583
|
});
|
|
@@ -65191,7 +66586,7 @@ var OccupancyRecheckController = class {
|
|
|
65191
66586
|
}
|
|
65192
66587
|
const answer = await this.deps.requestBurstOn(nodeId, deviceId, frames, reasonTag).catch((err) => ({
|
|
65193
66588
|
started: false,
|
|
65194
|
-
error: errMsg$
|
|
66589
|
+
error: errMsg$2(err)
|
|
65195
66590
|
}));
|
|
65196
66591
|
if (!answer.started) {
|
|
65197
66592
|
const refusedBecause = "refusedBecause" in answer ? answer.refusedBecause : void 0;
|
|
@@ -65239,7 +66634,7 @@ var OccupancyRecheckController = class {
|
|
|
65239
66634
|
meta: {
|
|
65240
66635
|
deviceId,
|
|
65241
66636
|
nodeId,
|
|
65242
|
-
error: errMsg$
|
|
66637
|
+
error: errMsg$2(err)
|
|
65243
66638
|
}
|
|
65244
66639
|
});
|
|
65245
66640
|
});
|
|
@@ -65296,7 +66691,7 @@ var OccupancyRecheckController = class {
|
|
|
65296
66691
|
meta: {
|
|
65297
66692
|
deviceId,
|
|
65298
66693
|
nodeId: burst.nodeId,
|
|
65299
|
-
error: errMsg$
|
|
66694
|
+
error: errMsg$2(err)
|
|
65300
66695
|
}
|
|
65301
66696
|
});
|
|
65302
66697
|
});
|
|
@@ -65511,7 +66906,15 @@ var TtlMemo = class {
|
|
|
65511
66906
|
//#endregion
|
|
65512
66907
|
//#region src/detection-session-registry.ts
|
|
65513
66908
|
var DetectionSessionRegistry = class {
|
|
66909
|
+
onChange;
|
|
65514
66910
|
sessions = /* @__PURE__ */ new Map();
|
|
66911
|
+
/**
|
|
66912
|
+
* @param onChange called after every register / deregister / clear with the
|
|
66913
|
+
* running set — feeds the placement statistics (D748). Never on a no-op.
|
|
66914
|
+
*/
|
|
66915
|
+
constructor(onChange = () => {}) {
|
|
66916
|
+
this.onChange = onChange;
|
|
66917
|
+
}
|
|
65515
66918
|
register(deviceId, nodeId, startedAt, triggeredBy, deviceKey) {
|
|
65516
66919
|
const existing = this.sessions.get(deviceId);
|
|
65517
66920
|
if (existing?.teardownTimer) clearTimeout(existing.teardownTimer);
|
|
@@ -65524,6 +66927,7 @@ var DetectionSessionRegistry = class {
|
|
|
65524
66927
|
...deviceKey ? { deviceKey } : {}
|
|
65525
66928
|
};
|
|
65526
66929
|
this.sessions.set(deviceId, session);
|
|
66930
|
+
this.onChange(this.list());
|
|
65527
66931
|
return session;
|
|
65528
66932
|
}
|
|
65529
66933
|
get(deviceId) {
|
|
@@ -65537,6 +66941,7 @@ var DetectionSessionRegistry = class {
|
|
|
65537
66941
|
if (!session) return void 0;
|
|
65538
66942
|
if (session.teardownTimer) clearTimeout(session.teardownTimer);
|
|
65539
66943
|
this.sessions.delete(deviceId);
|
|
66944
|
+
this.onChange(this.list());
|
|
65540
66945
|
return session;
|
|
65541
66946
|
}
|
|
65542
66947
|
setTeardownTimer(deviceId, timer) {
|
|
@@ -65559,9 +66964,39 @@ var DetectionSessionRegistry = class {
|
|
|
65559
66964
|
}
|
|
65560
66965
|
clear() {
|
|
65561
66966
|
for (const session of this.sessions.values()) if (session.teardownTimer) clearTimeout(session.teardownTimer);
|
|
66967
|
+
const hadSessions = this.sessions.size > 0;
|
|
65562
66968
|
this.sessions.clear();
|
|
66969
|
+
if (hadSessions) this.onChange([]);
|
|
65563
66970
|
}
|
|
65564
66971
|
};
|
|
66972
|
+
//#endregion
|
|
66973
|
+
//#region src/session-share-placement.ts
|
|
66974
|
+
function nodeSaturated(load) {
|
|
66975
|
+
return load.devices.length > 0 && load.devices.every((d) => isDeviceSaturated(d));
|
|
66976
|
+
}
|
|
66977
|
+
function electNodeByShare(input) {
|
|
66978
|
+
return electByShare(input.loads.filter((l) => l.nodeId.length > 0 && input.eligible.includes(l.nodeId)).map((load) => {
|
|
66979
|
+
const cap = input.caps[load.nodeId];
|
|
66980
|
+
const hasCap = cap !== null && cap !== void 0 && cap > 0;
|
|
66981
|
+
const weight = input.weights[load.nodeId];
|
|
66982
|
+
return {
|
|
66983
|
+
key: load.nodeId,
|
|
66984
|
+
share: weight !== void 0 && weight > 0 ? weight : 1,
|
|
66985
|
+
running: input.runningByNode.get(load.nodeId) ?? 0,
|
|
66986
|
+
capped: hasCap && load.attachedCameras >= cap,
|
|
66987
|
+
saturated: nodeSaturated(load),
|
|
66988
|
+
...hasCap ? { spare: Math.max(0, cap - load.attachedCameras) } : {}
|
|
66989
|
+
};
|
|
66990
|
+
}));
|
|
66991
|
+
}
|
|
66992
|
+
/** Parallel sessions per node: sessions and standing cameras, each camera once. */
|
|
66993
|
+
function countRunningByNode(placements) {
|
|
66994
|
+
const seen = /* @__PURE__ */ new Map();
|
|
66995
|
+
for (const p of placements) if (!seen.has(p.deviceId)) seen.set(p.deviceId, p.nodeId);
|
|
66996
|
+
const out = /* @__PURE__ */ new Map();
|
|
66997
|
+
for (const nodeId of seen.values()) out.set(nodeId, (out.get(nodeId) ?? 0) + 1);
|
|
66998
|
+
return out;
|
|
66999
|
+
}
|
|
65565
67000
|
/**
|
|
65566
67001
|
* Minimum time a detection session stays open, whatever closes it.
|
|
65567
67002
|
*
|
|
@@ -65674,7 +67109,15 @@ var SessionDispatchController = class {
|
|
|
65674
67109
|
* (not `private`) — see the class doc for the two production/test reasons
|
|
65675
67110
|
* `index.ts` reaches through it directly.
|
|
65676
67111
|
*/
|
|
65677
|
-
sessionRegistry = new DetectionSessionRegistry()
|
|
67112
|
+
sessionRegistry = new DetectionSessionRegistry((sessions) => this.deps.placementStats?.noteSessions(sessions.map((s) => ({
|
|
67113
|
+
nodeId: s.nodeId,
|
|
67114
|
+
deviceKey: s.deviceKey ?? ""
|
|
67115
|
+
}))));
|
|
67116
|
+
/**
|
|
67117
|
+
* `deviceId|deviceKey` of device pins the session path has already said it
|
|
67118
|
+
* ignored — one line per stale pin, not one per session (D748).
|
|
67119
|
+
*/
|
|
67120
|
+
reportedStalePins = /* @__PURE__ */ new Set();
|
|
65678
67121
|
/**
|
|
65679
67122
|
* Per-device async mutex for session dispatch/teardown critical sections —
|
|
65680
67123
|
* backed by the generic `KeyedAsyncLock` (mirrors the audio controller's
|
|
@@ -65953,7 +67396,7 @@ var SessionDispatchController = class {
|
|
|
65953
67396
|
deviceId,
|
|
65954
67397
|
nodeId: session.nodeId
|
|
65955
67398
|
},
|
|
65956
|
-
meta: { error: errMsg$
|
|
67399
|
+
meta: { error: errMsg$2(err) }
|
|
65957
67400
|
});
|
|
65958
67401
|
});
|
|
65959
67402
|
this.deps.dropAssignment(deviceId, "silent");
|
|
@@ -66135,6 +67578,76 @@ var SessionDispatchController = class {
|
|
|
66135
67578
|
});
|
|
66136
67579
|
}
|
|
66137
67580
|
/**
|
|
67581
|
+
* The session's node. `least-loaded` (and any node pin, under either
|
|
67582
|
+
* policy) is `balance()` exactly as before. `share` holds the node shares
|
|
67583
|
+
* of PARALLEL sessions (D748): sessions now running and standing cameras
|
|
67584
|
+
* count toward their node, and a node at its cap or drowning yields —
|
|
67585
|
+
* logged with the reason, and counted against the node in the stats.
|
|
67586
|
+
*/
|
|
67587
|
+
electSessionNode(input) {
|
|
67588
|
+
const balanced = () => balance({
|
|
67589
|
+
nodes: input.loads,
|
|
67590
|
+
preferredAgent: input.preferredAgent,
|
|
67591
|
+
nodeCaps: input.nodeCaps,
|
|
67592
|
+
weights: input.weights,
|
|
67593
|
+
eligibleNodes: input.eligible
|
|
67594
|
+
});
|
|
67595
|
+
if (input.policy !== "share" || input.preferredAgent) return balanced();
|
|
67596
|
+
const running = countRunningByNode([...this.sessionRegistry.list().map((s) => ({
|
|
67597
|
+
deviceId: s.deviceId,
|
|
67598
|
+
nodeId: s.nodeId
|
|
67599
|
+
})), ...this.deps.ledger.listAssignments().map((a) => ({
|
|
67600
|
+
deviceId: a.deviceId,
|
|
67601
|
+
nodeId: a.agentNodeId
|
|
67602
|
+
}))]);
|
|
67603
|
+
const election = electNodeByShare({
|
|
67604
|
+
loads: input.loads,
|
|
67605
|
+
eligible: input.eligible,
|
|
67606
|
+
weights: input.weights,
|
|
67607
|
+
caps: input.nodeCaps,
|
|
67608
|
+
runningByNode: running
|
|
67609
|
+
});
|
|
67610
|
+
if (election.kind === "none") return balanced();
|
|
67611
|
+
if (election.reason === "yield") {
|
|
67612
|
+
this.deps.placementStats?.noteYield(election.turnKey);
|
|
67613
|
+
this.deps.logger.info("detection session: share turn yielded — the node had no room", {
|
|
67614
|
+
tags: {
|
|
67615
|
+
deviceId: input.deviceId,
|
|
67616
|
+
nodeId: election.key
|
|
67617
|
+
},
|
|
67618
|
+
meta: {
|
|
67619
|
+
turnNodeId: election.turnKey,
|
|
67620
|
+
reason: election.yieldReason
|
|
67621
|
+
}
|
|
67622
|
+
});
|
|
67623
|
+
}
|
|
67624
|
+
const load = input.loads.find((l) => l.nodeId === election.key);
|
|
67625
|
+
return {
|
|
67626
|
+
kind: "assigned",
|
|
67627
|
+
agentNodeId: election.key,
|
|
67628
|
+
reason: "capacity",
|
|
67629
|
+
score: load ? load.attachedCameras + load.queueDepthTotal : 0
|
|
67630
|
+
};
|
|
67631
|
+
}
|
|
67632
|
+
/**
|
|
67633
|
+
* Say ONCE that a camera's device pin names a device this node does not
|
|
67634
|
+
* offer (disabled, absent): the session path ignores it, and the Load
|
|
67635
|
+
* balancing tab offers to clear it. Never cleared here (D748).
|
|
67636
|
+
*/
|
|
67637
|
+
noteStalePin(deviceId, pinnedDeviceKey, enabledDevices) {
|
|
67638
|
+
if (pinnedDeviceKey === null || pinnedDeviceKey in enabledDevices) return;
|
|
67639
|
+
const key = `${deviceId}|${pinnedDeviceKey}`;
|
|
67640
|
+
if (this.reportedStalePins.has(key)) return;
|
|
67641
|
+
this.reportedStalePins.add(key);
|
|
67642
|
+
this.deps.logger.warn("device pin ignored — the pinned device is not enabled on the elected node", {
|
|
67643
|
+
tags: { deviceId },
|
|
67644
|
+
meta: {
|
|
67645
|
+
pin: pinnedDeviceKey,
|
|
67646
|
+
enabled: Object.keys(enabledDevices).join(",")
|
|
67647
|
+
}
|
|
67648
|
+
});
|
|
67649
|
+
}
|
|
67650
|
+
/**
|
|
66138
67651
|
* Dispatch a session-time detection attach for an on-motion camera whose
|
|
66139
67652
|
* `decideSessionAction` returned `'dispatch'`. Builds a session
|
|
66140
67653
|
* `RunnerCameraConfig` the same way `startDetection` does, but forces
|
|
@@ -66170,7 +67683,7 @@ var SessionDispatchController = class {
|
|
|
66170
67683
|
};
|
|
66171
67684
|
if (reconcileBudgetExceeded) log.warn("detection session: placement census overran its budget — placing without it", { meta: { budgetMs: 500 } });
|
|
66172
67685
|
else if (censusOutcome.kind === "rejected") log.warn("detection session: placement census failed — placing on the ledger as it is", { meta: {
|
|
66173
|
-
error: errMsg$
|
|
67686
|
+
error: errMsg$2(censusOutcome.error),
|
|
66174
67687
|
reconcileMs
|
|
66175
67688
|
} });
|
|
66176
67689
|
const balanceStartedAt = Date.now();
|
|
@@ -66185,12 +67698,17 @@ var SessionDispatchController = class {
|
|
|
66185
67698
|
const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
|
|
66186
67699
|
const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
|
|
66187
67700
|
if (usableEligible.length > 0 && usableEligible.length < allEligible.length) log.info("nodes dropped from session placement — no usable inference device", { meta: { dropped: allEligible.filter((n) => !usableEligible.includes(n)) } });
|
|
66188
|
-
const
|
|
66189
|
-
|
|
67701
|
+
const nodeCaps = await this.nodeCaps.get(CLUSTER_KEY);
|
|
67702
|
+
const weights = await this.nodeWeights.get(CLUSTER_KEY);
|
|
67703
|
+
const policy = this.deps.sessionPlacement?.() ?? "least-loaded";
|
|
67704
|
+
const decision = this.electSessionNode({
|
|
67705
|
+
deviceId,
|
|
67706
|
+
policy,
|
|
67707
|
+
loads,
|
|
66190
67708
|
preferredAgent,
|
|
66191
|
-
nodeCaps
|
|
66192
|
-
weights
|
|
66193
|
-
|
|
67709
|
+
nodeCaps,
|
|
67710
|
+
weights,
|
|
67711
|
+
eligible
|
|
66194
67712
|
});
|
|
66195
67713
|
if (!decision || decision.kind !== "assigned") {
|
|
66196
67714
|
log.warn("detection session: no eligible node — will retry on next motion", { meta: { reason: decision?.kind === "pending" ? decision.reason : "no-runners-online" } });
|
|
@@ -66203,12 +67721,14 @@ var SessionDispatchController = class {
|
|
|
66203
67721
|
const targetLoad = loads.find((n) => n.nodeId === targetNodeId);
|
|
66204
67722
|
const deviceCaps = await this.inferenceDeviceCaps.get(targetNodeId);
|
|
66205
67723
|
const pinnedDeviceKey = await this.deps.readPipelineDevicePin(deviceId);
|
|
67724
|
+
this.noteStalePin(deviceId, pinnedDeviceKey, enabledDevices);
|
|
66206
67725
|
const selection = pickInferenceDevice({
|
|
66207
67726
|
eligible: enabledDevices,
|
|
66208
67727
|
loads: targetLoad?.devices ?? [],
|
|
66209
67728
|
caps: deviceCaps,
|
|
66210
|
-
pinnedDeviceKey,
|
|
66211
|
-
rotation: this.deps.inferenceRotation
|
|
67729
|
+
pinnedDeviceKey: pinnedDeviceKey !== null && pinnedDeviceKey in enabledDevices ? pinnedDeviceKey : null,
|
|
67730
|
+
rotation: this.deps.inferenceRotation,
|
|
67731
|
+
policy
|
|
66212
67732
|
});
|
|
66213
67733
|
const deviceKey = selectedDeviceKey(selection);
|
|
66214
67734
|
log.info("inference device selection", {
|
|
@@ -66296,6 +67816,7 @@ var SessionDispatchController = class {
|
|
|
66296
67816
|
attachedAt = Date.now();
|
|
66297
67817
|
this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
|
|
66298
67818
|
this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), triggeredBy, deviceKey);
|
|
67819
|
+
this.deps.placementStats?.noteStart(targetNodeId, deviceKey ?? "");
|
|
66299
67820
|
if (this.sessionRefusals.delete(deviceId)) log.info("detection session refusal cleared — a session opened", {
|
|
66300
67821
|
tags: {
|
|
66301
67822
|
deviceId,
|
|
@@ -66364,7 +67885,7 @@ var SessionDispatchController = class {
|
|
|
66364
67885
|
await this.resolveSessionLifecycleLocked(deviceId, reason);
|
|
66365
67886
|
}).catch((err) => {
|
|
66366
67887
|
log.warn("detection session: lifecycle re-check failed", { meta: {
|
|
66367
|
-
error: errMsg$
|
|
67888
|
+
error: errMsg$2(err),
|
|
66368
67889
|
reason
|
|
66369
67890
|
} });
|
|
66370
67891
|
});
|
|
@@ -66469,7 +67990,7 @@ var SessionDispatchController = class {
|
|
|
66469
67990
|
log.warn("detection session: teardown detach failed", {
|
|
66470
67991
|
tags: { nodeId: session.nodeId },
|
|
66471
67992
|
meta: {
|
|
66472
|
-
error: errMsg$
|
|
67993
|
+
error: errMsg$2(err),
|
|
66473
67994
|
reason
|
|
66474
67995
|
}
|
|
66475
67996
|
});
|
|
@@ -66576,7 +68097,7 @@ var SessionDispatchController = class {
|
|
|
66576
68097
|
deviceId,
|
|
66577
68098
|
nodeId: session.nodeId
|
|
66578
68099
|
},
|
|
66579
|
-
meta: { error: errMsg$
|
|
68100
|
+
meta: { error: errMsg$2(err) }
|
|
66580
68101
|
});
|
|
66581
68102
|
});
|
|
66582
68103
|
this.sessionRegistry.deregister(deviceId);
|
|
@@ -66597,7 +68118,7 @@ var SessionDispatchController = class {
|
|
|
66597
68118
|
deviceId,
|
|
66598
68119
|
nodeId: watchNodeId
|
|
66599
68120
|
},
|
|
66600
|
-
meta: { error: errMsg$
|
|
68121
|
+
meta: { error: errMsg$2(err) }
|
|
66601
68122
|
});
|
|
66602
68123
|
});
|
|
66603
68124
|
this.motionWatchByDevice.delete(deviceId);
|
|
@@ -66780,7 +68301,7 @@ async function sweepMirror(deps, hydrator, cameraIds) {
|
|
|
66780
68301
|
tags: { deviceId },
|
|
66781
68302
|
meta: {
|
|
66782
68303
|
mirror: hydrator.mirror,
|
|
66783
|
-
error: errMsg$
|
|
68304
|
+
error: errMsg$2(err)
|
|
66784
68305
|
}
|
|
66785
68306
|
});
|
|
66786
68307
|
return "threw";
|
|
@@ -66820,7 +68341,7 @@ async function readCameraIds(deps) {
|
|
|
66820
68341
|
lastError = err;
|
|
66821
68342
|
}
|
|
66822
68343
|
if (Date.now() >= deadline) {
|
|
66823
|
-
deps.logger.warn("zones mirror boot hydration SKIPPED — camera list never became readable; mirror-only consumers keep whatever the last write left", { meta: { error: lastError === null ? "ctx.api unavailable" : errMsg$
|
|
68344
|
+
deps.logger.warn("zones mirror boot hydration SKIPPED — camera list never became readable; mirror-only consumers keep whatever the last write left", { meta: { error: lastError === null ? "ctx.api unavailable" : errMsg$2(lastError) } });
|
|
66824
68345
|
return null;
|
|
66825
68346
|
}
|
|
66826
68347
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
@@ -67441,6 +68962,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67441
68962
|
readGlobalSettings: () => globalSettings,
|
|
67442
68963
|
onCensus: (entries) => {
|
|
67443
68964
|
motionSliceGate?.noteCensus(entries);
|
|
68965
|
+
deps.onRunnerCensus(entries.flatMap((e) => e.load === null ? [] : [e.load]));
|
|
67444
68966
|
providerSliceGate?.refreshIfStale("runner-census");
|
|
67445
68967
|
}
|
|
67446
68968
|
});
|
|
@@ -67467,7 +68989,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67467
68989
|
} catch (err) {
|
|
67468
68990
|
return {
|
|
67469
68991
|
kind: "unknown",
|
|
67470
|
-
reason: errMsg$
|
|
68992
|
+
reason: errMsg$2(err)
|
|
67471
68993
|
};
|
|
67472
68994
|
}
|
|
67473
68995
|
};
|
|
@@ -67552,18 +69074,19 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67552
69074
|
readGlobalSettings: () => globalSettings
|
|
67553
69075
|
});
|
|
67554
69076
|
await NodeStressMoveLedger.declare(deps.ctx().api.settingsStore).then(() => nodeStress.hydrate()).catch((err) => {
|
|
67555
|
-
deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$
|
|
69077
|
+
deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$2(err) } });
|
|
67556
69078
|
});
|
|
67557
69079
|
await NodeStressLts.declare(deps.ctx().api.settingsStore).catch((err) => {
|
|
67558
|
-
deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$
|
|
69080
|
+
deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$2(err) } });
|
|
67559
69081
|
});
|
|
67560
69082
|
const nodeStressLtsTimer = setInterval(() => {
|
|
67561
69083
|
nodeStressLts.flushDue().catch((err) => {
|
|
67562
|
-
deps.ctx().logger.warn("node-stress statistics flush failed", { meta: { error: errMsg$
|
|
69084
|
+
deps.ctx().logger.warn("node-stress statistics flush failed", { meta: { error: errMsg$2(err) } });
|
|
67563
69085
|
});
|
|
67564
69086
|
}, 6e4);
|
|
67565
69087
|
nodeStress.start();
|
|
67566
69088
|
const inferenceRotation = new RoundRobinInferenceDeviceRotation();
|
|
69089
|
+
const readSessionPlacement = () => parseSessionPlacementPolicy(globalSettings?.[SESSION_PLACEMENT_SETTING_KEY]);
|
|
67567
69090
|
/**
|
|
67568
69091
|
* Can the watchdog's audio stage ever receive a signal for this camera?
|
|
67569
69092
|
* Only `absent` — POSITIVE evidence of no audio track — disarms it.
|
|
@@ -67647,6 +69170,8 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67647
69170
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
67648
69171
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
67649
69172
|
inferenceRotation,
|
|
69173
|
+
sessionPlacement: () => readSessionPlacement(),
|
|
69174
|
+
placementStats: deps.placementStats,
|
|
67650
69175
|
logger: deps.ctx().logger
|
|
67651
69176
|
});
|
|
67652
69177
|
const deviceConfig = new DeviceConfigContributions({
|
|
@@ -67709,7 +69234,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67709
69234
|
} catch (err) {
|
|
67710
69235
|
return {
|
|
67711
69236
|
kind: "unreadable",
|
|
67712
|
-
error: errMsg$
|
|
69237
|
+
error: errMsg$2(err)
|
|
67713
69238
|
};
|
|
67714
69239
|
}
|
|
67715
69240
|
};
|
|
@@ -67743,7 +69268,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67743
69268
|
} catch (err) {
|
|
67744
69269
|
return {
|
|
67745
69270
|
kind: "unreadable",
|
|
67746
|
-
error: errMsg$
|
|
69271
|
+
error: errMsg$2(err)
|
|
67747
69272
|
};
|
|
67748
69273
|
}
|
|
67749
69274
|
},
|
|
@@ -67774,7 +69299,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67774
69299
|
} catch (err) {
|
|
67775
69300
|
return {
|
|
67776
69301
|
kind: "unreadable",
|
|
67777
|
-
error: errMsg$
|
|
69302
|
+
error: errMsg$2(err)
|
|
67778
69303
|
};
|
|
67779
69304
|
}
|
|
67780
69305
|
},
|
|
@@ -67787,7 +69312,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67787
69312
|
} catch (err) {
|
|
67788
69313
|
return {
|
|
67789
69314
|
kind: "unreadable",
|
|
67790
|
-
error: errMsg$
|
|
69315
|
+
error: errMsg$2(err)
|
|
67791
69316
|
};
|
|
67792
69317
|
}
|
|
67793
69318
|
},
|
|
@@ -67858,7 +69383,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67858
69383
|
deps.applyRuntimeSettings(globalSettings);
|
|
67859
69384
|
await topology.refreshNodeCapabilities();
|
|
67860
69385
|
} catch (err) {
|
|
67861
|
-
const msg = errMsg$
|
|
69386
|
+
const msg = errMsg$2(err);
|
|
67862
69387
|
deps.ctx().logger.warn("orchestrator settings load failed — using defaults", { meta: { error: msg } });
|
|
67863
69388
|
}
|
|
67864
69389
|
const rawNodeId = deps.ctx().kernel.localNodeId ?? "hub";
|
|
@@ -67899,7 +69424,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67899
69424
|
audio.markNodeOffline(nodeId);
|
|
67900
69425
|
deps.forgetInferenceDeviceProbe(nodeId);
|
|
67901
69426
|
nodeLifecycle.handleNodeDisconnect(nodeId).catch((err) => {
|
|
67902
|
-
const msg = errMsg$
|
|
69427
|
+
const msg = errMsg$2(err);
|
|
67903
69428
|
deps.ctx().logger.error("failover after disconnect failed", {
|
|
67904
69429
|
tags: { nodeId },
|
|
67905
69430
|
meta: { error: msg }
|
|
@@ -67934,7 +69459,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67934
69459
|
deps.subscribeToAudioAnalyzerReadiness(nodeId);
|
|
67935
69460
|
}
|
|
67936
69461
|
nodeLifecycle.handleNodeConnect(nodeId).catch((err) => {
|
|
67937
|
-
const msg = errMsg$
|
|
69462
|
+
const msg = errMsg$2(err);
|
|
67938
69463
|
deps.ctx().logger.error("reconnect handling failed", {
|
|
67939
69464
|
tags: { nodeId },
|
|
67940
69465
|
meta: { error: msg }
|
|
@@ -67953,7 +69478,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
67953
69478
|
meta: { capName }
|
|
67954
69479
|
});
|
|
67955
69480
|
nodeLifecycle.handleNodeDisconnect(nodeId).catch((err) => {
|
|
67956
|
-
const msg = errMsg$
|
|
69481
|
+
const msg = errMsg$2(err);
|
|
67957
69482
|
deps.ctx().logger.error("failover after cap-down failed", {
|
|
67958
69483
|
tags: { nodeId },
|
|
67959
69484
|
meta: {
|
|
@@ -68016,7 +69541,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68016
69541
|
doneFlag: settingsStore.bindingsMigrationDoneState,
|
|
68017
69542
|
logger: deps.ctx().logger
|
|
68018
69543
|
}).catch((err) => {
|
|
68019
|
-
deps.ctxIfReady()?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg$
|
|
69544
|
+
deps.ctxIfReady()?.logger.warn("bindings migration failed — will retry on next boot", { meta: { error: errMsg$2(err) } });
|
|
68020
69545
|
});
|
|
68021
69546
|
migrateStepGating({
|
|
68022
69547
|
readCameraSettings: () => settingsStore.readCameraSettingsMap(),
|
|
@@ -68029,7 +69554,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68029
69554
|
for (let i = 0; deps.ctxIfReady()?.api == null && i < 30; i++) await new Promise((r) => setTimeout(r, 200));
|
|
68030
69555
|
if (deps.ctxIfReady() !== null) await deps.resetRemovedModelPins("boot");
|
|
68031
69556
|
}).catch((err) => {
|
|
68032
|
-
deps.ctxIfReady()?.logger.warn("step-gating / per-node-device migration failed — retry next boot", { meta: { error: errMsg$
|
|
69557
|
+
deps.ctxIfReady()?.logger.warn("step-gating / per-node-device migration failed — retry next boot", { meta: { error: errMsg$2(err) } });
|
|
68033
69558
|
});
|
|
68034
69559
|
const zoneRulesProvider = new ZoneRulesProvider({
|
|
68035
69560
|
logger: deps.ctx().logger.child("zone-rules"),
|
|
@@ -68071,7 +69596,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68071
69596
|
deviceId,
|
|
68072
69597
|
nodeId: assignment.agentNodeId
|
|
68073
69598
|
},
|
|
68074
|
-
meta: { error: errMsg$
|
|
69599
|
+
meta: { error: errMsg$2(err) }
|
|
68075
69600
|
});
|
|
68076
69601
|
});
|
|
68077
69602
|
}
|
|
@@ -68087,7 +69612,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68087
69612
|
}],
|
|
68088
69613
|
logger: deps.ctx().logger.child("zones")
|
|
68089
69614
|
}).catch((err) => {
|
|
68090
|
-
deps.ctxIfReady()?.logger.warn("zones mirror boot hydration failed", { meta: { error: errMsg$
|
|
69615
|
+
deps.ctxIfReady()?.logger.warn("zones mirror boot hydration failed", { meta: { error: errMsg$2(err) } });
|
|
68091
69616
|
});
|
|
68092
69617
|
const unsubOrchestratorSubscriptions = wireOrchestratorSubscriptions({
|
|
68093
69618
|
eventBus: deps.ctx().eventBus,
|
|
@@ -68148,10 +69673,10 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68148
69673
|
deviceSettingsSchema: () => deps.deviceSettingsSchema()
|
|
68149
69674
|
});
|
|
68150
69675
|
await detectionWiring.hydrateFeaturesMirror().catch((err) => {
|
|
68151
|
-
deps.ctx().logger.warn("device-features mirror not restored — this boot starts cold", { meta: { error: errMsg$
|
|
69676
|
+
deps.ctx().logger.warn("device-features mirror not restored — this boot starts cold", { meta: { error: errMsg$2(err) } });
|
|
68152
69677
|
});
|
|
68153
69678
|
cameraSensors.start().catch((err) => {
|
|
68154
|
-
deps.ctx().logger.warn("camera sensors: boot refresh failed — waiting for the next lifecycle event", { meta: { error: errMsg$
|
|
69679
|
+
deps.ctx().logger.warn("camera sensors: boot refresh failed — waiting for the next lifecycle event", { meta: { error: errMsg$2(err) } });
|
|
68155
69680
|
});
|
|
68156
69681
|
return {
|
|
68157
69682
|
ledger,
|
|
@@ -68180,7 +69705,8 @@ async function buildOrchestratorControllers(deps) {
|
|
|
68180
69705
|
unsubAgentOffline,
|
|
68181
69706
|
unsubOrchestratorSubscriptions,
|
|
68182
69707
|
pendingRetryTimer,
|
|
68183
|
-
autoRebalanceTimer
|
|
69708
|
+
autoRebalanceTimer,
|
|
69709
|
+
sessionPlacement: () => readSessionPlacement()
|
|
68184
69710
|
};
|
|
68185
69711
|
}
|
|
68186
69712
|
//#endregion
|
|
@@ -68225,6 +69751,20 @@ function buildGlobalSettingsSections(options) {
|
|
|
68225
69751
|
title: "Load Balancer",
|
|
68226
69752
|
tab: "pipeline",
|
|
68227
69753
|
fields: [
|
|
69754
|
+
{
|
|
69755
|
+
key: SESSION_PLACEMENT_SETTING_KEY,
|
|
69756
|
+
type: "select",
|
|
69757
|
+
label: "Session placement",
|
|
69758
|
+
description: "How a new detection session picks its node and device. Least-loaded = the node with the fewest sessions per unit of weight; an idle node always wins the tie, so weights only matter once nodes are busy. Honour shares = the share of PARALLEL sessions is held to the node and device weights, and a node at its cap yields to a free one. Moving a weight slider never changes this policy.",
|
|
69759
|
+
options: [{
|
|
69760
|
+
value: "least-loaded",
|
|
69761
|
+
label: "Least-loaded"
|
|
69762
|
+
}, {
|
|
69763
|
+
value: "share",
|
|
69764
|
+
label: "Honour shares"
|
|
69765
|
+
}],
|
|
69766
|
+
default: DEFAULT_SESSION_PLACEMENT_POLICY
|
|
69767
|
+
},
|
|
68228
69768
|
{
|
|
68229
69769
|
key: "fpsMinThreshold",
|
|
68230
69770
|
type: "slider",
|
|
@@ -68783,6 +70323,959 @@ var pipelineOrchestratorWidgets = [{
|
|
|
68783
70323
|
defaultRows: 4
|
|
68784
70324
|
}];
|
|
68785
70325
|
//#endregion
|
|
70326
|
+
//#region src/best-settings/plan-diff.ts
|
|
70327
|
+
/**
|
|
70328
|
+
* The Best settings writes, as pure transforms of the orchestrator's two
|
|
70329
|
+
* settings blobs (D745). The plan runs them on the current blobs to SHOW the
|
|
70330
|
+
* diff; the apply runs the same functions inside each blob's read-modify-write.
|
|
70331
|
+
*
|
|
70332
|
+
* What changes — and nothing else:
|
|
70333
|
+
* - node level (`agentSettings[node].inferenceDevices[deviceKey].steps`): the
|
|
70334
|
+
* object-detection `modelId` of every recommended device is set; the
|
|
70335
|
+
* `modelId` pin of every other in-scope step is removed (each runs its
|
|
70336
|
+
* step default again); every model-bound threshold (and its binding key) is
|
|
70337
|
+
* removed from every device's step `settings`, so the running model's own
|
|
70338
|
+
* defaults apply (D738–D741). `enabled`, `weight`, `maxSessions`,
|
|
70339
|
+
* `jumpDeviceKey` and every other setting are kept.
|
|
70340
|
+
* - camera level, scoped to the touched nodes: the same `modelId` pins and
|
|
70341
|
+
* model-bound thresholds are removed from `stepOverridesByDevice[node]` and
|
|
70342
|
+
* from the wholesale `pipelineByAgent[node]` snapshot (recursively). A
|
|
70343
|
+
* wholesale step keeps `enabled`, `children`, `jumpDeviceKey` and its other
|
|
70344
|
+
* settings: the attach provisions the node pin over the snapshot
|
|
70345
|
+
* (`applyDeviceProvisioning`: override ?? base), so once the snapshot pin
|
|
70346
|
+
* is gone the node's recommended model runs.
|
|
70347
|
+
* - never: `stepToggles`, devices' enablement, pins of
|
|
70348
|
+
* embedding steps (cluster-scoped, changing one forces a re-index).
|
|
70349
|
+
*/
|
|
70350
|
+
/** The root detector step. */
|
|
70351
|
+
var DETECTOR_STEP_ID = "object-detection";
|
|
70352
|
+
/** Steps whose `modelId` pin Best settings never touches. */
|
|
70353
|
+
function isModelPinKept(stepId) {
|
|
70354
|
+
return stepId.endsWith("-embedding");
|
|
70355
|
+
}
|
|
70356
|
+
function boundKeysFor(input, stepId) {
|
|
70357
|
+
return input.boundKeysByStep[stepId] ?? [];
|
|
70358
|
+
}
|
|
70359
|
+
/** The model-bound keys present in `settings` (values or bindings), and the rest. */
|
|
70360
|
+
function stripBound(settings, boundKeys) {
|
|
70361
|
+
if (settings === void 0) return {
|
|
70362
|
+
kept: void 0,
|
|
70363
|
+
removed: []
|
|
70364
|
+
};
|
|
70365
|
+
const kept = { ...settings };
|
|
70366
|
+
const removed = [];
|
|
70367
|
+
for (const key of boundKeys) for (const k of [key, modelBindingKey(key)]) if (k in kept) {
|
|
70368
|
+
delete kept[k];
|
|
70369
|
+
removed.push(k);
|
|
70370
|
+
}
|
|
70371
|
+
return {
|
|
70372
|
+
kept,
|
|
70373
|
+
removed: removed.toSorted()
|
|
70374
|
+
};
|
|
70375
|
+
}
|
|
70376
|
+
function pinInScope(stepId) {
|
|
70377
|
+
return !isModelPinKept(stepId);
|
|
70378
|
+
}
|
|
70379
|
+
/** Rebuild one stored step without the in-scope pin and bound keys. */
|
|
70380
|
+
function scrubStep(stepId, step, boundKeys, targetModel) {
|
|
70381
|
+
const before = step.modelId ?? null;
|
|
70382
|
+
let modelId = step.modelId;
|
|
70383
|
+
if (stepId === "object-detection") {
|
|
70384
|
+
if (targetModel !== void 0) modelId = targetModel;
|
|
70385
|
+
} else if (pinInScope(stepId)) modelId = void 0;
|
|
70386
|
+
const { kept, removed } = stripBound(step.settings, boundKeys);
|
|
70387
|
+
const after = modelId ?? null;
|
|
70388
|
+
const next = {
|
|
70389
|
+
...modelId !== void 0 ? { modelId } : {},
|
|
70390
|
+
...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {},
|
|
70391
|
+
...step.jumpDeviceKey !== void 0 ? { jumpDeviceKey: step.jumpDeviceKey } : {}
|
|
70392
|
+
};
|
|
70393
|
+
return {
|
|
70394
|
+
next: Object.keys(next).length > 0 ? next : null,
|
|
70395
|
+
pin: before !== after ? {
|
|
70396
|
+
stepId,
|
|
70397
|
+
before,
|
|
70398
|
+
after
|
|
70399
|
+
} : null,
|
|
70400
|
+
cleared: removed
|
|
70401
|
+
};
|
|
70402
|
+
}
|
|
70403
|
+
/** The node-level transform. Returns the next blob and every change it made. */
|
|
70404
|
+
function applyNodeBestSettings(blob, input) {
|
|
70405
|
+
const next = { ...blob };
|
|
70406
|
+
const changes = [];
|
|
70407
|
+
for (const nodeId of [...input.touchedNodes].toSorted()) {
|
|
70408
|
+
const entry = blob[nodeId];
|
|
70409
|
+
const targets = new Map((input.targets.get(nodeId) ?? []).map((t) => [t.deviceKey, t]));
|
|
70410
|
+
const stored = entry?.inferenceDevices ?? {};
|
|
70411
|
+
const deviceKeys = [...new Set([...Object.keys(stored), ...targets.keys()])].toSorted();
|
|
70412
|
+
const nextDevices = {};
|
|
70413
|
+
for (const deviceKey of deviceKeys) {
|
|
70414
|
+
const device = stored[deviceKey];
|
|
70415
|
+
const target = targets.get(deviceKey);
|
|
70416
|
+
const steps = { ...device?.steps };
|
|
70417
|
+
if (target !== void 0 && steps["object-detection"] === void 0) steps[DETECTOR_STEP_ID] = {};
|
|
70418
|
+
const nextSteps = {};
|
|
70419
|
+
const pins = [];
|
|
70420
|
+
const thresholds = [];
|
|
70421
|
+
for (const stepId of Object.keys(steps).toSorted()) {
|
|
70422
|
+
const step = steps[stepId];
|
|
70423
|
+
if (step === void 0) continue;
|
|
70424
|
+
const scrubbed = scrubStep(stepId, step, boundKeysFor(input, stepId), target?.modelId);
|
|
70425
|
+
if (scrubbed.next !== null) nextSteps[stepId] = scrubbed.next;
|
|
70426
|
+
if (scrubbed.pin !== null) pins.push(scrubbed.pin);
|
|
70427
|
+
if (scrubbed.cleared.length > 0) thresholds.push({
|
|
70428
|
+
stepId,
|
|
70429
|
+
keys: scrubbed.cleared
|
|
70430
|
+
});
|
|
70431
|
+
}
|
|
70432
|
+
const madeExplicit = device === void 0;
|
|
70433
|
+
nextDevices[deviceKey] = {
|
|
70434
|
+
...device !== void 0 ? (({ steps: _steps, ...rest }) => rest)(device) : { enabled: target?.effectiveEnabled ?? false },
|
|
70435
|
+
...Object.keys(nextSteps).length > 0 ? { steps: nextSteps } : {}
|
|
70436
|
+
};
|
|
70437
|
+
if (pins.length > 0 || thresholds.length > 0) changes.push({
|
|
70438
|
+
nodeId,
|
|
70439
|
+
deviceKey,
|
|
70440
|
+
madeExplicit,
|
|
70441
|
+
modelPins: pins,
|
|
70442
|
+
thresholds
|
|
70443
|
+
});
|
|
70444
|
+
}
|
|
70445
|
+
if (entry === void 0 && deviceKeys.length === 0) continue;
|
|
70446
|
+
next[nodeId] = {
|
|
70447
|
+
...entry ?? { maxCameras: null },
|
|
70448
|
+
...deviceKeys.length > 0 ? { inferenceDevices: nextDevices } : {}
|
|
70449
|
+
};
|
|
70450
|
+
}
|
|
70451
|
+
return {
|
|
70452
|
+
next,
|
|
70453
|
+
changes
|
|
70454
|
+
};
|
|
70455
|
+
}
|
|
70456
|
+
/** Scrub a wholesale snapshot tree; collects one clear per changed step. */
|
|
70457
|
+
function scrubTree(steps, input, onClear) {
|
|
70458
|
+
return steps.map((step) => {
|
|
70459
|
+
const children = step.children ? scrubTree(step.children, input, onClear) : void 0;
|
|
70460
|
+
const inScope = step.addonId === "object-detection" || pinInScope(step.addonId);
|
|
70461
|
+
const { kept, removed } = stripBound(step.settings, boundKeysFor(input, step.addonId));
|
|
70462
|
+
const dropPin = inScope && step.modelId !== void 0;
|
|
70463
|
+
if (dropPin || removed.length > 0) onClear(step.addonId, dropPin ? step.modelId ?? null : null, removed);
|
|
70464
|
+
const { modelId: _modelId, settings: _settings, children: _children, ...rest } = step;
|
|
70465
|
+
return {
|
|
70466
|
+
...rest,
|
|
70467
|
+
...!dropPin && step.modelId !== void 0 ? { modelId: step.modelId } : {},
|
|
70468
|
+
...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {},
|
|
70469
|
+
...children !== void 0 ? { children } : {}
|
|
70470
|
+
};
|
|
70471
|
+
});
|
|
70472
|
+
}
|
|
70473
|
+
/** The camera-level transform. Returns the next blob and every override it removed. */
|
|
70474
|
+
function applyCameraBestSettings(blob, input) {
|
|
70475
|
+
const next = {};
|
|
70476
|
+
const clears = [];
|
|
70477
|
+
for (const key of Object.keys(blob).toSorted((a, b) => Number(a) - Number(b))) {
|
|
70478
|
+
const camera = blob[key];
|
|
70479
|
+
if (camera === void 0) continue;
|
|
70480
|
+
const deviceId = Number(key);
|
|
70481
|
+
let nextCamera = camera;
|
|
70482
|
+
const byNode = camera.stepOverridesByDevice;
|
|
70483
|
+
if (byNode !== void 0) {
|
|
70484
|
+
const nextByNode = {};
|
|
70485
|
+
for (const [nodeId, byDevice] of Object.entries(byNode)) {
|
|
70486
|
+
if (!input.touchedNodes.has(nodeId)) {
|
|
70487
|
+
nextByNode[nodeId] = byDevice;
|
|
70488
|
+
continue;
|
|
70489
|
+
}
|
|
70490
|
+
const nextByDevice = {};
|
|
70491
|
+
for (const [deviceKey, byStep] of Object.entries(byDevice)) {
|
|
70492
|
+
const nextByStep = {};
|
|
70493
|
+
for (const [stepId, patch] of Object.entries(byStep)) {
|
|
70494
|
+
const inScope = stepId === "object-detection" || pinInScope(stepId);
|
|
70495
|
+
const { kept, removed } = stripBound(patch.settings, boundKeysFor(input, stepId));
|
|
70496
|
+
const dropPin = inScope && patch.modelId !== void 0;
|
|
70497
|
+
if (dropPin || removed.length > 0) clears.push({
|
|
70498
|
+
deviceId,
|
|
70499
|
+
source: "step-override",
|
|
70500
|
+
nodeId,
|
|
70501
|
+
deviceKey,
|
|
70502
|
+
stepId,
|
|
70503
|
+
modelIdBefore: dropPin ? patch.modelId ?? null : null,
|
|
70504
|
+
keys: removed
|
|
70505
|
+
});
|
|
70506
|
+
const rebuilt = {
|
|
70507
|
+
...!dropPin && patch.modelId !== void 0 ? { modelId: patch.modelId } : {},
|
|
70508
|
+
...kept !== void 0 && Object.keys(kept).length > 0 ? { settings: kept } : {}
|
|
70509
|
+
};
|
|
70510
|
+
if (Object.keys(rebuilt).length > 0) nextByStep[stepId] = rebuilt;
|
|
70511
|
+
}
|
|
70512
|
+
if (Object.keys(nextByStep).length > 0) nextByDevice[deviceKey] = nextByStep;
|
|
70513
|
+
}
|
|
70514
|
+
if (Object.keys(nextByDevice).length > 0) nextByNode[nodeId] = nextByDevice;
|
|
70515
|
+
}
|
|
70516
|
+
const { stepOverridesByDevice: _drop, ...rest } = nextCamera;
|
|
70517
|
+
nextCamera = {
|
|
70518
|
+
...rest,
|
|
70519
|
+
...Object.keys(nextByNode).length > 0 ? { stepOverridesByDevice: nextByNode } : {}
|
|
70520
|
+
};
|
|
70521
|
+
}
|
|
70522
|
+
const wholesale = camera.pipelineByAgent;
|
|
70523
|
+
if (wholesale !== void 0) {
|
|
70524
|
+
const nextWholesale = {};
|
|
70525
|
+
for (const [nodeId, snapshot] of Object.entries(wholesale)) {
|
|
70526
|
+
if (!input.touchedNodes.has(nodeId)) {
|
|
70527
|
+
nextWholesale[nodeId] = snapshot;
|
|
70528
|
+
continue;
|
|
70529
|
+
}
|
|
70530
|
+
nextWholesale[nodeId] = {
|
|
70531
|
+
...snapshot,
|
|
70532
|
+
steps: scrubTree(snapshot.steps, input, (stepId, modelIdBefore, keys) => {
|
|
70533
|
+
clears.push({
|
|
70534
|
+
deviceId,
|
|
70535
|
+
source: "wholesale",
|
|
70536
|
+
nodeId,
|
|
70537
|
+
deviceKey: null,
|
|
70538
|
+
stepId,
|
|
70539
|
+
modelIdBefore,
|
|
70540
|
+
keys
|
|
70541
|
+
});
|
|
70542
|
+
})
|
|
70543
|
+
};
|
|
70544
|
+
}
|
|
70545
|
+
nextCamera = {
|
|
70546
|
+
...nextCamera,
|
|
70547
|
+
pipelineByAgent: nextWholesale
|
|
70548
|
+
};
|
|
70549
|
+
}
|
|
70550
|
+
next[key] = nextCamera;
|
|
70551
|
+
}
|
|
70552
|
+
return {
|
|
70553
|
+
next,
|
|
70554
|
+
clears
|
|
70555
|
+
};
|
|
70556
|
+
}
|
|
70557
|
+
//#endregion
|
|
70558
|
+
//#region src/best-settings/best-settings-service.ts
|
|
70559
|
+
/**
|
|
70560
|
+
* Best settings (D745): gather the inputs, compute the plan, and apply exactly
|
|
70561
|
+
* the plan the operator confirmed.
|
|
70562
|
+
*
|
|
70563
|
+
* The plan is the recommender (`@camstack/types` best-settings) over every
|
|
70564
|
+
* runner node, plus the diff of the writes (`plan-diff.ts`) and the
|
|
70565
|
+
* per-camera detector thresholds the hub's executor would clear. Its
|
|
70566
|
+
* `planHash` names everything the apply writes; the apply recomputes the plan
|
|
70567
|
+
* and refuses a different hash, so a configuration change between the two is
|
|
70568
|
+
* shown to the operator again instead of being written over.
|
|
70569
|
+
*
|
|
70570
|
+
* Apply order, each phase reported:
|
|
70571
|
+
* 1. download every newly picked detector on its node FIRST — a node never
|
|
70572
|
+
* switches to a model it does not have (an N100 compile takes 5–25 s, a
|
|
70573
|
+
* Core ML package 40 MB); any failure stops the apply before a write;
|
|
70574
|
+
* 2. camera overrides (one camera-blob read-modify-write);
|
|
70575
|
+
* 3. per-camera detector thresholds (the hub executor's store);
|
|
70576
|
+
* 4. node model pins (one agent-blob read-modify-write);
|
|
70577
|
+
* 5. re-dispatch the cameras of every touched node.
|
|
70578
|
+
* A failure after phase 2 leaves "overrides cleared, previous node model",
|
|
70579
|
+
* which is a valid configuration (the defaults run); re-running converges.
|
|
70580
|
+
*/
|
|
70581
|
+
function errMsg(err) {
|
|
70582
|
+
return err instanceof Error ? err.message : String(err);
|
|
70583
|
+
}
|
|
70584
|
+
/** Canonical JSON (sorted keys) — the plan hash must not depend on key order. */
|
|
70585
|
+
function canonical(value) {
|
|
70586
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
70587
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, v]) => v !== void 0).toSorted(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
70588
|
+
return JSON.stringify(value);
|
|
70589
|
+
}
|
|
70590
|
+
function formatsByModelOf(catalog) {
|
|
70591
|
+
const out = {};
|
|
70592
|
+
for (const slot of catalog?.slots ?? []) for (const addon of slot.addons) {
|
|
70593
|
+
if (addon.id !== "object-detection") continue;
|
|
70594
|
+
for (const model of addon.models) out[model.id] = MODEL_FORMATS.filter((f) => f in model.formats);
|
|
70595
|
+
}
|
|
70596
|
+
return out;
|
|
70597
|
+
}
|
|
70598
|
+
function boundKeysOf(catalogs) {
|
|
70599
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
70600
|
+
for (const catalog of catalogs) for (const slot of catalog?.slots ?? []) for (const addon of slot.addons) {
|
|
70601
|
+
const keys = collectModelBoundKeys(addon.configSchema ?? []);
|
|
70602
|
+
if (keys.length === 0) continue;
|
|
70603
|
+
const set = byStep.get(addon.id) ?? /* @__PURE__ */ new Set();
|
|
70604
|
+
for (const k of keys) set.add(k);
|
|
70605
|
+
byStep.set(addon.id, set);
|
|
70606
|
+
}
|
|
70607
|
+
return Object.fromEntries([...byStep].map(([step, set]) => [step, [...set].toSorted()]));
|
|
70608
|
+
}
|
|
70609
|
+
/** Bounded fixed point of the hardware share suggestion (D748). */
|
|
70610
|
+
var SUGGEST_MAX_ROUNDS = 3;
|
|
70611
|
+
/** Integer shares summing to 100 (largest remainder). */
|
|
70612
|
+
function toPercent(weights) {
|
|
70613
|
+
const total = [...weights.values()].reduce((a, b) => a + b, 0);
|
|
70614
|
+
const out = /* @__PURE__ */ new Map();
|
|
70615
|
+
if (total <= 0) return out;
|
|
70616
|
+
const raw = [...weights].map(([k, w]) => ({
|
|
70617
|
+
k,
|
|
70618
|
+
exact: w / total * 100
|
|
70619
|
+
}));
|
|
70620
|
+
let assigned = 0;
|
|
70621
|
+
for (const r of raw) {
|
|
70622
|
+
out.set(r.k, Math.floor(r.exact));
|
|
70623
|
+
assigned += Math.floor(r.exact);
|
|
70624
|
+
}
|
|
70625
|
+
const byRemainder = raw.toSorted((a, b) => b.exact - Math.floor(b.exact) - (a.exact - Math.floor(a.exact)) || a.k.localeCompare(b.k));
|
|
70626
|
+
for (let i = 0; i < 100 - assigned; i++) {
|
|
70627
|
+
const r = byRemainder[i % byRemainder.length];
|
|
70628
|
+
out.set(r.k, (out.get(r.k) ?? 0) + 1);
|
|
70629
|
+
}
|
|
70630
|
+
return out;
|
|
70631
|
+
}
|
|
70632
|
+
var BestSettingsService = class {
|
|
70633
|
+
deps;
|
|
70634
|
+
applying = false;
|
|
70635
|
+
constructor(deps) {
|
|
70636
|
+
this.deps = deps;
|
|
70637
|
+
}
|
|
70638
|
+
async plan(input) {
|
|
70639
|
+
return (await this.computePlan(input.headroom, input.shares)).plan;
|
|
70640
|
+
}
|
|
70641
|
+
/**
|
|
70642
|
+
* Shares proportional to each node's capacity at the models Best settings
|
|
70643
|
+
* would pick under those very shares (D748). Model and load depend on each
|
|
70644
|
+
* other, so it iterates from equal shares until the rounded shares stop
|
|
70645
|
+
* moving, at most {@link SUGGEST_MAX_ROUNDS} rounds. Per device it reports an
|
|
70646
|
+
* ADVISORY session cap — never written, never enforced.
|
|
70647
|
+
*/
|
|
70648
|
+
async suggestShares(input) {
|
|
70649
|
+
let shares = {
|
|
70650
|
+
nodes: {},
|
|
70651
|
+
devices: {}
|
|
70652
|
+
};
|
|
70653
|
+
let result = [];
|
|
70654
|
+
let cameraFps = 10;
|
|
70655
|
+
let rounds = 0;
|
|
70656
|
+
for (let round = 1; round <= SUGGEST_MAX_ROUNDS; round++) {
|
|
70657
|
+
rounds = round;
|
|
70658
|
+
const rec = await this.recommend(input.headroom, shares);
|
|
70659
|
+
cameraFps = rec.cameraFps;
|
|
70660
|
+
result = this.sharesFrom(rec, input.headroom);
|
|
70661
|
+
const next = {
|
|
70662
|
+
nodes: Object.fromEntries(result.flatMap((n) => n.share !== null && n.share > 0 ? [[n.nodeId, n.share]] : [])),
|
|
70663
|
+
devices: Object.fromEntries(result.map((n) => [n.nodeId, Object.fromEntries(n.devices.flatMap((d) => d.share > 0 ? [[d.deviceKey, d.share]] : []))]))
|
|
70664
|
+
};
|
|
70665
|
+
const settled = JSON.stringify(next) === JSON.stringify(shares);
|
|
70666
|
+
shares = next;
|
|
70667
|
+
if (settled) break;
|
|
70668
|
+
}
|
|
70669
|
+
return {
|
|
70670
|
+
headroom: input.headroom,
|
|
70671
|
+
cameraFps,
|
|
70672
|
+
iterations: rounds,
|
|
70673
|
+
nodes: [...result]
|
|
70674
|
+
};
|
|
70675
|
+
}
|
|
70676
|
+
sharesFrom(rec, headroom) {
|
|
70677
|
+
const capacityByNode = /* @__PURE__ */ new Map();
|
|
70678
|
+
const perNode = rec.recommendations.map((r) => {
|
|
70679
|
+
const devices = r.devices.filter((d) => d.enabled && d.available && d.chosen !== null).map((d) => {
|
|
70680
|
+
const fps = d.chosen?.fps ?? 0;
|
|
70681
|
+
return {
|
|
70682
|
+
d,
|
|
70683
|
+
fps,
|
|
70684
|
+
sessions: rec.cameraFps > 0 ? Math.floor(fps * (1 - headroom) / rec.cameraFps) : 0
|
|
70685
|
+
};
|
|
70686
|
+
});
|
|
70687
|
+
const capacity = devices.reduce((sum, x) => sum + x.sessions, 0);
|
|
70688
|
+
if (!r.skipped && capacity > 0) capacityByNode.set(r.nodeId, capacity);
|
|
70689
|
+
return {
|
|
70690
|
+
r,
|
|
70691
|
+
devices,
|
|
70692
|
+
capacity
|
|
70693
|
+
};
|
|
70694
|
+
});
|
|
70695
|
+
const nodePct = toPercent(capacityByNode);
|
|
70696
|
+
return perNode.map(({ r, devices, capacity }) => {
|
|
70697
|
+
const devicePct = toPercent(new Map(devices.map((x) => [x.d.deviceKey, Math.max(x.fps, 0)])));
|
|
70698
|
+
return {
|
|
70699
|
+
nodeId: r.nodeId,
|
|
70700
|
+
share: nodePct.get(r.nodeId) ?? null,
|
|
70701
|
+
capacitySessions: capacity,
|
|
70702
|
+
skippedReason: r.skipped ? r.reasons[0]?.kind ?? "skipped" : capacity > 0 ? null : "no-capacity",
|
|
70703
|
+
devices: devices.map((x) => ({
|
|
70704
|
+
deviceKey: x.d.deviceKey,
|
|
70705
|
+
share: devicePct.get(x.d.deviceKey) ?? 0,
|
|
70706
|
+
modelId: x.d.chosen?.modelId ?? null,
|
|
70707
|
+
fps: x.d.chosen?.fps ?? null,
|
|
70708
|
+
measured: x.d.chosen?.measured ?? false,
|
|
70709
|
+
suggestedMaxSessions: x.sessions
|
|
70710
|
+
}))
|
|
70711
|
+
};
|
|
70712
|
+
});
|
|
70713
|
+
}
|
|
70714
|
+
/**
|
|
70715
|
+
* Gather every node and recommend. `shares` (a what-if or the suggestion's
|
|
70716
|
+
* round) forces share placement with those shares overlaid on the stored
|
|
70717
|
+
* ones; absent, the STORED policy decides how on-motion load is charged.
|
|
70718
|
+
*/
|
|
70719
|
+
async recommend(headroom, shares) {
|
|
70720
|
+
const nodeIds = [...new Set(["hub", ...this.deps.runnerNodeIds()])].toSorted();
|
|
70721
|
+
const gathered = await Promise.all(nodeIds.map(async (nodeId) => {
|
|
70722
|
+
const [devices, hardware, catalog] = await Promise.all([
|
|
70723
|
+
this.deps.readNodeDevices(nodeId),
|
|
70724
|
+
this.deps.readHardware(nodeId).catch(() => null),
|
|
70725
|
+
this.deps.readCatalog(nodeId).catch(() => null)
|
|
70726
|
+
]);
|
|
70727
|
+
return {
|
|
70728
|
+
nodeId,
|
|
70729
|
+
devices,
|
|
70730
|
+
hardware,
|
|
70731
|
+
catalog
|
|
70732
|
+
};
|
|
70733
|
+
}));
|
|
70734
|
+
const deviceShare = (nodeId, key, stored) => shares?.devices[nodeId]?.[key] ?? stored;
|
|
70735
|
+
const nodes = gathered.map((g) => ({
|
|
70736
|
+
nodeId: g.nodeId,
|
|
70737
|
+
reachable: g.devices.reachable && g.catalog !== null,
|
|
70738
|
+
hardware: g.hardware,
|
|
70739
|
+
devices: g.devices.devices.map((d) => ({
|
|
70740
|
+
key: d.key,
|
|
70741
|
+
format: d.format,
|
|
70742
|
+
enabled: d.enabled,
|
|
70743
|
+
available: d.available
|
|
70744
|
+
})),
|
|
70745
|
+
formatsByModel: formatsByModelOf(g.catalog)
|
|
70746
|
+
}));
|
|
70747
|
+
const eligible = gathered.flatMap((g) => g.devices.reachable ? g.devices.devices.filter((d) => d.enabled && d.available).map((d) => ({
|
|
70748
|
+
nodeId: g.nodeId,
|
|
70749
|
+
deviceKey: d.key,
|
|
70750
|
+
weight: deviceShare(g.nodeId, d.key, d.weight)
|
|
70751
|
+
})) : []);
|
|
70752
|
+
const cameras = await this.deps.cameraLoads();
|
|
70753
|
+
const policy = shares ? "share" : this.deps.sessionPlacement();
|
|
70754
|
+
const demand = estimateDeviceDemand({
|
|
70755
|
+
cameras,
|
|
70756
|
+
eligible,
|
|
70757
|
+
peaks: this.deps.peaks(),
|
|
70758
|
+
...policy === "share" ? { shares: {
|
|
70759
|
+
nodes: {
|
|
70760
|
+
...await this.deps.readNodeShares(),
|
|
70761
|
+
...shares?.nodes
|
|
70762
|
+
},
|
|
70763
|
+
clusterPeak: this.deps.clusterPeak()
|
|
70764
|
+
} } : {}
|
|
70765
|
+
});
|
|
70766
|
+
const recommendations = nodes.map((node) => recommendNode({
|
|
70767
|
+
node,
|
|
70768
|
+
demand,
|
|
70769
|
+
headroom
|
|
70770
|
+
}));
|
|
70771
|
+
const cameraFps = cameras.reduce((m, c) => c.mode !== "disabled" && c.runsPipelineRoot ? Math.max(m, c.fps) : m, 0);
|
|
70772
|
+
return {
|
|
70773
|
+
gathered,
|
|
70774
|
+
nodes,
|
|
70775
|
+
recommendations,
|
|
70776
|
+
cameraFps: cameraFps > 0 ? cameraFps : 10
|
|
70777
|
+
};
|
|
70778
|
+
}
|
|
70779
|
+
async computePlan(headroom, shares) {
|
|
70780
|
+
const { gathered, nodes, recommendations } = await this.recommend(headroom, shares);
|
|
70781
|
+
const touchedNodes = new Set(recommendations.filter((r) => !r.skipped).map((r) => r.nodeId));
|
|
70782
|
+
const targets = /* @__PURE__ */ new Map();
|
|
70783
|
+
const formats = /* @__PURE__ */ new Map();
|
|
70784
|
+
for (const rec of recommendations) {
|
|
70785
|
+
if (rec.skipped) continue;
|
|
70786
|
+
const node = nodes.find((n) => n.nodeId === rec.nodeId);
|
|
70787
|
+
const list = [];
|
|
70788
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
70789
|
+
for (const d of rec.devices) {
|
|
70790
|
+
if (d.chosen === null) continue;
|
|
70791
|
+
list.push({
|
|
70792
|
+
deviceKey: d.deviceKey,
|
|
70793
|
+
modelId: d.chosen.modelId,
|
|
70794
|
+
effectiveEnabled: d.enabled
|
|
70795
|
+
});
|
|
70796
|
+
const format = node?.devices.find((x) => x.key === d.deviceKey)?.format;
|
|
70797
|
+
if (format !== void 0) byKey.set(d.deviceKey, format);
|
|
70798
|
+
}
|
|
70799
|
+
targets.set(rec.nodeId, list);
|
|
70800
|
+
formats.set(rec.nodeId, byKey);
|
|
70801
|
+
}
|
|
70802
|
+
const diffInput = {
|
|
70803
|
+
touchedNodes,
|
|
70804
|
+
targets,
|
|
70805
|
+
boundKeysByStep: boundKeysOf(gathered.map((g) => g.catalog))
|
|
70806
|
+
};
|
|
70807
|
+
const nodeChanges = applyNodeBestSettings(await this.deps.readAgentSettings(), diffInput).changes;
|
|
70808
|
+
const cameraClears = applyCameraBestSettings(await this.deps.readCameraSettings(), diffInput).clears;
|
|
70809
|
+
const warnings = [];
|
|
70810
|
+
let detectorStoreClears = [];
|
|
70811
|
+
if (touchedNodes.size > 0) try {
|
|
70812
|
+
const answer = await this.deps.detectorStore({ dryRun: true });
|
|
70813
|
+
detectorStoreClears = answer.entries.map((e) => ({
|
|
70814
|
+
deviceId: e.deviceId,
|
|
70815
|
+
keys: [...e.keys]
|
|
70816
|
+
}));
|
|
70817
|
+
if (answer.refused.length > 0) warnings.push({
|
|
70818
|
+
kind: "detector-store-unreadable",
|
|
70819
|
+
error: `${answer.refused.length} camera store(s) unreadable`
|
|
70820
|
+
});
|
|
70821
|
+
} catch (err) {
|
|
70822
|
+
warnings.push({
|
|
70823
|
+
kind: "detector-store-unreadable",
|
|
70824
|
+
error: errMsg(err)
|
|
70825
|
+
});
|
|
70826
|
+
}
|
|
70827
|
+
const skippedNodes = recommendations.filter((r) => r.skipped).map((r) => r.nodeId);
|
|
70828
|
+
if (detectorStoreClears.length > 0 && skippedNodes.length > 0) warnings.push({
|
|
70829
|
+
kind: "detector-store-cluster-wide",
|
|
70830
|
+
skippedNodes
|
|
70831
|
+
});
|
|
70832
|
+
for (const rec of recommendations) for (const d of rec.devices) if (d.accelerator === "coral" && d.enabled) warnings.push({
|
|
70833
|
+
kind: "coral-enabled",
|
|
70834
|
+
nodeId: rec.nodeId,
|
|
70835
|
+
deviceKey: d.deviceKey
|
|
70836
|
+
});
|
|
70837
|
+
return {
|
|
70838
|
+
plan: {
|
|
70839
|
+
planHash: createHash("sha256").update(canonical({
|
|
70840
|
+
version: 1,
|
|
70841
|
+
headroom,
|
|
70842
|
+
whatIf: shares ?? null,
|
|
70843
|
+
picks: recommendations.map((r) => ({
|
|
70844
|
+
nodeId: r.nodeId,
|
|
70845
|
+
skipped: r.skipped,
|
|
70846
|
+
devices: r.devices.map((d) => [d.deviceKey, d.chosen?.modelId ?? null])
|
|
70847
|
+
})),
|
|
70848
|
+
nodeChanges,
|
|
70849
|
+
cameraClears,
|
|
70850
|
+
detectorStoreClears
|
|
70851
|
+
})).digest("hex"),
|
|
70852
|
+
family: "yolov9",
|
|
70853
|
+
headroom,
|
|
70854
|
+
whatIf: shares !== void 0,
|
|
70855
|
+
generatedAt: this.deps.now(),
|
|
70856
|
+
nodes: recommendations,
|
|
70857
|
+
nodeChanges: [...nodeChanges],
|
|
70858
|
+
cameraClears: [...cameraClears],
|
|
70859
|
+
detectorStoreClears,
|
|
70860
|
+
warnings
|
|
70861
|
+
},
|
|
70862
|
+
diffInput,
|
|
70863
|
+
formats
|
|
70864
|
+
};
|
|
70865
|
+
}
|
|
70866
|
+
async apply(input) {
|
|
70867
|
+
if (this.applying) return {
|
|
70868
|
+
kind: "refused",
|
|
70869
|
+
reason: "apply-in-progress"
|
|
70870
|
+
};
|
|
70871
|
+
this.applying = true;
|
|
70872
|
+
try {
|
|
70873
|
+
return await this.applyLocked(input);
|
|
70874
|
+
} finally {
|
|
70875
|
+
this.applying = false;
|
|
70876
|
+
}
|
|
70877
|
+
}
|
|
70878
|
+
async applyLocked(input) {
|
|
70879
|
+
const { plan, diffInput, formats } = await this.computePlan(input.headroom);
|
|
70880
|
+
if (plan.planHash !== input.planHash) {
|
|
70881
|
+
this.deps.logger.warn("best settings: stale plan refused", { meta: {
|
|
70882
|
+
confirmed: input.planHash,
|
|
70883
|
+
current: plan.planHash
|
|
70884
|
+
} });
|
|
70885
|
+
return {
|
|
70886
|
+
kind: "stale",
|
|
70887
|
+
plan
|
|
70888
|
+
};
|
|
70889
|
+
}
|
|
70890
|
+
const phases = [];
|
|
70891
|
+
const downloads = plan.nodeChanges.flatMap((c) => c.modelPins.filter((p) => p.stepId === "object-detection" && p.after !== null && p.after !== p.before).map((p) => ({
|
|
70892
|
+
nodeId: c.nodeId,
|
|
70893
|
+
modelId: p.after ?? "",
|
|
70894
|
+
format: formats.get(c.nodeId)?.get(c.deviceKey)
|
|
70895
|
+
})));
|
|
70896
|
+
const unique = new Map(downloads.map((d) => [`${d.nodeId}|${d.modelId}|${d.format}`, d]));
|
|
70897
|
+
try {
|
|
70898
|
+
for (const d of unique.values()) {
|
|
70899
|
+
if (d.format === void 0) continue;
|
|
70900
|
+
await this.deps.downloadModel(d.nodeId, d.modelId, d.format);
|
|
70901
|
+
}
|
|
70902
|
+
phases.push({
|
|
70903
|
+
phase: "download",
|
|
70904
|
+
ok: true,
|
|
70905
|
+
detail: `${unique.size} model(s) present on their nodes`
|
|
70906
|
+
});
|
|
70907
|
+
} catch (err) {
|
|
70908
|
+
phases.push({
|
|
70909
|
+
phase: "download",
|
|
70910
|
+
ok: false,
|
|
70911
|
+
detail: errMsg(err)
|
|
70912
|
+
});
|
|
70913
|
+
this.deps.logger.warn("best settings: a model download failed — nothing was written", { meta: { error: errMsg(err) } });
|
|
70914
|
+
return {
|
|
70915
|
+
kind: "aborted",
|
|
70916
|
+
phases
|
|
70917
|
+
};
|
|
70918
|
+
}
|
|
70919
|
+
try {
|
|
70920
|
+
await this.deps.rewriteCameraSettings((prev) => applyCameraBestSettings(prev, diffInput).next);
|
|
70921
|
+
for (const c of plan.cameraClears) this.deps.logger.info("best settings: camera override cleared", {
|
|
70922
|
+
tags: {
|
|
70923
|
+
deviceId: c.deviceId,
|
|
70924
|
+
nodeId: c.nodeId
|
|
70925
|
+
},
|
|
70926
|
+
meta: {
|
|
70927
|
+
deviceId: c.deviceId,
|
|
70928
|
+
source: c.source,
|
|
70929
|
+
deviceKey: c.deviceKey,
|
|
70930
|
+
stepId: c.stepId,
|
|
70931
|
+
modelIdBefore: c.modelIdBefore,
|
|
70932
|
+
keys: c.keys
|
|
70933
|
+
}
|
|
70934
|
+
});
|
|
70935
|
+
phases.push({
|
|
70936
|
+
phase: "camera-overrides",
|
|
70937
|
+
ok: true,
|
|
70938
|
+
detail: `${plan.cameraClears.length} cleared`
|
|
70939
|
+
});
|
|
70940
|
+
} catch (err) {
|
|
70941
|
+
phases.push({
|
|
70942
|
+
phase: "camera-overrides",
|
|
70943
|
+
ok: false,
|
|
70944
|
+
detail: errMsg(err)
|
|
70945
|
+
});
|
|
70946
|
+
}
|
|
70947
|
+
if (plan.detectorStoreClears.length > 0) try {
|
|
70948
|
+
const answer = await this.deps.detectorStore({
|
|
70949
|
+
dryRun: false,
|
|
70950
|
+
deviceIds: plan.detectorStoreClears.map((c) => c.deviceId)
|
|
70951
|
+
});
|
|
70952
|
+
phases.push({
|
|
70953
|
+
phase: "detector-thresholds",
|
|
70954
|
+
ok: answer.refused.length === 0,
|
|
70955
|
+
detail: `${answer.entries.length} cleared, ${answer.refused.length} refused`
|
|
70956
|
+
});
|
|
70957
|
+
} catch (err) {
|
|
70958
|
+
phases.push({
|
|
70959
|
+
phase: "detector-thresholds",
|
|
70960
|
+
ok: false,
|
|
70961
|
+
detail: errMsg(err)
|
|
70962
|
+
});
|
|
70963
|
+
}
|
|
70964
|
+
try {
|
|
70965
|
+
await this.deps.rewriteAgentSettings((prev) => applyNodeBestSettings(prev, diffInput).next);
|
|
70966
|
+
for (const c of plan.nodeChanges) for (const pin of c.modelPins) this.deps.logger.info("best settings: node model set", {
|
|
70967
|
+
tags: { nodeId: c.nodeId },
|
|
70968
|
+
meta: {
|
|
70969
|
+
nodeId: c.nodeId,
|
|
70970
|
+
deviceKey: c.deviceKey,
|
|
70971
|
+
stepId: pin.stepId,
|
|
70972
|
+
from: pin.before,
|
|
70973
|
+
to: pin.after
|
|
70974
|
+
}
|
|
70975
|
+
});
|
|
70976
|
+
phases.push({
|
|
70977
|
+
phase: "node-models",
|
|
70978
|
+
ok: true,
|
|
70979
|
+
detail: `${plan.nodeChanges.length} device(s) updated`
|
|
70980
|
+
});
|
|
70981
|
+
} catch (err) {
|
|
70982
|
+
phases.push({
|
|
70983
|
+
phase: "node-models",
|
|
70984
|
+
ok: false,
|
|
70985
|
+
detail: errMsg(err)
|
|
70986
|
+
});
|
|
70987
|
+
}
|
|
70988
|
+
this.deps.invalidateCatalog();
|
|
70989
|
+
const failed = [];
|
|
70990
|
+
for (const nodeId of [...diffInput.touchedNodes].toSorted()) try {
|
|
70991
|
+
await this.deps.redispatch(nodeId);
|
|
70992
|
+
} catch (err) {
|
|
70993
|
+
failed.push(`${nodeId}: ${errMsg(err)}`);
|
|
70994
|
+
}
|
|
70995
|
+
phases.push({
|
|
70996
|
+
phase: "redispatch",
|
|
70997
|
+
ok: failed.length === 0,
|
|
70998
|
+
detail: failed.length === 0 ? `${diffInput.touchedNodes.size} node(s)` : failed.join("; ")
|
|
70999
|
+
});
|
|
71000
|
+
for (const rec of plan.nodes.filter((r) => r.skipped)) this.deps.logger.info("best settings: node skipped", {
|
|
71001
|
+
tags: { nodeId: rec.nodeId },
|
|
71002
|
+
meta: {
|
|
71003
|
+
nodeId: rec.nodeId,
|
|
71004
|
+
reasons: rec.reasons
|
|
71005
|
+
}
|
|
71006
|
+
});
|
|
71007
|
+
this.deps.logger.info("best settings: applied", { meta: {
|
|
71008
|
+
planHash: plan.planHash,
|
|
71009
|
+
nodeChanges: plan.nodeChanges.length,
|
|
71010
|
+
cameraClears: plan.cameraClears.length,
|
|
71011
|
+
detectorStoreClears: plan.detectorStoreClears.length,
|
|
71012
|
+
phases
|
|
71013
|
+
} });
|
|
71014
|
+
return {
|
|
71015
|
+
kind: "applied",
|
|
71016
|
+
planHash: plan.planHash,
|
|
71017
|
+
phases
|
|
71018
|
+
};
|
|
71019
|
+
}
|
|
71020
|
+
};
|
|
71021
|
+
//#endregion
|
|
71022
|
+
//#region src/best-settings/device-peak-tracker.ts
|
|
71023
|
+
var BUCKET_MS = 3600 * 1e3;
|
|
71024
|
+
var DevicePeakTracker = class DevicePeakTracker {
|
|
71025
|
+
now;
|
|
71026
|
+
series = /* @__PURE__ */ new Map();
|
|
71027
|
+
cluster = null;
|
|
71028
|
+
constructor(now = () => Date.now()) {
|
|
71029
|
+
this.now = now;
|
|
71030
|
+
}
|
|
71031
|
+
static key(nodeId, deviceKey) {
|
|
71032
|
+
return `${nodeId}\u0000${deviceKey}`;
|
|
71033
|
+
}
|
|
71034
|
+
/** Fold one runner census in. A runner that did not answer is simply absent. */
|
|
71035
|
+
noteCensus(loads) {
|
|
71036
|
+
const now = this.now();
|
|
71037
|
+
const bucket = now - now % BUCKET_MS;
|
|
71038
|
+
let clusterTotal = 0;
|
|
71039
|
+
for (const load of loads) {
|
|
71040
|
+
const rows = load.devices.length > 0 ? load.devices : [{
|
|
71041
|
+
deviceKey: "",
|
|
71042
|
+
attachedCameras: load.attachedCameras
|
|
71043
|
+
}];
|
|
71044
|
+
for (const device of rows) {
|
|
71045
|
+
clusterTotal += device.attachedCameras;
|
|
71046
|
+
const key = DevicePeakTracker.key(load.nodeId, device.deviceKey);
|
|
71047
|
+
let s = this.series.get(key);
|
|
71048
|
+
if (s === void 0) {
|
|
71049
|
+
s = {
|
|
71050
|
+
firstSeenAt: now,
|
|
71051
|
+
buckets: /* @__PURE__ */ new Map()
|
|
71052
|
+
};
|
|
71053
|
+
this.series.set(key, s);
|
|
71054
|
+
}
|
|
71055
|
+
s.buckets.set(bucket, Math.max(s.buckets.get(bucket) ?? 0, device.attachedCameras));
|
|
71056
|
+
for (const start of s.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) s.buckets.delete(start);
|
|
71057
|
+
}
|
|
71058
|
+
}
|
|
71059
|
+
if (loads.length === 0) return;
|
|
71060
|
+
const c = this.cluster ?? {
|
|
71061
|
+
firstSeenAt: now,
|
|
71062
|
+
buckets: /* @__PURE__ */ new Map()
|
|
71063
|
+
};
|
|
71064
|
+
this.cluster = c;
|
|
71065
|
+
c.buckets.set(bucket, Math.max(c.buckets.get(bucket) ?? 0, clusterTotal));
|
|
71066
|
+
for (const start of c.buckets.keys()) if (start + BUCKET_MS <= now - 864e5) c.buckets.delete(start);
|
|
71067
|
+
}
|
|
71068
|
+
/** The cluster-wide parallel peak over the window, or null before any census. */
|
|
71069
|
+
clusterPeak() {
|
|
71070
|
+
const c = this.cluster;
|
|
71071
|
+
if (c === null) return null;
|
|
71072
|
+
const now = this.now();
|
|
71073
|
+
let peak = 0;
|
|
71074
|
+
for (const [start, value] of c.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71075
|
+
return {
|
|
71076
|
+
peakSessions: peak,
|
|
71077
|
+
observedForMs: now - c.firstSeenAt
|
|
71078
|
+
};
|
|
71079
|
+
}
|
|
71080
|
+
/** Every observed device's peak over the window and how long it has been watched. */
|
|
71081
|
+
peaks() {
|
|
71082
|
+
const now = this.now();
|
|
71083
|
+
const out = [];
|
|
71084
|
+
for (const [key, s] of this.series) {
|
|
71085
|
+
const [nodeId = "", deviceKey = ""] = key.split("\0");
|
|
71086
|
+
let peak = 0;
|
|
71087
|
+
for (const [start, value] of s.buckets) if (start + BUCKET_MS > now - 864e5) peak = Math.max(peak, value);
|
|
71088
|
+
out.push({
|
|
71089
|
+
nodeId,
|
|
71090
|
+
deviceKey,
|
|
71091
|
+
peakSessions: peak,
|
|
71092
|
+
observedForMs: now - s.firstSeenAt
|
|
71093
|
+
});
|
|
71094
|
+
}
|
|
71095
|
+
return out.toSorted((a, b) => a.nodeId === b.nodeId ? a.deviceKey.localeCompare(b.deviceKey) : a.nodeId.localeCompare(b.nodeId));
|
|
71096
|
+
}
|
|
71097
|
+
};
|
|
71098
|
+
//#endregion
|
|
71099
|
+
//#region src/session-placement-stats.ts
|
|
71100
|
+
var RETAIN_MS = 1440 * 60 * 1e3;
|
|
71101
|
+
function keyOf(nodeId, deviceKey) {
|
|
71102
|
+
return `${nodeId}\u0000${deviceKey}`;
|
|
71103
|
+
}
|
|
71104
|
+
function split(key) {
|
|
71105
|
+
const [nodeId = "", deviceKey = ""] = key.split("\0");
|
|
71106
|
+
return {
|
|
71107
|
+
nodeId,
|
|
71108
|
+
deviceKey
|
|
71109
|
+
};
|
|
71110
|
+
}
|
|
71111
|
+
var SessionPlacementStats = class {
|
|
71112
|
+
now;
|
|
71113
|
+
segments = [];
|
|
71114
|
+
starts = [];
|
|
71115
|
+
yields = [];
|
|
71116
|
+
startedAt;
|
|
71117
|
+
constructor(now = () => Date.now()) {
|
|
71118
|
+
this.now = now;
|
|
71119
|
+
this.startedAt = now();
|
|
71120
|
+
}
|
|
71121
|
+
/** The running-session set changed: open a new segment with these counts. */
|
|
71122
|
+
noteSessions(sessions) {
|
|
71123
|
+
const counts = /* @__PURE__ */ new Map();
|
|
71124
|
+
for (const s of sessions) {
|
|
71125
|
+
const k = keyOf(s.nodeId, s.deviceKey);
|
|
71126
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
71127
|
+
}
|
|
71128
|
+
this.segments.push({
|
|
71129
|
+
at: this.now(),
|
|
71130
|
+
counts
|
|
71131
|
+
});
|
|
71132
|
+
this.prune();
|
|
71133
|
+
}
|
|
71134
|
+
noteStart(nodeId, deviceKey) {
|
|
71135
|
+
this.starts.push({
|
|
71136
|
+
at: this.now(),
|
|
71137
|
+
nodeId,
|
|
71138
|
+
deviceKey
|
|
71139
|
+
});
|
|
71140
|
+
this.prune();
|
|
71141
|
+
}
|
|
71142
|
+
/** A share election handed `nodeId`'s turn to another node. */
|
|
71143
|
+
noteYield(nodeId) {
|
|
71144
|
+
this.yields.push({
|
|
71145
|
+
at: this.now(),
|
|
71146
|
+
nodeId
|
|
71147
|
+
});
|
|
71148
|
+
this.prune();
|
|
71149
|
+
}
|
|
71150
|
+
/** For tests: how many segments are retained. */
|
|
71151
|
+
segmentCount() {
|
|
71152
|
+
return this.segments.length;
|
|
71153
|
+
}
|
|
71154
|
+
prune() {
|
|
71155
|
+
const floor = this.now() - RETAIN_MS;
|
|
71156
|
+
while (this.segments.length > 1 && this.segments[1].at <= floor) this.segments.shift();
|
|
71157
|
+
while (this.starts.length > 0 && this.starts[0].at < floor) this.starts.shift();
|
|
71158
|
+
while (this.yields.length > 0 && this.yields[0].at < floor) this.yields.shift();
|
|
71159
|
+
}
|
|
71160
|
+
snapshot(windowMs) {
|
|
71161
|
+
const now = this.now();
|
|
71162
|
+
const from = now - windowMs;
|
|
71163
|
+
const integral = /* @__PURE__ */ new Map();
|
|
71164
|
+
let clusterPeak = 0;
|
|
71165
|
+
for (let i = 0; i < this.segments.length; i++) {
|
|
71166
|
+
const seg = this.segments[i];
|
|
71167
|
+
const end = this.segments[i + 1]?.at ?? now;
|
|
71168
|
+
const start = Math.max(seg.at, from);
|
|
71169
|
+
if (end <= start) continue;
|
|
71170
|
+
let total = 0;
|
|
71171
|
+
for (const [k, n] of seg.counts) {
|
|
71172
|
+
integral.set(k, (integral.get(k) ?? 0) + n * (end - start));
|
|
71173
|
+
total += n;
|
|
71174
|
+
}
|
|
71175
|
+
clusterPeak = Math.max(clusterPeak, total);
|
|
71176
|
+
}
|
|
71177
|
+
const current = this.segments.at(-1)?.counts ?? /* @__PURE__ */ new Map();
|
|
71178
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
71179
|
+
for (const k of [...integral.keys(), ...current.keys()]) nodeIds.add(split(k).nodeId);
|
|
71180
|
+
for (const s of this.starts) if (s.at >= from) nodeIds.add(s.nodeId);
|
|
71181
|
+
const clusterIntegral = [...integral.values()].reduce((a, b) => a + b, 0);
|
|
71182
|
+
const nodes = [...nodeIds].toSorted().map((nodeId) => {
|
|
71183
|
+
const deviceKeys = /* @__PURE__ */ new Set();
|
|
71184
|
+
for (const k of [...integral.keys(), ...current.keys()]) {
|
|
71185
|
+
const s = split(k);
|
|
71186
|
+
if (s.nodeId === nodeId) deviceKeys.add(s.deviceKey);
|
|
71187
|
+
}
|
|
71188
|
+
for (const s of this.starts) if (s.at >= from && s.nodeId === nodeId) deviceKeys.add(s.deviceKey);
|
|
71189
|
+
const nodeIntegral = [...deviceKeys].reduce((sum, d) => sum + (integral.get(keyOf(nodeId, d)) ?? 0), 0);
|
|
71190
|
+
const devices = [...deviceKeys].toSorted().map((deviceKey) => ({
|
|
71191
|
+
deviceKey,
|
|
71192
|
+
sessionsNow: current.get(keyOf(nodeId, deviceKey)) ?? 0,
|
|
71193
|
+
sessionsStarted: this.starts.filter((s) => s.at >= from && s.nodeId === nodeId && s.deviceKey === deviceKey).length,
|
|
71194
|
+
observedShare: nodeIntegral > 0 ? (integral.get(keyOf(nodeId, deviceKey)) ?? 0) / nodeIntegral : null
|
|
71195
|
+
}));
|
|
71196
|
+
let peak = 0;
|
|
71197
|
+
for (let i = 0; i < this.segments.length; i++) {
|
|
71198
|
+
const seg = this.segments[i];
|
|
71199
|
+
if ((this.segments[i + 1]?.at ?? now) <= Math.max(seg.at, from)) continue;
|
|
71200
|
+
let n = 0;
|
|
71201
|
+
for (const [k, c] of seg.counts) if (split(k).nodeId === nodeId) n += c;
|
|
71202
|
+
peak = Math.max(peak, n);
|
|
71203
|
+
}
|
|
71204
|
+
return {
|
|
71205
|
+
nodeId,
|
|
71206
|
+
sessionsNow: devices.reduce((s, d) => s + d.sessionsNow, 0),
|
|
71207
|
+
sessionsStarted: devices.reduce((s, d) => s + d.sessionsStarted, 0),
|
|
71208
|
+
observedShare: clusterIntegral > 0 && nodeIntegral > 0 ? nodeIntegral / clusterIntegral : null,
|
|
71209
|
+
peakSessions: peak,
|
|
71210
|
+
yieldsAway: this.yields.filter((y) => y.at >= from && y.nodeId === nodeId).length,
|
|
71211
|
+
devices
|
|
71212
|
+
};
|
|
71213
|
+
});
|
|
71214
|
+
return {
|
|
71215
|
+
observedForMs: Math.min(windowMs, now - this.startedAt),
|
|
71216
|
+
sessionsNow: [...current.values()].reduce((a, b) => a + b, 0),
|
|
71217
|
+
clusterPeakSessions: clusterPeak,
|
|
71218
|
+
nodes
|
|
71219
|
+
};
|
|
71220
|
+
}
|
|
71221
|
+
};
|
|
71222
|
+
//#endregion
|
|
71223
|
+
//#region src/load-balancing-shares.ts
|
|
71224
|
+
function nextDevices(stored, shares, effective) {
|
|
71225
|
+
let changed = false;
|
|
71226
|
+
const out = { ...stored };
|
|
71227
|
+
for (const [key, weight] of Object.entries(shares)) {
|
|
71228
|
+
const existing = stored?.[key];
|
|
71229
|
+
if (existing !== void 0) {
|
|
71230
|
+
out[key] = {
|
|
71231
|
+
...existing,
|
|
71232
|
+
weight
|
|
71233
|
+
};
|
|
71234
|
+
changed = true;
|
|
71235
|
+
continue;
|
|
71236
|
+
}
|
|
71237
|
+
const enabled = effective[key];
|
|
71238
|
+
if (enabled === void 0) continue;
|
|
71239
|
+
out[key] = {
|
|
71240
|
+
enabled,
|
|
71241
|
+
weight
|
|
71242
|
+
};
|
|
71243
|
+
changed = true;
|
|
71244
|
+
}
|
|
71245
|
+
return changed ? out : stored;
|
|
71246
|
+
}
|
|
71247
|
+
function applyLoadBalancingShares(prev, write) {
|
|
71248
|
+
const next = { ...prev };
|
|
71249
|
+
const nodeIds = new Set([...Object.keys(write.nodes), ...Object.keys(write.devices)]);
|
|
71250
|
+
for (const nodeId of nodeIds) {
|
|
71251
|
+
const existing = prev[nodeId];
|
|
71252
|
+
const nodeShare = write.nodes[nodeId];
|
|
71253
|
+
const devices = nextDevices(existing?.inferenceDevices, write.devices[nodeId] ?? {}, write.effectiveEnabled[nodeId] ?? {});
|
|
71254
|
+
if (nodeShare === void 0 && devices === existing?.inferenceDevices) continue;
|
|
71255
|
+
next[nodeId] = {
|
|
71256
|
+
...existing ?? { maxCameras: null },
|
|
71257
|
+
...nodeShare !== void 0 ? { detectWeight: nodeShare } : {},
|
|
71258
|
+
...devices !== void 0 ? { inferenceDevices: devices } : {}
|
|
71259
|
+
};
|
|
71260
|
+
}
|
|
71261
|
+
return next;
|
|
71262
|
+
}
|
|
71263
|
+
//#endregion
|
|
71264
|
+
//#region src/stale-device-pins.ts
|
|
71265
|
+
function findStaleDevicePins(pins, nodes) {
|
|
71266
|
+
const usable = /* @__PURE__ */ new Map();
|
|
71267
|
+
for (const node of nodes) for (const d of node.devices) usable.set(d.key, (usable.get(d.key) ?? false) || d.enabled && d.available);
|
|
71268
|
+
return pins.flatMap((pin) => {
|
|
71269
|
+
const state = usable.get(pin.deviceKey);
|
|
71270
|
+
if (state === true) return [];
|
|
71271
|
+
return [{
|
|
71272
|
+
deviceId: pin.deviceId,
|
|
71273
|
+
deviceKey: pin.deviceKey,
|
|
71274
|
+
reason: state === false ? "disabled" : "absent"
|
|
71275
|
+
}];
|
|
71276
|
+
}).toSorted((a, b) => a.deviceId - b.deviceId);
|
|
71277
|
+
}
|
|
71278
|
+
//#endregion
|
|
68786
71279
|
//#region src/index.ts
|
|
68787
71280
|
/**
|
|
68788
71281
|
* addon-pipeline-orchestrator — hub-side camera-to-agent load balancer.
|
|
@@ -68850,6 +71343,13 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
68850
71343
|
* not a claim this flag makes.
|
|
68851
71344
|
*/
|
|
68852
71345
|
clusterModelChoiceReady = false;
|
|
71346
|
+
/** The per-device 24 h peak of concurrent sessions, fed by every runner census (D745). */
|
|
71347
|
+
devicePeaks = new DevicePeakTracker();
|
|
71348
|
+
/** What session placement did (D748): the Load balancing tab's "actual" column. */
|
|
71349
|
+
placementStats = new SessionPlacementStats();
|
|
71350
|
+
/** The stored session placement policy (D748); `least-loaded` before init. */
|
|
71351
|
+
readSessionPlacement = () => "least-loaded";
|
|
71352
|
+
bestSettingsService = null;
|
|
68853
71353
|
/**
|
|
68854
71354
|
* Single owner of the shared placement state (`assignments`/`cameraConfigs`/
|
|
68855
71355
|
* `pendingReasons`/`dispatchingDevices`) — see `placement-ledger.ts` (S6
|
|
@@ -69126,6 +71626,8 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
69126
71626
|
rebalance: (opts) => this.rebalance(opts),
|
|
69127
71627
|
setCameraPipelineForAgent: (input) => this.setCameraPipelineForAgent(input),
|
|
69128
71628
|
maintenancePaused: () => this.storageMigrationLeaseId !== null,
|
|
71629
|
+
onRunnerCensus: (loads) => this.devicePeaks.noteCensus(loads),
|
|
71630
|
+
placementStats: this.placementStats,
|
|
69129
71631
|
setTopology: (topology) => {
|
|
69130
71632
|
this.topology = topology;
|
|
69131
71633
|
},
|
|
@@ -69152,6 +71654,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
69152
71654
|
this.nodeStress = controllers.nodeStress;
|
|
69153
71655
|
this.placement = controllers.placement;
|
|
69154
71656
|
this.session = controllers.session;
|
|
71657
|
+
this.readSessionPlacement = controllers.sessionPlacement;
|
|
69155
71658
|
this.deviceConfig = controllers.deviceConfig;
|
|
69156
71659
|
this.cameraStatusService = controllers.cameraStatusService;
|
|
69157
71660
|
this.cameraSwitchService = controllers.cameraSwitchService;
|
|
@@ -69267,7 +71770,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
69267
71770
|
for (const deviceId of this.detectionWiring.activeDetectionDeviceIds()) try {
|
|
69268
71771
|
await this.releaseCamera({ deviceId });
|
|
69269
71772
|
} catch (err) {
|
|
69270
|
-
const msg = errMsg$
|
|
71773
|
+
const msg = errMsg$2(err);
|
|
69271
71774
|
this.ctx.logger.debug("releaseCamera failed during shutdown", {
|
|
69272
71775
|
tags: { deviceId },
|
|
69273
71776
|
meta: { error: msg }
|
|
@@ -69644,26 +72147,6 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
69644
72147
|
this.reconcile.schedulePendingRetry();
|
|
69645
72148
|
return { success: true };
|
|
69646
72149
|
}
|
|
69647
|
-
async setAgentDetectWeight(input) {
|
|
69648
|
-
const existing = (await this.settingsStore.readAgentSettingsMap())[input.agentNodeId];
|
|
69649
|
-
const weight = input.detectWeight ?? 1;
|
|
69650
|
-
const next = existing ? {
|
|
69651
|
-
...existing,
|
|
69652
|
-
detectWeight: weight
|
|
69653
|
-
} : {
|
|
69654
|
-
maxCameras: null,
|
|
69655
|
-
detectWeight: weight
|
|
69656
|
-
};
|
|
69657
|
-
await this.settingsStore.writeAgentSettings(input.agentNodeId, next);
|
|
69658
|
-
this.ctx.logger.info("agentSettings.detectWeight updated", {
|
|
69659
|
-
tags: { nodeId: input.agentNodeId },
|
|
69660
|
-
meta: { detectWeight: weight ?? null }
|
|
69661
|
-
});
|
|
69662
|
-
this.rebalance().catch((err) => {
|
|
69663
|
-
this.ctx.logger.warn("setAgentDetectWeight: rebalance failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
69664
|
-
});
|
|
69665
|
-
return { success: true };
|
|
69666
|
-
}
|
|
69667
72150
|
/**
|
|
69668
72151
|
* Set one node's placement capabilities — the per-node record that replaced
|
|
69669
72152
|
* the four flat orchestrator lists. Each flag is a tri-state patch: omit to
|
|
@@ -70062,7 +72545,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70062
72545
|
tags: { nodeId },
|
|
70063
72546
|
meta: {
|
|
70064
72547
|
nodeId,
|
|
70065
|
-
error: errMsg$
|
|
72548
|
+
error: errMsg$2(err)
|
|
70066
72549
|
}
|
|
70067
72550
|
});
|
|
70068
72551
|
return null;
|
|
@@ -70168,7 +72651,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70168
72651
|
tags: { nodeId },
|
|
70169
72652
|
meta: {
|
|
70170
72653
|
nodeId,
|
|
70171
|
-
error: errMsg$
|
|
72654
|
+
error: errMsg$2(err)
|
|
70172
72655
|
}
|
|
70173
72656
|
});
|
|
70174
72657
|
return {
|
|
@@ -70215,7 +72698,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70215
72698
|
} catch (err) {
|
|
70216
72699
|
this.ctx.logger.warn("resetNodePipelineDefaults: executor clearDeviceOverrides failed (node offline?)", {
|
|
70217
72700
|
tags: { nodeId: node },
|
|
70218
|
-
meta: { error: errMsg$
|
|
72701
|
+
meta: { error: errMsg$2(err) }
|
|
70219
72702
|
});
|
|
70220
72703
|
}
|
|
70221
72704
|
this.settingsStore.invalidateCatalog();
|
|
@@ -70226,7 +72709,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70226
72709
|
} catch (err) {
|
|
70227
72710
|
this.ctx.logger.warn("resetNodePipelineDefaults: effective-model probe failed (node offline?)", {
|
|
70228
72711
|
tags: { nodeId: node },
|
|
70229
|
-
meta: { error: errMsg$
|
|
72712
|
+
meta: { error: errMsg$2(err) }
|
|
70230
72713
|
});
|
|
70231
72714
|
}
|
|
70232
72715
|
this.ctx.logger.info("node pipeline reset to hardware-aware defaults", {
|
|
@@ -70242,6 +72725,137 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70242
72725
|
clearedCameraOverrides
|
|
70243
72726
|
};
|
|
70244
72727
|
}
|
|
72728
|
+
bestSettings() {
|
|
72729
|
+
if (this.bestSettingsService !== null) return this.bestSettingsService;
|
|
72730
|
+
const store = this.settingsStore;
|
|
72731
|
+
this.bestSettingsService = new BestSettingsService({
|
|
72732
|
+
logger: this.ctx.logger,
|
|
72733
|
+
runnerNodeIds: () => this.topology.collectRunnerNodeIds(),
|
|
72734
|
+
readNodeDevices: (nodeId) => this.getNodeInferenceDevices({ nodeId }),
|
|
72735
|
+
readHardware: async (nodeId) => {
|
|
72736
|
+
const api = this.ctx.api;
|
|
72737
|
+
if (!api) return null;
|
|
72738
|
+
return nodeId === this.localNodeId ? api.platformProbe.getHardware.query() : api.platformProbe.getHardware.query({ nodeId });
|
|
72739
|
+
},
|
|
72740
|
+
readCatalog: (nodeId) => store.readCatalogForNode(nodeId),
|
|
72741
|
+
readAgentSettings: () => store.readAgentSettingsMap(),
|
|
72742
|
+
readCameraSettings: () => store.readCameraSettingsMap(),
|
|
72743
|
+
rewriteAgentSettings: (transform) => store.rewriteAgentSettingsMap(transform),
|
|
72744
|
+
rewriteCameraSettings: (transform) => store.rewriteCameraSettingsMap(transform),
|
|
72745
|
+
cameraLoads: () => this.bestSettingsCameraLoads(),
|
|
72746
|
+
peaks: () => this.devicePeaks.peaks(),
|
|
72747
|
+
clusterPeak: () => this.devicePeaks.clusterPeak(),
|
|
72748
|
+
sessionPlacement: () => this.readSessionPlacement(),
|
|
72749
|
+
readNodeShares: () => store.buildNodeWeights(),
|
|
72750
|
+
detectorStore: async (input) => {
|
|
72751
|
+
const api = this.ctx.api;
|
|
72752
|
+
if (!api) throw new Error("ctx.api unavailable");
|
|
72753
|
+
return api.pipelineExecutor.clearModelBoundDeviceOverrides.mutate({
|
|
72754
|
+
nodeId: this.localNodeId,
|
|
72755
|
+
dryRun: input.dryRun,
|
|
72756
|
+
...input.deviceIds !== void 0 ? { deviceIds: input.deviceIds } : {}
|
|
72757
|
+
});
|
|
72758
|
+
},
|
|
72759
|
+
downloadModel: async (nodeId, modelId, format) => {
|
|
72760
|
+
const api = this.ctx.api;
|
|
72761
|
+
if (!api) throw new Error("ctx.api unavailable");
|
|
72762
|
+
await api.pipelineExecutor.downloadModel.mutate({
|
|
72763
|
+
addonId: "object-detection",
|
|
72764
|
+
modelId,
|
|
72765
|
+
format
|
|
72766
|
+
}, nodePin(nodeId));
|
|
72767
|
+
},
|
|
72768
|
+
invalidateCatalog: () => store.invalidateCatalog(),
|
|
72769
|
+
redispatch: (nodeId) => this.placement.redispatchCamerasOnAgent(nodeId),
|
|
72770
|
+
now: () => Date.now()
|
|
72771
|
+
});
|
|
72772
|
+
return this.bestSettingsService;
|
|
72773
|
+
}
|
|
72774
|
+
/** Every camera with detection started, as the recommender charges it (D745). */
|
|
72775
|
+
async bestSettingsCameraLoads() {
|
|
72776
|
+
const standing = new Map(this.getPipelineAssignments().map((a) => [a.deviceId, a.agentNodeId]));
|
|
72777
|
+
const loads = [];
|
|
72778
|
+
for (const [deviceId, config] of this.detectionWiring.activeDetections) {
|
|
72779
|
+
const nodePin = await this.readPipelinePin(deviceId);
|
|
72780
|
+
const pinnedKey = await this.readPipelineDevicePin(deviceId);
|
|
72781
|
+
const standingNode = config.detectionMode === "always-on" ? standing.get(deviceId) ?? null : null;
|
|
72782
|
+
loads.push({
|
|
72783
|
+
deviceId,
|
|
72784
|
+
mode: config.detectionMode,
|
|
72785
|
+
fps: config.detectionFps,
|
|
72786
|
+
runsPipelineRoot: (config.detectionSources ?? ["pipeline"]).includes("pipeline"),
|
|
72787
|
+
standingNodeId: standingNode,
|
|
72788
|
+
nodePinId: nodePin,
|
|
72789
|
+
devicePin: pinnedKey !== null ? {
|
|
72790
|
+
nodeId: nodePin ?? standingNode,
|
|
72791
|
+
deviceKey: pinnedKey
|
|
72792
|
+
} : null
|
|
72793
|
+
});
|
|
72794
|
+
}
|
|
72795
|
+
return loads;
|
|
72796
|
+
}
|
|
72797
|
+
async planBestSettings(input) {
|
|
72798
|
+
return this.bestSettings().plan(input);
|
|
72799
|
+
}
|
|
72800
|
+
/**
|
|
72801
|
+
* Node + device shares in ONE read-modify-write of the agent settings. A
|
|
72802
|
+
* device with no stored entry is made explicit with its effective
|
|
72803
|
+
* enablement, read from the probe-aware device list. Never touches the
|
|
72804
|
+
* `sessionPlacement` policy.
|
|
72805
|
+
*/
|
|
72806
|
+
async setLoadBalancingShares(input) {
|
|
72807
|
+
const effectiveEnabled = {};
|
|
72808
|
+
for (const nodeId of Object.keys(input.devices)) {
|
|
72809
|
+
const read = await this.getNodeInferenceDevices({ nodeId });
|
|
72810
|
+
effectiveEnabled[nodeId] = Object.fromEntries(read.devices.map((d) => [d.key, d.enabled]));
|
|
72811
|
+
}
|
|
72812
|
+
await this.settingsStore.rewriteAgentSettingsMap((prev) => applyLoadBalancingShares(prev, {
|
|
72813
|
+
nodes: input.nodes,
|
|
72814
|
+
devices: input.devices,
|
|
72815
|
+
effectiveEnabled
|
|
72816
|
+
}));
|
|
72817
|
+
this.ctx.logger.info("load balancing shares updated", { meta: {
|
|
72818
|
+
nodes: JSON.stringify(input.nodes),
|
|
72819
|
+
devices: JSON.stringify(input.devices),
|
|
72820
|
+
policy: this.readSessionPlacement()
|
|
72821
|
+
} });
|
|
72822
|
+
if (Object.keys(input.nodes).length > 0) this.rebalance().catch((err) => {
|
|
72823
|
+
this.ctx.logger.warn("setLoadBalancingShares: rebalance failed", { meta: { error: errMsg$2(err) } });
|
|
72824
|
+
});
|
|
72825
|
+
return { success: true };
|
|
72826
|
+
}
|
|
72827
|
+
async getPlacementStats(input) {
|
|
72828
|
+
const snap = this.placementStats.snapshot(input.windowMs);
|
|
72829
|
+
return {
|
|
72830
|
+
policy: this.readSessionPlacement(),
|
|
72831
|
+
windowMs: input.windowMs,
|
|
72832
|
+
observedForMs: snap.observedForMs,
|
|
72833
|
+
sessionsNow: snap.sessionsNow,
|
|
72834
|
+
clusterPeakSessions: snap.clusterPeakSessions,
|
|
72835
|
+
nodes: snap.nodes,
|
|
72836
|
+
stalePins: await this.listStaleDevicePins()
|
|
72837
|
+
};
|
|
72838
|
+
}
|
|
72839
|
+
/** Device pins the session path cannot honour — see `stale-device-pins.ts`. */
|
|
72840
|
+
async listStaleDevicePins() {
|
|
72841
|
+
const nodeIds = [...new Set(["hub", ...this.topology.collectRunnerNodeIds()])];
|
|
72842
|
+
const offered = await Promise.all(nodeIds.map(async (nodeId) => ({ devices: (await this.getNodeInferenceDevices({ nodeId }).catch(() => null))?.devices ?? [] })));
|
|
72843
|
+
const pins = [];
|
|
72844
|
+
for (const deviceId of this.detectionWiring.activeDetections.keys()) {
|
|
72845
|
+
const deviceKey = await this.readPipelineDevicePin(deviceId);
|
|
72846
|
+
if (deviceKey !== null) pins.push({
|
|
72847
|
+
deviceId,
|
|
72848
|
+
deviceKey
|
|
72849
|
+
});
|
|
72850
|
+
}
|
|
72851
|
+
return findStaleDevicePins(pins, offered);
|
|
72852
|
+
}
|
|
72853
|
+
async suggestShares(input) {
|
|
72854
|
+
return this.bestSettings().suggestShares(input);
|
|
72855
|
+
}
|
|
72856
|
+
async applyBestSettings(input) {
|
|
72857
|
+
return this.bestSettings().apply(input);
|
|
72858
|
+
}
|
|
70245
72859
|
async getCameraSettings(input) {
|
|
70246
72860
|
return (await this.settingsStore.readCameraSettingsMap())[String(input.deviceId)] ?? null;
|
|
70247
72861
|
}
|
|
@@ -70498,7 +73112,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
70498
73112
|
tags: { deviceId },
|
|
70499
73113
|
meta: {
|
|
70500
73114
|
capName,
|
|
70501
|
-
error: errMsg$
|
|
73115
|
+
error: errMsg$2(err)
|
|
70502
73116
|
}
|
|
70503
73117
|
});
|
|
70504
73118
|
return true;
|