@camstack/addon-terminal 0.1.50 → 0.1.52

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
@@ -8102,6 +8102,21 @@ var RelocateJobSchema = object({
8102
8102
  bytesMoved: number().int(),
8103
8103
  /** Total files discovered up front; null while (or when) unknown. */
8104
8104
  filesTotal: number().int().nullable(),
8105
+ /**
8106
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8107
+ * made that nobody asked for, so it is reported where the operator reads the
8108
+ * job rather than only in a log line.
8109
+ *
8110
+ * A footage segment records its byte count in its own NAME, and the durable
8111
+ * hour row derives its aggregates from those names. A file that does not
8112
+ * match its name therefore makes the ledger's sums — and with them quota and
8113
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8114
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8115
+ *
8116
+ * Absent on lanes where the question has no meaning: a media blob's size is
8117
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8118
+ */
8119
+ rowsReconciled: number().int().nonnegative().optional(),
8105
8120
  startedAt: number(),
8106
8121
  finishedAt: number().nullable(),
8107
8122
  error: string().nullable()
@@ -8170,14 +8185,42 @@ var RelocateMediaInputSchema = object({
8170
8185
  /** Omitted = `move`, the pre-existing behaviour. */
8171
8186
  mode: MediaRelocateModeSchema.optional()
8172
8187
  });
8173
- /** How many rows still carry NO `locationId` — the population a repoint would
8174
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8175
- * value that permits a non-blocking `eventMedia` cutover. */
8176
- var UnstampedEventMediaCountSchema = object({
8177
- media: number().int().nonnegative(),
8178
- retrainFrames: number().int().nonnegative(),
8179
- total: number().int().nonnegative()
8188
+ /**
8189
+ * The unstamped population of ONE collection split, because the gate and the
8190
+ * operator ask two different questions and only one of them has to be cheap.
8191
+ *
8192
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8193
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8194
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8195
+ * that matters — after a seal, when the population is empty.
8196
+ *
8197
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8198
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8199
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8200
+ * and useful answer: "there are some, and this read could not say how many"
8201
+ * still refuses the cutover, which is the whole job.
8202
+ */
8203
+ var UnstampedRowsSchema = object({
8204
+ present: boolean(),
8205
+ rows: number().int().nonnegative().nullable()
8180
8206
  });
8207
+ /**
8208
+ * How many rows still carry NO `locationId` — the population a repoint would
8209
+ * silently re-aim at a disk that does not hold their bytes.
8210
+ *
8211
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8212
+ * over. The gate opens on a measured absence and on nothing else; an unread
8213
+ * collection and an empty one are different facts, and this repo has already
8214
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8215
+ */
8216
+ var UnstampedEventMediaCountSchema = object({
8217
+ media: UnstampedRowsSchema,
8218
+ retrainFrames: UnstampedRowsSchema,
8219
+ /** True when EITHER collection holds one. The refusal reads this. */
8220
+ anyPresent: boolean(),
8221
+ /** Sum across both, or `null` when either lane could not be counted. */
8222
+ total: number().int().nonnegative().nullable()
8223
+ }).nullable();
8181
8224
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8182
8225
  /** The independently selectable logical storage classes — every class
8183
8226
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8288,6 +8331,10 @@ var StorageMigrationMoveProgressSchema = object({
8288
8331
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8289
8332
  filesTotal: number().int().nonnegative().nullable(),
8290
8333
  bytesMoved: number().int().nonnegative(),
8334
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8335
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8336
+ * rows would be the same failure as one that silently skipped them. */
8337
+ rowsReconciled: number().int().nonnegative().optional(),
8291
8338
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8292
8339
  * crash gets a new mover, and a rate computed from the migration's start
8293
8340
  * would silently average in the time nothing was running. */
@@ -13181,6 +13228,114 @@ method(object({
13181
13228
  height: number()
13182
13229
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13183
13230
  /**
13231
+ * `failure-contribution` — the capability an addon reports its OWN losses
13232
+ * through, per camera, with the denominator attached. It stores nothing.
13233
+ *
13234
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13235
+ *
13236
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13237
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13238
+ * copied: the contributor reports what it already knows, hub-main adds only
13239
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13240
+ * somebody to forget to edit.
13241
+ *
13242
+ * They are not merged, because their invariants are opposites:
13243
+ *
13244
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13245
+ * claim a camera cost nothing, which is a measurement nobody made;
13246
+ * - a `failure-contribution` zero is the **most valuable value on the
13247
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13248
+ * and it is exactly what an absent entry cannot say.
13249
+ *
13250
+ * Putting a loss counter on a cost entry would also break the reconciliation
13251
+ * that gives `load-contribution` its point: contributions are subtracted from
13252
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13253
+ * has no process.
13254
+ *
13255
+ * ## Why not a log line, since the counters already exist
13256
+ *
13257
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13258
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13259
+ * ends in a log line, and a log line is the thing the operator asked to stop
13260
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13261
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13262
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13263
+ * media blackout were both diagnosed. The counters stay; this is where they can
13264
+ * be READ.
13265
+ *
13266
+ * ## The rate is served with its denominator or not at all
13267
+ *
13268
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13269
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13270
+ * than yesterday" and was **flat across twelve hours** once divided by the
13271
+ * successes on the same path. A surface that publishes only the numerator
13272
+ * reproduces that mistake on every read.
13273
+ *
13274
+ * ## Shape
13275
+ *
13276
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13277
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13278
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13279
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13280
+ * a forked runner's entries reach hub-main over transport that already exists.
13281
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13282
+ * result through `system.getFailureContributions`.
13283
+ */
13284
+ var FailureReasonCountSchema = object({
13285
+ /**
13286
+ * Why the attempt did not land, in the contributor's own vocabulary —
13287
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13288
+ * strings that already appear in this repo's logs and, where one exists, the
13289
+ * same string the per-track `previewMissReason` records (D276): a second
13290
+ * vocabulary for the same loss would make the row and the counter
13291
+ * un-joinable.
13292
+ */
13293
+ reason: string(),
13294
+ count: number().int().nonnegative()
13295
+ });
13296
+ var FailureContributionSchema = object({
13297
+ /**
13298
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13299
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13300
+ * `unit` free: the families are owned by different addons and a shared enum
13301
+ * is a central list that rots invisibly.
13302
+ */
13303
+ family: string(),
13304
+ /**
13305
+ * The NUMERIC device id — the same value every log line carries as
13306
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13307
+ * cannot name the camera must not emit the entry, because a fleet total
13308
+ * cannot answer the only question anybody asks of this surface.
13309
+ */
13310
+ deviceId: number().int().positive(),
13311
+ /**
13312
+ * A second dimension inside the family: the model / step id for an inference
13313
+ * timeout, so "which camera AND which model" is one read. Absent when the
13314
+ * family has a single variant.
13315
+ */
13316
+ variant: string().optional(),
13317
+ /**
13318
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13319
+ * differencing two reads must drop the interval when it changes, because the
13320
+ * counter restarted from zero in a respawned runner. Same discipline as
13321
+ * `LoadContribution.startedAtMs`.
13322
+ */
13323
+ sinceMs: number(),
13324
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13325
+ atMs: number(),
13326
+ /**
13327
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13328
+ * window. A failure count published without it is the mistake this schema
13329
+ * exists to make impossible.
13330
+ */
13331
+ attempts: number().int().nonnegative(),
13332
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13333
+ succeeded: number().int().nonnegative(),
13334
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13335
+ reasons: array(FailureReasonCountSchema).readonly()
13336
+ });
13337
+ method(_void(), array(FailureContributionSchema).readonly());
13338
+ /**
13184
13339
  * filesystem-browse — per-node capability for browsing the node's local
13185
13340
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13186
13341
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13702,6 +13857,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13702
13857
  kind: "mutation",
13703
13858
  auth: "admin"
13704
13859
  });
13860
+ var LoadContributionSchema = object({
13861
+ role: _enum([
13862
+ "decode",
13863
+ "transcode",
13864
+ "recording",
13865
+ "streaming",
13866
+ "detection"
13867
+ ]),
13868
+ /**
13869
+ * The NUMERIC device id — the same value every log line carries as
13870
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13871
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13872
+ * contributor that cannot name its camera must not emit the entry at all,
13873
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13874
+ * and would quietly turn one camera's cost into everybody's.
13875
+ */
13876
+ deviceId: number().int().positive().nullable(),
13877
+ attribution: _enum([
13878
+ "measured",
13879
+ "accounted",
13880
+ "unattributable"
13881
+ ]),
13882
+ /**
13883
+ * What ONE entry is, in the contributor's own words — `615/high`,
13884
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13885
+ * family and inventing a common one would lose the only information that
13886
+ * makes two entries for the same camera distinguishable.
13887
+ */
13888
+ unit: string(),
13889
+ /**
13890
+ * The OS process this cost lives in, when there is one. Present so a
13891
+ * consumer can (a) tell two generations of the same unit apart across a
13892
+ * restart, and (b) subtract claimed processes from the node's process
13893
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13894
+ * process of its own.
13895
+ */
13896
+ pid: number().int().positive().optional(),
13897
+ /**
13898
+ * When this generation started. The pid's incarnation marker: a consumer
13899
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13900
+ * window when this changes, because the counter restarted from zero in a new
13901
+ * process.
13902
+ */
13903
+ startedAtMs: number().optional(),
13904
+ /**
13905
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13906
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13907
+ * contribution is asked for.
13908
+ *
13909
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13910
+ * needs a sampler, and a new per-node sampler is the defect half of
13911
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13912
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13913
+ *
13914
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13915
+ * an entry with no process.
13916
+ */
13917
+ cpuSeconds: number().optional(),
13918
+ /** Resident bytes of this unit's process, same source and same rules. */
13919
+ rssBytes: number().optional()
13920
+ });
13921
+ method(_void(), array(LoadContributionSchema).readonly());
13705
13922
  /**
13706
13923
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13707
13924
  * through. It stores nothing.
@@ -13778,176 +13995,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13778
13995
  tags: record(string(), string()).optional()
13779
13996
  }), array(LogEntrySchema).readonly());
13780
13997
  /**
13781
- * `failure-contribution` — the capability an addon reports its OWN losses
13782
- * through, per camera, with the denominator attached. It stores nothing.
13783
- *
13784
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13785
- *
13786
- * `load-contribution` answers *what did this camera COST*. This answers *what
13787
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13788
- * copied: the contributor reports what it already knows, hub-main adds only
13789
- * `addonId`, nothing needs global knowledge, and there is no central list for
13790
- * somebody to forget to edit.
13791
- *
13792
- * They are not merged, because their invariants are opposites:
13793
- *
13794
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13795
- * claim a camera cost nothing, which is a measurement nobody made;
13796
- * - a `failure-contribution` zero is the **most valuable value on the
13797
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13798
- * and it is exactly what an absent entry cannot say.
13799
- *
13800
- * Putting a loss counter on a cost entry would also break the reconciliation
13801
- * that gives `load-contribution` its point: contributions are subtracted from
13802
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13803
- * has no process.
13804
- *
13805
- * ## Why not a log line, since the counters already exist
13806
- *
13807
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13808
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13809
- * ends in a log line, and a log line is the thing the operator asked to stop
13810
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13811
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13812
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13813
- * media blackout were both diagnosed. The counters stay; this is where they can
13814
- * be READ.
13815
- *
13816
- * ## The rate is served with its denominator or not at all
13817
- *
13818
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13819
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13820
- * than yesterday" and was **flat across twelve hours** once divided by the
13821
- * successes on the same path. A surface that publishes only the numerator
13822
- * reproduces that mistake on every read.
13823
- *
13824
- * ## Shape
13825
- *
13826
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13827
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13828
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13829
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13830
- * a forked runner's entries reach hub-main over transport that already exists.
13831
- * No new UDS message, no second registry (D3). The operator reads the assembled
13832
- * result through `system.getFailureContributions`.
13833
- */
13834
- var FailureReasonCountSchema = object({
13835
- /**
13836
- * Why the attempt did not land, in the contributor's own vocabulary —
13837
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13838
- * strings that already appear in this repo's logs and, where one exists, the
13839
- * same string the per-track `previewMissReason` records (D276): a second
13840
- * vocabulary for the same loss would make the row and the counter
13841
- * un-joinable.
13842
- */
13843
- reason: string(),
13844
- count: number().int().nonnegative()
13845
- });
13846
- var FailureContributionSchema = object({
13847
- /**
13848
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13849
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13850
- * `unit` free: the families are owned by different addons and a shared enum
13851
- * is a central list that rots invisibly.
13852
- */
13853
- family: string(),
13854
- /**
13855
- * The NUMERIC device id — the same value every log line carries as
13856
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13857
- * cannot name the camera must not emit the entry, because a fleet total
13858
- * cannot answer the only question anybody asks of this surface.
13859
- */
13860
- deviceId: number().int().positive(),
13861
- /**
13862
- * A second dimension inside the family: the model / step id for an inference
13863
- * timeout, so "which camera AND which model" is one read. Absent when the
13864
- * family has a single variant.
13865
- */
13866
- variant: string().optional(),
13867
- /**
13868
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13869
- * differencing two reads must drop the interval when it changes, because the
13870
- * counter restarted from zero in a respawned runner. Same discipline as
13871
- * `LoadContribution.startedAtMs`.
13872
- */
13873
- sinceMs: number(),
13874
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13875
- atMs: number(),
13876
- /**
13877
- * THE DENOMINATOR — every attempt on this path for this camera in the
13878
- * window. A failure count published without it is the mistake this schema
13879
- * exists to make impossible.
13880
- */
13881
- attempts: number().int().nonnegative(),
13882
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13883
- succeeded: number().int().nonnegative(),
13884
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13885
- reasons: array(FailureReasonCountSchema).readonly()
13886
- });
13887
- method(_void(), array(FailureContributionSchema).readonly());
13888
- var LoadContributionSchema = object({
13889
- role: _enum([
13890
- "decode",
13891
- "transcode",
13892
- "recording",
13893
- "streaming",
13894
- "detection"
13895
- ]),
13896
- /**
13897
- * The NUMERIC device id — the same value every log line carries as
13898
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13899
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13900
- * contributor that cannot name its camera must not emit the entry at all,
13901
- * because an unnamed per-camera entry is indistinguishable from a shared one
13902
- * and would quietly turn one camera's cost into everybody's.
13903
- */
13904
- deviceId: number().int().positive().nullable(),
13905
- attribution: _enum([
13906
- "measured",
13907
- "accounted",
13908
- "unattributable"
13909
- ]),
13910
- /**
13911
- * What ONE entry is, in the contributor's own words — `615/high`,
13912
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13913
- * family and inventing a common one would lose the only information that
13914
- * makes two entries for the same camera distinguishable.
13915
- */
13916
- unit: string(),
13917
- /**
13918
- * The OS process this cost lives in, when there is one. Present so a
13919
- * consumer can (a) tell two generations of the same unit apart across a
13920
- * restart, and (b) subtract claimed processes from the node's process
13921
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13922
- * process of its own.
13923
- */
13924
- pid: number().int().positive().optional(),
13925
- /**
13926
- * When this generation started. The pid's incarnation marker: a consumer
13927
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13928
- * window when this changes, because the counter restarted from zero in a new
13929
- * process.
13930
- */
13931
- startedAtMs: number().optional(),
13932
- /**
13933
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13934
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13935
- * contribution is asked for.
13936
- *
13937
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13938
- * needs a sampler, and a new per-node sampler is the defect half of
13939
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13940
- * by whoever already keeps a history; a rate cannot be un-averaged.
13941
- *
13942
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13943
- * an entry with no process.
13944
- */
13945
- cpuSeconds: number().optional(),
13946
- /** Resident bytes of this unit's process, same source and same rules. */
13947
- rssBytes: number().optional()
13948
- });
13949
- method(_void(), array(LoadContributionSchema).readonly());
13950
- /**
13951
13998
  * `login-method` — collection cap through which auth addons contribute
13952
13999
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13953
14000
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18685,12 +18732,53 @@ var MediaFileKindEnum = _enum([
18685
18732
  "keyFrameSmall",
18686
18733
  "thumbnailSmall"
18687
18734
  ]);
18735
+ /**
18736
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18737
+ * ARE — never the bytes themselves.
18738
+ *
18739
+ * ## Why `url` and not `base64`
18740
+ *
18741
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18742
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18743
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18744
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18745
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18746
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18747
+ *
18748
+ * `url` points at the `event-media` data plane
18749
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18750
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18751
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18752
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18753
+ * no less protected than they were inside a `view`-level cap response — see
18754
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18755
+ * (per-device scoping).
18756
+ *
18757
+ * The URL is built from the row's **stored** key, which is not always its
18758
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18759
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18760
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18761
+ *
18762
+ * ## `base64` is TRANSITIONAL and is going away
18763
+ *
18764
+ * It is still populated for one reason: the deployed viewer's track-detail
18765
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18766
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18767
+ * triangle — not as absence. Removing the field before that viewer ships is an
18768
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18769
+ * delete this line and the `withBytes` pass-through in
18770
+ * `analytics-query-facade.ts`; nothing else reads it.
18771
+ */
18688
18772
  var MediaFileSchema = object({
18689
18773
  key: string(),
18690
18774
  kind: MediaFileKindEnum,
18691
- base64: string(),
18692
18775
  sizeBytes: number(),
18693
18776
  timestamp: number()
18777
+ }).extend({
18778
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18779
+ url: string(),
18780
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18781
+ base64: string()
18694
18782
  });
18695
18783
  /**
18696
18784
  * One media row WITHOUT its bytes.
@@ -18702,7 +18790,9 @@ var MediaFileSchema = object({
18702
18790
  * blocks the whole view.
18703
18791
  *
18704
18792
  * `sizeBytes` is carried because it is what lets a client decide between the
18705
- * stored blob and a `?variant=thumb` rendering without fetching either.
18793
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18794
+ * `url` because a client that had to build the plane path itself is a second
18795
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18706
18796
  */
18707
18797
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18708
18798
  /**
@@ -19391,6 +19481,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19391
19481
  }), array(MediaFileSchema).readonly()), method(object({
19392
19482
  trackId: string(),
19393
19483
  deviceId: number()
19484
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19485
+ eventId: string(),
19486
+ deviceId: number()
19394
19487
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19395
19488
  kind: "mutation",
19396
19489
  auth: "admin"
@@ -24529,10 +24622,24 @@ var FaceClusterSchema = object({
24529
24622
  size: number().int(),
24530
24623
  cohesion: number()
24531
24624
  });
24625
+ /**
24626
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24627
+ * are — never the bytes.
24628
+ *
24629
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24630
+ * track/event contract) is still populated because a deployed viewer requires
24631
+ * the field to parse a row at all; this method has no such reader. Its ONE
24632
+ * caller is the admin UI's detail modal, which was building
24633
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24634
+ * dialog already rendering its key FRAME from the `event-media` plane.
24635
+ *
24636
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24637
+ * media key directly, so this needed no new plane and no new access decision.
24638
+ */
24532
24639
  var MediaFileLiteSchema$1 = object({
24533
24640
  key: string(),
24534
24641
  kind: string(),
24535
- base64: string(),
24642
+ url: string(),
24536
24643
  sizeBytes: number(),
24537
24644
  timestamp: number()
24538
24645
  });
@@ -27554,10 +27661,24 @@ var PlateInfoSchema = object({
27554
27661
  */
27555
27662
  cropUrl: string().optional()
27556
27663
  });
27664
+ /**
27665
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27666
+ * are — never the bytes.
27667
+ *
27668
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27669
+ * track/event contract) is still populated because a deployed viewer requires
27670
+ * the field to parse a row at all; this method has no such reader. Its ONE
27671
+ * caller is the admin UI's detail modal, which was building
27672
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27673
+ * dialog already rendering its key FRAME from the `event-media` plane.
27674
+ *
27675
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27676
+ * media key directly, so this needed no new plane and no new access decision.
27677
+ */
27557
27678
  var MediaFileLiteSchema = object({
27558
27679
  key: string(),
27559
27680
  kind: string(),
27560
- base64: string(),
27681
+ url: string(),
27561
27682
  sizeBytes: number(),
27562
27683
  timestamp: number()
27563
27684
  });
@@ -35452,6 +35573,12 @@ Object.freeze({
35452
35573
  addonId: null,
35453
35574
  access: "view"
35454
35575
  },
35576
+ "pipelineAnalytics.listEventMedia": {
35577
+ capName: "pipeline-analytics",
35578
+ capScope: "device",
35579
+ addonId: null,
35580
+ access: "view"
35581
+ },
35455
35582
  "pipelineAnalytics.listGroups": {
35456
35583
  capName: "pipeline-analytics",
35457
35584
  capScope: "device",
@@ -39075,6 +39202,11 @@ Object.freeze({
39075
39202
  form: "array",
39076
39203
  optional: false
39077
39204
  }],
39205
+ "pipelineAnalytics.listEventMedia": [{
39206
+ name: "deviceId",
39207
+ form: "single",
39208
+ optional: false
39209
+ }],
39078
39210
  "pipelineAnalytics.listGroups": [{
39079
39211
  name: "deviceIds",
39080
39212
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8079,6 +8079,21 @@ var RelocateJobSchema = object({
8079
8079
  bytesMoved: number().int(),
8080
8080
  /** Total files discovered up front; null while (or when) unknown. */
8081
8081
  filesTotal: number().int().nullable(),
8082
+ /**
8083
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8084
+ * made that nobody asked for, so it is reported where the operator reads the
8085
+ * job rather than only in a log line.
8086
+ *
8087
+ * A footage segment records its byte count in its own NAME, and the durable
8088
+ * hour row derives its aggregates from those names. A file that does not
8089
+ * match its name therefore makes the ledger's sums — and with them quota and
8090
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8091
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8092
+ *
8093
+ * Absent on lanes where the question has no meaning: a media blob's size is
8094
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8095
+ */
8096
+ rowsReconciled: number().int().nonnegative().optional(),
8082
8097
  startedAt: number(),
8083
8098
  finishedAt: number().nullable(),
8084
8099
  error: string().nullable()
@@ -8147,14 +8162,42 @@ var RelocateMediaInputSchema = object({
8147
8162
  /** Omitted = `move`, the pre-existing behaviour. */
8148
8163
  mode: MediaRelocateModeSchema.optional()
8149
8164
  });
8150
- /** How many rows still carry NO `locationId` — the population a repoint would
8151
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8152
- * value that permits a non-blocking `eventMedia` cutover. */
8153
- var UnstampedEventMediaCountSchema = object({
8154
- media: number().int().nonnegative(),
8155
- retrainFrames: number().int().nonnegative(),
8156
- total: number().int().nonnegative()
8165
+ /**
8166
+ * The unstamped population of ONE collection split, because the gate and the
8167
+ * operator ask two different questions and only one of them has to be cheap.
8168
+ *
8169
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8170
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8171
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8172
+ * that matters — after a seal, when the population is empty.
8173
+ *
8174
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8175
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8176
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8177
+ * and useful answer: "there are some, and this read could not say how many"
8178
+ * still refuses the cutover, which is the whole job.
8179
+ */
8180
+ var UnstampedRowsSchema = object({
8181
+ present: boolean(),
8182
+ rows: number().int().nonnegative().nullable()
8157
8183
  });
8184
+ /**
8185
+ * How many rows still carry NO `locationId` — the population a repoint would
8186
+ * silently re-aim at a disk that does not hold their bytes.
8187
+ *
8188
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8189
+ * over. The gate opens on a measured absence and on nothing else; an unread
8190
+ * collection and an empty one are different facts, and this repo has already
8191
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8192
+ */
8193
+ var UnstampedEventMediaCountSchema = object({
8194
+ media: UnstampedRowsSchema,
8195
+ retrainFrames: UnstampedRowsSchema,
8196
+ /** True when EITHER collection holds one. The refusal reads this. */
8197
+ anyPresent: boolean(),
8198
+ /** Sum across both, or `null` when either lane could not be counted. */
8199
+ total: number().int().nonnegative().nullable()
8200
+ }).nullable();
8158
8201
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8159
8202
  /** The independently selectable logical storage classes — every class
8160
8203
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8265,6 +8308,10 @@ var StorageMigrationMoveProgressSchema = object({
8265
8308
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8266
8309
  filesTotal: number().int().nonnegative().nullable(),
8267
8310
  bytesMoved: number().int().nonnegative(),
8311
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8312
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8313
+ * rows would be the same failure as one that silently skipped them. */
8314
+ rowsReconciled: number().int().nonnegative().optional(),
8268
8315
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8269
8316
  * crash gets a new mover, and a rate computed from the migration's start
8270
8317
  * would silently average in the time nothing was running. */
@@ -13158,6 +13205,114 @@ method(object({
13158
13205
  height: number()
13159
13206
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13160
13207
  /**
13208
+ * `failure-contribution` — the capability an addon reports its OWN losses
13209
+ * through, per camera, with the denominator attached. It stores nothing.
13210
+ *
13211
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13212
+ *
13213
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13214
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13215
+ * copied: the contributor reports what it already knows, hub-main adds only
13216
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13217
+ * somebody to forget to edit.
13218
+ *
13219
+ * They are not merged, because their invariants are opposites:
13220
+ *
13221
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13222
+ * claim a camera cost nothing, which is a measurement nobody made;
13223
+ * - a `failure-contribution` zero is the **most valuable value on the
13224
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13225
+ * and it is exactly what an absent entry cannot say.
13226
+ *
13227
+ * Putting a loss counter on a cost entry would also break the reconciliation
13228
+ * that gives `load-contribution` its point: contributions are subtracted from
13229
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13230
+ * has no process.
13231
+ *
13232
+ * ## Why not a log line, since the counters already exist
13233
+ *
13234
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13235
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13236
+ * ends in a log line, and a log line is the thing the operator asked to stop
13237
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13238
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13239
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13240
+ * media blackout were both diagnosed. The counters stay; this is where they can
13241
+ * be READ.
13242
+ *
13243
+ * ## The rate is served with its denominator or not at all
13244
+ *
13245
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13246
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13247
+ * than yesterday" and was **flat across twelve hours** once divided by the
13248
+ * successes on the same path. A surface that publishes only the numerator
13249
+ * reproduces that mistake on every read.
13250
+ *
13251
+ * ## Shape
13252
+ *
13253
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13254
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13255
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13256
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13257
+ * a forked runner's entries reach hub-main over transport that already exists.
13258
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13259
+ * result through `system.getFailureContributions`.
13260
+ */
13261
+ var FailureReasonCountSchema = object({
13262
+ /**
13263
+ * Why the attempt did not land, in the contributor's own vocabulary —
13264
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13265
+ * strings that already appear in this repo's logs and, where one exists, the
13266
+ * same string the per-track `previewMissReason` records (D276): a second
13267
+ * vocabulary for the same loss would make the row and the counter
13268
+ * un-joinable.
13269
+ */
13270
+ reason: string(),
13271
+ count: number().int().nonnegative()
13272
+ });
13273
+ var FailureContributionSchema = object({
13274
+ /**
13275
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13276
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13277
+ * `unit` free: the families are owned by different addons and a shared enum
13278
+ * is a central list that rots invisibly.
13279
+ */
13280
+ family: string(),
13281
+ /**
13282
+ * The NUMERIC device id — the same value every log line carries as
13283
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13284
+ * cannot name the camera must not emit the entry, because a fleet total
13285
+ * cannot answer the only question anybody asks of this surface.
13286
+ */
13287
+ deviceId: number().int().positive(),
13288
+ /**
13289
+ * A second dimension inside the family: the model / step id for an inference
13290
+ * timeout, so "which camera AND which model" is one read. Absent when the
13291
+ * family has a single variant.
13292
+ */
13293
+ variant: string().optional(),
13294
+ /**
13295
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13296
+ * differencing two reads must drop the interval when it changes, because the
13297
+ * counter restarted from zero in a respawned runner. Same discipline as
13298
+ * `LoadContribution.startedAtMs`.
13299
+ */
13300
+ sinceMs: number(),
13301
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13302
+ atMs: number(),
13303
+ /**
13304
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13305
+ * window. A failure count published without it is the mistake this schema
13306
+ * exists to make impossible.
13307
+ */
13308
+ attempts: number().int().nonnegative(),
13309
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13310
+ succeeded: number().int().nonnegative(),
13311
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13312
+ reasons: array(FailureReasonCountSchema).readonly()
13313
+ });
13314
+ method(_void(), array(FailureContributionSchema).readonly());
13315
+ /**
13161
13316
  * filesystem-browse — per-node capability for browsing the node's local
13162
13317
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13163
13318
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13679,6 +13834,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13679
13834
  kind: "mutation",
13680
13835
  auth: "admin"
13681
13836
  });
13837
+ var LoadContributionSchema = object({
13838
+ role: _enum([
13839
+ "decode",
13840
+ "transcode",
13841
+ "recording",
13842
+ "streaming",
13843
+ "detection"
13844
+ ]),
13845
+ /**
13846
+ * The NUMERIC device id — the same value every log line carries as
13847
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13848
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13849
+ * contributor that cannot name its camera must not emit the entry at all,
13850
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13851
+ * and would quietly turn one camera's cost into everybody's.
13852
+ */
13853
+ deviceId: number().int().positive().nullable(),
13854
+ attribution: _enum([
13855
+ "measured",
13856
+ "accounted",
13857
+ "unattributable"
13858
+ ]),
13859
+ /**
13860
+ * What ONE entry is, in the contributor's own words — `615/high`,
13861
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13862
+ * family and inventing a common one would lose the only information that
13863
+ * makes two entries for the same camera distinguishable.
13864
+ */
13865
+ unit: string(),
13866
+ /**
13867
+ * The OS process this cost lives in, when there is one. Present so a
13868
+ * consumer can (a) tell two generations of the same unit apart across a
13869
+ * restart, and (b) subtract claimed processes from the node's process
13870
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13871
+ * process of its own.
13872
+ */
13873
+ pid: number().int().positive().optional(),
13874
+ /**
13875
+ * When this generation started. The pid's incarnation marker: a consumer
13876
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13877
+ * window when this changes, because the counter restarted from zero in a new
13878
+ * process.
13879
+ */
13880
+ startedAtMs: number().optional(),
13881
+ /**
13882
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13883
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13884
+ * contribution is asked for.
13885
+ *
13886
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13887
+ * needs a sampler, and a new per-node sampler is the defect half of
13888
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13889
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13890
+ *
13891
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13892
+ * an entry with no process.
13893
+ */
13894
+ cpuSeconds: number().optional(),
13895
+ /** Resident bytes of this unit's process, same source and same rules. */
13896
+ rssBytes: number().optional()
13897
+ });
13898
+ method(_void(), array(LoadContributionSchema).readonly());
13682
13899
  /**
13683
13900
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13684
13901
  * through. It stores nothing.
@@ -13755,176 +13972,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13755
13972
  tags: record(string(), string()).optional()
13756
13973
  }), array(LogEntrySchema).readonly());
13757
13974
  /**
13758
- * `failure-contribution` — the capability an addon reports its OWN losses
13759
- * through, per camera, with the denominator attached. It stores nothing.
13760
- *
13761
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13762
- *
13763
- * `load-contribution` answers *what did this camera COST*. This answers *what
13764
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13765
- * copied: the contributor reports what it already knows, hub-main adds only
13766
- * `addonId`, nothing needs global knowledge, and there is no central list for
13767
- * somebody to forget to edit.
13768
- *
13769
- * They are not merged, because their invariants are opposites:
13770
- *
13771
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13772
- * claim a camera cost nothing, which is a measurement nobody made;
13773
- * - a `failure-contribution` zero is the **most valuable value on the
13774
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13775
- * and it is exactly what an absent entry cannot say.
13776
- *
13777
- * Putting a loss counter on a cost entry would also break the reconciliation
13778
- * that gives `load-contribution` its point: contributions are subtracted from
13779
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13780
- * has no process.
13781
- *
13782
- * ## Why not a log line, since the counters already exist
13783
- *
13784
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13785
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13786
- * ends in a log line, and a log line is the thing the operator asked to stop
13787
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13788
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13789
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13790
- * media blackout were both diagnosed. The counters stay; this is where they can
13791
- * be READ.
13792
- *
13793
- * ## The rate is served with its denominator or not at all
13794
- *
13795
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13796
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13797
- * than yesterday" and was **flat across twelve hours** once divided by the
13798
- * successes on the same path. A surface that publishes only the numerator
13799
- * reproduces that mistake on every read.
13800
- *
13801
- * ## Shape
13802
- *
13803
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13804
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13805
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13806
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13807
- * a forked runner's entries reach hub-main over transport that already exists.
13808
- * No new UDS message, no second registry (D3). The operator reads the assembled
13809
- * result through `system.getFailureContributions`.
13810
- */
13811
- var FailureReasonCountSchema = object({
13812
- /**
13813
- * Why the attempt did not land, in the contributor's own vocabulary —
13814
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13815
- * strings that already appear in this repo's logs and, where one exists, the
13816
- * same string the per-track `previewMissReason` records (D276): a second
13817
- * vocabulary for the same loss would make the row and the counter
13818
- * un-joinable.
13819
- */
13820
- reason: string(),
13821
- count: number().int().nonnegative()
13822
- });
13823
- var FailureContributionSchema = object({
13824
- /**
13825
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13826
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13827
- * `unit` free: the families are owned by different addons and a shared enum
13828
- * is a central list that rots invisibly.
13829
- */
13830
- family: string(),
13831
- /**
13832
- * The NUMERIC device id — the same value every log line carries as
13833
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13834
- * cannot name the camera must not emit the entry, because a fleet total
13835
- * cannot answer the only question anybody asks of this surface.
13836
- */
13837
- deviceId: number().int().positive(),
13838
- /**
13839
- * A second dimension inside the family: the model / step id for an inference
13840
- * timeout, so "which camera AND which model" is one read. Absent when the
13841
- * family has a single variant.
13842
- */
13843
- variant: string().optional(),
13844
- /**
13845
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13846
- * differencing two reads must drop the interval when it changes, because the
13847
- * counter restarted from zero in a respawned runner. Same discipline as
13848
- * `LoadContribution.startedAtMs`.
13849
- */
13850
- sinceMs: number(),
13851
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13852
- atMs: number(),
13853
- /**
13854
- * THE DENOMINATOR — every attempt on this path for this camera in the
13855
- * window. A failure count published without it is the mistake this schema
13856
- * exists to make impossible.
13857
- */
13858
- attempts: number().int().nonnegative(),
13859
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13860
- succeeded: number().int().nonnegative(),
13861
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13862
- reasons: array(FailureReasonCountSchema).readonly()
13863
- });
13864
- method(_void(), array(FailureContributionSchema).readonly());
13865
- var LoadContributionSchema = object({
13866
- role: _enum([
13867
- "decode",
13868
- "transcode",
13869
- "recording",
13870
- "streaming",
13871
- "detection"
13872
- ]),
13873
- /**
13874
- * The NUMERIC device id — the same value every log line carries as
13875
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13876
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13877
- * contributor that cannot name its camera must not emit the entry at all,
13878
- * because an unnamed per-camera entry is indistinguishable from a shared one
13879
- * and would quietly turn one camera's cost into everybody's.
13880
- */
13881
- deviceId: number().int().positive().nullable(),
13882
- attribution: _enum([
13883
- "measured",
13884
- "accounted",
13885
- "unattributable"
13886
- ]),
13887
- /**
13888
- * What ONE entry is, in the contributor's own words — `615/high`,
13889
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13890
- * family and inventing a common one would lose the only information that
13891
- * makes two entries for the same camera distinguishable.
13892
- */
13893
- unit: string(),
13894
- /**
13895
- * The OS process this cost lives in, when there is one. Present so a
13896
- * consumer can (a) tell two generations of the same unit apart across a
13897
- * restart, and (b) subtract claimed processes from the node's process
13898
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13899
- * process of its own.
13900
- */
13901
- pid: number().int().positive().optional(),
13902
- /**
13903
- * When this generation started. The pid's incarnation marker: a consumer
13904
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13905
- * window when this changes, because the counter restarted from zero in a new
13906
- * process.
13907
- */
13908
- startedAtMs: number().optional(),
13909
- /**
13910
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13911
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13912
- * contribution is asked for.
13913
- *
13914
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13915
- * needs a sampler, and a new per-node sampler is the defect half of
13916
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13917
- * by whoever already keeps a history; a rate cannot be un-averaged.
13918
- *
13919
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13920
- * an entry with no process.
13921
- */
13922
- cpuSeconds: number().optional(),
13923
- /** Resident bytes of this unit's process, same source and same rules. */
13924
- rssBytes: number().optional()
13925
- });
13926
- method(_void(), array(LoadContributionSchema).readonly());
13927
- /**
13928
13975
  * `login-method` — collection cap through which auth addons contribute
13929
13976
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13930
13977
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18662,12 +18709,53 @@ var MediaFileKindEnum = _enum([
18662
18709
  "keyFrameSmall",
18663
18710
  "thumbnailSmall"
18664
18711
  ]);
18712
+ /**
18713
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18714
+ * ARE — never the bytes themselves.
18715
+ *
18716
+ * ## Why `url` and not `base64`
18717
+ *
18718
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18719
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18720
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18721
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18722
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18723
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18724
+ *
18725
+ * `url` points at the `event-media` data plane
18726
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18727
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18728
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18729
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18730
+ * no less protected than they were inside a `view`-level cap response — see
18731
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18732
+ * (per-device scoping).
18733
+ *
18734
+ * The URL is built from the row's **stored** key, which is not always its
18735
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18736
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18737
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18738
+ *
18739
+ * ## `base64` is TRANSITIONAL and is going away
18740
+ *
18741
+ * It is still populated for one reason: the deployed viewer's track-detail
18742
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18743
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18744
+ * triangle — not as absence. Removing the field before that viewer ships is an
18745
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18746
+ * delete this line and the `withBytes` pass-through in
18747
+ * `analytics-query-facade.ts`; nothing else reads it.
18748
+ */
18665
18749
  var MediaFileSchema = object({
18666
18750
  key: string(),
18667
18751
  kind: MediaFileKindEnum,
18668
- base64: string(),
18669
18752
  sizeBytes: number(),
18670
18753
  timestamp: number()
18754
+ }).extend({
18755
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18756
+ url: string(),
18757
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18758
+ base64: string()
18671
18759
  });
18672
18760
  /**
18673
18761
  * One media row WITHOUT its bytes.
@@ -18679,7 +18767,9 @@ var MediaFileSchema = object({
18679
18767
  * blocks the whole view.
18680
18768
  *
18681
18769
  * `sizeBytes` is carried because it is what lets a client decide between the
18682
- * stored blob and a `?variant=thumb` rendering without fetching either.
18770
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18771
+ * `url` because a client that had to build the plane path itself is a second
18772
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18683
18773
  */
18684
18774
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18685
18775
  /**
@@ -19368,6 +19458,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19368
19458
  }), array(MediaFileSchema).readonly()), method(object({
19369
19459
  trackId: string(),
19370
19460
  deviceId: number()
19461
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19462
+ eventId: string(),
19463
+ deviceId: number()
19371
19464
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19372
19465
  kind: "mutation",
19373
19466
  auth: "admin"
@@ -24506,10 +24599,24 @@ var FaceClusterSchema = object({
24506
24599
  size: number().int(),
24507
24600
  cohesion: number()
24508
24601
  });
24602
+ /**
24603
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24604
+ * are — never the bytes.
24605
+ *
24606
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24607
+ * track/event contract) is still populated because a deployed viewer requires
24608
+ * the field to parse a row at all; this method has no such reader. Its ONE
24609
+ * caller is the admin UI's detail modal, which was building
24610
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24611
+ * dialog already rendering its key FRAME from the `event-media` plane.
24612
+ *
24613
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24614
+ * media key directly, so this needed no new plane and no new access decision.
24615
+ */
24509
24616
  var MediaFileLiteSchema$1 = object({
24510
24617
  key: string(),
24511
24618
  kind: string(),
24512
- base64: string(),
24619
+ url: string(),
24513
24620
  sizeBytes: number(),
24514
24621
  timestamp: number()
24515
24622
  });
@@ -27531,10 +27638,24 @@ var PlateInfoSchema = object({
27531
27638
  */
27532
27639
  cropUrl: string().optional()
27533
27640
  });
27641
+ /**
27642
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27643
+ * are — never the bytes.
27644
+ *
27645
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27646
+ * track/event contract) is still populated because a deployed viewer requires
27647
+ * the field to parse a row at all; this method has no such reader. Its ONE
27648
+ * caller is the admin UI's detail modal, which was building
27649
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27650
+ * dialog already rendering its key FRAME from the `event-media` plane.
27651
+ *
27652
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27653
+ * media key directly, so this needed no new plane and no new access decision.
27654
+ */
27534
27655
  var MediaFileLiteSchema = object({
27535
27656
  key: string(),
27536
27657
  kind: string(),
27537
- base64: string(),
27658
+ url: string(),
27538
27659
  sizeBytes: number(),
27539
27660
  timestamp: number()
27540
27661
  });
@@ -35429,6 +35550,12 @@ Object.freeze({
35429
35550
  addonId: null,
35430
35551
  access: "view"
35431
35552
  },
35553
+ "pipelineAnalytics.listEventMedia": {
35554
+ capName: "pipeline-analytics",
35555
+ capScope: "device",
35556
+ addonId: null,
35557
+ access: "view"
35558
+ },
35432
35559
  "pipelineAnalytics.listGroups": {
35433
35560
  capName: "pipeline-analytics",
35434
35561
  capScope: "device",
@@ -39052,6 +39179,11 @@ Object.freeze({
39052
39179
  form: "array",
39053
39180
  optional: false
39054
39181
  }],
39182
+ "pipelineAnalytics.listEventMedia": [{
39183
+ name: "deviceId",
39184
+ form: "single",
39185
+ optional: false
39186
+ }],
39055
39187
  "pipelineAnalytics.listGroups": [{
39056
39188
  name: "deviceIds",
39057
39189
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",