@camstack/addon-provider-rtsp 1.2.45 → 1.2.47

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
@@ -8059,6 +8059,21 @@ var RelocateJobSchema = object({
8059
8059
  bytesMoved: number().int(),
8060
8060
  /** Total files discovered up front; null while (or when) unknown. */
8061
8061
  filesTotal: number().int().nullable(),
8062
+ /**
8063
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8064
+ * made that nobody asked for, so it is reported where the operator reads the
8065
+ * job rather than only in a log line.
8066
+ *
8067
+ * A footage segment records its byte count in its own NAME, and the durable
8068
+ * hour row derives its aggregates from those names. A file that does not
8069
+ * match its name therefore makes the ledger's sums — and with them quota and
8070
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8071
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8072
+ *
8073
+ * Absent on lanes where the question has no meaning: a media blob's size is
8074
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8075
+ */
8076
+ rowsReconciled: number().int().nonnegative().optional(),
8062
8077
  startedAt: number(),
8063
8078
  finishedAt: number().nullable(),
8064
8079
  error: string().nullable()
@@ -8127,14 +8142,42 @@ var RelocateMediaInputSchema = object({
8127
8142
  /** Omitted = `move`, the pre-existing behaviour. */
8128
8143
  mode: MediaRelocateModeSchema.optional()
8129
8144
  });
8130
- /** How many rows still carry NO `locationId` — the population a repoint would
8131
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8132
- * value that permits a non-blocking `eventMedia` cutover. */
8133
- var UnstampedEventMediaCountSchema = object({
8134
- media: number().int().nonnegative(),
8135
- retrainFrames: number().int().nonnegative(),
8136
- total: number().int().nonnegative()
8145
+ /**
8146
+ * The unstamped population of ONE collection split, because the gate and the
8147
+ * operator ask two different questions and only one of them has to be cheap.
8148
+ *
8149
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8150
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8151
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8152
+ * that matters — after a seal, when the population is empty.
8153
+ *
8154
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8155
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8156
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8157
+ * and useful answer: "there are some, and this read could not say how many"
8158
+ * still refuses the cutover, which is the whole job.
8159
+ */
8160
+ var UnstampedRowsSchema = object({
8161
+ present: boolean(),
8162
+ rows: number().int().nonnegative().nullable()
8137
8163
  });
8164
+ /**
8165
+ * How many rows still carry NO `locationId` — the population a repoint would
8166
+ * silently re-aim at a disk that does not hold their bytes.
8167
+ *
8168
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8169
+ * over. The gate opens on a measured absence and on nothing else; an unread
8170
+ * collection and an empty one are different facts, and this repo has already
8171
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8172
+ */
8173
+ var UnstampedEventMediaCountSchema = object({
8174
+ media: UnstampedRowsSchema,
8175
+ retrainFrames: UnstampedRowsSchema,
8176
+ /** True when EITHER collection holds one. The refusal reads this. */
8177
+ anyPresent: boolean(),
8178
+ /** Sum across both, or `null` when either lane could not be counted. */
8179
+ total: number().int().nonnegative().nullable()
8180
+ }).nullable();
8138
8181
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8139
8182
  /** The independently selectable logical storage classes — every class
8140
8183
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8245,6 +8288,10 @@ var StorageMigrationMoveProgressSchema = object({
8245
8288
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8246
8289
  filesTotal: number().int().nonnegative().nullable(),
8247
8290
  bytesMoved: number().int().nonnegative(),
8291
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8292
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8293
+ * rows would be the same failure as one that silently skipped them. */
8294
+ rowsReconciled: number().int().nonnegative().optional(),
8248
8295
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8249
8296
  * crash gets a new mover, and a rate computed from the migration's start
8250
8297
  * would silently average in the time nothing was running. */
@@ -13193,6 +13240,114 @@ method(object({
13193
13240
  height: number()
13194
13241
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13195
13242
  /**
13243
+ * `failure-contribution` — the capability an addon reports its OWN losses
13244
+ * through, per camera, with the denominator attached. It stores nothing.
13245
+ *
13246
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13247
+ *
13248
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13249
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13250
+ * copied: the contributor reports what it already knows, hub-main adds only
13251
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13252
+ * somebody to forget to edit.
13253
+ *
13254
+ * They are not merged, because their invariants are opposites:
13255
+ *
13256
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13257
+ * claim a camera cost nothing, which is a measurement nobody made;
13258
+ * - a `failure-contribution` zero is the **most valuable value on the
13259
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13260
+ * and it is exactly what an absent entry cannot say.
13261
+ *
13262
+ * Putting a loss counter on a cost entry would also break the reconciliation
13263
+ * that gives `load-contribution` its point: contributions are subtracted from
13264
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13265
+ * has no process.
13266
+ *
13267
+ * ## Why not a log line, since the counters already exist
13268
+ *
13269
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13270
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13271
+ * ends in a log line, and a log line is the thing the operator asked to stop
13272
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13273
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13274
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13275
+ * media blackout were both diagnosed. The counters stay; this is where they can
13276
+ * be READ.
13277
+ *
13278
+ * ## The rate is served with its denominator or not at all
13279
+ *
13280
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13281
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13282
+ * than yesterday" and was **flat across twelve hours** once divided by the
13283
+ * successes on the same path. A surface that publishes only the numerator
13284
+ * reproduces that mistake on every read.
13285
+ *
13286
+ * ## Shape
13287
+ *
13288
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13289
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13290
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13291
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13292
+ * a forked runner's entries reach hub-main over transport that already exists.
13293
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13294
+ * result through `system.getFailureContributions`.
13295
+ */
13296
+ var FailureReasonCountSchema = object({
13297
+ /**
13298
+ * Why the attempt did not land, in the contributor's own vocabulary —
13299
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13300
+ * strings that already appear in this repo's logs and, where one exists, the
13301
+ * same string the per-track `previewMissReason` records (D276): a second
13302
+ * vocabulary for the same loss would make the row and the counter
13303
+ * un-joinable.
13304
+ */
13305
+ reason: string(),
13306
+ count: number().int().nonnegative()
13307
+ });
13308
+ var FailureContributionSchema = object({
13309
+ /**
13310
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13311
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13312
+ * `unit` free: the families are owned by different addons and a shared enum
13313
+ * is a central list that rots invisibly.
13314
+ */
13315
+ family: string(),
13316
+ /**
13317
+ * The NUMERIC device id — the same value every log line carries as
13318
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13319
+ * cannot name the camera must not emit the entry, because a fleet total
13320
+ * cannot answer the only question anybody asks of this surface.
13321
+ */
13322
+ deviceId: number().int().positive(),
13323
+ /**
13324
+ * A second dimension inside the family: the model / step id for an inference
13325
+ * timeout, so "which camera AND which model" is one read. Absent when the
13326
+ * family has a single variant.
13327
+ */
13328
+ variant: string().optional(),
13329
+ /**
13330
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13331
+ * differencing two reads must drop the interval when it changes, because the
13332
+ * counter restarted from zero in a respawned runner. Same discipline as
13333
+ * `LoadContribution.startedAtMs`.
13334
+ */
13335
+ sinceMs: number(),
13336
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13337
+ atMs: number(),
13338
+ /**
13339
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13340
+ * window. A failure count published without it is the mistake this schema
13341
+ * exists to make impossible.
13342
+ */
13343
+ attempts: number().int().nonnegative(),
13344
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13345
+ succeeded: number().int().nonnegative(),
13346
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13347
+ reasons: array(FailureReasonCountSchema).readonly()
13348
+ });
13349
+ method(_void(), array(FailureContributionSchema).readonly());
13350
+ /**
13196
13351
  * filesystem-browse — per-node capability for browsing the node's local
13197
13352
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13198
13353
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13714,6 +13869,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13714
13869
  kind: "mutation",
13715
13870
  auth: "admin"
13716
13871
  });
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());
13717
13934
  /**
13718
13935
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13719
13936
  * through. It stores nothing.
@@ -13790,176 +14007,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13790
14007
  tags: record(string(), string()).optional()
13791
14008
  }), array(LogEntrySchema).readonly());
13792
14009
  /**
13793
- * `failure-contribution` — the capability an addon reports its OWN losses
13794
- * through, per camera, with the denominator attached. It stores nothing.
13795
- *
13796
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13797
- *
13798
- * `load-contribution` answers *what did this camera COST*. This answers *what
13799
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13800
- * copied: the contributor reports what it already knows, hub-main adds only
13801
- * `addonId`, nothing needs global knowledge, and there is no central list for
13802
- * somebody to forget to edit.
13803
- *
13804
- * They are not merged, because their invariants are opposites:
13805
- *
13806
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13807
- * claim a camera cost nothing, which is a measurement nobody made;
13808
- * - a `failure-contribution` zero is the **most valuable value on the
13809
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13810
- * and it is exactly what an absent entry cannot say.
13811
- *
13812
- * Putting a loss counter on a cost entry would also break the reconciliation
13813
- * that gives `load-contribution` its point: contributions are subtracted from
13814
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13815
- * has no process.
13816
- *
13817
- * ## Why not a log line, since the counters already exist
13818
- *
13819
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13820
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13821
- * ends in a log line, and a log line is the thing the operator asked to stop
13822
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13823
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13824
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13825
- * media blackout were both diagnosed. The counters stay; this is where they can
13826
- * be READ.
13827
- *
13828
- * ## The rate is served with its denominator or not at all
13829
- *
13830
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13831
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13832
- * than yesterday" and was **flat across twelve hours** once divided by the
13833
- * successes on the same path. A surface that publishes only the numerator
13834
- * reproduces that mistake on every read.
13835
- *
13836
- * ## Shape
13837
- *
13838
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13839
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13840
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13841
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13842
- * a forked runner's entries reach hub-main over transport that already exists.
13843
- * No new UDS message, no second registry (D3). The operator reads the assembled
13844
- * result through `system.getFailureContributions`.
13845
- */
13846
- var FailureReasonCountSchema = object({
13847
- /**
13848
- * Why the attempt did not land, in the contributor's own vocabulary —
13849
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13850
- * strings that already appear in this repo's logs and, where one exists, the
13851
- * same string the per-track `previewMissReason` records (D276): a second
13852
- * vocabulary for the same loss would make the row and the counter
13853
- * un-joinable.
13854
- */
13855
- reason: string(),
13856
- count: number().int().nonnegative()
13857
- });
13858
- var FailureContributionSchema = object({
13859
- /**
13860
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13861
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13862
- * `unit` free: the families are owned by different addons and a shared enum
13863
- * is a central list that rots invisibly.
13864
- */
13865
- family: string(),
13866
- /**
13867
- * The NUMERIC device id — the same value every log line carries as
13868
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13869
- * cannot name the camera must not emit the entry, because a fleet total
13870
- * cannot answer the only question anybody asks of this surface.
13871
- */
13872
- deviceId: number().int().positive(),
13873
- /**
13874
- * A second dimension inside the family: the model / step id for an inference
13875
- * timeout, so "which camera AND which model" is one read. Absent when the
13876
- * family has a single variant.
13877
- */
13878
- variant: string().optional(),
13879
- /**
13880
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13881
- * differencing two reads must drop the interval when it changes, because the
13882
- * counter restarted from zero in a respawned runner. Same discipline as
13883
- * `LoadContribution.startedAtMs`.
13884
- */
13885
- sinceMs: number(),
13886
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13887
- atMs: number(),
13888
- /**
13889
- * THE DENOMINATOR — every attempt on this path for this camera in the
13890
- * window. A failure count published without it is the mistake this schema
13891
- * exists to make impossible.
13892
- */
13893
- attempts: number().int().nonnegative(),
13894
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13895
- succeeded: number().int().nonnegative(),
13896
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13897
- reasons: array(FailureReasonCountSchema).readonly()
13898
- });
13899
- method(_void(), array(FailureContributionSchema).readonly());
13900
- var LoadContributionSchema = object({
13901
- role: _enum([
13902
- "decode",
13903
- "transcode",
13904
- "recording",
13905
- "streaming",
13906
- "detection"
13907
- ]),
13908
- /**
13909
- * The NUMERIC device id — the same value every log line carries as
13910
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13911
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13912
- * contributor that cannot name its camera must not emit the entry at all,
13913
- * because an unnamed per-camera entry is indistinguishable from a shared one
13914
- * and would quietly turn one camera's cost into everybody's.
13915
- */
13916
- deviceId: number().int().positive().nullable(),
13917
- attribution: _enum([
13918
- "measured",
13919
- "accounted",
13920
- "unattributable"
13921
- ]),
13922
- /**
13923
- * What ONE entry is, in the contributor's own words — `615/high`,
13924
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13925
- * family and inventing a common one would lose the only information that
13926
- * makes two entries for the same camera distinguishable.
13927
- */
13928
- unit: string(),
13929
- /**
13930
- * The OS process this cost lives in, when there is one. Present so a
13931
- * consumer can (a) tell two generations of the same unit apart across a
13932
- * restart, and (b) subtract claimed processes from the node's process
13933
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13934
- * process of its own.
13935
- */
13936
- pid: number().int().positive().optional(),
13937
- /**
13938
- * When this generation started. The pid's incarnation marker: a consumer
13939
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13940
- * window when this changes, because the counter restarted from zero in a new
13941
- * process.
13942
- */
13943
- startedAtMs: number().optional(),
13944
- /**
13945
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13946
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13947
- * contribution is asked for.
13948
- *
13949
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13950
- * needs a sampler, and a new per-node sampler is the defect half of
13951
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13952
- * by whoever already keeps a history; a rate cannot be un-averaged.
13953
- *
13954
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13955
- * an entry with no process.
13956
- */
13957
- cpuSeconds: number().optional(),
13958
- /** Resident bytes of this unit's process, same source and same rules. */
13959
- rssBytes: number().optional()
13960
- });
13961
- method(_void(), array(LoadContributionSchema).readonly());
13962
- /**
13963
14010
  * `login-method` — collection cap through which auth addons contribute
13964
14011
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13965
14012
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18697,12 +18744,53 @@ var MediaFileKindEnum = _enum([
18697
18744
  "keyFrameSmall",
18698
18745
  "thumbnailSmall"
18699
18746
  ]);
18747
+ /**
18748
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18749
+ * ARE — never the bytes themselves.
18750
+ *
18751
+ * ## Why `url` and not `base64`
18752
+ *
18753
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18754
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18755
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18756
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18757
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18758
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18759
+ *
18760
+ * `url` points at the `event-media` data plane
18761
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18762
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18763
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18764
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18765
+ * no less protected than they were inside a `view`-level cap response — see
18766
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18767
+ * (per-device scoping).
18768
+ *
18769
+ * The URL is built from the row's **stored** key, which is not always its
18770
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18771
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18772
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18773
+ *
18774
+ * ## `base64` is TRANSITIONAL and is going away
18775
+ *
18776
+ * It is still populated for one reason: the deployed viewer's track-detail
18777
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18778
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18779
+ * triangle — not as absence. Removing the field before that viewer ships is an
18780
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18781
+ * delete this line and the `withBytes` pass-through in
18782
+ * `analytics-query-facade.ts`; nothing else reads it.
18783
+ */
18700
18784
  var MediaFileSchema = object({
18701
18785
  key: string(),
18702
18786
  kind: MediaFileKindEnum,
18703
- base64: string(),
18704
18787
  sizeBytes: number(),
18705
18788
  timestamp: number()
18789
+ }).extend({
18790
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18791
+ url: string(),
18792
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18793
+ base64: string()
18706
18794
  });
18707
18795
  /**
18708
18796
  * One media row WITHOUT its bytes.
@@ -18714,7 +18802,9 @@ var MediaFileSchema = object({
18714
18802
  * blocks the whole view.
18715
18803
  *
18716
18804
  * `sizeBytes` is carried because it is what lets a client decide between the
18717
- * stored blob and a `?variant=thumb` rendering without fetching either.
18805
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18806
+ * `url` because a client that had to build the plane path itself is a second
18807
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18718
18808
  */
18719
18809
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18720
18810
  /**
@@ -19403,6 +19493,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19403
19493
  }), array(MediaFileSchema).readonly()), method(object({
19404
19494
  trackId: string(),
19405
19495
  deviceId: number()
19496
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19497
+ eventId: string(),
19498
+ deviceId: number()
19406
19499
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19407
19500
  kind: "mutation",
19408
19501
  auth: "admin"
@@ -24505,10 +24598,24 @@ var FaceClusterSchema = object({
24505
24598
  size: number().int(),
24506
24599
  cohesion: number()
24507
24600
  });
24601
+ /**
24602
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24603
+ * are — never the bytes.
24604
+ *
24605
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24606
+ * track/event contract) is still populated because a deployed viewer requires
24607
+ * the field to parse a row at all; this method has no such reader. Its ONE
24608
+ * caller is the admin UI's detail modal, which was building
24609
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24610
+ * dialog already rendering its key FRAME from the `event-media` plane.
24611
+ *
24612
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24613
+ * media key directly, so this needed no new plane and no new access decision.
24614
+ */
24508
24615
  var MediaFileLiteSchema$1 = object({
24509
24616
  key: string(),
24510
24617
  kind: string(),
24511
- base64: string(),
24618
+ url: string(),
24512
24619
  sizeBytes: number(),
24513
24620
  timestamp: number()
24514
24621
  });
@@ -27530,10 +27637,24 @@ var PlateInfoSchema = object({
27530
27637
  */
27531
27638
  cropUrl: string().optional()
27532
27639
  });
27640
+ /**
27641
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27642
+ * are — never the bytes.
27643
+ *
27644
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27645
+ * track/event contract) is still populated because a deployed viewer requires
27646
+ * the field to parse a row at all; this method has no such reader. Its ONE
27647
+ * caller is the admin UI's detail modal, which was building
27648
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27649
+ * dialog already rendering its key FRAME from the `event-media` plane.
27650
+ *
27651
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27652
+ * media key directly, so this needed no new plane and no new access decision.
27653
+ */
27533
27654
  var MediaFileLiteSchema = object({
27534
27655
  key: string(),
27535
27656
  kind: string(),
27536
- base64: string(),
27657
+ url: string(),
27537
27658
  sizeBytes: number(),
27538
27659
  timestamp: number()
27539
27660
  });
@@ -35496,6 +35617,12 @@ Object.freeze({
35496
35617
  addonId: null,
35497
35618
  access: "view"
35498
35619
  },
35620
+ "pipelineAnalytics.listEventMedia": {
35621
+ capName: "pipeline-analytics",
35622
+ capScope: "device",
35623
+ addonId: null,
35624
+ access: "view"
35625
+ },
35499
35626
  "pipelineAnalytics.listGroups": {
35500
35627
  capName: "pipeline-analytics",
35501
35628
  capScope: "device",
@@ -39119,6 +39246,11 @@ Object.freeze({
39119
39246
  form: "array",
39120
39247
  optional: false
39121
39248
  }],
39249
+ "pipelineAnalytics.listEventMedia": [{
39250
+ name: "deviceId",
39251
+ form: "single",
39252
+ optional: false
39253
+ }],
39122
39254
  "pipelineAnalytics.listGroups": [{
39123
39255
  name: "deviceIds",
39124
39256
  form: "array",
package/dist/addon.mjs 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. */
@@ -13169,6 +13216,114 @@ method(object({
13169
13216
  height: number()
13170
13217
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13171
13218
  /**
13219
+ * `failure-contribution` — the capability an addon reports its OWN losses
13220
+ * through, per camera, with the denominator attached. It stores nothing.
13221
+ *
13222
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13223
+ *
13224
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13225
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13226
+ * copied: the contributor reports what it already knows, hub-main adds only
13227
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13228
+ * somebody to forget to edit.
13229
+ *
13230
+ * They are not merged, because their invariants are opposites:
13231
+ *
13232
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13233
+ * claim a camera cost nothing, which is a measurement nobody made;
13234
+ * - a `failure-contribution` zero is the **most valuable value on the
13235
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13236
+ * and it is exactly what an absent entry cannot say.
13237
+ *
13238
+ * Putting a loss counter on a cost entry would also break the reconciliation
13239
+ * that gives `load-contribution` its point: contributions are subtracted from
13240
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13241
+ * has no process.
13242
+ *
13243
+ * ## Why not a log line, since the counters already exist
13244
+ *
13245
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13246
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13247
+ * ends in a log line, and a log line is the thing the operator asked to stop
13248
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13249
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13250
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13251
+ * media blackout were both diagnosed. The counters stay; this is where they can
13252
+ * be READ.
13253
+ *
13254
+ * ## The rate is served with its denominator or not at all
13255
+ *
13256
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13257
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13258
+ * than yesterday" and was **flat across twelve hours** once divided by the
13259
+ * successes on the same path. A surface that publishes only the numerator
13260
+ * reproduces that mistake on every read.
13261
+ *
13262
+ * ## Shape
13263
+ *
13264
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13265
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13266
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13267
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13268
+ * a forked runner's entries reach hub-main over transport that already exists.
13269
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13270
+ * result through `system.getFailureContributions`.
13271
+ */
13272
+ var FailureReasonCountSchema = object({
13273
+ /**
13274
+ * Why the attempt did not land, in the contributor's own vocabulary —
13275
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13276
+ * strings that already appear in this repo's logs and, where one exists, the
13277
+ * same string the per-track `previewMissReason` records (D276): a second
13278
+ * vocabulary for the same loss would make the row and the counter
13279
+ * un-joinable.
13280
+ */
13281
+ reason: string(),
13282
+ count: number().int().nonnegative()
13283
+ });
13284
+ var FailureContributionSchema = object({
13285
+ /**
13286
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13287
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13288
+ * `unit` free: the families are owned by different addons and a shared enum
13289
+ * is a central list that rots invisibly.
13290
+ */
13291
+ family: string(),
13292
+ /**
13293
+ * The NUMERIC device id — the same value every log line carries as
13294
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13295
+ * cannot name the camera must not emit the entry, because a fleet total
13296
+ * cannot answer the only question anybody asks of this surface.
13297
+ */
13298
+ deviceId: number().int().positive(),
13299
+ /**
13300
+ * A second dimension inside the family: the model / step id for an inference
13301
+ * timeout, so "which camera AND which model" is one read. Absent when the
13302
+ * family has a single variant.
13303
+ */
13304
+ variant: string().optional(),
13305
+ /**
13306
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13307
+ * differencing two reads must drop the interval when it changes, because the
13308
+ * counter restarted from zero in a respawned runner. Same discipline as
13309
+ * `LoadContribution.startedAtMs`.
13310
+ */
13311
+ sinceMs: number(),
13312
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13313
+ atMs: number(),
13314
+ /**
13315
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13316
+ * window. A failure count published without it is the mistake this schema
13317
+ * exists to make impossible.
13318
+ */
13319
+ attempts: number().int().nonnegative(),
13320
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13321
+ succeeded: number().int().nonnegative(),
13322
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13323
+ reasons: array(FailureReasonCountSchema).readonly()
13324
+ });
13325
+ method(_void(), array(FailureContributionSchema).readonly());
13326
+ /**
13172
13327
  * filesystem-browse — per-node capability for browsing the node's local
13173
13328
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13174
13329
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13690,6 +13845,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13690
13845
  kind: "mutation",
13691
13846
  auth: "admin"
13692
13847
  });
13848
+ var LoadContributionSchema = object({
13849
+ role: _enum([
13850
+ "decode",
13851
+ "transcode",
13852
+ "recording",
13853
+ "streaming",
13854
+ "detection"
13855
+ ]),
13856
+ /**
13857
+ * The NUMERIC device id — the same value every log line carries as
13858
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13859
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13860
+ * contributor that cannot name its camera must not emit the entry at all,
13861
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13862
+ * and would quietly turn one camera's cost into everybody's.
13863
+ */
13864
+ deviceId: number().int().positive().nullable(),
13865
+ attribution: _enum([
13866
+ "measured",
13867
+ "accounted",
13868
+ "unattributable"
13869
+ ]),
13870
+ /**
13871
+ * What ONE entry is, in the contributor's own words — `615/high`,
13872
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13873
+ * family and inventing a common one would lose the only information that
13874
+ * makes two entries for the same camera distinguishable.
13875
+ */
13876
+ unit: string(),
13877
+ /**
13878
+ * The OS process this cost lives in, when there is one. Present so a
13879
+ * consumer can (a) tell two generations of the same unit apart across a
13880
+ * restart, and (b) subtract claimed processes from the node's process
13881
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13882
+ * process of its own.
13883
+ */
13884
+ pid: number().int().positive().optional(),
13885
+ /**
13886
+ * When this generation started. The pid's incarnation marker: a consumer
13887
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13888
+ * window when this changes, because the counter restarted from zero in a new
13889
+ * process.
13890
+ */
13891
+ startedAtMs: number().optional(),
13892
+ /**
13893
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13894
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13895
+ * contribution is asked for.
13896
+ *
13897
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13898
+ * needs a sampler, and a new per-node sampler is the defect half of
13899
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13900
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13901
+ *
13902
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13903
+ * an entry with no process.
13904
+ */
13905
+ cpuSeconds: number().optional(),
13906
+ /** Resident bytes of this unit's process, same source and same rules. */
13907
+ rssBytes: number().optional()
13908
+ });
13909
+ method(_void(), array(LoadContributionSchema).readonly());
13693
13910
  /**
13694
13911
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13695
13912
  * through. It stores nothing.
@@ -13766,176 +13983,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13766
13983
  tags: record(string(), string()).optional()
13767
13984
  }), array(LogEntrySchema).readonly());
13768
13985
  /**
13769
- * `failure-contribution` — the capability an addon reports its OWN losses
13770
- * through, per camera, with the denominator attached. It stores nothing.
13771
- *
13772
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13773
- *
13774
- * `load-contribution` answers *what did this camera COST*. This answers *what
13775
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13776
- * copied: the contributor reports what it already knows, hub-main adds only
13777
- * `addonId`, nothing needs global knowledge, and there is no central list for
13778
- * somebody to forget to edit.
13779
- *
13780
- * They are not merged, because their invariants are opposites:
13781
- *
13782
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13783
- * claim a camera cost nothing, which is a measurement nobody made;
13784
- * - a `failure-contribution` zero is the **most valuable value on the
13785
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13786
- * and it is exactly what an absent entry cannot say.
13787
- *
13788
- * Putting a loss counter on a cost entry would also break the reconciliation
13789
- * that gives `load-contribution` its point: contributions are subtracted from
13790
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13791
- * has no process.
13792
- *
13793
- * ## Why not a log line, since the counters already exist
13794
- *
13795
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13796
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13797
- * ends in a log line, and a log line is the thing the operator asked to stop
13798
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13799
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13800
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13801
- * media blackout were both diagnosed. The counters stay; this is where they can
13802
- * be READ.
13803
- *
13804
- * ## The rate is served with its denominator or not at all
13805
- *
13806
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13807
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13808
- * than yesterday" and was **flat across twelve hours** once divided by the
13809
- * successes on the same path. A surface that publishes only the numerator
13810
- * reproduces that mistake on every read.
13811
- *
13812
- * ## Shape
13813
- *
13814
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13815
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13816
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13817
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13818
- * a forked runner's entries reach hub-main over transport that already exists.
13819
- * No new UDS message, no second registry (D3). The operator reads the assembled
13820
- * result through `system.getFailureContributions`.
13821
- */
13822
- var FailureReasonCountSchema = object({
13823
- /**
13824
- * Why the attempt did not land, in the contributor's own vocabulary —
13825
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13826
- * strings that already appear in this repo's logs and, where one exists, the
13827
- * same string the per-track `previewMissReason` records (D276): a second
13828
- * vocabulary for the same loss would make the row and the counter
13829
- * un-joinable.
13830
- */
13831
- reason: string(),
13832
- count: number().int().nonnegative()
13833
- });
13834
- var FailureContributionSchema = object({
13835
- /**
13836
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13837
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13838
- * `unit` free: the families are owned by different addons and a shared enum
13839
- * is a central list that rots invisibly.
13840
- */
13841
- family: string(),
13842
- /**
13843
- * The NUMERIC device id — the same value every log line carries as
13844
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13845
- * cannot name the camera must not emit the entry, because a fleet total
13846
- * cannot answer the only question anybody asks of this surface.
13847
- */
13848
- deviceId: number().int().positive(),
13849
- /**
13850
- * A second dimension inside the family: the model / step id for an inference
13851
- * timeout, so "which camera AND which model" is one read. Absent when the
13852
- * family has a single variant.
13853
- */
13854
- variant: string().optional(),
13855
- /**
13856
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13857
- * differencing two reads must drop the interval when it changes, because the
13858
- * counter restarted from zero in a respawned runner. Same discipline as
13859
- * `LoadContribution.startedAtMs`.
13860
- */
13861
- sinceMs: number(),
13862
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13863
- atMs: number(),
13864
- /**
13865
- * THE DENOMINATOR — every attempt on this path for this camera in the
13866
- * window. A failure count published without it is the mistake this schema
13867
- * exists to make impossible.
13868
- */
13869
- attempts: number().int().nonnegative(),
13870
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13871
- succeeded: number().int().nonnegative(),
13872
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13873
- reasons: array(FailureReasonCountSchema).readonly()
13874
- });
13875
- method(_void(), array(FailureContributionSchema).readonly());
13876
- var LoadContributionSchema = object({
13877
- role: _enum([
13878
- "decode",
13879
- "transcode",
13880
- "recording",
13881
- "streaming",
13882
- "detection"
13883
- ]),
13884
- /**
13885
- * The NUMERIC device id — the same value every log line carries as
13886
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13887
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13888
- * contributor that cannot name its camera must not emit the entry at all,
13889
- * because an unnamed per-camera entry is indistinguishable from a shared one
13890
- * and would quietly turn one camera's cost into everybody's.
13891
- */
13892
- deviceId: number().int().positive().nullable(),
13893
- attribution: _enum([
13894
- "measured",
13895
- "accounted",
13896
- "unattributable"
13897
- ]),
13898
- /**
13899
- * What ONE entry is, in the contributor's own words — `615/high`,
13900
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13901
- * family and inventing a common one would lose the only information that
13902
- * makes two entries for the same camera distinguishable.
13903
- */
13904
- unit: string(),
13905
- /**
13906
- * The OS process this cost lives in, when there is one. Present so a
13907
- * consumer can (a) tell two generations of the same unit apart across a
13908
- * restart, and (b) subtract claimed processes from the node's process
13909
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13910
- * process of its own.
13911
- */
13912
- pid: number().int().positive().optional(),
13913
- /**
13914
- * When this generation started. The pid's incarnation marker: a consumer
13915
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13916
- * window when this changes, because the counter restarted from zero in a new
13917
- * process.
13918
- */
13919
- startedAtMs: number().optional(),
13920
- /**
13921
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13922
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13923
- * contribution is asked for.
13924
- *
13925
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13926
- * needs a sampler, and a new per-node sampler is the defect half of
13927
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13928
- * by whoever already keeps a history; a rate cannot be un-averaged.
13929
- *
13930
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13931
- * an entry with no process.
13932
- */
13933
- cpuSeconds: number().optional(),
13934
- /** Resident bytes of this unit's process, same source and same rules. */
13935
- rssBytes: number().optional()
13936
- });
13937
- method(_void(), array(LoadContributionSchema).readonly());
13938
- /**
13939
13986
  * `login-method` — collection cap through which auth addons contribute
13940
13987
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13941
13988
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18673,12 +18720,53 @@ var MediaFileKindEnum = _enum([
18673
18720
  "keyFrameSmall",
18674
18721
  "thumbnailSmall"
18675
18722
  ]);
18723
+ /**
18724
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18725
+ * ARE — never the bytes themselves.
18726
+ *
18727
+ * ## Why `url` and not `base64`
18728
+ *
18729
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18730
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18731
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18732
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18733
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18734
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18735
+ *
18736
+ * `url` points at the `event-media` data plane
18737
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18738
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18739
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18740
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18741
+ * no less protected than they were inside a `view`-level cap response — see
18742
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18743
+ * (per-device scoping).
18744
+ *
18745
+ * The URL is built from the row's **stored** key, which is not always its
18746
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18747
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18748
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18749
+ *
18750
+ * ## `base64` is TRANSITIONAL and is going away
18751
+ *
18752
+ * It is still populated for one reason: the deployed viewer's track-detail
18753
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18754
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18755
+ * triangle — not as absence. Removing the field before that viewer ships is an
18756
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18757
+ * delete this line and the `withBytes` pass-through in
18758
+ * `analytics-query-facade.ts`; nothing else reads it.
18759
+ */
18676
18760
  var MediaFileSchema = object({
18677
18761
  key: string(),
18678
18762
  kind: MediaFileKindEnum,
18679
- base64: string(),
18680
18763
  sizeBytes: number(),
18681
18764
  timestamp: number()
18765
+ }).extend({
18766
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18767
+ url: string(),
18768
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18769
+ base64: string()
18682
18770
  });
18683
18771
  /**
18684
18772
  * One media row WITHOUT its bytes.
@@ -18690,7 +18778,9 @@ var MediaFileSchema = object({
18690
18778
  * blocks the whole view.
18691
18779
  *
18692
18780
  * `sizeBytes` is carried because it is what lets a client decide between the
18693
- * stored blob and a `?variant=thumb` rendering without fetching either.
18781
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18782
+ * `url` because a client that had to build the plane path itself is a second
18783
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18694
18784
  */
18695
18785
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18696
18786
  /**
@@ -19379,6 +19469,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19379
19469
  }), array(MediaFileSchema).readonly()), method(object({
19380
19470
  trackId: string(),
19381
19471
  deviceId: number()
19472
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19473
+ eventId: string(),
19474
+ deviceId: number()
19382
19475
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19383
19476
  kind: "mutation",
19384
19477
  auth: "admin"
@@ -24481,10 +24574,24 @@ var FaceClusterSchema = object({
24481
24574
  size: number().int(),
24482
24575
  cohesion: number()
24483
24576
  });
24577
+ /**
24578
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24579
+ * are — never the bytes.
24580
+ *
24581
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24582
+ * track/event contract) is still populated because a deployed viewer requires
24583
+ * the field to parse a row at all; this method has no such reader. Its ONE
24584
+ * caller is the admin UI's detail modal, which was building
24585
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24586
+ * dialog already rendering its key FRAME from the `event-media` plane.
24587
+ *
24588
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24589
+ * media key directly, so this needed no new plane and no new access decision.
24590
+ */
24484
24591
  var MediaFileLiteSchema$1 = object({
24485
24592
  key: string(),
24486
24593
  kind: string(),
24487
- base64: string(),
24594
+ url: string(),
24488
24595
  sizeBytes: number(),
24489
24596
  timestamp: number()
24490
24597
  });
@@ -27506,10 +27613,24 @@ var PlateInfoSchema = object({
27506
27613
  */
27507
27614
  cropUrl: string().optional()
27508
27615
  });
27616
+ /**
27617
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27618
+ * are — never the bytes.
27619
+ *
27620
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27621
+ * track/event contract) is still populated because a deployed viewer requires
27622
+ * the field to parse a row at all; this method has no such reader. Its ONE
27623
+ * caller is the admin UI's detail modal, which was building
27624
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27625
+ * dialog already rendering its key FRAME from the `event-media` plane.
27626
+ *
27627
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27628
+ * media key directly, so this needed no new plane and no new access decision.
27629
+ */
27509
27630
  var MediaFileLiteSchema = object({
27510
27631
  key: string(),
27511
27632
  kind: string(),
27512
- base64: string(),
27633
+ url: string(),
27513
27634
  sizeBytes: number(),
27514
27635
  timestamp: number()
27515
27636
  });
@@ -35472,6 +35593,12 @@ Object.freeze({
35472
35593
  addonId: null,
35473
35594
  access: "view"
35474
35595
  },
35596
+ "pipelineAnalytics.listEventMedia": {
35597
+ capName: "pipeline-analytics",
35598
+ capScope: "device",
35599
+ addonId: null,
35600
+ access: "view"
35601
+ },
35475
35602
  "pipelineAnalytics.listGroups": {
35476
35603
  capName: "pipeline-analytics",
35477
35604
  capScope: "device",
@@ -39095,6 +39222,11 @@ Object.freeze({
39095
39222
  form: "array",
39096
39223
  optional: false
39097
39224
  }],
39225
+ "pipelineAnalytics.listEventMedia": [{
39226
+ name: "deviceId",
39227
+ form: "single",
39228
+ optional: false
39229
+ }],
39098
39230
  "pipelineAnalytics.listGroups": [{
39099
39231
  name: "deviceIds",
39100
39232
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rtsp",
3
- "version": "1.2.45",
3
+ "version": "1.2.47",
4
4
  "description": "Generic RTSP camera device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",