@camstack/addon-provider-rademacher 0.2.44 → 0.2.46

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
@@ -8988,6 +8988,21 @@ var RelocateJobSchema = object({
8988
8988
  bytesMoved: number().int(),
8989
8989
  /** Total files discovered up front; null while (or when) unknown. */
8990
8990
  filesTotal: number().int().nullable(),
8991
+ /**
8992
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8993
+ * made that nobody asked for, so it is reported where the operator reads the
8994
+ * job rather than only in a log line.
8995
+ *
8996
+ * A footage segment records its byte count in its own NAME, and the durable
8997
+ * hour row derives its aggregates from those names. A file that does not
8998
+ * match its name therefore makes the ledger's sums — and with them quota and
8999
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
9000
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9001
+ *
9002
+ * Absent on lanes where the question has no meaning: a media blob's size is
9003
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9004
+ */
9005
+ rowsReconciled: number().int().nonnegative().optional(),
8991
9006
  startedAt: number(),
8992
9007
  finishedAt: number().nullable(),
8993
9008
  error: string().nullable()
@@ -9056,14 +9071,42 @@ var RelocateMediaInputSchema = object({
9056
9071
  /** Omitted = `move`, the pre-existing behaviour. */
9057
9072
  mode: MediaRelocateModeSchema.optional()
9058
9073
  });
9059
- /** How many rows still carry NO `locationId` — the population a repoint would
9060
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
9061
- * value that permits a non-blocking `eventMedia` cutover. */
9062
- var UnstampedEventMediaCountSchema = object({
9063
- media: number().int().nonnegative(),
9064
- retrainFrames: number().int().nonnegative(),
9065
- total: number().int().nonnegative()
9074
+ /**
9075
+ * The unstamped population of ONE collection split, because the gate and the
9076
+ * operator ask two different questions and only one of them has to be cheap.
9077
+ *
9078
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9079
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9080
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9081
+ * that matters — after a seal, when the population is empty.
9082
+ *
9083
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9084
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9085
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9086
+ * and useful answer: "there are some, and this read could not say how many"
9087
+ * still refuses the cutover, which is the whole job.
9088
+ */
9089
+ var UnstampedRowsSchema = object({
9090
+ present: boolean(),
9091
+ rows: number().int().nonnegative().nullable()
9066
9092
  });
9093
+ /**
9094
+ * How many rows still carry NO `locationId` — the population a repoint would
9095
+ * silently re-aim at a disk that does not hold their bytes.
9096
+ *
9097
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9098
+ * over. The gate opens on a measured absence and on nothing else; an unread
9099
+ * collection and an empty one are different facts, and this repo has already
9100
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9101
+ */
9102
+ var UnstampedEventMediaCountSchema = object({
9103
+ media: UnstampedRowsSchema,
9104
+ retrainFrames: UnstampedRowsSchema,
9105
+ /** True when EITHER collection holds one. The refusal reads this. */
9106
+ anyPresent: boolean(),
9107
+ /** Sum across both, or `null` when either lane could not be counted. */
9108
+ total: number().int().nonnegative().nullable()
9109
+ }).nullable();
9067
9110
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9068
9111
  /** The independently selectable logical storage classes — every class
9069
9112
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9174,6 +9217,10 @@ var StorageMigrationMoveProgressSchema = object({
9174
9217
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9175
9218
  filesTotal: number().int().nonnegative().nullable(),
9176
9219
  bytesMoved: number().int().nonnegative(),
9220
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9221
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9222
+ * rows would be the same failure as one that silently skipped them. */
9223
+ rowsReconciled: number().int().nonnegative().optional(),
9177
9224
  /** The MOVER's start, not the migration's: a drain restarted after an addon
9178
9225
  * crash gets a new mover, and a rate computed from the migration's start
9179
9226
  * would silently average in the time nothing was running. */
@@ -14122,6 +14169,114 @@ method(object({
14122
14169
  height: number()
14123
14170
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14124
14171
  /**
14172
+ * `failure-contribution` — the capability an addon reports its OWN losses
14173
+ * through, per camera, with the denominator attached. It stores nothing.
14174
+ *
14175
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14176
+ *
14177
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14178
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14179
+ * copied: the contributor reports what it already knows, hub-main adds only
14180
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14181
+ * somebody to forget to edit.
14182
+ *
14183
+ * They are not merged, because their invariants are opposites:
14184
+ *
14185
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14186
+ * claim a camera cost nothing, which is a measurement nobody made;
14187
+ * - a `failure-contribution` zero is the **most valuable value on the
14188
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14189
+ * and it is exactly what an absent entry cannot say.
14190
+ *
14191
+ * Putting a loss counter on a cost entry would also break the reconciliation
14192
+ * that gives `load-contribution` its point: contributions are subtracted from
14193
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14194
+ * has no process.
14195
+ *
14196
+ * ## Why not a log line, since the counters already exist
14197
+ *
14198
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14199
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14200
+ * ends in a log line, and a log line is the thing the operator asked to stop
14201
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14202
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14203
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14204
+ * media blackout were both diagnosed. The counters stay; this is where they can
14205
+ * be READ.
14206
+ *
14207
+ * ## The rate is served with its denominator or not at all
14208
+ *
14209
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14210
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14211
+ * than yesterday" and was **flat across twelve hours** once divided by the
14212
+ * successes on the same path. A surface that publishes only the numerator
14213
+ * reproduces that mistake on every read.
14214
+ *
14215
+ * ## Shape
14216
+ *
14217
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14218
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14219
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14220
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14221
+ * a forked runner's entries reach hub-main over transport that already exists.
14222
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14223
+ * result through `system.getFailureContributions`.
14224
+ */
14225
+ var FailureReasonCountSchema = object({
14226
+ /**
14227
+ * Why the attempt did not land, in the contributor's own vocabulary —
14228
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14229
+ * strings that already appear in this repo's logs and, where one exists, the
14230
+ * same string the per-track `previewMissReason` records (D276): a second
14231
+ * vocabulary for the same loss would make the row and the counter
14232
+ * un-joinable.
14233
+ */
14234
+ reason: string(),
14235
+ count: number().int().nonnegative()
14236
+ });
14237
+ var FailureContributionSchema = object({
14238
+ /**
14239
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14240
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14241
+ * `unit` free: the families are owned by different addons and a shared enum
14242
+ * is a central list that rots invisibly.
14243
+ */
14244
+ family: string(),
14245
+ /**
14246
+ * The NUMERIC device id — the same value every log line carries as
14247
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14248
+ * cannot name the camera must not emit the entry, because a fleet total
14249
+ * cannot answer the only question anybody asks of this surface.
14250
+ */
14251
+ deviceId: number().int().positive(),
14252
+ /**
14253
+ * A second dimension inside the family: the model / step id for an inference
14254
+ * timeout, so "which camera AND which model" is one read. Absent when the
14255
+ * family has a single variant.
14256
+ */
14257
+ variant: string().optional(),
14258
+ /**
14259
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14260
+ * differencing two reads must drop the interval when it changes, because the
14261
+ * counter restarted from zero in a respawned runner. Same discipline as
14262
+ * `LoadContribution.startedAtMs`.
14263
+ */
14264
+ sinceMs: number(),
14265
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14266
+ atMs: number(),
14267
+ /**
14268
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14269
+ * window. A failure count published without it is the mistake this schema
14270
+ * exists to make impossible.
14271
+ */
14272
+ attempts: number().int().nonnegative(),
14273
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14274
+ succeeded: number().int().nonnegative(),
14275
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14276
+ reasons: array(FailureReasonCountSchema).readonly()
14277
+ });
14278
+ method(_void(), array(FailureContributionSchema).readonly());
14279
+ /**
14125
14280
  * filesystem-browse — per-node capability for browsing the node's local
14126
14281
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14127
14282
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14643,6 +14798,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14643
14798
  kind: "mutation",
14644
14799
  auth: "admin"
14645
14800
  });
14801
+ var LoadContributionSchema = object({
14802
+ role: _enum([
14803
+ "decode",
14804
+ "transcode",
14805
+ "recording",
14806
+ "streaming",
14807
+ "detection"
14808
+ ]),
14809
+ /**
14810
+ * The NUMERIC device id — the same value every log line carries as
14811
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14812
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14813
+ * contributor that cannot name its camera must not emit the entry at all,
14814
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14815
+ * and would quietly turn one camera's cost into everybody's.
14816
+ */
14817
+ deviceId: number().int().positive().nullable(),
14818
+ attribution: _enum([
14819
+ "measured",
14820
+ "accounted",
14821
+ "unattributable"
14822
+ ]),
14823
+ /**
14824
+ * What ONE entry is, in the contributor's own words — `615/high`,
14825
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14826
+ * family and inventing a common one would lose the only information that
14827
+ * makes two entries for the same camera distinguishable.
14828
+ */
14829
+ unit: string(),
14830
+ /**
14831
+ * The OS process this cost lives in, when there is one. Present so a
14832
+ * consumer can (a) tell two generations of the same unit apart across a
14833
+ * restart, and (b) subtract claimed processes from the node's process
14834
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14835
+ * process of its own.
14836
+ */
14837
+ pid: number().int().positive().optional(),
14838
+ /**
14839
+ * When this generation started. The pid's incarnation marker: a consumer
14840
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14841
+ * window when this changes, because the counter restarted from zero in a new
14842
+ * process.
14843
+ */
14844
+ startedAtMs: number().optional(),
14845
+ /**
14846
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14847
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14848
+ * contribution is asked for.
14849
+ *
14850
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14851
+ * needs a sampler, and a new per-node sampler is the defect half of
14852
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14853
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14854
+ *
14855
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14856
+ * an entry with no process.
14857
+ */
14858
+ cpuSeconds: number().optional(),
14859
+ /** Resident bytes of this unit's process, same source and same rules. */
14860
+ rssBytes: number().optional()
14861
+ });
14862
+ method(_void(), array(LoadContributionSchema).readonly());
14646
14863
  /**
14647
14864
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14648
14865
  * through. It stores nothing.
@@ -14719,176 +14936,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14719
14936
  tags: record(string(), string()).optional()
14720
14937
  }), array(LogEntrySchema).readonly());
14721
14938
  /**
14722
- * `failure-contribution` — the capability an addon reports its OWN losses
14723
- * through, per camera, with the denominator attached. It stores nothing.
14724
- *
14725
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14726
- *
14727
- * `load-contribution` answers *what did this camera COST*. This answers *what
14728
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14729
- * copied: the contributor reports what it already knows, hub-main adds only
14730
- * `addonId`, nothing needs global knowledge, and there is no central list for
14731
- * somebody to forget to edit.
14732
- *
14733
- * They are not merged, because their invariants are opposites:
14734
- *
14735
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14736
- * claim a camera cost nothing, which is a measurement nobody made;
14737
- * - a `failure-contribution` zero is the **most valuable value on the
14738
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14739
- * and it is exactly what an absent entry cannot say.
14740
- *
14741
- * Putting a loss counter on a cost entry would also break the reconciliation
14742
- * that gives `load-contribution` its point: contributions are subtracted from
14743
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14744
- * has no process.
14745
- *
14746
- * ## Why not a log line, since the counters already exist
14747
- *
14748
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14749
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14750
- * ends in a log line, and a log line is the thing the operator asked to stop
14751
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14752
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14753
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14754
- * media blackout were both diagnosed. The counters stay; this is where they can
14755
- * be READ.
14756
- *
14757
- * ## The rate is served with its denominator or not at all
14758
- *
14759
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14760
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14761
- * than yesterday" and was **flat across twelve hours** once divided by the
14762
- * successes on the same path. A surface that publishes only the numerator
14763
- * reproduces that mistake on every read.
14764
- *
14765
- * ## Shape
14766
- *
14767
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14768
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14769
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14770
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14771
- * a forked runner's entries reach hub-main over transport that already exists.
14772
- * No new UDS message, no second registry (D3). The operator reads the assembled
14773
- * result through `system.getFailureContributions`.
14774
- */
14775
- var FailureReasonCountSchema = object({
14776
- /**
14777
- * Why the attempt did not land, in the contributor's own vocabulary —
14778
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14779
- * strings that already appear in this repo's logs and, where one exists, the
14780
- * same string the per-track `previewMissReason` records (D276): a second
14781
- * vocabulary for the same loss would make the row and the counter
14782
- * un-joinable.
14783
- */
14784
- reason: string(),
14785
- count: number().int().nonnegative()
14786
- });
14787
- var FailureContributionSchema = object({
14788
- /**
14789
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14790
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14791
- * `unit` free: the families are owned by different addons and a shared enum
14792
- * is a central list that rots invisibly.
14793
- */
14794
- family: string(),
14795
- /**
14796
- * The NUMERIC device id — the same value every log line carries as
14797
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14798
- * cannot name the camera must not emit the entry, because a fleet total
14799
- * cannot answer the only question anybody asks of this surface.
14800
- */
14801
- deviceId: number().int().positive(),
14802
- /**
14803
- * A second dimension inside the family: the model / step id for an inference
14804
- * timeout, so "which camera AND which model" is one read. Absent when the
14805
- * family has a single variant.
14806
- */
14807
- variant: string().optional(),
14808
- /**
14809
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14810
- * differencing two reads must drop the interval when it changes, because the
14811
- * counter restarted from zero in a respawned runner. Same discipline as
14812
- * `LoadContribution.startedAtMs`.
14813
- */
14814
- sinceMs: number(),
14815
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14816
- atMs: number(),
14817
- /**
14818
- * THE DENOMINATOR — every attempt on this path for this camera in the
14819
- * window. A failure count published without it is the mistake this schema
14820
- * exists to make impossible.
14821
- */
14822
- attempts: number().int().nonnegative(),
14823
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14824
- succeeded: number().int().nonnegative(),
14825
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14826
- reasons: array(FailureReasonCountSchema).readonly()
14827
- });
14828
- method(_void(), array(FailureContributionSchema).readonly());
14829
- var LoadContributionSchema = object({
14830
- role: _enum([
14831
- "decode",
14832
- "transcode",
14833
- "recording",
14834
- "streaming",
14835
- "detection"
14836
- ]),
14837
- /**
14838
- * The NUMERIC device id — the same value every log line carries as
14839
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14840
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14841
- * contributor that cannot name its camera must not emit the entry at all,
14842
- * because an unnamed per-camera entry is indistinguishable from a shared one
14843
- * and would quietly turn one camera's cost into everybody's.
14844
- */
14845
- deviceId: number().int().positive().nullable(),
14846
- attribution: _enum([
14847
- "measured",
14848
- "accounted",
14849
- "unattributable"
14850
- ]),
14851
- /**
14852
- * What ONE entry is, in the contributor's own words — `615/high`,
14853
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14854
- * family and inventing a common one would lose the only information that
14855
- * makes two entries for the same camera distinguishable.
14856
- */
14857
- unit: string(),
14858
- /**
14859
- * The OS process this cost lives in, when there is one. Present so a
14860
- * consumer can (a) tell two generations of the same unit apart across a
14861
- * restart, and (b) subtract claimed processes from the node's process
14862
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14863
- * process of its own.
14864
- */
14865
- pid: number().int().positive().optional(),
14866
- /**
14867
- * When this generation started. The pid's incarnation marker: a consumer
14868
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14869
- * window when this changes, because the counter restarted from zero in a new
14870
- * process.
14871
- */
14872
- startedAtMs: number().optional(),
14873
- /**
14874
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14875
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14876
- * contribution is asked for.
14877
- *
14878
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14879
- * needs a sampler, and a new per-node sampler is the defect half of
14880
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14881
- * by whoever already keeps a history; a rate cannot be un-averaged.
14882
- *
14883
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14884
- * an entry with no process.
14885
- */
14886
- cpuSeconds: number().optional(),
14887
- /** Resident bytes of this unit's process, same source and same rules. */
14888
- rssBytes: number().optional()
14889
- });
14890
- method(_void(), array(LoadContributionSchema).readonly());
14891
- /**
14892
14939
  * `login-method` — collection cap through which auth addons contribute
14893
14940
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14894
14941
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19626,12 +19673,53 @@ var MediaFileKindEnum = _enum([
19626
19673
  "keyFrameSmall",
19627
19674
  "thumbnailSmall"
19628
19675
  ]);
19676
+ /**
19677
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19678
+ * ARE — never the bytes themselves.
19679
+ *
19680
+ * ## Why `url` and not `base64`
19681
+ *
19682
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19683
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19684
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19685
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19686
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19687
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19688
+ *
19689
+ * `url` points at the `event-media` data plane
19690
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19691
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19692
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19693
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19694
+ * no less protected than they were inside a `view`-level cap response — see
19695
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19696
+ * (per-device scoping).
19697
+ *
19698
+ * The URL is built from the row's **stored** key, which is not always its
19699
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19700
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19701
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19702
+ *
19703
+ * ## `base64` is TRANSITIONAL and is going away
19704
+ *
19705
+ * It is still populated for one reason: the deployed viewer's track-detail
19706
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19707
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19708
+ * triangle — not as absence. Removing the field before that viewer ships is an
19709
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19710
+ * delete this line and the `withBytes` pass-through in
19711
+ * `analytics-query-facade.ts`; nothing else reads it.
19712
+ */
19629
19713
  var MediaFileSchema = object({
19630
19714
  key: string(),
19631
19715
  kind: MediaFileKindEnum,
19632
- base64: string(),
19633
19716
  sizeBytes: number(),
19634
19717
  timestamp: number()
19718
+ }).extend({
19719
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19720
+ url: string(),
19721
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19722
+ base64: string()
19635
19723
  });
19636
19724
  /**
19637
19725
  * One media row WITHOUT its bytes.
@@ -19643,7 +19731,9 @@ var MediaFileSchema = object({
19643
19731
  * blocks the whole view.
19644
19732
  *
19645
19733
  * `sizeBytes` is carried because it is what lets a client decide between the
19646
- * stored blob and a `?variant=thumb` rendering without fetching either.
19734
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19735
+ * `url` because a client that had to build the plane path itself is a second
19736
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19647
19737
  */
19648
19738
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19649
19739
  /**
@@ -20332,6 +20422,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20332
20422
  }), array(MediaFileSchema).readonly()), method(object({
20333
20423
  trackId: string(),
20334
20424
  deviceId: number()
20425
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20426
+ eventId: string(),
20427
+ deviceId: number()
20335
20428
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20336
20429
  kind: "mutation",
20337
20430
  auth: "admin"
@@ -25322,10 +25415,24 @@ var FaceClusterSchema = object({
25322
25415
  size: number().int(),
25323
25416
  cohesion: number()
25324
25417
  });
25418
+ /**
25419
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25420
+ * are — never the bytes.
25421
+ *
25422
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25423
+ * track/event contract) is still populated because a deployed viewer requires
25424
+ * the field to parse a row at all; this method has no such reader. Its ONE
25425
+ * caller is the admin UI's detail modal, which was building
25426
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25427
+ * dialog already rendering its key FRAME from the `event-media` plane.
25428
+ *
25429
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25430
+ * media key directly, so this needed no new plane and no new access decision.
25431
+ */
25325
25432
  var MediaFileLiteSchema$1 = object({
25326
25433
  key: string(),
25327
25434
  kind: string(),
25328
- base64: string(),
25435
+ url: string(),
25329
25436
  sizeBytes: number(),
25330
25437
  timestamp: number()
25331
25438
  });
@@ -28347,10 +28454,24 @@ var PlateInfoSchema = object({
28347
28454
  */
28348
28455
  cropUrl: string().optional()
28349
28456
  });
28457
+ /**
28458
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28459
+ * are — never the bytes.
28460
+ *
28461
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28462
+ * track/event contract) is still populated because a deployed viewer requires
28463
+ * the field to parse a row at all; this method has no such reader. Its ONE
28464
+ * caller is the admin UI's detail modal, which was building
28465
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28466
+ * dialog already rendering its key FRAME from the `event-media` plane.
28467
+ *
28468
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28469
+ * media key directly, so this needed no new plane and no new access decision.
28470
+ */
28350
28471
  var MediaFileLiteSchema = object({
28351
28472
  key: string(),
28352
28473
  kind: string(),
28353
- base64: string(),
28474
+ url: string(),
28354
28475
  sizeBytes: number(),
28355
28476
  timestamp: number()
28356
28477
  });
@@ -36247,6 +36368,12 @@ Object.freeze({
36247
36368
  addonId: null,
36248
36369
  access: "view"
36249
36370
  },
36371
+ "pipelineAnalytics.listEventMedia": {
36372
+ capName: "pipeline-analytics",
36373
+ capScope: "device",
36374
+ addonId: null,
36375
+ access: "view"
36376
+ },
36250
36377
  "pipelineAnalytics.listGroups": {
36251
36378
  capName: "pipeline-analytics",
36252
36379
  capScope: "device",
@@ -39870,6 +39997,11 @@ Object.freeze({
39870
39997
  form: "array",
39871
39998
  optional: false
39872
39999
  }],
40000
+ "pipelineAnalytics.listEventMedia": [{
40001
+ name: "deviceId",
40002
+ form: "single",
40003
+ optional: false
40004
+ }],
39873
40005
  "pipelineAnalytics.listGroups": [{
39874
40006
  name: "deviceIds",
39875
40007
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8987,6 +8987,21 @@ var RelocateJobSchema = object({
8987
8987
  bytesMoved: number().int(),
8988
8988
  /** Total files discovered up front; null while (or when) unknown. */
8989
8989
  filesTotal: number().int().nullable(),
8990
+ /**
8991
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8992
+ * made that nobody asked for, so it is reported where the operator reads the
8993
+ * job rather than only in a log line.
8994
+ *
8995
+ * A footage segment records its byte count in its own NAME, and the durable
8996
+ * hour row derives its aggregates from those names. A file that does not
8997
+ * match its name therefore makes the ledger's sums — and with them quota and
8998
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8999
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9000
+ *
9001
+ * Absent on lanes where the question has no meaning: a media blob's size is
9002
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9003
+ */
9004
+ rowsReconciled: number().int().nonnegative().optional(),
8990
9005
  startedAt: number(),
8991
9006
  finishedAt: number().nullable(),
8992
9007
  error: string().nullable()
@@ -9055,14 +9070,42 @@ var RelocateMediaInputSchema = object({
9055
9070
  /** Omitted = `move`, the pre-existing behaviour. */
9056
9071
  mode: MediaRelocateModeSchema.optional()
9057
9072
  });
9058
- /** How many rows still carry NO `locationId` — the population a repoint would
9059
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
9060
- * value that permits a non-blocking `eventMedia` cutover. */
9061
- var UnstampedEventMediaCountSchema = object({
9062
- media: number().int().nonnegative(),
9063
- retrainFrames: number().int().nonnegative(),
9064
- total: number().int().nonnegative()
9073
+ /**
9074
+ * The unstamped population of ONE collection split, because the gate and the
9075
+ * operator ask two different questions and only one of them has to be cheap.
9076
+ *
9077
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9078
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9079
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9080
+ * that matters — after a seal, when the population is empty.
9081
+ *
9082
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9083
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9084
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9085
+ * and useful answer: "there are some, and this read could not say how many"
9086
+ * still refuses the cutover, which is the whole job.
9087
+ */
9088
+ var UnstampedRowsSchema = object({
9089
+ present: boolean(),
9090
+ rows: number().int().nonnegative().nullable()
9065
9091
  });
9092
+ /**
9093
+ * How many rows still carry NO `locationId` — the population a repoint would
9094
+ * silently re-aim at a disk that does not hold their bytes.
9095
+ *
9096
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9097
+ * over. The gate opens on a measured absence and on nothing else; an unread
9098
+ * collection and an empty one are different facts, and this repo has already
9099
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9100
+ */
9101
+ var UnstampedEventMediaCountSchema = object({
9102
+ media: UnstampedRowsSchema,
9103
+ retrainFrames: UnstampedRowsSchema,
9104
+ /** True when EITHER collection holds one. The refusal reads this. */
9105
+ anyPresent: boolean(),
9106
+ /** Sum across both, or `null` when either lane could not be counted. */
9107
+ total: number().int().nonnegative().nullable()
9108
+ }).nullable();
9066
9109
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9067
9110
  /** The independently selectable logical storage classes — every class
9068
9111
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9173,6 +9216,10 @@ var StorageMigrationMoveProgressSchema = object({
9173
9216
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9174
9217
  filesTotal: number().int().nonnegative().nullable(),
9175
9218
  bytesMoved: number().int().nonnegative(),
9219
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9220
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9221
+ * rows would be the same failure as one that silently skipped them. */
9222
+ rowsReconciled: number().int().nonnegative().optional(),
9176
9223
  /** The MOVER's start, not the migration's: a drain restarted after an addon
9177
9224
  * crash gets a new mover, and a rate computed from the migration's start
9178
9225
  * would silently average in the time nothing was running. */
@@ -14121,6 +14168,114 @@ method(object({
14121
14168
  height: number()
14122
14169
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14123
14170
  /**
14171
+ * `failure-contribution` — the capability an addon reports its OWN losses
14172
+ * through, per camera, with the denominator attached. It stores nothing.
14173
+ *
14174
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14175
+ *
14176
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14177
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14178
+ * copied: the contributor reports what it already knows, hub-main adds only
14179
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14180
+ * somebody to forget to edit.
14181
+ *
14182
+ * They are not merged, because their invariants are opposites:
14183
+ *
14184
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14185
+ * claim a camera cost nothing, which is a measurement nobody made;
14186
+ * - a `failure-contribution` zero is the **most valuable value on the
14187
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14188
+ * and it is exactly what an absent entry cannot say.
14189
+ *
14190
+ * Putting a loss counter on a cost entry would also break the reconciliation
14191
+ * that gives `load-contribution` its point: contributions are subtracted from
14192
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14193
+ * has no process.
14194
+ *
14195
+ * ## Why not a log line, since the counters already exist
14196
+ *
14197
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14198
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14199
+ * ends in a log line, and a log line is the thing the operator asked to stop
14200
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14201
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14202
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14203
+ * media blackout were both diagnosed. The counters stay; this is where they can
14204
+ * be READ.
14205
+ *
14206
+ * ## The rate is served with its denominator or not at all
14207
+ *
14208
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14209
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14210
+ * than yesterday" and was **flat across twelve hours** once divided by the
14211
+ * successes on the same path. A surface that publishes only the numerator
14212
+ * reproduces that mistake on every read.
14213
+ *
14214
+ * ## Shape
14215
+ *
14216
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14217
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14218
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14219
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14220
+ * a forked runner's entries reach hub-main over transport that already exists.
14221
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14222
+ * result through `system.getFailureContributions`.
14223
+ */
14224
+ var FailureReasonCountSchema = object({
14225
+ /**
14226
+ * Why the attempt did not land, in the contributor's own vocabulary —
14227
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14228
+ * strings that already appear in this repo's logs and, where one exists, the
14229
+ * same string the per-track `previewMissReason` records (D276): a second
14230
+ * vocabulary for the same loss would make the row and the counter
14231
+ * un-joinable.
14232
+ */
14233
+ reason: string(),
14234
+ count: number().int().nonnegative()
14235
+ });
14236
+ var FailureContributionSchema = object({
14237
+ /**
14238
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14239
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14240
+ * `unit` free: the families are owned by different addons and a shared enum
14241
+ * is a central list that rots invisibly.
14242
+ */
14243
+ family: string(),
14244
+ /**
14245
+ * The NUMERIC device id — the same value every log line carries as
14246
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14247
+ * cannot name the camera must not emit the entry, because a fleet total
14248
+ * cannot answer the only question anybody asks of this surface.
14249
+ */
14250
+ deviceId: number().int().positive(),
14251
+ /**
14252
+ * A second dimension inside the family: the model / step id for an inference
14253
+ * timeout, so "which camera AND which model" is one read. Absent when the
14254
+ * family has a single variant.
14255
+ */
14256
+ variant: string().optional(),
14257
+ /**
14258
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14259
+ * differencing two reads must drop the interval when it changes, because the
14260
+ * counter restarted from zero in a respawned runner. Same discipline as
14261
+ * `LoadContribution.startedAtMs`.
14262
+ */
14263
+ sinceMs: number(),
14264
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14265
+ atMs: number(),
14266
+ /**
14267
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14268
+ * window. A failure count published without it is the mistake this schema
14269
+ * exists to make impossible.
14270
+ */
14271
+ attempts: number().int().nonnegative(),
14272
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14273
+ succeeded: number().int().nonnegative(),
14274
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14275
+ reasons: array(FailureReasonCountSchema).readonly()
14276
+ });
14277
+ method(_void(), array(FailureContributionSchema).readonly());
14278
+ /**
14124
14279
  * filesystem-browse — per-node capability for browsing the node's local
14125
14280
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14126
14281
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14642,6 +14797,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14642
14797
  kind: "mutation",
14643
14798
  auth: "admin"
14644
14799
  });
14800
+ var LoadContributionSchema = object({
14801
+ role: _enum([
14802
+ "decode",
14803
+ "transcode",
14804
+ "recording",
14805
+ "streaming",
14806
+ "detection"
14807
+ ]),
14808
+ /**
14809
+ * The NUMERIC device id — the same value every log line carries as
14810
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14811
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14812
+ * contributor that cannot name its camera must not emit the entry at all,
14813
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14814
+ * and would quietly turn one camera's cost into everybody's.
14815
+ */
14816
+ deviceId: number().int().positive().nullable(),
14817
+ attribution: _enum([
14818
+ "measured",
14819
+ "accounted",
14820
+ "unattributable"
14821
+ ]),
14822
+ /**
14823
+ * What ONE entry is, in the contributor's own words — `615/high`,
14824
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14825
+ * family and inventing a common one would lose the only information that
14826
+ * makes two entries for the same camera distinguishable.
14827
+ */
14828
+ unit: string(),
14829
+ /**
14830
+ * The OS process this cost lives in, when there is one. Present so a
14831
+ * consumer can (a) tell two generations of the same unit apart across a
14832
+ * restart, and (b) subtract claimed processes from the node's process
14833
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14834
+ * process of its own.
14835
+ */
14836
+ pid: number().int().positive().optional(),
14837
+ /**
14838
+ * When this generation started. The pid's incarnation marker: a consumer
14839
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14840
+ * window when this changes, because the counter restarted from zero in a new
14841
+ * process.
14842
+ */
14843
+ startedAtMs: number().optional(),
14844
+ /**
14845
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14846
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14847
+ * contribution is asked for.
14848
+ *
14849
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14850
+ * needs a sampler, and a new per-node sampler is the defect half of
14851
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14852
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14853
+ *
14854
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14855
+ * an entry with no process.
14856
+ */
14857
+ cpuSeconds: number().optional(),
14858
+ /** Resident bytes of this unit's process, same source and same rules. */
14859
+ rssBytes: number().optional()
14860
+ });
14861
+ method(_void(), array(LoadContributionSchema).readonly());
14645
14862
  /**
14646
14863
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14647
14864
  * through. It stores nothing.
@@ -14718,176 +14935,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14718
14935
  tags: record(string(), string()).optional()
14719
14936
  }), array(LogEntrySchema).readonly());
14720
14937
  /**
14721
- * `failure-contribution` — the capability an addon reports its OWN losses
14722
- * through, per camera, with the denominator attached. It stores nothing.
14723
- *
14724
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14725
- *
14726
- * `load-contribution` answers *what did this camera COST*. This answers *what
14727
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14728
- * copied: the contributor reports what it already knows, hub-main adds only
14729
- * `addonId`, nothing needs global knowledge, and there is no central list for
14730
- * somebody to forget to edit.
14731
- *
14732
- * They are not merged, because their invariants are opposites:
14733
- *
14734
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14735
- * claim a camera cost nothing, which is a measurement nobody made;
14736
- * - a `failure-contribution` zero is the **most valuable value on the
14737
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14738
- * and it is exactly what an absent entry cannot say.
14739
- *
14740
- * Putting a loss counter on a cost entry would also break the reconciliation
14741
- * that gives `load-contribution` its point: contributions are subtracted from
14742
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14743
- * has no process.
14744
- *
14745
- * ## Why not a log line, since the counters already exist
14746
- *
14747
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14748
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14749
- * ends in a log line, and a log line is the thing the operator asked to stop
14750
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14751
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14752
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14753
- * media blackout were both diagnosed. The counters stay; this is where they can
14754
- * be READ.
14755
- *
14756
- * ## The rate is served with its denominator or not at all
14757
- *
14758
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14759
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14760
- * than yesterday" and was **flat across twelve hours** once divided by the
14761
- * successes on the same path. A surface that publishes only the numerator
14762
- * reproduces that mistake on every read.
14763
- *
14764
- * ## Shape
14765
- *
14766
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14767
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14768
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14769
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14770
- * a forked runner's entries reach hub-main over transport that already exists.
14771
- * No new UDS message, no second registry (D3). The operator reads the assembled
14772
- * result through `system.getFailureContributions`.
14773
- */
14774
- var FailureReasonCountSchema = object({
14775
- /**
14776
- * Why the attempt did not land, in the contributor's own vocabulary —
14777
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14778
- * strings that already appear in this repo's logs and, where one exists, the
14779
- * same string the per-track `previewMissReason` records (D276): a second
14780
- * vocabulary for the same loss would make the row and the counter
14781
- * un-joinable.
14782
- */
14783
- reason: string(),
14784
- count: number().int().nonnegative()
14785
- });
14786
- var FailureContributionSchema = object({
14787
- /**
14788
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14789
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14790
- * `unit` free: the families are owned by different addons and a shared enum
14791
- * is a central list that rots invisibly.
14792
- */
14793
- family: string(),
14794
- /**
14795
- * The NUMERIC device id — the same value every log line carries as
14796
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14797
- * cannot name the camera must not emit the entry, because a fleet total
14798
- * cannot answer the only question anybody asks of this surface.
14799
- */
14800
- deviceId: number().int().positive(),
14801
- /**
14802
- * A second dimension inside the family: the model / step id for an inference
14803
- * timeout, so "which camera AND which model" is one read. Absent when the
14804
- * family has a single variant.
14805
- */
14806
- variant: string().optional(),
14807
- /**
14808
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14809
- * differencing two reads must drop the interval when it changes, because the
14810
- * counter restarted from zero in a respawned runner. Same discipline as
14811
- * `LoadContribution.startedAtMs`.
14812
- */
14813
- sinceMs: number(),
14814
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14815
- atMs: number(),
14816
- /**
14817
- * THE DENOMINATOR — every attempt on this path for this camera in the
14818
- * window. A failure count published without it is the mistake this schema
14819
- * exists to make impossible.
14820
- */
14821
- attempts: number().int().nonnegative(),
14822
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14823
- succeeded: number().int().nonnegative(),
14824
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14825
- reasons: array(FailureReasonCountSchema).readonly()
14826
- });
14827
- method(_void(), array(FailureContributionSchema).readonly());
14828
- var LoadContributionSchema = object({
14829
- role: _enum([
14830
- "decode",
14831
- "transcode",
14832
- "recording",
14833
- "streaming",
14834
- "detection"
14835
- ]),
14836
- /**
14837
- * The NUMERIC device id — the same value every log line carries as
14838
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14839
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14840
- * contributor that cannot name its camera must not emit the entry at all,
14841
- * because an unnamed per-camera entry is indistinguishable from a shared one
14842
- * and would quietly turn one camera's cost into everybody's.
14843
- */
14844
- deviceId: number().int().positive().nullable(),
14845
- attribution: _enum([
14846
- "measured",
14847
- "accounted",
14848
- "unattributable"
14849
- ]),
14850
- /**
14851
- * What ONE entry is, in the contributor's own words — `615/high`,
14852
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14853
- * family and inventing a common one would lose the only information that
14854
- * makes two entries for the same camera distinguishable.
14855
- */
14856
- unit: string(),
14857
- /**
14858
- * The OS process this cost lives in, when there is one. Present so a
14859
- * consumer can (a) tell two generations of the same unit apart across a
14860
- * restart, and (b) subtract claimed processes from the node's process
14861
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14862
- * process of its own.
14863
- */
14864
- pid: number().int().positive().optional(),
14865
- /**
14866
- * When this generation started. The pid's incarnation marker: a consumer
14867
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14868
- * window when this changes, because the counter restarted from zero in a new
14869
- * process.
14870
- */
14871
- startedAtMs: number().optional(),
14872
- /**
14873
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14874
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14875
- * contribution is asked for.
14876
- *
14877
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14878
- * needs a sampler, and a new per-node sampler is the defect half of
14879
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14880
- * by whoever already keeps a history; a rate cannot be un-averaged.
14881
- *
14882
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14883
- * an entry with no process.
14884
- */
14885
- cpuSeconds: number().optional(),
14886
- /** Resident bytes of this unit's process, same source and same rules. */
14887
- rssBytes: number().optional()
14888
- });
14889
- method(_void(), array(LoadContributionSchema).readonly());
14890
- /**
14891
14938
  * `login-method` — collection cap through which auth addons contribute
14892
14939
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14893
14940
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19625,12 +19672,53 @@ var MediaFileKindEnum = _enum([
19625
19672
  "keyFrameSmall",
19626
19673
  "thumbnailSmall"
19627
19674
  ]);
19675
+ /**
19676
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19677
+ * ARE — never the bytes themselves.
19678
+ *
19679
+ * ## Why `url` and not `base64`
19680
+ *
19681
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19682
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19683
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19684
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19685
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19686
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19687
+ *
19688
+ * `url` points at the `event-media` data plane
19689
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19690
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19691
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19692
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19693
+ * no less protected than they were inside a `view`-level cap response — see
19694
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19695
+ * (per-device scoping).
19696
+ *
19697
+ * The URL is built from the row's **stored** key, which is not always its
19698
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19699
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19700
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19701
+ *
19702
+ * ## `base64` is TRANSITIONAL and is going away
19703
+ *
19704
+ * It is still populated for one reason: the deployed viewer's track-detail
19705
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19706
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19707
+ * triangle — not as absence. Removing the field before that viewer ships is an
19708
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19709
+ * delete this line and the `withBytes` pass-through in
19710
+ * `analytics-query-facade.ts`; nothing else reads it.
19711
+ */
19628
19712
  var MediaFileSchema = object({
19629
19713
  key: string(),
19630
19714
  kind: MediaFileKindEnum,
19631
- base64: string(),
19632
19715
  sizeBytes: number(),
19633
19716
  timestamp: number()
19717
+ }).extend({
19718
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19719
+ url: string(),
19720
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19721
+ base64: string()
19634
19722
  });
19635
19723
  /**
19636
19724
  * One media row WITHOUT its bytes.
@@ -19642,7 +19730,9 @@ var MediaFileSchema = object({
19642
19730
  * blocks the whole view.
19643
19731
  *
19644
19732
  * `sizeBytes` is carried because it is what lets a client decide between the
19645
- * stored blob and a `?variant=thumb` rendering without fetching either.
19733
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19734
+ * `url` because a client that had to build the plane path itself is a second
19735
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19646
19736
  */
19647
19737
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19648
19738
  /**
@@ -20331,6 +20421,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20331
20421
  }), array(MediaFileSchema).readonly()), method(object({
20332
20422
  trackId: string(),
20333
20423
  deviceId: number()
20424
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20425
+ eventId: string(),
20426
+ deviceId: number()
20334
20427
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20335
20428
  kind: "mutation",
20336
20429
  auth: "admin"
@@ -25321,10 +25414,24 @@ var FaceClusterSchema = object({
25321
25414
  size: number().int(),
25322
25415
  cohesion: number()
25323
25416
  });
25417
+ /**
25418
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25419
+ * are — never the bytes.
25420
+ *
25421
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25422
+ * track/event contract) is still populated because a deployed viewer requires
25423
+ * the field to parse a row at all; this method has no such reader. Its ONE
25424
+ * caller is the admin UI's detail modal, which was building
25425
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25426
+ * dialog already rendering its key FRAME from the `event-media` plane.
25427
+ *
25428
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25429
+ * media key directly, so this needed no new plane and no new access decision.
25430
+ */
25324
25431
  var MediaFileLiteSchema$1 = object({
25325
25432
  key: string(),
25326
25433
  kind: string(),
25327
- base64: string(),
25434
+ url: string(),
25328
25435
  sizeBytes: number(),
25329
25436
  timestamp: number()
25330
25437
  });
@@ -28346,10 +28453,24 @@ var PlateInfoSchema = object({
28346
28453
  */
28347
28454
  cropUrl: string().optional()
28348
28455
  });
28456
+ /**
28457
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28458
+ * are — never the bytes.
28459
+ *
28460
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28461
+ * track/event contract) is still populated because a deployed viewer requires
28462
+ * the field to parse a row at all; this method has no such reader. Its ONE
28463
+ * caller is the admin UI's detail modal, which was building
28464
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28465
+ * dialog already rendering its key FRAME from the `event-media` plane.
28466
+ *
28467
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28468
+ * media key directly, so this needed no new plane and no new access decision.
28469
+ */
28349
28470
  var MediaFileLiteSchema = object({
28350
28471
  key: string(),
28351
28472
  kind: string(),
28352
- base64: string(),
28473
+ url: string(),
28353
28474
  sizeBytes: number(),
28354
28475
  timestamp: number()
28355
28476
  });
@@ -36246,6 +36367,12 @@ Object.freeze({
36246
36367
  addonId: null,
36247
36368
  access: "view"
36248
36369
  },
36370
+ "pipelineAnalytics.listEventMedia": {
36371
+ capName: "pipeline-analytics",
36372
+ capScope: "device",
36373
+ addonId: null,
36374
+ access: "view"
36375
+ },
36249
36376
  "pipelineAnalytics.listGroups": {
36250
36377
  capName: "pipeline-analytics",
36251
36378
  capScope: "device",
@@ -39869,6 +39996,11 @@ Object.freeze({
39869
39996
  form: "array",
39870
39997
  optional: false
39871
39998
  }],
39999
+ "pipelineAnalytics.listEventMedia": [{
40000
+ name: "deviceId",
40001
+ form: "single",
40002
+ optional: false
40003
+ }],
39872
40004
  "pipelineAnalytics.listGroups": [{
39873
40005
  name: "deviceIds",
39874
40006
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rademacher",
3
- "version": "0.2.44",
3
+ "version": "0.2.46",
4
4
  "description": "Rademacher HomePilot device-provider addon for CamStack — wraps the @apocaliss92/noderademacher local-hub client (roller shutters over the cover cap)",
5
5
  "keywords": [
6
6
  "camstack",