@camstack/addon-pipeline-orchestrator 1.2.45 → 1.2.47
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-ea01d8cace0464fa1c39149da612be36.js → Clipboard-7ce228c3105c6643f60262c6665333bb.js} +8 -8
- package/assets/viewer/_expo/static/js/web/{Haptics-35bc7b5bbbc3731f03b5d5fafff2ac0c.js → Haptics-a27d50d15732e0b48774f90db59c02bf.js} +3 -3
- package/assets/viewer/_expo/static/js/web/{index-5b79e9d56650c4720921fd27c27ccb33.js → index-a8dfe1019b6f61fc84bf4dba973b9571.js} +27 -26
- package/assets/viewer/_expo/static/js/web/{node-fs-7cc97cb7668ecaeabcbcfee447afb982.js → node-fs-48d2a76a1b4358828a29686e594be263.js} +1 -1
- package/assets/viewer/_expo/static/js/web/{node-path-b4104903c9511874cbbf037d17e9f78d.js → node-path-f5cd235cc4e30b6508669583b1b2c718.js} +1 -1
- package/assets/viewer/index.html +1 -1
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-9CKmTiwO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DVSFAO6F.mjs} +1 -1
- package/dist/{hostInit-CpXFeXA9.mjs → hostInit-D4Kq_1dM.mjs} +1 -1
- package/dist/index.js +304 -33
- package/dist/index.mjs +304 -33
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/assets/viewer/assets/node_modules/expo-router/assets/arrow_down.017bc6ba3fc25503e5eb5e53826d48a8.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/error.d1ea1496f9057eb392d5bbf3732a61b7.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/file.19eeb73b9593a38f8e9f418337fc7d10.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/forward.d8b800c443b8972542883e0b9de2bdc6.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/pkg.ab19f4cbc543357183a20571f68380a3.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/back-icon-mask.0a328cd9c1afd0afe8e3b1ec5165b1b4.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/back-icon.35ba0eaec5a4f5ed12ca16fabeae451d.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@2x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@3x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/clear-icon.c94f6478e7ae0cdd9f15de1fcb9e5e55@4x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@2x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@3x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/close-icon.808e1b1b9b53114ec2838071a7e6daa7@4x.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/react-navigation/elements/search-icon.286d67d3f74808a60a78d3ebf1a5fb57.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/sitemap.412dd9275b6b48ad28f5e3d81bb1f626.png +0 -0
- package/assets/viewer/assets/node_modules/expo-router/assets/unmatched.20e71bdf79e3a97bf55fd9e164041578.png +0 -0
package/dist/index.mjs
CHANGED
|
@@ -34960,6 +34960,48 @@ var DeviceFeaturesMirror = class {
|
|
|
34960
34960
|
}
|
|
34961
34961
|
};
|
|
34962
34962
|
//#endregion
|
|
34963
|
+
//#region src/watchdog-camera.ts
|
|
34964
|
+
/**
|
|
34965
|
+
* Derive the watchdog camera descriptor from a runner config. Captures which
|
|
34966
|
+
* stages are expected to flow CONTINUOUSLY (always-on audio, analyzer motion,
|
|
34967
|
+
* always-on detection) → the camStreamId feeding each. Audio has no dedicated
|
|
34968
|
+
* stream field on the runner config, so it shares the motion stream id.
|
|
34969
|
+
*
|
|
34970
|
+
* `pipelineEnabled === false` (the `object-detection` switch, D62/ADR-0109)
|
|
34971
|
+
* arms NOTHING: `startDetection` deliberately performs no attach and opens no
|
|
34972
|
+
* decode session for such a camera, so no continuous stage can ever produce a
|
|
34973
|
+
* signal. The camera is still returned (and still registered by the caller) so
|
|
34974
|
+
* its watchdog row stays visible — a switched-off camera must read as off, not
|
|
34975
|
+
* as broken.
|
|
34976
|
+
*
|
|
34977
|
+
* `hasAudioSource` is the caller's evidence that the camera's source actually
|
|
34978
|
+
* carries an audio track (`BrokerStats.audio !== null`). It fails OPEN: an
|
|
34979
|
+
* unknown or failed read passes `true`, because the audio stage is diagnostic
|
|
34980
|
+
* and a false negative silently stops watching a working camera.
|
|
34981
|
+
*/
|
|
34982
|
+
function buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
34983
|
+
const continuousStages = /* @__PURE__ */ new Map();
|
|
34984
|
+
if (config.pipelineEnabled === false) return {
|
|
34985
|
+
deviceId: config.deviceId,
|
|
34986
|
+
deviceName,
|
|
34987
|
+
audioMode: config.audioMode,
|
|
34988
|
+
detectionMode: config.detectionMode,
|
|
34989
|
+
motionSources: config.motionSources,
|
|
34990
|
+
continuousStages
|
|
34991
|
+
};
|
|
34992
|
+
if (config.audioMode === "always-on" && hasAudioSource) continuousStages.set("audio", config.motionStreamId);
|
|
34993
|
+
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
34994
|
+
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
34995
|
+
return {
|
|
34996
|
+
deviceId: config.deviceId,
|
|
34997
|
+
deviceName,
|
|
34998
|
+
audioMode: config.audioMode,
|
|
34999
|
+
detectionMode: config.detectionMode,
|
|
35000
|
+
motionSources: config.motionSources,
|
|
35001
|
+
continuousStages
|
|
35002
|
+
};
|
|
35003
|
+
}
|
|
35004
|
+
//#endregion
|
|
34963
35005
|
//#region src/detection-wiring-controller.ts
|
|
34964
35006
|
var DetectionWiringController = class {
|
|
34965
35007
|
deps;
|
|
@@ -35164,7 +35206,8 @@ var DetectionWiringController = class {
|
|
|
35164
35206
|
await this.stopDetection(deviceId);
|
|
35165
35207
|
}
|
|
35166
35208
|
this.activeDetections.set(deviceId, config);
|
|
35167
|
-
if (config.
|
|
35209
|
+
if (!config.pipelineEnabled) log.info("object detection switched off — no attach, no decode session", { meta: { detectionMode: config.detectionMode } });
|
|
35210
|
+
else if (config.detectionMode === "on-motion") {
|
|
35168
35211
|
if (config.motionSources.includes("analyzer")) try {
|
|
35169
35212
|
await this.deps.session.ensureMotionWatch(deviceId, config);
|
|
35170
35213
|
} catch (err) {
|
|
@@ -35255,11 +35298,44 @@ var DetectionWiringController = class {
|
|
|
35255
35298
|
this.deps.forgetDeviceZones(deviceId);
|
|
35256
35299
|
this.deps.forgetDeviceZoneRules(deviceId);
|
|
35257
35300
|
this.deps.loadShed.forgetDevice(deviceId);
|
|
35258
|
-
|
|
35301
|
+
const settings = this.deps.ctx().settings;
|
|
35302
|
+
if (!settings) return;
|
|
35259
35303
|
await this.deps.settingsStore.deleteCameraSettings(deviceId);
|
|
35304
|
+
try {
|
|
35305
|
+
await settings.clearDeviceStore(deviceId);
|
|
35306
|
+
} catch (err) {
|
|
35307
|
+
this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
|
|
35308
|
+
tags: { deviceId },
|
|
35309
|
+
meta: { error: errMsg(err) }
|
|
35310
|
+
});
|
|
35311
|
+
}
|
|
35260
35312
|
this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
|
|
35261
35313
|
}
|
|
35262
35314
|
/**
|
|
35315
|
+
* True ONLY when the device registry positively answered "this device does
|
|
35316
|
+
* not exist" (`getDevice` → `null`, per the cap contract).
|
|
35317
|
+
*
|
|
35318
|
+
* A FAILED read answers `false` — D49: a read that fails changes nothing, and
|
|
35319
|
+
* the destructive direction here is refusing to run a live camera.
|
|
35320
|
+
* Deliberately NOT routed through {@link lookupDeviceFeatures}'s mirror: that
|
|
35321
|
+
* one treats missing-as-unknown on purpose (a provider restart passes through
|
|
35322
|
+
* exactly that state and must not re-derive a battery camera's profile), and
|
|
35323
|
+
* conflating the two would make a provider restart look like a deletion.
|
|
35324
|
+
*/
|
|
35325
|
+
async isDeviceDeleted(deviceId) {
|
|
35326
|
+
const api = this.deps.api();
|
|
35327
|
+
if (!api) return false;
|
|
35328
|
+
try {
|
|
35329
|
+
return await api.deviceManager.getDevice.query({ deviceId }) === null;
|
|
35330
|
+
} catch (err) {
|
|
35331
|
+
this.deps.logger.debug("device existence check failed — proceeding as before", {
|
|
35332
|
+
tags: { deviceId },
|
|
35333
|
+
meta: { error: errMsg(err) }
|
|
35334
|
+
});
|
|
35335
|
+
return false;
|
|
35336
|
+
}
|
|
35337
|
+
}
|
|
35338
|
+
/**
|
|
35263
35339
|
* Handle a profile-slot change: build a fresh detection config from
|
|
35264
35340
|
* per-device settings and start detection if motion is enabled.
|
|
35265
35341
|
* Fires on `camera-streams.onProfileSlotsChanged` — any publish,
|
|
@@ -35269,6 +35345,11 @@ var DetectionWiringController = class {
|
|
|
35269
35345
|
if (!this.deps.ctx()) return;
|
|
35270
35346
|
const log = this.deps.logger.withTags({ deviceId });
|
|
35271
35347
|
log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
|
|
35348
|
+
if (await this.isDeviceDeleted(deviceId)) {
|
|
35349
|
+
log.warn("device no longer exists — refusing to start detection");
|
|
35350
|
+
await this.handleDeviceUnregistered(deviceId);
|
|
35351
|
+
return;
|
|
35352
|
+
}
|
|
35272
35353
|
const config = await this.buildDetectionConfig(deviceId);
|
|
35273
35354
|
if (config === TRANSIENT_SLOT_READ) {
|
|
35274
35355
|
log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
|
|
@@ -35408,28 +35489,17 @@ var DetectionWiringController = class {
|
|
|
35408
35489
|
});
|
|
35409
35490
|
}
|
|
35410
35491
|
/**
|
|
35411
|
-
* Derive the watchdog camera descriptor from a runner config
|
|
35412
|
-
*
|
|
35413
|
-
*
|
|
35414
|
-
*
|
|
35415
|
-
* the motion stream id.
|
|
35492
|
+
* Derive the watchdog camera descriptor from a runner config — thin delegate
|
|
35493
|
+
* onto the pure `buildWatchdogCamera` (`watchdog-camera.ts`), where the
|
|
35494
|
+
* arming rules live (a switched-off camera arms nothing; the audio stage
|
|
35495
|
+
* needs evidence of an audio track).
|
|
35416
35496
|
*
|
|
35417
35497
|
* Public — `PlacementService`'s `buildWatchdogCamera` closure (wired in
|
|
35418
|
-
* `orchestrator-bootstrap.ts`) calls straight through to this method
|
|
35498
|
+
* `orchestrator-bootstrap.ts`) calls straight through to this method; the
|
|
35499
|
+
* caller resolves `hasAudioSource` once per dispatch.
|
|
35419
35500
|
*/
|
|
35420
|
-
buildWatchdogCamera(config, deviceName) {
|
|
35421
|
-
|
|
35422
|
-
if (config.audioMode === "always-on") continuousStages.set("audio", config.motionStreamId);
|
|
35423
|
-
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35424
|
-
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35425
|
-
return {
|
|
35426
|
-
deviceId: config.deviceId,
|
|
35427
|
-
deviceName,
|
|
35428
|
-
audioMode: config.audioMode,
|
|
35429
|
-
detectionMode: config.detectionMode,
|
|
35430
|
-
motionSources: config.motionSources,
|
|
35431
|
-
continuousStages
|
|
35432
|
-
};
|
|
35501
|
+
buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35502
|
+
return buildWatchdogCamera(config, deviceName, hasAudioSource);
|
|
35433
35503
|
}
|
|
35434
35504
|
/**
|
|
35435
35505
|
* Watchdog recovery callback. Invoked when a camera's continuous stage
|
|
@@ -35442,7 +35512,7 @@ var DetectionWiringController = class {
|
|
|
35442
35512
|
* Public — `PipelineWatchdog`'s `recover` closure (wired in
|
|
35443
35513
|
* `orchestrator-bootstrap.ts`) calls straight through to this method.
|
|
35444
35514
|
*/
|
|
35445
|
-
recoverPipelineStage(deviceId, stage, streamId) {
|
|
35515
|
+
recoverPipelineStage(deviceId, stage, streamId, attempt) {
|
|
35446
35516
|
if (stage === "audio") {
|
|
35447
35517
|
const cfg = this.deps.ledger.getConfig(deviceId);
|
|
35448
35518
|
const audioCfg = cfg ? {
|
|
@@ -35456,6 +35526,14 @@ var DetectionWiringController = class {
|
|
|
35456
35526
|
});
|
|
35457
35527
|
});
|
|
35458
35528
|
}
|
|
35529
|
+
this.deps.logger.debug("watchdog recovery: requesting source refresh", {
|
|
35530
|
+
tags: { deviceId },
|
|
35531
|
+
meta: {
|
|
35532
|
+
stage,
|
|
35533
|
+
camStreamId: streamId,
|
|
35534
|
+
attempt
|
|
35535
|
+
}
|
|
35536
|
+
});
|
|
35459
35537
|
this.deps.eventBus.emit({
|
|
35460
35538
|
id: `watchdog-refresh-${deviceId}-${Date.now()}`,
|
|
35461
35539
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -35858,6 +35936,64 @@ function computeDispatchGap(desired, known) {
|
|
|
35858
35936
|
return [...desired].filter((id) => !known.has(id));
|
|
35859
35937
|
}
|
|
35860
35938
|
/**
|
|
35939
|
+
* Split the desired fleet (derived from broker profile slots) against the LIVE
|
|
35940
|
+
* device registry.
|
|
35941
|
+
*
|
|
35942
|
+
* The broker keeps a deleted device's profile-slot assignments forever, so
|
|
35943
|
+
* `desiredDeviceIdsFromSlots` alone re-proposes a camera the operator already
|
|
35944
|
+
* deleted — and the reconcile's gap-fill then resurrects it. Callers dispatch
|
|
35945
|
+
* `existing` only.
|
|
35946
|
+
*
|
|
35947
|
+
* The caller MUST NOT call this with a failed/partial registry read: a read
|
|
35948
|
+
* that fails changes nothing (D49), it never prunes the fleet.
|
|
35949
|
+
*/
|
|
35950
|
+
function splitDesiredByExistence(desired, liveDeviceIds) {
|
|
35951
|
+
const existing = /* @__PURE__ */ new Set();
|
|
35952
|
+
const vanished = /* @__PURE__ */ new Set();
|
|
35953
|
+
for (const deviceId of desired) if (liveDeviceIds.has(deviceId)) existing.add(deviceId);
|
|
35954
|
+
else vanished.add(deviceId);
|
|
35955
|
+
return {
|
|
35956
|
+
existing,
|
|
35957
|
+
vanished
|
|
35958
|
+
};
|
|
35959
|
+
}
|
|
35960
|
+
/**
|
|
35961
|
+
* Decide which vanished devices are CONFIRMED gone (D49's two-disagreements
|
|
35962
|
+
* rule). Teardown destroys work — stopping detection, releasing the camera and
|
|
35963
|
+
* purging its persisted config — so a single registry read is never enough: a
|
|
35964
|
+
* device must be missing in TWO consecutive reconcile passes before it is torn
|
|
35965
|
+
* down. Anything that puts the device back (a successful listing that contains
|
|
35966
|
+
* it, or a failed read the caller declines to act on) resets the streak, so the
|
|
35967
|
+
* count only ever advances on agreeing evidence.
|
|
35968
|
+
*
|
|
35969
|
+
* Teardown fires ONCE per vanish streak (`torn-down`) — the broker's slots
|
|
35970
|
+
* outlive the device, so it stays vanished on every later pass and would
|
|
35971
|
+
* otherwise be re-purged forever.
|
|
35972
|
+
*
|
|
35973
|
+
* Pure — the caller owns the (in-memory, boot-empty) state map.
|
|
35974
|
+
*/
|
|
35975
|
+
function reconcileVanishedDevices(vanished, prior) {
|
|
35976
|
+
const tearDown = [];
|
|
35977
|
+
const nextState = /* @__PURE__ */ new Map();
|
|
35978
|
+
for (const deviceId of vanished) {
|
|
35979
|
+
const state = prior.get(deviceId);
|
|
35980
|
+
if (state === void 0) {
|
|
35981
|
+
nextState.set(deviceId, "first-pass");
|
|
35982
|
+
continue;
|
|
35983
|
+
}
|
|
35984
|
+
if (state === "first-pass") {
|
|
35985
|
+
tearDown.push(deviceId);
|
|
35986
|
+
nextState.set(deviceId, "torn-down");
|
|
35987
|
+
continue;
|
|
35988
|
+
}
|
|
35989
|
+
nextState.set(deviceId, "torn-down");
|
|
35990
|
+
}
|
|
35991
|
+
return {
|
|
35992
|
+
tearDown,
|
|
35993
|
+
nextState
|
|
35994
|
+
};
|
|
35995
|
+
}
|
|
35996
|
+
/**
|
|
35861
35997
|
* Converge the orchestrator's `assignments` map to runner truth. Runner
|
|
35862
35998
|
* attachments (`getLocalCameras`) are AUTHORITATIVE; this returns the actions
|
|
35863
35999
|
* that make the map agree with them:
|
|
@@ -36105,7 +36241,10 @@ var PlacementService = class {
|
|
|
36105
36241
|
try {
|
|
36106
36242
|
await this.attachOn(targetNodeId, attachConfig);
|
|
36107
36243
|
this.deps.ledger.setConfig(runnerConfig.deviceId, attachConfig);
|
|
36108
|
-
if (targetNodeId === this.deps.localNodeId())
|
|
36244
|
+
if (targetNodeId === this.deps.localNodeId()) {
|
|
36245
|
+
const hasAudioSource = await this.resolveHasAudioSource(runnerConfig);
|
|
36246
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId), hasAudioSource));
|
|
36247
|
+
}
|
|
36109
36248
|
this.deps.ledger.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, reason === "manual");
|
|
36110
36249
|
} finally {
|
|
36111
36250
|
dispatchGuard.dispose();
|
|
@@ -36531,7 +36670,20 @@ var PlacementService = class {
|
|
|
36531
36670
|
return;
|
|
36532
36671
|
}
|
|
36533
36672
|
await this.attachOn(nodeId, cached);
|
|
36534
|
-
if (nodeId === this.deps.localNodeId())
|
|
36673
|
+
if (nodeId === this.deps.localNodeId()) {
|
|
36674
|
+
const hasAudioSource = await this.resolveHasAudioSource(cached);
|
|
36675
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId), hasAudioSource));
|
|
36676
|
+
}
|
|
36677
|
+
}
|
|
36678
|
+
/**
|
|
36679
|
+
* Resolve the watchdog's audio-track evidence for ONE dispatch. Skipped
|
|
36680
|
+
* entirely (and answered `false`) when `audioMode` could not arm the stage
|
|
36681
|
+
* anyway — `buildWatchdogCamera` needs both — so a camera with on-motion
|
|
36682
|
+
* audio costs no broker round-trip.
|
|
36683
|
+
*/
|
|
36684
|
+
async resolveHasAudioSource(config) {
|
|
36685
|
+
if (config.audioMode !== "always-on") return false;
|
|
36686
|
+
return this.deps.hasAudioSource(config.deviceId, config.motionStreamId);
|
|
36535
36687
|
}
|
|
36536
36688
|
/**
|
|
36537
36689
|
* Normalizes the `decision.kind === 'pending'` → `setPendingReason` +
|
|
@@ -37903,7 +38055,9 @@ var PipelineWatchdog = class {
|
|
|
37903
38055
|
for (const [stage] of cam.continuousStages) stages.set(stage, {
|
|
37904
38056
|
lastSeenMs: now,
|
|
37905
38057
|
attempts: 0,
|
|
37906
|
-
lastAttemptMs: 0
|
|
38058
|
+
lastAttemptMs: 0,
|
|
38059
|
+
everSeen: false,
|
|
38060
|
+
registeredAtMs: now
|
|
37907
38061
|
});
|
|
37908
38062
|
this.state.set(cam.deviceId, stages);
|
|
37909
38063
|
}
|
|
@@ -37917,20 +38071,34 @@ var PipelineWatchdog = class {
|
|
|
37917
38071
|
if (!rt) return;
|
|
37918
38072
|
rt.lastSeenMs = this.deps.now();
|
|
37919
38073
|
rt.attempts = 0;
|
|
38074
|
+
rt.everSeen = true;
|
|
37920
38075
|
}
|
|
37921
38076
|
tick() {
|
|
37922
38077
|
const now = this.deps.now();
|
|
37923
38078
|
for (const cam of this.cameras.values()) {
|
|
37924
|
-
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
38079
|
+
const { line, stalled, recoveries, disarms } = this.evaluate(cam, now);
|
|
37925
38080
|
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
37926
38081
|
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
38082
|
+
for (const d of disarms) {
|
|
38083
|
+
this.deps.logger.warn("watchdog stage never produced a signal — disarming", {
|
|
38084
|
+
tags: { deviceId: cam.deviceId },
|
|
38085
|
+
meta: {
|
|
38086
|
+
stage: d.stage,
|
|
38087
|
+
streamId: d.streamId,
|
|
38088
|
+
sinceRegisteredMs: d.sinceRegisteredMs
|
|
38089
|
+
}
|
|
38090
|
+
});
|
|
38091
|
+
this.state.get(cam.deviceId)?.delete(d.stage);
|
|
38092
|
+
}
|
|
37927
38093
|
for (const r of recoveries) {
|
|
37928
38094
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
38095
|
+
let attempt = this.deps.thresholds.maxRecoveryAttempts;
|
|
37929
38096
|
if (rt) {
|
|
37930
38097
|
if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
|
|
37931
38098
|
rt.lastAttemptMs = now;
|
|
38099
|
+
attempt = rt.attempts;
|
|
37932
38100
|
}
|
|
37933
|
-
this.deps.recover(cam.deviceId, r.stage, r.streamId);
|
|
38101
|
+
this.deps.recover(cam.deviceId, r.stage, r.streamId, attempt);
|
|
37934
38102
|
}
|
|
37935
38103
|
}
|
|
37936
38104
|
}
|
|
@@ -37945,6 +38113,7 @@ var PipelineWatchdog = class {
|
|
|
37945
38113
|
evaluate(cam, now) {
|
|
37946
38114
|
const parts = [];
|
|
37947
38115
|
const recoveries = [];
|
|
38116
|
+
const disarms = [];
|
|
37948
38117
|
let stalled = false;
|
|
37949
38118
|
const stageOrder = [
|
|
37950
38119
|
"audio",
|
|
@@ -37971,7 +38140,14 @@ var PipelineWatchdog = class {
|
|
|
37971
38140
|
const thresholdMs = this.deps.thresholds[STAGE_THRESHOLD_KEY[stage]];
|
|
37972
38141
|
const sSec = Math.round(staleness / 1e3);
|
|
37973
38142
|
if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
|
|
37974
|
-
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
38143
|
+
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts && !rt.everSeen) {
|
|
38144
|
+
disarms.push({
|
|
38145
|
+
stage,
|
|
38146
|
+
streamId,
|
|
38147
|
+
sinceRegisteredMs: now - rt.registeredAtMs
|
|
38148
|
+
});
|
|
38149
|
+
parts.push(`${stage}=${stageMode[stage]}@${streamId}(NEVER SIGNALLED ${sSec}s · disarmed)`);
|
|
38150
|
+
} else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
37975
38151
|
stalled = true;
|
|
37976
38152
|
if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
|
|
37977
38153
|
recoveries.push({
|
|
@@ -37992,7 +38168,8 @@ var PipelineWatchdog = class {
|
|
|
37992
38168
|
return {
|
|
37993
38169
|
line: `[watchdog] cam${cam.deviceId} ${cam.deviceName} · ${parts.join(" · ")} → ${stalled ? "STALLED" : "OK"}`,
|
|
37994
38170
|
stalled,
|
|
37995
|
-
recoveries
|
|
38171
|
+
recoveries,
|
|
38172
|
+
disarms
|
|
37996
38173
|
};
|
|
37997
38174
|
}
|
|
37998
38175
|
};
|
|
@@ -38889,6 +39066,14 @@ var ReconcileController = class ReconcileController {
|
|
|
38889
39066
|
/** Shared in-flight pass so a session dispatch waits for runner truth instead of racing it. */
|
|
38890
39067
|
attachmentReconcilePromise = null;
|
|
38891
39068
|
/**
|
|
39069
|
+
* Desired-but-nonexistent devices and how far their vanish streak has got
|
|
39070
|
+
* (`dispatch-reconcile.ts`). PURELY in-memory and seeded EMPTY on boot: it is
|
|
39071
|
+
* a confirmation counter, not a record — after a restart the first pass only
|
|
39072
|
+
* ever re-observes, never destroys. Cleared whenever the registry read fails,
|
|
39073
|
+
* so a failed read can never advance a device toward teardown.
|
|
39074
|
+
*/
|
|
39075
|
+
vanishedDevices = /* @__PURE__ */ new Map();
|
|
39076
|
+
/**
|
|
38892
39077
|
* Per-device backoff for retrying `handleDeviceRegistered` after a
|
|
38893
39078
|
* TRANSIENT profile-slot read failure. Timers are `unref`'d and cleared on
|
|
38894
39079
|
* shutdown / device removal.
|
|
@@ -38957,13 +39142,24 @@ var ReconcileController = class ReconcileController {
|
|
|
38957
39142
|
return;
|
|
38958
39143
|
}
|
|
38959
39144
|
const desired = desiredDeviceIdsFromSlots(slots);
|
|
39145
|
+
const liveDeviceIds = await this.readLiveDeviceIds();
|
|
39146
|
+
const { existing, vanished } = liveDeviceIds === null ? {
|
|
39147
|
+
existing: desired,
|
|
39148
|
+
vanished: /* @__PURE__ */ new Set()
|
|
39149
|
+
} : splitDesiredByExistence(desired, liveDeviceIds);
|
|
39150
|
+
if (liveDeviceIds === null) {
|
|
39151
|
+
this.deps.logger.warn("dispatch reconcile: device existence filter SKIPPED — registry read failed", { meta: { desired: desired.size } });
|
|
39152
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39153
|
+
}
|
|
38960
39154
|
const known = new Set(this.deps.ledger.listConfigIds());
|
|
38961
|
-
const gap = computeDispatchGap(
|
|
39155
|
+
const gap = computeDispatchGap(existing, known);
|
|
38962
39156
|
this.deps.logger.info("dispatch reconcile", { meta: {
|
|
38963
39157
|
desired: desired.size,
|
|
38964
39158
|
known: known.size,
|
|
38965
|
-
gap
|
|
39159
|
+
gap,
|
|
39160
|
+
vanished: vanished.size
|
|
38966
39161
|
} });
|
|
39162
|
+
await this.tearDownVanishedDevices(vanished);
|
|
38967
39163
|
for (const deviceId of gap) try {
|
|
38968
39164
|
await this.deps.handleDeviceRegistered(deviceId);
|
|
38969
39165
|
} catch (err) {
|
|
@@ -38982,6 +39178,53 @@ var ReconcileController = class ReconcileController {
|
|
|
38982
39178
|
}
|
|
38983
39179
|
}
|
|
38984
39180
|
/**
|
|
39181
|
+
* The live device registry as an id set, or `null` when the read FAILED.
|
|
39182
|
+
* `null` is the caller's signal to skip the existence filter entirely — it
|
|
39183
|
+
* must never be confused with "the registry is empty", which would prune the
|
|
39184
|
+
* whole fleet.
|
|
39185
|
+
*
|
|
39186
|
+
* Asks for the slim projection: this needs identity only, never
|
|
39187
|
+
* `config`/`sourceInfo` (a full listing of 293 devices measured 271 KB).
|
|
39188
|
+
*/
|
|
39189
|
+
async readLiveDeviceIds() {
|
|
39190
|
+
const api = this.deps.api();
|
|
39191
|
+
if (!api) return null;
|
|
39192
|
+
try {
|
|
39193
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
39194
|
+
return new Set(devices.map((device) => device.id));
|
|
39195
|
+
} catch (err) {
|
|
39196
|
+
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg(err) } });
|
|
39197
|
+
return null;
|
|
39198
|
+
}
|
|
39199
|
+
}
|
|
39200
|
+
/**
|
|
39201
|
+
* Tear down the desired devices the registry no longer has — but only once
|
|
39202
|
+
* TWO consecutive passes agree they are gone (`reconcileVanishedDevices`,
|
|
39203
|
+
* D49). Teardown destroys work, so a single listing that omits a device
|
|
39204
|
+
* (which a partial or racy read looks exactly like) is not enough.
|
|
39205
|
+
*
|
|
39206
|
+
* Runs the SAME `handleDeviceUnregistered` the `DeviceUnregistered` event
|
|
39207
|
+
* runs, so there is exactly one teardown path.
|
|
39208
|
+
*/
|
|
39209
|
+
async tearDownVanishedDevices(vanished) {
|
|
39210
|
+
const { tearDown, nextState } = reconcileVanishedDevices(vanished, this.vanishedDevices);
|
|
39211
|
+
this.vanishedDevices = nextState;
|
|
39212
|
+
for (const deviceId of tearDown) {
|
|
39213
|
+
this.deps.logger.warn("dispatch reconcile: device no longer exists — tearing down instead of dispatching", {
|
|
39214
|
+
tags: { deviceId },
|
|
39215
|
+
meta: { confirmedPasses: 2 }
|
|
39216
|
+
});
|
|
39217
|
+
try {
|
|
39218
|
+
await this.deps.handleDeviceUnregistered(deviceId);
|
|
39219
|
+
} catch (err) {
|
|
39220
|
+
this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
|
|
39221
|
+
tags: { deviceId },
|
|
39222
|
+
meta: { error: errMsg(err) }
|
|
39223
|
+
});
|
|
39224
|
+
}
|
|
39225
|
+
}
|
|
39226
|
+
}
|
|
39227
|
+
/**
|
|
38985
39228
|
* Converge the in-memory `assignments` map to runner truth (Phase 1 of the
|
|
38986
39229
|
* placement-derived-state design). Runner attachments (`getLocalCameras`) are
|
|
38987
39230
|
* authoritative: detach duplicates/orphans, and drop stale assignments
|
|
@@ -39292,6 +39535,7 @@ var ReconcileController = class ReconcileController {
|
|
|
39292
39535
|
this.slotReadRetryTimers.clear();
|
|
39293
39536
|
this.slotReadRetryAttempts.clear();
|
|
39294
39537
|
this.remoteHealthAttempts.clear();
|
|
39538
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39295
39539
|
}
|
|
39296
39540
|
};
|
|
39297
39541
|
//#endregion
|
|
@@ -40357,6 +40601,31 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40357
40601
|
getInitTimestamp: () => deps.initTimestamp
|
|
40358
40602
|
});
|
|
40359
40603
|
const inferenceRotation = new RoundRobinInferenceDeviceRotation();
|
|
40604
|
+
/**
|
|
40605
|
+
* Does this camera's source carry an audio track? Read from the broker that
|
|
40606
|
+
* owns the stream (`BrokerStats.audio === null` is the documented no-audio
|
|
40607
|
+
* signal), pinned to the cluster's source owner exactly like the
|
|
40608
|
+
* camera-status broker stage. FAILS OPEN: no api, an RPC error, or a broker
|
|
40609
|
+
* that has not detected the track yet (`undefined`, cold start) all answer
|
|
40610
|
+
* `true`, because the only consumer is the watchdog's diagnostic audio stage
|
|
40611
|
+
* and a false negative silently stops watching a working camera.
|
|
40612
|
+
*/
|
|
40613
|
+
const resolveHasAudioSource = async (deviceId, camStreamId) => {
|
|
40614
|
+
const api = deps.ctx().api;
|
|
40615
|
+
if (!api) return true;
|
|
40616
|
+
try {
|
|
40617
|
+
return (await api.streamBroker.getBrokerStats.query({ brokerId: makeSourceBrokerId(deviceId, camStreamId) }, nodePin(topology.assignSource(deviceId)))).audio !== null;
|
|
40618
|
+
} catch (err) {
|
|
40619
|
+
deps.ctx().logger.debug("audio-source probe failed — assuming the source has audio", {
|
|
40620
|
+
tags: { deviceId },
|
|
40621
|
+
meta: {
|
|
40622
|
+
camStreamId,
|
|
40623
|
+
error: errMsg(err)
|
|
40624
|
+
}
|
|
40625
|
+
});
|
|
40626
|
+
return true;
|
|
40627
|
+
}
|
|
40628
|
+
};
|
|
40360
40629
|
const placement = new PlacementService({
|
|
40361
40630
|
ctx: () => deps.ctx(),
|
|
40362
40631
|
ctxIfReady: () => deps.ctxIfReady(),
|
|
@@ -40372,7 +40641,8 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40372
40641
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
40373
40642
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
40374
40643
|
inferenceRotation,
|
|
40375
|
-
buildWatchdogCamera: (config, deviceName) => detectionWiring.buildWatchdogCamera(config, deviceName),
|
|
40644
|
+
buildWatchdogCamera: (config, deviceName, hasAudioSource) => detectionWiring.buildWatchdogCamera(config, deviceName, hasAudioSource),
|
|
40645
|
+
hasAudioSource: (deviceId, camStreamId) => resolveHasAudioSource(deviceId, camStreamId),
|
|
40376
40646
|
isIdleSessionCamera: (deviceId) => deps.isIdleSessionCamera(deviceId),
|
|
40377
40647
|
activeSessionNode: (deviceId) => session.activeSessionNode(deviceId),
|
|
40378
40648
|
closeActiveSession: (deviceId) => session.closeSessionForReassign(deviceId),
|
|
@@ -40447,6 +40717,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40447
40717
|
loadShed,
|
|
40448
40718
|
logger: deps.ctx().logger,
|
|
40449
40719
|
handleDeviceRegistered: (deviceId) => deps.handleDeviceRegistered(deviceId),
|
|
40720
|
+
handleDeviceUnregistered: (deviceId) => deps.handleDeviceUnregistered(deviceId),
|
|
40450
40721
|
dispatchCamera: (config) => deps.dispatchCamera(config),
|
|
40451
40722
|
detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
|
|
40452
40723
|
localNodeId: () => localNodeId,
|
|
@@ -40547,7 +40818,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40547
40818
|
const pipelineWatchdog = new PipelineWatchdog({
|
|
40548
40819
|
now: () => Date.now(),
|
|
40549
40820
|
logger: deps.ctx().logger,
|
|
40550
|
-
recover: (deviceId, stage, streamId) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId),
|
|
40821
|
+
recover: (deviceId, stage, streamId, attempt) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId, attempt),
|
|
40551
40822
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
40552
40823
|
});
|
|
40553
40824
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|
package/dist/remoteEntry.js
CHANGED
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DVSFAO6F.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline-orchestrator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.47",
|
|
4
4
|
"description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|