@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
|
@@ -1 +1 @@
|
|
|
1
|
-
__d(function(g,r,i,a,m,e,d){function n(){throw new Error("node:fs is unavailable in the Viewer runtime")}m.exports={chmodSync:n,createWriteStream:n,existsSync:n,mkdirSync:n,promises:new Proxy({},{get:()=>n}),readFileSync:n,readdirSync:n,renameSync:n,rmSync:n,unlinkSync:n,writeFileSync:n}},
|
|
1
|
+
__d(function(g,r,i,a,m,e,d){function n(){throw new Error("node:fs is unavailable in the Viewer runtime")}m.exports={chmodSync:n,createWriteStream:n,existsSync:n,mkdirSync:n,promises:new Proxy({},{get:()=>n}),readFileSync:n,readdirSync:n,renameSync:n,rmSync:n,unlinkSync:n,writeFileSync:n}},2164,[]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
__d(function(g,r,i,a,m,e,d){function n(){throw new Error("node:path is unavailable in the Viewer runtime")}m.exports={basename:n,join:n}},
|
|
1
|
+
__d(function(g,r,i,a,m,e,d){function n(){throw new Error("node:path is unavailable in the Viewer runtime")}m.exports={basename:n,join:n}},2163,[]);
|
package/assets/viewer/index.html
CHANGED
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
</noscript>
|
|
37
37
|
<!-- The root element for your Expo app. -->
|
|
38
38
|
<div id="root"></div>
|
|
39
|
-
<script src="/viewer/camstack/_expo/static/js/web/index-
|
|
39
|
+
<script src="/viewer/camstack/_expo/static/js/web/index-a8dfe1019b6f61fc84bf4dba973b9571.js" defer></script>
|
|
40
40
|
</body>
|
|
41
41
|
</html>
|
package/dist/index.js
CHANGED
|
@@ -34988,6 +34988,48 @@ var DeviceFeaturesMirror = class {
|
|
|
34988
34988
|
}
|
|
34989
34989
|
};
|
|
34990
34990
|
//#endregion
|
|
34991
|
+
//#region src/watchdog-camera.ts
|
|
34992
|
+
/**
|
|
34993
|
+
* Derive the watchdog camera descriptor from a runner config. Captures which
|
|
34994
|
+
* stages are expected to flow CONTINUOUSLY (always-on audio, analyzer motion,
|
|
34995
|
+
* always-on detection) → the camStreamId feeding each. Audio has no dedicated
|
|
34996
|
+
* stream field on the runner config, so it shares the motion stream id.
|
|
34997
|
+
*
|
|
34998
|
+
* `pipelineEnabled === false` (the `object-detection` switch, D62/ADR-0109)
|
|
34999
|
+
* arms NOTHING: `startDetection` deliberately performs no attach and opens no
|
|
35000
|
+
* decode session for such a camera, so no continuous stage can ever produce a
|
|
35001
|
+
* signal. The camera is still returned (and still registered by the caller) so
|
|
35002
|
+
* its watchdog row stays visible — a switched-off camera must read as off, not
|
|
35003
|
+
* as broken.
|
|
35004
|
+
*
|
|
35005
|
+
* `hasAudioSource` is the caller's evidence that the camera's source actually
|
|
35006
|
+
* carries an audio track (`BrokerStats.audio !== null`). It fails OPEN: an
|
|
35007
|
+
* unknown or failed read passes `true`, because the audio stage is diagnostic
|
|
35008
|
+
* and a false negative silently stops watching a working camera.
|
|
35009
|
+
*/
|
|
35010
|
+
function buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35011
|
+
const continuousStages = /* @__PURE__ */ new Map();
|
|
35012
|
+
if (config.pipelineEnabled === false) return {
|
|
35013
|
+
deviceId: config.deviceId,
|
|
35014
|
+
deviceName,
|
|
35015
|
+
audioMode: config.audioMode,
|
|
35016
|
+
detectionMode: config.detectionMode,
|
|
35017
|
+
motionSources: config.motionSources,
|
|
35018
|
+
continuousStages
|
|
35019
|
+
};
|
|
35020
|
+
if (config.audioMode === "always-on" && hasAudioSource) continuousStages.set("audio", config.motionStreamId);
|
|
35021
|
+
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35022
|
+
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35023
|
+
return {
|
|
35024
|
+
deviceId: config.deviceId,
|
|
35025
|
+
deviceName,
|
|
35026
|
+
audioMode: config.audioMode,
|
|
35027
|
+
detectionMode: config.detectionMode,
|
|
35028
|
+
motionSources: config.motionSources,
|
|
35029
|
+
continuousStages
|
|
35030
|
+
};
|
|
35031
|
+
}
|
|
35032
|
+
//#endregion
|
|
34991
35033
|
//#region src/detection-wiring-controller.ts
|
|
34992
35034
|
var DetectionWiringController = class {
|
|
34993
35035
|
deps;
|
|
@@ -35192,7 +35234,8 @@ var DetectionWiringController = class {
|
|
|
35192
35234
|
await this.stopDetection(deviceId);
|
|
35193
35235
|
}
|
|
35194
35236
|
this.activeDetections.set(deviceId, config);
|
|
35195
|
-
if (config.
|
|
35237
|
+
if (!config.pipelineEnabled) log.info("object detection switched off — no attach, no decode session", { meta: { detectionMode: config.detectionMode } });
|
|
35238
|
+
else if (config.detectionMode === "on-motion") {
|
|
35196
35239
|
if (config.motionSources.includes("analyzer")) try {
|
|
35197
35240
|
await this.deps.session.ensureMotionWatch(deviceId, config);
|
|
35198
35241
|
} catch (err) {
|
|
@@ -35283,11 +35326,44 @@ var DetectionWiringController = class {
|
|
|
35283
35326
|
this.deps.forgetDeviceZones(deviceId);
|
|
35284
35327
|
this.deps.forgetDeviceZoneRules(deviceId);
|
|
35285
35328
|
this.deps.loadShed.forgetDevice(deviceId);
|
|
35286
|
-
|
|
35329
|
+
const settings = this.deps.ctx().settings;
|
|
35330
|
+
if (!settings) return;
|
|
35287
35331
|
await this.deps.settingsStore.deleteCameraSettings(deviceId);
|
|
35332
|
+
try {
|
|
35333
|
+
await settings.clearDeviceStore(deviceId);
|
|
35334
|
+
} catch (err) {
|
|
35335
|
+
this.deps.logger.warn("failed to clear per-device orchestrator settings on removal", {
|
|
35336
|
+
tags: { deviceId },
|
|
35337
|
+
meta: { error: errMsg(err) }
|
|
35338
|
+
});
|
|
35339
|
+
}
|
|
35288
35340
|
this.deps.logger.info("Purged pipeline config for removed device", { tags: { deviceId } });
|
|
35289
35341
|
}
|
|
35290
35342
|
/**
|
|
35343
|
+
* True ONLY when the device registry positively answered "this device does
|
|
35344
|
+
* not exist" (`getDevice` → `null`, per the cap contract).
|
|
35345
|
+
*
|
|
35346
|
+
* A FAILED read answers `false` — D49: a read that fails changes nothing, and
|
|
35347
|
+
* the destructive direction here is refusing to run a live camera.
|
|
35348
|
+
* Deliberately NOT routed through {@link lookupDeviceFeatures}'s mirror: that
|
|
35349
|
+
* one treats missing-as-unknown on purpose (a provider restart passes through
|
|
35350
|
+
* exactly that state and must not re-derive a battery camera's profile), and
|
|
35351
|
+
* conflating the two would make a provider restart look like a deletion.
|
|
35352
|
+
*/
|
|
35353
|
+
async isDeviceDeleted(deviceId) {
|
|
35354
|
+
const api = this.deps.api();
|
|
35355
|
+
if (!api) return false;
|
|
35356
|
+
try {
|
|
35357
|
+
return await api.deviceManager.getDevice.query({ deviceId }) === null;
|
|
35358
|
+
} catch (err) {
|
|
35359
|
+
this.deps.logger.debug("device existence check failed — proceeding as before", {
|
|
35360
|
+
tags: { deviceId },
|
|
35361
|
+
meta: { error: errMsg(err) }
|
|
35362
|
+
});
|
|
35363
|
+
return false;
|
|
35364
|
+
}
|
|
35365
|
+
}
|
|
35366
|
+
/**
|
|
35291
35367
|
* Handle a profile-slot change: build a fresh detection config from
|
|
35292
35368
|
* per-device settings and start detection if motion is enabled.
|
|
35293
35369
|
* Fires on `camera-streams.onProfileSlotsChanged` — any publish,
|
|
@@ -35297,6 +35373,11 @@ var DetectionWiringController = class {
|
|
|
35297
35373
|
if (!this.deps.ctx()) return;
|
|
35298
35374
|
const log = this.deps.logger.withTags({ deviceId });
|
|
35299
35375
|
log.info("handleDeviceRegistered", { meta: { phase: "handleDeviceRegistered" } });
|
|
35376
|
+
if (await this.isDeviceDeleted(deviceId)) {
|
|
35377
|
+
log.warn("device no longer exists — refusing to start detection");
|
|
35378
|
+
await this.handleDeviceUnregistered(deviceId);
|
|
35379
|
+
return;
|
|
35380
|
+
}
|
|
35300
35381
|
const config = await this.buildDetectionConfig(deviceId);
|
|
35301
35382
|
if (config === TRANSIENT_SLOT_READ) {
|
|
35302
35383
|
log.warn("Profile-slot read failed transiently — keeping detection, scheduling retry");
|
|
@@ -35436,28 +35517,17 @@ var DetectionWiringController = class {
|
|
|
35436
35517
|
});
|
|
35437
35518
|
}
|
|
35438
35519
|
/**
|
|
35439
|
-
* Derive the watchdog camera descriptor from a runner config
|
|
35440
|
-
*
|
|
35441
|
-
*
|
|
35442
|
-
*
|
|
35443
|
-
* the motion stream id.
|
|
35520
|
+
* Derive the watchdog camera descriptor from a runner config — thin delegate
|
|
35521
|
+
* onto the pure `buildWatchdogCamera` (`watchdog-camera.ts`), where the
|
|
35522
|
+
* arming rules live (a switched-off camera arms nothing; the audio stage
|
|
35523
|
+
* needs evidence of an audio track).
|
|
35444
35524
|
*
|
|
35445
35525
|
* Public — `PlacementService`'s `buildWatchdogCamera` closure (wired in
|
|
35446
|
-
* `orchestrator-bootstrap.ts`) calls straight through to this method
|
|
35526
|
+
* `orchestrator-bootstrap.ts`) calls straight through to this method; the
|
|
35527
|
+
* caller resolves `hasAudioSource` once per dispatch.
|
|
35447
35528
|
*/
|
|
35448
|
-
buildWatchdogCamera(config, deviceName) {
|
|
35449
|
-
|
|
35450
|
-
if (config.audioMode === "always-on") continuousStages.set("audio", config.motionStreamId);
|
|
35451
|
-
if (config.motionSources.includes("analyzer")) continuousStages.set("motion", config.motionStreamId);
|
|
35452
|
-
if (config.detectionMode === "always-on") continuousStages.set("detection", config.detectionStreamId);
|
|
35453
|
-
return {
|
|
35454
|
-
deviceId: config.deviceId,
|
|
35455
|
-
deviceName,
|
|
35456
|
-
audioMode: config.audioMode,
|
|
35457
|
-
detectionMode: config.detectionMode,
|
|
35458
|
-
motionSources: config.motionSources,
|
|
35459
|
-
continuousStages
|
|
35460
|
-
};
|
|
35529
|
+
buildWatchdogCamera(config, deviceName, hasAudioSource) {
|
|
35530
|
+
return buildWatchdogCamera(config, deviceName, hasAudioSource);
|
|
35461
35531
|
}
|
|
35462
35532
|
/**
|
|
35463
35533
|
* Watchdog recovery callback. Invoked when a camera's continuous stage
|
|
@@ -35470,7 +35540,7 @@ var DetectionWiringController = class {
|
|
|
35470
35540
|
* Public — `PipelineWatchdog`'s `recover` closure (wired in
|
|
35471
35541
|
* `orchestrator-bootstrap.ts`) calls straight through to this method.
|
|
35472
35542
|
*/
|
|
35473
|
-
recoverPipelineStage(deviceId, stage, streamId) {
|
|
35543
|
+
recoverPipelineStage(deviceId, stage, streamId, attempt) {
|
|
35474
35544
|
if (stage === "audio") {
|
|
35475
35545
|
const cfg = this.deps.ledger.getConfig(deviceId);
|
|
35476
35546
|
const audioCfg = cfg ? {
|
|
@@ -35484,6 +35554,14 @@ var DetectionWiringController = class {
|
|
|
35484
35554
|
});
|
|
35485
35555
|
});
|
|
35486
35556
|
}
|
|
35557
|
+
this.deps.logger.debug("watchdog recovery: requesting source refresh", {
|
|
35558
|
+
tags: { deviceId },
|
|
35559
|
+
meta: {
|
|
35560
|
+
stage,
|
|
35561
|
+
camStreamId: streamId,
|
|
35562
|
+
attempt
|
|
35563
|
+
}
|
|
35564
|
+
});
|
|
35487
35565
|
this.deps.eventBus.emit({
|
|
35488
35566
|
id: `watchdog-refresh-${deviceId}-${Date.now()}`,
|
|
35489
35567
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -35886,6 +35964,64 @@ function computeDispatchGap(desired, known) {
|
|
|
35886
35964
|
return [...desired].filter((id) => !known.has(id));
|
|
35887
35965
|
}
|
|
35888
35966
|
/**
|
|
35967
|
+
* Split the desired fleet (derived from broker profile slots) against the LIVE
|
|
35968
|
+
* device registry.
|
|
35969
|
+
*
|
|
35970
|
+
* The broker keeps a deleted device's profile-slot assignments forever, so
|
|
35971
|
+
* `desiredDeviceIdsFromSlots` alone re-proposes a camera the operator already
|
|
35972
|
+
* deleted — and the reconcile's gap-fill then resurrects it. Callers dispatch
|
|
35973
|
+
* `existing` only.
|
|
35974
|
+
*
|
|
35975
|
+
* The caller MUST NOT call this with a failed/partial registry read: a read
|
|
35976
|
+
* that fails changes nothing (D49), it never prunes the fleet.
|
|
35977
|
+
*/
|
|
35978
|
+
function splitDesiredByExistence(desired, liveDeviceIds) {
|
|
35979
|
+
const existing = /* @__PURE__ */ new Set();
|
|
35980
|
+
const vanished = /* @__PURE__ */ new Set();
|
|
35981
|
+
for (const deviceId of desired) if (liveDeviceIds.has(deviceId)) existing.add(deviceId);
|
|
35982
|
+
else vanished.add(deviceId);
|
|
35983
|
+
return {
|
|
35984
|
+
existing,
|
|
35985
|
+
vanished
|
|
35986
|
+
};
|
|
35987
|
+
}
|
|
35988
|
+
/**
|
|
35989
|
+
* Decide which vanished devices are CONFIRMED gone (D49's two-disagreements
|
|
35990
|
+
* rule). Teardown destroys work — stopping detection, releasing the camera and
|
|
35991
|
+
* purging its persisted config — so a single registry read is never enough: a
|
|
35992
|
+
* device must be missing in TWO consecutive reconcile passes before it is torn
|
|
35993
|
+
* down. Anything that puts the device back (a successful listing that contains
|
|
35994
|
+
* it, or a failed read the caller declines to act on) resets the streak, so the
|
|
35995
|
+
* count only ever advances on agreeing evidence.
|
|
35996
|
+
*
|
|
35997
|
+
* Teardown fires ONCE per vanish streak (`torn-down`) — the broker's slots
|
|
35998
|
+
* outlive the device, so it stays vanished on every later pass and would
|
|
35999
|
+
* otherwise be re-purged forever.
|
|
36000
|
+
*
|
|
36001
|
+
* Pure — the caller owns the (in-memory, boot-empty) state map.
|
|
36002
|
+
*/
|
|
36003
|
+
function reconcileVanishedDevices(vanished, prior) {
|
|
36004
|
+
const tearDown = [];
|
|
36005
|
+
const nextState = /* @__PURE__ */ new Map();
|
|
36006
|
+
for (const deviceId of vanished) {
|
|
36007
|
+
const state = prior.get(deviceId);
|
|
36008
|
+
if (state === void 0) {
|
|
36009
|
+
nextState.set(deviceId, "first-pass");
|
|
36010
|
+
continue;
|
|
36011
|
+
}
|
|
36012
|
+
if (state === "first-pass") {
|
|
36013
|
+
tearDown.push(deviceId);
|
|
36014
|
+
nextState.set(deviceId, "torn-down");
|
|
36015
|
+
continue;
|
|
36016
|
+
}
|
|
36017
|
+
nextState.set(deviceId, "torn-down");
|
|
36018
|
+
}
|
|
36019
|
+
return {
|
|
36020
|
+
tearDown,
|
|
36021
|
+
nextState
|
|
36022
|
+
};
|
|
36023
|
+
}
|
|
36024
|
+
/**
|
|
35889
36025
|
* Converge the orchestrator's `assignments` map to runner truth. Runner
|
|
35890
36026
|
* attachments (`getLocalCameras`) are AUTHORITATIVE; this returns the actions
|
|
35891
36027
|
* that make the map agree with them:
|
|
@@ -36133,7 +36269,10 @@ var PlacementService = class {
|
|
|
36133
36269
|
try {
|
|
36134
36270
|
await this.attachOn(targetNodeId, attachConfig);
|
|
36135
36271
|
this.deps.ledger.setConfig(runnerConfig.deviceId, attachConfig);
|
|
36136
|
-
if (targetNodeId === this.deps.localNodeId())
|
|
36272
|
+
if (targetNodeId === this.deps.localNodeId()) {
|
|
36273
|
+
const hasAudioSource = await this.resolveHasAudioSource(runnerConfig);
|
|
36274
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(runnerConfig, String(runnerConfig.deviceId), hasAudioSource));
|
|
36275
|
+
}
|
|
36137
36276
|
this.deps.ledger.recordAssignment(runnerConfig.deviceId, targetNodeId, reason, reason === "manual");
|
|
36138
36277
|
} finally {
|
|
36139
36278
|
dispatchGuard.dispose();
|
|
@@ -36559,7 +36698,20 @@ var PlacementService = class {
|
|
|
36559
36698
|
return;
|
|
36560
36699
|
}
|
|
36561
36700
|
await this.attachOn(nodeId, cached);
|
|
36562
|
-
if (nodeId === this.deps.localNodeId())
|
|
36701
|
+
if (nodeId === this.deps.localNodeId()) {
|
|
36702
|
+
const hasAudioSource = await this.resolveHasAudioSource(cached);
|
|
36703
|
+
this.deps.watchdog()?.register(this.deps.buildWatchdogCamera(cached, String(cached.deviceId), hasAudioSource));
|
|
36704
|
+
}
|
|
36705
|
+
}
|
|
36706
|
+
/**
|
|
36707
|
+
* Resolve the watchdog's audio-track evidence for ONE dispatch. Skipped
|
|
36708
|
+
* entirely (and answered `false`) when `audioMode` could not arm the stage
|
|
36709
|
+
* anyway — `buildWatchdogCamera` needs both — so a camera with on-motion
|
|
36710
|
+
* audio costs no broker round-trip.
|
|
36711
|
+
*/
|
|
36712
|
+
async resolveHasAudioSource(config) {
|
|
36713
|
+
if (config.audioMode !== "always-on") return false;
|
|
36714
|
+
return this.deps.hasAudioSource(config.deviceId, config.motionStreamId);
|
|
36563
36715
|
}
|
|
36564
36716
|
/**
|
|
36565
36717
|
* Normalizes the `decision.kind === 'pending'` → `setPendingReason` +
|
|
@@ -37931,7 +38083,9 @@ var PipelineWatchdog = class {
|
|
|
37931
38083
|
for (const [stage] of cam.continuousStages) stages.set(stage, {
|
|
37932
38084
|
lastSeenMs: now,
|
|
37933
38085
|
attempts: 0,
|
|
37934
|
-
lastAttemptMs: 0
|
|
38086
|
+
lastAttemptMs: 0,
|
|
38087
|
+
everSeen: false,
|
|
38088
|
+
registeredAtMs: now
|
|
37935
38089
|
});
|
|
37936
38090
|
this.state.set(cam.deviceId, stages);
|
|
37937
38091
|
}
|
|
@@ -37945,20 +38099,34 @@ var PipelineWatchdog = class {
|
|
|
37945
38099
|
if (!rt) return;
|
|
37946
38100
|
rt.lastSeenMs = this.deps.now();
|
|
37947
38101
|
rt.attempts = 0;
|
|
38102
|
+
rt.everSeen = true;
|
|
37948
38103
|
}
|
|
37949
38104
|
tick() {
|
|
37950
38105
|
const now = this.deps.now();
|
|
37951
38106
|
for (const cam of this.cameras.values()) {
|
|
37952
|
-
const { line, stalled, recoveries } = this.evaluate(cam, now);
|
|
38107
|
+
const { line, stalled, recoveries, disarms } = this.evaluate(cam, now);
|
|
37953
38108
|
if (stalled) this.deps.logger.warn(line, { tags: { deviceId: cam.deviceId } });
|
|
37954
38109
|
else this.deps.logger.info(line, { tags: { deviceId: cam.deviceId } });
|
|
38110
|
+
for (const d of disarms) {
|
|
38111
|
+
this.deps.logger.warn("watchdog stage never produced a signal — disarming", {
|
|
38112
|
+
tags: { deviceId: cam.deviceId },
|
|
38113
|
+
meta: {
|
|
38114
|
+
stage: d.stage,
|
|
38115
|
+
streamId: d.streamId,
|
|
38116
|
+
sinceRegisteredMs: d.sinceRegisteredMs
|
|
38117
|
+
}
|
|
38118
|
+
});
|
|
38119
|
+
this.state.get(cam.deviceId)?.delete(d.stage);
|
|
38120
|
+
}
|
|
37955
38121
|
for (const r of recoveries) {
|
|
37956
38122
|
const rt = this.state.get(cam.deviceId)?.get(r.stage);
|
|
38123
|
+
let attempt = this.deps.thresholds.maxRecoveryAttempts;
|
|
37957
38124
|
if (rt) {
|
|
37958
38125
|
if (rt.attempts < this.deps.thresholds.maxRecoveryAttempts) rt.attempts += 1;
|
|
37959
38126
|
rt.lastAttemptMs = now;
|
|
38127
|
+
attempt = rt.attempts;
|
|
37960
38128
|
}
|
|
37961
|
-
this.deps.recover(cam.deviceId, r.stage, r.streamId);
|
|
38129
|
+
this.deps.recover(cam.deviceId, r.stage, r.streamId, attempt);
|
|
37962
38130
|
}
|
|
37963
38131
|
}
|
|
37964
38132
|
}
|
|
@@ -37973,6 +38141,7 @@ var PipelineWatchdog = class {
|
|
|
37973
38141
|
evaluate(cam, now) {
|
|
37974
38142
|
const parts = [];
|
|
37975
38143
|
const recoveries = [];
|
|
38144
|
+
const disarms = [];
|
|
37976
38145
|
let stalled = false;
|
|
37977
38146
|
const stageOrder = [
|
|
37978
38147
|
"audio",
|
|
@@ -37999,7 +38168,14 @@ var PipelineWatchdog = class {
|
|
|
37999
38168
|
const thresholdMs = this.deps.thresholds[STAGE_THRESHOLD_KEY[stage]];
|
|
38000
38169
|
const sSec = Math.round(staleness / 1e3);
|
|
38001
38170
|
if (staleness <= thresholdMs) parts.push(`${stage}=${stageMode[stage]}@${streamId}(OK ${sSec}s)`);
|
|
38002
|
-
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
38171
|
+
else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts && !rt.everSeen) {
|
|
38172
|
+
disarms.push({
|
|
38173
|
+
stage,
|
|
38174
|
+
streamId,
|
|
38175
|
+
sinceRegisteredMs: now - rt.registeredAtMs
|
|
38176
|
+
});
|
|
38177
|
+
parts.push(`${stage}=${stageMode[stage]}@${streamId}(NEVER SIGNALLED ${sSec}s · disarmed)`);
|
|
38178
|
+
} else if (rt.attempts >= this.deps.thresholds.maxRecoveryAttempts) {
|
|
38003
38179
|
stalled = true;
|
|
38004
38180
|
if (now - rt.lastAttemptMs >= this.deps.thresholds.exhaustedRetryMs) {
|
|
38005
38181
|
recoveries.push({
|
|
@@ -38020,7 +38196,8 @@ var PipelineWatchdog = class {
|
|
|
38020
38196
|
return {
|
|
38021
38197
|
line: `[watchdog] cam${cam.deviceId} ${cam.deviceName} · ${parts.join(" · ")} → ${stalled ? "STALLED" : "OK"}`,
|
|
38022
38198
|
stalled,
|
|
38023
|
-
recoveries
|
|
38199
|
+
recoveries,
|
|
38200
|
+
disarms
|
|
38024
38201
|
};
|
|
38025
38202
|
}
|
|
38026
38203
|
};
|
|
@@ -38917,6 +39094,14 @@ var ReconcileController = class ReconcileController {
|
|
|
38917
39094
|
/** Shared in-flight pass so a session dispatch waits for runner truth instead of racing it. */
|
|
38918
39095
|
attachmentReconcilePromise = null;
|
|
38919
39096
|
/**
|
|
39097
|
+
* Desired-but-nonexistent devices and how far their vanish streak has got
|
|
39098
|
+
* (`dispatch-reconcile.ts`). PURELY in-memory and seeded EMPTY on boot: it is
|
|
39099
|
+
* a confirmation counter, not a record — after a restart the first pass only
|
|
39100
|
+
* ever re-observes, never destroys. Cleared whenever the registry read fails,
|
|
39101
|
+
* so a failed read can never advance a device toward teardown.
|
|
39102
|
+
*/
|
|
39103
|
+
vanishedDevices = /* @__PURE__ */ new Map();
|
|
39104
|
+
/**
|
|
38920
39105
|
* Per-device backoff for retrying `handleDeviceRegistered` after a
|
|
38921
39106
|
* TRANSIENT profile-slot read failure. Timers are `unref`'d and cleared on
|
|
38922
39107
|
* shutdown / device removal.
|
|
@@ -38985,13 +39170,24 @@ var ReconcileController = class ReconcileController {
|
|
|
38985
39170
|
return;
|
|
38986
39171
|
}
|
|
38987
39172
|
const desired = desiredDeviceIdsFromSlots(slots);
|
|
39173
|
+
const liveDeviceIds = await this.readLiveDeviceIds();
|
|
39174
|
+
const { existing, vanished } = liveDeviceIds === null ? {
|
|
39175
|
+
existing: desired,
|
|
39176
|
+
vanished: /* @__PURE__ */ new Set()
|
|
39177
|
+
} : splitDesiredByExistence(desired, liveDeviceIds);
|
|
39178
|
+
if (liveDeviceIds === null) {
|
|
39179
|
+
this.deps.logger.warn("dispatch reconcile: device existence filter SKIPPED — registry read failed", { meta: { desired: desired.size } });
|
|
39180
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39181
|
+
}
|
|
38988
39182
|
const known = new Set(this.deps.ledger.listConfigIds());
|
|
38989
|
-
const gap = computeDispatchGap(
|
|
39183
|
+
const gap = computeDispatchGap(existing, known);
|
|
38990
39184
|
this.deps.logger.info("dispatch reconcile", { meta: {
|
|
38991
39185
|
desired: desired.size,
|
|
38992
39186
|
known: known.size,
|
|
38993
|
-
gap
|
|
39187
|
+
gap,
|
|
39188
|
+
vanished: vanished.size
|
|
38994
39189
|
} });
|
|
39190
|
+
await this.tearDownVanishedDevices(vanished);
|
|
38995
39191
|
for (const deviceId of gap) try {
|
|
38996
39192
|
await this.deps.handleDeviceRegistered(deviceId);
|
|
38997
39193
|
} catch (err) {
|
|
@@ -39010,6 +39206,53 @@ var ReconcileController = class ReconcileController {
|
|
|
39010
39206
|
}
|
|
39011
39207
|
}
|
|
39012
39208
|
/**
|
|
39209
|
+
* The live device registry as an id set, or `null` when the read FAILED.
|
|
39210
|
+
* `null` is the caller's signal to skip the existence filter entirely — it
|
|
39211
|
+
* must never be confused with "the registry is empty", which would prune the
|
|
39212
|
+
* whole fleet.
|
|
39213
|
+
*
|
|
39214
|
+
* Asks for the slim projection: this needs identity only, never
|
|
39215
|
+
* `config`/`sourceInfo` (a full listing of 293 devices measured 271 KB).
|
|
39216
|
+
*/
|
|
39217
|
+
async readLiveDeviceIds() {
|
|
39218
|
+
const api = this.deps.api();
|
|
39219
|
+
if (!api) return null;
|
|
39220
|
+
try {
|
|
39221
|
+
const devices = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
39222
|
+
return new Set(devices.map((device) => device.id));
|
|
39223
|
+
} catch (err) {
|
|
39224
|
+
this.deps.logger.debug("dispatch reconcile: deviceManager.listAll failed", { meta: { error: errMsg(err) } });
|
|
39225
|
+
return null;
|
|
39226
|
+
}
|
|
39227
|
+
}
|
|
39228
|
+
/**
|
|
39229
|
+
* Tear down the desired devices the registry no longer has — but only once
|
|
39230
|
+
* TWO consecutive passes agree they are gone (`reconcileVanishedDevices`,
|
|
39231
|
+
* D49). Teardown destroys work, so a single listing that omits a device
|
|
39232
|
+
* (which a partial or racy read looks exactly like) is not enough.
|
|
39233
|
+
*
|
|
39234
|
+
* Runs the SAME `handleDeviceUnregistered` the `DeviceUnregistered` event
|
|
39235
|
+
* runs, so there is exactly one teardown path.
|
|
39236
|
+
*/
|
|
39237
|
+
async tearDownVanishedDevices(vanished) {
|
|
39238
|
+
const { tearDown, nextState } = reconcileVanishedDevices(vanished, this.vanishedDevices);
|
|
39239
|
+
this.vanishedDevices = nextState;
|
|
39240
|
+
for (const deviceId of tearDown) {
|
|
39241
|
+
this.deps.logger.warn("dispatch reconcile: device no longer exists — tearing down instead of dispatching", {
|
|
39242
|
+
tags: { deviceId },
|
|
39243
|
+
meta: { confirmedPasses: 2 }
|
|
39244
|
+
});
|
|
39245
|
+
try {
|
|
39246
|
+
await this.deps.handleDeviceUnregistered(deviceId);
|
|
39247
|
+
} catch (err) {
|
|
39248
|
+
this.deps.logger.warn("dispatch reconcile: teardown of vanished device failed", {
|
|
39249
|
+
tags: { deviceId },
|
|
39250
|
+
meta: { error: errMsg(err) }
|
|
39251
|
+
});
|
|
39252
|
+
}
|
|
39253
|
+
}
|
|
39254
|
+
}
|
|
39255
|
+
/**
|
|
39013
39256
|
* Converge the in-memory `assignments` map to runner truth (Phase 1 of the
|
|
39014
39257
|
* placement-derived-state design). Runner attachments (`getLocalCameras`) are
|
|
39015
39258
|
* authoritative: detach duplicates/orphans, and drop stale assignments
|
|
@@ -39320,6 +39563,7 @@ var ReconcileController = class ReconcileController {
|
|
|
39320
39563
|
this.slotReadRetryTimers.clear();
|
|
39321
39564
|
this.slotReadRetryAttempts.clear();
|
|
39322
39565
|
this.remoteHealthAttempts.clear();
|
|
39566
|
+
this.vanishedDevices = /* @__PURE__ */ new Map();
|
|
39323
39567
|
}
|
|
39324
39568
|
};
|
|
39325
39569
|
//#endregion
|
|
@@ -40385,6 +40629,31 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40385
40629
|
getInitTimestamp: () => deps.initTimestamp
|
|
40386
40630
|
});
|
|
40387
40631
|
const inferenceRotation = new RoundRobinInferenceDeviceRotation();
|
|
40632
|
+
/**
|
|
40633
|
+
* Does this camera's source carry an audio track? Read from the broker that
|
|
40634
|
+
* owns the stream (`BrokerStats.audio === null` is the documented no-audio
|
|
40635
|
+
* signal), pinned to the cluster's source owner exactly like the
|
|
40636
|
+
* camera-status broker stage. FAILS OPEN: no api, an RPC error, or a broker
|
|
40637
|
+
* that has not detected the track yet (`undefined`, cold start) all answer
|
|
40638
|
+
* `true`, because the only consumer is the watchdog's diagnostic audio stage
|
|
40639
|
+
* and a false negative silently stops watching a working camera.
|
|
40640
|
+
*/
|
|
40641
|
+
const resolveHasAudioSource = async (deviceId, camStreamId) => {
|
|
40642
|
+
const api = deps.ctx().api;
|
|
40643
|
+
if (!api) return true;
|
|
40644
|
+
try {
|
|
40645
|
+
return (await api.streamBroker.getBrokerStats.query({ brokerId: makeSourceBrokerId(deviceId, camStreamId) }, nodePin(topology.assignSource(deviceId)))).audio !== null;
|
|
40646
|
+
} catch (err) {
|
|
40647
|
+
deps.ctx().logger.debug("audio-source probe failed — assuming the source has audio", {
|
|
40648
|
+
tags: { deviceId },
|
|
40649
|
+
meta: {
|
|
40650
|
+
camStreamId,
|
|
40651
|
+
error: errMsg(err)
|
|
40652
|
+
}
|
|
40653
|
+
});
|
|
40654
|
+
return true;
|
|
40655
|
+
}
|
|
40656
|
+
};
|
|
40388
40657
|
const placement = new PlacementService({
|
|
40389
40658
|
ctx: () => deps.ctx(),
|
|
40390
40659
|
ctxIfReady: () => deps.ctxIfReady(),
|
|
@@ -40400,7 +40669,8 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40400
40669
|
getInferenceDeviceCaps: (nodeId) => deps.getInferenceDeviceCaps(nodeId),
|
|
40401
40670
|
readPipelineDevicePin: (deviceId) => deps.readPipelineDevicePin(deviceId),
|
|
40402
40671
|
inferenceRotation,
|
|
40403
|
-
buildWatchdogCamera: (config, deviceName) => detectionWiring.buildWatchdogCamera(config, deviceName),
|
|
40672
|
+
buildWatchdogCamera: (config, deviceName, hasAudioSource) => detectionWiring.buildWatchdogCamera(config, deviceName, hasAudioSource),
|
|
40673
|
+
hasAudioSource: (deviceId, camStreamId) => resolveHasAudioSource(deviceId, camStreamId),
|
|
40404
40674
|
isIdleSessionCamera: (deviceId) => deps.isIdleSessionCamera(deviceId),
|
|
40405
40675
|
activeSessionNode: (deviceId) => session.activeSessionNode(deviceId),
|
|
40406
40676
|
closeActiveSession: (deviceId) => session.closeSessionForReassign(deviceId),
|
|
@@ -40475,6 +40745,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40475
40745
|
loadShed,
|
|
40476
40746
|
logger: deps.ctx().logger,
|
|
40477
40747
|
handleDeviceRegistered: (deviceId) => deps.handleDeviceRegistered(deviceId),
|
|
40748
|
+
handleDeviceUnregistered: (deviceId) => deps.handleDeviceUnregistered(deviceId),
|
|
40478
40749
|
dispatchCamera: (config) => deps.dispatchCamera(config),
|
|
40479
40750
|
detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
|
|
40480
40751
|
localNodeId: () => localNodeId,
|
|
@@ -40575,7 +40846,7 @@ async function buildOrchestratorControllers(deps) {
|
|
|
40575
40846
|
const pipelineWatchdog = new PipelineWatchdog({
|
|
40576
40847
|
now: () => Date.now(),
|
|
40577
40848
|
logger: deps.ctx().logger,
|
|
40578
|
-
recover: (deviceId, stage, streamId) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId),
|
|
40849
|
+
recover: (deviceId, stage, streamId, attempt) => detectionWiring.recoverPipelineStage(deviceId, stage, streamId, attempt),
|
|
40579
40850
|
thresholds: DEFAULT_WATCHDOG_THRESHOLDS
|
|
40580
40851
|
});
|
|
40581
40852
|
pipelineWatchdog.start(deps.watchdogIntervalMs);
|