@camstack/addon-pipeline-orchestrator 1.1.49 → 1.1.51
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/Haptics-46503cea14b9857c70d67ef803f14a29.js +3 -0
- package/assets/viewer/_expo/static/js/web/index-864eb219c6810859b34768e62dd81a03.js +15997 -0
- package/assets/viewer/index.html +1 -1
- package/dist/_stub.js +2 -2
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-BEIKqOjC.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DIMFPAv-.mjs} +3 -3
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DGSDXlX8.mjs +26 -0
- package/dist/{_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CVV0TbM1.mjs → _virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bdia6t2j.mjs} +1 -1
- package/dist/{hostInit-CCmoLG4T.mjs → hostInit-IhwxrfKe.mjs} +3 -3
- package/dist/index.js +375 -44
- package/dist/index.mjs +375 -44
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/assets/viewer/_expo/static/js/web/index-5b6b7a6f9c54fd4f38efb799e5f75952.js +0 -1544
- package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D0br6Ap8.mjs +0 -26
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ node_fs = __toESM(node_fs);
|
|
|
29
29
|
let node_path = require("node:path");
|
|
30
30
|
node_path = __toESM(node_path);
|
|
31
31
|
let node_url = require("node:url");
|
|
32
|
-
//#region ../types/dist/event-category-
|
|
32
|
+
//#region ../types/dist/event-category-D4HJq7Mw.mjs
|
|
33
33
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
34
34
|
EventCategory["SystemBoot"] = "system.boot";
|
|
35
35
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -182,6 +182,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
182
182
|
/** Runner-sampled scrub thumbnail (~1/5 s/camera). Telemetry (D8): a lost
|
|
183
183
|
* thumb is a scrub gap the recorder's keyframe backfill covers. */
|
|
184
184
|
EventCategory["RecordingThumbSampled"] = "recording.thumb-sampled";
|
|
185
|
+
/** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
|
|
186
|
+
* progress bar the client reconciles via `recordingExport.getExport`. */
|
|
187
|
+
EventCategory["RecordingExportProgress"] = "recording.export.progress";
|
|
188
|
+
EventCategory["RecordingExportCompleted"] = "recording.export.completed";
|
|
189
|
+
EventCategory["RecordingExportFailed"] = "recording.export.failed";
|
|
185
190
|
EventCategory["DetectionEvent"] = "detection.event";
|
|
186
191
|
EventCategory["SessionTrackNew"] = "session.track.new";
|
|
187
192
|
EventCategory["SessionTrackExpired"] = "session.track.expired";
|
|
@@ -471,6 +476,25 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
471
476
|
*/
|
|
472
477
|
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
473
478
|
/**
|
|
479
|
+
* Fired by `addon-post-analysis` when a package-drop is confirmed inside
|
|
480
|
+
* a package zone — a newly-appeared stationary object of a package class
|
|
481
|
+
* that cleared the class / zone / dwell / size gates. Payload:
|
|
482
|
+
* `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
|
|
483
|
+
* entryId, className, zoneIds, keyFrameMediaKey?, bbox, timestamp }`.
|
|
484
|
+
* Telemetry (D8): the durable record is the `package-events` store row;
|
|
485
|
+
* this bus topic drives notifier rules + live UI. See
|
|
486
|
+
* docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
|
|
487
|
+
*/
|
|
488
|
+
EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
|
|
489
|
+
/**
|
|
490
|
+
* Fired by `addon-post-analysis` when a previously-delivered package
|
|
491
|
+
* leaves its zone (the stationary entry departed — moved or swept).
|
|
492
|
+
* Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
|
|
493
|
+
* `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
|
|
494
|
+
* Telemetry (D8). See package-zones-design §5.2.
|
|
495
|
+
*/
|
|
496
|
+
EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
|
|
497
|
+
/**
|
|
474
498
|
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
475
499
|
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
476
500
|
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
@@ -5209,6 +5233,25 @@ function _instanceof(cls, params = {}) {
|
|
|
5209
5233
|
};
|
|
5210
5234
|
return inst;
|
|
5211
5235
|
}
|
|
5236
|
+
//#endregion
|
|
5237
|
+
//#region ../../node_modules/zod/v4/classic/compat.js
|
|
5238
|
+
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
|
|
5239
|
+
var ZodIssueCode = {
|
|
5240
|
+
invalid_type: "invalid_type",
|
|
5241
|
+
too_big: "too_big",
|
|
5242
|
+
too_small: "too_small",
|
|
5243
|
+
invalid_format: "invalid_format",
|
|
5244
|
+
not_multiple_of: "not_multiple_of",
|
|
5245
|
+
unrecognized_keys: "unrecognized_keys",
|
|
5246
|
+
invalid_union: "invalid_union",
|
|
5247
|
+
invalid_key: "invalid_key",
|
|
5248
|
+
invalid_element: "invalid_element",
|
|
5249
|
+
invalid_value: "invalid_value",
|
|
5250
|
+
custom: "custom"
|
|
5251
|
+
};
|
|
5252
|
+
/** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
|
|
5253
|
+
var ZodFirstPartyTypeKind;
|
|
5254
|
+
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5212
5255
|
Object.fromEntries([
|
|
5213
5256
|
{
|
|
5214
5257
|
id: "overview",
|
|
@@ -6494,6 +6537,19 @@ function scopeKey(scope) {
|
|
|
6494
6537
|
case "device": return `device:${scope.deviceId}`;
|
|
6495
6538
|
}
|
|
6496
6539
|
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Producer transition order within ONE generation: `starting → ready →
|
|
6542
|
+
* down`. Used by `hydrate` to accept only forward moves for
|
|
6543
|
+
* same-generation snapshot records (a snapshot can be ahead of a lost
|
|
6544
|
+
* delta, never behind it, within one producer generation).
|
|
6545
|
+
*/
|
|
6546
|
+
function stateRank(state) {
|
|
6547
|
+
switch (state) {
|
|
6548
|
+
case "starting": return 0;
|
|
6549
|
+
case "ready": return 1;
|
|
6550
|
+
case "down": return 2;
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6497
6553
|
function scopesEqual(a, b) {
|
|
6498
6554
|
if (a.type !== b.type) return false;
|
|
6499
6555
|
if (a.type === "global" || b.type === "global") return true;
|
|
@@ -6544,43 +6600,70 @@ var ReadinessRegistry = class {
|
|
|
6544
6600
|
return Array.from(this.snapshot.values());
|
|
6545
6601
|
}
|
|
6546
6602
|
/**
|
|
6547
|
-
* Hydrate the snapshot from an authoritative source
|
|
6548
|
-
*
|
|
6549
|
-
*
|
|
6550
|
-
*
|
|
6551
|
-
*
|
|
6552
|
-
*
|
|
6603
|
+
* Hydrate the snapshot from an authoritative source — a RECONCILE, not
|
|
6604
|
+
* an add-only merge (D8: readiness events are telemetry and may be
|
|
6605
|
+
* lost; the on-reconnect/periodic snapshot re-pull is the repair path,
|
|
6606
|
+
* so it MUST be able to advance a stale record — the 2026-07-17
|
|
6607
|
+
* "still awaiting platform-probe forever" wedge was this method
|
|
6608
|
+
* skipping every already-seen key).
|
|
6553
6609
|
*
|
|
6554
|
-
*
|
|
6555
|
-
* derived from
|
|
6556
|
-
*
|
|
6557
|
-
*
|
|
6610
|
+
* - **Unseen keys** are added (epoch 1 — mirrors the value the
|
|
6611
|
+
* consumer would have derived from the first observed `ready`).
|
|
6612
|
+
* - **Existing keys** are UPDATED when the authoritative record
|
|
6613
|
+
* carries a different generation (producer restart / synthetic-down
|
|
6614
|
+
* superseded — epoch bumps on a new-generation `ready`, mirroring
|
|
6615
|
+
* `ingest`), or a FORWARD state move within the same generation
|
|
6616
|
+
* (`starting → ready → down`; within one producer generation a
|
|
6617
|
+
* snapshot can only be ahead of a lost delta, never behind it).
|
|
6618
|
+
* - **Never a same-generation regress** — a snapshot pulled just
|
|
6619
|
+
* before a live delta landed must not un-ready a record (the next
|
|
6620
|
+
* reconcile round converges it anyway).
|
|
6621
|
+
* - **Never a record this process is authoritative for** — node-scoped
|
|
6622
|
+
* records for our own nodeId, and device/global records we emitted,
|
|
6623
|
+
* keep local truth (an authority's stale copy of OUR record must
|
|
6624
|
+
* not regress us).
|
|
6625
|
+
*
|
|
6626
|
+
* Every applied entry dispatches a one-shot transition to matching
|
|
6627
|
+
* subscriptions so pending `awaitReady` callers unblock without having
|
|
6628
|
+
* to wait for a fresh event.
|
|
6558
6629
|
*/
|
|
6559
6630
|
hydrate(records) {
|
|
6560
6631
|
const now = this.now();
|
|
6561
6632
|
for (const record of records) {
|
|
6562
6633
|
const key = readinessKey(record.capName, record.scope);
|
|
6563
|
-
|
|
6634
|
+
const prev = this.snapshot.get(key) ?? null;
|
|
6635
|
+
let epoch;
|
|
6636
|
+
let durationInPrevState;
|
|
6637
|
+
if (prev !== null) {
|
|
6638
|
+
if (this.isLocallyAuthoritative(prev)) continue;
|
|
6639
|
+
const sameGeneration = prev.generation === record.generation;
|
|
6640
|
+
if (sameGeneration && stateRank(record.state) <= stateRank(prev.state)) continue;
|
|
6641
|
+
epoch = !sameGeneration && record.state === "ready" ? prev.epoch + 1 : prev.epoch;
|
|
6642
|
+
durationInPrevState = Math.max(0, now - prev.lastChange);
|
|
6643
|
+
} else {
|
|
6644
|
+
epoch = 1;
|
|
6645
|
+
durationInPrevState = 0;
|
|
6646
|
+
}
|
|
6564
6647
|
const hydrated = {
|
|
6565
6648
|
capName: record.capName,
|
|
6566
6649
|
scope: record.scope,
|
|
6567
6650
|
state: record.state,
|
|
6568
6651
|
generation: record.generation,
|
|
6569
|
-
epoch
|
|
6652
|
+
epoch,
|
|
6570
6653
|
lastChange: now,
|
|
6571
6654
|
sourceNodeId: record.sourceNodeId
|
|
6572
6655
|
};
|
|
6573
6656
|
this.snapshot.set(key, hydrated);
|
|
6574
|
-
if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
|
|
6657
|
+
if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated${prev !== null ? " update" : ""}, gen=${record.generation.slice(0, 6)})`);
|
|
6575
6658
|
const transition = {
|
|
6576
6659
|
capName: record.capName,
|
|
6577
6660
|
scope: record.scope,
|
|
6578
6661
|
state: record.state,
|
|
6579
|
-
epoch
|
|
6662
|
+
epoch,
|
|
6580
6663
|
generation: record.generation,
|
|
6581
6664
|
sourceNodeId: "hydrated",
|
|
6582
6665
|
ts: now,
|
|
6583
|
-
durationInPrevState
|
|
6666
|
+
durationInPrevState
|
|
6584
6667
|
};
|
|
6585
6668
|
for (const sub of this.subscriptions) {
|
|
6586
6669
|
if (sub.capName !== record.capName) continue;
|
|
@@ -6593,6 +6676,17 @@ var ReadinessRegistry = class {
|
|
|
6593
6676
|
}
|
|
6594
6677
|
}
|
|
6595
6678
|
}
|
|
6679
|
+
/**
|
|
6680
|
+
* True when THIS process is the origin authority for `record` — a
|
|
6681
|
+
* snapshot pulled from elsewhere must never overwrite it:
|
|
6682
|
+
* - node-scoped records whose `scope.nodeId` is our own node id
|
|
6683
|
+
* (this process — or a child bridged onto our bus — emits them);
|
|
6684
|
+
* - device/global-scoped records whose latest transition WE emitted.
|
|
6685
|
+
*/
|
|
6686
|
+
isLocallyAuthoritative(record) {
|
|
6687
|
+
if (record.scope.type === "node") return record.scope.nodeId === this.sourceNodeId;
|
|
6688
|
+
return record.sourceNodeId === this.sourceNodeId;
|
|
6689
|
+
}
|
|
6596
6690
|
/** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
|
|
6597
6691
|
getAll() {
|
|
6598
6692
|
return new Map(this.snapshot);
|
|
@@ -7679,6 +7773,24 @@ var RecordingRetentionSchema = object({
|
|
|
7679
7773
|
maxSizeGb: number().min(0).optional()
|
|
7680
7774
|
});
|
|
7681
7775
|
/**
|
|
7776
|
+
* Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
|
|
7777
|
+
* sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
|
|
7778
|
+
* previews at. Five graduated steps; absent on a config = `standard` (the
|
|
7779
|
+
* shipped default, matching `sheet-geometry`/`sheet-composer`).
|
|
7780
|
+
*
|
|
7781
|
+
* Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
|
|
7782
|
+
* Each window's index sidecar carries its own tile dims, so a camera whose
|
|
7783
|
+
* preset changed over time renders every historical window at the dims it was
|
|
7784
|
+
* written with.
|
|
7785
|
+
*/
|
|
7786
|
+
var ScrubThumbnailPresetSchema = _enum([
|
|
7787
|
+
"minimal",
|
|
7788
|
+
"low",
|
|
7789
|
+
"standard",
|
|
7790
|
+
"high",
|
|
7791
|
+
"max"
|
|
7792
|
+
]);
|
|
7793
|
+
/**
|
|
7682
7794
|
* The full per-camera recording intent — the wire shape of a RecordingTarget.
|
|
7683
7795
|
*
|
|
7684
7796
|
* `mode` is the authoritative storage choice; `schedule`/`triggers`/`pre`/`post`
|
|
@@ -7715,7 +7827,14 @@ var RecordingConfigSchema = object({
|
|
|
7715
7827
|
* derived into bands once via `migrateConfigToBands`.
|
|
7716
7828
|
*/
|
|
7717
7829
|
bands: array(RecordingBandSchema).optional(),
|
|
7718
|
-
retention: RecordingRetentionSchema.optional()
|
|
7830
|
+
retention: RecordingRetentionSchema.optional(),
|
|
7831
|
+
/**
|
|
7832
|
+
* Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
|
|
7833
|
+
* timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
|
|
7834
|
+
* windows only — existing sheets are immutable, and each window's index
|
|
7835
|
+
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7836
|
+
*/
|
|
7837
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7719
7838
|
});
|
|
7720
7839
|
/**
|
|
7721
7840
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
@@ -13348,7 +13467,11 @@ array(ZoneRuleSchema).readonly();
|
|
|
13348
13467
|
* Extend the enum here when a new gating consumer comes online (audio
|
|
13349
13468
|
* gating, alert filtering, …) — no other surface needs to change.
|
|
13350
13469
|
*/
|
|
13351
|
-
var ZoneRuleStageEnum = _enum([
|
|
13470
|
+
var ZoneRuleStageEnum = _enum([
|
|
13471
|
+
"motion",
|
|
13472
|
+
"detection",
|
|
13473
|
+
"package"
|
|
13474
|
+
]);
|
|
13352
13475
|
/**
|
|
13353
13476
|
* Zone rules capability — per-camera CRUD over the {@link ZoneRule}
|
|
13354
13477
|
* arrays that decide how each pipeline stage uses the polygon zones.
|
|
@@ -13392,16 +13515,24 @@ var zoneRulesCapability = {
|
|
|
13392
13515
|
})
|
|
13393
13516
|
},
|
|
13394
13517
|
/**
|
|
13395
|
-
* Runtime-state slice —
|
|
13518
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
13396
13519
|
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
13397
|
-
* of
|
|
13398
|
-
* `{motion, detection}` shape, so subscribers always get the
|
|
13520
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
13521
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
13399
13522
|
* complete current set. Consumers that only care about one stage
|
|
13400
13523
|
* just read the matching property.
|
|
13524
|
+
*
|
|
13525
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
13526
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
13527
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
13528
|
+
* The orchestrator provider that writes this stage lands in a later
|
|
13529
|
+
* slice; until then the mirror carries only `{motion, detection}` and
|
|
13530
|
+
* consumers read `package` as absent (treat as `[]`).
|
|
13401
13531
|
*/
|
|
13402
13532
|
runtimeState: object({
|
|
13403
13533
|
motion: array(ZoneRuleSchema).readonly(),
|
|
13404
|
-
detection: array(ZoneRuleSchema).readonly()
|
|
13534
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
13535
|
+
package: array(ZoneRuleSchema).readonly()
|
|
13405
13536
|
})
|
|
13406
13537
|
};
|
|
13407
13538
|
var ProviderStatusSchema = object({
|
|
@@ -16650,6 +16781,7 @@ var EventKindIconSchema = _enum([
|
|
|
16650
16781
|
"smoke",
|
|
16651
16782
|
"water",
|
|
16652
16783
|
"button",
|
|
16784
|
+
"package",
|
|
16653
16785
|
"generic"
|
|
16654
16786
|
]);
|
|
16655
16787
|
var EventKindCategorySchema = _enum([
|
|
@@ -16657,7 +16789,8 @@ var EventKindCategorySchema = _enum([
|
|
|
16657
16789
|
"audio",
|
|
16658
16790
|
"detection",
|
|
16659
16791
|
"sensor",
|
|
16660
|
-
"custom"
|
|
16792
|
+
"custom",
|
|
16793
|
+
"package"
|
|
16661
16794
|
]);
|
|
16662
16795
|
var EventKindDescriptorSchema = object({
|
|
16663
16796
|
/** Stable kind id (e.g. 'motion', 'person', 'contact'). */
|
|
@@ -20514,6 +20647,108 @@ method(object({
|
|
|
20514
20647
|
auth: "admin"
|
|
20515
20648
|
});
|
|
20516
20649
|
/**
|
|
20650
|
+
* `recordingExport` cap — render a footage time range into a single downloadable
|
|
20651
|
+
* MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
|
|
20652
|
+
* bounded lifetime with a durable history, auto-expiry, and optional
|
|
20653
|
+
* delete-after-download.
|
|
20654
|
+
*
|
|
20655
|
+
* Like `recording` this is a `scope:'system', mode:'singleton'` cap: the
|
|
20656
|
+
* recorder self-gates so exactly ONE node (the designated `recordingNodeId`,
|
|
20657
|
+
* default `hub`) registers it. Device-scoped methods carry `deviceId` in their
|
|
20658
|
+
* input and dispatch to that single provider; the render runs on the node that
|
|
20659
|
+
* owns the footage (no cross-node segment transfer). Download rides the
|
|
20660
|
+
* framework addon data-plane. State persists in a DurableState blob (NOT
|
|
20661
|
+
* SQLite); history rows survive file deletion for audit.
|
|
20662
|
+
*/
|
|
20663
|
+
/** Playback-speed multiplier for the render (1 = realtime). */
|
|
20664
|
+
var ExportSpeedSchema = number().min(.25).max(32);
|
|
20665
|
+
/** Timelapse cadence — sample one source frame per `everyMs`, output at `outputFps`. */
|
|
20666
|
+
var ExportTimelapseSchema = object({
|
|
20667
|
+
everyMs: number().int().positive(),
|
|
20668
|
+
outputFps: number().int().min(1).max(60).optional()
|
|
20669
|
+
});
|
|
20670
|
+
/**
|
|
20671
|
+
* Render options. `speed` and `timelapse` are mutually exclusive. `includeAudio`
|
|
20672
|
+
* is honoured only for a realtime-ish speed (0.5–2×); timelapse is always
|
|
20673
|
+
* silent. `maxLifeMs` bounds how long the finished file is kept;
|
|
20674
|
+
* `deleteAfterDownload` removes it shortly after the first complete download.
|
|
20675
|
+
*/
|
|
20676
|
+
var ExportOptionsSchema = object({
|
|
20677
|
+
speed: ExportSpeedSchema.optional(),
|
|
20678
|
+
timelapse: ExportTimelapseSchema.optional(),
|
|
20679
|
+
includeAudio: boolean(),
|
|
20680
|
+
maxLifeMs: number().int().positive(),
|
|
20681
|
+
deleteAfterDownload: boolean(),
|
|
20682
|
+
title: string().max(200).optional()
|
|
20683
|
+
}).superRefine((v, ctx) => {
|
|
20684
|
+
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
20685
|
+
code: ZodIssueCode.custom,
|
|
20686
|
+
message: "speed and timelapse are mutually exclusive",
|
|
20687
|
+
path: ["timelapse"]
|
|
20688
|
+
});
|
|
20689
|
+
});
|
|
20690
|
+
var ExportStateSchema = _enum([
|
|
20691
|
+
"queued",
|
|
20692
|
+
"rendering",
|
|
20693
|
+
"ready",
|
|
20694
|
+
"failed",
|
|
20695
|
+
"expired",
|
|
20696
|
+
"deleted"
|
|
20697
|
+
]);
|
|
20698
|
+
/** One export job / history row. */
|
|
20699
|
+
var ExportRecordSchema = object({
|
|
20700
|
+
id: string(),
|
|
20701
|
+
deviceId: number(),
|
|
20702
|
+
profile: string(),
|
|
20703
|
+
fromMs: number(),
|
|
20704
|
+
toMs: number(),
|
|
20705
|
+
options: ExportOptionsSchema,
|
|
20706
|
+
state: ExportStateSchema,
|
|
20707
|
+
/** 0–100 while rendering; null otherwise. */
|
|
20708
|
+
progressPct: number().nullable(),
|
|
20709
|
+
/** File size once ready; null before. */
|
|
20710
|
+
fileBytes: number().nullable(),
|
|
20711
|
+
expiresAt: number(),
|
|
20712
|
+
deleteAfterDownload: boolean(),
|
|
20713
|
+
/** Epoch of the first complete download; null until then. */
|
|
20714
|
+
downloadedAt: number().nullable(),
|
|
20715
|
+
createdAt: number(),
|
|
20716
|
+
/** User id/name that requested the export. */
|
|
20717
|
+
createdBy: string(),
|
|
20718
|
+
/** Failure reason when state is 'failed'; null otherwise. */
|
|
20719
|
+
error: string().nullable()
|
|
20720
|
+
});
|
|
20721
|
+
/** Candidate download URLs (LAN first, then operator extra hosts). */
|
|
20722
|
+
var ExportDownloadSchema = object({
|
|
20723
|
+
url: string(),
|
|
20724
|
+
endpoints: array(string())
|
|
20725
|
+
});
|
|
20726
|
+
method(object({
|
|
20727
|
+
deviceId: number(),
|
|
20728
|
+
profile: string(),
|
|
20729
|
+
fromMs: number(),
|
|
20730
|
+
toMs: number(),
|
|
20731
|
+
options: ExportOptionsSchema
|
|
20732
|
+
}), ExportRecordSchema, {
|
|
20733
|
+
kind: "mutation",
|
|
20734
|
+
auth: "protected"
|
|
20735
|
+
}), method(object({ deviceId: number().optional() }), array(ExportRecordSchema), {
|
|
20736
|
+
kind: "query",
|
|
20737
|
+
auth: "protected"
|
|
20738
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
20739
|
+
kind: "query",
|
|
20740
|
+
auth: "protected"
|
|
20741
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
20742
|
+
kind: "mutation",
|
|
20743
|
+
auth: "protected"
|
|
20744
|
+
}), method(object({ exportId: string() }), ExportRecordSchema, {
|
|
20745
|
+
kind: "mutation",
|
|
20746
|
+
auth: "protected"
|
|
20747
|
+
}), method(object({ exportId: string() }), ExportDownloadSchema, {
|
|
20748
|
+
kind: "query",
|
|
20749
|
+
auth: "protected"
|
|
20750
|
+
});
|
|
20751
|
+
/**
|
|
20517
20752
|
* One publishable camera stream as its OWNING PROVIDER describes it — the same
|
|
20518
20753
|
* payload the (legacy) push `streamBroker.publishCameraStream` carried, minus
|
|
20519
20754
|
* `deviceId`.
|
|
@@ -24437,6 +24672,42 @@ Object.freeze({
|
|
|
24437
24672
|
addonId: null,
|
|
24438
24673
|
access: "create"
|
|
24439
24674
|
},
|
|
24675
|
+
"recordingExport.cancelExport": {
|
|
24676
|
+
capName: "recordingExport",
|
|
24677
|
+
capScope: "system",
|
|
24678
|
+
addonId: null,
|
|
24679
|
+
access: "create"
|
|
24680
|
+
},
|
|
24681
|
+
"recordingExport.createExport": {
|
|
24682
|
+
capName: "recordingExport",
|
|
24683
|
+
capScope: "system",
|
|
24684
|
+
addonId: null,
|
|
24685
|
+
access: "create"
|
|
24686
|
+
},
|
|
24687
|
+
"recordingExport.deleteExport": {
|
|
24688
|
+
capName: "recordingExport",
|
|
24689
|
+
capScope: "system",
|
|
24690
|
+
addonId: null,
|
|
24691
|
+
access: "delete"
|
|
24692
|
+
},
|
|
24693
|
+
"recordingExport.getDownloadUrl": {
|
|
24694
|
+
capName: "recordingExport",
|
|
24695
|
+
capScope: "system",
|
|
24696
|
+
addonId: null,
|
|
24697
|
+
access: "view"
|
|
24698
|
+
},
|
|
24699
|
+
"recordingExport.getExport": {
|
|
24700
|
+
capName: "recordingExport",
|
|
24701
|
+
capScope: "system",
|
|
24702
|
+
addonId: null,
|
|
24703
|
+
access: "view"
|
|
24704
|
+
},
|
|
24705
|
+
"recordingExport.listExports": {
|
|
24706
|
+
capName: "recordingExport",
|
|
24707
|
+
capScope: "system",
|
|
24708
|
+
addonId: null,
|
|
24709
|
+
access: "view"
|
|
24710
|
+
},
|
|
24440
24711
|
"sceneMonitor.captureReference": {
|
|
24441
24712
|
capName: "scene-monitor",
|
|
24442
24713
|
capScope: "device",
|
|
@@ -30518,6 +30789,27 @@ var PIPELINE_EXECUTOR_CAP = "pipeline-executor";
|
|
|
30518
30789
|
var PipelineSettingsStore = class PipelineSettingsStore {
|
|
30519
30790
|
deps;
|
|
30520
30791
|
static CATALOG_CACHE_TTL_MS = 3e4;
|
|
30792
|
+
/**
|
|
30793
|
+
* One reconcile round for {@link awaitExecutorCatalog}: the BOUNDED
|
|
30794
|
+
* registry wait between two authoritative provider probes. Matches the
|
|
30795
|
+
* registry's own "still awaiting" diagnostic cadence.
|
|
30796
|
+
*/
|
|
30797
|
+
static EXECUTOR_RECONCILE_ROUND_MS = 3e4;
|
|
30798
|
+
/**
|
|
30799
|
+
* Pacing between provider probes when the registry claims `ready` but
|
|
30800
|
+
* the provider is not answering yet (stale registry record) — without
|
|
30801
|
+
* it that combination would spin hot.
|
|
30802
|
+
*/
|
|
30803
|
+
static EXECUTOR_READY_PROBE_PACE_MS = 2e3;
|
|
30804
|
+
/**
|
|
30805
|
+
* Round bound for {@link seedAgentSettingsFromCatalog}: seeding is
|
|
30806
|
+
* invoked from event handlers (`handleDetectionPipelineReadiness`) that
|
|
30807
|
+
* must not accumulate unbounded loops across provider flaps — on
|
|
30808
|
+
* exhaustion it returns `null` and the periodic pending-retry sweep /
|
|
30809
|
+
* the next readiness epoch retries. `waitForAgentAndCatalog` keeps the
|
|
30810
|
+
* UNBOUNDED reconcile (cameras are never dispatched with empty steps).
|
|
30811
|
+
*/
|
|
30812
|
+
static SEED_MAX_RECONCILE_ROUNDS = 4;
|
|
30521
30813
|
constructor(deps) {
|
|
30522
30814
|
this.deps = deps;
|
|
30523
30815
|
}
|
|
@@ -30784,11 +31076,7 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
30784
31076
|
* absent.
|
|
30785
31077
|
*/
|
|
30786
31078
|
async seedAgentSettingsFromCatalog(nodeId) {
|
|
30787
|
-
await this.
|
|
30788
|
-
type: "node",
|
|
30789
|
-
nodeId
|
|
30790
|
-
});
|
|
30791
|
-
const catalog = await this.getCatalogForAgent(nodeId);
|
|
31079
|
+
const catalog = await this.awaitExecutorCatalog(nodeId, { maxRounds: PipelineSettingsStore.SEED_MAX_RECONCILE_ROUNDS });
|
|
30792
31080
|
if (!catalog) return null;
|
|
30793
31081
|
const existing = (await this.readAgentSettingsMap())[nodeId];
|
|
30794
31082
|
const nextAddonDefaults = seedAgentAddonDefaults(existing?.addonDefaults ?? {}, catalog);
|
|
@@ -30892,15 +31180,64 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
30892
31180
|
return resolved;
|
|
30893
31181
|
}
|
|
30894
31182
|
/**
|
|
30895
|
-
*
|
|
31183
|
+
* Loss-proof `pipeline-executor` gate (D8 — reconcile against the
|
|
31184
|
+
* authority, never wait solely on an event).
|
|
31185
|
+
*
|
|
31186
|
+
* Readiness events are telemetry and MAY be lost across process/node
|
|
31187
|
+
* hops (2026-07-17 incident: the agent's executor came back callable
|
|
31188
|
+
* after an update, its `ready` delta never reached the hub registry,
|
|
31189
|
+
* and the former `acquireCapability(…, Infinity)` gate wedged every
|
|
31190
|
+
* dispatch forever). Each round of this loop therefore:
|
|
31191
|
+
*
|
|
31192
|
+
* 1. Probes the PROVIDER itself — `getCatalogForAgent` is a live
|
|
31193
|
+
* `pipelineExecutor.getSchema` RPC routed to `nodeId` (30s success
|
|
31194
|
+
* cache). The provider answering IS readiness, whatever the local
|
|
31195
|
+
* registry believes.
|
|
31196
|
+
* 2. On a miss, falls back to a BOUNDED registry wait
|
|
31197
|
+
* (`EXECUTOR_RECONCILE_ROUND_MS`) — the event-driven fast path that
|
|
31198
|
+
* keeps the healthy cold-boot case latency-free (the wait resolves
|
|
31199
|
+
* the instant the `ready` delta lands). A lost event costs at most
|
|
31200
|
+
* one round, never forever.
|
|
31201
|
+
*
|
|
31202
|
+
* If the registry claims `ready` but the probe still misses (stale
|
|
31203
|
+
* record for a dead provider), rounds pace at
|
|
31204
|
+
* `EXECUTOR_READY_PROBE_PACE_MS` instead of spinning hot.
|
|
31205
|
+
*
|
|
31206
|
+
* Returns `null` only when `maxRounds` is exhausted (bounded callers)
|
|
31207
|
+
* or the store was disposed mid-wait.
|
|
31208
|
+
*/
|
|
31209
|
+
async awaitExecutorCatalog(nodeId, opts = {}) {
|
|
31210
|
+
const maxRounds = opts.maxRounds ?? Number.POSITIVE_INFINITY;
|
|
31211
|
+
const startedAt = Date.now();
|
|
31212
|
+
for (let round = 1; !this.disposed; round++) {
|
|
31213
|
+
const catalog = await this.getCatalogForAgent(nodeId);
|
|
31214
|
+
if (catalog) return catalog;
|
|
31215
|
+
if (round >= maxRounds) return null;
|
|
31216
|
+
if (round > 1) this.deps.logger.warn("executor reconcile: provider not answering yet — waiting one more round", {
|
|
31217
|
+
tags: { nodeId },
|
|
31218
|
+
meta: {
|
|
31219
|
+
round,
|
|
31220
|
+
elapsedMs: Date.now() - startedAt
|
|
31221
|
+
}
|
|
31222
|
+
});
|
|
31223
|
+
const readyPerRegistry = await this.deps.acquireCapability(PIPELINE_EXECUTOR_CAP, {
|
|
31224
|
+
type: "node",
|
|
31225
|
+
nodeId
|
|
31226
|
+
}, { timeoutMs: PipelineSettingsStore.EXECUTOR_RECONCILE_ROUND_MS }).then(() => true, () => false);
|
|
31227
|
+
if (this.disposed) break;
|
|
31228
|
+
if (readyPerRegistry) await sleep$1(PipelineSettingsStore.EXECUTOR_READY_PROBE_PACE_MS);
|
|
31229
|
+
}
|
|
31230
|
+
return null;
|
|
31231
|
+
}
|
|
31232
|
+
/**
|
|
31233
|
+
* Block until `pipeline-executor` answers on `nodeId` AND
|
|
30896
31234
|
* `getCatalogForAgent` returns a non-null catalog AND `agentSettings`
|
|
30897
31235
|
* for that node has populated `addonDefaults`.
|
|
30898
31236
|
*
|
|
30899
|
-
*
|
|
30900
|
-
*
|
|
30901
|
-
*
|
|
30902
|
-
*
|
|
30903
|
-
* propagation lag.
|
|
31237
|
+
* The executor gate is {@link awaitExecutorCatalog} — reconcile-driven
|
|
31238
|
+
* and UNBOUNDED here (cameras must never be dispatched with empty
|
|
31239
|
+
* steps), but it converges as soon as the provider answers even when
|
|
31240
|
+
* every readiness event was lost.
|
|
30904
31241
|
*
|
|
30905
31242
|
* Smell #20 fix: the retry loop used to be `while (true)` with no exit
|
|
30906
31243
|
* — a resolve in flight at addon shutdown would spin against a torn-
|
|
@@ -30910,16 +31247,9 @@ var PipelineSettingsStore = class PipelineSettingsStore {
|
|
|
30910
31247
|
* timing (2s backoff, same retry conditions) is unchanged.
|
|
30911
31248
|
*/
|
|
30912
31249
|
async waitForAgentAndCatalog(nodeId) {
|
|
30913
|
-
await this.deps.acquireCapability(PIPELINE_EXECUTOR_CAP, {
|
|
30914
|
-
type: "node",
|
|
30915
|
-
nodeId
|
|
30916
|
-
});
|
|
30917
31250
|
while (!this.disposed) {
|
|
30918
|
-
const catalog = await this.
|
|
30919
|
-
if (!catalog)
|
|
30920
|
-
await sleep$1(2e3);
|
|
30921
|
-
continue;
|
|
30922
|
-
}
|
|
31251
|
+
const catalog = await this.awaitExecutorCatalog(nodeId);
|
|
31252
|
+
if (!catalog) break;
|
|
30923
31253
|
let agent = (await this.readAgentSettingsMap())[nodeId];
|
|
30924
31254
|
if (!agent || Object.keys(agent.addonDefaults ?? {}).length === 0) {
|
|
30925
31255
|
if (!await this.seedAgentSettingsFromCatalog(nodeId)) {
|
|
@@ -31945,7 +32275,8 @@ var RulesArraySchema = array(ZoneRuleSchema);
|
|
|
31945
32275
|
*/
|
|
31946
32276
|
var ZoneRulesBlockSchema = object({
|
|
31947
32277
|
motion: unknown().optional(),
|
|
31948
|
-
detection: unknown().optional()
|
|
32278
|
+
detection: unknown().optional(),
|
|
32279
|
+
package: unknown().optional()
|
|
31949
32280
|
}).passthrough();
|
|
31950
32281
|
var ZoneRulesProvider = class {
|
|
31951
32282
|
ctx;
|