@camstack/addon-ai 0.4.41 → 0.4.43

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
@@ -8206,6 +8206,21 @@ var RelocateJobSchema = object({
8206
8206
  bytesMoved: number$1().int(),
8207
8207
  /** Total files discovered up front; null while (or when) unknown. */
8208
8208
  filesTotal: number$1().int().nullable(),
8209
+ /**
8210
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8211
+ * made that nobody asked for, so it is reported where the operator reads the
8212
+ * job rather than only in a log line.
8213
+ *
8214
+ * A footage segment records its byte count in its own NAME, and the durable
8215
+ * hour row derives its aggregates from those names. A file that does not
8216
+ * match its name therefore makes the ledger's sums — and with them quota and
8217
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8218
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8219
+ *
8220
+ * Absent on lanes where the question has no meaning: a media blob's size is
8221
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8222
+ */
8223
+ rowsReconciled: number$1().int().nonnegative().optional(),
8209
8224
  startedAt: number$1(),
8210
8225
  finishedAt: number$1().nullable(),
8211
8226
  error: string().nullable()
@@ -8274,14 +8289,42 @@ var RelocateMediaInputSchema = object({
8274
8289
  /** Omitted = `move`, the pre-existing behaviour. */
8275
8290
  mode: MediaRelocateModeSchema.optional()
8276
8291
  });
8277
- /** How many rows still carry NO `locationId` — the population a repoint would
8278
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8279
- * value that permits a non-blocking `eventMedia` cutover. */
8280
- var UnstampedEventMediaCountSchema = object({
8281
- media: number$1().int().nonnegative(),
8282
- retrainFrames: number$1().int().nonnegative(),
8283
- total: number$1().int().nonnegative()
8292
+ /**
8293
+ * The unstamped population of ONE collection — split, because the gate and the
8294
+ * operator ask two different questions and only one of them has to be cheap.
8295
+ *
8296
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8297
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8298
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8299
+ * that matters — after a seal, when the population is empty.
8300
+ *
8301
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8302
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8303
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8304
+ * and useful answer: "there are some, and this read could not say how many"
8305
+ * still refuses the cutover, which is the whole job.
8306
+ */
8307
+ var UnstampedRowsSchema = object({
8308
+ present: boolean(),
8309
+ rows: number$1().int().nonnegative().nullable()
8284
8310
  });
8311
+ /**
8312
+ * How many rows still carry NO `locationId` — the population a repoint would
8313
+ * silently re-aim at a disk that does not hold their bytes.
8314
+ *
8315
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8316
+ * over. The gate opens on a measured absence and on nothing else; an unread
8317
+ * collection and an empty one are different facts, and this repo has already
8318
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8319
+ */
8320
+ var UnstampedEventMediaCountSchema = object({
8321
+ media: UnstampedRowsSchema,
8322
+ retrainFrames: UnstampedRowsSchema,
8323
+ /** True when EITHER collection holds one. The refusal reads this. */
8324
+ anyPresent: boolean(),
8325
+ /** Sum across both, or `null` when either lane could not be counted. */
8326
+ total: number$1().int().nonnegative().nullable()
8327
+ }).nullable();
8285
8328
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8286
8329
  /** The independently selectable logical storage classes — every class
8287
8330
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8392,6 +8435,10 @@ var StorageMigrationMoveProgressSchema = object({
8392
8435
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8393
8436
  filesTotal: number$1().int().nonnegative().nullable(),
8394
8437
  bytesMoved: number$1().int().nonnegative(),
8438
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8439
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8440
+ * rows would be the same failure as one that silently skipped them. */
8441
+ rowsReconciled: number$1().int().nonnegative().optional(),
8395
8442
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8396
8443
  * crash gets a new mover, and a rate computed from the migration's start
8397
8444
  * would silently average in the time nothing was running. */
@@ -13076,6 +13123,114 @@ method(object({
13076
13123
  height: number$1()
13077
13124
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13078
13125
  /**
13126
+ * `failure-contribution` — the capability an addon reports its OWN losses
13127
+ * through, per camera, with the denominator attached. It stores nothing.
13128
+ *
13129
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13130
+ *
13131
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13132
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13133
+ * copied: the contributor reports what it already knows, hub-main adds only
13134
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13135
+ * somebody to forget to edit.
13136
+ *
13137
+ * They are not merged, because their invariants are opposites:
13138
+ *
13139
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13140
+ * claim a camera cost nothing, which is a measurement nobody made;
13141
+ * - a `failure-contribution` zero is the **most valuable value on the
13142
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13143
+ * and it is exactly what an absent entry cannot say.
13144
+ *
13145
+ * Putting a loss counter on a cost entry would also break the reconciliation
13146
+ * that gives `load-contribution` its point: contributions are subtracted from
13147
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13148
+ * has no process.
13149
+ *
13150
+ * ## Why not a log line, since the counters already exist
13151
+ *
13152
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13153
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13154
+ * ends in a log line, and a log line is the thing the operator asked to stop
13155
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13156
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13157
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13158
+ * media blackout were both diagnosed. The counters stay; this is where they can
13159
+ * be READ.
13160
+ *
13161
+ * ## The rate is served with its denominator or not at all
13162
+ *
13163
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13164
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13165
+ * than yesterday" and was **flat across twelve hours** once divided by the
13166
+ * successes on the same path. A surface that publishes only the numerator
13167
+ * reproduces that mistake on every read.
13168
+ *
13169
+ * ## Shape
13170
+ *
13171
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13172
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13173
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13174
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13175
+ * a forked runner's entries reach hub-main over transport that already exists.
13176
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13177
+ * result through `system.getFailureContributions`.
13178
+ */
13179
+ var FailureReasonCountSchema = object({
13180
+ /**
13181
+ * Why the attempt did not land, in the contributor's own vocabulary —
13182
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13183
+ * strings that already appear in this repo's logs and, where one exists, the
13184
+ * same string the per-track `previewMissReason` records (D276): a second
13185
+ * vocabulary for the same loss would make the row and the counter
13186
+ * un-joinable.
13187
+ */
13188
+ reason: string(),
13189
+ count: number$1().int().nonnegative()
13190
+ });
13191
+ var FailureContributionSchema = object({
13192
+ /**
13193
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13194
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13195
+ * `unit` free: the families are owned by different addons and a shared enum
13196
+ * is a central list that rots invisibly.
13197
+ */
13198
+ family: string(),
13199
+ /**
13200
+ * The NUMERIC device id — the same value every log line carries as
13201
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13202
+ * cannot name the camera must not emit the entry, because a fleet total
13203
+ * cannot answer the only question anybody asks of this surface.
13204
+ */
13205
+ deviceId: number$1().int().positive(),
13206
+ /**
13207
+ * A second dimension inside the family: the model / step id for an inference
13208
+ * timeout, so "which camera AND which model" is one read. Absent when the
13209
+ * family has a single variant.
13210
+ */
13211
+ variant: string().optional(),
13212
+ /**
13213
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13214
+ * differencing two reads must drop the interval when it changes, because the
13215
+ * counter restarted from zero in a respawned runner. Same discipline as
13216
+ * `LoadContribution.startedAtMs`.
13217
+ */
13218
+ sinceMs: number$1(),
13219
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13220
+ atMs: number$1(),
13221
+ /**
13222
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13223
+ * window. A failure count published without it is the mistake this schema
13224
+ * exists to make impossible.
13225
+ */
13226
+ attempts: number$1().int().nonnegative(),
13227
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13228
+ succeeded: number$1().int().nonnegative(),
13229
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13230
+ reasons: array(FailureReasonCountSchema).readonly()
13231
+ });
13232
+ method(_void(), array(FailureContributionSchema).readonly());
13233
+ /**
13079
13234
  * filesystem-browse — per-node capability for browsing the node's local
13080
13235
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13081
13236
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13691,6 +13846,68 @@ var llmCapability = {
13691
13846
  })
13692
13847
  }
13693
13848
  };
13849
+ var LoadContributionSchema = object({
13850
+ role: _enum([
13851
+ "decode",
13852
+ "transcode",
13853
+ "recording",
13854
+ "streaming",
13855
+ "detection"
13856
+ ]),
13857
+ /**
13858
+ * The NUMERIC device id — the same value every log line carries as
13859
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13860
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13861
+ * contributor that cannot name its camera must not emit the entry at all,
13862
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13863
+ * and would quietly turn one camera's cost into everybody's.
13864
+ */
13865
+ deviceId: number$1().int().positive().nullable(),
13866
+ attribution: _enum([
13867
+ "measured",
13868
+ "accounted",
13869
+ "unattributable"
13870
+ ]),
13871
+ /**
13872
+ * What ONE entry is, in the contributor's own words — `615/high`,
13873
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13874
+ * family and inventing a common one would lose the only information that
13875
+ * makes two entries for the same camera distinguishable.
13876
+ */
13877
+ unit: string(),
13878
+ /**
13879
+ * The OS process this cost lives in, when there is one. Present so a
13880
+ * consumer can (a) tell two generations of the same unit apart across a
13881
+ * restart, and (b) subtract claimed processes from the node's process
13882
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13883
+ * process of its own.
13884
+ */
13885
+ pid: number$1().int().positive().optional(),
13886
+ /**
13887
+ * When this generation started. The pid's incarnation marker: a consumer
13888
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13889
+ * window when this changes, because the counter restarted from zero in a new
13890
+ * process.
13891
+ */
13892
+ startedAtMs: number$1().optional(),
13893
+ /**
13894
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13895
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13896
+ * contribution is asked for.
13897
+ *
13898
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13899
+ * needs a sampler, and a new per-node sampler is the defect half of
13900
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13901
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13902
+ *
13903
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13904
+ * an entry with no process.
13905
+ */
13906
+ cpuSeconds: number$1().optional(),
13907
+ /** Resident bytes of this unit's process, same source and same rules. */
13908
+ rssBytes: number$1().optional()
13909
+ });
13910
+ method(_void(), array(LoadContributionSchema).readonly());
13694
13911
  /**
13695
13912
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13696
13913
  * through. It stores nothing.
@@ -13767,176 +13984,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13767
13984
  tags: record(string(), string()).optional()
13768
13985
  }), array(LogEntrySchema).readonly());
13769
13986
  /**
13770
- * `failure-contribution` — the capability an addon reports its OWN losses
13771
- * through, per camera, with the denominator attached. It stores nothing.
13772
- *
13773
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13774
- *
13775
- * `load-contribution` answers *what did this camera COST*. This answers *what
13776
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13777
- * copied: the contributor reports what it already knows, hub-main adds only
13778
- * `addonId`, nothing needs global knowledge, and there is no central list for
13779
- * somebody to forget to edit.
13780
- *
13781
- * They are not merged, because their invariants are opposites:
13782
- *
13783
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13784
- * claim a camera cost nothing, which is a measurement nobody made;
13785
- * - a `failure-contribution` zero is the **most valuable value on the
13786
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13787
- * and it is exactly what an absent entry cannot say.
13788
- *
13789
- * Putting a loss counter on a cost entry would also break the reconciliation
13790
- * that gives `load-contribution` its point: contributions are subtracted from
13791
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13792
- * has no process.
13793
- *
13794
- * ## Why not a log line, since the counters already exist
13795
- *
13796
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13797
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13798
- * ends in a log line, and a log line is the thing the operator asked to stop
13799
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13800
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13801
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13802
- * media blackout were both diagnosed. The counters stay; this is where they can
13803
- * be READ.
13804
- *
13805
- * ## The rate is served with its denominator or not at all
13806
- *
13807
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13808
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13809
- * than yesterday" and was **flat across twelve hours** once divided by the
13810
- * successes on the same path. A surface that publishes only the numerator
13811
- * reproduces that mistake on every read.
13812
- *
13813
- * ## Shape
13814
- *
13815
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13816
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13817
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13818
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13819
- * a forked runner's entries reach hub-main over transport that already exists.
13820
- * No new UDS message, no second registry (D3). The operator reads the assembled
13821
- * result through `system.getFailureContributions`.
13822
- */
13823
- var FailureReasonCountSchema = object({
13824
- /**
13825
- * Why the attempt did not land, in the contributor's own vocabulary —
13826
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13827
- * strings that already appear in this repo's logs and, where one exists, the
13828
- * same string the per-track `previewMissReason` records (D276): a second
13829
- * vocabulary for the same loss would make the row and the counter
13830
- * un-joinable.
13831
- */
13832
- reason: string(),
13833
- count: number$1().int().nonnegative()
13834
- });
13835
- var FailureContributionSchema = object({
13836
- /**
13837
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13838
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13839
- * `unit` free: the families are owned by different addons and a shared enum
13840
- * is a central list that rots invisibly.
13841
- */
13842
- family: string(),
13843
- /**
13844
- * The NUMERIC device id — the same value every log line carries as
13845
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13846
- * cannot name the camera must not emit the entry, because a fleet total
13847
- * cannot answer the only question anybody asks of this surface.
13848
- */
13849
- deviceId: number$1().int().positive(),
13850
- /**
13851
- * A second dimension inside the family: the model / step id for an inference
13852
- * timeout, so "which camera AND which model" is one read. Absent when the
13853
- * family has a single variant.
13854
- */
13855
- variant: string().optional(),
13856
- /**
13857
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13858
- * differencing two reads must drop the interval when it changes, because the
13859
- * counter restarted from zero in a respawned runner. Same discipline as
13860
- * `LoadContribution.startedAtMs`.
13861
- */
13862
- sinceMs: number$1(),
13863
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13864
- atMs: number$1(),
13865
- /**
13866
- * THE DENOMINATOR — every attempt on this path for this camera in the
13867
- * window. A failure count published without it is the mistake this schema
13868
- * exists to make impossible.
13869
- */
13870
- attempts: number$1().int().nonnegative(),
13871
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13872
- succeeded: number$1().int().nonnegative(),
13873
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13874
- reasons: array(FailureReasonCountSchema).readonly()
13875
- });
13876
- method(_void(), array(FailureContributionSchema).readonly());
13877
- var LoadContributionSchema = object({
13878
- role: _enum([
13879
- "decode",
13880
- "transcode",
13881
- "recording",
13882
- "streaming",
13883
- "detection"
13884
- ]),
13885
- /**
13886
- * The NUMERIC device id — the same value every log line carries as
13887
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13888
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13889
- * contributor that cannot name its camera must not emit the entry at all,
13890
- * because an unnamed per-camera entry is indistinguishable from a shared one
13891
- * and would quietly turn one camera's cost into everybody's.
13892
- */
13893
- deviceId: number$1().int().positive().nullable(),
13894
- attribution: _enum([
13895
- "measured",
13896
- "accounted",
13897
- "unattributable"
13898
- ]),
13899
- /**
13900
- * What ONE entry is, in the contributor's own words — `615/high`,
13901
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13902
- * family and inventing a common one would lose the only information that
13903
- * makes two entries for the same camera distinguishable.
13904
- */
13905
- unit: string(),
13906
- /**
13907
- * The OS process this cost lives in, when there is one. Present so a
13908
- * consumer can (a) tell two generations of the same unit apart across a
13909
- * restart, and (b) subtract claimed processes from the node's process
13910
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13911
- * process of its own.
13912
- */
13913
- pid: number$1().int().positive().optional(),
13914
- /**
13915
- * When this generation started. The pid's incarnation marker: a consumer
13916
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13917
- * window when this changes, because the counter restarted from zero in a new
13918
- * process.
13919
- */
13920
- startedAtMs: number$1().optional(),
13921
- /**
13922
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13923
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13924
- * contribution is asked for.
13925
- *
13926
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13927
- * needs a sampler, and a new per-node sampler is the defect half of
13928
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13929
- * by whoever already keeps a history; a rate cannot be un-averaged.
13930
- *
13931
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13932
- * an entry with no process.
13933
- */
13934
- cpuSeconds: number$1().optional(),
13935
- /** Resident bytes of this unit's process, same source and same rules. */
13936
- rssBytes: number$1().optional()
13937
- });
13938
- method(_void(), array(LoadContributionSchema).readonly());
13939
- /**
13940
13987
  * `login-method` — collection cap through which auth addons contribute
13941
13988
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13942
13989
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18596,12 +18643,53 @@ var MediaFileKindEnum = _enum([
18596
18643
  "keyFrameSmall",
18597
18644
  "thumbnailSmall"
18598
18645
  ]);
18646
+ /**
18647
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18648
+ * ARE — never the bytes themselves.
18649
+ *
18650
+ * ## Why `url` and not `base64`
18651
+ *
18652
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18653
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18654
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18655
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18656
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18657
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18658
+ *
18659
+ * `url` points at the `event-media` data plane
18660
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18661
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18662
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18663
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18664
+ * no less protected than they were inside a `view`-level cap response — see
18665
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18666
+ * (per-device scoping).
18667
+ *
18668
+ * The URL is built from the row's **stored** key, which is not always its
18669
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18670
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18671
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18672
+ *
18673
+ * ## `base64` is TRANSITIONAL and is going away
18674
+ *
18675
+ * It is still populated for one reason: the deployed viewer's track-detail
18676
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18677
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18678
+ * triangle — not as absence. Removing the field before that viewer ships is an
18679
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18680
+ * delete this line and the `withBytes` pass-through in
18681
+ * `analytics-query-facade.ts`; nothing else reads it.
18682
+ */
18599
18683
  var MediaFileSchema = object({
18600
18684
  key: string(),
18601
18685
  kind: MediaFileKindEnum,
18602
- base64: string(),
18603
18686
  sizeBytes: number$1(),
18604
18687
  timestamp: number$1()
18688
+ }).extend({
18689
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18690
+ url: string(),
18691
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18692
+ base64: string()
18605
18693
  });
18606
18694
  /**
18607
18695
  * One media row WITHOUT its bytes.
@@ -18613,7 +18701,9 @@ var MediaFileSchema = object({
18613
18701
  * blocks the whole view.
18614
18702
  *
18615
18703
  * `sizeBytes` is carried because it is what lets a client decide between the
18616
- * stored blob and a `?variant=thumb` rendering without fetching either.
18704
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18705
+ * `url` because a client that had to build the plane path itself is a second
18706
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18617
18707
  */
18618
18708
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18619
18709
  /**
@@ -19302,6 +19392,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19302
19392
  }), array(MediaFileSchema).readonly()), method(object({
19303
19393
  trackId: string(),
19304
19394
  deviceId: number$1()
19395
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19396
+ eventId: string(),
19397
+ deviceId: number$1()
19305
19398
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19306
19399
  kind: "mutation",
19307
19400
  auth: "admin"
@@ -23562,10 +23655,24 @@ var FaceClusterSchema = object({
23562
23655
  size: number$1().int(),
23563
23656
  cohesion: number$1()
23564
23657
  });
23658
+ /**
23659
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23660
+ * are — never the bytes.
23661
+ *
23662
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23663
+ * track/event contract) is still populated because a deployed viewer requires
23664
+ * the field to parse a row at all; this method has no such reader. Its ONE
23665
+ * caller is the admin UI's detail modal, which was building
23666
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23667
+ * dialog already rendering its key FRAME from the `event-media` plane.
23668
+ *
23669
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23670
+ * media key directly, so this needed no new plane and no new access decision.
23671
+ */
23565
23672
  var MediaFileLiteSchema$1 = object({
23566
23673
  key: string(),
23567
23674
  kind: string(),
23568
- base64: string(),
23675
+ url: string(),
23569
23676
  sizeBytes: number$1(),
23570
23677
  timestamp: number$1()
23571
23678
  });
@@ -25817,10 +25924,24 @@ var PlateInfoSchema = object({
25817
25924
  */
25818
25925
  cropUrl: string().optional()
25819
25926
  });
25927
+ /**
25928
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25929
+ * are — never the bytes.
25930
+ *
25931
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25932
+ * track/event contract) is still populated because a deployed viewer requires
25933
+ * the field to parse a row at all; this method has no such reader. Its ONE
25934
+ * caller is the admin UI's detail modal, which was building
25935
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25936
+ * dialog already rendering its key FRAME from the `event-media` plane.
25937
+ *
25938
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25939
+ * media key directly, so this needed no new plane and no new access decision.
25940
+ */
25820
25941
  var MediaFileLiteSchema = object({
25821
25942
  key: string(),
25822
25943
  kind: string(),
25823
- base64: string(),
25944
+ url: string(),
25824
25945
  sizeBytes: number$1(),
25825
25946
  timestamp: number$1()
25826
25947
  });
@@ -31833,6 +31954,12 @@ Object.freeze({
31833
31954
  addonId: null,
31834
31955
  access: "view"
31835
31956
  },
31957
+ "pipelineAnalytics.listEventMedia": {
31958
+ capName: "pipeline-analytics",
31959
+ capScope: "device",
31960
+ addonId: null,
31961
+ access: "view"
31962
+ },
31836
31963
  "pipelineAnalytics.listGroups": {
31837
31964
  capName: "pipeline-analytics",
31838
31965
  capScope: "device",
@@ -35456,6 +35583,11 @@ Object.freeze({
35456
35583
  form: "array",
35457
35584
  optional: false
35458
35585
  }],
35586
+ "pipelineAnalytics.listEventMedia": [{
35587
+ name: "deviceId",
35588
+ form: "single",
35589
+ optional: false
35590
+ }],
35459
35591
  "pipelineAnalytics.listGroups": [{
35460
35592
  name: "deviceIds",
35461
35593
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8233,6 +8233,21 @@ var RelocateJobSchema = object({
8233
8233
  bytesMoved: number$1().int(),
8234
8234
  /** Total files discovered up front; null while (or when) unknown. */
8235
8235
  filesTotal: number$1().int().nullable(),
8236
+ /**
8237
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8238
+ * made that nobody asked for, so it is reported where the operator reads the
8239
+ * job rather than only in a log line.
8240
+ *
8241
+ * A footage segment records its byte count in its own NAME, and the durable
8242
+ * hour row derives its aggregates from those names. A file that does not
8243
+ * match its name therefore makes the ledger's sums — and with them quota and
8244
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8245
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8246
+ *
8247
+ * Absent on lanes where the question has no meaning: a media blob's size is
8248
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8249
+ */
8250
+ rowsReconciled: number$1().int().nonnegative().optional(),
8236
8251
  startedAt: number$1(),
8237
8252
  finishedAt: number$1().nullable(),
8238
8253
  error: string().nullable()
@@ -8301,14 +8316,42 @@ var RelocateMediaInputSchema = object({
8301
8316
  /** Omitted = `move`, the pre-existing behaviour. */
8302
8317
  mode: MediaRelocateModeSchema.optional()
8303
8318
  });
8304
- /** How many rows still carry NO `locationId` — the population a repoint would
8305
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8306
- * value that permits a non-blocking `eventMedia` cutover. */
8307
- var UnstampedEventMediaCountSchema = object({
8308
- media: number$1().int().nonnegative(),
8309
- retrainFrames: number$1().int().nonnegative(),
8310
- total: number$1().int().nonnegative()
8319
+ /**
8320
+ * The unstamped population of ONE collection — split, because the gate and the
8321
+ * operator ask two different questions and only one of them has to be cheap.
8322
+ *
8323
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8324
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8325
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8326
+ * that matters — after a seal, when the population is empty.
8327
+ *
8328
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8329
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8330
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8331
+ * and useful answer: "there are some, and this read could not say how many"
8332
+ * still refuses the cutover, which is the whole job.
8333
+ */
8334
+ var UnstampedRowsSchema = object({
8335
+ present: boolean(),
8336
+ rows: number$1().int().nonnegative().nullable()
8311
8337
  });
8338
+ /**
8339
+ * How many rows still carry NO `locationId` — the population a repoint would
8340
+ * silently re-aim at a disk that does not hold their bytes.
8341
+ *
8342
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8343
+ * over. The gate opens on a measured absence and on nothing else; an unread
8344
+ * collection and an empty one are different facts, and this repo has already
8345
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8346
+ */
8347
+ var UnstampedEventMediaCountSchema = object({
8348
+ media: UnstampedRowsSchema,
8349
+ retrainFrames: UnstampedRowsSchema,
8350
+ /** True when EITHER collection holds one. The refusal reads this. */
8351
+ anyPresent: boolean(),
8352
+ /** Sum across both, or `null` when either lane could not be counted. */
8353
+ total: number$1().int().nonnegative().nullable()
8354
+ }).nullable();
8312
8355
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8313
8356
  /** The independently selectable logical storage classes — every class
8314
8357
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8419,6 +8462,10 @@ var StorageMigrationMoveProgressSchema = object({
8419
8462
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8420
8463
  filesTotal: number$1().int().nonnegative().nullable(),
8421
8464
  bytesMoved: number$1().int().nonnegative(),
8465
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8466
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8467
+ * rows would be the same failure as one that silently skipped them. */
8468
+ rowsReconciled: number$1().int().nonnegative().optional(),
8422
8469
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8423
8470
  * crash gets a new mover, and a rate computed from the migration's start
8424
8471
  * would silently average in the time nothing was running. */
@@ -13103,6 +13150,114 @@ method(object({
13103
13150
  height: number$1()
13104
13151
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13105
13152
  /**
13153
+ * `failure-contribution` — the capability an addon reports its OWN losses
13154
+ * through, per camera, with the denominator attached. It stores nothing.
13155
+ *
13156
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13157
+ *
13158
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13159
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13160
+ * copied: the contributor reports what it already knows, hub-main adds only
13161
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13162
+ * somebody to forget to edit.
13163
+ *
13164
+ * They are not merged, because their invariants are opposites:
13165
+ *
13166
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13167
+ * claim a camera cost nothing, which is a measurement nobody made;
13168
+ * - a `failure-contribution` zero is the **most valuable value on the
13169
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13170
+ * and it is exactly what an absent entry cannot say.
13171
+ *
13172
+ * Putting a loss counter on a cost entry would also break the reconciliation
13173
+ * that gives `load-contribution` its point: contributions are subtracted from
13174
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13175
+ * has no process.
13176
+ *
13177
+ * ## Why not a log line, since the counters already exist
13178
+ *
13179
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13180
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13181
+ * ends in a log line, and a log line is the thing the operator asked to stop
13182
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13183
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13184
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13185
+ * media blackout were both diagnosed. The counters stay; this is where they can
13186
+ * be READ.
13187
+ *
13188
+ * ## The rate is served with its denominator or not at all
13189
+ *
13190
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13191
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13192
+ * than yesterday" and was **flat across twelve hours** once divided by the
13193
+ * successes on the same path. A surface that publishes only the numerator
13194
+ * reproduces that mistake on every read.
13195
+ *
13196
+ * ## Shape
13197
+ *
13198
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13199
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13200
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13201
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13202
+ * a forked runner's entries reach hub-main over transport that already exists.
13203
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13204
+ * result through `system.getFailureContributions`.
13205
+ */
13206
+ var FailureReasonCountSchema = object({
13207
+ /**
13208
+ * Why the attempt did not land, in the contributor's own vocabulary —
13209
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13210
+ * strings that already appear in this repo's logs and, where one exists, the
13211
+ * same string the per-track `previewMissReason` records (D276): a second
13212
+ * vocabulary for the same loss would make the row and the counter
13213
+ * un-joinable.
13214
+ */
13215
+ reason: string(),
13216
+ count: number$1().int().nonnegative()
13217
+ });
13218
+ var FailureContributionSchema = object({
13219
+ /**
13220
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13221
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13222
+ * `unit` free: the families are owned by different addons and a shared enum
13223
+ * is a central list that rots invisibly.
13224
+ */
13225
+ family: string(),
13226
+ /**
13227
+ * The NUMERIC device id — the same value every log line carries as
13228
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13229
+ * cannot name the camera must not emit the entry, because a fleet total
13230
+ * cannot answer the only question anybody asks of this surface.
13231
+ */
13232
+ deviceId: number$1().int().positive(),
13233
+ /**
13234
+ * A second dimension inside the family: the model / step id for an inference
13235
+ * timeout, so "which camera AND which model" is one read. Absent when the
13236
+ * family has a single variant.
13237
+ */
13238
+ variant: string().optional(),
13239
+ /**
13240
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13241
+ * differencing two reads must drop the interval when it changes, because the
13242
+ * counter restarted from zero in a respawned runner. Same discipline as
13243
+ * `LoadContribution.startedAtMs`.
13244
+ */
13245
+ sinceMs: number$1(),
13246
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13247
+ atMs: number$1(),
13248
+ /**
13249
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13250
+ * window. A failure count published without it is the mistake this schema
13251
+ * exists to make impossible.
13252
+ */
13253
+ attempts: number$1().int().nonnegative(),
13254
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13255
+ succeeded: number$1().int().nonnegative(),
13256
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13257
+ reasons: array(FailureReasonCountSchema).readonly()
13258
+ });
13259
+ method(_void(), array(FailureContributionSchema).readonly());
13260
+ /**
13106
13261
  * filesystem-browse — per-node capability for browsing the node's local
13107
13262
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13108
13263
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13718,6 +13873,68 @@ var llmCapability = {
13718
13873
  })
13719
13874
  }
13720
13875
  };
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$1().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$1().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$1().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$1().optional(),
13934
+ /** Resident bytes of this unit's process, same source and same rules. */
13935
+ rssBytes: number$1().optional()
13936
+ });
13937
+ method(_void(), array(LoadContributionSchema).readonly());
13721
13938
  /**
13722
13939
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13723
13940
  * through. It stores nothing.
@@ -13794,176 +14011,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13794
14011
  tags: record(string(), string()).optional()
13795
14012
  }), array(LogEntrySchema).readonly());
13796
14013
  /**
13797
- * `failure-contribution` — the capability an addon reports its OWN losses
13798
- * through, per camera, with the denominator attached. It stores nothing.
13799
- *
13800
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13801
- *
13802
- * `load-contribution` answers *what did this camera COST*. This answers *what
13803
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13804
- * copied: the contributor reports what it already knows, hub-main adds only
13805
- * `addonId`, nothing needs global knowledge, and there is no central list for
13806
- * somebody to forget to edit.
13807
- *
13808
- * They are not merged, because their invariants are opposites:
13809
- *
13810
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13811
- * claim a camera cost nothing, which is a measurement nobody made;
13812
- * - a `failure-contribution` zero is the **most valuable value on the
13813
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13814
- * and it is exactly what an absent entry cannot say.
13815
- *
13816
- * Putting a loss counter on a cost entry would also break the reconciliation
13817
- * that gives `load-contribution` its point: contributions are subtracted from
13818
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13819
- * has no process.
13820
- *
13821
- * ## Why not a log line, since the counters already exist
13822
- *
13823
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13824
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13825
- * ends in a log line, and a log line is the thing the operator asked to stop
13826
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13827
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13828
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13829
- * media blackout were both diagnosed. The counters stay; this is where they can
13830
- * be READ.
13831
- *
13832
- * ## The rate is served with its denominator or not at all
13833
- *
13834
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13835
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13836
- * than yesterday" and was **flat across twelve hours** once divided by the
13837
- * successes on the same path. A surface that publishes only the numerator
13838
- * reproduces that mistake on every read.
13839
- *
13840
- * ## Shape
13841
- *
13842
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13843
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13844
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13845
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13846
- * a forked runner's entries reach hub-main over transport that already exists.
13847
- * No new UDS message, no second registry (D3). The operator reads the assembled
13848
- * result through `system.getFailureContributions`.
13849
- */
13850
- var FailureReasonCountSchema = object({
13851
- /**
13852
- * Why the attempt did not land, in the contributor's own vocabulary —
13853
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13854
- * strings that already appear in this repo's logs and, where one exists, the
13855
- * same string the per-track `previewMissReason` records (D276): a second
13856
- * vocabulary for the same loss would make the row and the counter
13857
- * un-joinable.
13858
- */
13859
- reason: string(),
13860
- count: number$1().int().nonnegative()
13861
- });
13862
- var FailureContributionSchema = object({
13863
- /**
13864
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13865
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13866
- * `unit` free: the families are owned by different addons and a shared enum
13867
- * is a central list that rots invisibly.
13868
- */
13869
- family: string(),
13870
- /**
13871
- * The NUMERIC device id — the same value every log line carries as
13872
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13873
- * cannot name the camera must not emit the entry, because a fleet total
13874
- * cannot answer the only question anybody asks of this surface.
13875
- */
13876
- deviceId: number$1().int().positive(),
13877
- /**
13878
- * A second dimension inside the family: the model / step id for an inference
13879
- * timeout, so "which camera AND which model" is one read. Absent when the
13880
- * family has a single variant.
13881
- */
13882
- variant: string().optional(),
13883
- /**
13884
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13885
- * differencing two reads must drop the interval when it changes, because the
13886
- * counter restarted from zero in a respawned runner. Same discipline as
13887
- * `LoadContribution.startedAtMs`.
13888
- */
13889
- sinceMs: number$1(),
13890
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13891
- atMs: number$1(),
13892
- /**
13893
- * THE DENOMINATOR — every attempt on this path for this camera in the
13894
- * window. A failure count published without it is the mistake this schema
13895
- * exists to make impossible.
13896
- */
13897
- attempts: number$1().int().nonnegative(),
13898
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13899
- succeeded: number$1().int().nonnegative(),
13900
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13901
- reasons: array(FailureReasonCountSchema).readonly()
13902
- });
13903
- method(_void(), array(FailureContributionSchema).readonly());
13904
- var LoadContributionSchema = object({
13905
- role: _enum([
13906
- "decode",
13907
- "transcode",
13908
- "recording",
13909
- "streaming",
13910
- "detection"
13911
- ]),
13912
- /**
13913
- * The NUMERIC device id — the same value every log line carries as
13914
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13915
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13916
- * contributor that cannot name its camera must not emit the entry at all,
13917
- * because an unnamed per-camera entry is indistinguishable from a shared one
13918
- * and would quietly turn one camera's cost into everybody's.
13919
- */
13920
- deviceId: number$1().int().positive().nullable(),
13921
- attribution: _enum([
13922
- "measured",
13923
- "accounted",
13924
- "unattributable"
13925
- ]),
13926
- /**
13927
- * What ONE entry is, in the contributor's own words — `615/high`,
13928
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13929
- * family and inventing a common one would lose the only information that
13930
- * makes two entries for the same camera distinguishable.
13931
- */
13932
- unit: string(),
13933
- /**
13934
- * The OS process this cost lives in, when there is one. Present so a
13935
- * consumer can (a) tell two generations of the same unit apart across a
13936
- * restart, and (b) subtract claimed processes from the node's process
13937
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13938
- * process of its own.
13939
- */
13940
- pid: number$1().int().positive().optional(),
13941
- /**
13942
- * When this generation started. The pid's incarnation marker: a consumer
13943
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13944
- * window when this changes, because the counter restarted from zero in a new
13945
- * process.
13946
- */
13947
- startedAtMs: number$1().optional(),
13948
- /**
13949
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13950
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13951
- * contribution is asked for.
13952
- *
13953
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13954
- * needs a sampler, and a new per-node sampler is the defect half of
13955
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13956
- * by whoever already keeps a history; a rate cannot be un-averaged.
13957
- *
13958
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13959
- * an entry with no process.
13960
- */
13961
- cpuSeconds: number$1().optional(),
13962
- /** Resident bytes of this unit's process, same source and same rules. */
13963
- rssBytes: number$1().optional()
13964
- });
13965
- method(_void(), array(LoadContributionSchema).readonly());
13966
- /**
13967
14014
  * `login-method` — collection cap through which auth addons contribute
13968
14015
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13969
14016
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18623,12 +18670,53 @@ var MediaFileKindEnum = _enum([
18623
18670
  "keyFrameSmall",
18624
18671
  "thumbnailSmall"
18625
18672
  ]);
18673
+ /**
18674
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18675
+ * ARE — never the bytes themselves.
18676
+ *
18677
+ * ## Why `url` and not `base64`
18678
+ *
18679
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18680
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18681
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18682
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18683
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18684
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18685
+ *
18686
+ * `url` points at the `event-media` data plane
18687
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18688
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18689
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18690
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18691
+ * no less protected than they were inside a `view`-level cap response — see
18692
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18693
+ * (per-device scoping).
18694
+ *
18695
+ * The URL is built from the row's **stored** key, which is not always its
18696
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18697
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18698
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18699
+ *
18700
+ * ## `base64` is TRANSITIONAL and is going away
18701
+ *
18702
+ * It is still populated for one reason: the deployed viewer's track-detail
18703
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18704
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18705
+ * triangle — not as absence. Removing the field before that viewer ships is an
18706
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18707
+ * delete this line and the `withBytes` pass-through in
18708
+ * `analytics-query-facade.ts`; nothing else reads it.
18709
+ */
18626
18710
  var MediaFileSchema = object({
18627
18711
  key: string(),
18628
18712
  kind: MediaFileKindEnum,
18629
- base64: string(),
18630
18713
  sizeBytes: number$1(),
18631
18714
  timestamp: number$1()
18715
+ }).extend({
18716
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18717
+ url: string(),
18718
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18719
+ base64: string()
18632
18720
  });
18633
18721
  /**
18634
18722
  * One media row WITHOUT its bytes.
@@ -18640,7 +18728,9 @@ var MediaFileSchema = object({
18640
18728
  * blocks the whole view.
18641
18729
  *
18642
18730
  * `sizeBytes` is carried because it is what lets a client decide between the
18643
- * stored blob and a `?variant=thumb` rendering without fetching either.
18731
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18732
+ * `url` because a client that had to build the plane path itself is a second
18733
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18644
18734
  */
18645
18735
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18646
18736
  /**
@@ -19329,6 +19419,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19329
19419
  }), array(MediaFileSchema).readonly()), method(object({
19330
19420
  trackId: string(),
19331
19421
  deviceId: number$1()
19422
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19423
+ eventId: string(),
19424
+ deviceId: number$1()
19332
19425
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19333
19426
  kind: "mutation",
19334
19427
  auth: "admin"
@@ -23589,10 +23682,24 @@ var FaceClusterSchema = object({
23589
23682
  size: number$1().int(),
23590
23683
  cohesion: number$1()
23591
23684
  });
23685
+ /**
23686
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23687
+ * are — never the bytes.
23688
+ *
23689
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23690
+ * track/event contract) is still populated because a deployed viewer requires
23691
+ * the field to parse a row at all; this method has no such reader. Its ONE
23692
+ * caller is the admin UI's detail modal, which was building
23693
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23694
+ * dialog already rendering its key FRAME from the `event-media` plane.
23695
+ *
23696
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23697
+ * media key directly, so this needed no new plane and no new access decision.
23698
+ */
23592
23699
  var MediaFileLiteSchema$1 = object({
23593
23700
  key: string(),
23594
23701
  kind: string(),
23595
- base64: string(),
23702
+ url: string(),
23596
23703
  sizeBytes: number$1(),
23597
23704
  timestamp: number$1()
23598
23705
  });
@@ -25844,10 +25951,24 @@ var PlateInfoSchema = object({
25844
25951
  */
25845
25952
  cropUrl: string().optional()
25846
25953
  });
25954
+ /**
25955
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25956
+ * are — never the bytes.
25957
+ *
25958
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25959
+ * track/event contract) is still populated because a deployed viewer requires
25960
+ * the field to parse a row at all; this method has no such reader. Its ONE
25961
+ * caller is the admin UI's detail modal, which was building
25962
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25963
+ * dialog already rendering its key FRAME from the `event-media` plane.
25964
+ *
25965
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25966
+ * media key directly, so this needed no new plane and no new access decision.
25967
+ */
25847
25968
  var MediaFileLiteSchema = object({
25848
25969
  key: string(),
25849
25970
  kind: string(),
25850
- base64: string(),
25971
+ url: string(),
25851
25972
  sizeBytes: number$1(),
25852
25973
  timestamp: number$1()
25853
25974
  });
@@ -31860,6 +31981,12 @@ Object.freeze({
31860
31981
  addonId: null,
31861
31982
  access: "view"
31862
31983
  },
31984
+ "pipelineAnalytics.listEventMedia": {
31985
+ capName: "pipeline-analytics",
31986
+ capScope: "device",
31987
+ addonId: null,
31988
+ access: "view"
31989
+ },
31863
31990
  "pipelineAnalytics.listGroups": {
31864
31991
  capName: "pipeline-analytics",
31865
31992
  capScope: "device",
@@ -35483,6 +35610,11 @@ Object.freeze({
35483
35610
  form: "array",
35484
35611
  optional: false
35485
35612
  }],
35613
+ "pipelineAnalytics.listEventMedia": [{
35614
+ name: "deviceId",
35615
+ form: "single",
35616
+ optional: false
35617
+ }],
35486
35618
  "pipelineAnalytics.listGroups": [{
35487
35619
  name: "deviceIds",
35488
35620
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.41",
3
+ "version": "0.4.43",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",