@camstack/addon-osd-manager 0.1.21 → 0.1.23

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.
Files changed (18) hide show
  1. package/dist/{MotionZonesSettings-C1tuU9vA.mjs → MotionZonesSettings-DhYwaRz3.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-BfYwmnx-.mjs → PrivacyMaskSettings-BquvldBM.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-Fui6aPeX.mjs → SceneMonitorEditor-wK0ulSnf.mjs} +3 -3
  4. package/dist/_stub.js +10 -10
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CUYUIYix.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CWCOPnMZ.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-6_-m0FOQ.mjs +26 -0
  7. package/dist/{hostInit-wPjKQ8mT.mjs → hostInit-BUTpXXH-.mjs} +3 -3
  8. package/dist/index.js +1807 -1321
  9. package/dist/index.mjs +1807 -1321
  10. package/dist/{player-overlays-B_C4YyEi.mjs → player-overlays-CS4sZzYZ.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-BzwsIYSh.mjs → responsive-VdLah82L.mjs} +1 -1
  13. package/dist/{square-X_zDrz0l.mjs → square-a4rMgWr9.mjs} +1 -1
  14. package/dist/{trash-2-BldQMEFN.mjs → trash-2-BCaHwGDI.mjs} +1 -1
  15. package/dist/{use-device-snapshot-Bxo1wH26.mjs → use-device-snapshot-CTWHDX3F.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-DZH4YPzH.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-C7MX67d_.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BhABwRxy.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -6629,7 +6629,7 @@ function method(input, output, options) {
6629
6629
  input,
6630
6630
  output,
6631
6631
  kind: options?.kind ?? "query",
6632
- auth: options?.auth ?? "protected",
6632
+ ...options?.auth !== void 0 ? { auth: options.auth } : {},
6633
6633
  ...options?.access !== void 0 ? { access: options.access } : {},
6634
6634
  ...options?.caller !== void 0 ? { caller: options.caller } : {},
6635
6635
  timeoutMs: options?.timeoutMs
@@ -6659,8 +6659,17 @@ var adminUiCapability = {
6659
6659
  mode: "singleton",
6660
6660
  internal: true,
6661
6661
  methods: {
6662
- getStaticDir: method(_void(), StaticDirOutputSchema$1),
6663
- getVersion: method(_void(), VersionOutputSchema$1)
6662
+ /**
6663
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
6664
+ * `data-store-provider`) — both methods were reachable on the AppRouter
6665
+ * by ANY authenticated session at the default `auth: 'protected'`, and
6666
+ * `getStaticDir` leaks a hub filesystem path. The only caller is
6667
+ * `main.ts`'s static-file bootstrap, via `capRegistry.getSingletonForNode`
6668
+ * — never tRPC — so `auth: 'admin'` costs it nothing. The actual admin-ui
6669
+ * SPA assets are served as plain static files, unaffected by this gate.
6670
+ */
6671
+ getStaticDir: method(_void(), StaticDirOutputSchema$1, { auth: "admin" }),
6672
+ getVersion: method(_void(), VersionOutputSchema$1, { auth: "admin" })
6664
6673
  }
6665
6674
  };
6666
6675
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
@@ -6996,8 +7005,23 @@ var viewerUiCapability = {
6996
7005
  mode: "singleton",
6997
7006
  internal: true,
6998
7007
  methods: {
6999
- getStaticDir: method(_void(), StaticDirOutputSchema),
7000
- getVersion: method(_void(), VersionOutputSchema)
7008
+ /**
7009
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
7010
+ * `data-store-provider`) — both methods were reachable on the AppRouter
7011
+ * by ANY authenticated session at the default `auth: 'protected'`, and
7012
+ * `getStaticDir` leaks a hub filesystem path.
7013
+ *
7014
+ * ⚠ Checked before gating: the VIEWER app IS used by a non-admin user
7015
+ * (`roberta`), but it never calls `viewerUi.*` over tRPC. The static SPA
7016
+ * it loads is served by the plain `GET /viewer/*` Fastify route in
7017
+ * `main.ts`, which reads `staticDir`/`indexPath` from a module-level
7018
+ * cache populated ONCE at boot via `capRegistry.getSingletonForNode`
7019
+ * (in-process, not tRPC) — the cap's tRPC procedure is never on that
7020
+ * request path. `auth: 'admin'` here does not block a single viewer
7021
+ * user from loading the app.
7022
+ */
7023
+ getStaticDir: method(_void(), StaticDirOutputSchema, { auth: "admin" }),
7024
+ getVersion: method(_void(), VersionOutputSchema, { auth: "admin" })
7001
7025
  }
7002
7026
  };
7003
7027
  /**
@@ -7672,24 +7696,6 @@ var RecordingRetentionSchema = object({
7672
7696
  maxSizeGb: number().min(0).optional()
7673
7697
  });
7674
7698
  /**
7675
- * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
7676
- * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
7677
- * previews at. Five graduated steps; absent on a config = `standard` (the
7678
- * shipped default, matching `sheet-geometry`/`sheet-composer`).
7679
- *
7680
- * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
7681
- * Each window's index sidecar carries its own tile dims, so a camera whose
7682
- * preset changed over time renders every historical window at the dims it was
7683
- * written with.
7684
- */
7685
- var ScrubThumbnailPresetSchema = _enum([
7686
- "minimal",
7687
- "low",
7688
- "standard",
7689
- "high",
7690
- "max"
7691
- ]);
7692
- /**
7693
7699
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
7694
7700
  *
7695
7701
  * `bands` is the ONLY authored recording intent: what to record, when, and on
@@ -7697,7 +7703,11 @@ var ScrubThumbnailPresetSchema = _enum([
7697
7703
  * other field is a storage knob (profiles, segment length, retention, scrub).
7698
7704
  *
7699
7705
  * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
7700
- * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
7706
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30,
7707
+ * and `scrubThumbnails` — a five-step fidelity knob for a sprite tier that was
7708
+ * deleted on 2026-07-24 and had ZERO consumers in the recorder — on 2026-08-25
7709
+ * (D62: a switch that writes a store nobody reads is worse than no switch).
7710
+ * Stored rows keep loading: the READ schema is `.strip()` (config-store.ts).
7701
7711
  * A stale caller must fail loudly — silently stripping its legacy intent would
7702
7712
  * persist a band-less config, i.e. silently stop recording the camera.
7703
7713
  */
@@ -7720,14 +7730,7 @@ var RecordingConfigSchema = object({
7720
7730
  * "off" is the absence of a covering band, never a band value.
7721
7731
  */
7722
7732
  bands: array(RecordingBandSchema).default([]),
7723
- retention: RecordingRetentionSchema.optional(),
7724
- /**
7725
- * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
7726
- * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
7727
- * windows only — existing sheets are immutable, and each window's index
7728
- * carries its own tile dims so mixed-preset history renders correctly.
7729
- */
7730
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7733
+ retention: RecordingRetentionSchema.optional()
7731
7734
  }).strict();
7732
7735
  /**
7733
7736
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
@@ -7803,10 +7806,11 @@ var RelocateFootageInputSchema = object({
7803
7806
  * `RecordingConfig.enabled` or camera wrapper bindings. */
7804
7807
  var StorageMigrationLeaseInputSchema = object({ leaseId: string().min(1) });
7805
7808
  var StorageMigrationFootageMoveInputSchema = RelocateFootageInputSchema.extend({ leaseId: string().min(1) });
7806
- var StorageMigrationMediaMoveInputSchema = object({
7809
+ var RelocateMediaInputSchema = object({
7807
7810
  toLocationId: string(),
7808
7811
  throttleMbps: number().min(1).max(1e3).optional()
7809
- }).extend({ leaseId: string().min(1) });
7812
+ });
7813
+ var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
7810
7814
  /** The independently selectable logical storage classes. `recordings`
7811
7815
  * encompasses the high and mid segment profiles; `recordingsLow` is low
7812
7816
  * segments; `eventMedia` is post-analysis blobs. */
@@ -8101,7 +8105,26 @@ var LabelDefinitionSchema = object({
8101
8105
  description: string().optional(),
8102
8106
  icon: string().optional()
8103
8107
  });
8104
- var ClassMapDefinitionSchema = object({
8108
+ /**
8109
+ * Wire schema for a per-model CATALOG classMap override
8110
+ * (`ModelCatalogEntry.classMap` / `ModelConvertMetadata.classMap`) —
8111
+ * restricted to {@link CLASS_MAP_MACRO_TARGETS}, the only macros the
8112
+ * detection pipeline executor actually routes.
8113
+ *
8114
+ * This is deliberately a DIFFERENT, narrower shape than the general-purpose
8115
+ * `ClassMapDefinition` interface above (e.g. `IDetectionAddon.getClassMap()`
8116
+ * and the audio `YAMNET_TO_MACRO` catalog both use macro targets outside this
8117
+ * enum) — the two used to share the name `ClassMapDefinition`/
8118
+ * `ClassMapDefinitionSchema`, which made the schema-type-twin guard
8119
+ * (`scripts/check-schema-type-twins.ts`) flag them as a duplicated shape. They
8120
+ * are not: it is two different concepts colliding on a name. Keep this type
8121
+ * under its own name rather than reusing `ClassMapDefinition` — reusing it
8122
+ * would either narrow every `ClassMapDefinition` consumer to the four
8123
+ * detection macros (breaking `YAMNET_TO_MACRO`) or drop the validation this
8124
+ * schema exists for (see the "rejects a classMap whose target is not a
8125
+ * detection macro" test in `model-catalog-schema.test.ts`).
8126
+ */
8127
+ var DetectionCatalogClassMapSchema = object({
8105
8128
  mapping: record(string(), _enum([
8106
8129
  "person",
8107
8130
  "vehicle",
@@ -8306,7 +8329,7 @@ var ModelCatalogEntrySchema = object({
8306
8329
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8307
8330
  * labels already ARE the CamStack macros (Scrypted identity map).
8308
8331
  */
8309
- classMap: ClassMapDefinitionSchema.optional()
8332
+ classMap: DetectionCatalogClassMapSchema.optional()
8310
8333
  });
8311
8334
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8312
8335
  format: literal("openvino"),
@@ -8336,7 +8359,7 @@ var ModelConvertMetadataSchema = object({
8336
8359
  "segmentation"
8337
8360
  ]),
8338
8361
  faceAlignment: boolean().optional(),
8339
- classMap: ClassMapDefinitionSchema.optional()
8362
+ classMap: DetectionCatalogClassMapSchema.optional()
8340
8363
  });
8341
8364
  var ConvertResultSchema = object({
8342
8365
  entry: ModelCatalogEntrySchema,
@@ -9209,7 +9232,18 @@ var addonPagesSourceCapability = {
9209
9232
  scope: "system",
9210
9233
  mode: "collection",
9211
9234
  internal: true,
9212
- methods: { listPages: method(_void(), array(AddonPageDeclarationSchema).readonly()) }
9235
+ methods: {
9236
+ /**
9237
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
9238
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
9239
+ * authenticated session at the default `auth: 'protected'`. The only
9240
+ * consumer, the `addon-pages-aggregator` builtin, reads it via
9241
+ * `ctx.capabilities.getCollection('addon-pages-source')` — LOCAL-PROCESS
9242
+ * only, never tRPC (per `ctx.capabilities` seeing only the local
9243
+ * process). The public, enriched listing admin-ui actually consumes is
9244
+ * the separate `addon-pages` cap, unaffected by this change.
9245
+ */
9246
+ listPages: method(_void(), array(AddonPageDeclarationSchema).readonly(), { auth: "admin" }) }
9213
9247
  };
9214
9248
  var AddonHttpRouteSchema = object({
9215
9249
  method: _enum([
@@ -9506,7 +9540,20 @@ var addonWidgetsSourceCapability = {
9506
9540
  scope: "system",
9507
9541
  mode: "collection",
9508
9542
  internal: true,
9509
- methods: { listWidgets: method(_void(), array(WidgetMetadataSchema).readonly()) }
9543
+ methods: {
9544
+ /**
9545
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
9546
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
9547
+ * authenticated session at the default `auth: 'protected'`. The only
9548
+ * consumer, the `addon-widgets-aggregator` builtin, reads it via
9549
+ * `ctx.capabilities.getCollection('addon-widgets-source')` — LOCAL-PROCESS
9550
+ * only, never tRPC. The public, enriched listing admin-ui actually
9551
+ * consumes is the separate `addon-widgets` cap (`listWidgets` there is
9552
+ * gated separately, and a `preAuth: true` widget is surfaced through the
9553
+ * PUBLIC `auth.listLoginMethods` contribution channel instead) —
9554
+ * neither is affected by this change.
9555
+ */
9556
+ listWidgets: method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" }) }
9510
9557
  };
9511
9558
  /**
9512
9559
  * `addon-widgets` — system-scoped singleton aggregator cap. Public-facing
@@ -11670,7 +11717,17 @@ var customModelRegistryCapability = {
11670
11717
  scope: "system",
11671
11718
  mode: "collection",
11672
11719
  internal: true,
11673
- methods: { listModels: method(_void(), array(CustomModelDescriptorSchema).readonly()) }
11720
+ methods: {
11721
+ /**
11722
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
11723
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
11724
+ * authenticated session at the default `auth: 'protected'`. The only
11725
+ * consumer, the detection-pipeline's model resolution
11726
+ * (`addon-pipeline/src/detection-pipeline/provider.ts`), calls
11727
+ * `ctx.api.customModelRegistry.listModels.query()` — UDS/Moleculer,
11728
+ * never tRPC — so `auth: 'admin'` costs it nothing.
11729
+ */
11730
+ listModels: method(_void(), array(CustomModelDescriptorSchema).readonly(), { auth: "admin" }) }
11674
11731
  };
11675
11732
  /**
11676
11733
  * Query filter for settings-store collections.
@@ -11794,11 +11851,26 @@ var settingsStoreCapability = {
11794
11851
  key: string(),
11795
11852
  value: unknown()
11796
11853
  }), _void(), { kind: "mutation" }),
11797
- /** Get all entries matching an optional filter. */
11854
+ /**
11855
+ * Rows matching a filter.
11856
+ *
11857
+ * `columns` is a SQL-level projection: the engine SELECTs only those
11858
+ * columns (plus the primary key) instead of the whole row. It is the only
11859
+ * way to stop paying disk for a column that is discarded — a heavy JSON
11860
+ * column dominates the row, and dropping it in JS after the read pays every
11861
+ * byte first. Measured on the live `pipeline-analytics:tracks` table: a
11862
+ * 501-row page is 4.6 MB whole and 0.11 MB projected, same index, same
11863
+ * plan.
11864
+ *
11865
+ * The engine REFUSES a column it cannot serve rather than omitting it —
11866
+ * see `SettingsQueryInput.columns` for why a projection may not be
11867
+ * forgiving the way a predicate is.
11868
+ */
11798
11869
  query: method(object({
11799
11870
  namespace: string().optional(),
11800
11871
  collection: string(),
11801
- filter: QueryFilterSchema.optional()
11872
+ filter: QueryFilterSchema.optional(),
11873
+ columns: array(string()).readonly().optional()
11802
11874
  }), array(SettingsRecordSchema).readonly()),
11803
11875
  /** Insert a new record. */
11804
11876
  insert: method(object({
@@ -11924,6 +11996,23 @@ var EngineInfoSchema = object({
11924
11996
  * go through `settings-store`; they never see this cap, and an engine
11925
11997
  * never sees a caller.
11926
11998
  *
11999
+ * ⚠ **`internal: true` DOES NOT gate the mount, and until 2026-08-26 this
12000
+ * comment claimed otherwise.** `resolveCapMount` never reads the field, so this
12001
+ * cap was live on the AppRouter with every method at the default
12002
+ * `auth: 'protected'` — i.e. readable by ANY authenticated session, admin or
12003
+ * not. Verified against the live hub: `dataStoreProvider.query` returned raw
12004
+ * settings rows including a provider credential, bypassing the secret redaction
12005
+ * that `deviceManager.listAll` and friends go through.
12006
+ *
12007
+ * Every method is now `auth: 'admin'` EXPLICITLY, including the five reads —
12008
+ * they had no options object at all, which is exactly how they kept the
12009
+ * permissive default while the mutations looked deliberate. The explicit
12010
+ * annotation is the fix that works today; making `internal` mean something at
12011
+ * mount time is the systemic one, and it is not free: 22 caps declare it, only
12012
+ * 4 are `mount: 'skip'`, and forcing admin on all of them would change the auth
12013
+ * of surfaces the viewer and admin-ui call as non-admin users. That decision is
12014
+ * the operator's, not a side effect of this file.
12015
+ *
11927
12016
  * Design notes:
11928
12017
  * - **Stateless dispatch.** Every method carries its own
11929
12018
  * `namespace` + `collection`, so an engine keeps no per-caller state
@@ -11945,64 +12034,105 @@ var dataStoreProviderCapability = {
11945
12034
  internal: true,
11946
12035
  methods: {
11947
12036
  /** Self-description — how the orchestrator picks a registrant. */
11948
- getEngineInfo: method(_void(), EngineInfoSchema),
12037
+ getEngineInfo: method(_void(), EngineInfoSchema, { auth: "admin" }),
11949
12038
  /** Get a single value by key from a collection. */
11950
12039
  get: method(object({
11951
12040
  namespace: string().optional(),
11952
12041
  collection: string(),
11953
12042
  key: string()
11954
- }), unknown()),
12043
+ }), unknown(), { auth: "admin" }),
11955
12044
  /** Set a value by key in a collection (upsert). */
11956
12045
  set: method(object({
11957
12046
  namespace: string().optional(),
11958
12047
  collection: string(),
11959
12048
  key: string(),
11960
12049
  value: unknown()
11961
- }), _void(), { kind: "mutation" }),
12050
+ }), _void(), {
12051
+ kind: "mutation",
12052
+ auth: "admin"
12053
+ }),
11962
12054
  /** Get all entries matching an optional filter. */
11963
12055
  query: method(object({
11964
12056
  namespace: string().optional(),
11965
12057
  collection: string(),
11966
- filter: QueryFilterSchema.optional()
11967
- }), array(SettingsRecordSchema).readonly()),
12058
+ filter: QueryFilterSchema.optional(),
12059
+ /**
12060
+ * SQL-level column projection — MUST mirror `settings-store.query`.
12061
+ *
12062
+ * ⚠ An earlier version of this comment blamed Zod stripping, and that
12063
+ * was wrong — corrected 2026-08-26 after the hop map
12064
+ * (`docs/design/2026-08-26-mappa-hop-argomenti.md`) traced the path.
12065
+ * There is **no Zod parse at all** between the door and the engine: the
12066
+ * dispatcher forwards the payload verbatim and UDS carries it whole. A
12067
+ * field declared here reaches `SqliteSettingsBackend` either way.
12068
+ *
12069
+ * What actually lost `columns` was the THIRD declaration of this shape:
12070
+ * `SettingsQueryInput` in `interfaces/storage.ts`, a hand-written TS
12071
+ * interface the engine destructures from. The field existed on both
12072
+ * schemas and the engine still never read it, because nothing checks a
12073
+ * registered provider against `InferProvider<cap>` —
12074
+ * `ProviderRegistration.provider` is typed `object`.
12075
+ *
12076
+ * It is declared here anyway, and must stay in step with
12077
+ * `settings-store.query`: a caller reading only the cap definitions has
12078
+ * to be able to see that this call carries a projection.
12079
+ * `data-door-schema-parity.spec.ts` keeps the two aligned.
12080
+ */
12081
+ columns: array(string()).readonly().optional()
12082
+ }), array(SettingsRecordSchema).readonly(), { auth: "admin" }),
11968
12083
  /** Insert a new record. */
11969
12084
  insert: method(object({
11970
12085
  namespace: string().optional(),
11971
12086
  collection: string(),
11972
12087
  record: SettingsRecordSchema
11973
- }), _void(), { kind: "mutation" }),
12088
+ }), _void(), {
12089
+ kind: "mutation",
12090
+ auth: "admin"
12091
+ }),
11974
12092
  /** Update an existing record by ID. */
11975
12093
  update: method(object({
11976
12094
  namespace: string().optional(),
11977
12095
  collection: string(),
11978
12096
  id: string(),
11979
12097
  data: record(string(), unknown())
11980
- }), _void(), { kind: "mutation" }),
12098
+ }), _void(), {
12099
+ kind: "mutation",
12100
+ auth: "admin"
12101
+ }),
11981
12102
  /** Delete a record by key/ID. */
11982
12103
  delete: method(object({
11983
12104
  namespace: string().optional(),
11984
12105
  collection: string(),
11985
12106
  key: string()
11986
- }), _void(), { kind: "mutation" }),
12107
+ }), _void(), {
12108
+ kind: "mutation",
12109
+ auth: "admin"
12110
+ }),
11987
12111
  /** Delete every record matching `filter`, in one statement. */
11988
12112
  deleteWhere: method(object({
11989
12113
  namespace: string().optional(),
11990
12114
  collection: string(),
11991
12115
  filter: MutationFilterSchema
11992
- }), object({ deleted: number().int() }), { kind: "mutation" }),
12116
+ }), object({ deleted: number().int() }), {
12117
+ kind: "mutation",
12118
+ auth: "admin"
12119
+ }),
11993
12120
  /** Apply `data` to every record matching `filter`, in one statement. */
11994
12121
  updateWhere: method(object({
11995
12122
  namespace: string().optional(),
11996
12123
  collection: string(),
11997
12124
  filter: MutationFilterSchema,
11998
12125
  data: record(string(), unknown())
11999
- }), object({ updated: number().int() }), { kind: "mutation" }),
12126
+ }), object({ updated: number().int() }), {
12127
+ kind: "mutation",
12128
+ auth: "admin"
12129
+ }),
12000
12130
  /** Count entries in a collection, optionally filtered. */
12001
12131
  count: method(object({
12002
12132
  namespace: string().optional(),
12003
12133
  collection: string(),
12004
12134
  filter: QueryFilterSchema.optional()
12005
- }), number()),
12135
+ }), number(), { auth: "admin" }),
12006
12136
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
12007
12137
  histogram: method(object({
12008
12138
  namespace: string().optional(),
@@ -12014,19 +12144,22 @@ var dataStoreProviderCapability = {
12014
12144
  }), array(object({
12015
12145
  bucket: number().int(),
12016
12146
  count: number().int()
12017
- })).readonly()),
12147
+ })).readonly(), { auth: "admin" }),
12018
12148
  /** Check if a collection is empty. */
12019
12149
  isEmpty: method(object({
12020
12150
  namespace: string().optional(),
12021
12151
  collection: string()
12022
- }), boolean()),
12152
+ }), boolean(), { auth: "admin" }),
12023
12153
  /** Declare a typed (SQL-backed) collection with columns + indexes. */
12024
12154
  declareCollection: method(object({
12025
12155
  namespace: string().optional(),
12026
12156
  collection: string(),
12027
12157
  columns: array(CollectionColumnSchema).readonly(),
12028
12158
  indexes: array(CollectionIndexSchema).readonly().optional()
12029
- }), _void(), { kind: "mutation" })
12159
+ }), _void(), {
12160
+ kind: "mutation",
12161
+ auth: "admin"
12162
+ })
12030
12163
  }
12031
12164
  };
12032
12165
  /**
@@ -13826,13 +13959,20 @@ var embeddingEncoderCapability = {
13826
13959
  mode: "collection",
13827
13960
  internal: true,
13828
13961
  methods: {
13962
+ /**
13963
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
13964
+ * `data-store-provider`) — every method below was reachable on the
13965
+ * AppRouter by ANY authenticated session at the default
13966
+ * `auth: 'protected'`. Consumers reach the encoder via `ctx.api` from
13967
+ * `addon-post-analysis`, never tRPC.
13968
+ */
13829
13969
  encode: method(object({
13830
13970
  crop: _instanceof(Uint8Array),
13831
13971
  width: number(),
13832
13972
  height: number()
13833
- }), EmbeddingResultSchema),
13834
- encodeText: method(object({ text: string() }), EmbeddingResultSchema),
13835
- getInfo: method(_void(), EmbeddingInfoSchema)
13973
+ }), EmbeddingResultSchema, { auth: "admin" }),
13974
+ encodeText: method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }),
13975
+ getInfo: method(_void(), EmbeddingInfoSchema, { auth: "admin" })
13836
13976
  }
13837
13977
  };
13838
13978
  /**
@@ -14141,12 +14281,28 @@ var llmRuntimeCapability = {
14141
14281
  mode: "singleton",
14142
14282
  internal: true,
14143
14283
  methods: {
14284
+ /**
14285
+ * `complete`, `status`, `listLocalModels` and `getDiskUsage` had NO
14286
+ * `auth` override — `internal: true` did not gate the mount (see the
14287
+ * 2026-08-26 note on `data-store-provider`), so they were reachable on
14288
+ * the AppRouter by ANY authenticated session at the default
14289
+ * `auth: 'protected'`, inconsistent with the rest of this cap (already
14290
+ * `auth: 'admin'`) and with this file's own docblock: "the operator
14291
+ * reaches this only through the `llm` cap's methods." The sole caller,
14292
+ * `addon-ai`'s `runtime-client.ts`, reaches every method here via
14293
+ * `ctx.api` + `nodePin` — never tRPC — so `auth: 'admin'` costs it
14294
+ * nothing; the public `llm` cap (which end users and non-admin sessions
14295
+ * legitimately call for chat/generation) is untouched.
14296
+ */
14144
14297
  complete: method(LlmGenerateBaseInputSchema.extend({
14145
14298
  images: array(LlmImageSchema).optional(),
14146
14299
  runtime: ManagedRuntimeConfigSchema,
14147
14300
  /** The managed profile's timeout, threaded by the hub provider. */
14148
14301
  timeoutMs: number().int().positive().optional()
14149
- }), LlmGenerateResultSchema, { kind: "mutation" }),
14302
+ }), LlmGenerateResultSchema, {
14303
+ kind: "mutation",
14304
+ auth: "admin"
14305
+ }),
14150
14306
  ensureStarted: method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
14151
14307
  kind: "mutation",
14152
14308
  auth: "admin"
@@ -14155,7 +14311,7 @@ var llmRuntimeCapability = {
14155
14311
  kind: "mutation",
14156
14312
  auth: "admin"
14157
14313
  }),
14158
- status: method(object({}), LlmRuntimeStatusSchema),
14314
+ status: method(object({}), LlmRuntimeStatusSchema, { auth: "admin" }),
14159
14315
  installModel: method(object({ model: ManagedModelRefSchema }), _void(), {
14160
14316
  kind: "mutation",
14161
14317
  auth: "admin"
@@ -14164,8 +14320,8 @@ var llmRuntimeCapability = {
14164
14320
  kind: "mutation",
14165
14321
  auth: "admin"
14166
14322
  }),
14167
- listLocalModels: method(object({}), array(LlmNodeModelSchema)),
14168
- getDiskUsage: method(object({}), LlmRuntimeDiskUsageSchema)
14323
+ listLocalModels: method(object({}), array(LlmNodeModelSchema), { auth: "admin" }),
14324
+ getDiskUsage: method(object({}), LlmRuntimeDiskUsageSchema, { auth: "admin" })
14169
14325
  }
14170
14326
  };
14171
14327
  /**
@@ -18431,210 +18587,1044 @@ var oauthIntegrationCapability = {
18431
18587
  scope: "system",
18432
18588
  mode: "collection",
18433
18589
  internal: true,
18434
- methods: { getDescriptor: method(_void(), OauthIntegrationDescriptorSchema) }
18590
+ methods: {
18591
+ /**
18592
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
18593
+ * `data-store-provider`) — `getDescriptor` was reachable on the AppRouter
18594
+ * by ANY authenticated session at the default `auth: 'protected'`. The
18595
+ * real caller is `/api/oauth2/authorize` and `/api/oauth2/integrations`
18596
+ * (`oauth2-routes.ts`), which resolve the provider directly off the
18597
+ * capability registry — never through tRPC. `auth: 'admin'` closes the
18598
+ * tRPC surface without touching that path.
18599
+ */
18600
+ getDescriptor: method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" }) }
18435
18601
  };
18436
18602
  /**
18437
- * pipeline-analytics device-scoped wrapper cap. Refines raw
18438
- * per-frame detections emitted by the pipeline runner into tracked
18439
- * objects, per-kind event collections (motion / object / audio), and
18440
- * persisted media. Owns the post-detection domain end-to-end:
18441
- *
18442
- * runner emits PipelineInferenceResult
18443
- * ↓ (event bus)
18444
- * pipeline-analytics subscriber
18445
- * ↓ SORT tracker + zone engine + state analyzer + event emitter
18446
- * → three DB collections (one per kind), one FS media tree, one
18447
- * unified event emitter (FrameTracked + TrackStarted/Ended +
18448
- * DetectionEvent on bus)
18449
- *
18450
- * Pure subscriber model. No `processFrame` cap method — the runner
18451
- * already publishes the raw frame on the bus. The cap surface is
18452
- * only QUERIES + per-device settings, bound on/off via
18453
- * `device-manager.setWrapperActive`. `defaultActive: true` because
18454
- * every camera with a detection pipeline wants its raw detections
18455
- * refined; operators opt out per-device via BindingsTab when needed.
18456
- *
18457
- * Replaces the legacy `analysis-pipeline`, `analysis-data-persistence`
18458
- * (per-device surface) and `track-trail` caps — see P11 cleanup.
18603
+ * Reference to the frame's retained NATIVE surface + the parent crop's placement
18604
+ * within the frame, so the executor can re-cut a leaf child ROI at native
18605
+ * resolution on the detail plane. See the `runPipeline` `nativeCropRef` field.
18459
18606
  */
18460
- var TrackStateSchema = _enum([
18461
- "new",
18462
- "entered",
18463
- "left",
18464
- "moving",
18465
- "idle"
18466
- ]);
18467
- var EventKindSchema = _enum([
18468
- "motion",
18469
- "object",
18470
- "audio"
18471
- ]);
18607
+ var NativeCropRefSchema = object({
18608
+ /** Handle keying the retained native surface (node-pinned to its owner). */
18609
+ handle: FrameHandleSchema,
18610
+ /** The parent crop's padded/clamped rectangle in FRAME-space pixels. */
18611
+ cropFrameSpace: object({
18612
+ x: number(),
18613
+ y: number(),
18614
+ w: number(),
18615
+ h: number()
18616
+ })
18617
+ });
18618
+ object({
18619
+ crop: object({
18620
+ left: number(),
18621
+ top: number(),
18622
+ width: number().positive(),
18623
+ height: number().positive()
18624
+ }).optional(),
18625
+ content: object({
18626
+ width: number().int().positive(),
18627
+ height: number().int().positive()
18628
+ }),
18629
+ fit: _enum(["stretch", "contain"]),
18630
+ format: _enum([
18631
+ "rgb",
18632
+ "gray",
18633
+ "jpeg"
18634
+ ])
18635
+ });
18472
18636
  /**
18473
- * Spatial filter for `listTracks` the rect + polygon variants of the shared
18474
- * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
18475
- * of the camera frame (top-left origin), matching the drawing-plane editor.
18637
+ * Process-local frame identity. It is serializable so it can ride an in-process
18638
+ * capability call, but `registryId` deliberately prevents resolution in any
18639
+ * other process or execution group.
18476
18640
  */
18477
- var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
18478
- /** Closed icon vocabulary so clients render a known glyph per kind. */
18479
- var EventKindIconSchema = _enum([
18480
- "motion",
18481
- "audio",
18482
- "person",
18483
- "vehicle",
18484
- "animal",
18485
- "door",
18486
- "pir",
18487
- "smoke",
18488
- "water",
18489
- "button",
18490
- "package",
18491
- "generic"
18641
+ var FrameRefSchema = object({
18642
+ registryId: string().min(1),
18643
+ id: string().min(1),
18644
+ width: number().int().positive(),
18645
+ height: number().int().positive(),
18646
+ format: _enum(["rgb", "gray"]),
18647
+ timestamp: number(),
18648
+ capturedAt: number().optional()
18649
+ });
18650
+ var ModelFormatSchema$1 = _enum([
18651
+ "onnx",
18652
+ "coreml",
18653
+ "openvino",
18654
+ "tflite",
18655
+ "pt",
18656
+ "gguf"
18492
18657
  ]);
18493
- var EventKindCategorySchema = _enum([
18494
- "motion",
18495
- "audio",
18496
- "detection",
18497
- "sensor",
18498
- "control",
18499
- "custom",
18500
- "package"
18658
+ var PipelineSlotSchema = _enum([
18659
+ "detector",
18660
+ "cropper",
18661
+ "classifier",
18662
+ "refiner",
18663
+ "audio-classifier"
18501
18664
  ]);
18502
- /** Taxonomy level — macro (timeline lane) vs sub (events-page leaf). */
18503
- var EventKindLevelSchema = _enum(["macro", "sub"]);
18504
- var EventKindDescriptorSchema = object({
18505
- /** Stable kind id (e.g. 'motion', 'vehicle', 'car', 'lock'). */
18506
- kind: string(),
18507
- /** i18n key resolved on the UI side; `label` is the English fallback. */
18508
- labelKey: string(),
18509
- /** English fallback label (kept for clients that don't translate). */
18510
- label: string(),
18511
- /** Hex color for timeline/legend rendering. */
18512
- color: string(),
18513
- /** Dictionary id → lucide component on the UI side. */
18514
- iconId: string(),
18515
- /** Legacy closed-vocab glyph — fallback for `iconId`. */
18516
- icon: EventKindIconSchema,
18517
- category: EventKindCategorySchema,
18518
- /** Macro parent for this kind ('car' → 'vehicle'); null for a macro. */
18519
- parentKind: string().nullable(),
18520
- /** Derived from `parentKind`, explicit for the client tree. */
18521
- level: EventKindLevelSchema,
18522
- /** Which cap + device contributes this kind. For built-ins the camera
18523
- * itself; for sensor kinds the LINKED source device. */
18524
- source: object({
18525
- capName: string(),
18526
- deviceId: number()
18527
- })
18665
+ var PipelineEngineChoiceSchema = object({
18666
+ runtime: _enum(["node", "python"]),
18667
+ backend: string(),
18668
+ format: ModelFormatSchema$1,
18669
+ device: string().optional()
18528
18670
  });
18529
- /** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
18530
- var EventKindsForDeviceSchema = object({
18531
- deviceId: number(),
18532
- kinds: array(EventKindDescriptorSchema).readonly()
18671
+ var AvailableEngineSchema = object({
18672
+ engine: PipelineEngineChoiceSchema,
18673
+ devices: array(object({
18674
+ id: string(),
18675
+ label: string(),
18676
+ description: string().optional()
18677
+ })).readonly(),
18678
+ defaultDevice: string()
18533
18679
  });
18534
- var SensorEventSchema = object({
18680
+ var PipelineDefaultStepSchema = lazy(() => object({
18681
+ addonId: string(),
18682
+ addonName: string(),
18683
+ slot: PipelineSlotSchema,
18684
+ inputClasses: array(string()).readonly(),
18685
+ outputClasses: array(string()).readonly(),
18686
+ enabled: boolean(),
18687
+ modelId: string(),
18688
+ children: array(PipelineDefaultStepSchema).readonly(),
18689
+ group: string().optional(),
18690
+ settings: record(string(), unknown()).optional()
18691
+ }));
18692
+ var PipelineTemplateStepSchema = lazy(() => object({
18693
+ addonId: string(),
18694
+ enabled: boolean(),
18695
+ modelId: string(),
18696
+ children: array(PipelineTemplateStepSchema).readonly(),
18697
+ settings: record(string(), unknown()).optional()
18698
+ }));
18699
+ var PipelineTemplateSchema$1 = object({
18535
18700
  id: string(),
18536
- /** The CAMERA the event is attributed to (a sensor linked to N cameras
18537
- * yields N rows, one per camera). */
18538
- deviceId: number(),
18539
- /** The linked sensor device whose state changed. */
18540
- sourceDeviceId: number(),
18541
- /** Event kind id — matches an `EventKindDescriptor.kind`. */
18542
- kind: string(),
18543
- /** Snapshot of the sensor cap's runtime-state slice at the change. */
18544
- value: record(string(), unknown()).nullable(),
18545
- timestamp: number()
18546
- });
18547
- var TrackPositionSchema = object({
18548
- x: number(),
18549
- y: number(),
18550
- timestamp: number(),
18551
- bbox: BoundingBoxSchema
18701
+ name: string(),
18702
+ createdAt: string(),
18703
+ updatedAt: string(),
18704
+ engine: PipelineEngineChoiceSchema,
18705
+ steps: array(PipelineTemplateStepSchema).readonly()
18552
18706
  });
18553
- var TrackSnapshotSchema = object({
18554
- timestamp: number(),
18555
- position: TrackPositionSchema,
18556
- /** MediaStore key; resolve via `getTrackMedia({ trackId })`. */
18557
- mediaKey: string()
18707
+ var PipelineModelOptionSchema = object({
18708
+ id: string(),
18709
+ name: string(),
18710
+ formats: record(string(), object({
18711
+ downloaded: boolean(),
18712
+ sizeMB: number()
18713
+ })),
18714
+ group: ModelVariantGroupSchema.optional(),
18715
+ legacy: boolean().optional(),
18716
+ provider: ModelProviderIdSchema.optional()
18558
18717
  });
18559
- /**
18560
- * Normalized 0..1 trajectory envelope (min/max over every position bbox,
18561
- * divided by the track's detection-frame dims), computed at persist time.
18562
- * Absent when the frame dims were unknown when the track was persisted
18563
- * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
18564
- */
18565
- var TrackEnvelopeSchema = object({
18566
- minX: number(),
18567
- minY: number(),
18568
- maxX: number(),
18569
- maxY: number()
18718
+ var ConfigFieldBridge = custom();
18719
+ var PipelineAddonSchemaSchema = object({
18720
+ id: string(),
18721
+ name: string(),
18722
+ slot: PipelineSlotSchema,
18723
+ inputClasses: array(string()).readonly(),
18724
+ outputClasses: array(string()).readonly(),
18725
+ childSlots: array(PipelineSlotSchema).readonly(),
18726
+ models: array(PipelineModelOptionSchema).readonly(),
18727
+ defaultModelId: string(),
18728
+ defaultModelIdByFormat: record(string(), string()).optional(),
18729
+ enabledByDefault: boolean().optional(),
18730
+ backfillIntoExistingOverrides: boolean().optional(),
18731
+ defaultConfidence: number(),
18732
+ group: string().optional(),
18733
+ configSchema: array(ConfigFieldBridge).readonly().optional()
18570
18734
  });
18571
- /**
18572
- * Row projection for track list queries. `full` (default) returns the
18573
- * complete Track including the frame-rate `positions[]` history and the
18574
- * `snapshots[]` references — megabytes across a page of tracks. `slim`
18575
- * keeps every scalar the list surfaces actually render (ids, class(es),
18576
- * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
18577
- * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
18578
- * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
18579
- * `getTrack`. Mirrors the event-store `projection` convention
18580
- * (`getObjectEvents` et al.).
18581
- */
18582
- var TrackProjectionSchema = _enum(["full", "slim"]);
18583
- /**
18584
- * One audio-classification label heard on the track's camera while the
18585
- * track was alive, aggregated per label. An "episode" is one persisted
18586
- * audio event (the confident-classification path: score ≥ the device's
18587
- * `classificationMinScore`, class-change-or-heartbeat coalesced) — NOT
18588
- * one 32 ms inference chunk, so counts stay human-scaled.
18589
- */
18590
- var TrackAudioLabelSchema = object({
18735
+ var PipelineSlotSchemaSchema = object({
18736
+ id: PipelineSlotSchema,
18591
18737
  label: string(),
18592
- /** Highest classification score observed across the label's episodes. */
18593
- peakScore: number(),
18594
- /** Number of coalesced audio-event episodes carrying this label. */
18595
- count: number(),
18596
- firstAt: number(),
18597
- lastAt: number()
18738
+ priority: number(),
18739
+ parentSlot: PipelineSlotSchema.nullable(),
18740
+ addons: array(PipelineAddonSchemaSchema).readonly()
18741
+ });
18742
+ var PipelineSchemaSchema = object({
18743
+ availableEngines: array(AvailableEngineSchema).readonly(),
18744
+ selectedEngine: PipelineEngineChoiceSchema,
18745
+ slots: array(PipelineSlotSchemaSchema).readonly()
18746
+ });
18747
+ var EngineProvisioningSchema = object({
18748
+ runtimeId: _enum([
18749
+ "onnx",
18750
+ "openvino",
18751
+ "coreml",
18752
+ "edgetpu"
18753
+ ]).nullable(),
18754
+ device: string().nullable(),
18755
+ state: _enum([
18756
+ "idle",
18757
+ "installing",
18758
+ "verifying",
18759
+ "ready",
18760
+ "failed"
18761
+ ]),
18762
+ progress: number().optional(),
18763
+ error: string().optional(),
18764
+ nextRetryAt: number().optional(),
18765
+ /**
18766
+ * Gate A (config-correctness gate at engine change): human-readable
18767
+ * config issues surfaced EAGERLY when the node's engine changes — model
18768
+ * substitutions ("chose X, running Y") and zero-build steps ("no model
18769
+ * has a <format> build"). Additive/optional: informational only, never
18770
+ * enforced here — `assertEngineReady` (readiness) still gates inference.
18771
+ * Absent/empty when the node-default tree resolves cleanly.
18772
+ */
18773
+ configIssues: array(string()).optional()
18774
+ });
18775
+ var PipelineStepInputSchema = lazy(() => object({
18776
+ addonId: string(),
18777
+ modelId: string().optional(),
18778
+ enabled: boolean().default(true),
18779
+ children: array(PipelineStepInputSchema).optional(),
18780
+ settings: record(string(), unknown()).optional(),
18781
+ jumpDeviceKey: string().optional()
18782
+ }));
18783
+ var ModelSubstitutionSchema = object({
18784
+ addonId: string(),
18785
+ chosen: string(),
18786
+ running: string(),
18787
+ format: string()
18788
+ });
18789
+ var PipelineValidationIssueSchema = object({
18790
+ addonId: string(),
18791
+ kind: _enum(["unknown-addon", "no-format-build"]),
18792
+ detail: string()
18793
+ });
18794
+ var PipelineValidationResultSchema = object({
18795
+ ok: boolean(),
18796
+ issues: array(PipelineValidationIssueSchema).readonly(),
18797
+ substitutions: array(ModelSubstitutionSchema).readonly(),
18798
+ /** The node's `currentEngine.format` this validation ran against. */
18799
+ format: string()
18800
+ });
18801
+ var ReferenceImageEntrySchema = object({
18802
+ filename: string(),
18803
+ stepIds: array(string()).readonly().optional()
18804
+ });
18805
+ var ReferenceImageBodySchema = object({
18806
+ base64: string(),
18807
+ filename: string()
18808
+ });
18809
+ var ReferenceAudioEntrySchema = object({
18810
+ filename: string(),
18811
+ sizeKb: number()
18812
+ });
18813
+ var ReferenceAudioBodySchema = object({ base64: string() });
18814
+ var AudioBackendSchema = object({
18815
+ id: string(),
18816
+ name: string(),
18817
+ description: string(),
18818
+ available: boolean(),
18819
+ /**
18820
+ * Raw classifier labels this backend can emit (e.g. YAMNet's
18821
+ * 521-class set or Apple SoundAnalysis's 303-class set). Used by
18822
+ * the benchmark UI to populate the `enabledMicroClasses` filter
18823
+ * specific to the selected backend without a separate fetch.
18824
+ */
18825
+ rawLabels: array(string()).readonly().optional()
18826
+ });
18827
+ var AudioCapabilitiesSchema = object({
18828
+ activeBackend: string(),
18829
+ availableBackends: array(AudioBackendSchema).readonly(),
18830
+ sampleRate: number(),
18831
+ chunkDurationMs: number()
18832
+ });
18833
+ var DownloadModelResultSchema = object({
18834
+ filePath: string(),
18835
+ sizeMB: number(),
18836
+ durationMs: number()
18598
18837
  });
18599
18838
  /**
18600
- * How a track was produced. `pipeline` (default / absent) = the spatial
18601
- * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
18602
- * no positions, a single snapshot, and no bbox trajectory at all:
18603
- *
18604
- * - `sensor` a linked sensor/control device state change.
18605
- * - `audio` an audio event on the camera itself that was anomalous for
18606
- * THAT camera, loud, and heard while nothing visual was happening (D62).
18607
- *
18608
- * The spatial subsystems (tracker association, occupancy count, re-id /
18609
- * embedding, resurrection) MUST skip every synthetic source. Test for that
18610
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
18611
- * check silently readmits every source added after it was written.
18839
+ * Wrapper carrying a single test run's result. Replaces the legacy
18840
+ * ad-hoc `{labels: [{className, originalClass, score}]}` shape with the
18841
+ * canonical `AudioResult` from the Phase 6 output rework: one
18842
+ * `AudioDetection` per class above `minScore`, top-N candidates in
18843
+ * `debug.alternateLabels['audio-classifier']`, per-source timings in
18844
+ * `debug.stepTimings`. The outer `success`/`error` fields stay so the
18845
+ * benchmark UI can still report a clean failure when the classifier
18846
+ * cap isn't available.
18612
18847
  */
18613
- var TrackSourceSchema = _enum([
18614
- "pipeline",
18615
- "sensor",
18616
- "audio"
18617
- ]);
18848
+ var AudioTestResultSchema = object({
18849
+ success: boolean(),
18850
+ error: string().optional(),
18851
+ frame: custom().optional()
18852
+ });
18853
+ var PipelineConfigBridge = custom();
18854
+ var ConfigUISchemaBridge = custom();
18855
+ var ConfigUISchemaNullableBridge = custom();
18856
+ var InferenceCapabilitiesBridge = custom();
18857
+ var ModelAvailabilityListBridge = custom();
18858
+ var PipelineRunResultBridge = custom();
18618
18859
  /**
18619
- * Where a track sits in the RETRAIN lifecycle (D81).
18860
+ * Pipeline executor detection engine + configuration + inference API.
18620
18861
  *
18621
- * - `none` never marked, or un-marked. Evictable.
18622
- * - `staging` the operator wants this track as training material and has not
18623
- * finished with it. **This is the only state retention holds**: the track and
18624
- * everything it owns (object events, crops, keyframes, CLIP vector) survive
18625
- * the device's age window.
18626
- * - `trained` — the retrain page has taken what it needed. The frames it chose
18627
- * were COPIED into the retrain dataset at selection time, so the dataset no
18628
- * longer depends on the track's media and the track becomes EVICTABLE again.
18629
- * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
18630
- * a deliberate action of the retrain page, not a side effect of a checkbox.
18862
+ * Merged from: pipeline-executor, pipeline-config, inference, detection-config.
18863
+ * Implemented by the detection-pipeline addon.
18631
18864
  *
18632
- * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
18633
- * the store's filter language has only positive equality and `whereIn` — no
18634
- * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
18635
- * would make the entire pre-column history immortal in one deploy.
18865
+ * Per-device surface (DeviceSettingsContribution + the "is detection
18866
+ * enabled for this camera?" toggle) lives on the paired
18867
+ * `detection-pipeline` cap (device-scoped, singleton, wrapper
18868
+ * defaultActive) same split pattern used by stream-broker /
18869
+ * camera-streams and audio-analyzer / audio-analysis.
18636
18870
  */
18637
- var RetrainStatusSchema = _enum([
18871
+ var pipelineExecutorCapability = {
18872
+ name: "pipeline-executor",
18873
+ scope: "system",
18874
+ mode: "singleton",
18875
+ methods: {
18876
+ getAvailableEngines: method(_void(), array(PipelineEngineChoiceSchema)),
18877
+ getSelectedEngine: method(_void(), PipelineEngineChoiceSchema),
18878
+ getDefaultSteps: method(PipelineEngineChoiceSchema, array(PipelineDefaultStepSchema)),
18879
+ /**
18880
+ * Per-node detection-engine provisioning snapshot. Returns the live
18881
+ * state of the lazy runtime-provisioning machine on `nodeId`
18882
+ * (idle / installing / verifying / ready / failed). The UI pairs this
18883
+ * one-shot query with the `pipeline.engine-provisioning` live event
18884
+ * (emitted on every transition) to drive a per-node "engine ready?"
18885
+ * indicator without polling. Phase 2.
18886
+ */
18887
+ getEngineProvisioning: method(object({ nodeId: string() }), EngineProvisioningSchema),
18888
+ getVideoPipelineSteps: method(_void(), record(string(), object({
18889
+ modelId: string(),
18890
+ settings: record(string(), unknown()).readonly()
18891
+ }))),
18892
+ setVideoPipelineSteps: method(object({ steps: record(string(), object({
18893
+ modelId: string(),
18894
+ settings: record(string(), unknown()).readonly()
18895
+ })) }), object({ success: literal(true) }), {
18896
+ kind: "mutation",
18897
+ auth: "admin"
18898
+ }),
18899
+ /**
18900
+ * Clear THIS node's executor-side PER-DEVICE settings stores (the
18901
+ * per-camera step overrides the object-detection root reads via
18902
+ * `applyDeviceOverridesToTree`). `nodeId` is the ROUTING key — the
18903
+ * generated cap-router strips it (default `nodeIdMode: 'routing'`) and
18904
+ * dispatches to that node, so the provider method runs ON the target
18905
+ * node and receives no `nodeId`.
18906
+ *
18907
+ * This is the slimmed executor leg of the orchestrator's
18908
+ * `resetNodePipelineDefaults` flow (which owns the real reset: node
18909
+ * addonDefaults pins + per-camera orchestrator overrides). The legacy
18910
+ * `resetToDefault` — which reset a persisted global step-tree seed
18911
+ * nothing in the live per-camera path read — was removed together with
18912
+ * that seed.
18913
+ */
18914
+ clearDeviceOverrides: method(object({ nodeId: string() }), object({
18915
+ success: literal(true),
18916
+ clearedDevices: number()
18917
+ }), {
18918
+ kind: "mutation",
18919
+ auth: "admin"
18920
+ }),
18921
+ /**
18922
+ * Which of THIS node's inference devices the executor currently refuses,
18923
+ * and why. `nodeId` is the ROUTING key (stripped by the generated router).
18924
+ *
18925
+ * The channel that did not exist. Pool health was known only inside the
18926
+ * detection addon and was an input to no routing decision anywhere: the
18927
+ * per-dispatch capability gate is keyed on model FORMAT and so can never
18928
+ * separate `openvino:gpu` from `openvino:npu`, and the orchestrator's live
18929
+ * eligibility probe (`platformProbe.getCapabilities`) answers about
18930
+ * HARDWARE — which was present throughout. So when the hub's `openvino:gpu`
18931
+ * Python worker was SIGABRT'd by the Intel GPU plugin on 2026-08-25, the
18932
+ * balancer went on handing that dead pool cameras by rotation for 31 hours:
18933
+ * ~370 000 `PoolWorker[w0]: not initialized` lines, every frame lost.
18934
+ *
18935
+ * Read semantics the caller depends on, and which the provider guarantees:
18936
+ * this is a synchronous read of in-memory state. It never probes hardware,
18937
+ * never spawns a pool and never throws — an EMPTY `unhealthy` means "asked,
18938
+ * nothing is refused", which is what re-admits a device. A read that FAILS
18939
+ * (node offline, version skew) must therefore be distinguishable from an
18940
+ * empty answer, and it is: it rejects.
18941
+ */
18942
+ getInferenceDeviceHealth: method(object({ nodeId: string() }), object({ unhealthy: array(object({
18943
+ /** `<backend>:<device>`, e.g. `openvino:gpu`. */
18944
+ deviceKey: string(),
18945
+ /**
18946
+ * `failed` — the per-device restart budget is exhausted; no pool
18947
+ * will be spawned until an operator re-arms it or the runner
18948
+ * respawns. `backoff` — under budget, waiting out the backoff (or
18949
+ * a cached pool observed dead and not yet condemned).
18950
+ */
18951
+ state: _enum(["failed", "backoff"]),
18952
+ /** Epoch ms of the death that produced this state. */
18953
+ since: number(),
18954
+ /** Pool deaths inside the current window. */
18955
+ deaths: number(),
18956
+ /** The last death's message. */
18957
+ lastError: string()
18958
+ })).readonly() })),
18959
+ /**
18960
+ * Re-arm a terminally `failed` inference device on `nodeId`: forget its
18961
+ * restart budget so the next dispatch builds a fresh pool.
18962
+ *
18963
+ * The terminal state is deliberate (the abort it bounds is deterministic —
18964
+ * an automatic probation would just respawn Python forever, more slowly),
18965
+ * and a terminal state an operator cannot leave is a silent fault. This is
18966
+ * the way out. `rearmed:false` means there was nothing to forget.
18967
+ */
18968
+ rearmInferenceDevice: method(object({
18969
+ nodeId: string(),
18970
+ deviceKey: string()
18971
+ }), object({ rearmed: boolean() }), {
18972
+ kind: "mutation",
18973
+ auth: "admin"
18974
+ }),
18975
+ getSchema: method(_void(), PipelineSchemaSchema),
18976
+ getGlobalSteps: method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()),
18977
+ getGlobalPipelineConfig: method(_void(), PipelineConfigBridge),
18978
+ getOrchestratorConfigSchema: method(_void(), ConfigUISchemaBridge),
18979
+ /**
18980
+ * Gate B (pre-init config-correctness gate). PURE COMPUTE against this
18981
+ * node's `currentEngine.format` — resolves `steps` the same way the
18982
+ * runtime dispatch path would, and reports what WOULD happen without
18983
+ * touching any node-global state. Called by the orchestrator at attach
18984
+ * time (`attachOn`), node-pinned to the TARGET node, so config problems
18985
+ * surface BEFORE `pipelineRunner.attachCamera` rather than at the first
18986
+ * per-frame resolve. `ok` is false iff `issues` is non-empty (both
18987
+ * `unknown-addon` and `no-format-build` are HARD issues); `substitutions`
18988
+ * is informational (a degraded-but-loadable model swap) and never
18989
+ * affects `ok`. Never throws.
18990
+ */
18991
+ validatePipeline: method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema),
18992
+ listTemplates: method(_void(), array(PipelineTemplateSchema$1).readonly()),
18993
+ saveTemplate: method(object({
18994
+ name: string(),
18995
+ steps: array(PipelineTemplateStepSchema).readonly(),
18996
+ engine: PipelineEngineChoiceSchema
18997
+ }), PipelineTemplateSchema$1, { kind: "mutation" }),
18998
+ updateTemplate: method(object({
18999
+ id: string(),
19000
+ name: string().optional(),
19001
+ steps: array(PipelineTemplateStepSchema).readonly().optional()
19002
+ }), PipelineTemplateSchema$1, { kind: "mutation" }),
19003
+ deleteTemplate: method(object({ id: string() }), _void(), { kind: "mutation" }),
19004
+ getCapabilities: method(_void(), InferenceCapabilitiesBridge),
19005
+ getAddonModels: method(object({ addonId: string() }), ModelAvailabilityListBridge),
19006
+ downloadModel: method(object({
19007
+ addonId: string(),
19008
+ modelId: string(),
19009
+ format: ModelFormatSchema$1
19010
+ }), DownloadModelResultSchema, { kind: "mutation" }),
19011
+ deleteModel: method(object({
19012
+ addonId: string(),
19013
+ modelId: string(),
19014
+ format: ModelFormatSchema$1
19015
+ }), object({ success: literal(true) }), { kind: "mutation" }),
19016
+ /**
19017
+ * Stateless single-frame execution. Callers (runner, benchmark) pass
19018
+ * the complete `engine` + `steps` tree; the executor holds no state
19019
+ * about cameras or saved pipelines.
19020
+ *
19021
+ * `engine` is optional during the migration window to preserve the
19022
+ * legacy call shape used by existing benchmark code; once all
19023
+ * callers pass it explicitly we make it required.
19024
+ *
19025
+ * Exactly one of `frame`, `frameRef`, `frameHandle`, `imageBase64`,
19026
+ * `referenceImage` must be provided:
19027
+ * - `frame`: runtime dispatch path (runner → decoded broker frame).
19028
+ * Carries the raw buffer, dimensions, and format; the executor
19029
+ * uses it directly without base64 round-tripping.
19030
+ * - `frameHandle` (CB5): a zero-pixel shm `FrameHandle` for the SAME
19031
+ * decoded frame. Both runner and executor are hub-local processes
19032
+ * sharing `/dev/shm`, so the executor maps the named segment and
19033
+ * reads the pixels back zero-copy — eliminating the ~1.2MB
19034
+ * re-serialisation over UDS/MsgPack the `frame` path pays per call.
19035
+ * High-risk: the FrameRing is a latest-wins seqlock with no
19036
+ * refcount, so a recycled slot yields a null read; the executor
19037
+ * then degrades to an empty result and the runner ships pixels via
19038
+ * `frame` as the fallback (queue-depth gated on the runner side).
19039
+ * - `imageBase64`: one-shot test path (benchmark ImageTab).
19040
+ * - `referenceImage`: named file from the reference-image store.
19041
+ */
19042
+ runPipeline: method(object({
19043
+ engine: PipelineEngineChoiceSchema.optional(),
19044
+ steps: array(PipelineStepInputSchema).min(1),
19045
+ frame: FrameInputSchema.optional(),
19046
+ /**
19047
+ * Process-local lazy frame. Valid only when caller and provider resolve
19048
+ * in the same execution-group process; split/cross-node callers use
19049
+ * `frame`/`image` inline compatibility instead.
19050
+ */
19051
+ frameRef: FrameRefSchema.optional(),
19052
+ /**
19053
+ * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
19054
+ * the decoded pixels live in. One more member of the one-of
19055
+ * frame/frameHandle/image/imageBase64/referenceImage group.
19056
+ */
19057
+ frameHandle: FrameHandleSchema.optional(),
19058
+ imageBase64: string().optional(),
19059
+ /**
19060
+ * Binary JPEG bytes — preferred over `imageBase64` on internal
19061
+ * hops (hub → forked worker via Moleculer MsgPack) because it
19062
+ * skips the 33% base64 overhead + the per-call base64 decode on
19063
+ * the detection-pipeline worker. Callers can pass either; exactly
19064
+ * one of `frame`/`image`/`imageBase64`/`referenceImage` is required.
19065
+ */
19066
+ image: _instanceof(Uint8Array).optional(),
19067
+ referenceImage: string().optional(),
19068
+ deviceId: number().optional(),
19069
+ sessionId: string().optional(),
19070
+ /**
19071
+ * Execution plane. 'full' (default) runs the whole tree — benchmark,
19072
+ * reference-image, and detail-subtree calls. 'frame' is the live
19073
+ * per-frame dispatch: ONLY root-plane steps run; crop children
19074
+ * (inputClasses ≠ null) are skipped and served per-track via
19075
+ * pipelineRunner.runDetailSubtree (two-plane design).
19076
+ */
19077
+ plane: _enum(["full", "frame"]).optional(),
19078
+ /**
19079
+ * Inference-device selector (Phase 2 multi-device). Format
19080
+ * `<backend>:<device>` (e.g. `openvino:gpu`, `edgetpu:usb`, `cpu`).
19081
+ * Omitted ⇒ the runner's default device (current single-engine
19082
+ * behaviour). Selects WHICH device pool of the node runs the call.
19083
+ */
19084
+ deviceKey: string().optional(),
19085
+ /**
19086
+ * Two-plane NATIVE child-crop reference. Set by `runDetailSubtree` ONLY
19087
+ * when the parent crop was resolved from the frame's retained NATIVE
19088
+ * surface (a frameHandle HIT). Lets the executor re-cut a LEAF crop
19089
+ * child's ROI (plate-ocr, face-embedding, leaf classifiers) at native
19090
+ * resolution from that surface — the SAME quality path faces already
19091
+ * had — instead of the downscaled parent tile. `handle` keys the native
19092
+ * surface (node-pinned to its owner); `cropFrameSpace` is the parent
19093
+ * crop's padded/clamped rectangle in FRAME-space pixels, used to compose
19094
+ * the executor's crop-normalized child ROI back into frame-normalized
19095
+ * coordinates. Auxiliary to the image source (`image`/`frame`/…), NOT one
19096
+ * of the mutually-exclusive image inputs. Absent ⇒ tile-crop children
19097
+ * (today's behaviour on the fallback path).
19098
+ */
19099
+ nativeCropRef: NativeCropRefSchema.optional()
19100
+ }), PipelineRunResultBridge, { kind: "mutation" }),
19101
+ /**
19102
+ * Batched run — N raw frames packed into one cap call. The provider
19103
+ * routes the batch through `SharedInferencePool.inferBatch`
19104
+ * (`MSG_INFER_BATCH = 0x03`) so the IPC framing and JSON response
19105
+ * envelope cost is amortised N:1 vs N concurrent `runPipeline`
19106
+ * calls. Single root step + uniform model assumed; trees with crop
19107
+ * children fall back to sequential execution.
19108
+ *
19109
+ * Used by `scripts/bench-batch-style.mts` for batch benchmarking —
19110
+ * N frames in one call to amortise per-call IPC overhead.
19111
+ */
19112
+ runPipelineBatch: method(object({
19113
+ engine: PipelineEngineChoiceSchema.optional(),
19114
+ steps: array(PipelineStepInputSchema).min(1),
19115
+ frames: array(FrameInputSchema).min(1).max(255),
19116
+ deviceId: number().optional(),
19117
+ sessionId: string().optional(),
19118
+ /**
19119
+ * Pure-inference benchmark hint. A NONZERO uint32 pins every frame in
19120
+ * the batch to the Python pool's bench preprocess cache
19121
+ * (`_bench_frame_id`) so a REPEATED benchmark frame is decoded +
19122
+ * preprocessed ONCE and every later inference is a pure-inference cache
19123
+ * hit — the sustained-throughput run measures inference, not
19124
+ * decode+preprocess+infer. Omitted/0 for live frames (all different →
19125
+ * full preprocess every call, correct). Fresh per sustained run;
19126
+ * released via `uncacheFrame`.
19127
+ */
19128
+ frameId: number().int().nonnegative().optional(),
19129
+ /** Inference-device selector (Phase 2 multi-device); see runPipeline. */
19130
+ deviceKey: string().optional()
19131
+ }), object({ results: array(PipelineRunResultBridge).readonly() }), { kind: "mutation" }),
19132
+ /**
19133
+ * Cache a raw frame inside the Python inference pool's memory.
19134
+ * Returns a numeric `frameId` that `inferCached` references —
19135
+ * subsequent calls send only 5 bytes through the pipe instead of
19136
+ * 1.2MB raw data, eliminating the pipe transfer bottleneck.
19137
+ */
19138
+ cacheFrameInPool: method(object({
19139
+ data: _instanceof(Uint8Array),
19140
+ width: number().int().positive(),
19141
+ height: number().int().positive(),
19142
+ format: _enum([
19143
+ "rgb",
19144
+ "bgr",
19145
+ "gray"
19146
+ ])
19147
+ }), object({
19148
+ frameId: number(),
19149
+ width: number(),
19150
+ height: number()
19151
+ }), { kind: "mutation" }),
19152
+ /**
19153
+ * Run inference on a previously cached frame. Sends only 5 bytes
19154
+ * (model_idx + frameId) through the IPC pipe — eliminates the
19155
+ * ~35ms per-call overhead of transferring 1.2MB raw data.
19156
+ */
19157
+ inferCached: method(object({
19158
+ stepId: string(),
19159
+ frameId: number().int()
19160
+ }), record(string(), unknown()), { kind: "mutation" }),
19161
+ /**
19162
+ * Release a cached frame from the Python pool's memory.
19163
+ */
19164
+ uncacheFrame: method(object({ frameId: number().int() }), _void(), { kind: "mutation" }),
19165
+ /** Returns the effective pool tuning (resolved from user overrides + backend defaults). */
19166
+ getEffectiveTuning: method(_void(), object({
19167
+ batchMode: string(),
19168
+ windowMs: number(),
19169
+ maxBatchSize: number(),
19170
+ concurrency: number()
19171
+ })),
19172
+ /**
19173
+ * List every EngineFactory currently loaded in this executor's RAM,
19174
+ * with the models resident and a coarse "in use" marker derived from
19175
+ * ongoing inference activity. Used by the Pipeline page Engines tab.
19176
+ */
19177
+ listLoadedEngines: method(_void(), array(object({
19178
+ engineKey: string(),
19179
+ engine: PipelineEngineChoiceSchema,
19180
+ modelsLoaded: array(string()).readonly(),
19181
+ inUseByCameras: array(number()).readonly(),
19182
+ /**
19183
+ * Origin of this resident factory.
19184
+ * - `runtime` — main camera-serving engine (no idle TTL).
19185
+ * - `warm-override` — benchmark/test override held in the warm
19186
+ * cache; auto-disposed after the idle TTL.
19187
+ * - `device-pool` — a concurrent per-device pool (Phase 2
19188
+ * multi-device, keyed by `deviceKey`) resolved
19189
+ * via `resolveDeviceFactory`. Runs alongside the
19190
+ * `runtime` engine on a DIFFERENT accelerator
19191
+ * (NPU / iGPU / Coral) — this is how the
19192
+ * Engines tab shows all pools running at once.
19193
+ */
19194
+ kind: _enum([
19195
+ "runtime",
19196
+ "warm-override",
19197
+ "device-pool"
19198
+ ]),
19199
+ /** Native pid of the underlying Python pool (null when no pool). */
19200
+ poolPid: number().nullable(),
19201
+ /** ms since this factory was last used (null when not warm-tracked). */
19202
+ idleMs: number().nullable(),
19203
+ /** Idle TTL after which `warm-override` factories self-evict (null when not applicable). */
19204
+ idleTtlMs: number().nullable()
19205
+ })).readonly()),
19206
+ /** Warm up an engine without running a frame. No-op if already loaded. */
19207
+ spinEngine: method(object({ engine: PipelineEngineChoiceSchema }), object({ success: literal(true) }), {
19208
+ kind: "mutation",
19209
+ auth: "admin"
19210
+ }),
19211
+ /**
19212
+ * Unload an engine from RAM. `force:true` unloads even when cameras
19213
+ * are actively using it (they re-spin on next frame). Default is
19214
+ * gated — returns `{success:false, reason}` when in use.
19215
+ */
19216
+ killEngine: method(object({
19217
+ engine: PipelineEngineChoiceSchema,
19218
+ force: boolean().optional()
19219
+ }), object({
19220
+ success: boolean(),
19221
+ reason: string().optional()
19222
+ }), {
19223
+ kind: "mutation",
19224
+ auth: "admin"
19225
+ }),
19226
+ listReferenceImages: method(_void(), array(ReferenceImageEntrySchema).readonly()),
19227
+ getReferenceImage: method(object({ filename: string() }), ReferenceImageBodySchema.nullable()),
19228
+ getReferenceAudioFiles: method(_void(), array(ReferenceAudioEntrySchema).readonly()),
19229
+ getReferenceAudio: method(object({ filename: string() }), ReferenceAudioBodySchema.nullable()),
19230
+ getAudioCapabilities: method(_void(), AudioCapabilitiesSchema),
19231
+ runAudioTest: method(object({
19232
+ addonId: string(),
19233
+ modelId: string(),
19234
+ filename: string().optional(),
19235
+ settings: record(string(), unknown()).optional()
19236
+ }), AudioTestResultSchema, { kind: "mutation" }),
19237
+ getDetectionConfigSchema: method(_void(), ConfigUISchemaNullableBridge)
19238
+ }
19239
+ };
19240
+ /**
19241
+ * Per-stage gating mode applied to the zones a rule references.
19242
+ *
19243
+ * - `include`: the rule contributes to a **whitelist** for its stage.
19244
+ * When at least one `include` rule fires for a stage, only entities
19245
+ * inside one of those zones pass that stage.
19246
+ * - `exclude`: the rule contributes to a **blacklist** for its stage.
19247
+ * Entities inside one of those zones are dropped at that stage.
19248
+ *
19249
+ * `monitor`-style observation (count without filtering) is not a rule
19250
+ * mode — zones without any matching rule are observed naturally by
19251
+ * `zone-analytics` (live snapshot + history), so an "I just want to
19252
+ * count, not filter" use case needs no rule at all.
19253
+ */
19254
+ var ZoneRuleModeEnum = _enum(["include", "exclude"]);
19255
+ /**
19256
+ * Per-consumer rule that references existing zones (geometry) and
19257
+ * defines how a specific pipeline stage should treat them. Each
19258
+ * consumer addon owns its own `ZoneRule[]` array in its per-device
19259
+ * settings:
19260
+ *
19261
+ * - `addon-motion-wasm` → `motionZoneRules: ZoneRule[]` (motion stage)
19262
+ * - `addon-detection-pipeline` → `detectionZoneRules: ZoneRule[]` (detection stage)
19263
+ * - future: notification rules, audio gating, etc.
19264
+ *
19265
+ * One rule applies to N zones (`zoneIds[]`) so the operator can
19266
+ * express "ignore motion in ALL of {garden, street}" with a single
19267
+ * rule. `classFilter` narrows the rule to specific object classes —
19268
+ * "drop person detections in the street, but keep cars" is one
19269
+ * `exclude` rule with `classFilter: ['person']`.
19270
+ *
19271
+ * `enabled` is a soft toggle — the operator can keep the rule
19272
+ * configured but inert without deleting it.
19273
+ */
19274
+ var ZoneRuleSchema = object({
19275
+ /** Stable rule id — survives edits, used by the UI for diffing. */
19276
+ id: string(),
19277
+ /** Optional human-readable label rendered in the rule editor. */
19278
+ name: string().optional(),
19279
+ /** Zones this rule targets. The rule's `mode` applies to ALL
19280
+ * listed zones (OR-set: a detection in any one of them counts).
19281
+ * At least one zone id required — a rule with no targets is a
19282
+ * configuration mistake and the form validator rejects it. */
19283
+ zoneIds: array(string()).min(1).readonly(),
19284
+ mode: ZoneRuleModeEnum,
19285
+ /**
19286
+ * Class names this rule applies to. Empty / undefined ⇒ rule
19287
+ * applies to every class. Class strings match the `macroClass`
19288
+ * field on detections (e.g. `person`, `car`, `dog`).
19289
+ */
19290
+ classFilter: array(string()).readonly().optional(),
19291
+ /**
19292
+ * Minimum bbox/mask overlap (0–1) with any of the rule's zones
19293
+ * required to consider an entity "in the zone". Defaults to the
19294
+ * consumer's stage default when omitted. Kept for back-compat with
19295
+ * existing per-rule overrides; new operators pick the value via
19296
+ * `bboxInclusionPct` (operator-friendly 0–100). Whichever field is
19297
+ * set, the lower-level engine reads it as a 0–1 fraction.
19298
+ */
19299
+ overlapThreshold: number().min(0).max(1).optional(),
19300
+ /**
19301
+ * Operator-friendly version of `overlapThreshold` — the percentage
19302
+ * of the detection's bbox that must lie inside the zone for the
19303
+ * rule to match. Documented default is 85%; the engine substitutes
19304
+ * that when the field is omitted (kept optional so existing rules
19305
+ * stored without it stay valid).
19306
+ *
19307
+ * When BOTH `overlapThreshold` and `bboxInclusionPct` are set on a
19308
+ * rule, the engine prefers `bboxInclusionPct` because it's the
19309
+ * field exposed in the UI. Internally both feed the same gate.
19310
+ */
19311
+ bboxInclusionPct: number().min(0).max(100).optional(),
19312
+ /**
19313
+ * When `true` and a detection has a segmentation mask, use the
19314
+ * mask for overlap instead of the bbox. Detection-stage only;
19315
+ * motion rules ignore this field.
19316
+ */
19317
+ preferMask: boolean().optional(),
19318
+ /**
19319
+ * Soft-toggle: `false` disables the rule without deleting it.
19320
+ * Defaults to `true` so operators creating a rule via the UI
19321
+ * see it active immediately.
19322
+ */
19323
+ enabled: boolean().default(true)
19324
+ });
19325
+ array(ZoneRuleSchema).readonly();
19326
+ /**
19327
+ * Zone — pure geometry + identity. NO filtering behaviour.
19328
+ *
19329
+ * Zones describe **where** in the frame the operator wants to flag
19330
+ * something; consumer-owned {@link ZoneRule} arrays describe **how**
19331
+ * each pipeline stage uses them. Splitting the two means a single
19332
+ * polygon "Driveway" can simultaneously back a motion-exclude rule,
19333
+ * a detection-include rule on `['car']`, and an occupancy aggregate
19334
+ * — without three duplicated polygons.
19335
+ *
19336
+ * Owned by the orchestrator addon (provider) and mirrored into the
19337
+ * `zones` device-state slice on every mutation. Consumers
19338
+ * (motion-wasm, pipeline-executor, analytics, admin UI) read either
19339
+ * via `api.zones.listZones` (one-shot) or via `dev.state.zones` (live
19340
+ * mirror with `onChanged`).
19341
+ *
19342
+ * Coordinates are normalised fractions of the frame (0–1) so zones
19343
+ * survive resolution changes and stream profile switches.
19344
+ *
19345
+ * `kind` discriminates between full polygons (closed regions used
19346
+ * for intrusion / occupancy filters) and tripwires (open 2-point
19347
+ * line segments used for cross events). Onboard / firmware-reported
19348
+ * zones (Reolink, ONVIF) are out of scope for now — see the deferred
19349
+ * task list.
19350
+ */
19351
+ var ZoneKindEnum = _enum(["polygon", "tripwire"]);
19352
+ /** Polygon vertex in fraction-of-frame coordinates (0–1). */
19353
+ var PolygonPointSchema = object({
19354
+ x: number(),
19355
+ y: number()
19356
+ });
19357
+ /** A camera detection zone — pure geometry/identity. */
19358
+ var ZoneSchema = object({
19359
+ id: string(),
19360
+ name: string(),
19361
+ kind: ZoneKindEnum.default("polygon"),
19362
+ /** Polygon vertices, fraction of frame (0–1). */
19363
+ polygon: array(PolygonPointSchema).readonly(),
19364
+ /** Visual color for UI rendering. */
19365
+ color: string().default("#3b82f6")
19366
+ });
19367
+ /**
19368
+ * Zones capability — per-camera CRUD over polygon detection zones.
19369
+ *
19370
+ * Provider lives in `addon-pipeline-orchestrator` (hub-only). Persists
19371
+ * to per-device settings and mirrors into the `zones` device-state
19372
+ * slice on every mutation, so downstream consumers can subscribe via
19373
+ * `dev.state.zones.onChanged`.
19374
+ *
19375
+ * The cap surface only handles geometry + identity; filtering
19376
+ * behaviour (per-class, include/exclude, threshold) lives in the
19377
+ * consumer addons' rule arrays — see `ZoneRuleSchema` exported from
19378
+ * `capabilities/schemas/zone-rule.js`.
19379
+ */
19380
+ var zonesCapability = {
19381
+ name: "zones",
19382
+ scope: "device",
19383
+ mode: "singleton",
19384
+ deviceTypes: [DeviceType.Camera],
19385
+ methods: {
19386
+ listZones: method(object({ deviceId: number() }), array(ZoneSchema).readonly()),
19387
+ addZone: method(object({
19388
+ deviceId: number(),
19389
+ zone: ZoneSchema
19390
+ }), _void(), {
19391
+ kind: "mutation",
19392
+ auth: "admin"
19393
+ }),
19394
+ removeZone: method(object({
19395
+ deviceId: number(),
19396
+ zoneId: string()
19397
+ }), _void(), {
19398
+ kind: "mutation",
19399
+ auth: "admin"
19400
+ }),
19401
+ updateZone: method(object({
19402
+ deviceId: number(),
19403
+ zone: ZoneSchema
19404
+ }), _void(), {
19405
+ kind: "mutation",
19406
+ auth: "admin"
19407
+ })
19408
+ },
19409
+ /**
19410
+ * Runtime-state slice — the live zone catalogue mirrored by the
19411
+ * orchestrator on every CRUD mutation. Consumers read via
19412
+ * `device.state.zones.value` / `.watch(...)` without round-tripping
19413
+ * the cap, and the codegen DeviceProxy auto-wires the reactive
19414
+ * handle. Slice shape is `{ zones: Zone[] }` so future extensions
19415
+ * (e.g. zone groupings) can sit alongside the polygon list.
19416
+ */
19417
+ runtimeState: object({ zones: array(ZoneSchema).readonly() }),
19418
+ /**
19419
+ * Runtime-state durability: **restored** — written only on operator mutation, so a camera that never had one has nothing to re-derive from. This is the slice `zone-mirror-hydration.ts` exists to paper over.
19420
+ *
19421
+ * See `RuntimeStateDurability`. Enforced by
19422
+ * `scripts/check-runtime-state-durability.ts`.
19423
+ */
19424
+ durability: "restored"
19425
+ };
19426
+ /**
19427
+ * pipeline-analytics — device-scoped wrapper cap. Refines raw
19428
+ * per-frame detections emitted by the pipeline runner into tracked
19429
+ * objects, per-kind event collections (motion / object / audio), and
19430
+ * persisted media. Owns the post-detection domain end-to-end:
19431
+ *
19432
+ * runner emits PipelineInferenceResult
19433
+ * ↓ (event bus)
19434
+ * pipeline-analytics subscriber
19435
+ * ↓ SORT tracker + zone engine + state analyzer + event emitter
19436
+ * → three DB collections (one per kind), one FS media tree, one
19437
+ * unified event emitter (FrameTracked + TrackStarted/Ended +
19438
+ * DetectionEvent on bus)
19439
+ *
19440
+ * Pure subscriber model. No `processFrame` cap method — the runner
19441
+ * already publishes the raw frame on the bus. The cap surface is
19442
+ * only QUERIES + per-device settings, bound on/off via
19443
+ * `device-manager.setWrapperActive`. `defaultActive: true` because
19444
+ * every camera with a detection pipeline wants its raw detections
19445
+ * refined; operators opt out per-device via BindingsTab when needed.
19446
+ *
19447
+ * Replaces the legacy `analysis-pipeline`, `analysis-data-persistence`
19448
+ * (per-device surface) and `track-trail` caps — see P11 cleanup.
19449
+ */
19450
+ var TrackStateSchema = _enum([
19451
+ "new",
19452
+ "entered",
19453
+ "left",
19454
+ "moving",
19455
+ "idle"
19456
+ ]);
19457
+ var EventKindSchema = _enum([
19458
+ "motion",
19459
+ "object",
19460
+ "audio"
19461
+ ]);
19462
+ /**
19463
+ * Spatial filter for `listTracks` — the rect + polygon variants of the shared
19464
+ * MaskShape vocabulary (see `mask-shape.ts`). Coordinates are NORMALIZED 0..1
19465
+ * of the camera frame (top-left origin), matching the drawing-plane editor.
19466
+ */
19467
+ var TrackZoneFilterSchema = discriminatedUnion("kind", [MaskRectShapeSchema, MaskPolygonShapeSchema]);
19468
+ /** Closed icon vocabulary so clients render a known glyph per kind. */
19469
+ var EventKindIconSchema = _enum([
19470
+ "motion",
19471
+ "audio",
19472
+ "person",
19473
+ "vehicle",
19474
+ "animal",
19475
+ "door",
19476
+ "pir",
19477
+ "smoke",
19478
+ "water",
19479
+ "button",
19480
+ "package",
19481
+ "generic"
19482
+ ]);
19483
+ var EventKindCategorySchema = _enum([
19484
+ "motion",
19485
+ "audio",
19486
+ "detection",
19487
+ "sensor",
19488
+ "control",
19489
+ "custom",
19490
+ "package"
19491
+ ]);
19492
+ /** Taxonomy level — macro (timeline lane) vs sub (events-page leaf). */
19493
+ var EventKindLevelSchema = _enum(["macro", "sub"]);
19494
+ var EventKindDescriptorSchema = object({
19495
+ /** Stable kind id (e.g. 'motion', 'vehicle', 'car', 'lock'). */
19496
+ kind: string(),
19497
+ /** i18n key resolved on the UI side; `label` is the English fallback. */
19498
+ labelKey: string(),
19499
+ /** English fallback label (kept for clients that don't translate). */
19500
+ label: string(),
19501
+ /** Hex color for timeline/legend rendering. */
19502
+ color: string(),
19503
+ /** Dictionary id → lucide component on the UI side. */
19504
+ iconId: string(),
19505
+ /** Legacy closed-vocab glyph — fallback for `iconId`. */
19506
+ icon: EventKindIconSchema,
19507
+ category: EventKindCategorySchema,
19508
+ /** Macro parent for this kind ('car' → 'vehicle'); null for a macro. */
19509
+ parentKind: string().nullable(),
19510
+ /** Derived from `parentKind`, explicit for the client tree. */
19511
+ level: EventKindLevelSchema,
19512
+ /** Which cap + device contributes this kind. For built-ins the camera
19513
+ * itself; for sensor kinds the LINKED source device. */
19514
+ source: object({
19515
+ capName: string(),
19516
+ deviceId: number()
19517
+ })
19518
+ });
19519
+ /** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
19520
+ var EventKindsForDeviceSchema = object({
19521
+ deviceId: number(),
19522
+ kinds: array(EventKindDescriptorSchema).readonly()
19523
+ });
19524
+ var SensorEventSchema = object({
19525
+ id: string(),
19526
+ /** The CAMERA the event is attributed to (a sensor linked to N cameras
19527
+ * yields N rows, one per camera). */
19528
+ deviceId: number(),
19529
+ /** The linked sensor device whose state changed. */
19530
+ sourceDeviceId: number(),
19531
+ /** Event kind id — matches an `EventKindDescriptor.kind`. */
19532
+ kind: string(),
19533
+ /** Snapshot of the sensor cap's runtime-state slice at the change. */
19534
+ value: record(string(), unknown()).nullable(),
19535
+ timestamp: number()
19536
+ });
19537
+ var TrackPositionSchema = object({
19538
+ x: number(),
19539
+ y: number(),
19540
+ timestamp: number(),
19541
+ bbox: BoundingBoxSchema
19542
+ });
19543
+ var TrackSnapshotSchema = object({
19544
+ timestamp: number(),
19545
+ position: TrackPositionSchema,
19546
+ /** MediaStore key; resolve via `getTrackMedia({ trackId })`. */
19547
+ mediaKey: string()
19548
+ });
19549
+ /**
19550
+ * Normalized 0..1 trajectory envelope (min/max over every position bbox,
19551
+ * divided by the track's detection-frame dims), computed at persist time.
19552
+ * Absent when the frame dims were unknown when the track was persisted
19553
+ * (legacy rows / dims-less sources) and on active (in-RAM) tracks.
19554
+ */
19555
+ var TrackEnvelopeSchema = object({
19556
+ minX: number(),
19557
+ minY: number(),
19558
+ maxX: number(),
19559
+ maxY: number()
19560
+ });
19561
+ /**
19562
+ * Row projection for track list queries. `full` (default) returns the
19563
+ * complete Track including the frame-rate `positions[]` history and the
19564
+ * `snapshots[]` references — megabytes across a page of tracks. `slim`
19565
+ * keeps every scalar the list surfaces actually render (ids, class(es),
19566
+ * label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
19567
+ * zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
19568
+ * `snapshots` as EMPTY arrays — detail views re-fetch the full row via
19569
+ * `getTrack`. Mirrors the event-store `projection` convention
19570
+ * (`getObjectEvents` et al.).
19571
+ */
19572
+ var TrackProjectionSchema = _enum(["full", "slim"]);
19573
+ /**
19574
+ * One audio-classification label heard on the track's camera while the
19575
+ * track was alive, aggregated per label. An "episode" is one persisted
19576
+ * audio event (the confident-classification path: score ≥ the device's
19577
+ * `classificationMinScore`, class-change-or-heartbeat coalesced) — NOT
19578
+ * one 32 ms inference chunk, so counts stay human-scaled.
19579
+ */
19580
+ var TrackAudioLabelSchema = object({
19581
+ label: string(),
19582
+ /** Highest classification score observed across the label's episodes. */
19583
+ peakScore: number(),
19584
+ /** Number of coalesced audio-event episodes carrying this label. */
19585
+ count: number(),
19586
+ firstAt: number(),
19587
+ lastAt: number()
19588
+ });
19589
+ /**
19590
+ * How a track was produced. `pipeline` (default / absent) = the spatial
19591
+ * detection+tracking pipeline. Every OTHER value is a SYNTHETIC projection —
19592
+ * no positions, a single snapshot, and no bbox trajectory at all:
19593
+ *
19594
+ * - `sensor` — a linked sensor/control device state change.
19595
+ * - `audio` — an audio event on the camera itself that was anomalous for
19596
+ * THAT camera, loud, and heard while nothing visual was happening (D62).
19597
+ *
19598
+ * The spatial subsystems (tracker association, occupancy count, re-id /
19599
+ * embedding, resurrection) MUST skip every synthetic source. Test for that
19600
+ * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
19601
+ * check silently readmits every source added after it was written.
19602
+ */
19603
+ var TrackSourceSchema = _enum([
19604
+ "pipeline",
19605
+ "sensor",
19606
+ "audio"
19607
+ ]);
19608
+ /**
19609
+ * Where a track sits in the RETRAIN lifecycle (D81).
19610
+ *
19611
+ * - `none` — never marked, or un-marked. Evictable.
19612
+ * - `staging` — the operator wants this track as training material and has not
19613
+ * finished with it. **This is the only state retention holds**: the track and
19614
+ * everything it owns (object events, crops, keyframes, CLIP vector) survive
19615
+ * the device's age window.
19616
+ * - `trained` — the retrain page has taken what it needed. The frames it chose
19617
+ * were COPIED into the retrain dataset at selection time, so the dataset no
19618
+ * longer depends on the track's media and the track becomes EVICTABLE again.
19619
+ * Terminal for the plain `markForTrain` toggle: returning it to `staging` is
19620
+ * a deliberate action of the retrain page, not a side effect of a checkbox.
19621
+ *
19622
+ * There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
19623
+ * the store's filter language has only positive equality and `whereIn` — no
19624
+ * negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
19625
+ * would make the entire pre-column history immortal in one deploy.
19626
+ */
19627
+ var RetrainStatusSchema = _enum([
18638
19628
  "none",
18639
19629
  "staging",
18640
19630
  "trained"
@@ -19522,6 +20512,24 @@ var RebuildStatusSchema = object({
19522
20512
  /** Present when the pass ended by throwing. */
19523
20513
  error: string().nullable()
19524
20514
  });
20515
+ var ReplayFrameInputSchema = object({
20516
+ timestamp: number(),
20517
+ frame: PipelineRunResultBridge
20518
+ });
20519
+ var RunReplayFrameProcessorResultSchema = object({ tracks: array(object({
20520
+ className: string(),
20521
+ firstSeenMs: number(),
20522
+ lastSeenMs: number(),
20523
+ /** Bbox of the track's FIRST matched detection, pixel-space in the clip's
20524
+ * frame — a representative box for the diff's `(className, window, IoU)`
20525
+ * pairing (`replay-diff.ts`). A replay does not need the full per-frame
20526
+ * trajectory production's `Track.positions` keeps. */
20527
+ bbox: BoundingBoxSchema,
20528
+ /** How many of the input frames this track matched a real detection on
20529
+ * (never a coasted/extrapolated frame) — the replay's own signal for "how
20530
+ * solid is this track", cheaper than re-deriving it from a trajectory. */
20531
+ framesMatched: number().int()
20532
+ })).readonly() });
19525
20533
  var pipelineAnalyticsCapability = {
19526
20534
  name: "pipeline-analytics",
19527
20535
  scope: "device",
@@ -19807,931 +20815,401 @@ var pipelineAnalyticsCapability = {
19807
20815
  * Backed by a declared pipeline-analytics SQLite collection. */
19808
20816
  /** Internal migration-participant lease. It drains active MediaStore
19809
20817
  * writes and refuses new ones without changing analytics bindings. */
19810
- pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19811
- kind: "mutation",
19812
- auth: "admin"
19813
- }),
19814
- resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
19815
- kind: "mutation",
19816
- auth: "admin"
19817
- }),
19818
- /** Drops cached location roots after the storage coordinator repoints a
19819
- * default so future event-media writes use the new root. */
19820
- refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
19821
- kind: "mutation",
19822
- auth: "admin"
19823
- }),
19824
- startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
19825
- kind: "mutation",
19826
- auth: "admin"
19827
- }),
19828
- getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
19829
- cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
19830
- kind: "mutation",
19831
- auth: "admin"
19832
- }),
19833
- listOpsLog: method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
19834
- kind: "query",
19835
- auth: "admin"
19836
- }),
19837
- /**
19838
- * The CHEAP QUESTION, asked before any media moves: how big is the dataset
19839
- * the marked (`markForTrain`) tracks would produce?
19840
- *
19841
- * Answered from media INDEX rows only — key, kind, size, timestamp — so it
19842
- * costs ~2 KB of reads per track and no blob reads at all. The measured harm
19843
- * behind D56 was a bulk pass that read and base64'd every blob a track owned
19844
- * before deciding anything, taking hub-main to 82 s busy out of 120; an
19845
- * export is that same I/O shape, so it inherits the same discipline: know
19846
- * the size, then decide.
19847
- *
19848
- * `truncated` reports that more marked tracks exist than one pass carries.
19849
- * Empty `deviceIds` ⇒ every device that has marked tracks.
19850
- */
19851
- getTrainingExportSummary: method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
19852
- kind: "query",
19853
- auth: "admin"
19854
- }),
19855
- /**
19856
- * Where to download the dataset archive.
19857
- *
19858
- * The BYTES do not come back through this cap — they come from the returned
19859
- * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
19860
- * archive base64'd through a unary RPC envelope would be held whole in
19861
- * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
19862
- * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
19863
- * precedent, and this follows it deliberately.
19864
- *
19865
- * The archive contains a `manifest.json` FIRST, then the stored media
19866
- * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
19867
- * model is run: a training set's pixels must be the pixels the pipeline saw.
19868
- */
19869
- getTrainingExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
19870
- kind: "query",
19871
- auth: "admin"
19872
- }),
19873
- /**
19874
- * The staging worklist for one camera, or for every camera that has one.
19875
- *
19876
- * Fetched ON DEMAND, over the staging set only — the page never scans
19877
- * history, because making the working set small is the entire purpose of
19878
- * the mark. Each row carries how many frames the dataset already holds from
19879
- * the track and how many subjects were annotated on them, so
19880
- * `frameCount: 0` reads as "still to work" without a second call per track.
19881
- *
19882
- * `auth: 'admin'`, unlike the viewer-level mark itself: marking a track is
19883
- * curation you do while looking at it, but building the training set the
19884
- * fleet's models are fine-tuned on is not.
19885
- */
19886
- listRetrainStaging: method(object({
19887
- /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
19888
- * `deviceId`, deliberately: `deviceId` would make this device-bound and
19889
- * route it at one camera's owner, and "every camera" would stop being
19890
- * expressible at all. */
19891
- deviceIds: array(number()).optional(),
19892
- limit: number().int().min(1).max(500).optional()
19893
- }), array(RetrainTrackSchema).readonly(), {
19894
- kind: "query",
19895
- auth: "admin"
19896
- }),
19897
- /**
19898
- * What a track can contribute, and what it already has.
19899
- *
19900
- * `candidates` are the track's whole, unannotated frames — index rows only,
19901
- * so this is cheap. `copies` are the frames already inside the dataset, and
19902
- * a candidate whose copy exists is marked `copied: true`: selecting it again
19903
- * is free and CANNOT fail, whatever became of the original.
19904
- *
19905
- * A crop, a thumbnail and `fullFrameBoxed` are never candidates. The last
19906
- * one matters most: it has the model's own rectangle burned into the pixels,
19907
- * and a detector trained on it learns to find a green line.
19908
- */
19909
- listRetrainFrames: method(object({ trackId: string() }), RetrainFrameListSchema, {
19910
- kind: "query",
19911
- auth: "admin"
19912
- }),
19913
- /**
19914
- * COPY-ON-SELECT — the write that makes `trained` safe to evict.
19915
- *
19916
- * Selecting a frame copies its bytes into retrain storage immediately: not
19917
- * a reference, not a lease. Once the copy exists the dataset no longer
19918
- * depends on the track's media, which is exactly what lets D81 hand a
19919
- * `trained` track back to retention.
19920
- *
19921
- * The order inside is load-bearing and is pinned by a test: an EXISTING
19922
- * copy is returned without touching the source, so an original that
19923
- * evaporated blocks the selection of THAT ORIGINAL and never the copy
19924
- * already taken. Every refusal comes back named — a dropped selection is
19925
- * never silent, on the wire or in the log.
19926
- */
19927
- selectRetrainFrames: method(object({
19928
- deviceId: number(),
19929
- trackId: string(),
19930
- mediaKeys: array(string()).min(1)
19931
- }), RetrainFrameSelectionSchema, {
20818
+ pauseForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
19932
20819
  kind: "mutation",
19933
20820
  auth: "admin"
19934
20821
  }),
19935
- /** Un-select a frame: its annotations go first, then the copy and its blob.
19936
- * Deliberately destructive and deliberately explicit — it is the only way
19937
- * a frame leaves the dataset before export. */
19938
- deselectRetrainFrame: method(object({
19939
- deviceId: number(),
19940
- trackId: string(),
19941
- frameId: string()
19942
- }), object({
19943
- removed: boolean(),
19944
- removedAnnotations: number().int()
19945
- }), {
20822
+ resumeForStorageMigration: method(StorageMigrationLeaseInputSchema, object({ resumed: literal(true) }), {
19946
20823
  kind: "mutation",
19947
20824
  auth: "admin"
19948
20825
  }),
19949
- /**
19950
- * The pixels of ONE copied frame, base64.
19951
- *
19952
- * Through the cap rather than a data plane because it is genuinely one
19953
- * frame at a time, on demand, at human speed — the shape D9/D18 permit
19954
- * (what they forbid is frames crossing a boundary at frame RATE). The
19955
- * annotation canvas needs the image and its exact dimensions in the same
19956
- * answer: a canvas that places a normalised box against a size it guessed
19957
- * draws every box in the wrong place.
19958
- */
19959
- getRetrainFrameImage: method(object({ frameId: string() }), object({
19960
- base64: string(),
19961
- width: number().int(),
19962
- height: number().int()
19963
- }), {
19964
- kind: "query",
20826
+ /** Drops cached location roots after the storage coordinator repoints a
20827
+ * default so future event-media writes use the new root. */
20828
+ refreshStorageLocationsForMigration: method(StorageMigrationLeaseInputSchema, object({ refreshed: literal(true) }), {
20829
+ kind: "mutation",
19965
20830
  auth: "admin"
19966
20831
  }),
19967
- /**
19968
- * Ask the pipeline what it sees, as a PROPOSAL.
19969
- *
19970
- * Runs through `pipelineRunner.runStatelessStep` on the COPIED frame, and
19971
- * every box comes back as a draft with `source: 'assist'` plus the model and
19972
- * score that produced it. The operator confirms, edits, adds and deletes;
19973
- * nothing is stored until `saveRetrainAnnotations`.
19974
- *
19975
- * For packages the request is `rfdetr-package` on the ZONE CROP at 0.35 —
19976
- * never the whole frame, where a package detector at that threshold proposes
19977
- * furniture. A package request with no zone is REFUSED rather than widened,
19978
- * because the silent widening would look like a bad model for as long as
19979
- * nobody checked which rectangle it ran on.
19980
- */
19981
- proposeRetrainAnnotations: method(object({
19982
- deviceId: number(),
19983
- trackId: string(),
19984
- frameId: string(),
19985
- subject: RetrainAssistSubjectSchema,
19986
- /** Which node runs it. Absent ⇒ wherever an unowned call lands. */
19987
- nodeId: string().optional()
19988
- }), RetrainAssistResultSchema, {
20832
+ startStorageMigrationMove: method(StorageMigrationMediaMoveInputSchema, object({ jobId: string() }), {
19989
20833
  kind: "mutation",
19990
20834
  auth: "admin"
19991
20835
  }),
19992
- /** Every annotation on a track, oldest first. */
19993
- listRetrainAnnotations: method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
19994
- kind: "query",
20836
+ getStorageMigrationMoveStatus: method(object({ jobId: string() }), RelocateJobSchema.nullable(), { auth: "admin" }),
20837
+ cancelStorageMigrationMove: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20838
+ kind: "mutation",
19995
20839
  auth: "admin"
19996
20840
  }),
19997
20841
  /**
19998
- * Replace EVERY annotation on one frame with the supplied set.
19999
- *
20000
- * Whole-frame replacement, not per-box upsert: the unit of ground truth is
20001
- * the frame, and "the operator deleted a box" must be the same durable
20002
- * outcome as "the operator never drew it". A per-box patch would let a frame
20003
- * keep a box the operator removed on a surface that only knew about the
20004
- * boxes it sent.
20005
- *
20006
- * Refuses a macro class typed into `label` or `subLabel` — the tiers are
20007
- * separate and the guard is at the WRITE, because a mixed taxonomy cannot
20008
- * be un-mixed by reading it.
20842
+ * Moves event media between locations the OPERATOR's mover, without the
20843
+ * coordinator's lease. Twin of `recording.relocateFootage`: the same
20844
+ * engine the lease-gated coordinated migration uses
20845
+ * (`startStorageMigrationMove`), armable in the background and WITHOUT
20846
+ * pausing anything. Exists because `eventMedia` was the only storage
20847
+ * class whose only move path went through the recorder's global pause.
20009
20848
  */
20010
- saveRetrainAnnotations: method(object({
20011
- deviceId: number(),
20012
- trackId: string(),
20013
- frameId: string(),
20014
- annotations: array(RetrainAnnotationDraftSchema)
20015
- }), array(RetrainAnnotationSchema).readonly(), {
20849
+ relocateMedia: method(RelocateMediaInputSchema, object({ jobId: string() }), {
20016
20850
  kind: "mutation",
20017
20851
  auth: "admin"
20018
20852
  }),
20019
- /**
20020
- * Finish with a track: `staging trained`. **The only writer of that
20021
- * state** D81 shipped the column with it deliberately unreachable.
20022
- *
20023
- * Refuses a track the dataset holds no copies from. `trained` un-pins the
20024
- * track's media, so completing without a copy is a delete order for material
20025
- * nothing ever extracted anything from; that refusal IS the safety argument
20026
- * of D81, expressed as a precondition.
20027
- */
20028
- completeRetrainTrack: method(object({
20029
- deviceId: number(),
20030
- trackId: string()
20031
- }), RetrainTransitionResultSchema, {
20032
- kind: "mutation",
20853
+ /** Every relocate job this addon knows about, newest first (in RAM: the
20854
+ * move is resumable, so a lost list costs nothing but the display). */
20855
+ listRelocateMediaJobs: method(object({}), array(RelocateJobSchema).readonly(), {
20856
+ kind: "query",
20033
20857
  auth: "admin"
20034
20858
  }),
20035
- /**
20036
- * The deliberate return: `trained staging`, for the rare case.
20037
- *
20038
- * The generic `setTrackFlags` toggle refuses this in both directions by
20039
- * design (D81) — re-staging from a checkbox is how the same material gets
20040
- * annotated twice under two ground truths. Doing it here means the operator
20041
- * is looking at the annotations that already exist while they decide, and
20042
- * those annotations are LEFT ALONE: "put this back" must not be a
20043
- * destructive act wearing a navigational name.
20044
- */
20045
- restageRetrainTrack: method(object({
20046
- deviceId: number(),
20047
- trackId: string()
20048
- }), RetrainTransitionResultSchema, {
20859
+ /** Cancel a running or queued relocate job. */
20860
+ cancelRelocateMedia: method(object({ jobId: string() }), object({ cancelled: boolean() }), {
20049
20861
  kind: "mutation",
20050
20862
  auth: "admin"
20051
20863
  }),
20864
+ listOpsLog: method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
20865
+ kind: "query",
20866
+ auth: "admin"
20867
+ }),
20052
20868
  /**
20053
- * Where to download the ANNOTATED dataset.
20869
+ * The CHEAP QUESTION, asked before any media moves: how big is the dataset
20870
+ * the marked (`markForTrain`) tracks would produce?
20054
20871
  *
20055
- * The sibling of `getTrainingExportUrl` and deliberately not the same
20056
- * archive: that one streams a marked track's stored media verbatim, this one
20057
- * streams the retrain COPIES plus an `annotations.json` carrying, for every
20058
- * subject, the canonical full-frame box AND the geometry derived for each
20059
- * model shape (letterboxed root / zone-cropped package / subject-cropped
20060
- * classifier). Derived at export, never stored — one box in, three shapes
20061
- * out, so two crops of the same subject can never end up in one feature
20062
- * space (D52).
20872
+ * Answered from media INDEX rows only key, kind, size, timestamp — so it
20873
+ * costs ~2 KB of reads per track and no blob reads at all. The measured harm
20874
+ * behind D56 was a bulk pass that read and base64'd every blob a track owned
20875
+ * before deciding anything, taking hub-main to 82 s busy out of 120; an
20876
+ * export is that same I/O shape, so it inherits the same discipline: know
20877
+ * the size, then decide.
20878
+ *
20879
+ * `truncated` reports that more marked tracks exist than one pass carries.
20880
+ * Empty `deviceIds` ⇒ every device that has marked tracks.
20063
20881
  */
20064
- getRetrainExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
20882
+ getTrainingExportSummary: method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
20065
20883
  kind: "query",
20066
20884
  auth: "admin"
20067
20885
  }),
20068
- getEventMedia: method(object({
20069
- eventId: string(),
20070
- kind: MediaFileKindEnum.optional(),
20071
- deviceId: number()
20072
- }), array(MediaFileSchema).readonly()),
20073
- /** All media rows owned by a track. `kinds` narrows to a kind subset so a
20074
- * client can fetch the SMALL display variants on open and pull the
20075
- * multi-MB native variants only on demand (mirrors `getEventMedia.kind`).
20076
- * Absent ⇒ every kind (back-compat). */
20077
- getTrackMedia: method(object({
20078
- trackId: string(),
20079
- kinds: array(MediaFileKindEnum).optional(),
20080
- deviceId: number()
20081
- }), array(MediaFileSchema).readonly()),
20082
20886
  /**
20083
- * What media a track HAS, without any of it.
20887
+ * Where to download the dataset archive.
20084
20888
  *
20085
- * The detail view needs the shape of a track's media to build its strip
20086
- * which kinds exist, in what order, at what size and then wants each tile
20087
- * fetched as an image, not as base64 inside this response. Measured: the
20088
- * full `getTrackMedia` is 5.3-7.7 MB and blocks the view; this manifest is
20089
- * ~2 KB.
20889
+ * The BYTES do not come back through this cap they come from the returned
20890
+ * data-plane URL, which streams a tar built entry by entry. A multi-gigabyte
20891
+ * archive base64'd through a unary RPC envelope would be held whole in
20892
+ * memory twice on a hub this repo has already OOM'd once (D9/D18 are the
20893
+ * same lesson about frames). `getDownloadUrl` on `recordingExport` is the
20894
+ * precedent, and this follows it deliberately.
20090
20895
  *
20091
- * It also restores a fact a `kinds` filter destroys: filtering
20092
- * `getTrackMedia` drops whole ROWS, taking `kind` and `sizeBytes` with
20093
- * them, so a client that fetched only the small variants could no longer
20094
- * tell a full-resolution variant existed — and the affordance that opens it
20095
- * would silently disappear.
20096
- */
20097
- listTrackMedia: method(object({
20098
- trackId: string(),
20099
- deviceId: number()
20100
- }), array(MediaFileInfoSchema).readonly()),
20101
- /**
20102
- * Search object events by text query using CLIP cosine similarity.
20103
- * Encodes `text` via the `embedding-encoder` cap, queries the
20104
- * `ObjectEmbeddingStore` with optional prefilters, ranks all matching
20105
- * embeddings by cosine similarity, and joins winners to their
20106
- * ObjectEvents by trackId. Returns up to `limit` events scored ≥
20107
- * `minScore`, sorted descending by score.
20896
+ * The archive contains a `manifest.json` FIRST, then the stored media
20897
+ * VERBATIM under `tracks/<deviceId>/<trackId>/…`. No crop is derived and no
20898
+ * model is run: a training set's pixels must be the pixels the pipeline saw.
20108
20899
  */
20109
- searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()),
20110
- wipeObjectEmbeddings: method(object({}), WipeObjectEmbeddingsResultSchema, {
20111
- kind: "mutation",
20112
- auth: "admin"
20113
- }),
20114
- rebuildObjectEmbeddings: method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
20115
- kind: "mutation",
20900
+ getTrainingExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
20901
+ kind: "query",
20116
20902
  auth: "admin"
20117
20903
  }),
20118
- getObjectEmbeddingRebuildStatus: method(object({}), RebuildStatusSchema)
20119
- },
20120
- events: {
20121
20904
  /**
20122
- * Enriched frame emitted after refinement the live-overlay source of
20123
- * truth (two-plane re-injection). Carries the frame's detections in the
20124
- * `ObjectDetection` wire shape: first-level roots (with track info +
20125
- * enrichment labels) plus synthesized `kind:'detail'` face/plate entries
20126
- * re-projected from per-track detail state, so stream overlays render
20127
- * boxes + recognized names without querying full Track state.
20905
+ * The staging worklist for one camera, or for every camera that has one.
20906
+ *
20907
+ * Fetched ON DEMAND, over the staging set only — the page never scans
20908
+ * history, because making the working set small is the entire purpose of
20909
+ * the mark. Each row carries how many frames the dataset already holds from
20910
+ * the track and how many subjects were annotated on them, so
20911
+ * `frameCount: 0` reads as "still to work" without a second call per track.
20912
+ *
20913
+ * `auth: 'admin'`, unlike the viewer-level mark itself: marking a track is
20914
+ * curation you do while looking at it, but building the training set the
20915
+ * fleet's models are fine-tuned on is not.
20128
20916
  */
20129
- onFrameTracked: { data: object({
20130
- deviceId: number(),
20131
- timestamp: number(),
20132
- frameWidth: number(),
20133
- frameHeight: number(),
20134
- detections: array(OverlayDetectionSchema).readonly()
20135
- }) },
20136
- /** Track entered active state (first-seen). */
20137
- onTrackStarted: { data: object({
20138
- deviceId: number(),
20139
- trackId: string(),
20140
- className: string()
20141
- }) },
20142
- /** Track expired (TTL reached after last detection). */
20143
- onTrackEnded: { data: object({
20144
- deviceId: number(),
20145
- trackId: string(),
20146
- className: string(),
20147
- durationMs: number()
20148
- }) },
20149
- /** Canonical "something happened at device X" event, per-kind. */
20150
- onDetectionEvent: { data: object({
20151
- deviceId: number(),
20152
- kind: EventKindSchema,
20153
- eventId: string(),
20154
- timestamp: number()
20155
- }) }
20156
- }
20157
- };
20158
- /**
20159
- * Reference to the frame's retained NATIVE surface + the parent crop's placement
20160
- * within the frame, so the executor can re-cut a leaf child ROI at native
20161
- * resolution on the detail plane. See the `runPipeline` `nativeCropRef` field.
20162
- */
20163
- var NativeCropRefSchema = object({
20164
- /** Handle keying the retained native surface (node-pinned to its owner). */
20165
- handle: FrameHandleSchema,
20166
- /** The parent crop's padded/clamped rectangle in FRAME-space pixels. */
20167
- cropFrameSpace: object({
20168
- x: number(),
20169
- y: number(),
20170
- w: number(),
20171
- h: number()
20172
- })
20173
- });
20174
- object({
20175
- crop: object({
20176
- left: number(),
20177
- top: number(),
20178
- width: number().positive(),
20179
- height: number().positive()
20180
- }).optional(),
20181
- content: object({
20182
- width: number().int().positive(),
20183
- height: number().int().positive()
20184
- }),
20185
- fit: _enum(["stretch", "contain"]),
20186
- format: _enum([
20187
- "rgb",
20188
- "gray",
20189
- "jpeg"
20190
- ])
20191
- });
20192
- var FrameRefSchema = object({
20193
- registryId: string().min(1),
20194
- id: string().min(1),
20195
- width: number().int().positive(),
20196
- height: number().int().positive(),
20197
- format: _enum(["rgb", "gray"]),
20198
- timestamp: number(),
20199
- capturedAt: number().optional()
20200
- });
20201
- var ModelFormatSchema$1 = _enum([
20202
- "onnx",
20203
- "coreml",
20204
- "openvino",
20205
- "tflite",
20206
- "pt",
20207
- "gguf"
20208
- ]);
20209
- var PipelineSlotSchema = _enum([
20210
- "detector",
20211
- "cropper",
20212
- "classifier",
20213
- "refiner",
20214
- "audio-classifier"
20215
- ]);
20216
- var PipelineEngineChoiceSchema = object({
20217
- runtime: _enum(["node", "python"]),
20218
- backend: string(),
20219
- format: ModelFormatSchema$1,
20220
- device: string().optional()
20221
- });
20222
- var AvailableEngineSchema = object({
20223
- engine: PipelineEngineChoiceSchema,
20224
- devices: array(object({
20225
- id: string(),
20226
- label: string(),
20227
- description: string().optional()
20228
- })).readonly(),
20229
- defaultDevice: string()
20230
- });
20231
- var PipelineDefaultStepSchema = lazy(() => object({
20232
- addonId: string(),
20233
- addonName: string(),
20234
- slot: PipelineSlotSchema,
20235
- inputClasses: array(string()).readonly(),
20236
- outputClasses: array(string()).readonly(),
20237
- enabled: boolean(),
20238
- modelId: string(),
20239
- children: array(PipelineDefaultStepSchema).readonly(),
20240
- group: string().optional(),
20241
- settings: record(string(), unknown()).optional()
20242
- }));
20243
- var PipelineTemplateStepSchema = lazy(() => object({
20244
- addonId: string(),
20245
- enabled: boolean(),
20246
- modelId: string(),
20247
- children: array(PipelineTemplateStepSchema).readonly(),
20248
- settings: record(string(), unknown()).optional()
20249
- }));
20250
- var PipelineTemplateSchema$1 = object({
20251
- id: string(),
20252
- name: string(),
20253
- createdAt: string(),
20254
- updatedAt: string(),
20255
- engine: PipelineEngineChoiceSchema,
20256
- steps: array(PipelineTemplateStepSchema).readonly()
20257
- });
20258
- var PipelineModelOptionSchema = object({
20259
- id: string(),
20260
- name: string(),
20261
- formats: record(string(), object({
20262
- downloaded: boolean(),
20263
- sizeMB: number()
20264
- })),
20265
- group: ModelVariantGroupSchema.optional(),
20266
- legacy: boolean().optional(),
20267
- provider: ModelProviderIdSchema.optional()
20268
- });
20269
- var ConfigFieldBridge = custom();
20270
- var PipelineAddonSchemaSchema = object({
20271
- id: string(),
20272
- name: string(),
20273
- slot: PipelineSlotSchema,
20274
- inputClasses: array(string()).readonly(),
20275
- outputClasses: array(string()).readonly(),
20276
- childSlots: array(PipelineSlotSchema).readonly(),
20277
- models: array(PipelineModelOptionSchema).readonly(),
20278
- defaultModelId: string(),
20279
- defaultModelIdByFormat: record(string(), string()).optional(),
20280
- enabledByDefault: boolean().optional(),
20281
- backfillIntoExistingOverrides: boolean().optional(),
20282
- defaultConfidence: number(),
20283
- group: string().optional(),
20284
- configSchema: array(ConfigFieldBridge).readonly().optional()
20285
- });
20286
- var PipelineSlotSchemaSchema = object({
20287
- id: PipelineSlotSchema,
20288
- label: string(),
20289
- priority: number(),
20290
- parentSlot: PipelineSlotSchema.nullable(),
20291
- addons: array(PipelineAddonSchemaSchema).readonly()
20292
- });
20293
- var PipelineSchemaSchema = object({
20294
- availableEngines: array(AvailableEngineSchema).readonly(),
20295
- selectedEngine: PipelineEngineChoiceSchema,
20296
- slots: array(PipelineSlotSchemaSchema).readonly()
20297
- });
20298
- var EngineProvisioningSchema = object({
20299
- runtimeId: _enum([
20300
- "onnx",
20301
- "openvino",
20302
- "coreml",
20303
- "edgetpu"
20304
- ]).nullable(),
20305
- device: string().nullable(),
20306
- state: _enum([
20307
- "idle",
20308
- "installing",
20309
- "verifying",
20310
- "ready",
20311
- "failed"
20312
- ]),
20313
- progress: number().optional(),
20314
- error: string().optional(),
20315
- nextRetryAt: number().optional(),
20316
- /**
20317
- * Gate A (config-correctness gate at engine change): human-readable
20318
- * config issues surfaced EAGERLY when the node's engine changes — model
20319
- * substitutions ("chose X, running Y") and zero-build steps ("no model
20320
- * has a <format> build"). Additive/optional: informational only, never
20321
- * enforced here — `assertEngineReady` (readiness) still gates inference.
20322
- * Absent/empty when the node-default tree resolves cleanly.
20323
- */
20324
- configIssues: array(string()).optional()
20325
- });
20326
- var PipelineStepInputSchema = lazy(() => object({
20327
- addonId: string(),
20328
- modelId: string().optional(),
20329
- enabled: boolean().default(true),
20330
- children: array(PipelineStepInputSchema).optional(),
20331
- settings: record(string(), unknown()).optional(),
20332
- jumpDeviceKey: string().optional()
20333
- }));
20334
- var ModelSubstitutionSchema = object({
20335
- addonId: string(),
20336
- chosen: string(),
20337
- running: string(),
20338
- format: string()
20339
- });
20340
- var PipelineValidationIssueSchema = object({
20341
- addonId: string(),
20342
- kind: _enum(["unknown-addon", "no-format-build"]),
20343
- detail: string()
20344
- });
20345
- var PipelineValidationResultSchema = object({
20346
- ok: boolean(),
20347
- issues: array(PipelineValidationIssueSchema).readonly(),
20348
- substitutions: array(ModelSubstitutionSchema).readonly(),
20349
- /** The node's `currentEngine.format` this validation ran against. */
20350
- format: string()
20351
- });
20352
- var ReferenceImageEntrySchema = object({
20353
- filename: string(),
20354
- stepIds: array(string()).readonly().optional()
20355
- });
20356
- var ReferenceImageBodySchema = object({
20357
- base64: string(),
20358
- filename: string()
20359
- });
20360
- var ReferenceAudioEntrySchema = object({
20361
- filename: string(),
20362
- sizeKb: number()
20363
- });
20364
- var ReferenceAudioBodySchema = object({ base64: string() });
20365
- var AudioBackendSchema = object({
20366
- id: string(),
20367
- name: string(),
20368
- description: string(),
20369
- available: boolean(),
20370
- /**
20371
- * Raw classifier labels this backend can emit (e.g. YAMNet's
20372
- * 521-class set or Apple SoundAnalysis's 303-class set). Used by
20373
- * the benchmark UI to populate the `enabledMicroClasses` filter
20374
- * specific to the selected backend without a separate fetch.
20375
- */
20376
- rawLabels: array(string()).readonly().optional()
20377
- });
20378
- var AudioCapabilitiesSchema = object({
20379
- activeBackend: string(),
20380
- availableBackends: array(AudioBackendSchema).readonly(),
20381
- sampleRate: number(),
20382
- chunkDurationMs: number()
20383
- });
20384
- var DownloadModelResultSchema = object({
20385
- filePath: string(),
20386
- sizeMB: number(),
20387
- durationMs: number()
20388
- });
20389
- /**
20390
- * Wrapper carrying a single test run's result. Replaces the legacy
20391
- * ad-hoc `{labels: [{className, originalClass, score}]}` shape with the
20392
- * canonical `AudioResult` from the Phase 6 output rework: one
20393
- * `AudioDetection` per class above `minScore`, top-N candidates in
20394
- * `debug.alternateLabels['audio-classifier']`, per-source timings in
20395
- * `debug.stepTimings`. The outer `success`/`error` fields stay so the
20396
- * benchmark UI can still report a clean failure when the classifier
20397
- * cap isn't available.
20398
- */
20399
- var AudioTestResultSchema = object({
20400
- success: boolean(),
20401
- error: string().optional(),
20402
- frame: custom().optional()
20403
- });
20404
- var PipelineConfigBridge = custom();
20405
- var ConfigUISchemaBridge = custom();
20406
- var ConfigUISchemaNullableBridge = custom();
20407
- var InferenceCapabilitiesBridge = custom();
20408
- var ModelAvailabilityListBridge = custom();
20409
- var PipelineRunResultBridge = custom();
20410
- /**
20411
- * Pipeline executor — detection engine + configuration + inference API.
20412
- *
20413
- * Merged from: pipeline-executor, pipeline-config, inference, detection-config.
20414
- * Implemented by the detection-pipeline addon.
20415
- *
20416
- * Per-device surface (DeviceSettingsContribution + the "is detection
20417
- * enabled for this camera?" toggle) lives on the paired
20418
- * `detection-pipeline` cap (device-scoped, singleton, wrapper
20419
- * defaultActive) — same split pattern used by stream-broker /
20420
- * camera-streams and audio-analyzer / audio-analysis.
20421
- */
20422
- var pipelineExecutorCapability = {
20423
- name: "pipeline-executor",
20424
- scope: "system",
20425
- mode: "singleton",
20426
- methods: {
20427
- getAvailableEngines: method(_void(), array(PipelineEngineChoiceSchema)),
20428
- getSelectedEngine: method(_void(), PipelineEngineChoiceSchema),
20429
- getDefaultSteps: method(PipelineEngineChoiceSchema, array(PipelineDefaultStepSchema)),
20917
+ listRetrainStaging: method(object({
20918
+ /** Empty ⇒ every camera that has staging tracks. A LIST, not a single
20919
+ * `deviceId`, deliberately: `deviceId` would make this device-bound and
20920
+ * route it at one camera's owner, and "every camera" would stop being
20921
+ * expressible at all. */
20922
+ deviceIds: array(number()).optional(),
20923
+ limit: number().int().min(1).max(500).optional()
20924
+ }), array(RetrainTrackSchema).readonly(), {
20925
+ kind: "query",
20926
+ auth: "admin"
20927
+ }),
20430
20928
  /**
20431
- * Per-node detection-engine provisioning snapshot. Returns the live
20432
- * state of the lazy runtime-provisioning machine on `nodeId`
20433
- * (idle / installing / verifying / ready / failed). The UI pairs this
20434
- * one-shot query with the `pipeline.engine-provisioning` live event
20435
- * (emitted on every transition) to drive a per-node "engine ready?"
20436
- * indicator without polling. Phase 2.
20929
+ * What a track can contribute, and what it already has.
20930
+ *
20931
+ * `candidates` are the track's whole, unannotated frames index rows only,
20932
+ * so this is cheap. `copies` are the frames already inside the dataset, and
20933
+ * a candidate whose copy exists is marked `copied: true`: selecting it again
20934
+ * is free and CANNOT fail, whatever became of the original.
20935
+ *
20936
+ * A crop, a thumbnail and `fullFrameBoxed` are never candidates. The last
20937
+ * one matters most: it has the model's own rectangle burned into the pixels,
20938
+ * and a detector trained on it learns to find a green line.
20437
20939
  */
20438
- getEngineProvisioning: method(object({ nodeId: string() }), EngineProvisioningSchema),
20439
- getVideoPipelineSteps: method(_void(), record(string(), object({
20440
- modelId: string(),
20441
- settings: record(string(), unknown()).readonly()
20442
- }))),
20443
- setVideoPipelineSteps: method(object({ steps: record(string(), object({
20444
- modelId: string(),
20445
- settings: record(string(), unknown()).readonly()
20446
- })) }), object({ success: literal(true) }), {
20447
- kind: "mutation",
20940
+ listRetrainFrames: method(object({ trackId: string() }), RetrainFrameListSchema, {
20941
+ kind: "query",
20448
20942
  auth: "admin"
20449
20943
  }),
20450
20944
  /**
20451
- * Clear THIS node's executor-side PER-DEVICE settings stores (the
20452
- * per-camera step overrides the object-detection root reads via
20453
- * `applyDeviceOverridesToTree`). `nodeId` is the ROUTING key — the
20454
- * generated cap-router strips it (default `nodeIdMode: 'routing'`) and
20455
- * dispatches to that node, so the provider method runs ON the target
20456
- * node and receives no `nodeId`.
20945
+ * COPY-ON-SELECT the write that makes `trained` safe to evict.
20457
20946
  *
20458
- * This is the slimmed executor leg of the orchestrator's
20459
- * `resetNodePipelineDefaults` flow (which owns the real reset: node
20460
- * addonDefaults pins + per-camera orchestrator overrides). The legacy
20461
- * `resetToDefault` which reset a persisted global step-tree seed
20462
- * nothing in the live per-camera path read — was removed together with
20463
- * that seed.
20947
+ * Selecting a frame copies its bytes into retrain storage immediately: not
20948
+ * a reference, not a lease. Once the copy exists the dataset no longer
20949
+ * depends on the track's media, which is exactly what lets D81 hand a
20950
+ * `trained` track back to retention.
20951
+ *
20952
+ * The order inside is load-bearing and is pinned by a test: an EXISTING
20953
+ * copy is returned without touching the source, so an original that
20954
+ * evaporated blocks the selection of THAT ORIGINAL and never the copy
20955
+ * already taken. Every refusal comes back named — a dropped selection is
20956
+ * never silent, on the wire or in the log.
20464
20957
  */
20465
- clearDeviceOverrides: method(object({ nodeId: string() }), object({
20466
- success: literal(true),
20467
- clearedDevices: number()
20958
+ selectRetrainFrames: method(object({
20959
+ deviceId: number(),
20960
+ trackId: string(),
20961
+ mediaKeys: array(string()).min(1)
20962
+ }), RetrainFrameSelectionSchema, {
20963
+ kind: "mutation",
20964
+ auth: "admin"
20965
+ }),
20966
+ /** Un-select a frame: its annotations go first, then the copy and its blob.
20967
+ * Deliberately destructive and deliberately explicit — it is the only way
20968
+ * a frame leaves the dataset before export. */
20969
+ deselectRetrainFrame: method(object({
20970
+ deviceId: number(),
20971
+ trackId: string(),
20972
+ frameId: string()
20973
+ }), object({
20974
+ removed: boolean(),
20975
+ removedAnnotations: number().int()
20468
20976
  }), {
20469
20977
  kind: "mutation",
20470
20978
  auth: "admin"
20471
20979
  }),
20472
- getSchema: method(_void(), PipelineSchemaSchema),
20473
- getGlobalSteps: method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()),
20474
- getGlobalPipelineConfig: method(_void(), PipelineConfigBridge),
20475
- getOrchestratorConfigSchema: method(_void(), ConfigUISchemaBridge),
20476
20980
  /**
20477
- * Gate B (pre-init config-correctness gate). PURE COMPUTE against this
20478
- * node's `currentEngine.format` — resolves `steps` the same way the
20479
- * runtime dispatch path would, and reports what WOULD happen without
20480
- * touching any node-global state. Called by the orchestrator at attach
20481
- * time (`attachOn`), node-pinned to the TARGET node, so config problems
20482
- * surface BEFORE `pipelineRunner.attachCamera` rather than at the first
20483
- * per-frame resolve. `ok` is false iff `issues` is non-empty (both
20484
- * `unknown-addon` and `no-format-build` are HARD issues); `substitutions`
20485
- * is informational (a degraded-but-loadable model swap) and never
20486
- * affects `ok`. Never throws.
20981
+ * The pixels of ONE copied frame, base64.
20982
+ *
20983
+ * Through the cap rather than a data plane because it is genuinely one
20984
+ * frame at a time, on demand, at human speed — the shape D9/D18 permit
20985
+ * (what they forbid is frames crossing a boundary at frame RATE). The
20986
+ * annotation canvas needs the image and its exact dimensions in the same
20987
+ * answer: a canvas that places a normalised box against a size it guessed
20988
+ * draws every box in the wrong place.
20487
20989
  */
20488
- validatePipeline: method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema),
20489
- listTemplates: method(_void(), array(PipelineTemplateSchema$1).readonly()),
20490
- saveTemplate: method(object({
20491
- name: string(),
20492
- steps: array(PipelineTemplateStepSchema).readonly(),
20493
- engine: PipelineEngineChoiceSchema
20494
- }), PipelineTemplateSchema$1, { kind: "mutation" }),
20495
- updateTemplate: method(object({
20496
- id: string(),
20497
- name: string().optional(),
20498
- steps: array(PipelineTemplateStepSchema).readonly().optional()
20499
- }), PipelineTemplateSchema$1, { kind: "mutation" }),
20500
- deleteTemplate: method(object({ id: string() }), _void(), { kind: "mutation" }),
20501
- getCapabilities: method(_void(), InferenceCapabilitiesBridge),
20502
- getAddonModels: method(object({ addonId: string() }), ModelAvailabilityListBridge),
20503
- downloadModel: method(object({
20504
- addonId: string(),
20505
- modelId: string(),
20506
- format: ModelFormatSchema$1
20507
- }), DownloadModelResultSchema, { kind: "mutation" }),
20508
- deleteModel: method(object({
20509
- addonId: string(),
20510
- modelId: string(),
20511
- format: ModelFormatSchema$1
20512
- }), object({ success: literal(true) }), { kind: "mutation" }),
20990
+ getRetrainFrameImage: method(object({ frameId: string() }), object({
20991
+ base64: string(),
20992
+ width: number().int(),
20993
+ height: number().int()
20994
+ }), {
20995
+ kind: "query",
20996
+ auth: "admin"
20997
+ }),
20513
20998
  /**
20514
- * Stateless single-frame execution. Callers (runner, benchmark) pass
20515
- * the complete `engine` + `steps` tree; the executor holds no state
20516
- * about cameras or saved pipelines.
20999
+ * Ask the pipeline what it sees, as a PROPOSAL.
20517
21000
  *
20518
- * `engine` is optional during the migration window to preserve the
20519
- * legacy call shape used by existing benchmark code; once all
20520
- * callers pass it explicitly we make it required.
21001
+ * Runs through `pipelineRunner.runStatelessStep` on the COPIED frame, and
21002
+ * every box comes back as a draft with `source: 'assist'` plus the model and
21003
+ * score that produced it. The operator confirms, edits, adds and deletes;
21004
+ * nothing is stored until `saveRetrainAnnotations`.
20521
21005
  *
20522
- * Exactly one of `frame`, `frameRef`, `frameHandle`, `imageBase64`,
20523
- * `referenceImage` must be provided:
20524
- * - `frame`: runtime dispatch path (runner decoded broker frame).
20525
- * Carries the raw buffer, dimensions, and format; the executor
20526
- * uses it directly without base64 round-tripping.
20527
- * - `frameHandle` (CB5): a zero-pixel shm `FrameHandle` for the SAME
20528
- * decoded frame. Both runner and executor are hub-local processes
20529
- * sharing `/dev/shm`, so the executor maps the named segment and
20530
- * reads the pixels back zero-copy — eliminating the ~1.2MB
20531
- * re-serialisation over UDS/MsgPack the `frame` path pays per call.
20532
- * High-risk: the FrameRing is a latest-wins seqlock with no
20533
- * refcount, so a recycled slot yields a null read; the executor
20534
- * then degrades to an empty result and the runner ships pixels via
20535
- * `frame` as the fallback (queue-depth gated on the runner side).
20536
- * - `imageBase64`: one-shot test path (benchmark ImageTab).
20537
- * - `referenceImage`: named file from the reference-image store.
21006
+ * For packages the request is `rfdetr-package` on the ZONE CROP at 0.35 —
21007
+ * never the whole frame, where a package detector at that threshold proposes
21008
+ * furniture. A package request with no zone is REFUSED rather than widened,
21009
+ * because the silent widening would look like a bad model for as long as
21010
+ * nobody checked which rectangle it ran on.
20538
21011
  */
20539
- runPipeline: method(object({
20540
- engine: PipelineEngineChoiceSchema.optional(),
20541
- steps: array(PipelineStepInputSchema).min(1),
20542
- frame: FrameInputSchema.optional(),
20543
- /**
20544
- * Process-local lazy frame. Valid only when caller and provider resolve
20545
- * in the same execution-group process; split/cross-node callers use
20546
- * `frame`/`image` inline compatibility instead.
20547
- */
20548
- frameRef: FrameRefSchema.optional(),
20549
- /**
20550
- * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
20551
- * the decoded pixels live in. One more member of the one-of
20552
- * frame/frameHandle/image/imageBase64/referenceImage group.
20553
- */
20554
- frameHandle: FrameHandleSchema.optional(),
20555
- imageBase64: string().optional(),
20556
- /**
20557
- * Binary JPEG bytes — preferred over `imageBase64` on internal
20558
- * hops (hub → forked worker via Moleculer MsgPack) because it
20559
- * skips the 33% base64 overhead + the per-call base64 decode on
20560
- * the detection-pipeline worker. Callers can pass either; exactly
20561
- * one of `frame`/`image`/`imageBase64`/`referenceImage` is required.
20562
- */
20563
- image: _instanceof(Uint8Array).optional(),
20564
- referenceImage: string().optional(),
20565
- deviceId: number().optional(),
20566
- sessionId: string().optional(),
20567
- /**
20568
- * Execution plane. 'full' (default) runs the whole tree — benchmark,
20569
- * reference-image, and detail-subtree calls. 'frame' is the live
20570
- * per-frame dispatch: ONLY root-plane steps run; crop children
20571
- * (inputClasses ≠ null) are skipped and served per-track via
20572
- * pipelineRunner.runDetailSubtree (two-plane design).
20573
- */
20574
- plane: _enum(["full", "frame"]).optional(),
20575
- /**
20576
- * Inference-device selector (Phase 2 multi-device). Format
20577
- * `<backend>:<device>` (e.g. `openvino:gpu`, `edgetpu:usb`, `cpu`).
20578
- * Omitted ⇒ the runner's default device (current single-engine
20579
- * behaviour). Selects WHICH device pool of the node runs the call.
20580
- */
20581
- deviceKey: string().optional(),
20582
- /**
20583
- * Two-plane NATIVE child-crop reference. Set by `runDetailSubtree` ONLY
20584
- * when the parent crop was resolved from the frame's retained NATIVE
20585
- * surface (a frameHandle HIT). Lets the executor re-cut a LEAF crop
20586
- * child's ROI (plate-ocr, face-embedding, leaf classifiers) at native
20587
- * resolution from that surface — the SAME quality path faces already
20588
- * had — instead of the downscaled parent tile. `handle` keys the native
20589
- * surface (node-pinned to its owner); `cropFrameSpace` is the parent
20590
- * crop's padded/clamped rectangle in FRAME-space pixels, used to compose
20591
- * the executor's crop-normalized child ROI back into frame-normalized
20592
- * coordinates. Auxiliary to the image source (`image`/`frame`/…), NOT one
20593
- * of the mutually-exclusive image inputs. Absent ⇒ tile-crop children
20594
- * (today's behaviour on the fallback path).
20595
- */
20596
- nativeCropRef: NativeCropRefSchema.optional()
20597
- }), PipelineRunResultBridge, { kind: "mutation" }),
21012
+ proposeRetrainAnnotations: method(object({
21013
+ deviceId: number(),
21014
+ trackId: string(),
21015
+ frameId: string(),
21016
+ subject: RetrainAssistSubjectSchema,
21017
+ /** Which node runs it. Absent wherever an unowned call lands. */
21018
+ nodeId: string().optional()
21019
+ }), RetrainAssistResultSchema, {
21020
+ kind: "mutation",
21021
+ auth: "admin"
21022
+ }),
20598
21023
  /**
20599
- * Batched run N raw frames packed into one cap call. The provider
20600
- * routes the batch through `SharedInferencePool.inferBatch`
20601
- * (`MSG_INFER_BATCH = 0x03`) so the IPC framing and JSON response
20602
- * envelope cost is amortised N:1 vs N concurrent `runPipeline`
20603
- * calls. Single root step + uniform model assumed; trees with crop
20604
- * children fall back to sequential execution.
21024
+ * The FrameProcessor pass of a replay run see the `Replay` section
21025
+ * above this capability's definition for why this is not the
21026
+ * `processFrame` method this file's header says pipeline-analytics does
21027
+ * not have.
20605
21028
  *
20606
- * Used by `scripts/bench-batch-style.mts` for batch benchmarking
20607
- * N frames in one call to amortise per-call IPC overhead.
21029
+ * Constructs a FRESH `FrameProcessor` for `(deviceId, source)`, feeds it
21030
+ * `frames` IN THE ORDER GIVEN (the caller is responsible for time
21031
+ * ordering — this method does not sort), and returns the tracks it
21032
+ * produced. Zero persistence: no `TrackStore`, no event bus, no media
21033
+ * capture. `zones` / `detectionRules` are the run's OWN zone set —
21034
+ * typically the camera's real zones plus an ephemeral overlay
21035
+ * (`addon-benchmark`'s `replay-plan.ts`), never read from or written to
21036
+ * the `zones` capability by this method itself.
20608
21037
  */
20609
- runPipelineBatch: method(object({
20610
- engine: PipelineEngineChoiceSchema.optional(),
20611
- steps: array(PipelineStepInputSchema).min(1),
20612
- frames: array(FrameInputSchema).min(1).max(255),
20613
- deviceId: number().optional(),
20614
- sessionId: string().optional(),
20615
- /**
20616
- * Pure-inference benchmark hint. A NONZERO uint32 pins every frame in
20617
- * the batch to the Python pool's bench preprocess cache
20618
- * (`_bench_frame_id`) so a REPEATED benchmark frame is decoded +
20619
- * preprocessed ONCE and every later inference is a pure-inference cache
20620
- * hit the sustained-throughput run measures inference, not
20621
- * decode+preprocess+infer. Omitted/0 for live frames (all different →
20622
- * full preprocess every call, correct). Fresh per sustained run;
20623
- * released via `uncacheFrame`.
20624
- */
20625
- frameId: number().int().nonnegative().optional(),
20626
- /** Inference-device selector (Phase 2 multi-device); see runPipeline. */
20627
- deviceKey: string().optional()
20628
- }), object({ results: array(PipelineRunResultBridge).readonly() }), { kind: "mutation" }),
21038
+ runReplayFrameProcessor: method(object({
21039
+ deviceId: number(),
21040
+ source: DetectionSourceSchema,
21041
+ zones: array(ZoneSchema).readonly().optional(),
21042
+ detectionRules: array(ZoneRuleSchema).readonly().optional(),
21043
+ zoneMembershipMinOverlap: number().min(0).max(1).optional(),
21044
+ frames: array(ReplayFrameInputSchema).min(1)
21045
+ }), RunReplayFrameProcessorResultSchema, {
21046
+ kind: "mutation",
21047
+ auth: "admin"
21048
+ }),
21049
+ /** Every annotation on a track, oldest first. */
21050
+ listRetrainAnnotations: method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
21051
+ kind: "query",
21052
+ auth: "admin"
21053
+ }),
20629
21054
  /**
20630
- * Cache a raw frame inside the Python inference pool's memory.
20631
- * Returns a numeric `frameId` that `inferCached` references —
20632
- * subsequent calls send only 5 bytes through the pipe instead of
20633
- * 1.2MB raw data, eliminating the pipe transfer bottleneck.
21055
+ * Replace EVERY annotation on one frame with the supplied set.
21056
+ *
21057
+ * Whole-frame replacement, not per-box upsert: the unit of ground truth is
21058
+ * the frame, and "the operator deleted a box" must be the same durable
21059
+ * outcome as "the operator never drew it". A per-box patch would let a frame
21060
+ * keep a box the operator removed on a surface that only knew about the
21061
+ * boxes it sent.
21062
+ *
21063
+ * Refuses a macro class typed into `label` or `subLabel` — the tiers are
21064
+ * separate and the guard is at the WRITE, because a mixed taxonomy cannot
21065
+ * be un-mixed by reading it.
20634
21066
  */
20635
- cacheFrameInPool: method(object({
20636
- data: _instanceof(Uint8Array),
20637
- width: number().int().positive(),
20638
- height: number().int().positive(),
20639
- format: _enum([
20640
- "rgb",
20641
- "bgr",
20642
- "gray"
20643
- ])
20644
- }), object({
20645
- frameId: number(),
20646
- width: number(),
20647
- height: number()
20648
- }), { kind: "mutation" }),
21067
+ saveRetrainAnnotations: method(object({
21068
+ deviceId: number(),
21069
+ trackId: string(),
21070
+ frameId: string(),
21071
+ annotations: array(RetrainAnnotationDraftSchema)
21072
+ }), array(RetrainAnnotationSchema).readonly(), {
21073
+ kind: "mutation",
21074
+ auth: "admin"
21075
+ }),
20649
21076
  /**
20650
- * Run inference on a previously cached frame. Sends only 5 bytes
20651
- * (model_idx + frameId) through the IPC pipe eliminates the
20652
- * ~35ms per-call overhead of transferring 1.2MB raw data.
21077
+ * Finish with a track: `staging trained`. **The only writer of that
21078
+ * state** D81 shipped the column with it deliberately unreachable.
21079
+ *
21080
+ * Refuses a track the dataset holds no copies from. `trained` un-pins the
21081
+ * track's media, so completing without a copy is a delete order for material
21082
+ * nothing ever extracted anything from; that refusal IS the safety argument
21083
+ * of D81, expressed as a precondition.
20653
21084
  */
20654
- inferCached: method(object({
20655
- stepId: string(),
20656
- frameId: number().int()
20657
- }), record(string(), unknown()), { kind: "mutation" }),
21085
+ completeRetrainTrack: method(object({
21086
+ deviceId: number(),
21087
+ trackId: string()
21088
+ }), RetrainTransitionResultSchema, {
21089
+ kind: "mutation",
21090
+ auth: "admin"
21091
+ }),
20658
21092
  /**
20659
- * Release a cached frame from the Python pool's memory.
21093
+ * The deliberate return: `trained staging`, for the rare case.
21094
+ *
21095
+ * The generic `setTrackFlags` toggle refuses this in both directions by
21096
+ * design (D81) — re-staging from a checkbox is how the same material gets
21097
+ * annotated twice under two ground truths. Doing it here means the operator
21098
+ * is looking at the annotations that already exist while they decide, and
21099
+ * those annotations are LEFT ALONE: "put this back" must not be a
21100
+ * destructive act wearing a navigational name.
20660
21101
  */
20661
- uncacheFrame: method(object({ frameId: number().int() }), _void(), { kind: "mutation" }),
20662
- /** Returns the effective pool tuning (resolved from user overrides + backend defaults). */
20663
- getEffectiveTuning: method(_void(), object({
20664
- batchMode: string(),
20665
- windowMs: number(),
20666
- maxBatchSize: number(),
20667
- concurrency: number()
20668
- })),
21102
+ restageRetrainTrack: method(object({
21103
+ deviceId: number(),
21104
+ trackId: string()
21105
+ }), RetrainTransitionResultSchema, {
21106
+ kind: "mutation",
21107
+ auth: "admin"
21108
+ }),
20669
21109
  /**
20670
- * List every EngineFactory currently loaded in this executor's RAM,
20671
- * with the models resident and a coarse "in use" marker derived from
20672
- * ongoing inference activity. Used by the Pipeline page Engines tab.
21110
+ * Where to download the ANNOTATED dataset.
21111
+ *
21112
+ * The sibling of `getTrainingExportUrl` and deliberately not the same
21113
+ * archive: that one streams a marked track's stored media verbatim, this one
21114
+ * streams the retrain COPIES plus an `annotations.json` carrying, for every
21115
+ * subject, the canonical full-frame box AND the geometry derived for each
21116
+ * model shape (letterboxed root / zone-cropped package / subject-cropped
21117
+ * classifier). Derived at export, never stored — one box in, three shapes
21118
+ * out, so two crops of the same subject can never end up in one feature
21119
+ * space (D52).
20673
21120
  */
20674
- listLoadedEngines: method(_void(), array(object({
20675
- engineKey: string(),
20676
- engine: PipelineEngineChoiceSchema,
20677
- modelsLoaded: array(string()).readonly(),
20678
- inUseByCameras: array(number()).readonly(),
20679
- /**
20680
- * Origin of this resident factory.
20681
- * - `runtime` — main camera-serving engine (no idle TTL).
20682
- * - `warm-override` — benchmark/test override held in the warm
20683
- * cache; auto-disposed after the idle TTL.
20684
- * - `device-pool` — a concurrent per-device pool (Phase 2
20685
- * multi-device, keyed by `deviceKey`) resolved
20686
- * via `resolveDeviceFactory`. Runs alongside the
20687
- * `runtime` engine on a DIFFERENT accelerator
20688
- * (NPU / iGPU / Coral) — this is how the
20689
- * Engines tab shows all pools running at once.
20690
- */
20691
- kind: _enum([
20692
- "runtime",
20693
- "warm-override",
20694
- "device-pool"
20695
- ]),
20696
- /** Native pid of the underlying Python pool (null when no pool). */
20697
- poolPid: number().nullable(),
20698
- /** ms since this factory was last used (null when not warm-tracked). */
20699
- idleMs: number().nullable(),
20700
- /** Idle TTL after which `warm-override` factories self-evict (null when not applicable). */
20701
- idleTtlMs: number().nullable()
20702
- })).readonly()),
20703
- /** Warm up an engine without running a frame. No-op if already loaded. */
20704
- spinEngine: method(object({ engine: PipelineEngineChoiceSchema }), object({ success: literal(true) }), {
20705
- kind: "mutation",
21121
+ getRetrainExportUrl: method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
21122
+ kind: "query",
20706
21123
  auth: "admin"
20707
21124
  }),
21125
+ getEventMedia: method(object({
21126
+ eventId: string(),
21127
+ kind: MediaFileKindEnum.optional(),
21128
+ deviceId: number()
21129
+ }), array(MediaFileSchema).readonly()),
21130
+ /** All media rows owned by a track. `kinds` narrows to a kind subset so a
21131
+ * client can fetch the SMALL display variants on open and pull the
21132
+ * multi-MB native variants only on demand (mirrors `getEventMedia.kind`).
21133
+ * Absent ⇒ every kind (back-compat). */
21134
+ getTrackMedia: method(object({
21135
+ trackId: string(),
21136
+ kinds: array(MediaFileKindEnum).optional(),
21137
+ deviceId: number()
21138
+ }), array(MediaFileSchema).readonly()),
20708
21139
  /**
20709
- * Unload an engine from RAM. `force:true` unloads even when cameras
20710
- * are actively using it (they re-spin on next frame). Default is
20711
- * gated returns `{success:false, reason}` when in use.
21140
+ * What media a track HAS, without any of it.
21141
+ *
21142
+ * The detail view needs the shape of a track's media to build its strip —
21143
+ * which kinds exist, in what order, at what size — and then wants each tile
21144
+ * fetched as an image, not as base64 inside this response. Measured: the
21145
+ * full `getTrackMedia` is 5.3-7.7 MB and blocks the view; this manifest is
21146
+ * ~2 KB.
21147
+ *
21148
+ * It also restores a fact a `kinds` filter destroys: filtering
21149
+ * `getTrackMedia` drops whole ROWS, taking `kind` and `sizeBytes` with
21150
+ * them, so a client that fetched only the small variants could no longer
21151
+ * tell a full-resolution variant existed — and the affordance that opens it
21152
+ * would silently disappear.
20712
21153
  */
20713
- killEngine: method(object({
20714
- engine: PipelineEngineChoiceSchema,
20715
- force: boolean().optional()
20716
- }), object({
20717
- success: boolean(),
20718
- reason: string().optional()
20719
- }), {
21154
+ listTrackMedia: method(object({
21155
+ trackId: string(),
21156
+ deviceId: number()
21157
+ }), array(MediaFileInfoSchema).readonly()),
21158
+ /**
21159
+ * Search object events by text query using CLIP cosine similarity.
21160
+ * Encodes `text` via the `embedding-encoder` cap, queries the
21161
+ * `ObjectEmbeddingStore` with optional prefilters, ranks all matching
21162
+ * embeddings by cosine similarity, and joins winners to their
21163
+ * ObjectEvents by trackId. Returns up to `limit` events scored ≥
21164
+ * `minScore`, sorted descending by score.
21165
+ */
21166
+ searchObjectEvents: method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()),
21167
+ wipeObjectEmbeddings: method(object({}), WipeObjectEmbeddingsResultSchema, {
20720
21168
  kind: "mutation",
20721
21169
  auth: "admin"
20722
21170
  }),
20723
- listReferenceImages: method(_void(), array(ReferenceImageEntrySchema).readonly()),
20724
- getReferenceImage: method(object({ filename: string() }), ReferenceImageBodySchema.nullable()),
20725
- getReferenceAudioFiles: method(_void(), array(ReferenceAudioEntrySchema).readonly()),
20726
- getReferenceAudio: method(object({ filename: string() }), ReferenceAudioBodySchema.nullable()),
20727
- getAudioCapabilities: method(_void(), AudioCapabilitiesSchema),
20728
- runAudioTest: method(object({
20729
- addonId: string(),
20730
- modelId: string(),
20731
- filename: string().optional(),
20732
- settings: record(string(), unknown()).optional()
20733
- }), AudioTestResultSchema, { kind: "mutation" }),
20734
- getDetectionConfigSchema: method(_void(), ConfigUISchemaNullableBridge)
21171
+ rebuildObjectEmbeddings: method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
21172
+ kind: "mutation",
21173
+ auth: "admin"
21174
+ }),
21175
+ getObjectEmbeddingRebuildStatus: method(object({}), RebuildStatusSchema)
21176
+ },
21177
+ events: {
21178
+ /**
21179
+ * Enriched frame emitted after refinement — the live-overlay source of
21180
+ * truth (two-plane re-injection). Carries the frame's detections in the
21181
+ * `ObjectDetection` wire shape: first-level roots (with track info +
21182
+ * enrichment labels) plus synthesized `kind:'detail'` face/plate entries
21183
+ * re-projected from per-track detail state, so stream overlays render
21184
+ * boxes + recognized names without querying full Track state.
21185
+ */
21186
+ onFrameTracked: { data: object({
21187
+ deviceId: number(),
21188
+ timestamp: number(),
21189
+ frameWidth: number(),
21190
+ frameHeight: number(),
21191
+ detections: array(OverlayDetectionSchema).readonly()
21192
+ }) },
21193
+ /** Track entered active state (first-seen). */
21194
+ onTrackStarted: { data: object({
21195
+ deviceId: number(),
21196
+ trackId: string(),
21197
+ className: string()
21198
+ }) },
21199
+ /** Track expired (TTL reached after last detection). */
21200
+ onTrackEnded: { data: object({
21201
+ deviceId: number(),
21202
+ trackId: string(),
21203
+ className: string(),
21204
+ durationMs: number()
21205
+ }) },
21206
+ /** Canonical "something happened at device X" event, per-kind. */
21207
+ onDetectionEvent: { data: object({
21208
+ deviceId: number(),
21209
+ kind: EventKindSchema,
21210
+ eventId: string(),
21211
+ timestamp: number()
21212
+ }) }
20735
21213
  }
20736
21214
  };
20737
21215
  object({
@@ -20759,106 +21237,6 @@ var CameraMetricsSchema = object({
20759
21237
  });
20760
21238
  var CameraMetricsWithDeviceIdSchema = CameraMetricsSchema.extend({ deviceId: number() });
20761
21239
  /**
20762
- * Zone — pure geometry + identity. NO filtering behaviour.
20763
- *
20764
- * Zones describe **where** in the frame the operator wants to flag
20765
- * something; consumer-owned {@link ZoneRule} arrays describe **how**
20766
- * each pipeline stage uses them. Splitting the two means a single
20767
- * polygon "Driveway" can simultaneously back a motion-exclude rule,
20768
- * a detection-include rule on `['car']`, and an occupancy aggregate
20769
- * — without three duplicated polygons.
20770
- *
20771
- * Owned by the orchestrator addon (provider) and mirrored into the
20772
- * `zones` device-state slice on every mutation. Consumers
20773
- * (motion-wasm, pipeline-executor, analytics, admin UI) read either
20774
- * via `api.zones.listZones` (one-shot) or via `dev.state.zones` (live
20775
- * mirror with `onChanged`).
20776
- *
20777
- * Coordinates are normalised fractions of the frame (0–1) so zones
20778
- * survive resolution changes and stream profile switches.
20779
- *
20780
- * `kind` discriminates between full polygons (closed regions used
20781
- * for intrusion / occupancy filters) and tripwires (open 2-point
20782
- * line segments used for cross events). Onboard / firmware-reported
20783
- * zones (Reolink, ONVIF) are out of scope for now — see the deferred
20784
- * task list.
20785
- */
20786
- var ZoneKindEnum = _enum(["polygon", "tripwire"]);
20787
- /** Polygon vertex in fraction-of-frame coordinates (0–1). */
20788
- var PolygonPointSchema = object({
20789
- x: number(),
20790
- y: number()
20791
- });
20792
- /** A camera detection zone — pure geometry/identity. */
20793
- var ZoneSchema = object({
20794
- id: string(),
20795
- name: string(),
20796
- kind: ZoneKindEnum.default("polygon"),
20797
- /** Polygon vertices, fraction of frame (0–1). */
20798
- polygon: array(PolygonPointSchema).readonly(),
20799
- /** Visual color for UI rendering. */
20800
- color: string().default("#3b82f6")
20801
- });
20802
- /**
20803
- * Zones capability — per-camera CRUD over polygon detection zones.
20804
- *
20805
- * Provider lives in `addon-pipeline-orchestrator` (hub-only). Persists
20806
- * to per-device settings and mirrors into the `zones` device-state
20807
- * slice on every mutation, so downstream consumers can subscribe via
20808
- * `dev.state.zones.onChanged`.
20809
- *
20810
- * The cap surface only handles geometry + identity; filtering
20811
- * behaviour (per-class, include/exclude, threshold) lives in the
20812
- * consumer addons' rule arrays — see `ZoneRuleSchema` exported from
20813
- * `capabilities/schemas/zone-rule.js`.
20814
- */
20815
- var zonesCapability = {
20816
- name: "zones",
20817
- scope: "device",
20818
- mode: "singleton",
20819
- deviceTypes: [DeviceType.Camera],
20820
- methods: {
20821
- listZones: method(object({ deviceId: number() }), array(ZoneSchema).readonly()),
20822
- addZone: method(object({
20823
- deviceId: number(),
20824
- zone: ZoneSchema
20825
- }), _void(), {
20826
- kind: "mutation",
20827
- auth: "admin"
20828
- }),
20829
- removeZone: method(object({
20830
- deviceId: number(),
20831
- zoneId: string()
20832
- }), _void(), {
20833
- kind: "mutation",
20834
- auth: "admin"
20835
- }),
20836
- updateZone: method(object({
20837
- deviceId: number(),
20838
- zone: ZoneSchema
20839
- }), _void(), {
20840
- kind: "mutation",
20841
- auth: "admin"
20842
- })
20843
- },
20844
- /**
20845
- * Runtime-state slice — the live zone catalogue mirrored by the
20846
- * orchestrator on every CRUD mutation. Consumers read via
20847
- * `device.state.zones.value` / `.watch(...)` without round-tripping
20848
- * the cap, and the codegen DeviceProxy auto-wires the reactive
20849
- * handle. Slice shape is `{ zones: Zone[] }` so future extensions
20850
- * (e.g. zone groupings) can sit alongside the polygon list.
20851
- */
20852
- runtimeState: object({ zones: array(ZoneSchema).readonly() }),
20853
- /**
20854
- * Runtime-state durability: **restored** — written only on operator mutation, so a camera that never had one has nothing to re-derive from. This is the slice `zone-mirror-hydration.ts` exists to paper over.
20855
- *
20856
- * See `RuntimeStateDurability`. Enforced by
20857
- * `scripts/check-runtime-state-durability.ts`.
20858
- */
20859
- durability: "restored"
20860
- };
20861
- /**
20862
21240
  * A bounding box in NORMALIZED [0,1] frame coordinates for `getNativeCrop`. The
20863
21241
  * decode worker resolves it against the RETAINED native frame's real pixel dims,
20864
21242
  * so the caller supplies only the detection-res bbox divided by the detection
@@ -23142,6 +23520,20 @@ var ssoBridgeCapability = {
23142
23520
  mode: "singleton",
23143
23521
  internal: true,
23144
23522
  methods: {
23523
+ /**
23524
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
23525
+ * `data-store-provider`) — `signBridgeToken` was reachable on the
23526
+ * AppRouter by ANY authenticated session at the default
23527
+ * `auth: 'protected'`. The claims (including `isAdmin`) are CALLER-
23528
+ * SUPPLIED: an unprivileged session could have minted its own
23529
+ * `isAdmin: true` bridge token and redeemed it at
23530
+ * `/api/auth/sso/finish` to become admin — a full privilege escalation
23531
+ * through the exact surface this cap exists to close (see the module
23532
+ * docblock). Every real caller (`oauth-grants.ts`, `main.ts`
23533
+ * registration, the OIDC/magic-link addons) reaches this through
23534
+ * `ctx.api` / the in-process registry — never through tRPC — so
23535
+ * `auth: 'admin'` costs those callers nothing.
23536
+ */
23145
23537
  signBridgeToken: method(object({
23146
23538
  claims: SsoBridgeClaimsSchema,
23147
23539
  /**
@@ -23154,8 +23546,8 @@ var ssoBridgeCapability = {
23154
23546
  * linking rather than produce an eternal token.
23155
23547
  */
23156
23548
  ttlSec: union([number().int().positive(), literal("never")]).optional()
23157
- }), object({ token: string() })),
23158
- verifyBridgeToken: method(object({ token: string() }), SsoBridgeClaimsSchema.nullable())
23549
+ }), object({ token: string() }), { auth: "admin" }),
23550
+ verifyBridgeToken: method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" })
23159
23551
  }
23160
23552
  };
23161
23553
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
@@ -23304,8 +23696,15 @@ var storageEvictableCapability = {
23304
23696
  mode: "collection",
23305
23697
  internal: true,
23306
23698
  methods: {
23699
+ /**
23700
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
23701
+ * `data-store-provider`) — this was reachable on the AppRouter by ANY
23702
+ * authenticated session at the default `auth: 'protected'`. The only
23703
+ * consumer is the orchestrator's `StoragePressureManager`, which calls
23704
+ * `ctx.api` — never tRPC — so `auth: 'admin'` costs it nothing.
23705
+ */
23307
23706
  /** Bytes this provider holds on the given location — drives proportional fan-out. */
23308
- getEvictableUsage: method(object({ locationId: string() }), EvictableUsageSchema),
23707
+ getEvictableUsage: method(object({ locationId: string() }), EvictableUsageSchema, { auth: "admin" }),
23309
23708
  /**
23310
23709
  * Free approximately `targetBytes` of this provider's OWN least-valuable
23311
23710
  * data on the location (oldest footage, expired clips, …). Returns what it
@@ -23314,7 +23713,10 @@ var storageEvictableCapability = {
23314
23713
  evict: method(object({
23315
23714
  locationId: string(),
23316
23715
  targetBytes: number().int().positive()
23317
- }), EvictResultSchema, { kind: "mutation" })
23716
+ }), EvictResultSchema, {
23717
+ kind: "mutation",
23718
+ auth: "admin"
23719
+ })
23318
23720
  }
23319
23721
  };
23320
23722
  /**
@@ -23417,43 +23819,82 @@ var storageProviderCapability = {
23417
23819
  mode: "collection",
23418
23820
  internal: true,
23419
23821
  methods: {
23822
+ /**
23823
+ * Every method below except `testLocation` had NO `auth` override —
23824
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
23825
+ * `data-store-provider`), so this was reachable on the AppRouter by ANY
23826
+ * authenticated session at the default `auth: 'protected'`: arbitrary
23827
+ * read/write/delete/list against any storage location the operator has
23828
+ * configured, keyed only by a caller-supplied `location` + `relativePath`
23829
+ * — the same "public consumers never see this cap" claim that was false
23830
+ * for `data-store-provider`, here gating raw file I/O instead of raw
23831
+ * settings rows. The only legitimate caller,
23832
+ * `addon-registry.service.ts`'s `activeStorageProvider`, calls the
23833
+ * provider object directly (in-process) — never tRPC — so `auth: 'admin'`
23834
+ * on every method costs it nothing. Public consumers already go through
23835
+ * the separate `storage` singleton cap.
23836
+ */
23420
23837
  /** Self-description for the "Add location" wizard. */
23421
- getProviderInfo: method(_void(), ProviderInfoSchema),
23838
+ getProviderInfo: method(_void(), ProviderInfoSchema, { auth: "admin" }),
23422
23839
  /** Validate config + probe connectivity. Called on add and on edit. */
23423
23840
  testLocation: method(object({ config: record(string(), unknown()) }), TestLocationResultSchema, { auth: "admin" }),
23424
23841
  resolve: method(object({
23425
23842
  location: StorageLocationSchema,
23426
23843
  relativePath: string()
23427
- }), string()),
23844
+ }), string(), { auth: "admin" }),
23428
23845
  write: method(object({
23429
23846
  location: StorageLocationSchema,
23430
23847
  relativePath: string(),
23431
23848
  data: _instanceof(Uint8Array)
23432
- }), _void(), { kind: "mutation" }),
23849
+ }), _void(), {
23850
+ kind: "mutation",
23851
+ auth: "admin"
23852
+ }),
23433
23853
  read: method(object({
23434
23854
  location: StorageLocationSchema,
23435
23855
  relativePath: string()
23436
- }), _instanceof(Uint8Array)),
23856
+ }), _instanceof(Uint8Array), { auth: "admin" }),
23437
23857
  exists: method(object({
23438
23858
  location: StorageLocationSchema,
23439
23859
  relativePath: string()
23440
- }), boolean()),
23860
+ }), boolean(), { auth: "admin" }),
23441
23861
  list: method(object({
23442
23862
  location: StorageLocationSchema,
23443
23863
  prefix: string().optional()
23444
- }), array(string()).readonly()),
23864
+ }), array(string()).readonly(), { auth: "admin" }),
23445
23865
  delete: method(object({
23446
23866
  location: StorageLocationSchema,
23447
23867
  relativePath: string()
23448
- }), _void(), { kind: "mutation" }),
23449
- getAvailableSpace: method(object({ location: StorageLocationSchema }), number().nullable()),
23450
- beginUpload: method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }),
23451
- writeChunk: method(WriteChunkInputSchema, _void(), { kind: "mutation" }),
23452
- finalizeUpload: method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }),
23453
- abortUpload: method(AbortUploadInputSchema, _void(), { kind: "mutation" }),
23454
- beginDownload: method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }),
23455
- readChunk: method(ReadChunkInputSchema, _instanceof(Uint8Array)),
23456
- endDownload: method(EndDownloadInputSchema, _void(), { kind: "mutation" })
23868
+ }), _void(), {
23869
+ kind: "mutation",
23870
+ auth: "admin"
23871
+ }),
23872
+ getAvailableSpace: method(object({ location: StorageLocationSchema }), number().nullable(), { auth: "admin" }),
23873
+ beginUpload: method(BeginUploadInputSchema, BeginUploadResultSchema, {
23874
+ kind: "mutation",
23875
+ auth: "admin"
23876
+ }),
23877
+ writeChunk: method(WriteChunkInputSchema, _void(), {
23878
+ kind: "mutation",
23879
+ auth: "admin"
23880
+ }),
23881
+ finalizeUpload: method(FinalizeUploadInputSchema, _void(), {
23882
+ kind: "mutation",
23883
+ auth: "admin"
23884
+ }),
23885
+ abortUpload: method(AbortUploadInputSchema, _void(), {
23886
+ kind: "mutation",
23887
+ auth: "admin"
23888
+ }),
23889
+ beginDownload: method(BeginDownloadInputSchema, BeginDownloadResultSchema, {
23890
+ kind: "mutation",
23891
+ auth: "admin"
23892
+ }),
23893
+ readChunk: method(ReadChunkInputSchema, _instanceof(Uint8Array), { auth: "admin" }),
23894
+ endDownload: method(EndDownloadInputSchema, _void(), {
23895
+ kind: "mutation",
23896
+ auth: "admin"
23897
+ })
23457
23898
  }
23458
23899
  };
23459
23900
  /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
@@ -23786,9 +24227,28 @@ var userPasskeysCapability = {
23786
24227
  auth: "admin",
23787
24228
  access: "create"
23788
24229
  }),
24230
+ /**
24231
+ * The four methods below had NO `auth` override — `internal: true` did
24232
+ * not gate the mount (see the 2026-08-26 note on `data-store-provider`),
24233
+ * so they were reachable on the AppRouter at the default
24234
+ * `auth: 'protected'` (any authenticated session), inconsistent with
24235
+ * every other method on this cap (all already `auth: 'admin'`).
24236
+ *
24237
+ * ⚠ Checked before gating: these are also the PRE-AUTH login-ceremony
24238
+ * methods, so admin-gating them here must not break login for an
24239
+ * unauthenticated principal. It doesn't — the real login path is the
24240
+ * PUBLIC `auth.beginAuthentication` / `auth.finishAuthentication` /
24241
+ * `auth.beginDiscoverableAuthentication` / `auth.finishDiscoverableAuthentication`
24242
+ * procedures in `server/backend/src/api/core/auth.router.ts`, which call
24243
+ * `provider.beginAuthentication(...)` etc. directly against the
24244
+ * capability-registry-resolved provider — never through this cap's own
24245
+ * tRPC route. `auth: 'admin'` here only closes a redundant, unused
24246
+ * second entry point onto the same ceremony.
24247
+ */
23789
24248
  beginAuthentication: method(object({ userId: string().optional() }), object({ optionsJSON: record(string(), unknown()) }), {
23790
24249
  kind: "mutation",
23791
- access: "view"
24250
+ access: "view",
24251
+ auth: "admin"
23792
24252
  }),
23793
24253
  finishAuthentication: method(object({
23794
24254
  /** Required — the user the assertion belongs to (verified). */
@@ -23797,11 +24257,13 @@ var userPasskeysCapability = {
23797
24257
  response: record(string(), unknown())
23798
24258
  }), object({ verified: boolean() }), {
23799
24259
  kind: "mutation",
23800
- access: "view"
24260
+ access: "view",
24261
+ auth: "admin"
23801
24262
  }),
23802
24263
  beginDiscoverableAuthentication: method(object({}), object({ optionsJSON: record(string(), unknown()) }), {
23803
24264
  kind: "mutation",
23804
- access: "view"
24265
+ access: "view",
24266
+ auth: "admin"
23805
24267
  }),
23806
24268
  finishDiscoverableAuthentication: method(object({
23807
24269
  /** AuthenticationResponseJSON from the browser. */
@@ -23810,7 +24272,8 @@ response: record(string(), unknown()) }), object({
23810
24272
  userId: string().nullable()
23811
24273
  }), {
23812
24274
  kind: "mutation",
23813
- access: "view"
24275
+ access: "view",
24276
+ auth: "admin"
23814
24277
  }),
23815
24278
  listPasskeys: method(object({ userId: string() }), array(PasskeySummarySchema), { auth: "admin" }),
23816
24279
  removePasskey: method(object({
@@ -23988,6 +24451,14 @@ var VectorStatsResultSchema = object({
23988
24451
  /** False when the backend ranks approximately. */
23989
24452
  exact: boolean()
23990
24453
  });
24454
+ /**
24455
+ * `internal: true` did not gate the mount (see the 2026-08-26 note on
24456
+ * `data-store-provider`) — every method below was reachable on the AppRouter
24457
+ * by ANY authenticated session at the default `auth: 'protected'`. The only
24458
+ * consumers (`addon-post-analysis`'s embedding stores) reach this cap via
24459
+ * `ctx.api` — UDS/Moleculer, never tRPC — so `auth: 'admin'` on every method
24460
+ * costs them nothing. No client (viewer or admin-ui) calls `vectorStore.*`.
24461
+ */
23991
24462
  var vectorStoreCapability = {
23992
24463
  name: "vector-store",
23993
24464
  scope: "system",
@@ -23995,14 +24466,26 @@ var vectorStoreCapability = {
23995
24466
  internal: true,
23996
24467
  methods: {
23997
24468
  /** Idempotent. Re-declaring with a different `dim` is an error, not a wipe. */
23998
- declareIndex: method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }),
23999
- upsert: method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }),
24000
- query: method(VectorQueryInputSchema, VectorQueryResultSchema),
24469
+ declareIndex: method(VectorDeclareIndexInputSchema, _void(), {
24470
+ kind: "mutation",
24471
+ auth: "admin"
24472
+ }),
24473
+ upsert: method(VectorUpsertInputSchema, VectorUpsertResultSchema, {
24474
+ kind: "mutation",
24475
+ auth: "admin"
24476
+ }),
24477
+ query: method(VectorQueryInputSchema, VectorQueryResultSchema, { auth: "admin" }),
24001
24478
  /** Metadata by id, no vectors — see {@link VectorGetResultSchema}. */
24002
- getByIds: method(VectorGetInputSchema, VectorGetResultSchema),
24003
- deleteByIds: method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }),
24004
- deleteByFilter: method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }),
24005
- stats: method(VectorStatsInputSchema, VectorStatsResultSchema)
24479
+ getByIds: method(VectorGetInputSchema, VectorGetResultSchema, { auth: "admin" }),
24480
+ deleteByIds: method(VectorDeleteInputSchema, VectorDeleteResultSchema, {
24481
+ kind: "mutation",
24482
+ auth: "admin"
24483
+ }),
24484
+ deleteByFilter: method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, {
24485
+ kind: "mutation",
24486
+ auth: "admin"
24487
+ }),
24488
+ stats: method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" })
24006
24489
  }
24007
24490
  };
24008
24491
  var ClipSchema = object({
@@ -26851,7 +27334,27 @@ var faceGalleryCapability = {
26851
27334
  scope: "system",
26852
27335
  mode: "singleton",
26853
27336
  methods: {
26854
- listIdentities: method(_void(), array(IdentitySchema).readonly()),
27337
+ listIdentities: method(object({
27338
+ /**
27339
+ * Inline {@link IdentitySchema.coverBase64} on every row.
27340
+ *
27341
+ * Default `false`, the same inversion `listRecentFaces` and
27342
+ * `listPlates` took on 2026-08-25 (see `include-crops-default.ts` for
27343
+ * why the burden belongs on the caller that WANTS the bytes). Measured
27344
+ * on the live hub the same day: four identities cost 40,979 B with the
27345
+ * covers inline, ~10 KB of base64 per row, on a query this UI mounts
27346
+ * four times and the viewer holds at `staleTime: 30_000`.
27347
+ *
27348
+ * Nothing loses its avatar: `coverMediaKey` is already on every row and
27349
+ * the `event-media` plane serves that key `immutable` with an ETag.
27350
+ *
27351
+ * **This is an INPUT field, so it does not reach the addon until the
27352
+ * next train** — the hub router validates cap inputs against its own
27353
+ * compiled Zod and strips a key it does not know. Until then the
27354
+ * provider sees `undefined`, which resolves to `false`: the cheap shape
27355
+ * is what ships, and the opt-in becomes reachable when the train lands.
27356
+ */
27357
+ includeCrops: boolean().optional() }).optional(), array(IdentitySchema).readonly()),
26855
27358
  createIdentity: method(object({ name: string().min(1) }), IdentitySchema, {
26856
27359
  kind: "mutation",
26857
27360
  auth: "admin"
@@ -30104,8 +30607,10 @@ var PlateInfoSchema = object({
30104
30607
  keyFrameMediaKey: string().optional(),
30105
30608
  base64: string().optional(),
30106
30609
  /**
30107
- * Same crop as a data-plane URL. `getPlateByTrack` returns this and
30108
- * never inlines JPEG; `listPlates` still inlines for the admin-ui.
30610
+ * Same crop as a data-plane URL, always present when the plate has a stored
30611
+ * crop. `getPlateByTrack` returns this and never inlines JPEG; `listPlates`
30612
+ * and `searchPlates` inline the crop only when their `includeCrops` input is
30613
+ * left at its `true` default.
30109
30614
  */
30110
30615
  cropUrl: string().optional()
30111
30616
  });
@@ -30138,7 +30643,25 @@ var plateGalleryCapability = {
30138
30643
  methods: {
30139
30644
  listPlates: method(object({
30140
30645
  deviceId: number().int().optional(),
30141
- limit: number().int().positive().optional()
30646
+ limit: number().int().positive().optional(),
30647
+ /**
30648
+ * Inline the base64 crop on every row. Default `true` — the existing
30649
+ * behaviour, kept so no caller breaks.
30650
+ *
30651
+ * Set `false` once the caller renders {@link PlateInfo.cropUrl}.
30652
+ * Measured on the live hub at the 500 rows the Plates view asks for:
30653
+ * 879,403 B and 27.7 s with the crops inline, against a few KiB of
30654
+ * metadata without them — and the browser then caches the images.
30655
+ *
30656
+ * This is the plate twin of `faceGallery.listRecentFaces`'s option;
30657
+ * plates were the one gallery list left without it.
30658
+ *
30659
+ * **This is an INPUT field, so it does not reach the addon until the
30660
+ * next train.** The hub router validates cap inputs against its own
30661
+ * compiled Zod and strips a key it does not know. Until the train
30662
+ * ships, sending `false` is harmless and keeps the crops inline.
30663
+ */
30664
+ includeCrops: boolean().optional()
30142
30665
  }).optional(), array(PlateInfoSchema).readonly()),
30143
30666
  getPlateByTrack: method(object({
30144
30667
  deviceId: number().int(),
@@ -30149,7 +30672,9 @@ var plateGalleryCapability = {
30149
30672
  searchPlates: method(object({
30150
30673
  text: string().min(1),
30151
30674
  maxDistance: number().int().min(0).optional(),
30152
- limit: number().int().positive().optional()
30675
+ limit: number().int().positive().optional(),
30676
+ /** See `listPlates.includeCrops`. Default `true`. */
30677
+ includeCrops: boolean().optional()
30153
30678
  }), array(PlateInfoSchema).readonly()),
30154
30679
  /** Suggest groups of near-identical reads (same physical plate) for review/merge. */
30155
30680
  suggestPlateClusters: method(object({
@@ -30169,7 +30694,13 @@ var plateGalleryCapability = {
30169
30694
  kind: "mutation",
30170
30695
  auth: "admin"
30171
30696
  }),
30172
- listVehicles: method(_void(), array(VehicleSchema).readonly()),
30697
+ listVehicles: method(object({
30698
+ /** Inline {@link VehicleSchema.coverBase64} on every row. Default
30699
+ * `false` — the vehicle twin of `faceGallery.listIdentities`'s
30700
+ * option; `coverMediaKey` + the `event-media` plane carry the picture.
30701
+ * INPUT field: stripped by the hub router until the train ships, which
30702
+ * resolves to `false` and is exactly the intended default. */
30703
+ includeCrops: boolean().optional() }).optional(), array(VehicleSchema).readonly()),
30173
30704
  createVehicle: method(object({ name: string().min(1) }), VehicleSchema, {
30174
30705
  kind: "mutation",
30175
30706
  auth: "admin"
@@ -31912,92 +32443,6 @@ var sceneMonitorCapability = {
31912
32443
  durability: "session"
31913
32444
  };
31914
32445
  /**
31915
- * Per-stage gating mode applied to the zones a rule references.
31916
- *
31917
- * - `include`: the rule contributes to a **whitelist** for its stage.
31918
- * When at least one `include` rule fires for a stage, only entities
31919
- * inside one of those zones pass that stage.
31920
- * - `exclude`: the rule contributes to a **blacklist** for its stage.
31921
- * Entities inside one of those zones are dropped at that stage.
31922
- *
31923
- * `monitor`-style observation (count without filtering) is not a rule
31924
- * mode — zones without any matching rule are observed naturally by
31925
- * `zone-analytics` (live snapshot + history), so an "I just want to
31926
- * count, not filter" use case needs no rule at all.
31927
- */
31928
- var ZoneRuleModeEnum = _enum(["include", "exclude"]);
31929
- /**
31930
- * Per-consumer rule that references existing zones (geometry) and
31931
- * defines how a specific pipeline stage should treat them. Each
31932
- * consumer addon owns its own `ZoneRule[]` array in its per-device
31933
- * settings:
31934
- *
31935
- * - `addon-motion-wasm` → `motionZoneRules: ZoneRule[]` (motion stage)
31936
- * - `addon-detection-pipeline` → `detectionZoneRules: ZoneRule[]` (detection stage)
31937
- * - future: notification rules, audio gating, etc.
31938
- *
31939
- * One rule applies to N zones (`zoneIds[]`) so the operator can
31940
- * express "ignore motion in ALL of {garden, street}" with a single
31941
- * rule. `classFilter` narrows the rule to specific object classes —
31942
- * "drop person detections in the street, but keep cars" is one
31943
- * `exclude` rule with `classFilter: ['person']`.
31944
- *
31945
- * `enabled` is a soft toggle — the operator can keep the rule
31946
- * configured but inert without deleting it.
31947
- */
31948
- var ZoneRuleSchema = object({
31949
- /** Stable rule id — survives edits, used by the UI for diffing. */
31950
- id: string(),
31951
- /** Optional human-readable label rendered in the rule editor. */
31952
- name: string().optional(),
31953
- /** Zones this rule targets. The rule's `mode` applies to ALL
31954
- * listed zones (OR-set: a detection in any one of them counts).
31955
- * At least one zone id required — a rule with no targets is a
31956
- * configuration mistake and the form validator rejects it. */
31957
- zoneIds: array(string()).min(1).readonly(),
31958
- mode: ZoneRuleModeEnum,
31959
- /**
31960
- * Class names this rule applies to. Empty / undefined ⇒ rule
31961
- * applies to every class. Class strings match the `macroClass`
31962
- * field on detections (e.g. `person`, `car`, `dog`).
31963
- */
31964
- classFilter: array(string()).readonly().optional(),
31965
- /**
31966
- * Minimum bbox/mask overlap (0–1) with any of the rule's zones
31967
- * required to consider an entity "in the zone". Defaults to the
31968
- * consumer's stage default when omitted. Kept for back-compat with
31969
- * existing per-rule overrides; new operators pick the value via
31970
- * `bboxInclusionPct` (operator-friendly 0–100). Whichever field is
31971
- * set, the lower-level engine reads it as a 0–1 fraction.
31972
- */
31973
- overlapThreshold: number().min(0).max(1).optional(),
31974
- /**
31975
- * Operator-friendly version of `overlapThreshold` — the percentage
31976
- * of the detection's bbox that must lie inside the zone for the
31977
- * rule to match. Documented default is 85%; the engine substitutes
31978
- * that when the field is omitted (kept optional so existing rules
31979
- * stored without it stay valid).
31980
- *
31981
- * When BOTH `overlapThreshold` and `bboxInclusionPct` are set on a
31982
- * rule, the engine prefers `bboxInclusionPct` because it's the
31983
- * field exposed in the UI. Internally both feed the same gate.
31984
- */
31985
- bboxInclusionPct: number().min(0).max(100).optional(),
31986
- /**
31987
- * When `true` and a detection has a segmentation mask, use the
31988
- * mask for overlap instead of the bbox. Detection-stage only;
31989
- * motion rules ignore this field.
31990
- */
31991
- preferMask: boolean().optional(),
31992
- /**
31993
- * Soft-toggle: `false` disables the rule without deleting it.
31994
- * Defaults to `true` so operators creating a rule via the UI
31995
- * see it active immediately.
31996
- */
31997
- enabled: boolean().default(true)
31998
- });
31999
- array(ZoneRuleSchema).readonly();
32000
- /**
32001
32446
  * Script-runner cap. Models HA `script.*` entities on
32002
32447
  * `DeviceType.Script`. A Script is a pre-recorded action sequence
32003
32448
  * that can be invoked imperatively — optionally with a variables
@@ -36913,6 +37358,12 @@ Object.freeze({
36913
37358
  addonId: null,
36914
37359
  access: "create"
36915
37360
  },
37361
+ "pipelineAnalytics.cancelRelocateMedia": {
37362
+ capName: "pipeline-analytics",
37363
+ capScope: "device",
37364
+ addonId: null,
37365
+ access: "create"
37366
+ },
36916
37367
  "pipelineAnalytics.cancelStorageMigrationMove": {
36917
37368
  capName: "pipeline-analytics",
36918
37369
  capScope: "device",
@@ -37087,6 +37538,12 @@ Object.freeze({
37087
37538
  addonId: null,
37088
37539
  access: "view"
37089
37540
  },
37541
+ "pipelineAnalytics.listRelocateMediaJobs": {
37542
+ capName: "pipeline-analytics",
37543
+ capScope: "device",
37544
+ addonId: null,
37545
+ access: "view"
37546
+ },
37090
37547
  "pipelineAnalytics.listRetrainAnnotations": {
37091
37548
  capName: "pipeline-analytics",
37092
37549
  capScope: "device",
@@ -37165,6 +37622,12 @@ Object.freeze({
37165
37622
  addonId: null,
37166
37623
  access: "create"
37167
37624
  },
37625
+ "pipelineAnalytics.relocateMedia": {
37626
+ capName: "pipeline-analytics",
37627
+ capScope: "device",
37628
+ addonId: null,
37629
+ access: "create"
37630
+ },
37168
37631
  "pipelineAnalytics.restageRetrainTrack": {
37169
37632
  capName: "pipeline-analytics",
37170
37633
  capScope: "device",
@@ -37177,6 +37640,12 @@ Object.freeze({
37177
37640
  addonId: null,
37178
37641
  access: "create"
37179
37642
  },
37643
+ "pipelineAnalytics.runReplayFrameProcessor": {
37644
+ capName: "pipeline-analytics",
37645
+ capScope: "device",
37646
+ addonId: null,
37647
+ access: "create"
37648
+ },
37180
37649
  "pipelineAnalytics.saveRetrainAnnotations": {
37181
37650
  capName: "pipeline-analytics",
37182
37651
  capScope: "device",
@@ -37309,6 +37778,12 @@ Object.freeze({
37309
37778
  addonId: null,
37310
37779
  access: "view"
37311
37780
  },
37781
+ "pipelineExecutor.getInferenceDeviceHealth": {
37782
+ capName: "pipeline-executor",
37783
+ capScope: "system",
37784
+ addonId: null,
37785
+ access: "view"
37786
+ },
37312
37787
  "pipelineExecutor.getOrchestratorConfigSchema": {
37313
37788
  capName: "pipeline-executor",
37314
37789
  capScope: "system",
@@ -37381,6 +37856,12 @@ Object.freeze({
37381
37856
  addonId: null,
37382
37857
  access: "view"
37383
37858
  },
37859
+ "pipelineExecutor.rearmInferenceDevice": {
37860
+ capName: "pipeline-executor",
37861
+ capScope: "system",
37862
+ addonId: null,
37863
+ access: "create"
37864
+ },
37384
37865
  "pipelineExecutor.runAudioTest": {
37385
37866
  capName: "pipeline-executor",
37386
37867
  capScope: "system",
@@ -40629,6 +41110,11 @@ Object.freeze({
40629
41110
  form: "single",
40630
41111
  optional: false
40631
41112
  }],
41113
+ "pipelineAnalytics.runReplayFrameProcessor": [{
41114
+ name: "deviceId",
41115
+ form: "single",
41116
+ optional: false
41117
+ }],
40632
41118
  "pipelineAnalytics.saveRetrainAnnotations": [{
40633
41119
  name: "deviceId",
40634
41120
  form: "single",