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