@camstack/addon-provider-wyze 0.2.49 → 0.2.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +313 -181
  2. package/dist/addon.mjs +313 -181
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8035,6 +8035,21 @@ var RelocateJobSchema = object({
8035
8035
  bytesMoved: number().int(),
8036
8036
  /** Total files discovered up front; null while (or when) unknown. */
8037
8037
  filesTotal: number().int().nullable(),
8038
+ /**
8039
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8040
+ * made that nobody asked for, so it is reported where the operator reads the
8041
+ * job rather than only in a log line.
8042
+ *
8043
+ * A footage segment records its byte count in its own NAME, and the durable
8044
+ * hour row derives its aggregates from those names. A file that does not
8045
+ * match its name therefore makes the ledger's sums — and with them quota and
8046
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8047
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8048
+ *
8049
+ * Absent on lanes where the question has no meaning: a media blob's size is
8050
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8051
+ */
8052
+ rowsReconciled: number().int().nonnegative().optional(),
8038
8053
  startedAt: number(),
8039
8054
  finishedAt: number().nullable(),
8040
8055
  error: string().nullable()
@@ -8103,14 +8118,42 @@ var RelocateMediaInputSchema = object({
8103
8118
  /** Omitted = `move`, the pre-existing behaviour. */
8104
8119
  mode: MediaRelocateModeSchema.optional()
8105
8120
  });
8106
- /** How many rows still carry NO `locationId` — the population a repoint would
8107
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8108
- * value that permits a non-blocking `eventMedia` cutover. */
8109
- var UnstampedEventMediaCountSchema = object({
8110
- media: number().int().nonnegative(),
8111
- retrainFrames: number().int().nonnegative(),
8112
- total: number().int().nonnegative()
8121
+ /**
8122
+ * The unstamped population of ONE collection — split, because the gate and the
8123
+ * operator ask two different questions and only one of them has to be cheap.
8124
+ *
8125
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8126
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8127
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8128
+ * that matters — after a seal, when the population is empty.
8129
+ *
8130
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8131
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8132
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8133
+ * and useful answer: "there are some, and this read could not say how many"
8134
+ * still refuses the cutover, which is the whole job.
8135
+ */
8136
+ var UnstampedRowsSchema = object({
8137
+ present: boolean(),
8138
+ rows: number().int().nonnegative().nullable()
8113
8139
  });
8140
+ /**
8141
+ * How many rows still carry NO `locationId` — the population a repoint would
8142
+ * silently re-aim at a disk that does not hold their bytes.
8143
+ *
8144
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8145
+ * over. The gate opens on a measured absence and on nothing else; an unread
8146
+ * collection and an empty one are different facts, and this repo has already
8147
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8148
+ */
8149
+ var UnstampedEventMediaCountSchema = object({
8150
+ media: UnstampedRowsSchema,
8151
+ retrainFrames: UnstampedRowsSchema,
8152
+ /** True when EITHER collection holds one. The refusal reads this. */
8153
+ anyPresent: boolean(),
8154
+ /** Sum across both, or `null` when either lane could not be counted. */
8155
+ total: number().int().nonnegative().nullable()
8156
+ }).nullable();
8114
8157
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8115
8158
  /** The independently selectable logical storage classes — every class
8116
8159
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8221,6 +8264,10 @@ var StorageMigrationMoveProgressSchema = object({
8221
8264
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8222
8265
  filesTotal: number().int().nonnegative().nullable(),
8223
8266
  bytesMoved: number().int().nonnegative(),
8267
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8268
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8269
+ * rows would be the same failure as one that silently skipped them. */
8270
+ rowsReconciled: number().int().nonnegative().optional(),
8224
8271
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8225
8272
  * crash gets a new mover, and a rate computed from the migration's start
8226
8273
  * would silently average in the time nothing was running. */
@@ -13186,6 +13233,114 @@ method(object({
13186
13233
  height: number()
13187
13234
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13188
13235
  /**
13236
+ * `failure-contribution` — the capability an addon reports its OWN losses
13237
+ * through, per camera, with the denominator attached. It stores nothing.
13238
+ *
13239
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13240
+ *
13241
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13242
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13243
+ * copied: the contributor reports what it already knows, hub-main adds only
13244
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13245
+ * somebody to forget to edit.
13246
+ *
13247
+ * They are not merged, because their invariants are opposites:
13248
+ *
13249
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13250
+ * claim a camera cost nothing, which is a measurement nobody made;
13251
+ * - a `failure-contribution` zero is the **most valuable value on the
13252
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13253
+ * and it is exactly what an absent entry cannot say.
13254
+ *
13255
+ * Putting a loss counter on a cost entry would also break the reconciliation
13256
+ * that gives `load-contribution` its point: contributions are subtracted from
13257
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13258
+ * has no process.
13259
+ *
13260
+ * ## Why not a log line, since the counters already exist
13261
+ *
13262
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13263
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13264
+ * ends in a log line, and a log line is the thing the operator asked to stop
13265
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13266
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13267
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13268
+ * media blackout were both diagnosed. The counters stay; this is where they can
13269
+ * be READ.
13270
+ *
13271
+ * ## The rate is served with its denominator or not at all
13272
+ *
13273
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13274
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13275
+ * than yesterday" and was **flat across twelve hours** once divided by the
13276
+ * successes on the same path. A surface that publishes only the numerator
13277
+ * reproduces that mistake on every read.
13278
+ *
13279
+ * ## Shape
13280
+ *
13281
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13282
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13283
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13284
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13285
+ * a forked runner's entries reach hub-main over transport that already exists.
13286
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13287
+ * result through `system.getFailureContributions`.
13288
+ */
13289
+ var FailureReasonCountSchema = object({
13290
+ /**
13291
+ * Why the attempt did not land, in the contributor's own vocabulary —
13292
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13293
+ * strings that already appear in this repo's logs and, where one exists, the
13294
+ * same string the per-track `previewMissReason` records (D276): a second
13295
+ * vocabulary for the same loss would make the row and the counter
13296
+ * un-joinable.
13297
+ */
13298
+ reason: string(),
13299
+ count: number().int().nonnegative()
13300
+ });
13301
+ var FailureContributionSchema = object({
13302
+ /**
13303
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13304
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13305
+ * `unit` free: the families are owned by different addons and a shared enum
13306
+ * is a central list that rots invisibly.
13307
+ */
13308
+ family: string(),
13309
+ /**
13310
+ * The NUMERIC device id — the same value every log line carries as
13311
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13312
+ * cannot name the camera must not emit the entry, because a fleet total
13313
+ * cannot answer the only question anybody asks of this surface.
13314
+ */
13315
+ deviceId: number().int().positive(),
13316
+ /**
13317
+ * A second dimension inside the family: the model / step id for an inference
13318
+ * timeout, so "which camera AND which model" is one read. Absent when the
13319
+ * family has a single variant.
13320
+ */
13321
+ variant: string().optional(),
13322
+ /**
13323
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13324
+ * differencing two reads must drop the interval when it changes, because the
13325
+ * counter restarted from zero in a respawned runner. Same discipline as
13326
+ * `LoadContribution.startedAtMs`.
13327
+ */
13328
+ sinceMs: number(),
13329
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13330
+ atMs: number(),
13331
+ /**
13332
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13333
+ * window. A failure count published without it is the mistake this schema
13334
+ * exists to make impossible.
13335
+ */
13336
+ attempts: number().int().nonnegative(),
13337
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13338
+ succeeded: number().int().nonnegative(),
13339
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13340
+ reasons: array(FailureReasonCountSchema).readonly()
13341
+ });
13342
+ method(_void(), array(FailureContributionSchema).readonly());
13343
+ /**
13189
13344
  * filesystem-browse — per-node capability for browsing the node's local
13190
13345
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13191
13346
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13707,6 +13862,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13707
13862
  kind: "mutation",
13708
13863
  auth: "admin"
13709
13864
  });
13865
+ var LoadContributionSchema = object({
13866
+ role: _enum([
13867
+ "decode",
13868
+ "transcode",
13869
+ "recording",
13870
+ "streaming",
13871
+ "detection"
13872
+ ]),
13873
+ /**
13874
+ * The NUMERIC device id — the same value every log line carries as
13875
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13876
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13877
+ * contributor that cannot name its camera must not emit the entry at all,
13878
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13879
+ * and would quietly turn one camera's cost into everybody's.
13880
+ */
13881
+ deviceId: number().int().positive().nullable(),
13882
+ attribution: _enum([
13883
+ "measured",
13884
+ "accounted",
13885
+ "unattributable"
13886
+ ]),
13887
+ /**
13888
+ * What ONE entry is, in the contributor's own words — `615/high`,
13889
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13890
+ * family and inventing a common one would lose the only information that
13891
+ * makes two entries for the same camera distinguishable.
13892
+ */
13893
+ unit: string(),
13894
+ /**
13895
+ * The OS process this cost lives in, when there is one. Present so a
13896
+ * consumer can (a) tell two generations of the same unit apart across a
13897
+ * restart, and (b) subtract claimed processes from the node's process
13898
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13899
+ * process of its own.
13900
+ */
13901
+ pid: number().int().positive().optional(),
13902
+ /**
13903
+ * When this generation started. The pid's incarnation marker: a consumer
13904
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13905
+ * window when this changes, because the counter restarted from zero in a new
13906
+ * process.
13907
+ */
13908
+ startedAtMs: number().optional(),
13909
+ /**
13910
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13911
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13912
+ * contribution is asked for.
13913
+ *
13914
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13915
+ * needs a sampler, and a new per-node sampler is the defect half of
13916
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13917
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13918
+ *
13919
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13920
+ * an entry with no process.
13921
+ */
13922
+ cpuSeconds: number().optional(),
13923
+ /** Resident bytes of this unit's process, same source and same rules. */
13924
+ rssBytes: number().optional()
13925
+ });
13926
+ method(_void(), array(LoadContributionSchema).readonly());
13710
13927
  /**
13711
13928
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13712
13929
  * through. It stores nothing.
@@ -13783,176 +14000,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13783
14000
  tags: record(string(), string()).optional()
13784
14001
  }), array(LogEntrySchema).readonly());
13785
14002
  /**
13786
- * `failure-contribution` — the capability an addon reports its OWN losses
13787
- * through, per camera, with the denominator attached. It stores nothing.
13788
- *
13789
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13790
- *
13791
- * `load-contribution` answers *what did this camera COST*. This answers *what
13792
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13793
- * copied: the contributor reports what it already knows, hub-main adds only
13794
- * `addonId`, nothing needs global knowledge, and there is no central list for
13795
- * somebody to forget to edit.
13796
- *
13797
- * They are not merged, because their invariants are opposites:
13798
- *
13799
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13800
- * claim a camera cost nothing, which is a measurement nobody made;
13801
- * - a `failure-contribution` zero is the **most valuable value on the
13802
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13803
- * and it is exactly what an absent entry cannot say.
13804
- *
13805
- * Putting a loss counter on a cost entry would also break the reconciliation
13806
- * that gives `load-contribution` its point: contributions are subtracted from
13807
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13808
- * has no process.
13809
- *
13810
- * ## Why not a log line, since the counters already exist
13811
- *
13812
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13813
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13814
- * ends in a log line, and a log line is the thing the operator asked to stop
13815
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13816
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13817
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13818
- * media blackout were both diagnosed. The counters stay; this is where they can
13819
- * be READ.
13820
- *
13821
- * ## The rate is served with its denominator or not at all
13822
- *
13823
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13824
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13825
- * than yesterday" and was **flat across twelve hours** once divided by the
13826
- * successes on the same path. A surface that publishes only the numerator
13827
- * reproduces that mistake on every read.
13828
- *
13829
- * ## Shape
13830
- *
13831
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13832
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13833
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13834
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13835
- * a forked runner's entries reach hub-main over transport that already exists.
13836
- * No new UDS message, no second registry (D3). The operator reads the assembled
13837
- * result through `system.getFailureContributions`.
13838
- */
13839
- var FailureReasonCountSchema = object({
13840
- /**
13841
- * Why the attempt did not land, in the contributor's own vocabulary —
13842
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13843
- * strings that already appear in this repo's logs and, where one exists, the
13844
- * same string the per-track `previewMissReason` records (D276): a second
13845
- * vocabulary for the same loss would make the row and the counter
13846
- * un-joinable.
13847
- */
13848
- reason: string(),
13849
- count: number().int().nonnegative()
13850
- });
13851
- var FailureContributionSchema = object({
13852
- /**
13853
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13854
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13855
- * `unit` free: the families are owned by different addons and a shared enum
13856
- * is a central list that rots invisibly.
13857
- */
13858
- family: string(),
13859
- /**
13860
- * The NUMERIC device id — the same value every log line carries as
13861
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13862
- * cannot name the camera must not emit the entry, because a fleet total
13863
- * cannot answer the only question anybody asks of this surface.
13864
- */
13865
- deviceId: number().int().positive(),
13866
- /**
13867
- * A second dimension inside the family: the model / step id for an inference
13868
- * timeout, so "which camera AND which model" is one read. Absent when the
13869
- * family has a single variant.
13870
- */
13871
- variant: string().optional(),
13872
- /**
13873
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13874
- * differencing two reads must drop the interval when it changes, because the
13875
- * counter restarted from zero in a respawned runner. Same discipline as
13876
- * `LoadContribution.startedAtMs`.
13877
- */
13878
- sinceMs: number(),
13879
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13880
- atMs: number(),
13881
- /**
13882
- * THE DENOMINATOR — every attempt on this path for this camera in the
13883
- * window. A failure count published without it is the mistake this schema
13884
- * exists to make impossible.
13885
- */
13886
- attempts: number().int().nonnegative(),
13887
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13888
- succeeded: number().int().nonnegative(),
13889
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13890
- reasons: array(FailureReasonCountSchema).readonly()
13891
- });
13892
- method(_void(), array(FailureContributionSchema).readonly());
13893
- var LoadContributionSchema = object({
13894
- role: _enum([
13895
- "decode",
13896
- "transcode",
13897
- "recording",
13898
- "streaming",
13899
- "detection"
13900
- ]),
13901
- /**
13902
- * The NUMERIC device id — the same value every log line carries as
13903
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13904
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13905
- * contributor that cannot name its camera must not emit the entry at all,
13906
- * because an unnamed per-camera entry is indistinguishable from a shared one
13907
- * and would quietly turn one camera's cost into everybody's.
13908
- */
13909
- deviceId: number().int().positive().nullable(),
13910
- attribution: _enum([
13911
- "measured",
13912
- "accounted",
13913
- "unattributable"
13914
- ]),
13915
- /**
13916
- * What ONE entry is, in the contributor's own words — `615/high`,
13917
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13918
- * family and inventing a common one would lose the only information that
13919
- * makes two entries for the same camera distinguishable.
13920
- */
13921
- unit: string(),
13922
- /**
13923
- * The OS process this cost lives in, when there is one. Present so a
13924
- * consumer can (a) tell two generations of the same unit apart across a
13925
- * restart, and (b) subtract claimed processes from the node's process
13926
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13927
- * process of its own.
13928
- */
13929
- pid: number().int().positive().optional(),
13930
- /**
13931
- * When this generation started. The pid's incarnation marker: a consumer
13932
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13933
- * window when this changes, because the counter restarted from zero in a new
13934
- * process.
13935
- */
13936
- startedAtMs: number().optional(),
13937
- /**
13938
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13939
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13940
- * contribution is asked for.
13941
- *
13942
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13943
- * needs a sampler, and a new per-node sampler is the defect half of
13944
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13945
- * by whoever already keeps a history; a rate cannot be un-averaged.
13946
- *
13947
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13948
- * an entry with no process.
13949
- */
13950
- cpuSeconds: number().optional(),
13951
- /** Resident bytes of this unit's process, same source and same rules. */
13952
- rssBytes: number().optional()
13953
- });
13954
- method(_void(), array(LoadContributionSchema).readonly());
13955
- /**
13956
14003
  * `login-method` — collection cap through which auth addons contribute
13957
14004
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13958
14005
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18690,12 +18737,53 @@ var MediaFileKindEnum = _enum([
18690
18737
  "keyFrameSmall",
18691
18738
  "thumbnailSmall"
18692
18739
  ]);
18740
+ /**
18741
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18742
+ * ARE — never the bytes themselves.
18743
+ *
18744
+ * ## Why `url` and not `base64`
18745
+ *
18746
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18747
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18748
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18749
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18750
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18751
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18752
+ *
18753
+ * `url` points at the `event-media` data plane
18754
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18755
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18756
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18757
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18758
+ * no less protected than they were inside a `view`-level cap response — see
18759
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18760
+ * (per-device scoping).
18761
+ *
18762
+ * The URL is built from the row's **stored** key, which is not always its
18763
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18764
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18765
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18766
+ *
18767
+ * ## `base64` is TRANSITIONAL and is going away
18768
+ *
18769
+ * It is still populated for one reason: the deployed viewer's track-detail
18770
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18771
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18772
+ * triangle — not as absence. Removing the field before that viewer ships is an
18773
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18774
+ * delete this line and the `withBytes` pass-through in
18775
+ * `analytics-query-facade.ts`; nothing else reads it.
18776
+ */
18693
18777
  var MediaFileSchema = object({
18694
18778
  key: string(),
18695
18779
  kind: MediaFileKindEnum,
18696
- base64: string(),
18697
18780
  sizeBytes: number(),
18698
18781
  timestamp: number()
18782
+ }).extend({
18783
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18784
+ url: string(),
18785
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18786
+ base64: string()
18699
18787
  });
18700
18788
  /**
18701
18789
  * One media row WITHOUT its bytes.
@@ -18707,7 +18795,9 @@ var MediaFileSchema = object({
18707
18795
  * blocks the whole view.
18708
18796
  *
18709
18797
  * `sizeBytes` is carried because it is what lets a client decide between the
18710
- * stored blob and a `?variant=thumb` rendering without fetching either.
18798
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18799
+ * `url` because a client that had to build the plane path itself is a second
18800
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18711
18801
  */
18712
18802
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18713
18803
  /**
@@ -19396,6 +19486,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19396
19486
  }), array(MediaFileSchema).readonly()), method(object({
19397
19487
  trackId: string(),
19398
19488
  deviceId: number()
19489
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19490
+ eventId: string(),
19491
+ deviceId: number()
19399
19492
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19400
19493
  kind: "mutation",
19401
19494
  auth: "admin"
@@ -24498,10 +24591,24 @@ var FaceClusterSchema = object({
24498
24591
  size: number().int(),
24499
24592
  cohesion: number()
24500
24593
  });
24594
+ /**
24595
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24596
+ * are — never the bytes.
24597
+ *
24598
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24599
+ * track/event contract) is still populated because a deployed viewer requires
24600
+ * the field to parse a row at all; this method has no such reader. Its ONE
24601
+ * caller is the admin UI's detail modal, which was building
24602
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24603
+ * dialog already rendering its key FRAME from the `event-media` plane.
24604
+ *
24605
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24606
+ * media key directly, so this needed no new plane and no new access decision.
24607
+ */
24501
24608
  var MediaFileLiteSchema$1 = object({
24502
24609
  key: string(),
24503
24610
  kind: string(),
24504
- base64: string(),
24611
+ url: string(),
24505
24612
  sizeBytes: number(),
24506
24613
  timestamp: number()
24507
24614
  });
@@ -27523,10 +27630,24 @@ var PlateInfoSchema = object({
27523
27630
  */
27524
27631
  cropUrl: string().optional()
27525
27632
  });
27633
+ /**
27634
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27635
+ * are — never the bytes.
27636
+ *
27637
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27638
+ * track/event contract) is still populated because a deployed viewer requires
27639
+ * the field to parse a row at all; this method has no such reader. Its ONE
27640
+ * caller is the admin UI's detail modal, which was building
27641
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27642
+ * dialog already rendering its key FRAME from the `event-media` plane.
27643
+ *
27644
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27645
+ * media key directly, so this needed no new plane and no new access decision.
27646
+ */
27526
27647
  var MediaFileLiteSchema = object({
27527
27648
  key: string(),
27528
27649
  kind: string(),
27529
- base64: string(),
27650
+ url: string(),
27530
27651
  sizeBytes: number(),
27531
27652
  timestamp: number()
27532
27653
  });
@@ -35423,6 +35544,12 @@ Object.freeze({
35423
35544
  addonId: null,
35424
35545
  access: "view"
35425
35546
  },
35547
+ "pipelineAnalytics.listEventMedia": {
35548
+ capName: "pipeline-analytics",
35549
+ capScope: "device",
35550
+ addonId: null,
35551
+ access: "view"
35552
+ },
35426
35553
  "pipelineAnalytics.listGroups": {
35427
35554
  capName: "pipeline-analytics",
35428
35555
  capScope: "device",
@@ -39046,6 +39173,11 @@ Object.freeze({
39046
39173
  form: "array",
39047
39174
  optional: false
39048
39175
  }],
39176
+ "pipelineAnalytics.listEventMedia": [{
39177
+ name: "deviceId",
39178
+ form: "single",
39179
+ optional: false
39180
+ }],
39049
39181
  "pipelineAnalytics.listGroups": [{
39050
39182
  name: "deviceIds",
39051
39183
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8014,6 +8014,21 @@ var RelocateJobSchema = object({
8014
8014
  bytesMoved: number().int(),
8015
8015
  /** Total files discovered up front; null while (or when) unknown. */
8016
8016
  filesTotal: number().int().nullable(),
8017
+ /**
8018
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8019
+ * made that nobody asked for, so it is reported where the operator reads the
8020
+ * job rather than only in a log line.
8021
+ *
8022
+ * A footage segment records its byte count in its own NAME, and the durable
8023
+ * hour row derives its aggregates from those names. A file that does not
8024
+ * match its name therefore makes the ledger's sums — and with them quota and
8025
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8026
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8027
+ *
8028
+ * Absent on lanes where the question has no meaning: a media blob's size is
8029
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8030
+ */
8031
+ rowsReconciled: number().int().nonnegative().optional(),
8017
8032
  startedAt: number(),
8018
8033
  finishedAt: number().nullable(),
8019
8034
  error: string().nullable()
@@ -8082,14 +8097,42 @@ var RelocateMediaInputSchema = object({
8082
8097
  /** Omitted = `move`, the pre-existing behaviour. */
8083
8098
  mode: MediaRelocateModeSchema.optional()
8084
8099
  });
8085
- /** How many rows still carry NO `locationId` — the population a repoint would
8086
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8087
- * value that permits a non-blocking `eventMedia` cutover. */
8088
- var UnstampedEventMediaCountSchema = object({
8089
- media: number().int().nonnegative(),
8090
- retrainFrames: number().int().nonnegative(),
8091
- total: number().int().nonnegative()
8100
+ /**
8101
+ * The unstamped population of ONE collection — split, because the gate and the
8102
+ * operator ask two different questions and only one of them has to be cheap.
8103
+ *
8104
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8105
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8106
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8107
+ * that matters — after a seal, when the population is empty.
8108
+ *
8109
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8110
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8111
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8112
+ * and useful answer: "there are some, and this read could not say how many"
8113
+ * still refuses the cutover, which is the whole job.
8114
+ */
8115
+ var UnstampedRowsSchema = object({
8116
+ present: boolean(),
8117
+ rows: number().int().nonnegative().nullable()
8092
8118
  });
8119
+ /**
8120
+ * How many rows still carry NO `locationId` — the population a repoint would
8121
+ * silently re-aim at a disk that does not hold their bytes.
8122
+ *
8123
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8124
+ * over. The gate opens on a measured absence and on nothing else; an unread
8125
+ * collection and an empty one are different facts, and this repo has already
8126
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8127
+ */
8128
+ var UnstampedEventMediaCountSchema = object({
8129
+ media: UnstampedRowsSchema,
8130
+ retrainFrames: UnstampedRowsSchema,
8131
+ /** True when EITHER collection holds one. The refusal reads this. */
8132
+ anyPresent: boolean(),
8133
+ /** Sum across both, or `null` when either lane could not be counted. */
8134
+ total: number().int().nonnegative().nullable()
8135
+ }).nullable();
8093
8136
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8094
8137
  /** The independently selectable logical storage classes — every class
8095
8138
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8200,6 +8243,10 @@ var StorageMigrationMoveProgressSchema = object({
8200
8243
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8201
8244
  filesTotal: number().int().nonnegative().nullable(),
8202
8245
  bytesMoved: number().int().nonnegative(),
8246
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8247
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8248
+ * rows would be the same failure as one that silently skipped them. */
8249
+ rowsReconciled: number().int().nonnegative().optional(),
8203
8250
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8204
8251
  * crash gets a new mover, and a rate computed from the migration's start
8205
8252
  * would silently average in the time nothing was running. */
@@ -13165,6 +13212,114 @@ method(object({
13165
13212
  height: number()
13166
13213
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13167
13214
  /**
13215
+ * `failure-contribution` — the capability an addon reports its OWN losses
13216
+ * through, per camera, with the denominator attached. It stores nothing.
13217
+ *
13218
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13219
+ *
13220
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13221
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13222
+ * copied: the contributor reports what it already knows, hub-main adds only
13223
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13224
+ * somebody to forget to edit.
13225
+ *
13226
+ * They are not merged, because their invariants are opposites:
13227
+ *
13228
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13229
+ * claim a camera cost nothing, which is a measurement nobody made;
13230
+ * - a `failure-contribution` zero is the **most valuable value on the
13231
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13232
+ * and it is exactly what an absent entry cannot say.
13233
+ *
13234
+ * Putting a loss counter on a cost entry would also break the reconciliation
13235
+ * that gives `load-contribution` its point: contributions are subtracted from
13236
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13237
+ * has no process.
13238
+ *
13239
+ * ## Why not a log line, since the counters already exist
13240
+ *
13241
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13242
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13243
+ * ends in a log line, and a log line is the thing the operator asked to stop
13244
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13245
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13246
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13247
+ * media blackout were both diagnosed. The counters stay; this is where they can
13248
+ * be READ.
13249
+ *
13250
+ * ## The rate is served with its denominator or not at all
13251
+ *
13252
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13253
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13254
+ * than yesterday" and was **flat across twelve hours** once divided by the
13255
+ * successes on the same path. A surface that publishes only the numerator
13256
+ * reproduces that mistake on every read.
13257
+ *
13258
+ * ## Shape
13259
+ *
13260
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13261
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13262
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13263
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13264
+ * a forked runner's entries reach hub-main over transport that already exists.
13265
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13266
+ * result through `system.getFailureContributions`.
13267
+ */
13268
+ var FailureReasonCountSchema = object({
13269
+ /**
13270
+ * Why the attempt did not land, in the contributor's own vocabulary —
13271
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13272
+ * strings that already appear in this repo's logs and, where one exists, the
13273
+ * same string the per-track `previewMissReason` records (D276): a second
13274
+ * vocabulary for the same loss would make the row and the counter
13275
+ * un-joinable.
13276
+ */
13277
+ reason: string(),
13278
+ count: number().int().nonnegative()
13279
+ });
13280
+ var FailureContributionSchema = object({
13281
+ /**
13282
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13283
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13284
+ * `unit` free: the families are owned by different addons and a shared enum
13285
+ * is a central list that rots invisibly.
13286
+ */
13287
+ family: string(),
13288
+ /**
13289
+ * The NUMERIC device id — the same value every log line carries as
13290
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13291
+ * cannot name the camera must not emit the entry, because a fleet total
13292
+ * cannot answer the only question anybody asks of this surface.
13293
+ */
13294
+ deviceId: number().int().positive(),
13295
+ /**
13296
+ * A second dimension inside the family: the model / step id for an inference
13297
+ * timeout, so "which camera AND which model" is one read. Absent when the
13298
+ * family has a single variant.
13299
+ */
13300
+ variant: string().optional(),
13301
+ /**
13302
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13303
+ * differencing two reads must drop the interval when it changes, because the
13304
+ * counter restarted from zero in a respawned runner. Same discipline as
13305
+ * `LoadContribution.startedAtMs`.
13306
+ */
13307
+ sinceMs: number(),
13308
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13309
+ atMs: number(),
13310
+ /**
13311
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13312
+ * window. A failure count published without it is the mistake this schema
13313
+ * exists to make impossible.
13314
+ */
13315
+ attempts: number().int().nonnegative(),
13316
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13317
+ succeeded: number().int().nonnegative(),
13318
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13319
+ reasons: array(FailureReasonCountSchema).readonly()
13320
+ });
13321
+ method(_void(), array(FailureContributionSchema).readonly());
13322
+ /**
13168
13323
  * filesystem-browse — per-node capability for browsing the node's local
13169
13324
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13170
13325
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13686,6 +13841,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13686
13841
  kind: "mutation",
13687
13842
  auth: "admin"
13688
13843
  });
13844
+ var LoadContributionSchema = object({
13845
+ role: _enum([
13846
+ "decode",
13847
+ "transcode",
13848
+ "recording",
13849
+ "streaming",
13850
+ "detection"
13851
+ ]),
13852
+ /**
13853
+ * The NUMERIC device id — the same value every log line carries as
13854
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13855
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13856
+ * contributor that cannot name its camera must not emit the entry at all,
13857
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13858
+ * and would quietly turn one camera's cost into everybody's.
13859
+ */
13860
+ deviceId: number().int().positive().nullable(),
13861
+ attribution: _enum([
13862
+ "measured",
13863
+ "accounted",
13864
+ "unattributable"
13865
+ ]),
13866
+ /**
13867
+ * What ONE entry is, in the contributor's own words — `615/high`,
13868
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13869
+ * family and inventing a common one would lose the only information that
13870
+ * makes two entries for the same camera distinguishable.
13871
+ */
13872
+ unit: string(),
13873
+ /**
13874
+ * The OS process this cost lives in, when there is one. Present so a
13875
+ * consumer can (a) tell two generations of the same unit apart across a
13876
+ * restart, and (b) subtract claimed processes from the node's process
13877
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13878
+ * process of its own.
13879
+ */
13880
+ pid: number().int().positive().optional(),
13881
+ /**
13882
+ * When this generation started. The pid's incarnation marker: a consumer
13883
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13884
+ * window when this changes, because the counter restarted from zero in a new
13885
+ * process.
13886
+ */
13887
+ startedAtMs: number().optional(),
13888
+ /**
13889
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13890
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13891
+ * contribution is asked for.
13892
+ *
13893
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13894
+ * needs a sampler, and a new per-node sampler is the defect half of
13895
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13896
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13897
+ *
13898
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13899
+ * an entry with no process.
13900
+ */
13901
+ cpuSeconds: number().optional(),
13902
+ /** Resident bytes of this unit's process, same source and same rules. */
13903
+ rssBytes: number().optional()
13904
+ });
13905
+ method(_void(), array(LoadContributionSchema).readonly());
13689
13906
  /**
13690
13907
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13691
13908
  * through. It stores nothing.
@@ -13762,176 +13979,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13762
13979
  tags: record(string(), string()).optional()
13763
13980
  }), array(LogEntrySchema).readonly());
13764
13981
  /**
13765
- * `failure-contribution` — the capability an addon reports its OWN losses
13766
- * through, per camera, with the denominator attached. It stores nothing.
13767
- *
13768
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13769
- *
13770
- * `load-contribution` answers *what did this camera COST*. This answers *what
13771
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13772
- * copied: the contributor reports what it already knows, hub-main adds only
13773
- * `addonId`, nothing needs global knowledge, and there is no central list for
13774
- * somebody to forget to edit.
13775
- *
13776
- * They are not merged, because their invariants are opposites:
13777
- *
13778
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13779
- * claim a camera cost nothing, which is a measurement nobody made;
13780
- * - a `failure-contribution` zero is the **most valuable value on the
13781
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13782
- * and it is exactly what an absent entry cannot say.
13783
- *
13784
- * Putting a loss counter on a cost entry would also break the reconciliation
13785
- * that gives `load-contribution` its point: contributions are subtracted from
13786
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13787
- * has no process.
13788
- *
13789
- * ## Why not a log line, since the counters already exist
13790
- *
13791
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13792
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13793
- * ends in a log line, and a log line is the thing the operator asked to stop
13794
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13795
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13796
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13797
- * media blackout were both diagnosed. The counters stay; this is where they can
13798
- * be READ.
13799
- *
13800
- * ## The rate is served with its denominator or not at all
13801
- *
13802
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13803
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13804
- * than yesterday" and was **flat across twelve hours** once divided by the
13805
- * successes on the same path. A surface that publishes only the numerator
13806
- * reproduces that mistake on every read.
13807
- *
13808
- * ## Shape
13809
- *
13810
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13811
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13812
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13813
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13814
- * a forked runner's entries reach hub-main over transport that already exists.
13815
- * No new UDS message, no second registry (D3). The operator reads the assembled
13816
- * result through `system.getFailureContributions`.
13817
- */
13818
- var FailureReasonCountSchema = object({
13819
- /**
13820
- * Why the attempt did not land, in the contributor's own vocabulary —
13821
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13822
- * strings that already appear in this repo's logs and, where one exists, the
13823
- * same string the per-track `previewMissReason` records (D276): a second
13824
- * vocabulary for the same loss would make the row and the counter
13825
- * un-joinable.
13826
- */
13827
- reason: string(),
13828
- count: number().int().nonnegative()
13829
- });
13830
- var FailureContributionSchema = object({
13831
- /**
13832
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13833
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13834
- * `unit` free: the families are owned by different addons and a shared enum
13835
- * is a central list that rots invisibly.
13836
- */
13837
- family: string(),
13838
- /**
13839
- * The NUMERIC device id — the same value every log line carries as
13840
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13841
- * cannot name the camera must not emit the entry, because a fleet total
13842
- * cannot answer the only question anybody asks of this surface.
13843
- */
13844
- deviceId: number().int().positive(),
13845
- /**
13846
- * A second dimension inside the family: the model / step id for an inference
13847
- * timeout, so "which camera AND which model" is one read. Absent when the
13848
- * family has a single variant.
13849
- */
13850
- variant: string().optional(),
13851
- /**
13852
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13853
- * differencing two reads must drop the interval when it changes, because the
13854
- * counter restarted from zero in a respawned runner. Same discipline as
13855
- * `LoadContribution.startedAtMs`.
13856
- */
13857
- sinceMs: number(),
13858
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13859
- atMs: number(),
13860
- /**
13861
- * THE DENOMINATOR — every attempt on this path for this camera in the
13862
- * window. A failure count published without it is the mistake this schema
13863
- * exists to make impossible.
13864
- */
13865
- attempts: number().int().nonnegative(),
13866
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13867
- succeeded: number().int().nonnegative(),
13868
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13869
- reasons: array(FailureReasonCountSchema).readonly()
13870
- });
13871
- method(_void(), array(FailureContributionSchema).readonly());
13872
- var LoadContributionSchema = object({
13873
- role: _enum([
13874
- "decode",
13875
- "transcode",
13876
- "recording",
13877
- "streaming",
13878
- "detection"
13879
- ]),
13880
- /**
13881
- * The NUMERIC device id — the same value every log line carries as
13882
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13883
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13884
- * contributor that cannot name its camera must not emit the entry at all,
13885
- * because an unnamed per-camera entry is indistinguishable from a shared one
13886
- * and would quietly turn one camera's cost into everybody's.
13887
- */
13888
- deviceId: number().int().positive().nullable(),
13889
- attribution: _enum([
13890
- "measured",
13891
- "accounted",
13892
- "unattributable"
13893
- ]),
13894
- /**
13895
- * What ONE entry is, in the contributor's own words — `615/high`,
13896
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13897
- * family and inventing a common one would lose the only information that
13898
- * makes two entries for the same camera distinguishable.
13899
- */
13900
- unit: string(),
13901
- /**
13902
- * The OS process this cost lives in, when there is one. Present so a
13903
- * consumer can (a) tell two generations of the same unit apart across a
13904
- * restart, and (b) subtract claimed processes from the node's process
13905
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13906
- * process of its own.
13907
- */
13908
- pid: number().int().positive().optional(),
13909
- /**
13910
- * When this generation started. The pid's incarnation marker: a consumer
13911
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13912
- * window when this changes, because the counter restarted from zero in a new
13913
- * process.
13914
- */
13915
- startedAtMs: number().optional(),
13916
- /**
13917
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13918
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13919
- * contribution is asked for.
13920
- *
13921
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13922
- * needs a sampler, and a new per-node sampler is the defect half of
13923
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13924
- * by whoever already keeps a history; a rate cannot be un-averaged.
13925
- *
13926
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13927
- * an entry with no process.
13928
- */
13929
- cpuSeconds: number().optional(),
13930
- /** Resident bytes of this unit's process, same source and same rules. */
13931
- rssBytes: number().optional()
13932
- });
13933
- method(_void(), array(LoadContributionSchema).readonly());
13934
- /**
13935
13982
  * `login-method` — collection cap through which auth addons contribute
13936
13983
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13937
13984
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18669,12 +18716,53 @@ var MediaFileKindEnum = _enum([
18669
18716
  "keyFrameSmall",
18670
18717
  "thumbnailSmall"
18671
18718
  ]);
18719
+ /**
18720
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18721
+ * ARE — never the bytes themselves.
18722
+ *
18723
+ * ## Why `url` and not `base64`
18724
+ *
18725
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18726
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18727
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18728
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18729
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18730
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18731
+ *
18732
+ * `url` points at the `event-media` data plane
18733
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18734
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18735
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18736
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18737
+ * no less protected than they were inside a `view`-level cap response — see
18738
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18739
+ * (per-device scoping).
18740
+ *
18741
+ * The URL is built from the row's **stored** key, which is not always its
18742
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18743
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18744
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18745
+ *
18746
+ * ## `base64` is TRANSITIONAL and is going away
18747
+ *
18748
+ * It is still populated for one reason: the deployed viewer's track-detail
18749
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18750
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18751
+ * triangle — not as absence. Removing the field before that viewer ships is an
18752
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18753
+ * delete this line and the `withBytes` pass-through in
18754
+ * `analytics-query-facade.ts`; nothing else reads it.
18755
+ */
18672
18756
  var MediaFileSchema = object({
18673
18757
  key: string(),
18674
18758
  kind: MediaFileKindEnum,
18675
- base64: string(),
18676
18759
  sizeBytes: number(),
18677
18760
  timestamp: number()
18761
+ }).extend({
18762
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18763
+ url: string(),
18764
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18765
+ base64: string()
18678
18766
  });
18679
18767
  /**
18680
18768
  * One media row WITHOUT its bytes.
@@ -18686,7 +18774,9 @@ var MediaFileSchema = object({
18686
18774
  * blocks the whole view.
18687
18775
  *
18688
18776
  * `sizeBytes` is carried because it is what lets a client decide between the
18689
- * stored blob and a `?variant=thumb` rendering without fetching either.
18777
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18778
+ * `url` because a client that had to build the plane path itself is a second
18779
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18690
18780
  */
18691
18781
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18692
18782
  /**
@@ -19375,6 +19465,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19375
19465
  }), array(MediaFileSchema).readonly()), method(object({
19376
19466
  trackId: string(),
19377
19467
  deviceId: number()
19468
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19469
+ eventId: string(),
19470
+ deviceId: number()
19378
19471
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19379
19472
  kind: "mutation",
19380
19473
  auth: "admin"
@@ -24477,10 +24570,24 @@ var FaceClusterSchema = object({
24477
24570
  size: number().int(),
24478
24571
  cohesion: number()
24479
24572
  });
24573
+ /**
24574
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24575
+ * are — never the bytes.
24576
+ *
24577
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24578
+ * track/event contract) is still populated because a deployed viewer requires
24579
+ * the field to parse a row at all; this method has no such reader. Its ONE
24580
+ * caller is the admin UI's detail modal, which was building
24581
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24582
+ * dialog already rendering its key FRAME from the `event-media` plane.
24583
+ *
24584
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24585
+ * media key directly, so this needed no new plane and no new access decision.
24586
+ */
24480
24587
  var MediaFileLiteSchema$1 = object({
24481
24588
  key: string(),
24482
24589
  kind: string(),
24483
- base64: string(),
24590
+ url: string(),
24484
24591
  sizeBytes: number(),
24485
24592
  timestamp: number()
24486
24593
  });
@@ -27502,10 +27609,24 @@ var PlateInfoSchema = object({
27502
27609
  */
27503
27610
  cropUrl: string().optional()
27504
27611
  });
27612
+ /**
27613
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27614
+ * are — never the bytes.
27615
+ *
27616
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27617
+ * track/event contract) is still populated because a deployed viewer requires
27618
+ * the field to parse a row at all; this method has no such reader. Its ONE
27619
+ * caller is the admin UI's detail modal, which was building
27620
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27621
+ * dialog already rendering its key FRAME from the `event-media` plane.
27622
+ *
27623
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27624
+ * media key directly, so this needed no new plane and no new access decision.
27625
+ */
27505
27626
  var MediaFileLiteSchema = object({
27506
27627
  key: string(),
27507
27628
  kind: string(),
27508
- base64: string(),
27629
+ url: string(),
27509
27630
  sizeBytes: number(),
27510
27631
  timestamp: number()
27511
27632
  });
@@ -35402,6 +35523,12 @@ Object.freeze({
35402
35523
  addonId: null,
35403
35524
  access: "view"
35404
35525
  },
35526
+ "pipelineAnalytics.listEventMedia": {
35527
+ capName: "pipeline-analytics",
35528
+ capScope: "device",
35529
+ addonId: null,
35530
+ access: "view"
35531
+ },
35405
35532
  "pipelineAnalytics.listGroups": {
35406
35533
  capName: "pipeline-analytics",
35407
35534
  capScope: "device",
@@ -39025,6 +39152,11 @@ Object.freeze({
39025
39152
  form: "array",
39026
39153
  optional: false
39027
39154
  }],
39155
+ "pipelineAnalytics.listEventMedia": [{
39156
+ name: "deviceId",
39157
+ form: "single",
39158
+ optional: false
39159
+ }],
39028
39160
  "pipelineAnalytics.listGroups": [{
39029
39161
  name: "deviceIds",
39030
39162
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-wyze",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "Wyze camera device-provider addon for CamStack — wraps the @apocaliss92/wyze-bridge-js P2P/DTLS client, feeding the stream-broker via the pull-rfc4571 lazy-publish path (a structural twin of addon-provider-reolink)",
5
5
  "keywords": [
6
6
  "camstack",