@camstack/addon-pipeline 1.2.222 → 1.2.224
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +53 -14
- package/dist/audio-analyzer/index.mjs +53 -14
- package/dist/pipeline-runner/index.js +61 -45
- package/dist/pipeline-runner/index.mjs +61 -45
- package/dist/recorder/index.js +88 -25
- package/dist/recorder/index.mjs +88 -25
- package/dist/stream-broker/index.js +93 -51
- package/dist/stream-broker/index.mjs +93 -51
- package/package.json +1 -1
|
@@ -397,8 +397,20 @@ function resampleMono16k(chunk) {
|
|
|
397
397
|
}
|
|
398
398
|
return out;
|
|
399
399
|
}
|
|
400
|
-
/**
|
|
401
|
-
|
|
400
|
+
/**
|
|
401
|
+
* How many times one tick may re-poll immediately after a FULL batch (D465).
|
|
402
|
+
*
|
|
403
|
+
* A batch that comes back full is not a drain, it is a backlog: there is more
|
|
404
|
+
* waiting and the next chunk is already older than the window it belongs to.
|
|
405
|
+
* Continuing bounds the damage for any producer cadence — including one this
|
|
406
|
+
* file cannot know — without adding a single request when there is no backlog.
|
|
407
|
+
* The broker's FIFO holds 64, so two continuations clear a completely full
|
|
408
|
+
* queue; four is margin, and the bound is what stops a permanently faster
|
|
409
|
+
* producer from turning this into a spin.
|
|
410
|
+
*/
|
|
411
|
+
var MAX_DRAIN_CONTINUATIONS = 4;
|
|
412
|
+
/** Re-state a standing backlog at most this often. */
|
|
413
|
+
var BACKLOG_RELOG_MS = 3e4;
|
|
402
414
|
/**
|
|
403
415
|
* Consecutive pull failures before we attempt to re-subscribe. A single failed
|
|
404
416
|
* poll is treated as a blip (re-subscribing would leak a broker-side FIFO); a
|
|
@@ -441,7 +453,9 @@ function startAudioChunkPoller(options) {
|
|
|
441
453
|
stopped: false,
|
|
442
454
|
retryTimer: void 0,
|
|
443
455
|
pollTimer: void 0,
|
|
444
|
-
activeSubscriptionId: null
|
|
456
|
+
activeSubscriptionId: null,
|
|
457
|
+
backlogged: false,
|
|
458
|
+
lastBacklogLogAt: 0
|
|
445
459
|
};
|
|
446
460
|
const teardown = () => {
|
|
447
461
|
if (lifecycle.stopped) return;
|
|
@@ -567,19 +581,44 @@ function startPolling(options, lifecycle) {
|
|
|
567
581
|
const subId = lifecycle.activeSubscriptionId;
|
|
568
582
|
if (!subId) return;
|
|
569
583
|
try {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
584
|
+
let continuations = 0;
|
|
585
|
+
let drained = 0;
|
|
586
|
+
for (;;) {
|
|
587
|
+
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
588
|
+
subscriptionId: subId,
|
|
589
|
+
maxCount: 32
|
|
590
|
+
}, pin);
|
|
591
|
+
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
592
|
+
brokerId,
|
|
593
|
+
subscriptionId: subId
|
|
594
|
+
} });
|
|
595
|
+
consecutiveFailures = 0;
|
|
596
|
+
drained += chunks.length;
|
|
597
|
+
for (const chunk of chunks) {
|
|
598
|
+
if (lifecycle.stopped) break;
|
|
599
|
+
await onChunk(chunk);
|
|
600
|
+
}
|
|
580
601
|
if (lifecycle.stopped) break;
|
|
581
|
-
|
|
602
|
+
if (chunks.length < 32) break;
|
|
603
|
+
if (continuations >= MAX_DRAIN_CONTINUATIONS) break;
|
|
604
|
+
continuations += 1;
|
|
582
605
|
}
|
|
606
|
+
if (continuations > 0) {
|
|
607
|
+
const now = Date.now();
|
|
608
|
+
if (!lifecycle.backlogged || now - lifecycle.lastBacklogLogAt >= BACKLOG_RELOG_MS) {
|
|
609
|
+
lifecycle.lastBacklogLogAt = now;
|
|
610
|
+
logger.warn("audio-chunk poller: backlog — a full batch is not a drain", { meta: {
|
|
611
|
+
brokerId,
|
|
612
|
+
subscriptionId: subId,
|
|
613
|
+
drained,
|
|
614
|
+
maxCount: 32,
|
|
615
|
+
continuations,
|
|
616
|
+
pollIntervalMs: options.pollIntervalMs ?? 200,
|
|
617
|
+
hint: "this camera produces chunks faster than one poll of this cadence can carry — the broker FIFO is overwriting next"
|
|
618
|
+
} });
|
|
619
|
+
}
|
|
620
|
+
lifecycle.backlogged = true;
|
|
621
|
+
} else lifecycle.backlogged = false;
|
|
583
622
|
} catch (err) {
|
|
584
623
|
consecutiveFailures += 1;
|
|
585
624
|
if (consecutiveFailures === 1) logger.warn("audio-chunk poller: pullAudioChunks failed — attempting recovery", { meta: {
|
|
@@ -391,8 +391,20 @@ function resampleMono16k(chunk) {
|
|
|
391
391
|
}
|
|
392
392
|
return out;
|
|
393
393
|
}
|
|
394
|
-
/**
|
|
395
|
-
|
|
394
|
+
/**
|
|
395
|
+
* How many times one tick may re-poll immediately after a FULL batch (D465).
|
|
396
|
+
*
|
|
397
|
+
* A batch that comes back full is not a drain, it is a backlog: there is more
|
|
398
|
+
* waiting and the next chunk is already older than the window it belongs to.
|
|
399
|
+
* Continuing bounds the damage for any producer cadence — including one this
|
|
400
|
+
* file cannot know — without adding a single request when there is no backlog.
|
|
401
|
+
* The broker's FIFO holds 64, so two continuations clear a completely full
|
|
402
|
+
* queue; four is margin, and the bound is what stops a permanently faster
|
|
403
|
+
* producer from turning this into a spin.
|
|
404
|
+
*/
|
|
405
|
+
var MAX_DRAIN_CONTINUATIONS = 4;
|
|
406
|
+
/** Re-state a standing backlog at most this often. */
|
|
407
|
+
var BACKLOG_RELOG_MS = 3e4;
|
|
396
408
|
/**
|
|
397
409
|
* Consecutive pull failures before we attempt to re-subscribe. A single failed
|
|
398
410
|
* poll is treated as a blip (re-subscribing would leak a broker-side FIFO); a
|
|
@@ -435,7 +447,9 @@ function startAudioChunkPoller(options) {
|
|
|
435
447
|
stopped: false,
|
|
436
448
|
retryTimer: void 0,
|
|
437
449
|
pollTimer: void 0,
|
|
438
|
-
activeSubscriptionId: null
|
|
450
|
+
activeSubscriptionId: null,
|
|
451
|
+
backlogged: false,
|
|
452
|
+
lastBacklogLogAt: 0
|
|
439
453
|
};
|
|
440
454
|
const teardown = () => {
|
|
441
455
|
if (lifecycle.stopped) return;
|
|
@@ -561,19 +575,44 @@ function startPolling(options, lifecycle) {
|
|
|
561
575
|
const subId = lifecycle.activeSubscriptionId;
|
|
562
576
|
if (!subId) return;
|
|
563
577
|
try {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
578
|
+
let continuations = 0;
|
|
579
|
+
let drained = 0;
|
|
580
|
+
for (;;) {
|
|
581
|
+
const chunks = await api.streamBroker.pullAudioChunks.query({
|
|
582
|
+
subscriptionId: subId,
|
|
583
|
+
maxCount: 32
|
|
584
|
+
}, pin);
|
|
585
|
+
if (consecutiveFailures > 0) logger.info("audio-chunk poller: stream recovered", { meta: {
|
|
586
|
+
brokerId,
|
|
587
|
+
subscriptionId: subId
|
|
588
|
+
} });
|
|
589
|
+
consecutiveFailures = 0;
|
|
590
|
+
drained += chunks.length;
|
|
591
|
+
for (const chunk of chunks) {
|
|
592
|
+
if (lifecycle.stopped) break;
|
|
593
|
+
await onChunk(chunk);
|
|
594
|
+
}
|
|
574
595
|
if (lifecycle.stopped) break;
|
|
575
|
-
|
|
596
|
+
if (chunks.length < 32) break;
|
|
597
|
+
if (continuations >= MAX_DRAIN_CONTINUATIONS) break;
|
|
598
|
+
continuations += 1;
|
|
576
599
|
}
|
|
600
|
+
if (continuations > 0) {
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
if (!lifecycle.backlogged || now - lifecycle.lastBacklogLogAt >= BACKLOG_RELOG_MS) {
|
|
603
|
+
lifecycle.lastBacklogLogAt = now;
|
|
604
|
+
logger.warn("audio-chunk poller: backlog — a full batch is not a drain", { meta: {
|
|
605
|
+
brokerId,
|
|
606
|
+
subscriptionId: subId,
|
|
607
|
+
drained,
|
|
608
|
+
maxCount: 32,
|
|
609
|
+
continuations,
|
|
610
|
+
pollIntervalMs: options.pollIntervalMs ?? 200,
|
|
611
|
+
hint: "this camera produces chunks faster than one poll of this cadence can carry — the broker FIFO is overwriting next"
|
|
612
|
+
} });
|
|
613
|
+
}
|
|
614
|
+
lifecycle.backlogged = true;
|
|
615
|
+
} else lifecycle.backlogged = false;
|
|
577
616
|
} catch (err) {
|
|
578
617
|
consecutiveFailures += 1;
|
|
579
618
|
if (consecutiveFailures === 1) logger.warn("audio-chunk poller: pullAudioChunks failed — attempting recovery", { meta: {
|
|
@@ -6507,41 +6507,34 @@ function isUsable(bbox) {
|
|
|
6507
6507
|
//#endregion
|
|
6508
6508
|
//#region src/pipeline-runner/runtime-labels.ts
|
|
6509
6509
|
/**
|
|
6510
|
-
* `null`
|
|
6511
|
-
*
|
|
6512
|
-
*
|
|
6513
|
-
*
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
const
|
|
6510
|
+
* `null` means the read FAILED; `[]` means nothing is resident. Both omit the
|
|
6511
|
+
* fields, but they are different facts and the caller logs the failure.
|
|
6512
|
+
*
|
|
6513
|
+
* The `runtime` pool is named first because it is the one serving cameras; the
|
|
6514
|
+
* rest follow in arrival order. `python` is deliberately NOT printed: every
|
|
6515
|
+
* engine available on this cluster is python, and a token that cannot vary
|
|
6516
|
+
* costs a column and tells the reader nothing.
|
|
6517
|
+
*/
|
|
6518
|
+
function buildRuntimeLabels(engines) {
|
|
6519
|
+
if (engines === null || engines.length === 0) return {};
|
|
6520
|
+
const ordered = [...engines.filter((e) => e.kind === "runtime"), ...engines.filter((e) => e.kind !== "runtime")];
|
|
6521
|
+
const devices = [...new Set(ordered.map((e) => e.device).filter((d) => !!d))];
|
|
6522
|
+
const backends = [...new Set(ordered.map((e) => e.backend))];
|
|
6523
|
+
const backend = backends.length === 1 ? backends[0] : "mixed";
|
|
6524
|
+
const runtime = devices.length === 0 ? backend : `${backend}/${devices.join("+")}`;
|
|
6525
|
+
const primary = ordered[0];
|
|
6526
|
+
if (primary === void 0) return {};
|
|
6527
|
+
const models = [];
|
|
6528
|
+
for (const engine of ordered) for (const model of engine.modelsLoaded) {
|
|
6529
|
+
if (models.includes(model)) continue;
|
|
6530
|
+
if (engine === primary || primary.modelsLoaded.includes(model)) models.push(model);
|
|
6531
|
+
else models.push(engine.device === void 0 ? model : `${model}@${engine.device}`);
|
|
6532
|
+
}
|
|
6522
6533
|
return {
|
|
6523
|
-
|
|
6524
|
-
|
|
6534
|
+
pipelineRuntime: runtime,
|
|
6535
|
+
pipelineModels: models
|
|
6525
6536
|
};
|
|
6526
6537
|
}
|
|
6527
|
-
/**
|
|
6528
|
-
* Depth-first flatten of the active pipeline tree.
|
|
6529
|
-
*
|
|
6530
|
-
* A DISABLED step takes its subtree with it: the children of a step that does
|
|
6531
|
-
* not run did not run either, and listing a face model under a disabled face
|
|
6532
|
-
* detector would name a model that never loaded.
|
|
6533
|
-
*/
|
|
6534
|
-
function flattenLabelledSteps(steps) {
|
|
6535
|
-
const out = [];
|
|
6536
|
-
for (const step of steps) {
|
|
6537
|
-
out.push({
|
|
6538
|
-
modelId: step.modelId,
|
|
6539
|
-
enabled: step.enabled
|
|
6540
|
-
});
|
|
6541
|
-
if (step.enabled && step.children !== void 0) out.push(...flattenLabelledSteps(step.children));
|
|
6542
|
-
}
|
|
6543
|
-
return out;
|
|
6544
|
-
}
|
|
6545
6538
|
//#endregion
|
|
6546
6539
|
//#region src/pipeline-runner/track-frame-park.ts
|
|
6547
6540
|
/** Park one frame for one track. Never throws; a refusal is an answer. */
|
|
@@ -6702,6 +6695,8 @@ var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
|
|
|
6702
6695
|
* operator re-selects one or lazy provisioning completes — not per frame.
|
|
6703
6696
|
*/
|
|
6704
6697
|
var RUNTIME_INFO_REFRESH_MS = 3e5;
|
|
6698
|
+
/** Sentinel for "no api" so it is one transition, not a per-refresh line. */
|
|
6699
|
+
var NO_API_FAILURE = "no addon api";
|
|
6705
6700
|
/** How often a camera reports parcels that left without a release (D422). */
|
|
6706
6701
|
var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
|
|
6707
6702
|
/**
|
|
@@ -7139,6 +7134,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7139
7134
|
nodeId = "unknown";
|
|
7140
7135
|
metricsSnapshotTimer = null;
|
|
7141
7136
|
runtimeInfoTimer = null;
|
|
7137
|
+
/** Last runtime-label failure, so only a CHANGE is logged (D463). */
|
|
7138
|
+
lastRuntimeInfoFailure = null;
|
|
7142
7139
|
unsubMotionEvents = null;
|
|
7143
7140
|
/** Devices we have positively observed reporting `battery.sleeping: true`.
|
|
7144
7141
|
* Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
|
|
@@ -8823,28 +8820,47 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
8823
8820
|
*/
|
|
8824
8821
|
async refreshRuntimeInfo() {
|
|
8825
8822
|
const api = this.ctxIfReady?.api;
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
engine = await api.pipelineExecutor.getSelectedEngine.query();
|
|
8831
|
-
} catch (err) {
|
|
8832
|
-
this.ctxIfReady?.logger.debug("runtime labels: selected engine unreadable", { meta: { error: require_dist.errMsg(err) } });
|
|
8823
|
+
const sampler = this.runner?.timingSampler;
|
|
8824
|
+
if (!api || sampler === void 0 || sampler === null) {
|
|
8825
|
+
this.noteRuntimeInfoFailure(NO_API_FAILURE);
|
|
8826
|
+
return;
|
|
8833
8827
|
}
|
|
8828
|
+
let engines = null;
|
|
8829
|
+
let failure = null;
|
|
8834
8830
|
try {
|
|
8835
|
-
|
|
8836
|
-
|
|
8831
|
+
engines = (await api.pipelineExecutor.listLoadedEngines.query()).map((r) => ({
|
|
8832
|
+
backend: r.engine.backend,
|
|
8833
|
+
...r.engine.device !== void 0 ? { device: r.engine.device } : {},
|
|
8834
|
+
kind: r.kind,
|
|
8835
|
+
modelsLoaded: r.modelsLoaded
|
|
8836
|
+
}));
|
|
8837
8837
|
} catch (err) {
|
|
8838
|
-
|
|
8838
|
+
failure = require_dist.errMsg(err);
|
|
8839
8839
|
}
|
|
8840
|
-
|
|
8841
|
-
if (sampler === void 0 || sampler === null) return;
|
|
8840
|
+
this.noteRuntimeInfoFailure(failure);
|
|
8842
8841
|
sampler.runtimeInfo = {
|
|
8843
8842
|
...sampler.runtimeInfo,
|
|
8844
|
-
...buildRuntimeLabels(
|
|
8843
|
+
...buildRuntimeLabels(engines)
|
|
8845
8844
|
};
|
|
8846
8845
|
}
|
|
8847
8846
|
/**
|
|
8847
|
+
* Log a runtime-label failure at its TRANSITION, and at `warn`.
|
|
8848
|
+
*
|
|
8849
|
+
* This branch first shipped at `debug`, on the argument that the absent field
|
|
8850
|
+
* in the line was signal enough. It was not: within the hour `little-unraid`
|
|
8851
|
+
* was the one node still logging `pipelineRuntime=null` and there was no way
|
|
8852
|
+
* to tell "this node has no engine" from "the read threw", because the hub
|
|
8853
|
+
* writes `info` and the agent forwards to it. That is D438's blind
|
|
8854
|
+
* instrument for the third time. Logging the transition rather than every
|
|
8855
|
+
* refresh is what keeps it from becoming a storm.
|
|
8856
|
+
*/
|
|
8857
|
+
noteRuntimeInfoFailure(failure) {
|
|
8858
|
+
if (failure === this.lastRuntimeInfoFailure) return;
|
|
8859
|
+
if (failure === null) this.ctxIfReady?.logger.info("runtime labels: readable again");
|
|
8860
|
+
else this.ctxIfReady?.logger.warn("runtime labels unreadable — this node cannot name its engines", { meta: { error: failure } });
|
|
8861
|
+
this.lastRuntimeInfoFailure = failure;
|
|
8862
|
+
}
|
|
8863
|
+
/**
|
|
8848
8864
|
* The model format this node dispatches on when no `deviceKey` is given —
|
|
8849
8865
|
* its selected engine's format. `null` on any failure, which the caller
|
|
8850
8866
|
* turns into a refusal rather than a guess: a model pin that cannot be
|
|
@@ -6499,41 +6499,34 @@ function isUsable(bbox) {
|
|
|
6499
6499
|
//#endregion
|
|
6500
6500
|
//#region src/pipeline-runner/runtime-labels.ts
|
|
6501
6501
|
/**
|
|
6502
|
-
* `null`
|
|
6503
|
-
*
|
|
6504
|
-
*
|
|
6505
|
-
*
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
const
|
|
6502
|
+
* `null` means the read FAILED; `[]` means nothing is resident. Both omit the
|
|
6503
|
+
* fields, but they are different facts and the caller logs the failure.
|
|
6504
|
+
*
|
|
6505
|
+
* The `runtime` pool is named first because it is the one serving cameras; the
|
|
6506
|
+
* rest follow in arrival order. `python` is deliberately NOT printed: every
|
|
6507
|
+
* engine available on this cluster is python, and a token that cannot vary
|
|
6508
|
+
* costs a column and tells the reader nothing.
|
|
6509
|
+
*/
|
|
6510
|
+
function buildRuntimeLabels(engines) {
|
|
6511
|
+
if (engines === null || engines.length === 0) return {};
|
|
6512
|
+
const ordered = [...engines.filter((e) => e.kind === "runtime"), ...engines.filter((e) => e.kind !== "runtime")];
|
|
6513
|
+
const devices = [...new Set(ordered.map((e) => e.device).filter((d) => !!d))];
|
|
6514
|
+
const backends = [...new Set(ordered.map((e) => e.backend))];
|
|
6515
|
+
const backend = backends.length === 1 ? backends[0] : "mixed";
|
|
6516
|
+
const runtime = devices.length === 0 ? backend : `${backend}/${devices.join("+")}`;
|
|
6517
|
+
const primary = ordered[0];
|
|
6518
|
+
if (primary === void 0) return {};
|
|
6519
|
+
const models = [];
|
|
6520
|
+
for (const engine of ordered) for (const model of engine.modelsLoaded) {
|
|
6521
|
+
if (models.includes(model)) continue;
|
|
6522
|
+
if (engine === primary || primary.modelsLoaded.includes(model)) models.push(model);
|
|
6523
|
+
else models.push(engine.device === void 0 ? model : `${model}@${engine.device}`);
|
|
6524
|
+
}
|
|
6514
6525
|
return {
|
|
6515
|
-
|
|
6516
|
-
|
|
6526
|
+
pipelineRuntime: runtime,
|
|
6527
|
+
pipelineModels: models
|
|
6517
6528
|
};
|
|
6518
6529
|
}
|
|
6519
|
-
/**
|
|
6520
|
-
* Depth-first flatten of the active pipeline tree.
|
|
6521
|
-
*
|
|
6522
|
-
* A DISABLED step takes its subtree with it: the children of a step that does
|
|
6523
|
-
* not run did not run either, and listing a face model under a disabled face
|
|
6524
|
-
* detector would name a model that never loaded.
|
|
6525
|
-
*/
|
|
6526
|
-
function flattenLabelledSteps(steps) {
|
|
6527
|
-
const out = [];
|
|
6528
|
-
for (const step of steps) {
|
|
6529
|
-
out.push({
|
|
6530
|
-
modelId: step.modelId,
|
|
6531
|
-
enabled: step.enabled
|
|
6532
|
-
});
|
|
6533
|
-
if (step.enabled && step.children !== void 0) out.push(...flattenLabelledSteps(step.children));
|
|
6534
|
-
}
|
|
6535
|
-
return out;
|
|
6536
|
-
}
|
|
6537
6530
|
//#endregion
|
|
6538
6531
|
//#region src/pipeline-runner/track-frame-park.ts
|
|
6539
6532
|
/** Park one frame for one track. Never throws; a refusal is an answer. */
|
|
@@ -6694,6 +6687,8 @@ var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
|
|
|
6694
6687
|
* operator re-selects one or lazy provisioning completes — not per frame.
|
|
6695
6688
|
*/
|
|
6696
6689
|
var RUNTIME_INFO_REFRESH_MS = 3e5;
|
|
6690
|
+
/** Sentinel for "no api" so it is one transition, not a per-refresh line. */
|
|
6691
|
+
var NO_API_FAILURE = "no addon api";
|
|
6697
6692
|
/** How often a camera reports parcels that left without a release (D422). */
|
|
6698
6693
|
var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
|
|
6699
6694
|
/**
|
|
@@ -7131,6 +7126,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7131
7126
|
nodeId = "unknown";
|
|
7132
7127
|
metricsSnapshotTimer = null;
|
|
7133
7128
|
runtimeInfoTimer = null;
|
|
7129
|
+
/** Last runtime-label failure, so only a CHANGE is logged (D463). */
|
|
7130
|
+
lastRuntimeInfoFailure = null;
|
|
7134
7131
|
unsubMotionEvents = null;
|
|
7135
7132
|
/** Devices we have positively observed reporting `battery.sleeping: true`.
|
|
7136
7133
|
* Absent = unknown or awake; both mean "do not gate" (D49). Fed only by
|
|
@@ -8815,28 +8812,47 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
8815
8812
|
*/
|
|
8816
8813
|
async refreshRuntimeInfo() {
|
|
8817
8814
|
const api = this.ctxIfReady?.api;
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
engine = await api.pipelineExecutor.getSelectedEngine.query();
|
|
8823
|
-
} catch (err) {
|
|
8824
|
-
this.ctxIfReady?.logger.debug("runtime labels: selected engine unreadable", { meta: { error: errMsg(err) } });
|
|
8815
|
+
const sampler = this.runner?.timingSampler;
|
|
8816
|
+
if (!api || sampler === void 0 || sampler === null) {
|
|
8817
|
+
this.noteRuntimeInfoFailure(NO_API_FAILURE);
|
|
8818
|
+
return;
|
|
8825
8819
|
}
|
|
8820
|
+
let engines = null;
|
|
8821
|
+
let failure = null;
|
|
8826
8822
|
try {
|
|
8827
|
-
|
|
8828
|
-
|
|
8823
|
+
engines = (await api.pipelineExecutor.listLoadedEngines.query()).map((r) => ({
|
|
8824
|
+
backend: r.engine.backend,
|
|
8825
|
+
...r.engine.device !== void 0 ? { device: r.engine.device } : {},
|
|
8826
|
+
kind: r.kind,
|
|
8827
|
+
modelsLoaded: r.modelsLoaded
|
|
8828
|
+
}));
|
|
8829
8829
|
} catch (err) {
|
|
8830
|
-
|
|
8830
|
+
failure = errMsg(err);
|
|
8831
8831
|
}
|
|
8832
|
-
|
|
8833
|
-
if (sampler === void 0 || sampler === null) return;
|
|
8832
|
+
this.noteRuntimeInfoFailure(failure);
|
|
8834
8833
|
sampler.runtimeInfo = {
|
|
8835
8834
|
...sampler.runtimeInfo,
|
|
8836
|
-
...buildRuntimeLabels(
|
|
8835
|
+
...buildRuntimeLabels(engines)
|
|
8837
8836
|
};
|
|
8838
8837
|
}
|
|
8839
8838
|
/**
|
|
8839
|
+
* Log a runtime-label failure at its TRANSITION, and at `warn`.
|
|
8840
|
+
*
|
|
8841
|
+
* This branch first shipped at `debug`, on the argument that the absent field
|
|
8842
|
+
* in the line was signal enough. It was not: within the hour `little-unraid`
|
|
8843
|
+
* was the one node still logging `pipelineRuntime=null` and there was no way
|
|
8844
|
+
* to tell "this node has no engine" from "the read threw", because the hub
|
|
8845
|
+
* writes `info` and the agent forwards to it. That is D438's blind
|
|
8846
|
+
* instrument for the third time. Logging the transition rather than every
|
|
8847
|
+
* refresh is what keeps it from becoming a storm.
|
|
8848
|
+
*/
|
|
8849
|
+
noteRuntimeInfoFailure(failure) {
|
|
8850
|
+
if (failure === this.lastRuntimeInfoFailure) return;
|
|
8851
|
+
if (failure === null) this.ctxIfReady?.logger.info("runtime labels: readable again");
|
|
8852
|
+
else this.ctxIfReady?.logger.warn("runtime labels unreadable — this node cannot name its engines", { meta: { error: failure } });
|
|
8853
|
+
this.lastRuntimeInfoFailure = failure;
|
|
8854
|
+
}
|
|
8855
|
+
/**
|
|
8840
8856
|
* The model format this node dispatches on when no `deviceKey` is given —
|
|
8841
8857
|
* its selected engine's format. `null` on any failure, which the caller
|
|
8842
8858
|
* turns into a refusal rather than a guess: a model pin that cannot be
|
package/dist/recorder/index.js
CHANGED
|
@@ -7777,6 +7777,7 @@ function buildRecordingProvider(deps) {
|
|
|
7777
7777
|
},
|
|
7778
7778
|
setDeviceConfig: async ({ deviceId, config }) => {
|
|
7779
7779
|
const saved = await saveDeviceConfig(deps.configStore, deviceId, config);
|
|
7780
|
+
deps.onConfigSaved?.(deviceId, saved);
|
|
7780
7781
|
if (deps.onConfigChanged) try {
|
|
7781
7782
|
await deps.onConfigChanged(deviceId);
|
|
7782
7783
|
} catch (err) {
|
|
@@ -10963,29 +10964,75 @@ Object.freeze({
|
|
|
10963
10964
|
"turn-provider": ["getTurnServers"],
|
|
10964
10965
|
"user-passkeys": ["listPasskeys"]
|
|
10965
10966
|
});
|
|
10966
|
-
//#endregion
|
|
10967
|
-
//#region src/recorder/addon/last-known-config.ts
|
|
10968
10967
|
/**
|
|
10969
|
-
*
|
|
10968
|
+
* Build the recorder's config mirror.
|
|
10970
10969
|
*
|
|
10971
|
-
*
|
|
10972
|
-
* call site
|
|
10973
|
-
*
|
|
10974
|
-
*
|
|
10970
|
+
* `load` is the fallible store read (`loadDeviceConfig(configStore, id)`), and
|
|
10971
|
+
* every existing call site keeps its signature: the controller is handed
|
|
10972
|
+
* {@link RecordingConfigMirror.read} and cannot tell the difference, which is
|
|
10973
|
+
* the point — the trigger post-buffer is the path that got CAUGHT writing a
|
|
10974
|
+
* wrong number, but it is one of six reads and the other five are the volume.
|
|
10975
10975
|
*/
|
|
10976
|
-
function
|
|
10976
|
+
function createRecordingConfigMirror(deps) {
|
|
10977
10977
|
const now = deps.now ?? (() => Date.now());
|
|
10978
|
-
const
|
|
10979
|
-
|
|
10978
|
+
const freshMs = deps.freshMs ?? 3e4;
|
|
10979
|
+
const reportIntervalMs = deps.reportIntervalMs ?? 6e4;
|
|
10980
|
+
const mirror = /* @__PURE__ */ new Map();
|
|
10981
|
+
const window = {
|
|
10982
|
+
served: 0,
|
|
10983
|
+
storeReads: 0,
|
|
10984
|
+
fallbacks: 0,
|
|
10985
|
+
startedAt: now()
|
|
10986
|
+
};
|
|
10987
|
+
/**
|
|
10988
|
+
* One line per window, fleet-level (no `deviceId`: this aggregates every
|
|
10989
|
+
* camera, and the per-camera lines are the fallback WARNs above).
|
|
10990
|
+
*
|
|
10991
|
+
* It exists because a branch that ACCEPTS work and produces no store traffic
|
|
10992
|
+
* is indistinguishable from a branch nobody calls: without this line, a
|
|
10993
|
+
* mirror that silently stopped mirroring and a mirror doing its job both look
|
|
10994
|
+
* like silence, and the only visible difference is 1 800 calls a minute on a
|
|
10995
|
+
* runner nobody is watching.
|
|
10996
|
+
*/
|
|
10997
|
+
const reportIfDue = (atMs) => {
|
|
10998
|
+
const windowMs = atMs - window.startedAt;
|
|
10999
|
+
if (windowMs < reportIntervalMs) return;
|
|
11000
|
+
const calls = window.served + window.storeReads;
|
|
11001
|
+
window.startedAt = atMs;
|
|
11002
|
+
const served = window.served;
|
|
11003
|
+
const storeReads = window.storeReads;
|
|
11004
|
+
const fallbacks = window.fallbacks;
|
|
11005
|
+
window.served = 0;
|
|
11006
|
+
window.storeReads = 0;
|
|
11007
|
+
window.fallbacks = 0;
|
|
11008
|
+
if (calls === 0) return;
|
|
11009
|
+
deps.logger.info("recorder: config mirror window", { meta: {
|
|
11010
|
+
served,
|
|
11011
|
+
storeReads,
|
|
11012
|
+
fallbacks,
|
|
11013
|
+
devices: mirror.size,
|
|
11014
|
+
windowMs,
|
|
11015
|
+
freshMs
|
|
11016
|
+
} });
|
|
11017
|
+
};
|
|
11018
|
+
const read = async (deviceId) => {
|
|
11019
|
+
const atMs = now();
|
|
11020
|
+
reportIfDue(atMs);
|
|
11021
|
+
const held = mirror.get(deviceId);
|
|
11022
|
+
if (held !== void 0 && atMs - held.atMs < freshMs) {
|
|
11023
|
+
window.served += 1;
|
|
11024
|
+
return held.config;
|
|
11025
|
+
}
|
|
11026
|
+
window.storeReads += 1;
|
|
10980
11027
|
try {
|
|
10981
11028
|
const config = await deps.load(deviceId);
|
|
10982
|
-
|
|
11029
|
+
mirror.set(deviceId, {
|
|
10983
11030
|
config,
|
|
10984
11031
|
atMs: now()
|
|
10985
11032
|
});
|
|
10986
11033
|
return config;
|
|
10987
11034
|
} catch (err) {
|
|
10988
|
-
const cached =
|
|
11035
|
+
const cached = mirror.get(deviceId);
|
|
10989
11036
|
if (cached === void 0) {
|
|
10990
11037
|
deps.logger.warn("recorder: config read failed and NOTHING is cached for this camera — no last-known-good to serve", {
|
|
10991
11038
|
tags: { deviceId },
|
|
@@ -10993,17 +11040,29 @@ function withLastKnownConfig(deps) {
|
|
|
10993
11040
|
});
|
|
10994
11041
|
throw err;
|
|
10995
11042
|
}
|
|
10996
|
-
|
|
11043
|
+
window.fallbacks += 1;
|
|
10997
11044
|
deps.logger.warn("recorder: config read failed — serving the LAST-KNOWN-GOOD config (not the shipped default)", {
|
|
10998
11045
|
tags: { deviceId },
|
|
10999
11046
|
meta: {
|
|
11000
|
-
ageMs,
|
|
11047
|
+
ageMs: now() - cached.atMs,
|
|
11001
11048
|
error: require_dist.errMsg(err)
|
|
11002
11049
|
}
|
|
11003
11050
|
});
|
|
11004
11051
|
return cached.config;
|
|
11005
11052
|
}
|
|
11006
11053
|
};
|
|
11054
|
+
return {
|
|
11055
|
+
read,
|
|
11056
|
+
put(deviceId, config) {
|
|
11057
|
+
mirror.set(deviceId, {
|
|
11058
|
+
config,
|
|
11059
|
+
atMs: now()
|
|
11060
|
+
});
|
|
11061
|
+
},
|
|
11062
|
+
invalidate(deviceId) {
|
|
11063
|
+
mirror.delete(deviceId);
|
|
11064
|
+
}
|
|
11065
|
+
};
|
|
11007
11066
|
}
|
|
11008
11067
|
//#endregion
|
|
11009
11068
|
//#region src/recorder/addon/ledger-walk-reconcile.ts
|
|
@@ -17459,21 +17518,22 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17459
17518
|
/** Continuous + events band write-path controller (ffmpeg writers + watchers). */
|
|
17460
17519
|
controller = null;
|
|
17461
17520
|
/**
|
|
17462
|
-
* The controller's config read
|
|
17521
|
+
* The controller's config read — this addon's mirror of the rows it owns
|
|
17522
|
+
* (`config-mirror.ts`, D466), with the last-known-good fallback.
|
|
17463
17523
|
*
|
|
17464
|
-
* Built ONCE and memoized, because the
|
|
17465
|
-
*
|
|
17466
|
-
* to
|
|
17467
|
-
*
|
|
17468
|
-
* `undefined` at that point.
|
|
17524
|
+
* Built ONCE and memoized, because the map lives INSIDE it: rebuilding it per
|
|
17525
|
+
* call would throw away both the freshness the trigger path is served from
|
|
17526
|
+
* and the last value a failed read falls back to. Lazy rather than a field
|
|
17527
|
+
* initializer — a field initializer runs before the constructor assigns
|
|
17528
|
+
* `this.ctx`, so `this.ctx.logger` would be `undefined` at that point.
|
|
17469
17529
|
*/
|
|
17470
|
-
|
|
17471
|
-
get
|
|
17472
|
-
this.
|
|
17530
|
+
mirroredConfigRead = null;
|
|
17531
|
+
get configMirror() {
|
|
17532
|
+
this.mirroredConfigRead ??= createRecordingConfigMirror({
|
|
17473
17533
|
load: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
|
|
17474
17534
|
logger: this.ctx.logger
|
|
17475
17535
|
});
|
|
17476
|
-
return this.
|
|
17536
|
+
return this.mirroredConfigRead;
|
|
17477
17537
|
}
|
|
17478
17538
|
/** Unsubscribe handle for the EventCapture motion subscription. */
|
|
17479
17539
|
eventUnsub = null;
|
|
@@ -17721,7 +17781,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17721
17781
|
nodeId: this.nodeId,
|
|
17722
17782
|
ownerNodeId: ingestOwner.ownerNodeId,
|
|
17723
17783
|
segmentStore: this.segmentStore,
|
|
17724
|
-
loadConfig: this.
|
|
17784
|
+
loadConfig: this.configMirror.read,
|
|
17725
17785
|
enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
|
|
17726
17786
|
/**
|
|
17727
17787
|
* The DURABLE half of the trigger path (D8/D11). `pipelineAnalytics` rows
|
|
@@ -17860,6 +17920,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
17860
17920
|
calendar: this.calendar,
|
|
17861
17921
|
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
17862
17922
|
hydrateWindow: (deviceId, fromMs, toMs, verifyClosedHours) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger, this.segmentHours ?? void 0, { verifyClosedHours: verifyClosedHours === true }),
|
|
17923
|
+
onConfigSaved: (deviceId, config) => {
|
|
17924
|
+
this.configMirror.put(deviceId, config);
|
|
17925
|
+
},
|
|
17863
17926
|
onConfigChanged: async (deviceId) => {
|
|
17864
17927
|
await this.refreshSensorTriggerBindings();
|
|
17865
17928
|
await (this.controller?.onConfigChanged(deviceId) ?? Promise.resolve());
|