@camstack/addon-pipeline-orchestrator 1.2.55 → 1.2.57
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/assets/viewer/.viewer-version +1 -1
- package/assets/viewer/_expo/static/js/web/{Clipboard-dbfb8d2b9b663367bf606c4e4156c5ed.js → Clipboard-2a9c34a2d9e4522a4b07afe082f73a11.js} +8 -8
- package/assets/viewer/_expo/static/js/web/{Haptics-3fc6898fa899589be5efd6c4561b70a1.js → Haptics-d691ef5c23765594fc76d7d0159e12a9.js} +3 -3
- package/assets/viewer/_expo/static/js/web/{index-f066fed82c1011c63951f43a4c81626f.js → index-cdddfaafe98f9abb2eed34c2d3ea7fce.js} +80 -78
- package/assets/viewer/_expo/static/js/web/{node-fs-3444d48a73ceb72dd0e6a45d4ba686f4.js → node-fs-fbe6ecbf78f47e60bf5606ef62613a4d.js} +1 -1
- package/assets/viewer/_expo/static/js/web/{node-path-31d2d4f03c0bb8e91685e0b2170248e7.js → node-path-88d7caa558342a85e438efa999e4aecd.js} +1 -1
- package/assets/viewer/embed/assets/{MaskShapeCanvas-DI4BY7W2-BPL877bq.js → MaskShapeCanvas-DI4BY7W2-BG9b15XX.js} +1 -1
- package/assets/viewer/embed/assets/{MotionZonesSettings-NcxxQN8r-9o1vTDhU.js → MotionZonesSettings-NcxxQN8r-6tRLDb49.js} +1 -1
- package/assets/viewer/embed/assets/{PrivacyMaskSettings-APgPLF7p-BgXKxbFU.js → PrivacyMaskSettings-APgPLF7p-Bbna4XaY.js} +1 -1
- package/assets/viewer/embed/assets/index-IodqkuCI.js +116 -0
- package/assets/viewer/embed/index.html +1 -1
- package/assets/viewer/index.html +1 -1
- package/dist/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DQ-9VJzz.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-D4ElH3_I.mjs} +3 -3
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B5aOYEp_.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CoB82Qob.mjs} +1 -1
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DUDj4-xm.mjs +26 -0
- package/dist/{hostInit-DEefQ-oR.mjs → hostInit-CDG9B2az.mjs} +3 -3
- package/dist/index.js +596 -12
- package/dist/index.mjs +596 -12
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/assets/viewer/embed/assets/index-CN50_nGz.js +0 -116
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Dea44qi5.mjs +0 -26
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
//#region ../types/dist/event-category-
|
|
5
|
+
//#region ../types/dist/event-category-Bxo5yJjt.mjs
|
|
5
6
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
6
7
|
EventCategory["SystemBoot"] = "system.boot";
|
|
7
8
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -208,6 +209,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
208
209
|
EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
|
|
209
210
|
EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
|
|
210
211
|
/**
|
|
212
|
+
* A node the orchestrator would otherwise place cameras on has NO usable
|
|
213
|
+
* inference device: the operator enabled one or more accelerators there and
|
|
214
|
+
* the live probe reports every one of them unavailable. Emitted once per
|
|
215
|
+
* TRANSITION into that state (never per dispatch), and the node is dropped
|
|
216
|
+
* from the placement candidate set for as long as it holds.
|
|
217
|
+
*
|
|
218
|
+
* This exists because the state was previously invisible: little-unraid
|
|
219
|
+
* absorbed 283k inference errors in a day while still being handed cameras,
|
|
220
|
+
* and nothing in the system said so.
|
|
221
|
+
*
|
|
222
|
+
* A node with no accelerators configured at all is NOT this — its devices
|
|
223
|
+
* are `disabled`, not `unavailable`, and the runner's default CPU pool
|
|
224
|
+
* serves it exactly as before.
|
|
225
|
+
*/
|
|
226
|
+
EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
|
|
227
|
+
/**
|
|
228
|
+
* A camera has an OPEN detection session and has produced no detection at
|
|
229
|
+
* all for longer than the blind threshold — the camera is being decoded and
|
|
230
|
+
* inferred and is returning nothing. Emitted once per transition into blind,
|
|
231
|
+
* per camera.
|
|
232
|
+
*
|
|
233
|
+
* The failure it reports: a 1h43 detection blackout on the entrance camera
|
|
234
|
+
* that nobody noticed, because "a camera that detects nothing" and "a quiet
|
|
235
|
+
* camera" produce byte-identical silence.
|
|
236
|
+
*/
|
|
237
|
+
EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
|
|
238
|
+
/**
|
|
211
239
|
* Per-camera pipeline config was mutated by the orchestrator
|
|
212
240
|
* (3-level settings change via `setAgentAddonDefaults` /
|
|
213
241
|
* `setCameraStepToggle` / `setCameraPipelineForAgent` or a
|
|
@@ -5372,6 +5400,12 @@ Object.fromEntries([
|
|
|
5372
5400
|
icon: "shapes",
|
|
5373
5401
|
order: 38
|
|
5374
5402
|
},
|
|
5403
|
+
{
|
|
5404
|
+
id: "scenes",
|
|
5405
|
+
label: "Scenes",
|
|
5406
|
+
icon: "scan-eye",
|
|
5407
|
+
order: 36
|
|
5408
|
+
},
|
|
5375
5409
|
{
|
|
5376
5410
|
id: "analytics",
|
|
5377
5411
|
label: "Analytics",
|
|
@@ -14899,6 +14933,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14899
14933
|
"stream-offline",
|
|
14900
14934
|
"node-online",
|
|
14901
14935
|
"node-offline",
|
|
14936
|
+
"node-inference-unavailable",
|
|
14937
|
+
"detection-blind",
|
|
14902
14938
|
"addon-update-available",
|
|
14903
14939
|
"server-update-available",
|
|
14904
14940
|
"alarm-triggered",
|
|
@@ -15670,7 +15706,50 @@ var NcRuleInputSchema = object({
|
|
|
15670
15706
|
* tests every zone the track visited and a `crossing` condition can no longer
|
|
15671
15707
|
* be satisfied, because a closed track carries no crossing.
|
|
15672
15708
|
*/
|
|
15673
|
-
waitForEnhancement: boolean().optional()
|
|
15709
|
+
waitForEnhancement: boolean().optional(),
|
|
15710
|
+
/**
|
|
15711
|
+
* GROUP a burst of subjects into ONE notification that grows.
|
|
15712
|
+
*
|
|
15713
|
+
* Seconds of quiet after the last matching subject before the burst is
|
|
15714
|
+
* considered over. While it is open, the first subject enqueues immediately —
|
|
15715
|
+
* **exactly as today, with no added latency** — and every real growth (a new
|
|
15716
|
+
* subject, or a name confirmed on one already in it) REPLACES that
|
|
15717
|
+
* notification with an updated one naming everybody. The push carries the
|
|
15718
|
+
* group's own coalescing tag, so the phone replaces rather than stacks.
|
|
15719
|
+
*
|
|
15720
|
+
* `0` / absent = off, and off is today's behaviour byte for byte.
|
|
15721
|
+
*
|
|
15722
|
+
* ### Why an idle cutoff and not a window
|
|
15723
|
+
*
|
|
15724
|
+
* The measured seven-person arrival on device 590 spans 110 s with every
|
|
15725
|
+
* internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
|
|
15726
|
+
* idle cutoff holds it as one and ends it when the arrival actually ends.
|
|
15727
|
+
* 30 is Frigate's shipped value for the same decision.
|
|
15728
|
+
*
|
|
15729
|
+
* ### What it replaces
|
|
15730
|
+
*
|
|
15731
|
+
* The blind cooldown, which collapses a burst by DISCARDING it. Measured on
|
|
15732
|
+
* device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
|
|
15733
|
+
* notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
|
|
15734
|
+
* track that did fire and 7 carrying a confirmed identity nobody heard about.
|
|
15735
|
+
* A group collapses the same volume by MERGING, so the cooldown becomes a
|
|
15736
|
+
* budget over GROUPS — which is what it always meant — and a growth is never
|
|
15737
|
+
* throttled by the window its own first member spent.
|
|
15738
|
+
*
|
|
15739
|
+
* ### Interaction with {@link waitForEnhancement}
|
|
15740
|
+
*
|
|
15741
|
+
* They compose, and the order matters. `waitForEnhancement` defers the rule to
|
|
15742
|
+
* TRACK CLOSE, so with both set the group is opened by the first member to
|
|
15743
|
+
* CLOSE — already carrying its name — and grows as later members close. That
|
|
15744
|
+
* is later, and complete. With grouping alone the group opens on the first
|
|
15745
|
+
* object event and picks up names as they are confirmed, through the growth
|
|
15746
|
+
* path. Neither combination fires twice for one subject.
|
|
15747
|
+
*
|
|
15748
|
+
* `.optional()` and deliberately NOT `.default()`: a Zod default does not run
|
|
15749
|
+
* on the addon cap path, so absent must keep meaning what it meant before this
|
|
15750
|
+
* field existed.
|
|
15751
|
+
*/
|
|
15752
|
+
groupIdleSec: number().int().min(0).max(600).optional()
|
|
15674
15753
|
});
|
|
15675
15754
|
/**
|
|
15676
15755
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -16596,7 +16675,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16596
16675
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16597
16676
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16598
16677
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16599
|
-
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
16678
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16600
16679
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16601
16680
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16602
16681
|
* (`getObjectEvents` et al.).
|
|
@@ -16883,6 +16962,28 @@ var TrackSchema = object({
|
|
|
16883
16962
|
* `=== true` and render nothing otherwise, never infer "no face".
|
|
16884
16963
|
*/
|
|
16885
16964
|
hasFace: boolean().optional(),
|
|
16965
|
+
/**
|
|
16966
|
+
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16967
|
+
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16968
|
+
* so the passage is tracked once and as a VEHICLE.
|
|
16969
|
+
*
|
|
16970
|
+
* It exists because the fold's record was dishonest. D34 and the code both
|
|
16971
|
+
* said "the person is not lost — it is reported so both entities stay on the
|
|
16972
|
+
* record"; in fact the pair went into a per-processor RAM field behind an
|
|
16973
|
+
* accessor nobody called, and every durable surface said `vehicle`, full
|
|
16974
|
+
* stop. This is the composition note that makes the row true.
|
|
16975
|
+
*
|
|
16976
|
+
* A COMPOSITION, never a class and never a label. "This vehicle contains a
|
|
16977
|
+
* person" is not an answer to "what is this" — both label tiers would refuse
|
|
16978
|
+
* a macro token anyway (D89), and correctly. Nothing here changes what the
|
|
16979
|
+
* subject IS: a cyclist stays one vehicle track, occupancy still counts one,
|
|
16980
|
+
* and a `person` rule still does not fire for someone cycling past.
|
|
16981
|
+
*
|
|
16982
|
+
* **Absent ≠ false**, exactly like {@link hasFace}: every row written before
|
|
16983
|
+
* the column, and every hub that predates the field, omits it. Test
|
|
16984
|
+
* `=== true` and render nothing otherwise — never infer "no rider".
|
|
16985
|
+
*/
|
|
16986
|
+
hasRider: boolean().optional(),
|
|
16886
16987
|
...TrackFlagFields,
|
|
16887
16988
|
...TrackRetrainFields
|
|
16888
16989
|
});
|
|
@@ -25579,9 +25680,33 @@ method(object({
|
|
|
25579
25680
|
* settings-contribution methods. `status.kind:'push'` — the engine pushes on
|
|
25580
25681
|
* every hysteresis flip / availability change; consumers never poll.
|
|
25581
25682
|
*/
|
|
25582
|
-
/** Extensible condition tag. Seeded 'day' | '
|
|
25583
|
-
*
|
|
25683
|
+
/** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
|
|
25684
|
+
* captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
|
|
25685
|
+
* Open by design so more can be added without a wire break.
|
|
25686
|
+
*
|
|
25687
|
+
* Matching does NOT fall back across conditions: cross-condition cosines are
|
|
25688
|
+
* not comparable, so "I have never seen this scene in this light" is reported
|
|
25689
|
+
* as `unknown`, never guessed. A day reference scored against an IR frame
|
|
25690
|
+
* collapses the cosine and would latch a false alarm every single night. */
|
|
25584
25691
|
var SceneConditionSchema = string();
|
|
25692
|
+
/** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
|
|
25693
|
+
* `unknown` = we cannot judge (no reference for this condition, encoder model
|
|
25694
|
+
* changed, view shifted, no snapshot). `unknown` is a real value, not a null,
|
|
25695
|
+
* and never counts toward hysteresis in either direction. */
|
|
25696
|
+
var SceneVerdictSchema = _enum([
|
|
25697
|
+
"matched",
|
|
25698
|
+
"diverged",
|
|
25699
|
+
"unknown"
|
|
25700
|
+
]);
|
|
25701
|
+
/** Why a scene cannot judge. Named, because this feature's failure mode is
|
|
25702
|
+
* silence that reads as "nothing has happened". */
|
|
25703
|
+
var SceneUnavailableSchema = _enum([
|
|
25704
|
+
"no-reference-for-condition",
|
|
25705
|
+
"view-shifted",
|
|
25706
|
+
"no-vision-profile",
|
|
25707
|
+
"encoder-model-changed",
|
|
25708
|
+
"no-snapshot"
|
|
25709
|
+
]);
|
|
25585
25710
|
/** One captured reference — condition-tagged, model-version-gated. `embedding`
|
|
25586
25711
|
* is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
|
|
25587
25712
|
var SceneReferenceSchema = object({
|
|
@@ -25589,7 +25714,14 @@ var SceneReferenceSchema = object({
|
|
|
25589
25714
|
modelId: string(),
|
|
25590
25715
|
condition: SceneConditionSchema,
|
|
25591
25716
|
capturedAt: number(),
|
|
25592
|
-
thumbnailMediaId: string().optional()
|
|
25717
|
+
thumbnailMediaId: string().optional(),
|
|
25718
|
+
/** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
|
|
25719
|
+
* anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
|
|
25720
|
+
* normalized rect frame a different piece of world, and the scene would
|
|
25721
|
+
* diverge forever with a perfectly plausible cosine. Checked LAZILY, only
|
|
25722
|
+
* when hysteresis is about to flip — one extra encode per candidate
|
|
25723
|
+
* transition, not per poll. */
|
|
25724
|
+
anchorEmbedding: array(number()).optional()
|
|
25593
25725
|
});
|
|
25594
25726
|
var SceneMonitorStateSchema = object({
|
|
25595
25727
|
id: string(),
|
|
@@ -25611,6 +25743,25 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
|
|
|
25611
25743
|
profileId: string().optional(),
|
|
25612
25744
|
hysteresisCount: number().int().positive()
|
|
25613
25745
|
})]);
|
|
25746
|
+
var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
|
|
25747
|
+
/**
|
|
25748
|
+
* Vision-model adjudication of a candidate flip. Field names deliberately
|
|
25749
|
+
* mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
|
|
25750
|
+
*
|
|
25751
|
+
* `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
|
|
25752
|
+
* fail-open: a notification suppressed is the worse error there, but a vision
|
|
25753
|
+
* model that timed out has not told us the bin is gone, and a latch is a
|
|
25754
|
+
* stateful claim that costs the operator a trip to reset.
|
|
25755
|
+
*/
|
|
25756
|
+
var SceneConfirmSchema = object({
|
|
25757
|
+
enabled: boolean().default(false),
|
|
25758
|
+
prompt: string().min(1).max(1e3),
|
|
25759
|
+
profileId: string().optional(),
|
|
25760
|
+
timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
|
|
25761
|
+
maxImagePx: number().int().min(64).max(2048).default(448),
|
|
25762
|
+
/** What a timeout / unavailable model means for the PENDING flip. */
|
|
25763
|
+
onTimeout: _enum(["flip", "hold"]).default("hold")
|
|
25764
|
+
});
|
|
25614
25765
|
var SceneMonitorSchema = object({
|
|
25615
25766
|
id: string(),
|
|
25616
25767
|
label: string(),
|
|
@@ -25629,7 +25780,41 @@ var SceneMonitorSchema = object({
|
|
|
25629
25780
|
lastConfidence: number().nullable(),
|
|
25630
25781
|
currentCondition: SceneConditionSchema.nullable(),
|
|
25631
25782
|
availability: _enum(["ok", "unavailable"]),
|
|
25632
|
-
unavailableReason: string().nullable()
|
|
25783
|
+
unavailableReason: string().nullable(),
|
|
25784
|
+
/** Which state is "the initial screen". `null` until the first capture. */
|
|
25785
|
+
baselineStateId: string().nullable(),
|
|
25786
|
+
/** Which boolean drives notification rules and any export. */
|
|
25787
|
+
emit: _enum(["latched", "live"]).default("latched"),
|
|
25788
|
+
/** Live: does the region match the baseline RIGHT NOW. */
|
|
25789
|
+
verdict: SceneVerdictSchema,
|
|
25790
|
+
/** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
|
|
25791
|
+
latched: boolean(),
|
|
25792
|
+
/** Last reset (or creation). */
|
|
25793
|
+
armedAt: number(),
|
|
25794
|
+
divergedAt: number().nullable(),
|
|
25795
|
+
restoredAt: number().nullable(),
|
|
25796
|
+
/** A check is only COUNTED when the device has been quiet this long. Motion
|
|
25797
|
+
* during the window DISCARDS the observation — a car pulling up in front of
|
|
25798
|
+
* the bin must not be able to spend hysteresis credit. */
|
|
25799
|
+
quietSeconds: number().int().min(0).max(3600).default(60),
|
|
25800
|
+
/** An observation only advances the pending count when it is at least this
|
|
25801
|
+
* far from the previously counted one, so N agreeing checks span real time
|
|
25802
|
+
* rather than N adjacent polls inside one occlusion. */
|
|
25803
|
+
minObservationSpacingSec: number().int().min(0).max(3600).default(120),
|
|
25804
|
+
/** Vision-model adjudication of a candidate flip. Similarity primary only. */
|
|
25805
|
+
confirm: SceneConfirmSchema.optional(),
|
|
25806
|
+
/** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
|
|
25807
|
+
anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
|
|
25808
|
+
/** Clear the latch on its own when the scene matches again? Default false —
|
|
25809
|
+
* `restoredAt` and the `scene-restored` edge are recorded regardless, so an
|
|
25810
|
+
* automation can react to the bin coming back without the operator's own
|
|
25811
|
+
* alarm silently clearing itself. */
|
|
25812
|
+
autoRestore: boolean().default(false),
|
|
25813
|
+
/** Named cause when `verdict === 'unknown'`. */
|
|
25814
|
+
unavailable: SceneUnavailableSchema.nullable(),
|
|
25815
|
+
/** Conditions that have at least one comparable reference — the coverage line
|
|
25816
|
+
* ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
|
|
25817
|
+
coveredConditions: array(SceneConditionSchema)
|
|
25633
25818
|
});
|
|
25634
25819
|
var SceneMonitorStatusSchema = object({
|
|
25635
25820
|
monitors: array(SceneMonitorSchema),
|
|
@@ -25662,7 +25847,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
|
|
|
25662
25847
|
"both"
|
|
25663
25848
|
]).optional(),
|
|
25664
25849
|
checkIntervalSec: number().optional(),
|
|
25665
|
-
check: SceneCheckSchema.optional()
|
|
25850
|
+
check: SceneCheckSchema.optional(),
|
|
25851
|
+
emit: _enum(["latched", "live"]).optional(),
|
|
25852
|
+
quietSeconds: number().int().min(0).max(3600).optional(),
|
|
25853
|
+
minObservationSpacingSec: number().int().min(0).max(3600).optional(),
|
|
25854
|
+
anchorThreshold: number().min(0).max(1).optional(),
|
|
25855
|
+
autoRestore: boolean().optional(),
|
|
25856
|
+
/** `null` clears the vision-model adjudicator. */
|
|
25857
|
+
confirm: SceneConfirmSchema.nullable().optional()
|
|
25666
25858
|
})
|
|
25667
25859
|
}), _void(), {
|
|
25668
25860
|
kind: "mutation",
|
|
@@ -25699,6 +25891,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
|
|
|
25699
25891
|
}), _void(), {
|
|
25700
25892
|
kind: "mutation",
|
|
25701
25893
|
auth: "admin"
|
|
25894
|
+
}), method(object({
|
|
25895
|
+
deviceId: number(),
|
|
25896
|
+
monitorId: string(),
|
|
25897
|
+
/** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
|
|
25898
|
+
recapture: boolean().optional()
|
|
25899
|
+
}), _void(), {
|
|
25900
|
+
kind: "mutation",
|
|
25901
|
+
auth: "admin"
|
|
25702
25902
|
});
|
|
25703
25903
|
/**
|
|
25704
25904
|
* Per-stage gating mode applied to the zones a rule references.
|
|
@@ -31076,6 +31276,12 @@ Object.freeze({
|
|
|
31076
31276
|
addonId: null,
|
|
31077
31277
|
access: "create"
|
|
31078
31278
|
},
|
|
31279
|
+
"sceneMonitor.resetScene": {
|
|
31280
|
+
capName: "scene-monitor",
|
|
31281
|
+
capScope: "device",
|
|
31282
|
+
addonId: null,
|
|
31283
|
+
access: "delete"
|
|
31284
|
+
},
|
|
31079
31285
|
"sceneMonitor.updateScene": {
|
|
31080
31286
|
capName: "scene-monitor",
|
|
31081
31287
|
capScope: "device",
|
|
@@ -33758,6 +33964,11 @@ Object.freeze({
|
|
|
33758
33964
|
form: "single",
|
|
33759
33965
|
optional: false
|
|
33760
33966
|
}],
|
|
33967
|
+
"sceneMonitor.resetScene": [{
|
|
33968
|
+
name: "deviceId",
|
|
33969
|
+
form: "single",
|
|
33970
|
+
optional: false
|
|
33971
|
+
}],
|
|
33761
33972
|
"sceneMonitor.updateScene": [{
|
|
33762
33973
|
name: "deviceId",
|
|
33763
33974
|
form: "single",
|
|
@@ -34777,6 +34988,15 @@ function resolveInferenceDeviceEligibility(probed, stored, canRunRoot) {
|
|
|
34777
34988
|
excluded
|
|
34778
34989
|
};
|
|
34779
34990
|
}
|
|
34991
|
+
function resolveNodeInferenceUsability(eligibility) {
|
|
34992
|
+
const eligibleKeys = Object.keys(eligibility.eligible).toSorted();
|
|
34993
|
+
const unavailableKeys = eligibility.excluded.filter((e) => e.reason === "unavailable").map((e) => e.key).toSorted();
|
|
34994
|
+
return {
|
|
34995
|
+
usable: eligibleKeys.length > 0 || unavailableKeys.length === 0,
|
|
34996
|
+
unavailableKeys,
|
|
34997
|
+
eligibleKeys
|
|
34998
|
+
};
|
|
34999
|
+
}
|
|
34780
35000
|
/**
|
|
34781
35001
|
* Step-tree device jump (phase 1): the attach-payload roster of a node's
|
|
34782
35002
|
* enabled∧available inference devices with the balancer knobs (`weight`,
|
|
@@ -34795,6 +35015,67 @@ function buildInferenceDeviceRoster(eligible, caps) {
|
|
|
34795
35015
|
})).toSorted((a, b) => a.deviceKey < b.deviceKey ? -1 : a.deviceKey > b.deviceKey ? 1 : 0);
|
|
34796
35016
|
}
|
|
34797
35017
|
//#endregion
|
|
35018
|
+
//#region src/node-inference-usability-mirror.ts
|
|
35019
|
+
var NodeInferenceUsabilityMirror = class {
|
|
35020
|
+
state = /* @__PURE__ */ new Map();
|
|
35021
|
+
/**
|
|
35022
|
+
* Fold one observation in and report whether the caller should act.
|
|
35023
|
+
* Never throws.
|
|
35024
|
+
*/
|
|
35025
|
+
observe(nodeId, usable) {
|
|
35026
|
+
const prev = this.state.get(nodeId);
|
|
35027
|
+
if (usable) {
|
|
35028
|
+
this.state.set(nodeId, {
|
|
35029
|
+
usable: true,
|
|
35030
|
+
armed: false
|
|
35031
|
+
});
|
|
35032
|
+
return prev !== void 0 && !prev.usable ? "recovered" : null;
|
|
35033
|
+
}
|
|
35034
|
+
if (prev === void 0) {
|
|
35035
|
+
this.state.set(nodeId, {
|
|
35036
|
+
usable: true,
|
|
35037
|
+
armed: true
|
|
35038
|
+
});
|
|
35039
|
+
return null;
|
|
35040
|
+
}
|
|
35041
|
+
if (!prev.usable) {
|
|
35042
|
+
this.state.set(nodeId, {
|
|
35043
|
+
usable: false,
|
|
35044
|
+
armed: true
|
|
35045
|
+
});
|
|
35046
|
+
return null;
|
|
35047
|
+
}
|
|
35048
|
+
if (!prev.armed) {
|
|
35049
|
+
this.state.set(nodeId, {
|
|
35050
|
+
usable: true,
|
|
35051
|
+
armed: true
|
|
35052
|
+
});
|
|
35053
|
+
return null;
|
|
35054
|
+
}
|
|
35055
|
+
this.state.set(nodeId, {
|
|
35056
|
+
usable: false,
|
|
35057
|
+
armed: true
|
|
35058
|
+
});
|
|
35059
|
+
return "became-unusable";
|
|
35060
|
+
}
|
|
35061
|
+
/** Can this node be given cameras? Unknown nodes answer YES. */
|
|
35062
|
+
isUsable(nodeId) {
|
|
35063
|
+
return this.state.get(nodeId)?.usable ?? true;
|
|
35064
|
+
}
|
|
35065
|
+
/** Nodes currently excluded — for the placement log and diagnostics. */
|
|
35066
|
+
unusableNodeIds() {
|
|
35067
|
+
const out = [];
|
|
35068
|
+
for (const [nodeId, s] of this.state) if (!s.usable) out.push(nodeId);
|
|
35069
|
+
return out.toSorted();
|
|
35070
|
+
}
|
|
35071
|
+
forget(nodeId) {
|
|
35072
|
+
this.state.delete(nodeId);
|
|
35073
|
+
}
|
|
35074
|
+
reset() {
|
|
35075
|
+
this.state.clear();
|
|
35076
|
+
}
|
|
35077
|
+
};
|
|
35078
|
+
//#endregion
|
|
34798
35079
|
//#region src/orchestrator-types.ts
|
|
34799
35080
|
var PHASE_MODE_VALUES = new Set([
|
|
34800
35081
|
"disabled",
|
|
@@ -36305,6 +36586,69 @@ var LoadShedController = class LoadShedController {
|
|
|
36305
36586
|
}
|
|
36306
36587
|
}
|
|
36307
36588
|
};
|
|
36589
|
+
var DEFAULT_DETECTION_BLIND_CONFIG = {
|
|
36590
|
+
thresholdMs: 60 * 6e4,
|
|
36591
|
+
frameFreshnessMs: 5 * 6e4
|
|
36592
|
+
};
|
|
36593
|
+
var DetectionBlindMonitor = class {
|
|
36594
|
+
config;
|
|
36595
|
+
state = /* @__PURE__ */ new Map();
|
|
36596
|
+
constructor(config = DEFAULT_DETECTION_BLIND_CONFIG) {
|
|
36597
|
+
this.config = config;
|
|
36598
|
+
}
|
|
36599
|
+
/**
|
|
36600
|
+
* One inference frame arrived for a device. `hadDetection` is false for a
|
|
36601
|
+
* frame that returned nothing — which is the whole point: an empty frame
|
|
36602
|
+
* still proves the pipeline is alive, and it is the ONLY evidence that
|
|
36603
|
+
* distinguishes blind from detached.
|
|
36604
|
+
*/
|
|
36605
|
+
noteFrame(deviceId, hadDetection, now) {
|
|
36606
|
+
const prev = this.state.get(deviceId);
|
|
36607
|
+
if (hadDetection) {
|
|
36608
|
+
this.state.set(deviceId, {
|
|
36609
|
+
lastFrameAt: now,
|
|
36610
|
+
lastDetectionAt: now,
|
|
36611
|
+
reported: false
|
|
36612
|
+
});
|
|
36613
|
+
return;
|
|
36614
|
+
}
|
|
36615
|
+
this.state.set(deviceId, {
|
|
36616
|
+
lastFrameAt: now,
|
|
36617
|
+
lastDetectionAt: prev?.lastDetectionAt ?? now,
|
|
36618
|
+
reported: prev?.reported ?? false
|
|
36619
|
+
});
|
|
36620
|
+
}
|
|
36621
|
+
/**
|
|
36622
|
+
* Devices that have JUST crossed into blind. Call from an existing periodic
|
|
36623
|
+
* sweep; it allocates nothing on the healthy path.
|
|
36624
|
+
*/
|
|
36625
|
+
sweep(now) {
|
|
36626
|
+
const out = [];
|
|
36627
|
+
for (const [deviceId, s] of this.state) {
|
|
36628
|
+
if (s.reported) continue;
|
|
36629
|
+
if (now - s.lastFrameAt > this.config.frameFreshnessMs) continue;
|
|
36630
|
+
const blindForMs = now - s.lastDetectionAt;
|
|
36631
|
+
if (blindForMs < this.config.thresholdMs) continue;
|
|
36632
|
+
this.state.set(deviceId, {
|
|
36633
|
+
...s,
|
|
36634
|
+
reported: true
|
|
36635
|
+
});
|
|
36636
|
+
out.push({
|
|
36637
|
+
deviceId,
|
|
36638
|
+
blindForMs,
|
|
36639
|
+
thresholdMs: this.config.thresholdMs
|
|
36640
|
+
});
|
|
36641
|
+
}
|
|
36642
|
+
return out;
|
|
36643
|
+
}
|
|
36644
|
+
/** Drop a device (detach / unbind), so a re-attach starts a fresh clock. */
|
|
36645
|
+
forget(deviceId) {
|
|
36646
|
+
this.state.delete(deviceId);
|
|
36647
|
+
}
|
|
36648
|
+
reset() {
|
|
36649
|
+
this.state.clear();
|
|
36650
|
+
}
|
|
36651
|
+
};
|
|
36308
36652
|
//#endregion
|
|
36309
36653
|
//#region src/agent-load-service.ts
|
|
36310
36654
|
var AgentLoadService = class AgentLoadService {
|
|
@@ -38557,6 +38901,7 @@ var DetectionWiringController = class {
|
|
|
38557
38901
|
if (!this.deps.ctx()) return;
|
|
38558
38902
|
const { deviceId, frame, frameHandle, capturedAt } = payload;
|
|
38559
38903
|
this.deps.watchdog()?.noteSignal(deviceId, "detection");
|
|
38904
|
+
this.deps.blindMonitor()?.noteFrame(deviceId, frame.detections.length > 0, Date.now());
|
|
38560
38905
|
if (frame.detections.length === 0 && (frame.discarded?.length ?? 0) === 0) return;
|
|
38561
38906
|
this.deps.eventBus.emit({
|
|
38562
38907
|
id: `detection-${deviceId}-${Date.now()}`,
|
|
@@ -40344,6 +40689,34 @@ var PlacementService = class {
|
|
|
40344
40689
|
constructor(deps) {
|
|
40345
40690
|
this.deps = deps;
|
|
40346
40691
|
}
|
|
40692
|
+
/**
|
|
40693
|
+
* Drop nodes that cannot infer from a candidate set.
|
|
40694
|
+
*
|
|
40695
|
+
* Fails OPEN in one specific way that matters: if the filter would leave NO
|
|
40696
|
+
* candidate at all, the unfiltered set is used. Placing a camera on a node
|
|
40697
|
+
* that might fail its inference is bad; refusing to place it anywhere
|
|
40698
|
+
* because every node is currently mid-probe is worse, and that is the exact
|
|
40699
|
+
* shape a cold cluster start has.
|
|
40700
|
+
*/
|
|
40701
|
+
filterInferenceUsable(nodes, deviceId) {
|
|
40702
|
+
const usable = nodes.filter((nodeId) => this.deps.isNodeInferenceUsable(nodeId));
|
|
40703
|
+
if (usable.length === nodes.length) return nodes;
|
|
40704
|
+
if (usable.length === 0) {
|
|
40705
|
+
this.deps.ctx().logger.warn("every candidate node reports no usable inference device — placing anyway", {
|
|
40706
|
+
tags: { deviceId },
|
|
40707
|
+
meta: { nodes: [...nodes] }
|
|
40708
|
+
});
|
|
40709
|
+
return nodes;
|
|
40710
|
+
}
|
|
40711
|
+
this.deps.ctx().logger.info("nodes dropped from placement — no usable inference device", {
|
|
40712
|
+
tags: { deviceId },
|
|
40713
|
+
meta: {
|
|
40714
|
+
dropped: nodes.filter((n) => !usable.includes(n)),
|
|
40715
|
+
kept: usable
|
|
40716
|
+
}
|
|
40717
|
+
});
|
|
40718
|
+
return usable;
|
|
40719
|
+
}
|
|
40347
40720
|
async dispatchCamera(runnerConfig) {
|
|
40348
40721
|
if (!this.deps.ctx()) throw new Error("PipelineOrchestrator: dispatchCamera called before initialize");
|
|
40349
40722
|
if (this.deps.maintenancePaused?.() === true) return {
|
|
@@ -40358,7 +40731,7 @@ var PlacementService = class {
|
|
|
40358
40731
|
};
|
|
40359
40732
|
const preferredAgent = await this.deps.readPipelinePin(runnerConfig.deviceId);
|
|
40360
40733
|
const loads = await this.deps.loadService.collectAgentLoad({ onlyEnabled: true });
|
|
40361
|
-
const eligible = this.deps.topology.detectionEligibleNodes(runnerConfig.deviceId);
|
|
40734
|
+
const eligible = this.filterInferenceUsable(this.deps.topology.detectionEligibleNodes(runnerConfig.deviceId), runnerConfig.deviceId);
|
|
40362
40735
|
const nodeCaps = await this.deps.settingsStore.buildNodeCaps();
|
|
40363
40736
|
const motionRoleCandidate = pickMotionRolePlacement({
|
|
40364
40737
|
isAnalyzer: runnerConfig.motionSources.includes("analyzer"),
|
|
@@ -44231,7 +44604,10 @@ var SessionDispatchController = class {
|
|
|
44231
44604
|
await this.deps.reconcilePlacementFromRunners();
|
|
44232
44605
|
const preferredAgent = await this.deps.readPipelinePin(deviceId);
|
|
44233
44606
|
const loads = await this.agentLoads.get(CLUSTER_KEY);
|
|
44234
|
-
const
|
|
44607
|
+
const allEligible = this.deps.topology.detectionEligibleNodes(deviceId);
|
|
44608
|
+
const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
|
|
44609
|
+
const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
|
|
44610
|
+
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)) } });
|
|
44235
44611
|
const decision = balance({
|
|
44236
44612
|
nodes: loads,
|
|
44237
44613
|
preferredAgent,
|
|
@@ -45153,6 +45529,78 @@ var ZonesProvider = class {
|
|
|
45153
45529
|
//#endregion
|
|
45154
45530
|
//#region src/orchestrator-bootstrap.ts
|
|
45155
45531
|
/**
|
|
45532
|
+
* `orchestrator-bootstrap.ts` — the controller-construction body of
|
|
45533
|
+
* `onInitialize` (S11 Task 4, last extraction of the S11 series).
|
|
45534
|
+
*
|
|
45535
|
+
* `buildOrchestratorControllers(deps)` constructs all ~16 controllers the
|
|
45536
|
+
* addon owns, in the EXACT order `onInitialize` built them in before this
|
|
45537
|
+
* extraction (ledger → topology → loadService → audio → settingsStore →
|
|
45538
|
+
* loadShed → placement → session → deviceConfig → cameraStatusService →
|
|
45539
|
+
* reconcile → [settings load + localNodeId resolve] → nodeLifecycle →
|
|
45540
|
+
* [event subs + readiness seed] → pipelineWatchdog → [timers + legacy
|
|
45541
|
+
* migration] → zoneRulesProvider → zonesProvider →
|
|
45542
|
+
* [wireOrchestratorSubscriptions + eager reconcile] → detectionWiring).
|
|
45543
|
+
* Every dep closure is carried over byte-for-byte — only `this.X` became
|
|
45544
|
+
* either a local variable (for a controller already constructed earlier in
|
|
45545
|
+
* THIS function) or a `deps.X` accessor (for addon-owned state/methods that
|
|
45546
|
+
* stay on `PipelineOrchestratorAddon`, or for a controller referenced
|
|
45547
|
+
* before its own construction point — a genuine forward ref, always via a
|
|
45548
|
+
* LAZY closure, exactly as it was in `onInitialize`).
|
|
45549
|
+
*
|
|
45550
|
+
* Three things could not simply become local variables, because an EXISTING
|
|
45551
|
+
* (unmoved) addon method reads the REAL class field synchronously, mid-
|
|
45552
|
+
* construction, not through a lazy closure:
|
|
45553
|
+
* - `applyRuntimeSettings` (stays on the addon) reads `this.topology!`
|
|
45554
|
+
* and `this.reconcile!` directly — called synchronously from the
|
|
45555
|
+
* settings-load block below. `deps.setTopology`/`deps.setReconcile`
|
|
45556
|
+
* progressively sync the REAL addon fields the instant each is built,
|
|
45557
|
+
* so `applyRuntimeSettings` sees them.
|
|
45558
|
+
* - `subscribeToAudioAnalyzerReadiness` (stays on the addon) reads
|
|
45559
|
+
* `this.nodeLifecycle!.readinessRegistry` AND `this.audio!` directly —
|
|
45560
|
+
* called synchronously right after `nodeLifecycle` is built (readiness
|
|
45561
|
+
* seed + the `AgentOnline` handler). `deps.setNodeLifecycle` /
|
|
45562
|
+
* `deps.setAudio` cover it the same way. This one bit LIVE: `audio` is
|
|
45563
|
+
* constructed early (fourth) but `subscribeToAudioAnalyzerReadiness`
|
|
45564
|
+
* wasn't called until much later (after `nodeLifecycle` exists), so the
|
|
45565
|
+
* old code never noticed `this.audio` was still unsynced — until
|
|
45566
|
+
* `ReadinessRegistry.onReadyState`'s `queueMicrotask` hydration replay
|
|
45567
|
+
* (an already-`ready` cap fires the handler once, asynchronously, on
|
|
45568
|
+
* subscribe) won the race against the `await buildOrchestratorControllers(...)`
|
|
45569
|
+
* continuation in `onInitialize` that assigns `this.audio` — both are
|
|
45570
|
+
* microtasks, and the replay's `queueMicrotask` was enqueued (during
|
|
45571
|
+
* this still-synchronous function body) strictly before this function's
|
|
45572
|
+
* `return` resolves the outer promise, so it always runs first. Fixed
|
|
45573
|
+
* by syncing `this.audio` the instant it's constructed, exactly like
|
|
45574
|
+
* `topology`/`reconcile`/`nodeLifecycle` — no lazy closure ever reads a
|
|
45575
|
+
* controller before this function has assigned its real field.
|
|
45576
|
+
* Every other controller-to-controller reference in this function — even
|
|
45577
|
+
* ones that look like a forward ref (e.g. `loadShed`'s `attach` closure
|
|
45578
|
+
* onto `placement`, built one step later) — is a LAZY closure that is
|
|
45579
|
+
* never invoked synchronously during construction (only later, off
|
|
45580
|
+
* event-bus subscriptions, timers, or RPC handlers), so a plain local
|
|
45581
|
+
* `let` variable captured by reference is sufficient and exactly
|
|
45582
|
+
* preserves the original semantics: see each field's inline comment in
|
|
45583
|
+
* `index.ts` for the original construction-order reasoning, unchanged
|
|
45584
|
+
* here.
|
|
45585
|
+
*
|
|
45586
|
+
* `globalSettings` is a purely local variable — read/written ONLY by
|
|
45587
|
+
* closures built inside this same function (the one exception,
|
|
45588
|
+
* `deviceConfig`'s `setGlobalSettings`, is ALSO built here, so the local
|
|
45589
|
+
* variable stays the single source of truth for every reader). No code
|
|
45590
|
+
* outside this closure network ever read the former `this.globalSettings`
|
|
45591
|
+
* class field, so it isn't part of the returned `OrchestratorControllers`
|
|
45592
|
+
* — the field was removed from the class entirely (S11 Task 4 cleanup).
|
|
45593
|
+
* `localNodeId` IS returned — `index.ts` still reads `this.localNodeId`
|
|
45594
|
+
* from a couple of unrelated cap methods (`dumpDiagnostics`,
|
|
45595
|
+
* `getDecoderAssignments`), so the class field is populated once, from the
|
|
45596
|
+
* returned `OrchestratorControllers`, after this function returns.
|
|
45597
|
+
* `failoverPolicy` is the opposite case from `globalSettings` —
|
|
45598
|
+
* `applyRuntimeSettings` (unmoved) writes the REAL field on every future
|
|
45599
|
+
* settings change, so `nodeLifecycle`'s `failoverPolicy` closure MUST keep
|
|
45600
|
+
* reading through `deps.failoverPolicy()` (never a local snapshot) to see
|
|
45601
|
+
* those later updates.
|
|
45602
|
+
*/
|
|
45603
|
+
/**
|
|
45156
45604
|
* Construct every orchestrator controller, wire the agent-lifecycle event
|
|
45157
45605
|
* subscriptions + readiness seed + detection-wiring subscriptions, and
|
|
45158
45606
|
* kick the eager reconcile — the ENTIRE former `onInitialize` body between
|
|
@@ -45311,6 +45759,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45311
45759
|
localNodeId: () => localNodeId,
|
|
45312
45760
|
readPipelinePin: (deviceId) => deps.readPipelinePin(deviceId),
|
|
45313
45761
|
getEligibleInferenceDevices: (nodeId) => deps.getEligibleInferenceDevices(nodeId),
|
|
45762
|
+
isNodeInferenceUsable: (nodeId) => deps.isNodeInferenceUsable(nodeId),
|
|
45314
45763
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
45315
45764
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
45316
45765
|
inferenceRotation,
|
|
@@ -45340,6 +45789,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45340
45789
|
listZones: async (deviceId) => await zonesProvider?.listZones({ deviceId }) ?? [],
|
|
45341
45790
|
readPipelinePin: (deviceId) => deps.readPipelinePin(deviceId),
|
|
45342
45791
|
getEligibleInferenceDevices: (nodeId) => deps.getEligibleInferenceDevices(nodeId),
|
|
45792
|
+
isNodeInferenceUsable: (nodeId) => deps.isNodeInferenceUsable(nodeId),
|
|
45343
45793
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
45344
45794
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
45345
45795
|
inferenceRotation,
|
|
@@ -45498,7 +45948,34 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45498
45948
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
45499
45949
|
});
|
|
45500
45950
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|
|
45501
|
-
const
|
|
45951
|
+
const blindMonitor = new DetectionBlindMonitor();
|
|
45952
|
+
const pendingRetryTimer = setInterval(() => {
|
|
45953
|
+
reconcile.retryPendingDispatches();
|
|
45954
|
+
for (const blind of blindMonitor.sweep(Date.now())) {
|
|
45955
|
+
deps.ctx().logger.error("camera is being analysed and detecting NOTHING", {
|
|
45956
|
+
tags: { deviceId: blind.deviceId },
|
|
45957
|
+
meta: {
|
|
45958
|
+
blindForMs: blind.blindForMs,
|
|
45959
|
+
thresholdMs: blind.thresholdMs
|
|
45960
|
+
}
|
|
45961
|
+
});
|
|
45962
|
+
deps.ctx().eventBus.emit({
|
|
45963
|
+
id: randomUUID(),
|
|
45964
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
45965
|
+
source: {
|
|
45966
|
+
type: "device",
|
|
45967
|
+
id: blind.deviceId,
|
|
45968
|
+
deviceId: blind.deviceId
|
|
45969
|
+
},
|
|
45970
|
+
category: EventCategory.PipelineDetectionBlind,
|
|
45971
|
+
data: {
|
|
45972
|
+
deviceId: blind.deviceId,
|
|
45973
|
+
blindForMs: blind.blindForMs,
|
|
45974
|
+
thresholdMs: blind.thresholdMs
|
|
45975
|
+
}
|
|
45976
|
+
});
|
|
45977
|
+
}
|
|
45978
|
+
}, PENDING_RETRY_INTERVAL_MS);
|
|
45502
45979
|
const autoRebalanceTimer = setInterval(() => void placement.runAutoRebalance(), AUTO_REBALANCE_INTERVAL_MS);
|
|
45503
45980
|
deps.ctx().logger.info("Pipeline orchestrator detection-wiring subscriptions installed");
|
|
45504
45981
|
migrateLegacyFlagsToBindings({
|
|
@@ -45625,6 +46102,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45625
46102
|
loadShed,
|
|
45626
46103
|
nodeStress,
|
|
45627
46104
|
watchdog: () => pipelineWatchdog,
|
|
46105
|
+
blindMonitor: () => blindMonitor,
|
|
45628
46106
|
api: () => deps.ctx().api ?? null,
|
|
45629
46107
|
eventBus: deps.ctx().eventBus,
|
|
45630
46108
|
logger: deps.ctx().logger,
|
|
@@ -46202,6 +46680,48 @@ function deriveRuntimeSettings(config) {
|
|
|
46202
46680
|
//#endregion
|
|
46203
46681
|
//#region src/index.ts
|
|
46204
46682
|
/**
|
|
46683
|
+
* addon-pipeline-orchestrator — hub-side camera-to-agent load balancer.
|
|
46684
|
+
*
|
|
46685
|
+
* Owns the control plane for the distributed detection pipeline. Tracks
|
|
46686
|
+
* which `addon-pipeline-runner` instance is responsible for each camera,
|
|
46687
|
+
* queries each runner's capacity + hardware profile, and dispatches
|
|
46688
|
+
* `attachCamera` / `detachCamera` cap calls to the appropriate runner via
|
|
46689
|
+
* the Moleculer broker. Persists manual pins via the per-device settings
|
|
46690
|
+
* store so user overrides survive hub restarts.
|
|
46691
|
+
*
|
|
46692
|
+
* Strictly control plane — never sees a frame, never runs inference, never
|
|
46693
|
+
* drains queues. The runner does that work locally on each vision node.
|
|
46694
|
+
*
|
|
46695
|
+
* ## Dispatch entrypoints
|
|
46696
|
+
*
|
|
46697
|
+
* - **`dispatchCamera(runnerConfig)`** — in-process only (not in the cap
|
|
46698
|
+
* def). Called by `DetectionWiringService` with a freshly-built
|
|
46699
|
+
* `RunnerCameraConfig`. Runs the L1/L2/L3 balancer, picks an agent, then
|
|
46700
|
+
* issues `broker.call('pipeline-runner.pipeline-runner.attachCamera', ...)`.
|
|
46701
|
+
* Caches the config so future rebalance / pinning can re-dispatch without
|
|
46702
|
+
* the caller rebuilding it.
|
|
46703
|
+
*
|
|
46704
|
+
* - **`assignCamera({deviceId, agentNodeId})`** — cap method. Persists a
|
|
46705
|
+
* manual `preferredAgent` pin to per-device settings and re-dispatches
|
|
46706
|
+
* immediately if the camera is already attached.
|
|
46707
|
+
*
|
|
46708
|
+
* - **`unassignCamera({deviceId})`** — cap method. Clears the pin,
|
|
46709
|
+
* detaches the camera from its current agent.
|
|
46710
|
+
*
|
|
46711
|
+
* - **`rebalance()`** — cap method. Walks every cached camera config and
|
|
46712
|
+
* re-runs the balancer; migrates cameras whose current agent is no
|
|
46713
|
+
* longer optimal. Pinned cameras are never migrated.
|
|
46714
|
+
*
|
|
46715
|
+
* ## Runner dispatch
|
|
46716
|
+
*
|
|
46717
|
+
* All runner calls go through `this.ctx.api.pipelineRunner.*` (the generated
|
|
46718
|
+
* tRPC cap router). The router extracts `nodeId` from the input and
|
|
46719
|
+
* routes transparently to the local runner (direct in-process call) or
|
|
46720
|
+
* to a remote agent (via Moleculer). Node discovery is driven by
|
|
46721
|
+
* `EventCategory.AgentOnline/AgentOffline` events forwarded by
|
|
46722
|
+
* `agent-registry.service.ts`.
|
|
46723
|
+
*/
|
|
46724
|
+
/**
|
|
46205
46725
|
* The FULL action catalog, under the name the HUB HARVESTS.
|
|
46206
46726
|
*
|
|
46207
46727
|
* The hub's forked-addon harvest imports this entry module and reads the
|
|
@@ -46460,6 +46980,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
46460
46980
|
detachOn: (nodeId, deviceId) => this.detachOn(nodeId, deviceId),
|
|
46461
46981
|
readPipelinePin: (deviceId) => this.readPipelinePin(deviceId),
|
|
46462
46982
|
getEligibleInferenceDevices: (nodeId) => this.resolveEligibleInferenceDevices(nodeId),
|
|
46983
|
+
isNodeInferenceUsable: (nodeId) => this.isNodeInferenceUsable(nodeId),
|
|
46463
46984
|
getInferenceDeviceCaps: (nodeId) => this.getInferenceDeviceCaps(nodeId),
|
|
46464
46985
|
readPipelineDevicePin: (deviceId) => this.readPipelineDevicePin(deviceId),
|
|
46465
46986
|
subscribeToAudioAnalyzerReadiness: (nodeId) => this.subscribeToAudioAnalyzerReadiness(nodeId),
|
|
@@ -47116,11 +47637,66 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
47116
47637
|
const stored = (await this.settingsStore.readAgentSettingsMap())[nodeId]?.inferenceDevices ?? {};
|
|
47117
47638
|
const { probed } = await this.probeNodeInferenceDevices(nodeId);
|
|
47118
47639
|
const catalog = await this.settingsStore.getCatalogForNode(nodeId);
|
|
47119
|
-
const
|
|
47640
|
+
const eligibility = resolveInferenceDeviceEligibility(probed, stored, catalog ? makeRootCapabilityGuard(catalog) : void 0);
|
|
47641
|
+
const { eligible, excluded } = eligibility;
|
|
47120
47642
|
this.logInferenceDeviceShrinkage(nodeId, eligible, excluded);
|
|
47643
|
+
this.noteNodeInferenceUsability(nodeId, resolveNodeInferenceUsability(eligibility));
|
|
47121
47644
|
return eligible;
|
|
47122
47645
|
}
|
|
47123
47646
|
/**
|
|
47647
|
+
* Fold one usability observation into the placement mirror and announce the
|
|
47648
|
+
* transition.
|
|
47649
|
+
*
|
|
47650
|
+
* This is called from the SAME read the dispatcher already makes, plus the
|
|
47651
|
+
* session controller's background refresher — so a node's usability is
|
|
47652
|
+
* re-observed on the cadence placement itself runs at, without a timer of
|
|
47653
|
+
* its own. The mirror is what makes the answer readable synchronously by
|
|
47654
|
+
* `detectionEligibleNodes`' callers; see its own docblock for why it refuses
|
|
47655
|
+
* to act on a single bad read.
|
|
47656
|
+
*
|
|
47657
|
+
* The system event is emitted ONLY on the transition into unusable — the
|
|
47658
|
+
* kind is `node-inference-unavailable` and it is authorable in a rule.
|
|
47659
|
+
* Recovery is logged and deliberately has no event: a notification saying
|
|
47660
|
+
* the broken thing is no longer broken is a second rule nobody asked for.
|
|
47661
|
+
*/
|
|
47662
|
+
noteNodeInferenceUsability(nodeId, usability) {
|
|
47663
|
+
const transition = this.nodeInferenceUsability.observe(nodeId, usability.usable);
|
|
47664
|
+
if (transition === null) return;
|
|
47665
|
+
if (transition === "recovered") {
|
|
47666
|
+
this.ctx.logger.info("node inference recovered — back in the placement candidate set", {
|
|
47667
|
+
tags: { nodeId },
|
|
47668
|
+
meta: { eligible: usability.eligibleKeys }
|
|
47669
|
+
});
|
|
47670
|
+
return;
|
|
47671
|
+
}
|
|
47672
|
+
this.ctx.logger.error("node has NO usable inference device — removed from the placement candidate set", {
|
|
47673
|
+
tags: { nodeId },
|
|
47674
|
+
meta: {
|
|
47675
|
+
unavailable: usability.unavailableKeys,
|
|
47676
|
+
eligible: usability.eligibleKeys
|
|
47677
|
+
}
|
|
47678
|
+
});
|
|
47679
|
+
this.ctx.eventBus.emit({
|
|
47680
|
+
id: randomUUID(),
|
|
47681
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
47682
|
+
source: {
|
|
47683
|
+
type: "addon",
|
|
47684
|
+
id: "pipeline-orchestrator",
|
|
47685
|
+
addonId: "pipeline-orchestrator"
|
|
47686
|
+
},
|
|
47687
|
+
category: EventCategory.PipelineNodeInferenceUnavailable,
|
|
47688
|
+
data: {
|
|
47689
|
+
nodeId,
|
|
47690
|
+
unavailableDeviceKeys: usability.unavailableKeys
|
|
47691
|
+
}
|
|
47692
|
+
});
|
|
47693
|
+
}
|
|
47694
|
+
/** Can this node be given cameras? Read synchronously by the two dispatch
|
|
47695
|
+
* paths before they hand `eligibleNodes` to the balancer. */
|
|
47696
|
+
isNodeInferenceUsable(nodeId) {
|
|
47697
|
+
return this.nodeInferenceUsability.isUsable(nodeId);
|
|
47698
|
+
}
|
|
47699
|
+
/**
|
|
47124
47700
|
* Per-node signature of the last logged eligible/excluded split, so
|
|
47125
47701
|
* {@link logInferenceDeviceShrinkage} fires on CHANGE only. This runs on
|
|
47126
47702
|
* every camera dispatch; without the gate it would be one line per dispatch
|
|
@@ -47128,6 +47704,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
47128
47704
|
*/
|
|
47129
47705
|
lastInferenceEligibilitySignature = /* @__PURE__ */ new Map();
|
|
47130
47706
|
/**
|
|
47707
|
+
* Which nodes can still infer. Refreshed by
|
|
47708
|
+
* {@link resolveEligibleInferenceDevices}; read synchronously by both
|
|
47709
|
+
* dispatch paths so a node whose every enabled accelerator vanished stops
|
|
47710
|
+
* receiving cameras. little-unraid was handed cameras for a whole day in
|
|
47711
|
+
* exactly that state.
|
|
47712
|
+
*/
|
|
47713
|
+
nodeInferenceUsability = new NodeInferenceUsabilityMirror();
|
|
47714
|
+
/**
|
|
47131
47715
|
* Say out loud which of a node's inference devices were dropped from the
|
|
47132
47716
|
* candidate set, and why.
|
|
47133
47717
|
*
|