@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.js
CHANGED
|
@@ -24,12 +24,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
enumerable: true
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
//#endregion
|
|
27
|
+
let node_crypto = require("node:crypto");
|
|
27
28
|
let node_fs = require("node:fs");
|
|
28
29
|
node_fs = __toESM(node_fs);
|
|
29
30
|
let node_path = require("node:path");
|
|
30
31
|
node_path = __toESM(node_path);
|
|
31
32
|
let node_url = require("node:url");
|
|
32
|
-
//#region ../types/dist/event-category-
|
|
33
|
+
//#region ../types/dist/event-category-Bxo5yJjt.mjs
|
|
33
34
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
34
35
|
EventCategory["SystemBoot"] = "system.boot";
|
|
35
36
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -236,6 +237,33 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
236
237
|
EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
|
|
237
238
|
EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
|
|
238
239
|
/**
|
|
240
|
+
* A node the orchestrator would otherwise place cameras on has NO usable
|
|
241
|
+
* inference device: the operator enabled one or more accelerators there and
|
|
242
|
+
* the live probe reports every one of them unavailable. Emitted once per
|
|
243
|
+
* TRANSITION into that state (never per dispatch), and the node is dropped
|
|
244
|
+
* from the placement candidate set for as long as it holds.
|
|
245
|
+
*
|
|
246
|
+
* This exists because the state was previously invisible: little-unraid
|
|
247
|
+
* absorbed 283k inference errors in a day while still being handed cameras,
|
|
248
|
+
* and nothing in the system said so.
|
|
249
|
+
*
|
|
250
|
+
* A node with no accelerators configured at all is NOT this — its devices
|
|
251
|
+
* are `disabled`, not `unavailable`, and the runner's default CPU pool
|
|
252
|
+
* serves it exactly as before.
|
|
253
|
+
*/
|
|
254
|
+
EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
|
|
255
|
+
/**
|
|
256
|
+
* A camera has an OPEN detection session and has produced no detection at
|
|
257
|
+
* all for longer than the blind threshold — the camera is being decoded and
|
|
258
|
+
* inferred and is returning nothing. Emitted once per transition into blind,
|
|
259
|
+
* per camera.
|
|
260
|
+
*
|
|
261
|
+
* The failure it reports: a 1h43 detection blackout on the entrance camera
|
|
262
|
+
* that nobody noticed, because "a camera that detects nothing" and "a quiet
|
|
263
|
+
* camera" produce byte-identical silence.
|
|
264
|
+
*/
|
|
265
|
+
EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
|
|
266
|
+
/**
|
|
239
267
|
* Per-camera pipeline config was mutated by the orchestrator
|
|
240
268
|
* (3-level settings change via `setAgentAddonDefaults` /
|
|
241
269
|
* `setCameraStepToggle` / `setCameraPipelineForAgent` or a
|
|
@@ -5400,6 +5428,12 @@ Object.fromEntries([
|
|
|
5400
5428
|
icon: "shapes",
|
|
5401
5429
|
order: 38
|
|
5402
5430
|
},
|
|
5431
|
+
{
|
|
5432
|
+
id: "scenes",
|
|
5433
|
+
label: "Scenes",
|
|
5434
|
+
icon: "scan-eye",
|
|
5435
|
+
order: 36
|
|
5436
|
+
},
|
|
5403
5437
|
{
|
|
5404
5438
|
id: "analytics",
|
|
5405
5439
|
label: "Analytics",
|
|
@@ -14927,6 +14961,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14927
14961
|
"stream-offline",
|
|
14928
14962
|
"node-online",
|
|
14929
14963
|
"node-offline",
|
|
14964
|
+
"node-inference-unavailable",
|
|
14965
|
+
"detection-blind",
|
|
14930
14966
|
"addon-update-available",
|
|
14931
14967
|
"server-update-available",
|
|
14932
14968
|
"alarm-triggered",
|
|
@@ -15698,7 +15734,50 @@ var NcRuleInputSchema = object({
|
|
|
15698
15734
|
* tests every zone the track visited and a `crossing` condition can no longer
|
|
15699
15735
|
* be satisfied, because a closed track carries no crossing.
|
|
15700
15736
|
*/
|
|
15701
|
-
waitForEnhancement: boolean().optional()
|
|
15737
|
+
waitForEnhancement: boolean().optional(),
|
|
15738
|
+
/**
|
|
15739
|
+
* GROUP a burst of subjects into ONE notification that grows.
|
|
15740
|
+
*
|
|
15741
|
+
* Seconds of quiet after the last matching subject before the burst is
|
|
15742
|
+
* considered over. While it is open, the first subject enqueues immediately —
|
|
15743
|
+
* **exactly as today, with no added latency** — and every real growth (a new
|
|
15744
|
+
* subject, or a name confirmed on one already in it) REPLACES that
|
|
15745
|
+
* notification with an updated one naming everybody. The push carries the
|
|
15746
|
+
* group's own coalescing tag, so the phone replaces rather than stacks.
|
|
15747
|
+
*
|
|
15748
|
+
* `0` / absent = off, and off is today's behaviour byte for byte.
|
|
15749
|
+
*
|
|
15750
|
+
* ### Why an idle cutoff and not a window
|
|
15751
|
+
*
|
|
15752
|
+
* The measured seven-person arrival on device 590 spans 110 s with every
|
|
15753
|
+
* internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
|
|
15754
|
+
* idle cutoff holds it as one and ends it when the arrival actually ends.
|
|
15755
|
+
* 30 is Frigate's shipped value for the same decision.
|
|
15756
|
+
*
|
|
15757
|
+
* ### What it replaces
|
|
15758
|
+
*
|
|
15759
|
+
* The blind cooldown, which collapses a burst by DISCARDING it. Measured on
|
|
15760
|
+
* device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
|
|
15761
|
+
* notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
|
|
15762
|
+
* track that did fire and 7 carrying a confirmed identity nobody heard about.
|
|
15763
|
+
* A group collapses the same volume by MERGING, so the cooldown becomes a
|
|
15764
|
+
* budget over GROUPS — which is what it always meant — and a growth is never
|
|
15765
|
+
* throttled by the window its own first member spent.
|
|
15766
|
+
*
|
|
15767
|
+
* ### Interaction with {@link waitForEnhancement}
|
|
15768
|
+
*
|
|
15769
|
+
* They compose, and the order matters. `waitForEnhancement` defers the rule to
|
|
15770
|
+
* TRACK CLOSE, so with both set the group is opened by the first member to
|
|
15771
|
+
* CLOSE — already carrying its name — and grows as later members close. That
|
|
15772
|
+
* is later, and complete. With grouping alone the group opens on the first
|
|
15773
|
+
* object event and picks up names as they are confirmed, through the growth
|
|
15774
|
+
* path. Neither combination fires twice for one subject.
|
|
15775
|
+
*
|
|
15776
|
+
* `.optional()` and deliberately NOT `.default()`: a Zod default does not run
|
|
15777
|
+
* on the addon cap path, so absent must keep meaning what it meant before this
|
|
15778
|
+
* field existed.
|
|
15779
|
+
*/
|
|
15780
|
+
groupIdleSec: number().int().min(0).max(600).optional()
|
|
15702
15781
|
});
|
|
15703
15782
|
/**
|
|
15704
15783
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -16624,7 +16703,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16624
16703
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16625
16704
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16626
16705
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16627
|
-
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
16706
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16628
16707
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16629
16708
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16630
16709
|
* (`getObjectEvents` et al.).
|
|
@@ -16911,6 +16990,28 @@ var TrackSchema = object({
|
|
|
16911
16990
|
* `=== true` and render nothing otherwise, never infer "no face".
|
|
16912
16991
|
*/
|
|
16913
16992
|
hasFace: boolean().optional(),
|
|
16993
|
+
/**
|
|
16994
|
+
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16995
|
+
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16996
|
+
* so the passage is tracked once and as a VEHICLE.
|
|
16997
|
+
*
|
|
16998
|
+
* It exists because the fold's record was dishonest. D34 and the code both
|
|
16999
|
+
* said "the person is not lost — it is reported so both entities stay on the
|
|
17000
|
+
* record"; in fact the pair went into a per-processor RAM field behind an
|
|
17001
|
+
* accessor nobody called, and every durable surface said `vehicle`, full
|
|
17002
|
+
* stop. This is the composition note that makes the row true.
|
|
17003
|
+
*
|
|
17004
|
+
* A COMPOSITION, never a class and never a label. "This vehicle contains a
|
|
17005
|
+
* person" is not an answer to "what is this" — both label tiers would refuse
|
|
17006
|
+
* a macro token anyway (D89), and correctly. Nothing here changes what the
|
|
17007
|
+
* subject IS: a cyclist stays one vehicle track, occupancy still counts one,
|
|
17008
|
+
* and a `person` rule still does not fire for someone cycling past.
|
|
17009
|
+
*
|
|
17010
|
+
* **Absent ≠ false**, exactly like {@link hasFace}: every row written before
|
|
17011
|
+
* the column, and every hub that predates the field, omits it. Test
|
|
17012
|
+
* `=== true` and render nothing otherwise — never infer "no rider".
|
|
17013
|
+
*/
|
|
17014
|
+
hasRider: boolean().optional(),
|
|
16914
17015
|
...TrackFlagFields,
|
|
16915
17016
|
...TrackRetrainFields
|
|
16916
17017
|
});
|
|
@@ -25607,9 +25708,33 @@ method(object({
|
|
|
25607
25708
|
* settings-contribution methods. `status.kind:'push'` — the engine pushes on
|
|
25608
25709
|
* every hysteresis flip / availability change; consumers never poll.
|
|
25609
25710
|
*/
|
|
25610
|
-
/** Extensible condition tag. Seeded 'day' | '
|
|
25611
|
-
*
|
|
25711
|
+
/** Extensible condition tag. Seeded 'day' | 'ir' (the two variants the operator
|
|
25712
|
+
* captures) plus 'night' | 'dawn' | 'dusk' from the resolver's sun-times band.
|
|
25713
|
+
* Open by design so more can be added without a wire break.
|
|
25714
|
+
*
|
|
25715
|
+
* Matching does NOT fall back across conditions: cross-condition cosines are
|
|
25716
|
+
* not comparable, so "I have never seen this scene in this light" is reported
|
|
25717
|
+
* as `unknown`, never guessed. A day reference scored against an IR frame
|
|
25718
|
+
* collapses the cosine and would latch a false alarm every single night. */
|
|
25612
25719
|
var SceneConditionSchema = string();
|
|
25720
|
+
/** `matched` = the baseline is what we see; `diverged` = it demonstrably is not;
|
|
25721
|
+
* `unknown` = we cannot judge (no reference for this condition, encoder model
|
|
25722
|
+
* changed, view shifted, no snapshot). `unknown` is a real value, not a null,
|
|
25723
|
+
* and never counts toward hysteresis in either direction. */
|
|
25724
|
+
var SceneVerdictSchema = _enum([
|
|
25725
|
+
"matched",
|
|
25726
|
+
"diverged",
|
|
25727
|
+
"unknown"
|
|
25728
|
+
]);
|
|
25729
|
+
/** Why a scene cannot judge. Named, because this feature's failure mode is
|
|
25730
|
+
* silence that reads as "nothing has happened". */
|
|
25731
|
+
var SceneUnavailableSchema = _enum([
|
|
25732
|
+
"no-reference-for-condition",
|
|
25733
|
+
"view-shifted",
|
|
25734
|
+
"no-vision-profile",
|
|
25735
|
+
"encoder-model-changed",
|
|
25736
|
+
"no-snapshot"
|
|
25737
|
+
]);
|
|
25613
25738
|
/** One captured reference — condition-tagged, model-version-gated. `embedding`
|
|
25614
25739
|
* is `number[]` (Float32Array does NOT survive MsgPack/UDS). */
|
|
25615
25740
|
var SceneReferenceSchema = object({
|
|
@@ -25617,7 +25742,14 @@ var SceneReferenceSchema = object({
|
|
|
25617
25742
|
modelId: string(),
|
|
25618
25743
|
condition: SceneConditionSchema,
|
|
25619
25744
|
capturedAt: number(),
|
|
25620
|
-
thumbnailMediaId: string().optional()
|
|
25745
|
+
thumbnailMediaId: string().optional(),
|
|
25746
|
+
/** Whole-frame (downscaled) embedding captured alongside the ROI crop. The
|
|
25747
|
+
* anti-view-shift anchor: a bumped camera, a PTZ preset or a re-aim makes the
|
|
25748
|
+
* normalized rect frame a different piece of world, and the scene would
|
|
25749
|
+
* diverge forever with a perfectly plausible cosine. Checked LAZILY, only
|
|
25750
|
+
* when hysteresis is about to flip — one extra encode per candidate
|
|
25751
|
+
* transition, not per poll. */
|
|
25752
|
+
anchorEmbedding: array(number()).optional()
|
|
25621
25753
|
});
|
|
25622
25754
|
var SceneMonitorStateSchema = object({
|
|
25623
25755
|
id: string(),
|
|
@@ -25639,6 +25771,25 @@ var SceneCheckSchema = discriminatedUnion("mode", [object({
|
|
|
25639
25771
|
profileId: string().optional(),
|
|
25640
25772
|
hysteresisCount: number().int().positive()
|
|
25641
25773
|
})]);
|
|
25774
|
+
var SCENE_DEFAULT_ANCHOR_THRESHOLD = .85;
|
|
25775
|
+
/**
|
|
25776
|
+
* Vision-model adjudication of a candidate flip. Field names deliberately
|
|
25777
|
+
* mirror `NcConfirmSchema` so an operator meets one vocabulary, not two.
|
|
25778
|
+
*
|
|
25779
|
+
* `onTimeout` defaults to **'hold'**, the OPPOSITE of `NcConfirmGate`'s
|
|
25780
|
+
* fail-open: a notification suppressed is the worse error there, but a vision
|
|
25781
|
+
* model that timed out has not told us the bin is gone, and a latch is a
|
|
25782
|
+
* stateful claim that costs the operator a trip to reset.
|
|
25783
|
+
*/
|
|
25784
|
+
var SceneConfirmSchema = object({
|
|
25785
|
+
enabled: boolean().default(false),
|
|
25786
|
+
prompt: string().min(1).max(1e3),
|
|
25787
|
+
profileId: string().optional(),
|
|
25788
|
+
timeoutMs: number().int().min(1e3).max(2e4).default(8e3),
|
|
25789
|
+
maxImagePx: number().int().min(64).max(2048).default(448),
|
|
25790
|
+
/** What a timeout / unavailable model means for the PENDING flip. */
|
|
25791
|
+
onTimeout: _enum(["flip", "hold"]).default("hold")
|
|
25792
|
+
});
|
|
25642
25793
|
var SceneMonitorSchema = object({
|
|
25643
25794
|
id: string(),
|
|
25644
25795
|
label: string(),
|
|
@@ -25657,7 +25808,41 @@ var SceneMonitorSchema = object({
|
|
|
25657
25808
|
lastConfidence: number().nullable(),
|
|
25658
25809
|
currentCondition: SceneConditionSchema.nullable(),
|
|
25659
25810
|
availability: _enum(["ok", "unavailable"]),
|
|
25660
|
-
unavailableReason: string().nullable()
|
|
25811
|
+
unavailableReason: string().nullable(),
|
|
25812
|
+
/** Which state is "the initial screen". `null` until the first capture. */
|
|
25813
|
+
baselineStateId: string().nullable(),
|
|
25814
|
+
/** Which boolean drives notification rules and any export. */
|
|
25815
|
+
emit: _enum(["latched", "live"]).default("latched"),
|
|
25816
|
+
/** Live: does the region match the baseline RIGHT NOW. */
|
|
25817
|
+
verdict: SceneVerdictSchema,
|
|
25818
|
+
/** Has it been `diverged` at least once since `armedAt` — the operator's boolean. */
|
|
25819
|
+
latched: boolean(),
|
|
25820
|
+
/** Last reset (or creation). */
|
|
25821
|
+
armedAt: number(),
|
|
25822
|
+
divergedAt: number().nullable(),
|
|
25823
|
+
restoredAt: number().nullable(),
|
|
25824
|
+
/** A check is only COUNTED when the device has been quiet this long. Motion
|
|
25825
|
+
* during the window DISCARDS the observation — a car pulling up in front of
|
|
25826
|
+
* the bin must not be able to spend hysteresis credit. */
|
|
25827
|
+
quietSeconds: number().int().min(0).max(3600).default(60),
|
|
25828
|
+
/** An observation only advances the pending count when it is at least this
|
|
25829
|
+
* far from the previously counted one, so N agreeing checks span real time
|
|
25830
|
+
* rather than N adjacent polls inside one occlusion. */
|
|
25831
|
+
minObservationSpacingSec: number().int().min(0).max(3600).default(120),
|
|
25832
|
+
/** Vision-model adjudication of a candidate flip. Similarity primary only. */
|
|
25833
|
+
confirm: SceneConfirmSchema.optional(),
|
|
25834
|
+
/** Whole-frame anchor cosine below which a flip is REFUSED as `view-shifted`. */
|
|
25835
|
+
anchorThreshold: number().min(0).max(1).default(SCENE_DEFAULT_ANCHOR_THRESHOLD),
|
|
25836
|
+
/** Clear the latch on its own when the scene matches again? Default false —
|
|
25837
|
+
* `restoredAt` and the `scene-restored` edge are recorded regardless, so an
|
|
25838
|
+
* automation can react to the bin coming back without the operator's own
|
|
25839
|
+
* alarm silently clearing itself. */
|
|
25840
|
+
autoRestore: boolean().default(false),
|
|
25841
|
+
/** Named cause when `verdict === 'unknown'`. */
|
|
25842
|
+
unavailable: SceneUnavailableSchema.nullable(),
|
|
25843
|
+
/** Conditions that have at least one comparable reference — the coverage line
|
|
25844
|
+
* ("day ✓ · ir ✓ · dusk ✗") that turns a silent fallback into a visible fact. */
|
|
25845
|
+
coveredConditions: array(SceneConditionSchema)
|
|
25661
25846
|
});
|
|
25662
25847
|
var SceneMonitorStatusSchema = object({
|
|
25663
25848
|
monitors: array(SceneMonitorSchema),
|
|
@@ -25690,7 +25875,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
|
|
|
25690
25875
|
"both"
|
|
25691
25876
|
]).optional(),
|
|
25692
25877
|
checkIntervalSec: number().optional(),
|
|
25693
|
-
check: SceneCheckSchema.optional()
|
|
25878
|
+
check: SceneCheckSchema.optional(),
|
|
25879
|
+
emit: _enum(["latched", "live"]).optional(),
|
|
25880
|
+
quietSeconds: number().int().min(0).max(3600).optional(),
|
|
25881
|
+
minObservationSpacingSec: number().int().min(0).max(3600).optional(),
|
|
25882
|
+
anchorThreshold: number().min(0).max(1).optional(),
|
|
25883
|
+
autoRestore: boolean().optional(),
|
|
25884
|
+
/** `null` clears the vision-model adjudicator. */
|
|
25885
|
+
confirm: SceneConfirmSchema.nullable().optional()
|
|
25694
25886
|
})
|
|
25695
25887
|
}), _void(), {
|
|
25696
25888
|
kind: "mutation",
|
|
@@ -25727,6 +25919,14 @@ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSche
|
|
|
25727
25919
|
}), _void(), {
|
|
25728
25920
|
kind: "mutation",
|
|
25729
25921
|
auth: "admin"
|
|
25922
|
+
}), method(object({
|
|
25923
|
+
deviceId: number(),
|
|
25924
|
+
monitorId: string(),
|
|
25925
|
+
/** Defaults to TRUE at the provider seam — see `SCENE_RESET_RECAPTURES`. */
|
|
25926
|
+
recapture: boolean().optional()
|
|
25927
|
+
}), _void(), {
|
|
25928
|
+
kind: "mutation",
|
|
25929
|
+
auth: "admin"
|
|
25730
25930
|
});
|
|
25731
25931
|
/**
|
|
25732
25932
|
* Per-stage gating mode applied to the zones a rule references.
|
|
@@ -31104,6 +31304,12 @@ Object.freeze({
|
|
|
31104
31304
|
addonId: null,
|
|
31105
31305
|
access: "create"
|
|
31106
31306
|
},
|
|
31307
|
+
"sceneMonitor.resetScene": {
|
|
31308
|
+
capName: "scene-monitor",
|
|
31309
|
+
capScope: "device",
|
|
31310
|
+
addonId: null,
|
|
31311
|
+
access: "delete"
|
|
31312
|
+
},
|
|
31107
31313
|
"sceneMonitor.updateScene": {
|
|
31108
31314
|
capName: "scene-monitor",
|
|
31109
31315
|
capScope: "device",
|
|
@@ -33786,6 +33992,11 @@ Object.freeze({
|
|
|
33786
33992
|
form: "single",
|
|
33787
33993
|
optional: false
|
|
33788
33994
|
}],
|
|
33995
|
+
"sceneMonitor.resetScene": [{
|
|
33996
|
+
name: "deviceId",
|
|
33997
|
+
form: "single",
|
|
33998
|
+
optional: false
|
|
33999
|
+
}],
|
|
33789
34000
|
"sceneMonitor.updateScene": [{
|
|
33790
34001
|
name: "deviceId",
|
|
33791
34002
|
form: "single",
|
|
@@ -34805,6 +35016,15 @@ function resolveInferenceDeviceEligibility(probed, stored, canRunRoot) {
|
|
|
34805
35016
|
excluded
|
|
34806
35017
|
};
|
|
34807
35018
|
}
|
|
35019
|
+
function resolveNodeInferenceUsability(eligibility) {
|
|
35020
|
+
const eligibleKeys = Object.keys(eligibility.eligible).toSorted();
|
|
35021
|
+
const unavailableKeys = eligibility.excluded.filter((e) => e.reason === "unavailable").map((e) => e.key).toSorted();
|
|
35022
|
+
return {
|
|
35023
|
+
usable: eligibleKeys.length > 0 || unavailableKeys.length === 0,
|
|
35024
|
+
unavailableKeys,
|
|
35025
|
+
eligibleKeys
|
|
35026
|
+
};
|
|
35027
|
+
}
|
|
34808
35028
|
/**
|
|
34809
35029
|
* Step-tree device jump (phase 1): the attach-payload roster of a node's
|
|
34810
35030
|
* enabled∧available inference devices with the balancer knobs (`weight`,
|
|
@@ -34823,6 +35043,67 @@ function buildInferenceDeviceRoster(eligible, caps) {
|
|
|
34823
35043
|
})).toSorted((a, b) => a.deviceKey < b.deviceKey ? -1 : a.deviceKey > b.deviceKey ? 1 : 0);
|
|
34824
35044
|
}
|
|
34825
35045
|
//#endregion
|
|
35046
|
+
//#region src/node-inference-usability-mirror.ts
|
|
35047
|
+
var NodeInferenceUsabilityMirror = class {
|
|
35048
|
+
state = /* @__PURE__ */ new Map();
|
|
35049
|
+
/**
|
|
35050
|
+
* Fold one observation in and report whether the caller should act.
|
|
35051
|
+
* Never throws.
|
|
35052
|
+
*/
|
|
35053
|
+
observe(nodeId, usable) {
|
|
35054
|
+
const prev = this.state.get(nodeId);
|
|
35055
|
+
if (usable) {
|
|
35056
|
+
this.state.set(nodeId, {
|
|
35057
|
+
usable: true,
|
|
35058
|
+
armed: false
|
|
35059
|
+
});
|
|
35060
|
+
return prev !== void 0 && !prev.usable ? "recovered" : null;
|
|
35061
|
+
}
|
|
35062
|
+
if (prev === void 0) {
|
|
35063
|
+
this.state.set(nodeId, {
|
|
35064
|
+
usable: true,
|
|
35065
|
+
armed: true
|
|
35066
|
+
});
|
|
35067
|
+
return null;
|
|
35068
|
+
}
|
|
35069
|
+
if (!prev.usable) {
|
|
35070
|
+
this.state.set(nodeId, {
|
|
35071
|
+
usable: false,
|
|
35072
|
+
armed: true
|
|
35073
|
+
});
|
|
35074
|
+
return null;
|
|
35075
|
+
}
|
|
35076
|
+
if (!prev.armed) {
|
|
35077
|
+
this.state.set(nodeId, {
|
|
35078
|
+
usable: true,
|
|
35079
|
+
armed: true
|
|
35080
|
+
});
|
|
35081
|
+
return null;
|
|
35082
|
+
}
|
|
35083
|
+
this.state.set(nodeId, {
|
|
35084
|
+
usable: false,
|
|
35085
|
+
armed: true
|
|
35086
|
+
});
|
|
35087
|
+
return "became-unusable";
|
|
35088
|
+
}
|
|
35089
|
+
/** Can this node be given cameras? Unknown nodes answer YES. */
|
|
35090
|
+
isUsable(nodeId) {
|
|
35091
|
+
return this.state.get(nodeId)?.usable ?? true;
|
|
35092
|
+
}
|
|
35093
|
+
/** Nodes currently excluded — for the placement log and diagnostics. */
|
|
35094
|
+
unusableNodeIds() {
|
|
35095
|
+
const out = [];
|
|
35096
|
+
for (const [nodeId, s] of this.state) if (!s.usable) out.push(nodeId);
|
|
35097
|
+
return out.toSorted();
|
|
35098
|
+
}
|
|
35099
|
+
forget(nodeId) {
|
|
35100
|
+
this.state.delete(nodeId);
|
|
35101
|
+
}
|
|
35102
|
+
reset() {
|
|
35103
|
+
this.state.clear();
|
|
35104
|
+
}
|
|
35105
|
+
};
|
|
35106
|
+
//#endregion
|
|
34826
35107
|
//#region src/orchestrator-types.ts
|
|
34827
35108
|
var PHASE_MODE_VALUES = new Set([
|
|
34828
35109
|
"disabled",
|
|
@@ -36333,6 +36614,69 @@ var LoadShedController = class LoadShedController {
|
|
|
36333
36614
|
}
|
|
36334
36615
|
}
|
|
36335
36616
|
};
|
|
36617
|
+
var DEFAULT_DETECTION_BLIND_CONFIG = {
|
|
36618
|
+
thresholdMs: 60 * 6e4,
|
|
36619
|
+
frameFreshnessMs: 5 * 6e4
|
|
36620
|
+
};
|
|
36621
|
+
var DetectionBlindMonitor = class {
|
|
36622
|
+
config;
|
|
36623
|
+
state = /* @__PURE__ */ new Map();
|
|
36624
|
+
constructor(config = DEFAULT_DETECTION_BLIND_CONFIG) {
|
|
36625
|
+
this.config = config;
|
|
36626
|
+
}
|
|
36627
|
+
/**
|
|
36628
|
+
* One inference frame arrived for a device. `hadDetection` is false for a
|
|
36629
|
+
* frame that returned nothing — which is the whole point: an empty frame
|
|
36630
|
+
* still proves the pipeline is alive, and it is the ONLY evidence that
|
|
36631
|
+
* distinguishes blind from detached.
|
|
36632
|
+
*/
|
|
36633
|
+
noteFrame(deviceId, hadDetection, now) {
|
|
36634
|
+
const prev = this.state.get(deviceId);
|
|
36635
|
+
if (hadDetection) {
|
|
36636
|
+
this.state.set(deviceId, {
|
|
36637
|
+
lastFrameAt: now,
|
|
36638
|
+
lastDetectionAt: now,
|
|
36639
|
+
reported: false
|
|
36640
|
+
});
|
|
36641
|
+
return;
|
|
36642
|
+
}
|
|
36643
|
+
this.state.set(deviceId, {
|
|
36644
|
+
lastFrameAt: now,
|
|
36645
|
+
lastDetectionAt: prev?.lastDetectionAt ?? now,
|
|
36646
|
+
reported: prev?.reported ?? false
|
|
36647
|
+
});
|
|
36648
|
+
}
|
|
36649
|
+
/**
|
|
36650
|
+
* Devices that have JUST crossed into blind. Call from an existing periodic
|
|
36651
|
+
* sweep; it allocates nothing on the healthy path.
|
|
36652
|
+
*/
|
|
36653
|
+
sweep(now) {
|
|
36654
|
+
const out = [];
|
|
36655
|
+
for (const [deviceId, s] of this.state) {
|
|
36656
|
+
if (s.reported) continue;
|
|
36657
|
+
if (now - s.lastFrameAt > this.config.frameFreshnessMs) continue;
|
|
36658
|
+
const blindForMs = now - s.lastDetectionAt;
|
|
36659
|
+
if (blindForMs < this.config.thresholdMs) continue;
|
|
36660
|
+
this.state.set(deviceId, {
|
|
36661
|
+
...s,
|
|
36662
|
+
reported: true
|
|
36663
|
+
});
|
|
36664
|
+
out.push({
|
|
36665
|
+
deviceId,
|
|
36666
|
+
blindForMs,
|
|
36667
|
+
thresholdMs: this.config.thresholdMs
|
|
36668
|
+
});
|
|
36669
|
+
}
|
|
36670
|
+
return out;
|
|
36671
|
+
}
|
|
36672
|
+
/** Drop a device (detach / unbind), so a re-attach starts a fresh clock. */
|
|
36673
|
+
forget(deviceId) {
|
|
36674
|
+
this.state.delete(deviceId);
|
|
36675
|
+
}
|
|
36676
|
+
reset() {
|
|
36677
|
+
this.state.clear();
|
|
36678
|
+
}
|
|
36679
|
+
};
|
|
36336
36680
|
//#endregion
|
|
36337
36681
|
//#region src/agent-load-service.ts
|
|
36338
36682
|
var AgentLoadService = class AgentLoadService {
|
|
@@ -38585,6 +38929,7 @@ var DetectionWiringController = class {
|
|
|
38585
38929
|
if (!this.deps.ctx()) return;
|
|
38586
38930
|
const { deviceId, frame, frameHandle, capturedAt } = payload;
|
|
38587
38931
|
this.deps.watchdog()?.noteSignal(deviceId, "detection");
|
|
38932
|
+
this.deps.blindMonitor()?.noteFrame(deviceId, frame.detections.length > 0, Date.now());
|
|
38588
38933
|
if (frame.detections.length === 0 && (frame.discarded?.length ?? 0) === 0) return;
|
|
38589
38934
|
this.deps.eventBus.emit({
|
|
38590
38935
|
id: `detection-${deviceId}-${Date.now()}`,
|
|
@@ -40372,6 +40717,34 @@ var PlacementService = class {
|
|
|
40372
40717
|
constructor(deps) {
|
|
40373
40718
|
this.deps = deps;
|
|
40374
40719
|
}
|
|
40720
|
+
/**
|
|
40721
|
+
* Drop nodes that cannot infer from a candidate set.
|
|
40722
|
+
*
|
|
40723
|
+
* Fails OPEN in one specific way that matters: if the filter would leave NO
|
|
40724
|
+
* candidate at all, the unfiltered set is used. Placing a camera on a node
|
|
40725
|
+
* that might fail its inference is bad; refusing to place it anywhere
|
|
40726
|
+
* because every node is currently mid-probe is worse, and that is the exact
|
|
40727
|
+
* shape a cold cluster start has.
|
|
40728
|
+
*/
|
|
40729
|
+
filterInferenceUsable(nodes, deviceId) {
|
|
40730
|
+
const usable = nodes.filter((nodeId) => this.deps.isNodeInferenceUsable(nodeId));
|
|
40731
|
+
if (usable.length === nodes.length) return nodes;
|
|
40732
|
+
if (usable.length === 0) {
|
|
40733
|
+
this.deps.ctx().logger.warn("every candidate node reports no usable inference device — placing anyway", {
|
|
40734
|
+
tags: { deviceId },
|
|
40735
|
+
meta: { nodes: [...nodes] }
|
|
40736
|
+
});
|
|
40737
|
+
return nodes;
|
|
40738
|
+
}
|
|
40739
|
+
this.deps.ctx().logger.info("nodes dropped from placement — no usable inference device", {
|
|
40740
|
+
tags: { deviceId },
|
|
40741
|
+
meta: {
|
|
40742
|
+
dropped: nodes.filter((n) => !usable.includes(n)),
|
|
40743
|
+
kept: usable
|
|
40744
|
+
}
|
|
40745
|
+
});
|
|
40746
|
+
return usable;
|
|
40747
|
+
}
|
|
40375
40748
|
async dispatchCamera(runnerConfig) {
|
|
40376
40749
|
if (!this.deps.ctx()) throw new Error("PipelineOrchestrator: dispatchCamera called before initialize");
|
|
40377
40750
|
if (this.deps.maintenancePaused?.() === true) return {
|
|
@@ -40386,7 +40759,7 @@ var PlacementService = class {
|
|
|
40386
40759
|
};
|
|
40387
40760
|
const preferredAgent = await this.deps.readPipelinePin(runnerConfig.deviceId);
|
|
40388
40761
|
const loads = await this.deps.loadService.collectAgentLoad({ onlyEnabled: true });
|
|
40389
|
-
const eligible = this.deps.topology.detectionEligibleNodes(runnerConfig.deviceId);
|
|
40762
|
+
const eligible = this.filterInferenceUsable(this.deps.topology.detectionEligibleNodes(runnerConfig.deviceId), runnerConfig.deviceId);
|
|
40390
40763
|
const nodeCaps = await this.deps.settingsStore.buildNodeCaps();
|
|
40391
40764
|
const motionRoleCandidate = pickMotionRolePlacement({
|
|
40392
40765
|
isAnalyzer: runnerConfig.motionSources.includes("analyzer"),
|
|
@@ -44259,7 +44632,10 @@ var SessionDispatchController = class {
|
|
|
44259
44632
|
await this.deps.reconcilePlacementFromRunners();
|
|
44260
44633
|
const preferredAgent = await this.deps.readPipelinePin(deviceId);
|
|
44261
44634
|
const loads = await this.agentLoads.get(CLUSTER_KEY);
|
|
44262
|
-
const
|
|
44635
|
+
const allEligible = this.deps.topology.detectionEligibleNodes(deviceId);
|
|
44636
|
+
const usableEligible = allEligible.filter((n) => this.deps.isNodeInferenceUsable(n));
|
|
44637
|
+
const eligible = usableEligible.length > 0 ? usableEligible : allEligible;
|
|
44638
|
+
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)) } });
|
|
44263
44639
|
const decision = balance({
|
|
44264
44640
|
nodes: loads,
|
|
44265
44641
|
preferredAgent,
|
|
@@ -45181,6 +45557,78 @@ var ZonesProvider = class {
|
|
|
45181
45557
|
//#endregion
|
|
45182
45558
|
//#region src/orchestrator-bootstrap.ts
|
|
45183
45559
|
/**
|
|
45560
|
+
* `orchestrator-bootstrap.ts` — the controller-construction body of
|
|
45561
|
+
* `onInitialize` (S11 Task 4, last extraction of the S11 series).
|
|
45562
|
+
*
|
|
45563
|
+
* `buildOrchestratorControllers(deps)` constructs all ~16 controllers the
|
|
45564
|
+
* addon owns, in the EXACT order `onInitialize` built them in before this
|
|
45565
|
+
* extraction (ledger → topology → loadService → audio → settingsStore →
|
|
45566
|
+
* loadShed → placement → session → deviceConfig → cameraStatusService →
|
|
45567
|
+
* reconcile → [settings load + localNodeId resolve] → nodeLifecycle →
|
|
45568
|
+
* [event subs + readiness seed] → pipelineWatchdog → [timers + legacy
|
|
45569
|
+
* migration] → zoneRulesProvider → zonesProvider →
|
|
45570
|
+
* [wireOrchestratorSubscriptions + eager reconcile] → detectionWiring).
|
|
45571
|
+
* Every dep closure is carried over byte-for-byte — only `this.X` became
|
|
45572
|
+
* either a local variable (for a controller already constructed earlier in
|
|
45573
|
+
* THIS function) or a `deps.X` accessor (for addon-owned state/methods that
|
|
45574
|
+
* stay on `PipelineOrchestratorAddon`, or for a controller referenced
|
|
45575
|
+
* before its own construction point — a genuine forward ref, always via a
|
|
45576
|
+
* LAZY closure, exactly as it was in `onInitialize`).
|
|
45577
|
+
*
|
|
45578
|
+
* Three things could not simply become local variables, because an EXISTING
|
|
45579
|
+
* (unmoved) addon method reads the REAL class field synchronously, mid-
|
|
45580
|
+
* construction, not through a lazy closure:
|
|
45581
|
+
* - `applyRuntimeSettings` (stays on the addon) reads `this.topology!`
|
|
45582
|
+
* and `this.reconcile!` directly — called synchronously from the
|
|
45583
|
+
* settings-load block below. `deps.setTopology`/`deps.setReconcile`
|
|
45584
|
+
* progressively sync the REAL addon fields the instant each is built,
|
|
45585
|
+
* so `applyRuntimeSettings` sees them.
|
|
45586
|
+
* - `subscribeToAudioAnalyzerReadiness` (stays on the addon) reads
|
|
45587
|
+
* `this.nodeLifecycle!.readinessRegistry` AND `this.audio!` directly —
|
|
45588
|
+
* called synchronously right after `nodeLifecycle` is built (readiness
|
|
45589
|
+
* seed + the `AgentOnline` handler). `deps.setNodeLifecycle` /
|
|
45590
|
+
* `deps.setAudio` cover it the same way. This one bit LIVE: `audio` is
|
|
45591
|
+
* constructed early (fourth) but `subscribeToAudioAnalyzerReadiness`
|
|
45592
|
+
* wasn't called until much later (after `nodeLifecycle` exists), so the
|
|
45593
|
+
* old code never noticed `this.audio` was still unsynced — until
|
|
45594
|
+
* `ReadinessRegistry.onReadyState`'s `queueMicrotask` hydration replay
|
|
45595
|
+
* (an already-`ready` cap fires the handler once, asynchronously, on
|
|
45596
|
+
* subscribe) won the race against the `await buildOrchestratorControllers(...)`
|
|
45597
|
+
* continuation in `onInitialize` that assigns `this.audio` — both are
|
|
45598
|
+
* microtasks, and the replay's `queueMicrotask` was enqueued (during
|
|
45599
|
+
* this still-synchronous function body) strictly before this function's
|
|
45600
|
+
* `return` resolves the outer promise, so it always runs first. Fixed
|
|
45601
|
+
* by syncing `this.audio` the instant it's constructed, exactly like
|
|
45602
|
+
* `topology`/`reconcile`/`nodeLifecycle` — no lazy closure ever reads a
|
|
45603
|
+
* controller before this function has assigned its real field.
|
|
45604
|
+
* Every other controller-to-controller reference in this function — even
|
|
45605
|
+
* ones that look like a forward ref (e.g. `loadShed`'s `attach` closure
|
|
45606
|
+
* onto `placement`, built one step later) — is a LAZY closure that is
|
|
45607
|
+
* never invoked synchronously during construction (only later, off
|
|
45608
|
+
* event-bus subscriptions, timers, or RPC handlers), so a plain local
|
|
45609
|
+
* `let` variable captured by reference is sufficient and exactly
|
|
45610
|
+
* preserves the original semantics: see each field's inline comment in
|
|
45611
|
+
* `index.ts` for the original construction-order reasoning, unchanged
|
|
45612
|
+
* here.
|
|
45613
|
+
*
|
|
45614
|
+
* `globalSettings` is a purely local variable — read/written ONLY by
|
|
45615
|
+
* closures built inside this same function (the one exception,
|
|
45616
|
+
* `deviceConfig`'s `setGlobalSettings`, is ALSO built here, so the local
|
|
45617
|
+
* variable stays the single source of truth for every reader). No code
|
|
45618
|
+
* outside this closure network ever read the former `this.globalSettings`
|
|
45619
|
+
* class field, so it isn't part of the returned `OrchestratorControllers`
|
|
45620
|
+
* — the field was removed from the class entirely (S11 Task 4 cleanup).
|
|
45621
|
+
* `localNodeId` IS returned — `index.ts` still reads `this.localNodeId`
|
|
45622
|
+
* from a couple of unrelated cap methods (`dumpDiagnostics`,
|
|
45623
|
+
* `getDecoderAssignments`), so the class field is populated once, from the
|
|
45624
|
+
* returned `OrchestratorControllers`, after this function returns.
|
|
45625
|
+
* `failoverPolicy` is the opposite case from `globalSettings` —
|
|
45626
|
+
* `applyRuntimeSettings` (unmoved) writes the REAL field on every future
|
|
45627
|
+
* settings change, so `nodeLifecycle`'s `failoverPolicy` closure MUST keep
|
|
45628
|
+
* reading through `deps.failoverPolicy()` (never a local snapshot) to see
|
|
45629
|
+
* those later updates.
|
|
45630
|
+
*/
|
|
45631
|
+
/**
|
|
45184
45632
|
* Construct every orchestrator controller, wire the agent-lifecycle event
|
|
45185
45633
|
* subscriptions + readiness seed + detection-wiring subscriptions, and
|
|
45186
45634
|
* kick the eager reconcile — the ENTIRE former `onInitialize` body between
|
|
@@ -45339,6 +45787,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45339
45787
|
localNodeId: () => localNodeId,
|
|
45340
45788
|
readPipelinePin: (deviceId) => deps.readPipelinePin(deviceId),
|
|
45341
45789
|
getEligibleInferenceDevices: (nodeId) => deps.getEligibleInferenceDevices(nodeId),
|
|
45790
|
+
isNodeInferenceUsable: (nodeId) => deps.isNodeInferenceUsable(nodeId),
|
|
45342
45791
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
45343
45792
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
45344
45793
|
inferenceRotation,
|
|
@@ -45368,6 +45817,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45368
45817
|
listZones: async (deviceId) => await zonesProvider?.listZones({ deviceId }) ?? [],
|
|
45369
45818
|
readPipelinePin: (deviceId) => deps.readPipelinePin(deviceId),
|
|
45370
45819
|
getEligibleInferenceDevices: (nodeId) => deps.getEligibleInferenceDevices(nodeId),
|
|
45820
|
+
isNodeInferenceUsable: (nodeId) => deps.isNodeInferenceUsable(nodeId),
|
|
45371
45821
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
45372
45822
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
45373
45823
|
inferenceRotation,
|
|
@@ -45526,7 +45976,34 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45526
45976
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
45527
45977
|
});
|
|
45528
45978
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|
|
45529
|
-
const
|
|
45979
|
+
const blindMonitor = new DetectionBlindMonitor();
|
|
45980
|
+
const pendingRetryTimer = setInterval(() => {
|
|
45981
|
+
reconcile.retryPendingDispatches();
|
|
45982
|
+
for (const blind of blindMonitor.sweep(Date.now())) {
|
|
45983
|
+
deps.ctx().logger.error("camera is being analysed and detecting NOTHING", {
|
|
45984
|
+
tags: { deviceId: blind.deviceId },
|
|
45985
|
+
meta: {
|
|
45986
|
+
blindForMs: blind.blindForMs,
|
|
45987
|
+
thresholdMs: blind.thresholdMs
|
|
45988
|
+
}
|
|
45989
|
+
});
|
|
45990
|
+
deps.ctx().eventBus.emit({
|
|
45991
|
+
id: (0, node_crypto.randomUUID)(),
|
|
45992
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
45993
|
+
source: {
|
|
45994
|
+
type: "device",
|
|
45995
|
+
id: blind.deviceId,
|
|
45996
|
+
deviceId: blind.deviceId
|
|
45997
|
+
},
|
|
45998
|
+
category: EventCategory.PipelineDetectionBlind,
|
|
45999
|
+
data: {
|
|
46000
|
+
deviceId: blind.deviceId,
|
|
46001
|
+
blindForMs: blind.blindForMs,
|
|
46002
|
+
thresholdMs: blind.thresholdMs
|
|
46003
|
+
}
|
|
46004
|
+
});
|
|
46005
|
+
}
|
|
46006
|
+
}, PENDING_RETRY_INTERVAL_MS);
|
|
45530
46007
|
const autoRebalanceTimer = setInterval(() => void placement.runAutoRebalance(), AUTO_REBALANCE_INTERVAL_MS);
|
|
45531
46008
|
deps.ctx().logger.info("Pipeline orchestrator detection-wiring subscriptions installed");
|
|
45532
46009
|
migrateLegacyFlagsToBindings({
|
|
@@ -45653,6 +46130,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
45653
46130
|
loadShed,
|
|
45654
46131
|
nodeStress,
|
|
45655
46132
|
watchdog: () => pipelineWatchdog,
|
|
46133
|
+
blindMonitor: () => blindMonitor,
|
|
45656
46134
|
api: () => deps.ctx().api ?? null,
|
|
45657
46135
|
eventBus: deps.ctx().eventBus,
|
|
45658
46136
|
logger: deps.ctx().logger,
|
|
@@ -46230,6 +46708,48 @@ function deriveRuntimeSettings(config) {
|
|
|
46230
46708
|
//#endregion
|
|
46231
46709
|
//#region src/index.ts
|
|
46232
46710
|
/**
|
|
46711
|
+
* addon-pipeline-orchestrator — hub-side camera-to-agent load balancer.
|
|
46712
|
+
*
|
|
46713
|
+
* Owns the control plane for the distributed detection pipeline. Tracks
|
|
46714
|
+
* which `addon-pipeline-runner` instance is responsible for each camera,
|
|
46715
|
+
* queries each runner's capacity + hardware profile, and dispatches
|
|
46716
|
+
* `attachCamera` / `detachCamera` cap calls to the appropriate runner via
|
|
46717
|
+
* the Moleculer broker. Persists manual pins via the per-device settings
|
|
46718
|
+
* store so user overrides survive hub restarts.
|
|
46719
|
+
*
|
|
46720
|
+
* Strictly control plane — never sees a frame, never runs inference, never
|
|
46721
|
+
* drains queues. The runner does that work locally on each vision node.
|
|
46722
|
+
*
|
|
46723
|
+
* ## Dispatch entrypoints
|
|
46724
|
+
*
|
|
46725
|
+
* - **`dispatchCamera(runnerConfig)`** — in-process only (not in the cap
|
|
46726
|
+
* def). Called by `DetectionWiringService` with a freshly-built
|
|
46727
|
+
* `RunnerCameraConfig`. Runs the L1/L2/L3 balancer, picks an agent, then
|
|
46728
|
+
* issues `broker.call('pipeline-runner.pipeline-runner.attachCamera', ...)`.
|
|
46729
|
+
* Caches the config so future rebalance / pinning can re-dispatch without
|
|
46730
|
+
* the caller rebuilding it.
|
|
46731
|
+
*
|
|
46732
|
+
* - **`assignCamera({deviceId, agentNodeId})`** — cap method. Persists a
|
|
46733
|
+
* manual `preferredAgent` pin to per-device settings and re-dispatches
|
|
46734
|
+
* immediately if the camera is already attached.
|
|
46735
|
+
*
|
|
46736
|
+
* - **`unassignCamera({deviceId})`** — cap method. Clears the pin,
|
|
46737
|
+
* detaches the camera from its current agent.
|
|
46738
|
+
*
|
|
46739
|
+
* - **`rebalance()`** — cap method. Walks every cached camera config and
|
|
46740
|
+
* re-runs the balancer; migrates cameras whose current agent is no
|
|
46741
|
+
* longer optimal. Pinned cameras are never migrated.
|
|
46742
|
+
*
|
|
46743
|
+
* ## Runner dispatch
|
|
46744
|
+
*
|
|
46745
|
+
* All runner calls go through `this.ctx.api.pipelineRunner.*` (the generated
|
|
46746
|
+
* tRPC cap router). The router extracts `nodeId` from the input and
|
|
46747
|
+
* routes transparently to the local runner (direct in-process call) or
|
|
46748
|
+
* to a remote agent (via Moleculer). Node discovery is driven by
|
|
46749
|
+
* `EventCategory.AgentOnline/AgentOffline` events forwarded by
|
|
46750
|
+
* `agent-registry.service.ts`.
|
|
46751
|
+
*/
|
|
46752
|
+
/**
|
|
46233
46753
|
* The FULL action catalog, under the name the HUB HARVESTS.
|
|
46234
46754
|
*
|
|
46235
46755
|
* The hub's forked-addon harvest imports this entry module and reads the
|
|
@@ -46488,6 +47008,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
46488
47008
|
detachOn: (nodeId, deviceId) => this.detachOn(nodeId, deviceId),
|
|
46489
47009
|
readPipelinePin: (deviceId) => this.readPipelinePin(deviceId),
|
|
46490
47010
|
getEligibleInferenceDevices: (nodeId) => this.resolveEligibleInferenceDevices(nodeId),
|
|
47011
|
+
isNodeInferenceUsable: (nodeId) => this.isNodeInferenceUsable(nodeId),
|
|
46491
47012
|
getInferenceDeviceCaps: (nodeId) => this.getInferenceDeviceCaps(nodeId),
|
|
46492
47013
|
readPipelineDevicePin: (deviceId) => this.readPipelineDevicePin(deviceId),
|
|
46493
47014
|
subscribeToAudioAnalyzerReadiness: (nodeId) => this.subscribeToAudioAnalyzerReadiness(nodeId),
|
|
@@ -47144,11 +47665,66 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
47144
47665
|
const stored = (await this.settingsStore.readAgentSettingsMap())[nodeId]?.inferenceDevices ?? {};
|
|
47145
47666
|
const { probed } = await this.probeNodeInferenceDevices(nodeId);
|
|
47146
47667
|
const catalog = await this.settingsStore.getCatalogForNode(nodeId);
|
|
47147
|
-
const
|
|
47668
|
+
const eligibility = resolveInferenceDeviceEligibility(probed, stored, catalog ? makeRootCapabilityGuard(catalog) : void 0);
|
|
47669
|
+
const { eligible, excluded } = eligibility;
|
|
47148
47670
|
this.logInferenceDeviceShrinkage(nodeId, eligible, excluded);
|
|
47671
|
+
this.noteNodeInferenceUsability(nodeId, resolveNodeInferenceUsability(eligibility));
|
|
47149
47672
|
return eligible;
|
|
47150
47673
|
}
|
|
47151
47674
|
/**
|
|
47675
|
+
* Fold one usability observation into the placement mirror and announce the
|
|
47676
|
+
* transition.
|
|
47677
|
+
*
|
|
47678
|
+
* This is called from the SAME read the dispatcher already makes, plus the
|
|
47679
|
+
* session controller's background refresher — so a node's usability is
|
|
47680
|
+
* re-observed on the cadence placement itself runs at, without a timer of
|
|
47681
|
+
* its own. The mirror is what makes the answer readable synchronously by
|
|
47682
|
+
* `detectionEligibleNodes`' callers; see its own docblock for why it refuses
|
|
47683
|
+
* to act on a single bad read.
|
|
47684
|
+
*
|
|
47685
|
+
* The system event is emitted ONLY on the transition into unusable — the
|
|
47686
|
+
* kind is `node-inference-unavailable` and it is authorable in a rule.
|
|
47687
|
+
* Recovery is logged and deliberately has no event: a notification saying
|
|
47688
|
+
* the broken thing is no longer broken is a second rule nobody asked for.
|
|
47689
|
+
*/
|
|
47690
|
+
noteNodeInferenceUsability(nodeId, usability) {
|
|
47691
|
+
const transition = this.nodeInferenceUsability.observe(nodeId, usability.usable);
|
|
47692
|
+
if (transition === null) return;
|
|
47693
|
+
if (transition === "recovered") {
|
|
47694
|
+
this.ctx.logger.info("node inference recovered — back in the placement candidate set", {
|
|
47695
|
+
tags: { nodeId },
|
|
47696
|
+
meta: { eligible: usability.eligibleKeys }
|
|
47697
|
+
});
|
|
47698
|
+
return;
|
|
47699
|
+
}
|
|
47700
|
+
this.ctx.logger.error("node has NO usable inference device — removed from the placement candidate set", {
|
|
47701
|
+
tags: { nodeId },
|
|
47702
|
+
meta: {
|
|
47703
|
+
unavailable: usability.unavailableKeys,
|
|
47704
|
+
eligible: usability.eligibleKeys
|
|
47705
|
+
}
|
|
47706
|
+
});
|
|
47707
|
+
this.ctx.eventBus.emit({
|
|
47708
|
+
id: (0, node_crypto.randomUUID)(),
|
|
47709
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
47710
|
+
source: {
|
|
47711
|
+
type: "addon",
|
|
47712
|
+
id: "pipeline-orchestrator",
|
|
47713
|
+
addonId: "pipeline-orchestrator"
|
|
47714
|
+
},
|
|
47715
|
+
category: EventCategory.PipelineNodeInferenceUnavailable,
|
|
47716
|
+
data: {
|
|
47717
|
+
nodeId,
|
|
47718
|
+
unavailableDeviceKeys: usability.unavailableKeys
|
|
47719
|
+
}
|
|
47720
|
+
});
|
|
47721
|
+
}
|
|
47722
|
+
/** Can this node be given cameras? Read synchronously by the two dispatch
|
|
47723
|
+
* paths before they hand `eligibleNodes` to the balancer. */
|
|
47724
|
+
isNodeInferenceUsable(nodeId) {
|
|
47725
|
+
return this.nodeInferenceUsability.isUsable(nodeId);
|
|
47726
|
+
}
|
|
47727
|
+
/**
|
|
47152
47728
|
* Per-node signature of the last logged eligible/excluded split, so
|
|
47153
47729
|
* {@link logInferenceDeviceShrinkage} fires on CHANGE only. This runs on
|
|
47154
47730
|
* every camera dispatch; without the gate it would be one line per dispatch
|
|
@@ -47156,6 +47732,14 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
47156
47732
|
*/
|
|
47157
47733
|
lastInferenceEligibilitySignature = /* @__PURE__ */ new Map();
|
|
47158
47734
|
/**
|
|
47735
|
+
* Which nodes can still infer. Refreshed by
|
|
47736
|
+
* {@link resolveEligibleInferenceDevices}; read synchronously by both
|
|
47737
|
+
* dispatch paths so a node whose every enabled accelerator vanished stops
|
|
47738
|
+
* receiving cameras. little-unraid was handed cameras for a whole day in
|
|
47739
|
+
* exactly that state.
|
|
47740
|
+
*/
|
|
47741
|
+
nodeInferenceUsability = new NodeInferenceUsabilityMirror();
|
|
47742
|
+
/**
|
|
47159
47743
|
* Say out loud which of a node's inference devices were dropped from the
|
|
47160
47744
|
* candidate set, and why.
|
|
47161
47745
|
*
|