@camstack/addon-provider-amcrest 0.2.46 → 0.2.48

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
@@ -7996,6 +7996,21 @@ var RelocateJobSchema = object({
7996
7996
  bytesMoved: number().int(),
7997
7997
  /** Total files discovered up front; null while (or when) unknown. */
7998
7998
  filesTotal: number().int().nullable(),
7999
+ /**
8000
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8001
+ * made that nobody asked for, so it is reported where the operator reads the
8002
+ * job rather than only in a log line.
8003
+ *
8004
+ * A footage segment records its byte count in its own NAME, and the durable
8005
+ * hour row derives its aggregates from those names. A file that does not
8006
+ * match its name therefore makes the ledger's sums — and with them quota and
8007
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8008
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8009
+ *
8010
+ * Absent on lanes where the question has no meaning: a media blob's size is
8011
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8012
+ */
8013
+ rowsReconciled: number().int().nonnegative().optional(),
7999
8014
  startedAt: number(),
8000
8015
  finishedAt: number().nullable(),
8001
8016
  error: string().nullable()
@@ -8064,14 +8079,42 @@ var RelocateMediaInputSchema = object({
8064
8079
  /** Omitted = `move`, the pre-existing behaviour. */
8065
8080
  mode: MediaRelocateModeSchema.optional()
8066
8081
  });
8067
- /** How many rows still carry NO `locationId` — the population a repoint would
8068
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8069
- * value that permits a non-blocking `eventMedia` cutover. */
8070
- var UnstampedEventMediaCountSchema = object({
8071
- media: number().int().nonnegative(),
8072
- retrainFrames: number().int().nonnegative(),
8073
- total: number().int().nonnegative()
8082
+ /**
8083
+ * The unstamped population of ONE collection split, because the gate and the
8084
+ * operator ask two different questions and only one of them has to be cheap.
8085
+ *
8086
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8087
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8088
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8089
+ * that matters — after a seal, when the population is empty.
8090
+ *
8091
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8092
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8093
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8094
+ * and useful answer: "there are some, and this read could not say how many"
8095
+ * still refuses the cutover, which is the whole job.
8096
+ */
8097
+ var UnstampedRowsSchema = object({
8098
+ present: boolean(),
8099
+ rows: number().int().nonnegative().nullable()
8074
8100
  });
8101
+ /**
8102
+ * How many rows still carry NO `locationId` — the population a repoint would
8103
+ * silently re-aim at a disk that does not hold their bytes.
8104
+ *
8105
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8106
+ * over. The gate opens on a measured absence and on nothing else; an unread
8107
+ * collection and an empty one are different facts, and this repo has already
8108
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8109
+ */
8110
+ var UnstampedEventMediaCountSchema = object({
8111
+ media: UnstampedRowsSchema,
8112
+ retrainFrames: UnstampedRowsSchema,
8113
+ /** True when EITHER collection holds one. The refusal reads this. */
8114
+ anyPresent: boolean(),
8115
+ /** Sum across both, or `null` when either lane could not be counted. */
8116
+ total: number().int().nonnegative().nullable()
8117
+ }).nullable();
8075
8118
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8076
8119
  /** The independently selectable logical storage classes — every class
8077
8120
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8182,6 +8225,10 @@ var StorageMigrationMoveProgressSchema = object({
8182
8225
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8183
8226
  filesTotal: number().int().nonnegative().nullable(),
8184
8227
  bytesMoved: number().int().nonnegative(),
8228
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8229
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8230
+ * rows would be the same failure as one that silently skipped them. */
8231
+ rowsReconciled: number().int().nonnegative().optional(),
8185
8232
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8186
8233
  * crash gets a new mover, and a rate computed from the migration's start
8187
8234
  * would silently average in the time nothing was running. */
@@ -13130,6 +13177,114 @@ method(object({
13130
13177
  height: number()
13131
13178
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13132
13179
  /**
13180
+ * `failure-contribution` — the capability an addon reports its OWN losses
13181
+ * through, per camera, with the denominator attached. It stores nothing.
13182
+ *
13183
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13184
+ *
13185
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13186
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13187
+ * copied: the contributor reports what it already knows, hub-main adds only
13188
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13189
+ * somebody to forget to edit.
13190
+ *
13191
+ * They are not merged, because their invariants are opposites:
13192
+ *
13193
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13194
+ * claim a camera cost nothing, which is a measurement nobody made;
13195
+ * - a `failure-contribution` zero is the **most valuable value on the
13196
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13197
+ * and it is exactly what an absent entry cannot say.
13198
+ *
13199
+ * Putting a loss counter on a cost entry would also break the reconciliation
13200
+ * that gives `load-contribution` its point: contributions are subtracted from
13201
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13202
+ * has no process.
13203
+ *
13204
+ * ## Why not a log line, since the counters already exist
13205
+ *
13206
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13207
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13208
+ * ends in a log line, and a log line is the thing the operator asked to stop
13209
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13210
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13211
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13212
+ * media blackout were both diagnosed. The counters stay; this is where they can
13213
+ * be READ.
13214
+ *
13215
+ * ## The rate is served with its denominator or not at all
13216
+ *
13217
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13218
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13219
+ * than yesterday" and was **flat across twelve hours** once divided by the
13220
+ * successes on the same path. A surface that publishes only the numerator
13221
+ * reproduces that mistake on every read.
13222
+ *
13223
+ * ## Shape
13224
+ *
13225
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13226
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13227
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13228
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13229
+ * a forked runner's entries reach hub-main over transport that already exists.
13230
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13231
+ * result through `system.getFailureContributions`.
13232
+ */
13233
+ var FailureReasonCountSchema = object({
13234
+ /**
13235
+ * Why the attempt did not land, in the contributor's own vocabulary —
13236
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13237
+ * strings that already appear in this repo's logs and, where one exists, the
13238
+ * same string the per-track `previewMissReason` records (D276): a second
13239
+ * vocabulary for the same loss would make the row and the counter
13240
+ * un-joinable.
13241
+ */
13242
+ reason: string(),
13243
+ count: number().int().nonnegative()
13244
+ });
13245
+ var FailureContributionSchema = object({
13246
+ /**
13247
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13248
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13249
+ * `unit` free: the families are owned by different addons and a shared enum
13250
+ * is a central list that rots invisibly.
13251
+ */
13252
+ family: string(),
13253
+ /**
13254
+ * The NUMERIC device id — the same value every log line carries as
13255
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13256
+ * cannot name the camera must not emit the entry, because a fleet total
13257
+ * cannot answer the only question anybody asks of this surface.
13258
+ */
13259
+ deviceId: number().int().positive(),
13260
+ /**
13261
+ * A second dimension inside the family: the model / step id for an inference
13262
+ * timeout, so "which camera AND which model" is one read. Absent when the
13263
+ * family has a single variant.
13264
+ */
13265
+ variant: string().optional(),
13266
+ /**
13267
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13268
+ * differencing two reads must drop the interval when it changes, because the
13269
+ * counter restarted from zero in a respawned runner. Same discipline as
13270
+ * `LoadContribution.startedAtMs`.
13271
+ */
13272
+ sinceMs: number(),
13273
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13274
+ atMs: number(),
13275
+ /**
13276
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13277
+ * window. A failure count published without it is the mistake this schema
13278
+ * exists to make impossible.
13279
+ */
13280
+ attempts: number().int().nonnegative(),
13281
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13282
+ succeeded: number().int().nonnegative(),
13283
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13284
+ reasons: array(FailureReasonCountSchema).readonly()
13285
+ });
13286
+ method(_void(), array(FailureContributionSchema).readonly());
13287
+ /**
13133
13288
  * filesystem-browse — per-node capability for browsing the node's local
13134
13289
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13135
13290
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13651,6 +13806,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13651
13806
  kind: "mutation",
13652
13807
  auth: "admin"
13653
13808
  });
13809
+ var LoadContributionSchema = object({
13810
+ role: _enum([
13811
+ "decode",
13812
+ "transcode",
13813
+ "recording",
13814
+ "streaming",
13815
+ "detection"
13816
+ ]),
13817
+ /**
13818
+ * The NUMERIC device id — the same value every log line carries as
13819
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13820
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13821
+ * contributor that cannot name its camera must not emit the entry at all,
13822
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13823
+ * and would quietly turn one camera's cost into everybody's.
13824
+ */
13825
+ deviceId: number().int().positive().nullable(),
13826
+ attribution: _enum([
13827
+ "measured",
13828
+ "accounted",
13829
+ "unattributable"
13830
+ ]),
13831
+ /**
13832
+ * What ONE entry is, in the contributor's own words — `615/high`,
13833
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13834
+ * family and inventing a common one would lose the only information that
13835
+ * makes two entries for the same camera distinguishable.
13836
+ */
13837
+ unit: string(),
13838
+ /**
13839
+ * The OS process this cost lives in, when there is one. Present so a
13840
+ * consumer can (a) tell two generations of the same unit apart across a
13841
+ * restart, and (b) subtract claimed processes from the node's process
13842
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13843
+ * process of its own.
13844
+ */
13845
+ pid: number().int().positive().optional(),
13846
+ /**
13847
+ * When this generation started. The pid's incarnation marker: a consumer
13848
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13849
+ * window when this changes, because the counter restarted from zero in a new
13850
+ * process.
13851
+ */
13852
+ startedAtMs: number().optional(),
13853
+ /**
13854
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13855
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13856
+ * contribution is asked for.
13857
+ *
13858
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13859
+ * needs a sampler, and a new per-node sampler is the defect half of
13860
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13861
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13862
+ *
13863
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13864
+ * an entry with no process.
13865
+ */
13866
+ cpuSeconds: number().optional(),
13867
+ /** Resident bytes of this unit's process, same source and same rules. */
13868
+ rssBytes: number().optional()
13869
+ });
13870
+ method(_void(), array(LoadContributionSchema).readonly());
13654
13871
  /**
13655
13872
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13656
13873
  * through. It stores nothing.
@@ -13727,176 +13944,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13727
13944
  tags: record(string(), string()).optional()
13728
13945
  }), array(LogEntrySchema).readonly());
13729
13946
  /**
13730
- * `failure-contribution` — the capability an addon reports its OWN losses
13731
- * through, per camera, with the denominator attached. It stores nothing.
13732
- *
13733
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13734
- *
13735
- * `load-contribution` answers *what did this camera COST*. This answers *what
13736
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13737
- * copied: the contributor reports what it already knows, hub-main adds only
13738
- * `addonId`, nothing needs global knowledge, and there is no central list for
13739
- * somebody to forget to edit.
13740
- *
13741
- * They are not merged, because their invariants are opposites:
13742
- *
13743
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13744
- * claim a camera cost nothing, which is a measurement nobody made;
13745
- * - a `failure-contribution` zero is the **most valuable value on the
13746
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13747
- * and it is exactly what an absent entry cannot say.
13748
- *
13749
- * Putting a loss counter on a cost entry would also break the reconciliation
13750
- * that gives `load-contribution` its point: contributions are subtracted from
13751
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13752
- * has no process.
13753
- *
13754
- * ## Why not a log line, since the counters already exist
13755
- *
13756
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13757
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13758
- * ends in a log line, and a log line is the thing the operator asked to stop
13759
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13760
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13761
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13762
- * media blackout were both diagnosed. The counters stay; this is where they can
13763
- * be READ.
13764
- *
13765
- * ## The rate is served with its denominator or not at all
13766
- *
13767
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13768
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13769
- * than yesterday" and was **flat across twelve hours** once divided by the
13770
- * successes on the same path. A surface that publishes only the numerator
13771
- * reproduces that mistake on every read.
13772
- *
13773
- * ## Shape
13774
- *
13775
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13776
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13777
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13778
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13779
- * a forked runner's entries reach hub-main over transport that already exists.
13780
- * No new UDS message, no second registry (D3). The operator reads the assembled
13781
- * result through `system.getFailureContributions`.
13782
- */
13783
- var FailureReasonCountSchema = object({
13784
- /**
13785
- * Why the attempt did not land, in the contributor's own vocabulary —
13786
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13787
- * strings that already appear in this repo's logs and, where one exists, the
13788
- * same string the per-track `previewMissReason` records (D276): a second
13789
- * vocabulary for the same loss would make the row and the counter
13790
- * un-joinable.
13791
- */
13792
- reason: string(),
13793
- count: number().int().nonnegative()
13794
- });
13795
- var FailureContributionSchema = object({
13796
- /**
13797
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13798
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13799
- * `unit` free: the families are owned by different addons and a shared enum
13800
- * is a central list that rots invisibly.
13801
- */
13802
- family: string(),
13803
- /**
13804
- * The NUMERIC device id — the same value every log line carries as
13805
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13806
- * cannot name the camera must not emit the entry, because a fleet total
13807
- * cannot answer the only question anybody asks of this surface.
13808
- */
13809
- deviceId: number().int().positive(),
13810
- /**
13811
- * A second dimension inside the family: the model / step id for an inference
13812
- * timeout, so "which camera AND which model" is one read. Absent when the
13813
- * family has a single variant.
13814
- */
13815
- variant: string().optional(),
13816
- /**
13817
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13818
- * differencing two reads must drop the interval when it changes, because the
13819
- * counter restarted from zero in a respawned runner. Same discipline as
13820
- * `LoadContribution.startedAtMs`.
13821
- */
13822
- sinceMs: number(),
13823
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13824
- atMs: number(),
13825
- /**
13826
- * THE DENOMINATOR — every attempt on this path for this camera in the
13827
- * window. A failure count published without it is the mistake this schema
13828
- * exists to make impossible.
13829
- */
13830
- attempts: number().int().nonnegative(),
13831
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13832
- succeeded: number().int().nonnegative(),
13833
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13834
- reasons: array(FailureReasonCountSchema).readonly()
13835
- });
13836
- method(_void(), array(FailureContributionSchema).readonly());
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());
13899
- /**
13900
13947
  * `login-method` — collection cap through which auth addons contribute
13901
13948
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13902
13949
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18634,12 +18681,53 @@ var MediaFileKindEnum = _enum([
18634
18681
  "keyFrameSmall",
18635
18682
  "thumbnailSmall"
18636
18683
  ]);
18684
+ /**
18685
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18686
+ * ARE — never the bytes themselves.
18687
+ *
18688
+ * ## Why `url` and not `base64`
18689
+ *
18690
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18691
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18692
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18693
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18694
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18695
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18696
+ *
18697
+ * `url` points at the `event-media` data plane
18698
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18699
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18700
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18701
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18702
+ * no less protected than they were inside a `view`-level cap response — see
18703
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18704
+ * (per-device scoping).
18705
+ *
18706
+ * The URL is built from the row's **stored** key, which is not always its
18707
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18708
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18709
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18710
+ *
18711
+ * ## `base64` is TRANSITIONAL and is going away
18712
+ *
18713
+ * It is still populated for one reason: the deployed viewer's track-detail
18714
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18715
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18716
+ * triangle — not as absence. Removing the field before that viewer ships is an
18717
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18718
+ * delete this line and the `withBytes` pass-through in
18719
+ * `analytics-query-facade.ts`; nothing else reads it.
18720
+ */
18637
18721
  var MediaFileSchema = object({
18638
18722
  key: string(),
18639
18723
  kind: MediaFileKindEnum,
18640
- base64: string(),
18641
18724
  sizeBytes: number(),
18642
18725
  timestamp: number()
18726
+ }).extend({
18727
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18728
+ url: string(),
18729
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18730
+ base64: string()
18643
18731
  });
18644
18732
  /**
18645
18733
  * One media row WITHOUT its bytes.
@@ -18651,7 +18739,9 @@ var MediaFileSchema = object({
18651
18739
  * blocks the whole view.
18652
18740
  *
18653
18741
  * `sizeBytes` is carried because it is what lets a client decide between the
18654
- * stored blob and a `?variant=thumb` rendering without fetching either.
18742
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18743
+ * `url` because a client that had to build the plane path itself is a second
18744
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18655
18745
  */
18656
18746
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18657
18747
  /**
@@ -19340,6 +19430,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19340
19430
  }), array(MediaFileSchema).readonly()), method(object({
19341
19431
  trackId: string(),
19342
19432
  deviceId: number()
19433
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19434
+ eventId: string(),
19435
+ deviceId: number()
19343
19436
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19344
19437
  kind: "mutation",
19345
19438
  auth: "admin"
@@ -24434,10 +24527,24 @@ var FaceClusterSchema = object({
24434
24527
  size: number().int(),
24435
24528
  cohesion: number()
24436
24529
  });
24530
+ /**
24531
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24532
+ * are — never the bytes.
24533
+ *
24534
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24535
+ * track/event contract) is still populated because a deployed viewer requires
24536
+ * the field to parse a row at all; this method has no such reader. Its ONE
24537
+ * caller is the admin UI's detail modal, which was building
24538
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24539
+ * dialog already rendering its key FRAME from the `event-media` plane.
24540
+ *
24541
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24542
+ * media key directly, so this needed no new plane and no new access decision.
24543
+ */
24437
24544
  var MediaFileLiteSchema$1 = object({
24438
24545
  key: string(),
24439
24546
  kind: string(),
24440
- base64: string(),
24547
+ url: string(),
24441
24548
  sizeBytes: number(),
24442
24549
  timestamp: number()
24443
24550
  });
@@ -27505,10 +27612,24 @@ var PlateInfoSchema = object({
27505
27612
  */
27506
27613
  cropUrl: string().optional()
27507
27614
  });
27615
+ /**
27616
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27617
+ * are — never the bytes.
27618
+ *
27619
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27620
+ * track/event contract) is still populated because a deployed viewer requires
27621
+ * the field to parse a row at all; this method has no such reader. Its ONE
27622
+ * caller is the admin UI's detail modal, which was building
27623
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27624
+ * dialog already rendering its key FRAME from the `event-media` plane.
27625
+ *
27626
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27627
+ * media key directly, so this needed no new plane and no new access decision.
27628
+ */
27508
27629
  var MediaFileLiteSchema = object({
27509
27630
  key: string(),
27510
27631
  kind: string(),
27511
- base64: string(),
27632
+ url: string(),
27512
27633
  sizeBytes: number(),
27513
27634
  timestamp: number()
27514
27635
  });
@@ -35851,6 +35972,12 @@ Object.freeze({
35851
35972
  addonId: null,
35852
35973
  access: "view"
35853
35974
  },
35975
+ "pipelineAnalytics.listEventMedia": {
35976
+ capName: "pipeline-analytics",
35977
+ capScope: "device",
35978
+ addonId: null,
35979
+ access: "view"
35980
+ },
35854
35981
  "pipelineAnalytics.listGroups": {
35855
35982
  capName: "pipeline-analytics",
35856
35983
  capScope: "device",
@@ -39474,6 +39601,11 @@ Object.freeze({
39474
39601
  form: "array",
39475
39602
  optional: false
39476
39603
  }],
39604
+ "pipelineAnalytics.listEventMedia": [{
39605
+ name: "deviceId",
39606
+ form: "single",
39607
+ optional: false
39608
+ }],
39477
39609
  "pipelineAnalytics.listGroups": [{
39478
39610
  name: "deviceIds",
39479
39611
  form: "array",
package/dist/addon.mjs CHANGED
@@ -7997,6 +7997,21 @@ var RelocateJobSchema = object({
7997
7997
  bytesMoved: number().int(),
7998
7998
  /** Total files discovered up front; null while (or when) unknown. */
7999
7999
  filesTotal: number().int().nullable(),
8000
+ /**
8001
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8002
+ * made that nobody asked for, so it is reported where the operator reads the
8003
+ * job rather than only in a log line.
8004
+ *
8005
+ * A footage segment records its byte count in its own NAME, and the durable
8006
+ * hour row derives its aggregates from those names. A file that does not
8007
+ * match its name therefore makes the ledger's sums — and with them quota and
8008
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8009
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8010
+ *
8011
+ * Absent on lanes where the question has no meaning: a media blob's size is
8012
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8013
+ */
8014
+ rowsReconciled: number().int().nonnegative().optional(),
8000
8015
  startedAt: number(),
8001
8016
  finishedAt: number().nullable(),
8002
8017
  error: string().nullable()
@@ -8065,14 +8080,42 @@ var RelocateMediaInputSchema = object({
8065
8080
  /** Omitted = `move`, the pre-existing behaviour. */
8066
8081
  mode: MediaRelocateModeSchema.optional()
8067
8082
  });
8068
- /** How many rows still carry NO `locationId` — the population a repoint would
8069
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8070
- * value that permits a non-blocking `eventMedia` cutover. */
8071
- var UnstampedEventMediaCountSchema = object({
8072
- media: number().int().nonnegative(),
8073
- retrainFrames: number().int().nonnegative(),
8074
- total: number().int().nonnegative()
8083
+ /**
8084
+ * The unstamped population of ONE collection split, because the gate and the
8085
+ * operator ask two different questions and only one of them has to be cheap.
8086
+ *
8087
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8088
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8089
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8090
+ * that matters — after a seal, when the population is empty.
8091
+ *
8092
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8093
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8094
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8095
+ * and useful answer: "there are some, and this read could not say how many"
8096
+ * still refuses the cutover, which is the whole job.
8097
+ */
8098
+ var UnstampedRowsSchema = object({
8099
+ present: boolean(),
8100
+ rows: number().int().nonnegative().nullable()
8075
8101
  });
8102
+ /**
8103
+ * How many rows still carry NO `locationId` — the population a repoint would
8104
+ * silently re-aim at a disk that does not hold their bytes.
8105
+ *
8106
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8107
+ * over. The gate opens on a measured absence and on nothing else; an unread
8108
+ * collection and an empty one are different facts, and this repo has already
8109
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8110
+ */
8111
+ var UnstampedEventMediaCountSchema = object({
8112
+ media: UnstampedRowsSchema,
8113
+ retrainFrames: UnstampedRowsSchema,
8114
+ /** True when EITHER collection holds one. The refusal reads this. */
8115
+ anyPresent: boolean(),
8116
+ /** Sum across both, or `null` when either lane could not be counted. */
8117
+ total: number().int().nonnegative().nullable()
8118
+ }).nullable();
8076
8119
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8077
8120
  /** The independently selectable logical storage classes — every class
8078
8121
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8183,6 +8226,10 @@ var StorageMigrationMoveProgressSchema = object({
8183
8226
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8184
8227
  filesTotal: number().int().nonnegative().nullable(),
8185
8228
  bytesMoved: number().int().nonnegative(),
8229
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8230
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8231
+ * rows would be the same failure as one that silently skipped them. */
8232
+ rowsReconciled: number().int().nonnegative().optional(),
8186
8233
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8187
8234
  * crash gets a new mover, and a rate computed from the migration's start
8188
8235
  * would silently average in the time nothing was running. */
@@ -13131,6 +13178,114 @@ method(object({
13131
13178
  height: number()
13132
13179
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13133
13180
  /**
13181
+ * `failure-contribution` — the capability an addon reports its OWN losses
13182
+ * through, per camera, with the denominator attached. It stores nothing.
13183
+ *
13184
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13185
+ *
13186
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13187
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13188
+ * copied: the contributor reports what it already knows, hub-main adds only
13189
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13190
+ * somebody to forget to edit.
13191
+ *
13192
+ * They are not merged, because their invariants are opposites:
13193
+ *
13194
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13195
+ * claim a camera cost nothing, which is a measurement nobody made;
13196
+ * - a `failure-contribution` zero is the **most valuable value on the
13197
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13198
+ * and it is exactly what an absent entry cannot say.
13199
+ *
13200
+ * Putting a loss counter on a cost entry would also break the reconciliation
13201
+ * that gives `load-contribution` its point: contributions are subtracted from
13202
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13203
+ * has no process.
13204
+ *
13205
+ * ## Why not a log line, since the counters already exist
13206
+ *
13207
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13208
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13209
+ * ends in a log line, and a log line is the thing the operator asked to stop
13210
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13211
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13212
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13213
+ * media blackout were both diagnosed. The counters stay; this is where they can
13214
+ * be READ.
13215
+ *
13216
+ * ## The rate is served with its denominator or not at all
13217
+ *
13218
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13219
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13220
+ * than yesterday" and was **flat across twelve hours** once divided by the
13221
+ * successes on the same path. A surface that publishes only the numerator
13222
+ * reproduces that mistake on every read.
13223
+ *
13224
+ * ## Shape
13225
+ *
13226
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13227
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13228
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13229
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13230
+ * a forked runner's entries reach hub-main over transport that already exists.
13231
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13232
+ * result through `system.getFailureContributions`.
13233
+ */
13234
+ var FailureReasonCountSchema = object({
13235
+ /**
13236
+ * Why the attempt did not land, in the contributor's own vocabulary —
13237
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13238
+ * strings that already appear in this repo's logs and, where one exists, the
13239
+ * same string the per-track `previewMissReason` records (D276): a second
13240
+ * vocabulary for the same loss would make the row and the counter
13241
+ * un-joinable.
13242
+ */
13243
+ reason: string(),
13244
+ count: number().int().nonnegative()
13245
+ });
13246
+ var FailureContributionSchema = object({
13247
+ /**
13248
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13249
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13250
+ * `unit` free: the families are owned by different addons and a shared enum
13251
+ * is a central list that rots invisibly.
13252
+ */
13253
+ family: string(),
13254
+ /**
13255
+ * The NUMERIC device id — the same value every log line carries as
13256
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13257
+ * cannot name the camera must not emit the entry, because a fleet total
13258
+ * cannot answer the only question anybody asks of this surface.
13259
+ */
13260
+ deviceId: number().int().positive(),
13261
+ /**
13262
+ * A second dimension inside the family: the model / step id for an inference
13263
+ * timeout, so "which camera AND which model" is one read. Absent when the
13264
+ * family has a single variant.
13265
+ */
13266
+ variant: string().optional(),
13267
+ /**
13268
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13269
+ * differencing two reads must drop the interval when it changes, because the
13270
+ * counter restarted from zero in a respawned runner. Same discipline as
13271
+ * `LoadContribution.startedAtMs`.
13272
+ */
13273
+ sinceMs: number(),
13274
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13275
+ atMs: number(),
13276
+ /**
13277
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13278
+ * window. A failure count published without it is the mistake this schema
13279
+ * exists to make impossible.
13280
+ */
13281
+ attempts: number().int().nonnegative(),
13282
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13283
+ succeeded: number().int().nonnegative(),
13284
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13285
+ reasons: array(FailureReasonCountSchema).readonly()
13286
+ });
13287
+ method(_void(), array(FailureContributionSchema).readonly());
13288
+ /**
13134
13289
  * filesystem-browse — per-node capability for browsing the node's local
13135
13290
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13136
13291
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13652,6 +13807,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13652
13807
  kind: "mutation",
13653
13808
  auth: "admin"
13654
13809
  });
13810
+ var LoadContributionSchema = object({
13811
+ role: _enum([
13812
+ "decode",
13813
+ "transcode",
13814
+ "recording",
13815
+ "streaming",
13816
+ "detection"
13817
+ ]),
13818
+ /**
13819
+ * The NUMERIC device id — the same value every log line carries as
13820
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13821
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13822
+ * contributor that cannot name its camera must not emit the entry at all,
13823
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13824
+ * and would quietly turn one camera's cost into everybody's.
13825
+ */
13826
+ deviceId: number().int().positive().nullable(),
13827
+ attribution: _enum([
13828
+ "measured",
13829
+ "accounted",
13830
+ "unattributable"
13831
+ ]),
13832
+ /**
13833
+ * What ONE entry is, in the contributor's own words — `615/high`,
13834
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13835
+ * family and inventing a common one would lose the only information that
13836
+ * makes two entries for the same camera distinguishable.
13837
+ */
13838
+ unit: string(),
13839
+ /**
13840
+ * The OS process this cost lives in, when there is one. Present so a
13841
+ * consumer can (a) tell two generations of the same unit apart across a
13842
+ * restart, and (b) subtract claimed processes from the node's process
13843
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13844
+ * process of its own.
13845
+ */
13846
+ pid: number().int().positive().optional(),
13847
+ /**
13848
+ * When this generation started. The pid's incarnation marker: a consumer
13849
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13850
+ * window when this changes, because the counter restarted from zero in a new
13851
+ * process.
13852
+ */
13853
+ startedAtMs: number().optional(),
13854
+ /**
13855
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13856
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13857
+ * contribution is asked for.
13858
+ *
13859
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13860
+ * needs a sampler, and a new per-node sampler is the defect half of
13861
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13862
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13863
+ *
13864
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13865
+ * an entry with no process.
13866
+ */
13867
+ cpuSeconds: number().optional(),
13868
+ /** Resident bytes of this unit's process, same source and same rules. */
13869
+ rssBytes: number().optional()
13870
+ });
13871
+ method(_void(), array(LoadContributionSchema).readonly());
13655
13872
  /**
13656
13873
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13657
13874
  * through. It stores nothing.
@@ -13728,176 +13945,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13728
13945
  tags: record(string(), string()).optional()
13729
13946
  }), array(LogEntrySchema).readonly());
13730
13947
  /**
13731
- * `failure-contribution` — the capability an addon reports its OWN losses
13732
- * through, per camera, with the denominator attached. It stores nothing.
13733
- *
13734
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13735
- *
13736
- * `load-contribution` answers *what did this camera COST*. This answers *what
13737
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13738
- * copied: the contributor reports what it already knows, hub-main adds only
13739
- * `addonId`, nothing needs global knowledge, and there is no central list for
13740
- * somebody to forget to edit.
13741
- *
13742
- * They are not merged, because their invariants are opposites:
13743
- *
13744
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13745
- * claim a camera cost nothing, which is a measurement nobody made;
13746
- * - a `failure-contribution` zero is the **most valuable value on the
13747
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13748
- * and it is exactly what an absent entry cannot say.
13749
- *
13750
- * Putting a loss counter on a cost entry would also break the reconciliation
13751
- * that gives `load-contribution` its point: contributions are subtracted from
13752
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13753
- * has no process.
13754
- *
13755
- * ## Why not a log line, since the counters already exist
13756
- *
13757
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13758
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13759
- * ends in a log line, and a log line is the thing the operator asked to stop
13760
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13761
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13762
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13763
- * media blackout were both diagnosed. The counters stay; this is where they can
13764
- * be READ.
13765
- *
13766
- * ## The rate is served with its denominator or not at all
13767
- *
13768
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13769
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13770
- * than yesterday" and was **flat across twelve hours** once divided by the
13771
- * successes on the same path. A surface that publishes only the numerator
13772
- * reproduces that mistake on every read.
13773
- *
13774
- * ## Shape
13775
- *
13776
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13777
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13778
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13779
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13780
- * a forked runner's entries reach hub-main over transport that already exists.
13781
- * No new UDS message, no second registry (D3). The operator reads the assembled
13782
- * result through `system.getFailureContributions`.
13783
- */
13784
- var FailureReasonCountSchema = object({
13785
- /**
13786
- * Why the attempt did not land, in the contributor's own vocabulary —
13787
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13788
- * strings that already appear in this repo's logs and, where one exists, the
13789
- * same string the per-track `previewMissReason` records (D276): a second
13790
- * vocabulary for the same loss would make the row and the counter
13791
- * un-joinable.
13792
- */
13793
- reason: string(),
13794
- count: number().int().nonnegative()
13795
- });
13796
- var FailureContributionSchema = object({
13797
- /**
13798
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13799
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13800
- * `unit` free: the families are owned by different addons and a shared enum
13801
- * is a central list that rots invisibly.
13802
- */
13803
- family: string(),
13804
- /**
13805
- * The NUMERIC device id — the same value every log line carries as
13806
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13807
- * cannot name the camera must not emit the entry, because a fleet total
13808
- * cannot answer the only question anybody asks of this surface.
13809
- */
13810
- deviceId: number().int().positive(),
13811
- /**
13812
- * A second dimension inside the family: the model / step id for an inference
13813
- * timeout, so "which camera AND which model" is one read. Absent when the
13814
- * family has a single variant.
13815
- */
13816
- variant: string().optional(),
13817
- /**
13818
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13819
- * differencing two reads must drop the interval when it changes, because the
13820
- * counter restarted from zero in a respawned runner. Same discipline as
13821
- * `LoadContribution.startedAtMs`.
13822
- */
13823
- sinceMs: number(),
13824
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13825
- atMs: number(),
13826
- /**
13827
- * THE DENOMINATOR — every attempt on this path for this camera in the
13828
- * window. A failure count published without it is the mistake this schema
13829
- * exists to make impossible.
13830
- */
13831
- attempts: number().int().nonnegative(),
13832
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13833
- succeeded: number().int().nonnegative(),
13834
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13835
- reasons: array(FailureReasonCountSchema).readonly()
13836
- });
13837
- method(_void(), array(FailureContributionSchema).readonly());
13838
- var LoadContributionSchema = object({
13839
- role: _enum([
13840
- "decode",
13841
- "transcode",
13842
- "recording",
13843
- "streaming",
13844
- "detection"
13845
- ]),
13846
- /**
13847
- * The NUMERIC device id — the same value every log line carries as
13848
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13849
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13850
- * contributor that cannot name its camera must not emit the entry at all,
13851
- * because an unnamed per-camera entry is indistinguishable from a shared one
13852
- * and would quietly turn one camera's cost into everybody's.
13853
- */
13854
- deviceId: number().int().positive().nullable(),
13855
- attribution: _enum([
13856
- "measured",
13857
- "accounted",
13858
- "unattributable"
13859
- ]),
13860
- /**
13861
- * What ONE entry is, in the contributor's own words — `615/high`,
13862
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13863
- * family and inventing a common one would lose the only information that
13864
- * makes two entries for the same camera distinguishable.
13865
- */
13866
- unit: string(),
13867
- /**
13868
- * The OS process this cost lives in, when there is one. Present so a
13869
- * consumer can (a) tell two generations of the same unit apart across a
13870
- * restart, and (b) subtract claimed processes from the node's process
13871
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13872
- * process of its own.
13873
- */
13874
- pid: number().int().positive().optional(),
13875
- /**
13876
- * When this generation started. The pid's incarnation marker: a consumer
13877
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13878
- * window when this changes, because the counter restarted from zero in a new
13879
- * process.
13880
- */
13881
- startedAtMs: number().optional(),
13882
- /**
13883
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13884
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13885
- * contribution is asked for.
13886
- *
13887
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13888
- * needs a sampler, and a new per-node sampler is the defect half of
13889
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13890
- * by whoever already keeps a history; a rate cannot be un-averaged.
13891
- *
13892
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13893
- * an entry with no process.
13894
- */
13895
- cpuSeconds: number().optional(),
13896
- /** Resident bytes of this unit's process, same source and same rules. */
13897
- rssBytes: number().optional()
13898
- });
13899
- method(_void(), array(LoadContributionSchema).readonly());
13900
- /**
13901
13948
  * `login-method` — collection cap through which auth addons contribute
13902
13949
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13903
13950
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18635,12 +18682,53 @@ var MediaFileKindEnum = _enum([
18635
18682
  "keyFrameSmall",
18636
18683
  "thumbnailSmall"
18637
18684
  ]);
18685
+ /**
18686
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18687
+ * ARE — never the bytes themselves.
18688
+ *
18689
+ * ## Why `url` and not `base64`
18690
+ *
18691
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18692
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18693
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18694
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18695
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18696
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18697
+ *
18698
+ * `url` points at the `event-media` data plane
18699
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18700
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18701
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18702
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18703
+ * no less protected than they were inside a `view`-level cap response — see
18704
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18705
+ * (per-device scoping).
18706
+ *
18707
+ * The URL is built from the row's **stored** key, which is not always its
18708
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18709
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18710
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18711
+ *
18712
+ * ## `base64` is TRANSITIONAL and is going away
18713
+ *
18714
+ * It is still populated for one reason: the deployed viewer's track-detail
18715
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18716
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18717
+ * triangle — not as absence. Removing the field before that viewer ships is an
18718
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18719
+ * delete this line and the `withBytes` pass-through in
18720
+ * `analytics-query-facade.ts`; nothing else reads it.
18721
+ */
18638
18722
  var MediaFileSchema = object({
18639
18723
  key: string(),
18640
18724
  kind: MediaFileKindEnum,
18641
- base64: string(),
18642
18725
  sizeBytes: number(),
18643
18726
  timestamp: number()
18727
+ }).extend({
18728
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18729
+ url: string(),
18730
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18731
+ base64: string()
18644
18732
  });
18645
18733
  /**
18646
18734
  * One media row WITHOUT its bytes.
@@ -18652,7 +18740,9 @@ var MediaFileSchema = object({
18652
18740
  * blocks the whole view.
18653
18741
  *
18654
18742
  * `sizeBytes` is carried because it is what lets a client decide between the
18655
- * stored blob and a `?variant=thumb` rendering without fetching either.
18743
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18744
+ * `url` because a client that had to build the plane path itself is a second
18745
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18656
18746
  */
18657
18747
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18658
18748
  /**
@@ -19341,6 +19431,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19341
19431
  }), array(MediaFileSchema).readonly()), method(object({
19342
19432
  trackId: string(),
19343
19433
  deviceId: number()
19434
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19435
+ eventId: string(),
19436
+ deviceId: number()
19344
19437
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19345
19438
  kind: "mutation",
19346
19439
  auth: "admin"
@@ -24435,10 +24528,24 @@ var FaceClusterSchema = object({
24435
24528
  size: number().int(),
24436
24529
  cohesion: number()
24437
24530
  });
24531
+ /**
24532
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24533
+ * are — never the bytes.
24534
+ *
24535
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24536
+ * track/event contract) is still populated because a deployed viewer requires
24537
+ * the field to parse a row at all; this method has no such reader. Its ONE
24538
+ * caller is the admin UI's detail modal, which was building
24539
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24540
+ * dialog already rendering its key FRAME from the `event-media` plane.
24541
+ *
24542
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24543
+ * media key directly, so this needed no new plane and no new access decision.
24544
+ */
24438
24545
  var MediaFileLiteSchema$1 = object({
24439
24546
  key: string(),
24440
24547
  kind: string(),
24441
- base64: string(),
24548
+ url: string(),
24442
24549
  sizeBytes: number(),
24443
24550
  timestamp: number()
24444
24551
  });
@@ -27506,10 +27613,24 @@ var PlateInfoSchema = object({
27506
27613
  */
27507
27614
  cropUrl: string().optional()
27508
27615
  });
27616
+ /**
27617
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27618
+ * are — never the bytes.
27619
+ *
27620
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27621
+ * track/event contract) is still populated because a deployed viewer requires
27622
+ * the field to parse a row at all; this method has no such reader. Its ONE
27623
+ * caller is the admin UI's detail modal, which was building
27624
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27625
+ * dialog already rendering its key FRAME from the `event-media` plane.
27626
+ *
27627
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27628
+ * media key directly, so this needed no new plane and no new access decision.
27629
+ */
27509
27630
  var MediaFileLiteSchema = object({
27510
27631
  key: string(),
27511
27632
  kind: string(),
27512
- base64: string(),
27633
+ url: string(),
27513
27634
  sizeBytes: number(),
27514
27635
  timestamp: number()
27515
27636
  });
@@ -35852,6 +35973,12 @@ Object.freeze({
35852
35973
  addonId: null,
35853
35974
  access: "view"
35854
35975
  },
35976
+ "pipelineAnalytics.listEventMedia": {
35977
+ capName: "pipeline-analytics",
35978
+ capScope: "device",
35979
+ addonId: null,
35980
+ access: "view"
35981
+ },
35855
35982
  "pipelineAnalytics.listGroups": {
35856
35983
  capName: "pipeline-analytics",
35857
35984
  capScope: "device",
@@ -39475,6 +39602,11 @@ Object.freeze({
39475
39602
  form: "array",
39476
39603
  optional: false
39477
39604
  }],
39605
+ "pipelineAnalytics.listEventMedia": [{
39606
+ name: "deviceId",
39607
+ form: "single",
39608
+ optional: false
39609
+ }],
39478
39610
  "pipelineAnalytics.listGroups": [{
39479
39611
  name: "deviceIds",
39480
39612
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-amcrest",
3
- "version": "0.2.46",
3
+ "version": "0.2.48",
4
4
  "description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
5
5
  "keywords": [
6
6
  "camstack",