@camstack/addon-provider-petkit 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +313 -181
  2. package/dist/addon.mjs +313 -181
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -9105,6 +9105,21 @@ var RelocateJobSchema = object({
9105
9105
  bytesMoved: number().int(),
9106
9106
  /** Total files discovered up front; null while (or when) unknown. */
9107
9107
  filesTotal: number().int().nullable(),
9108
+ /**
9109
+ * Rows this run CORRECTED while moving them — a durable mutation the move
9110
+ * made that nobody asked for, so it is reported where the operator reads the
9111
+ * job rather than only in a log line.
9112
+ *
9113
+ * A footage segment records its byte count in its own NAME, and the durable
9114
+ * hour row derives its aggregates from those names. A file that does not
9115
+ * match its name therefore makes the ledger's sums — and with them quota and
9116
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
9117
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9118
+ *
9119
+ * Absent on lanes where the question has no meaning: a media blob's size is
9120
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9121
+ */
9122
+ rowsReconciled: number().int().nonnegative().optional(),
9108
9123
  startedAt: number(),
9109
9124
  finishedAt: number().nullable(),
9110
9125
  error: string().nullable()
@@ -9173,14 +9188,42 @@ var RelocateMediaInputSchema = object({
9173
9188
  /** Omitted = `move`, the pre-existing behaviour. */
9174
9189
  mode: MediaRelocateModeSchema.optional()
9175
9190
  });
9176
- /** How many rows still carry NO `locationId` — the population a repoint would
9177
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
9178
- * value that permits a non-blocking `eventMedia` cutover. */
9179
- var UnstampedEventMediaCountSchema = object({
9180
- media: number().int().nonnegative(),
9181
- retrainFrames: number().int().nonnegative(),
9182
- total: number().int().nonnegative()
9191
+ /**
9192
+ * The unstamped population of ONE collection split, because the gate and the
9193
+ * operator ask two different questions and only one of them has to be cheap.
9194
+ *
9195
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9196
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9197
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9198
+ * that matters — after a seal, when the population is empty.
9199
+ *
9200
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9201
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9202
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9203
+ * and useful answer: "there are some, and this read could not say how many"
9204
+ * still refuses the cutover, which is the whole job.
9205
+ */
9206
+ var UnstampedRowsSchema = object({
9207
+ present: boolean(),
9208
+ rows: number().int().nonnegative().nullable()
9183
9209
  });
9210
+ /**
9211
+ * How many rows still carry NO `locationId` — the population a repoint would
9212
+ * silently re-aim at a disk that does not hold their bytes.
9213
+ *
9214
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9215
+ * over. The gate opens on a measured absence and on nothing else; an unread
9216
+ * collection and an empty one are different facts, and this repo has already
9217
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9218
+ */
9219
+ var UnstampedEventMediaCountSchema = object({
9220
+ media: UnstampedRowsSchema,
9221
+ retrainFrames: UnstampedRowsSchema,
9222
+ /** True when EITHER collection holds one. The refusal reads this. */
9223
+ anyPresent: boolean(),
9224
+ /** Sum across both, or `null` when either lane could not be counted. */
9225
+ total: number().int().nonnegative().nullable()
9226
+ }).nullable();
9184
9227
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9185
9228
  /** The independently selectable logical storage classes — every class
9186
9229
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9291,6 +9334,10 @@ var StorageMigrationMoveProgressSchema = object({
9291
9334
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9292
9335
  filesTotal: number().int().nonnegative().nullable(),
9293
9336
  bytesMoved: number().int().nonnegative(),
9337
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9338
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9339
+ * rows would be the same failure as one that silently skipped them. */
9340
+ rowsReconciled: number().int().nonnegative().optional(),
9294
9341
  /** The MOVER's start, not the migration's: a drain restarted after an addon
9295
9342
  * crash gets a new mover, and a rate computed from the migration's start
9296
9343
  * would silently average in the time nothing was running. */
@@ -14256,6 +14303,114 @@ method(object({
14256
14303
  height: number()
14257
14304
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14258
14305
  /**
14306
+ * `failure-contribution` — the capability an addon reports its OWN losses
14307
+ * through, per camera, with the denominator attached. It stores nothing.
14308
+ *
14309
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14310
+ *
14311
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14312
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14313
+ * copied: the contributor reports what it already knows, hub-main adds only
14314
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14315
+ * somebody to forget to edit.
14316
+ *
14317
+ * They are not merged, because their invariants are opposites:
14318
+ *
14319
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14320
+ * claim a camera cost nothing, which is a measurement nobody made;
14321
+ * - a `failure-contribution` zero is the **most valuable value on the
14322
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14323
+ * and it is exactly what an absent entry cannot say.
14324
+ *
14325
+ * Putting a loss counter on a cost entry would also break the reconciliation
14326
+ * that gives `load-contribution` its point: contributions are subtracted from
14327
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14328
+ * has no process.
14329
+ *
14330
+ * ## Why not a log line, since the counters already exist
14331
+ *
14332
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14333
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14334
+ * ends in a log line, and a log line is the thing the operator asked to stop
14335
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14336
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14337
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14338
+ * media blackout were both diagnosed. The counters stay; this is where they can
14339
+ * be READ.
14340
+ *
14341
+ * ## The rate is served with its denominator or not at all
14342
+ *
14343
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14344
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14345
+ * than yesterday" and was **flat across twelve hours** once divided by the
14346
+ * successes on the same path. A surface that publishes only the numerator
14347
+ * reproduces that mistake on every read.
14348
+ *
14349
+ * ## Shape
14350
+ *
14351
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14352
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14353
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14354
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14355
+ * a forked runner's entries reach hub-main over transport that already exists.
14356
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14357
+ * result through `system.getFailureContributions`.
14358
+ */
14359
+ var FailureReasonCountSchema = object({
14360
+ /**
14361
+ * Why the attempt did not land, in the contributor's own vocabulary —
14362
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14363
+ * strings that already appear in this repo's logs and, where one exists, the
14364
+ * same string the per-track `previewMissReason` records (D276): a second
14365
+ * vocabulary for the same loss would make the row and the counter
14366
+ * un-joinable.
14367
+ */
14368
+ reason: string(),
14369
+ count: number().int().nonnegative()
14370
+ });
14371
+ var FailureContributionSchema = object({
14372
+ /**
14373
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14374
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14375
+ * `unit` free: the families are owned by different addons and a shared enum
14376
+ * is a central list that rots invisibly.
14377
+ */
14378
+ family: string(),
14379
+ /**
14380
+ * The NUMERIC device id — the same value every log line carries as
14381
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14382
+ * cannot name the camera must not emit the entry, because a fleet total
14383
+ * cannot answer the only question anybody asks of this surface.
14384
+ */
14385
+ deviceId: number().int().positive(),
14386
+ /**
14387
+ * A second dimension inside the family: the model / step id for an inference
14388
+ * timeout, so "which camera AND which model" is one read. Absent when the
14389
+ * family has a single variant.
14390
+ */
14391
+ variant: string().optional(),
14392
+ /**
14393
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14394
+ * differencing two reads must drop the interval when it changes, because the
14395
+ * counter restarted from zero in a respawned runner. Same discipline as
14396
+ * `LoadContribution.startedAtMs`.
14397
+ */
14398
+ sinceMs: number(),
14399
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14400
+ atMs: number(),
14401
+ /**
14402
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14403
+ * window. A failure count published without it is the mistake this schema
14404
+ * exists to make impossible.
14405
+ */
14406
+ attempts: number().int().nonnegative(),
14407
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14408
+ succeeded: number().int().nonnegative(),
14409
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14410
+ reasons: array(FailureReasonCountSchema).readonly()
14411
+ });
14412
+ method(_void(), array(FailureContributionSchema).readonly());
14413
+ /**
14259
14414
  * filesystem-browse — per-node capability for browsing the node's local
14260
14415
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14261
14416
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14777,6 +14932,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14777
14932
  kind: "mutation",
14778
14933
  auth: "admin"
14779
14934
  });
14935
+ var LoadContributionSchema = object({
14936
+ role: _enum([
14937
+ "decode",
14938
+ "transcode",
14939
+ "recording",
14940
+ "streaming",
14941
+ "detection"
14942
+ ]),
14943
+ /**
14944
+ * The NUMERIC device id — the same value every log line carries as
14945
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14946
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14947
+ * contributor that cannot name its camera must not emit the entry at all,
14948
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14949
+ * and would quietly turn one camera's cost into everybody's.
14950
+ */
14951
+ deviceId: number().int().positive().nullable(),
14952
+ attribution: _enum([
14953
+ "measured",
14954
+ "accounted",
14955
+ "unattributable"
14956
+ ]),
14957
+ /**
14958
+ * What ONE entry is, in the contributor's own words — `615/high`,
14959
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14960
+ * family and inventing a common one would lose the only information that
14961
+ * makes two entries for the same camera distinguishable.
14962
+ */
14963
+ unit: string(),
14964
+ /**
14965
+ * The OS process this cost lives in, when there is one. Present so a
14966
+ * consumer can (a) tell two generations of the same unit apart across a
14967
+ * restart, and (b) subtract claimed processes from the node's process
14968
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14969
+ * process of its own.
14970
+ */
14971
+ pid: number().int().positive().optional(),
14972
+ /**
14973
+ * When this generation started. The pid's incarnation marker: a consumer
14974
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14975
+ * window when this changes, because the counter restarted from zero in a new
14976
+ * process.
14977
+ */
14978
+ startedAtMs: number().optional(),
14979
+ /**
14980
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14981
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14982
+ * contribution is asked for.
14983
+ *
14984
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14985
+ * needs a sampler, and a new per-node sampler is the defect half of
14986
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14987
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14988
+ *
14989
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14990
+ * an entry with no process.
14991
+ */
14992
+ cpuSeconds: number().optional(),
14993
+ /** Resident bytes of this unit's process, same source and same rules. */
14994
+ rssBytes: number().optional()
14995
+ });
14996
+ method(_void(), array(LoadContributionSchema).readonly());
14780
14997
  /**
14781
14998
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14782
14999
  * through. It stores nothing.
@@ -14853,176 +15070,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14853
15070
  tags: record(string(), string()).optional()
14854
15071
  }), array(LogEntrySchema).readonly());
14855
15072
  /**
14856
- * `failure-contribution` — the capability an addon reports its OWN losses
14857
- * through, per camera, with the denominator attached. It stores nothing.
14858
- *
14859
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14860
- *
14861
- * `load-contribution` answers *what did this camera COST*. This answers *what
14862
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14863
- * copied: the contributor reports what it already knows, hub-main adds only
14864
- * `addonId`, nothing needs global knowledge, and there is no central list for
14865
- * somebody to forget to edit.
14866
- *
14867
- * They are not merged, because their invariants are opposites:
14868
- *
14869
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14870
- * claim a camera cost nothing, which is a measurement nobody made;
14871
- * - a `failure-contribution` zero is the **most valuable value on the
14872
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14873
- * and it is exactly what an absent entry cannot say.
14874
- *
14875
- * Putting a loss counter on a cost entry would also break the reconciliation
14876
- * that gives `load-contribution` its point: contributions are subtracted from
14877
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14878
- * has no process.
14879
- *
14880
- * ## Why not a log line, since the counters already exist
14881
- *
14882
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14883
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14884
- * ends in a log line, and a log line is the thing the operator asked to stop
14885
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14886
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14887
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14888
- * media blackout were both diagnosed. The counters stay; this is where they can
14889
- * be READ.
14890
- *
14891
- * ## The rate is served with its denominator or not at all
14892
- *
14893
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14894
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14895
- * than yesterday" and was **flat across twelve hours** once divided by the
14896
- * successes on the same path. A surface that publishes only the numerator
14897
- * reproduces that mistake on every read.
14898
- *
14899
- * ## Shape
14900
- *
14901
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14902
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14903
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14904
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14905
- * a forked runner's entries reach hub-main over transport that already exists.
14906
- * No new UDS message, no second registry (D3). The operator reads the assembled
14907
- * result through `system.getFailureContributions`.
14908
- */
14909
- var FailureReasonCountSchema = object({
14910
- /**
14911
- * Why the attempt did not land, in the contributor's own vocabulary —
14912
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14913
- * strings that already appear in this repo's logs and, where one exists, the
14914
- * same string the per-track `previewMissReason` records (D276): a second
14915
- * vocabulary for the same loss would make the row and the counter
14916
- * un-joinable.
14917
- */
14918
- reason: string(),
14919
- count: number().int().nonnegative()
14920
- });
14921
- var FailureContributionSchema = object({
14922
- /**
14923
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14924
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14925
- * `unit` free: the families are owned by different addons and a shared enum
14926
- * is a central list that rots invisibly.
14927
- */
14928
- family: string(),
14929
- /**
14930
- * The NUMERIC device id — the same value every log line carries as
14931
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14932
- * cannot name the camera must not emit the entry, because a fleet total
14933
- * cannot answer the only question anybody asks of this surface.
14934
- */
14935
- deviceId: number().int().positive(),
14936
- /**
14937
- * A second dimension inside the family: the model / step id for an inference
14938
- * timeout, so "which camera AND which model" is one read. Absent when the
14939
- * family has a single variant.
14940
- */
14941
- variant: string().optional(),
14942
- /**
14943
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14944
- * differencing two reads must drop the interval when it changes, because the
14945
- * counter restarted from zero in a respawned runner. Same discipline as
14946
- * `LoadContribution.startedAtMs`.
14947
- */
14948
- sinceMs: number(),
14949
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14950
- atMs: number(),
14951
- /**
14952
- * THE DENOMINATOR — every attempt on this path for this camera in the
14953
- * window. A failure count published without it is the mistake this schema
14954
- * exists to make impossible.
14955
- */
14956
- attempts: number().int().nonnegative(),
14957
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14958
- succeeded: number().int().nonnegative(),
14959
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14960
- reasons: array(FailureReasonCountSchema).readonly()
14961
- });
14962
- method(_void(), array(FailureContributionSchema).readonly());
14963
- var LoadContributionSchema = object({
14964
- role: _enum([
14965
- "decode",
14966
- "transcode",
14967
- "recording",
14968
- "streaming",
14969
- "detection"
14970
- ]),
14971
- /**
14972
- * The NUMERIC device id — the same value every log line carries as
14973
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14974
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14975
- * contributor that cannot name its camera must not emit the entry at all,
14976
- * because an unnamed per-camera entry is indistinguishable from a shared one
14977
- * and would quietly turn one camera's cost into everybody's.
14978
- */
14979
- deviceId: number().int().positive().nullable(),
14980
- attribution: _enum([
14981
- "measured",
14982
- "accounted",
14983
- "unattributable"
14984
- ]),
14985
- /**
14986
- * What ONE entry is, in the contributor's own words — `615/high`,
14987
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14988
- * family and inventing a common one would lose the only information that
14989
- * makes two entries for the same camera distinguishable.
14990
- */
14991
- unit: string(),
14992
- /**
14993
- * The OS process this cost lives in, when there is one. Present so a
14994
- * consumer can (a) tell two generations of the same unit apart across a
14995
- * restart, and (b) subtract claimed processes from the node's process
14996
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14997
- * process of its own.
14998
- */
14999
- pid: number().int().positive().optional(),
15000
- /**
15001
- * When this generation started. The pid's incarnation marker: a consumer
15002
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15003
- * window when this changes, because the counter restarted from zero in a new
15004
- * process.
15005
- */
15006
- startedAtMs: number().optional(),
15007
- /**
15008
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15009
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
15010
- * contribution is asked for.
15011
- *
15012
- * Cumulative and not a rate on purpose: a rate needs a window, a window
15013
- * needs a sampler, and a new per-node sampler is the defect half of
15014
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15015
- * by whoever already keeps a history; a rate cannot be un-averaged.
15016
- *
15017
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15018
- * an entry with no process.
15019
- */
15020
- cpuSeconds: number().optional(),
15021
- /** Resident bytes of this unit's process, same source and same rules. */
15022
- rssBytes: number().optional()
15023
- });
15024
- method(_void(), array(LoadContributionSchema).readonly());
15025
- /**
15026
15073
  * `login-method` — collection cap through which auth addons contribute
15027
15074
  * their pre-auth login surfaces to the login page. This is the SINGLE,
15028
15075
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19760,12 +19807,53 @@ var MediaFileKindEnum = _enum([
19760
19807
  "keyFrameSmall",
19761
19808
  "thumbnailSmall"
19762
19809
  ]);
19810
+ /**
19811
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19812
+ * ARE — never the bytes themselves.
19813
+ *
19814
+ * ## Why `url` and not `base64`
19815
+ *
19816
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19817
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19818
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19819
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19820
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19821
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19822
+ *
19823
+ * `url` points at the `event-media` data plane
19824
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19825
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19826
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19827
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19828
+ * no less protected than they were inside a `view`-level cap response — see
19829
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19830
+ * (per-device scoping).
19831
+ *
19832
+ * The URL is built from the row's **stored** key, which is not always its
19833
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19834
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19835
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19836
+ *
19837
+ * ## `base64` is TRANSITIONAL and is going away
19838
+ *
19839
+ * It is still populated for one reason: the deployed viewer's track-detail
19840
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19841
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19842
+ * triangle — not as absence. Removing the field before that viewer ships is an
19843
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19844
+ * delete this line and the `withBytes` pass-through in
19845
+ * `analytics-query-facade.ts`; nothing else reads it.
19846
+ */
19763
19847
  var MediaFileSchema = object({
19764
19848
  key: string(),
19765
19849
  kind: MediaFileKindEnum,
19766
- base64: string(),
19767
19850
  sizeBytes: number(),
19768
19851
  timestamp: number()
19852
+ }).extend({
19853
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19854
+ url: string(),
19855
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19856
+ base64: string()
19769
19857
  });
19770
19858
  /**
19771
19859
  * One media row WITHOUT its bytes.
@@ -19777,7 +19865,9 @@ var MediaFileSchema = object({
19777
19865
  * blocks the whole view.
19778
19866
  *
19779
19867
  * `sizeBytes` is carried because it is what lets a client decide between the
19780
- * stored blob and a `?variant=thumb` rendering without fetching either.
19868
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19869
+ * `url` because a client that had to build the plane path itself is a second
19870
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19781
19871
  */
19782
19872
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19783
19873
  /**
@@ -20466,6 +20556,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20466
20556
  }), array(MediaFileSchema).readonly()), method(object({
20467
20557
  trackId: string(),
20468
20558
  deviceId: number()
20559
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20560
+ eventId: string(),
20561
+ deviceId: number()
20469
20562
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20470
20563
  kind: "mutation",
20471
20564
  auth: "admin"
@@ -25464,10 +25557,24 @@ var FaceClusterSchema = object({
25464
25557
  size: number().int(),
25465
25558
  cohesion: number()
25466
25559
  });
25560
+ /**
25561
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25562
+ * are — never the bytes.
25563
+ *
25564
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25565
+ * track/event contract) is still populated because a deployed viewer requires
25566
+ * the field to parse a row at all; this method has no such reader. Its ONE
25567
+ * caller is the admin UI's detail modal, which was building
25568
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25569
+ * dialog already rendering its key FRAME from the `event-media` plane.
25570
+ *
25571
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25572
+ * media key directly, so this needed no new plane and no new access decision.
25573
+ */
25467
25574
  var MediaFileLiteSchema$1 = object({
25468
25575
  key: string(),
25469
25576
  kind: string(),
25470
- base64: string(),
25577
+ url: string(),
25471
25578
  sizeBytes: number(),
25472
25579
  timestamp: number()
25473
25580
  });
@@ -28489,10 +28596,24 @@ var PlateInfoSchema = object({
28489
28596
  */
28490
28597
  cropUrl: string().optional()
28491
28598
  });
28599
+ /**
28600
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28601
+ * are — never the bytes.
28602
+ *
28603
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28604
+ * track/event contract) is still populated because a deployed viewer requires
28605
+ * the field to parse a row at all; this method has no such reader. Its ONE
28606
+ * caller is the admin UI's detail modal, which was building
28607
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28608
+ * dialog already rendering its key FRAME from the `event-media` plane.
28609
+ *
28610
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28611
+ * media key directly, so this needed no new plane and no new access decision.
28612
+ */
28492
28613
  var MediaFileLiteSchema = object({
28493
28614
  key: string(),
28494
28615
  kind: string(),
28495
- base64: string(),
28616
+ url: string(),
28496
28617
  sizeBytes: number(),
28497
28618
  timestamp: number()
28498
28619
  });
@@ -36389,6 +36510,12 @@ Object.freeze({
36389
36510
  addonId: null,
36390
36511
  access: "view"
36391
36512
  },
36513
+ "pipelineAnalytics.listEventMedia": {
36514
+ capName: "pipeline-analytics",
36515
+ capScope: "device",
36516
+ addonId: null,
36517
+ access: "view"
36518
+ },
36392
36519
  "pipelineAnalytics.listGroups": {
36393
36520
  capName: "pipeline-analytics",
36394
36521
  capScope: "device",
@@ -40012,6 +40139,11 @@ Object.freeze({
40012
40139
  form: "array",
40013
40140
  optional: false
40014
40141
  }],
40142
+ "pipelineAnalytics.listEventMedia": [{
40143
+ name: "deviceId",
40144
+ form: "single",
40145
+ optional: false
40146
+ }],
40015
40147
  "pipelineAnalytics.listGroups": [{
40016
40148
  name: "deviceIds",
40017
40149
  form: "array",
package/dist/addon.mjs CHANGED
@@ -9104,6 +9104,21 @@ var RelocateJobSchema = object({
9104
9104
  bytesMoved: number().int(),
9105
9105
  /** Total files discovered up front; null while (or when) unknown. */
9106
9106
  filesTotal: number().int().nullable(),
9107
+ /**
9108
+ * Rows this run CORRECTED while moving them — a durable mutation the move
9109
+ * made that nobody asked for, so it is reported where the operator reads the
9110
+ * job rather than only in a log line.
9111
+ *
9112
+ * A footage segment records its byte count in its own NAME, and the durable
9113
+ * hour row derives its aggregates from those names. A file that does not
9114
+ * match its name therefore makes the ledger's sums — and with them quota and
9115
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
9116
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
9117
+ *
9118
+ * Absent on lanes where the question has no meaning: a media blob's size is
9119
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
9120
+ */
9121
+ rowsReconciled: number().int().nonnegative().optional(),
9107
9122
  startedAt: number(),
9108
9123
  finishedAt: number().nullable(),
9109
9124
  error: string().nullable()
@@ -9172,14 +9187,42 @@ var RelocateMediaInputSchema = object({
9172
9187
  /** Omitted = `move`, the pre-existing behaviour. */
9173
9188
  mode: MediaRelocateModeSchema.optional()
9174
9189
  });
9175
- /** How many rows still carry NO `locationId` — the population a repoint would
9176
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
9177
- * value that permits a non-blocking `eventMedia` cutover. */
9178
- var UnstampedEventMediaCountSchema = object({
9179
- media: number().int().nonnegative(),
9180
- retrainFrames: number().int().nonnegative(),
9181
- total: number().int().nonnegative()
9190
+ /**
9191
+ * The unstamped population of ONE collection split, because the gate and the
9192
+ * operator ask two different questions and only one of them has to be cheap.
9193
+ *
9194
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
9195
+ * repoint". It is a single indexed seek to the first matching row, so it stays
9196
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
9197
+ * that matters — after a seal, when the population is empty.
9198
+ *
9199
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
9200
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
9201
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
9202
+ * and useful answer: "there are some, and this read could not say how many"
9203
+ * still refuses the cutover, which is the whole job.
9204
+ */
9205
+ var UnstampedRowsSchema = object({
9206
+ present: boolean(),
9207
+ rows: number().int().nonnegative().nullable()
9182
9208
  });
9209
+ /**
9210
+ * How many rows still carry NO `locationId` — the population a repoint would
9211
+ * silently re-aim at a disk that does not hold their bytes.
9212
+ *
9213
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
9214
+ * over. The gate opens on a measured absence and on nothing else; an unread
9215
+ * collection and an empty one are different facts, and this repo has already
9216
+ * paid for conflating them (`RelocateResidueSchema`, D295).
9217
+ */
9218
+ var UnstampedEventMediaCountSchema = object({
9219
+ media: UnstampedRowsSchema,
9220
+ retrainFrames: UnstampedRowsSchema,
9221
+ /** True when EITHER collection holds one. The refusal reads this. */
9222
+ anyPresent: boolean(),
9223
+ /** Sum across both, or `null` when either lane could not be counted. */
9224
+ total: number().int().nonnegative().nullable()
9225
+ }).nullable();
9183
9226
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
9184
9227
  /** The independently selectable logical storage classes — every class
9185
9228
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -9290,6 +9333,10 @@ var StorageMigrationMoveProgressSchema = object({
9290
9333
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
9291
9334
  filesTotal: number().int().nonnegative().nullable(),
9292
9335
  bytesMoved: number().int().nonnegative(),
9336
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
9337
+ * a lane that cannot reconcile. A migration that silently rewrote durable
9338
+ * rows would be the same failure as one that silently skipped them. */
9339
+ rowsReconciled: number().int().nonnegative().optional(),
9293
9340
  /** The MOVER's start, not the migration's: a drain restarted after an addon
9294
9341
  * crash gets a new mover, and a rate computed from the migration's start
9295
9342
  * would silently average in the time nothing was running. */
@@ -14255,6 +14302,114 @@ method(object({
14255
14302
  height: number()
14256
14303
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
14257
14304
  /**
14305
+ * `failure-contribution` — the capability an addon reports its OWN losses
14306
+ * through, per camera, with the denominator attached. It stores nothing.
14307
+ *
14308
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
14309
+ *
14310
+ * `load-contribution` answers *what did this camera COST*. This answers *what
14311
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
14312
+ * copied: the contributor reports what it already knows, hub-main adds only
14313
+ * `addonId`, nothing needs global knowledge, and there is no central list for
14314
+ * somebody to forget to edit.
14315
+ *
14316
+ * They are not merged, because their invariants are opposites:
14317
+ *
14318
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
14319
+ * claim a camera cost nothing, which is a measurement nobody made;
14320
+ * - a `failure-contribution` zero is the **most valuable value on the
14321
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14322
+ * and it is exactly what an absent entry cannot say.
14323
+ *
14324
+ * Putting a loss counter on a cost entry would also break the reconciliation
14325
+ * that gives `load-contribution` its point: contributions are subtracted from
14326
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14327
+ * has no process.
14328
+ *
14329
+ * ## Why not a log line, since the counters already exist
14330
+ *
14331
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
14332
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14333
+ * ends in a log line, and a log line is the thing the operator asked to stop
14334
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14335
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14336
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14337
+ * media blackout were both diagnosed. The counters stay; this is where they can
14338
+ * be READ.
14339
+ *
14340
+ * ## The rate is served with its denominator or not at all
14341
+ *
14342
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
14343
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14344
+ * than yesterday" and was **flat across twelve hours** once divided by the
14345
+ * successes on the same path. A surface that publishes only the numerator
14346
+ * reproduces that mistake on every read.
14347
+ *
14348
+ * ## Shape
14349
+ *
14350
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14351
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14352
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14353
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14354
+ * a forked runner's entries reach hub-main over transport that already exists.
14355
+ * No new UDS message, no second registry (D3). The operator reads the assembled
14356
+ * result through `system.getFailureContributions`.
14357
+ */
14358
+ var FailureReasonCountSchema = object({
14359
+ /**
14360
+ * Why the attempt did not land, in the contributor's own vocabulary —
14361
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14362
+ * strings that already appear in this repo's logs and, where one exists, the
14363
+ * same string the per-track `previewMissReason` records (D276): a second
14364
+ * vocabulary for the same loss would make the row and the counter
14365
+ * un-joinable.
14366
+ */
14367
+ reason: string(),
14368
+ count: number().int().nonnegative()
14369
+ });
14370
+ var FailureContributionSchema = object({
14371
+ /**
14372
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14373
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14374
+ * `unit` free: the families are owned by different addons and a shared enum
14375
+ * is a central list that rots invisibly.
14376
+ */
14377
+ family: string(),
14378
+ /**
14379
+ * The NUMERIC device id — the same value every log line carries as
14380
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
14381
+ * cannot name the camera must not emit the entry, because a fleet total
14382
+ * cannot answer the only question anybody asks of this surface.
14383
+ */
14384
+ deviceId: number().int().positive(),
14385
+ /**
14386
+ * A second dimension inside the family: the model / step id for an inference
14387
+ * timeout, so "which camera AND which model" is one read. Absent when the
14388
+ * family has a single variant.
14389
+ */
14390
+ variant: string().optional(),
14391
+ /**
14392
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14393
+ * differencing two reads must drop the interval when it changes, because the
14394
+ * counter restarted from zero in a respawned runner. Same discipline as
14395
+ * `LoadContribution.startedAtMs`.
14396
+ */
14397
+ sinceMs: number(),
14398
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14399
+ atMs: number(),
14400
+ /**
14401
+ * THE DENOMINATOR — every attempt on this path for this camera in the
14402
+ * window. A failure count published without it is the mistake this schema
14403
+ * exists to make impossible.
14404
+ */
14405
+ attempts: number().int().nonnegative(),
14406
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
14407
+ succeeded: number().int().nonnegative(),
14408
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
14409
+ reasons: array(FailureReasonCountSchema).readonly()
14410
+ });
14411
+ method(_void(), array(FailureContributionSchema).readonly());
14412
+ /**
14258
14413
  * filesystem-browse — per-node capability for browsing the node's local
14259
14414
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
14260
14415
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14776,6 +14931,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14776
14931
  kind: "mutation",
14777
14932
  auth: "admin"
14778
14933
  });
14934
+ var LoadContributionSchema = object({
14935
+ role: _enum([
14936
+ "decode",
14937
+ "transcode",
14938
+ "recording",
14939
+ "streaming",
14940
+ "detection"
14941
+ ]),
14942
+ /**
14943
+ * The NUMERIC device id — the same value every log line carries as
14944
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14945
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14946
+ * contributor that cannot name its camera must not emit the entry at all,
14947
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14948
+ * and would quietly turn one camera's cost into everybody's.
14949
+ */
14950
+ deviceId: number().int().positive().nullable(),
14951
+ attribution: _enum([
14952
+ "measured",
14953
+ "accounted",
14954
+ "unattributable"
14955
+ ]),
14956
+ /**
14957
+ * What ONE entry is, in the contributor's own words — `615/high`,
14958
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14959
+ * family and inventing a common one would lose the only information that
14960
+ * makes two entries for the same camera distinguishable.
14961
+ */
14962
+ unit: string(),
14963
+ /**
14964
+ * The OS process this cost lives in, when there is one. Present so a
14965
+ * consumer can (a) tell two generations of the same unit apart across a
14966
+ * restart, and (b) subtract claimed processes from the node's process
14967
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14968
+ * process of its own.
14969
+ */
14970
+ pid: number().int().positive().optional(),
14971
+ /**
14972
+ * When this generation started. The pid's incarnation marker: a consumer
14973
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14974
+ * window when this changes, because the counter restarted from zero in a new
14975
+ * process.
14976
+ */
14977
+ startedAtMs: number().optional(),
14978
+ /**
14979
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14980
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14981
+ * contribution is asked for.
14982
+ *
14983
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14984
+ * needs a sampler, and a new per-node sampler is the defect half of
14985
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14986
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14987
+ *
14988
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14989
+ * an entry with no process.
14990
+ */
14991
+ cpuSeconds: number().optional(),
14992
+ /** Resident bytes of this unit's process, same source and same rules. */
14993
+ rssBytes: number().optional()
14994
+ });
14995
+ method(_void(), array(LoadContributionSchema).readonly());
14779
14996
  /**
14780
14997
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14781
14998
  * through. It stores nothing.
@@ -14852,176 +15069,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14852
15069
  tags: record(string(), string()).optional()
14853
15070
  }), array(LogEntrySchema).readonly());
14854
15071
  /**
14855
- * `failure-contribution` — the capability an addon reports its OWN losses
14856
- * through, per camera, with the denominator attached. It stores nothing.
14857
- *
14858
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14859
- *
14860
- * `load-contribution` answers *what did this camera COST*. This answers *what
14861
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14862
- * copied: the contributor reports what it already knows, hub-main adds only
14863
- * `addonId`, nothing needs global knowledge, and there is no central list for
14864
- * somebody to forget to edit.
14865
- *
14866
- * They are not merged, because their invariants are opposites:
14867
- *
14868
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14869
- * claim a camera cost nothing, which is a measurement nobody made;
14870
- * - a `failure-contribution` zero is the **most valuable value on the
14871
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14872
- * and it is exactly what an absent entry cannot say.
14873
- *
14874
- * Putting a loss counter on a cost entry would also break the reconciliation
14875
- * that gives `load-contribution` its point: contributions are subtracted from
14876
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14877
- * has no process.
14878
- *
14879
- * ## Why not a log line, since the counters already exist
14880
- *
14881
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14882
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14883
- * ends in a log line, and a log line is the thing the operator asked to stop
14884
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14885
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14886
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14887
- * media blackout were both diagnosed. The counters stay; this is where they can
14888
- * be READ.
14889
- *
14890
- * ## The rate is served with its denominator or not at all
14891
- *
14892
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14893
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14894
- * than yesterday" and was **flat across twelve hours** once divided by the
14895
- * successes on the same path. A surface that publishes only the numerator
14896
- * reproduces that mistake on every read.
14897
- *
14898
- * ## Shape
14899
- *
14900
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14901
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14902
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14903
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14904
- * a forked runner's entries reach hub-main over transport that already exists.
14905
- * No new UDS message, no second registry (D3). The operator reads the assembled
14906
- * result through `system.getFailureContributions`.
14907
- */
14908
- var FailureReasonCountSchema = object({
14909
- /**
14910
- * Why the attempt did not land, in the contributor's own vocabulary —
14911
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14912
- * strings that already appear in this repo's logs and, where one exists, the
14913
- * same string the per-track `previewMissReason` records (D276): a second
14914
- * vocabulary for the same loss would make the row and the counter
14915
- * un-joinable.
14916
- */
14917
- reason: string(),
14918
- count: number().int().nonnegative()
14919
- });
14920
- var FailureContributionSchema = object({
14921
- /**
14922
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14923
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14924
- * `unit` free: the families are owned by different addons and a shared enum
14925
- * is a central list that rots invisibly.
14926
- */
14927
- family: string(),
14928
- /**
14929
- * The NUMERIC device id — the same value every log line carries as
14930
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14931
- * cannot name the camera must not emit the entry, because a fleet total
14932
- * cannot answer the only question anybody asks of this surface.
14933
- */
14934
- deviceId: number().int().positive(),
14935
- /**
14936
- * A second dimension inside the family: the model / step id for an inference
14937
- * timeout, so "which camera AND which model" is one read. Absent when the
14938
- * family has a single variant.
14939
- */
14940
- variant: string().optional(),
14941
- /**
14942
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14943
- * differencing two reads must drop the interval when it changes, because the
14944
- * counter restarted from zero in a respawned runner. Same discipline as
14945
- * `LoadContribution.startedAtMs`.
14946
- */
14947
- sinceMs: number(),
14948
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14949
- atMs: number(),
14950
- /**
14951
- * THE DENOMINATOR — every attempt on this path for this camera in the
14952
- * window. A failure count published without it is the mistake this schema
14953
- * exists to make impossible.
14954
- */
14955
- attempts: number().int().nonnegative(),
14956
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14957
- succeeded: number().int().nonnegative(),
14958
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14959
- reasons: array(FailureReasonCountSchema).readonly()
14960
- });
14961
- method(_void(), array(FailureContributionSchema).readonly());
14962
- var LoadContributionSchema = object({
14963
- role: _enum([
14964
- "decode",
14965
- "transcode",
14966
- "recording",
14967
- "streaming",
14968
- "detection"
14969
- ]),
14970
- /**
14971
- * The NUMERIC device id — the same value every log line carries as
14972
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14973
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14974
- * contributor that cannot name its camera must not emit the entry at all,
14975
- * because an unnamed per-camera entry is indistinguishable from a shared one
14976
- * and would quietly turn one camera's cost into everybody's.
14977
- */
14978
- deviceId: number().int().positive().nullable(),
14979
- attribution: _enum([
14980
- "measured",
14981
- "accounted",
14982
- "unattributable"
14983
- ]),
14984
- /**
14985
- * What ONE entry is, in the contributor's own words — `615/high`,
14986
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14987
- * family and inventing a common one would lose the only information that
14988
- * makes two entries for the same camera distinguishable.
14989
- */
14990
- unit: string(),
14991
- /**
14992
- * The OS process this cost lives in, when there is one. Present so a
14993
- * consumer can (a) tell two generations of the same unit apart across a
14994
- * restart, and (b) subtract claimed processes from the node's process
14995
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14996
- * process of its own.
14997
- */
14998
- pid: number().int().positive().optional(),
14999
- /**
15000
- * When this generation started. The pid's incarnation marker: a consumer
15001
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
15002
- * window when this changes, because the counter restarted from zero in a new
15003
- * process.
15004
- */
15005
- startedAtMs: number().optional(),
15006
- /**
15007
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
15008
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
15009
- * contribution is asked for.
15010
- *
15011
- * Cumulative and not a rate on purpose: a rate needs a window, a window
15012
- * needs a sampler, and a new per-node sampler is the defect half of
15013
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
15014
- * by whoever already keeps a history; a rate cannot be un-averaged.
15015
- *
15016
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
15017
- * an entry with no process.
15018
- */
15019
- cpuSeconds: number().optional(),
15020
- /** Resident bytes of this unit's process, same source and same rules. */
15021
- rssBytes: number().optional()
15022
- });
15023
- method(_void(), array(LoadContributionSchema).readonly());
15024
- /**
15025
15072
  * `login-method` — collection cap through which auth addons contribute
15026
15073
  * their pre-auth login surfaces to the login page. This is the SINGLE,
15027
15074
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19759,12 +19806,53 @@ var MediaFileKindEnum = _enum([
19759
19806
  "keyFrameSmall",
19760
19807
  "thumbnailSmall"
19761
19808
  ]);
19809
+ /**
19810
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19811
+ * ARE — never the bytes themselves.
19812
+ *
19813
+ * ## Why `url` and not `base64`
19814
+ *
19815
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19816
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19817
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19818
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19819
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19820
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19821
+ *
19822
+ * `url` points at the `event-media` data plane
19823
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19824
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19825
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19826
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19827
+ * no less protected than they were inside a `view`-level cap response — see
19828
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19829
+ * (per-device scoping).
19830
+ *
19831
+ * The URL is built from the row's **stored** key, which is not always its
19832
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19833
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19834
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19835
+ *
19836
+ * ## `base64` is TRANSITIONAL and is going away
19837
+ *
19838
+ * It is still populated for one reason: the deployed viewer's track-detail
19839
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19840
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19841
+ * triangle — not as absence. Removing the field before that viewer ships is an
19842
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19843
+ * delete this line and the `withBytes` pass-through in
19844
+ * `analytics-query-facade.ts`; nothing else reads it.
19845
+ */
19762
19846
  var MediaFileSchema = object({
19763
19847
  key: string(),
19764
19848
  kind: MediaFileKindEnum,
19765
- base64: string(),
19766
19849
  sizeBytes: number(),
19767
19850
  timestamp: number()
19851
+ }).extend({
19852
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19853
+ url: string(),
19854
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19855
+ base64: string()
19768
19856
  });
19769
19857
  /**
19770
19858
  * One media row WITHOUT its bytes.
@@ -19776,7 +19864,9 @@ var MediaFileSchema = object({
19776
19864
  * blocks the whole view.
19777
19865
  *
19778
19866
  * `sizeBytes` is carried because it is what lets a client decide between the
19779
- * stored blob and a `?variant=thumb` rendering without fetching either.
19867
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19868
+ * `url` because a client that had to build the plane path itself is a second
19869
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19780
19870
  */
19781
19871
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19782
19872
  /**
@@ -20465,6 +20555,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
20465
20555
  }), array(MediaFileSchema).readonly()), method(object({
20466
20556
  trackId: string(),
20467
20557
  deviceId: number()
20558
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20559
+ eventId: string(),
20560
+ deviceId: number()
20468
20561
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
20469
20562
  kind: "mutation",
20470
20563
  auth: "admin"
@@ -25463,10 +25556,24 @@ var FaceClusterSchema = object({
25463
25556
  size: number().int(),
25464
25557
  cohesion: number()
25465
25558
  });
25559
+ /**
25560
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25561
+ * are — never the bytes.
25562
+ *
25563
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25564
+ * track/event contract) is still populated because a deployed viewer requires
25565
+ * the field to parse a row at all; this method has no such reader. Its ONE
25566
+ * caller is the admin UI's detail modal, which was building
25567
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25568
+ * dialog already rendering its key FRAME from the `event-media` plane.
25569
+ *
25570
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25571
+ * media key directly, so this needed no new plane and no new access decision.
25572
+ */
25466
25573
  var MediaFileLiteSchema$1 = object({
25467
25574
  key: string(),
25468
25575
  kind: string(),
25469
- base64: string(),
25576
+ url: string(),
25470
25577
  sizeBytes: number(),
25471
25578
  timestamp: number()
25472
25579
  });
@@ -28488,10 +28595,24 @@ var PlateInfoSchema = object({
28488
28595
  */
28489
28596
  cropUrl: string().optional()
28490
28597
  });
28598
+ /**
28599
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28600
+ * are — never the bytes.
28601
+ *
28602
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28603
+ * track/event contract) is still populated because a deployed viewer requires
28604
+ * the field to parse a row at all; this method has no such reader. Its ONE
28605
+ * caller is the admin UI's detail modal, which was building
28606
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28607
+ * dialog already rendering its key FRAME from the `event-media` plane.
28608
+ *
28609
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28610
+ * media key directly, so this needed no new plane and no new access decision.
28611
+ */
28491
28612
  var MediaFileLiteSchema = object({
28492
28613
  key: string(),
28493
28614
  kind: string(),
28494
- base64: string(),
28615
+ url: string(),
28495
28616
  sizeBytes: number(),
28496
28617
  timestamp: number()
28497
28618
  });
@@ -36388,6 +36509,12 @@ Object.freeze({
36388
36509
  addonId: null,
36389
36510
  access: "view"
36390
36511
  },
36512
+ "pipelineAnalytics.listEventMedia": {
36513
+ capName: "pipeline-analytics",
36514
+ capScope: "device",
36515
+ addonId: null,
36516
+ access: "view"
36517
+ },
36391
36518
  "pipelineAnalytics.listGroups": {
36392
36519
  capName: "pipeline-analytics",
36393
36520
  capScope: "device",
@@ -40011,6 +40138,11 @@ Object.freeze({
40011
40138
  form: "array",
40012
40139
  optional: false
40013
40140
  }],
40141
+ "pipelineAnalytics.listEventMedia": [{
40142
+ name: "deviceId",
40143
+ form: "single",
40144
+ optional: false
40145
+ }],
40014
40146
  "pipelineAnalytics.listGroups": [{
40015
40147
  name: "deviceIds",
40016
40148
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",