@camstack/addon-decoder-nodeav 1.2.45 → 1.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/index.js +313 -181
  2. package/dist/index.mjs +313 -181
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8003,6 +8003,21 @@ var RelocateJobSchema = object({
8003
8003
  bytesMoved: number().int(),
8004
8004
  /** Total files discovered up front; null while (or when) unknown. */
8005
8005
  filesTotal: number().int().nullable(),
8006
+ /**
8007
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8008
+ * made that nobody asked for, so it is reported where the operator reads the
8009
+ * job rather than only in a log line.
8010
+ *
8011
+ * A footage segment records its byte count in its own NAME, and the durable
8012
+ * hour row derives its aggregates from those names. A file that does not
8013
+ * match its name therefore makes the ledger's sums — and with them quota and
8014
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8015
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8016
+ *
8017
+ * Absent on lanes where the question has no meaning: a media blob's size is
8018
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8019
+ */
8020
+ rowsReconciled: number().int().nonnegative().optional(),
8006
8021
  startedAt: number(),
8007
8022
  finishedAt: number().nullable(),
8008
8023
  error: string().nullable()
@@ -8071,14 +8086,42 @@ var RelocateMediaInputSchema = object({
8071
8086
  /** Omitted = `move`, the pre-existing behaviour. */
8072
8087
  mode: MediaRelocateModeSchema.optional()
8073
8088
  });
8074
- /** How many rows still carry NO `locationId` — the population a repoint would
8075
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8076
- * value that permits a non-blocking `eventMedia` cutover. */
8077
- var UnstampedEventMediaCountSchema = object({
8078
- media: number().int().nonnegative(),
8079
- retrainFrames: number().int().nonnegative(),
8080
- total: number().int().nonnegative()
8089
+ /**
8090
+ * The unstamped population of ONE collection split, because the gate and the
8091
+ * operator ask two different questions and only one of them has to be cheap.
8092
+ *
8093
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8094
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8095
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8096
+ * that matters — after a seal, when the population is empty.
8097
+ *
8098
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8099
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8100
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8101
+ * and useful answer: "there are some, and this read could not say how many"
8102
+ * still refuses the cutover, which is the whole job.
8103
+ */
8104
+ var UnstampedRowsSchema = object({
8105
+ present: boolean(),
8106
+ rows: number().int().nonnegative().nullable()
8081
8107
  });
8108
+ /**
8109
+ * How many rows still carry NO `locationId` — the population a repoint would
8110
+ * silently re-aim at a disk that does not hold their bytes.
8111
+ *
8112
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8113
+ * over. The gate opens on a measured absence and on nothing else; an unread
8114
+ * collection and an empty one are different facts, and this repo has already
8115
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8116
+ */
8117
+ var UnstampedEventMediaCountSchema = object({
8118
+ media: UnstampedRowsSchema,
8119
+ retrainFrames: UnstampedRowsSchema,
8120
+ /** True when EITHER collection holds one. The refusal reads this. */
8121
+ anyPresent: boolean(),
8122
+ /** Sum across both, or `null` when either lane could not be counted. */
8123
+ total: number().int().nonnegative().nullable()
8124
+ }).nullable();
8082
8125
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8083
8126
  /** The independently selectable logical storage classes — every class
8084
8127
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8189,6 +8232,10 @@ var StorageMigrationMoveProgressSchema = object({
8189
8232
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8190
8233
  filesTotal: number().int().nonnegative().nullable(),
8191
8234
  bytesMoved: number().int().nonnegative(),
8235
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8236
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8237
+ * rows would be the same failure as one that silently skipped them. */
8238
+ rowsReconciled: number().int().nonnegative().optional(),
8192
8239
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8193
8240
  * crash gets a new mover, and a rate computed from the migration's start
8194
8241
  * would silently average in the time nothing was running. */
@@ -13003,6 +13050,114 @@ method(object({
13003
13050
  height: number()
13004
13051
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13005
13052
  /**
13053
+ * `failure-contribution` — the capability an addon reports its OWN losses
13054
+ * through, per camera, with the denominator attached. It stores nothing.
13055
+ *
13056
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13057
+ *
13058
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13059
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13060
+ * copied: the contributor reports what it already knows, hub-main adds only
13061
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13062
+ * somebody to forget to edit.
13063
+ *
13064
+ * They are not merged, because their invariants are opposites:
13065
+ *
13066
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13067
+ * claim a camera cost nothing, which is a measurement nobody made;
13068
+ * - a `failure-contribution` zero is the **most valuable value on the
13069
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13070
+ * and it is exactly what an absent entry cannot say.
13071
+ *
13072
+ * Putting a loss counter on a cost entry would also break the reconciliation
13073
+ * that gives `load-contribution` its point: contributions are subtracted from
13074
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13075
+ * has no process.
13076
+ *
13077
+ * ## Why not a log line, since the counters already exist
13078
+ *
13079
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13080
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13081
+ * ends in a log line, and a log line is the thing the operator asked to stop
13082
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13083
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13084
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13085
+ * media blackout were both diagnosed. The counters stay; this is where they can
13086
+ * be READ.
13087
+ *
13088
+ * ## The rate is served with its denominator or not at all
13089
+ *
13090
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13091
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13092
+ * than yesterday" and was **flat across twelve hours** once divided by the
13093
+ * successes on the same path. A surface that publishes only the numerator
13094
+ * reproduces that mistake on every read.
13095
+ *
13096
+ * ## Shape
13097
+ *
13098
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13099
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13100
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13101
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13102
+ * a forked runner's entries reach hub-main over transport that already exists.
13103
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13104
+ * result through `system.getFailureContributions`.
13105
+ */
13106
+ var FailureReasonCountSchema = object({
13107
+ /**
13108
+ * Why the attempt did not land, in the contributor's own vocabulary —
13109
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13110
+ * strings that already appear in this repo's logs and, where one exists, the
13111
+ * same string the per-track `previewMissReason` records (D276): a second
13112
+ * vocabulary for the same loss would make the row and the counter
13113
+ * un-joinable.
13114
+ */
13115
+ reason: string(),
13116
+ count: number().int().nonnegative()
13117
+ });
13118
+ var FailureContributionSchema = object({
13119
+ /**
13120
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13121
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13122
+ * `unit` free: the families are owned by different addons and a shared enum
13123
+ * is a central list that rots invisibly.
13124
+ */
13125
+ family: string(),
13126
+ /**
13127
+ * The NUMERIC device id — the same value every log line carries as
13128
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13129
+ * cannot name the camera must not emit the entry, because a fleet total
13130
+ * cannot answer the only question anybody asks of this surface.
13131
+ */
13132
+ deviceId: number().int().positive(),
13133
+ /**
13134
+ * A second dimension inside the family: the model / step id for an inference
13135
+ * timeout, so "which camera AND which model" is one read. Absent when the
13136
+ * family has a single variant.
13137
+ */
13138
+ variant: string().optional(),
13139
+ /**
13140
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13141
+ * differencing two reads must drop the interval when it changes, because the
13142
+ * counter restarted from zero in a respawned runner. Same discipline as
13143
+ * `LoadContribution.startedAtMs`.
13144
+ */
13145
+ sinceMs: number(),
13146
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13147
+ atMs: number(),
13148
+ /**
13149
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13150
+ * window. A failure count published without it is the mistake this schema
13151
+ * exists to make impossible.
13152
+ */
13153
+ attempts: number().int().nonnegative(),
13154
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13155
+ succeeded: number().int().nonnegative(),
13156
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13157
+ reasons: array(FailureReasonCountSchema).readonly()
13158
+ });
13159
+ method(_void(), array(FailureContributionSchema).readonly());
13160
+ /**
13006
13161
  * filesystem-browse — per-node capability for browsing the node's local
13007
13162
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13008
13163
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13524,6 +13679,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13524
13679
  kind: "mutation",
13525
13680
  auth: "admin"
13526
13681
  });
13682
+ var LoadContributionSchema = object({
13683
+ role: _enum([
13684
+ "decode",
13685
+ "transcode",
13686
+ "recording",
13687
+ "streaming",
13688
+ "detection"
13689
+ ]),
13690
+ /**
13691
+ * The NUMERIC device id — the same value every log line carries as
13692
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13693
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13694
+ * contributor that cannot name its camera must not emit the entry at all,
13695
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13696
+ * and would quietly turn one camera's cost into everybody's.
13697
+ */
13698
+ deviceId: number().int().positive().nullable(),
13699
+ attribution: _enum([
13700
+ "measured",
13701
+ "accounted",
13702
+ "unattributable"
13703
+ ]),
13704
+ /**
13705
+ * What ONE entry is, in the contributor's own words — `615/high`,
13706
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13707
+ * family and inventing a common one would lose the only information that
13708
+ * makes two entries for the same camera distinguishable.
13709
+ */
13710
+ unit: string(),
13711
+ /**
13712
+ * The OS process this cost lives in, when there is one. Present so a
13713
+ * consumer can (a) tell two generations of the same unit apart across a
13714
+ * restart, and (b) subtract claimed processes from the node's process
13715
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13716
+ * process of its own.
13717
+ */
13718
+ pid: number().int().positive().optional(),
13719
+ /**
13720
+ * When this generation started. The pid's incarnation marker: a consumer
13721
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13722
+ * window when this changes, because the counter restarted from zero in a new
13723
+ * process.
13724
+ */
13725
+ startedAtMs: number().optional(),
13726
+ /**
13727
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13728
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13729
+ * contribution is asked for.
13730
+ *
13731
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13732
+ * needs a sampler, and a new per-node sampler is the defect half of
13733
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13734
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13735
+ *
13736
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13737
+ * an entry with no process.
13738
+ */
13739
+ cpuSeconds: number().optional(),
13740
+ /** Resident bytes of this unit's process, same source and same rules. */
13741
+ rssBytes: number().optional()
13742
+ });
13743
+ method(_void(), array(LoadContributionSchema).readonly());
13527
13744
  /**
13528
13745
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13529
13746
  * through. It stores nothing.
@@ -13600,176 +13817,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13600
13817
  tags: record(string(), string()).optional()
13601
13818
  }), array(LogEntrySchema).readonly());
13602
13819
  /**
13603
- * `failure-contribution` — the capability an addon reports its OWN losses
13604
- * through, per camera, with the denominator attached. It stores nothing.
13605
- *
13606
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13607
- *
13608
- * `load-contribution` answers *what did this camera COST*. This answers *what
13609
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13610
- * copied: the contributor reports what it already knows, hub-main adds only
13611
- * `addonId`, nothing needs global knowledge, and there is no central list for
13612
- * somebody to forget to edit.
13613
- *
13614
- * They are not merged, because their invariants are opposites:
13615
- *
13616
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13617
- * claim a camera cost nothing, which is a measurement nobody made;
13618
- * - a `failure-contribution` zero is the **most valuable value on the
13619
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13620
- * and it is exactly what an absent entry cannot say.
13621
- *
13622
- * Putting a loss counter on a cost entry would also break the reconciliation
13623
- * that gives `load-contribution` its point: contributions are subtracted from
13624
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13625
- * has no process.
13626
- *
13627
- * ## Why not a log line, since the counters already exist
13628
- *
13629
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13630
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13631
- * ends in a log line, and a log line is the thing the operator asked to stop
13632
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13633
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13634
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13635
- * media blackout were both diagnosed. The counters stay; this is where they can
13636
- * be READ.
13637
- *
13638
- * ## The rate is served with its denominator or not at all
13639
- *
13640
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13641
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13642
- * than yesterday" and was **flat across twelve hours** once divided by the
13643
- * successes on the same path. A surface that publishes only the numerator
13644
- * reproduces that mistake on every read.
13645
- *
13646
- * ## Shape
13647
- *
13648
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13649
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13650
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13651
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13652
- * a forked runner's entries reach hub-main over transport that already exists.
13653
- * No new UDS message, no second registry (D3). The operator reads the assembled
13654
- * result through `system.getFailureContributions`.
13655
- */
13656
- var FailureReasonCountSchema = object({
13657
- /**
13658
- * Why the attempt did not land, in the contributor's own vocabulary —
13659
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13660
- * strings that already appear in this repo's logs and, where one exists, the
13661
- * same string the per-track `previewMissReason` records (D276): a second
13662
- * vocabulary for the same loss would make the row and the counter
13663
- * un-joinable.
13664
- */
13665
- reason: string(),
13666
- count: number().int().nonnegative()
13667
- });
13668
- var FailureContributionSchema = object({
13669
- /**
13670
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13671
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13672
- * `unit` free: the families are owned by different addons and a shared enum
13673
- * is a central list that rots invisibly.
13674
- */
13675
- family: string(),
13676
- /**
13677
- * The NUMERIC device id — the same value every log line carries as
13678
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13679
- * cannot name the camera must not emit the entry, because a fleet total
13680
- * cannot answer the only question anybody asks of this surface.
13681
- */
13682
- deviceId: number().int().positive(),
13683
- /**
13684
- * A second dimension inside the family: the model / step id for an inference
13685
- * timeout, so "which camera AND which model" is one read. Absent when the
13686
- * family has a single variant.
13687
- */
13688
- variant: string().optional(),
13689
- /**
13690
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13691
- * differencing two reads must drop the interval when it changes, because the
13692
- * counter restarted from zero in a respawned runner. Same discipline as
13693
- * `LoadContribution.startedAtMs`.
13694
- */
13695
- sinceMs: number(),
13696
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13697
- atMs: number(),
13698
- /**
13699
- * THE DENOMINATOR — every attempt on this path for this camera in the
13700
- * window. A failure count published without it is the mistake this schema
13701
- * exists to make impossible.
13702
- */
13703
- attempts: number().int().nonnegative(),
13704
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13705
- succeeded: number().int().nonnegative(),
13706
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13707
- reasons: array(FailureReasonCountSchema).readonly()
13708
- });
13709
- method(_void(), array(FailureContributionSchema).readonly());
13710
- var LoadContributionSchema = object({
13711
- role: _enum([
13712
- "decode",
13713
- "transcode",
13714
- "recording",
13715
- "streaming",
13716
- "detection"
13717
- ]),
13718
- /**
13719
- * The NUMERIC device id — the same value every log line carries as
13720
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13721
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13722
- * contributor that cannot name its camera must not emit the entry at all,
13723
- * because an unnamed per-camera entry is indistinguishable from a shared one
13724
- * and would quietly turn one camera's cost into everybody's.
13725
- */
13726
- deviceId: number().int().positive().nullable(),
13727
- attribution: _enum([
13728
- "measured",
13729
- "accounted",
13730
- "unattributable"
13731
- ]),
13732
- /**
13733
- * What ONE entry is, in the contributor's own words — `615/high`,
13734
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13735
- * family and inventing a common one would lose the only information that
13736
- * makes two entries for the same camera distinguishable.
13737
- */
13738
- unit: string(),
13739
- /**
13740
- * The OS process this cost lives in, when there is one. Present so a
13741
- * consumer can (a) tell two generations of the same unit apart across a
13742
- * restart, and (b) subtract claimed processes from the node's process
13743
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13744
- * process of its own.
13745
- */
13746
- pid: number().int().positive().optional(),
13747
- /**
13748
- * When this generation started. The pid's incarnation marker: a consumer
13749
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13750
- * window when this changes, because the counter restarted from zero in a new
13751
- * process.
13752
- */
13753
- startedAtMs: number().optional(),
13754
- /**
13755
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13756
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13757
- * contribution is asked for.
13758
- *
13759
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13760
- * needs a sampler, and a new per-node sampler is the defect half of
13761
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13762
- * by whoever already keeps a history; a rate cannot be un-averaged.
13763
- *
13764
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13765
- * an entry with no process.
13766
- */
13767
- cpuSeconds: number().optional(),
13768
- /** Resident bytes of this unit's process, same source and same rules. */
13769
- rssBytes: number().optional()
13770
- });
13771
- method(_void(), array(LoadContributionSchema).readonly());
13772
- /**
13773
13820
  * `login-method` — collection cap through which auth addons contribute
13774
13821
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13775
13822
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18429,12 +18476,53 @@ var MediaFileKindEnum = _enum([
18429
18476
  "keyFrameSmall",
18430
18477
  "thumbnailSmall"
18431
18478
  ]);
18479
+ /**
18480
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18481
+ * ARE — never the bytes themselves.
18482
+ *
18483
+ * ## Why `url` and not `base64`
18484
+ *
18485
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18486
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18487
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18488
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18489
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18490
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18491
+ *
18492
+ * `url` points at the `event-media` data plane
18493
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18494
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18495
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18496
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18497
+ * no less protected than they were inside a `view`-level cap response — see
18498
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18499
+ * (per-device scoping).
18500
+ *
18501
+ * The URL is built from the row's **stored** key, which is not always its
18502
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18503
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18504
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18505
+ *
18506
+ * ## `base64` is TRANSITIONAL and is going away
18507
+ *
18508
+ * It is still populated for one reason: the deployed viewer's track-detail
18509
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18510
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18511
+ * triangle — not as absence. Removing the field before that viewer ships is an
18512
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18513
+ * delete this line and the `withBytes` pass-through in
18514
+ * `analytics-query-facade.ts`; nothing else reads it.
18515
+ */
18432
18516
  var MediaFileSchema = object({
18433
18517
  key: string(),
18434
18518
  kind: MediaFileKindEnum,
18435
- base64: string(),
18436
18519
  sizeBytes: number(),
18437
18520
  timestamp: number()
18521
+ }).extend({
18522
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18523
+ url: string(),
18524
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18525
+ base64: string()
18438
18526
  });
18439
18527
  /**
18440
18528
  * One media row WITHOUT its bytes.
@@ -18446,7 +18534,9 @@ var MediaFileSchema = object({
18446
18534
  * blocks the whole view.
18447
18535
  *
18448
18536
  * `sizeBytes` is carried because it is what lets a client decide between the
18449
- * stored blob and a `?variant=thumb` rendering without fetching either.
18537
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18538
+ * `url` because a client that had to build the plane path itself is a second
18539
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18450
18540
  */
18451
18541
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18452
18542
  /**
@@ -19135,6 +19225,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19135
19225
  }), array(MediaFileSchema).readonly()), method(object({
19136
19226
  trackId: string(),
19137
19227
  deviceId: number()
19228
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19229
+ eventId: string(),
19230
+ deviceId: number()
19138
19231
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19139
19232
  kind: "mutation",
19140
19233
  auth: "admin"
@@ -23395,10 +23488,24 @@ var FaceClusterSchema = object({
23395
23488
  size: number().int(),
23396
23489
  cohesion: number()
23397
23490
  });
23491
+ /**
23492
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23493
+ * are — never the bytes.
23494
+ *
23495
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23496
+ * track/event contract) is still populated because a deployed viewer requires
23497
+ * the field to parse a row at all; this method has no such reader. Its ONE
23498
+ * caller is the admin UI's detail modal, which was building
23499
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23500
+ * dialog already rendering its key FRAME from the `event-media` plane.
23501
+ *
23502
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23503
+ * media key directly, so this needed no new plane and no new access decision.
23504
+ */
23398
23505
  var MediaFileLiteSchema$1 = object({
23399
23506
  key: string(),
23400
23507
  kind: string(),
23401
- base64: string(),
23508
+ url: string(),
23402
23509
  sizeBytes: number(),
23403
23510
  timestamp: number()
23404
23511
  });
@@ -25650,10 +25757,24 @@ var PlateInfoSchema = object({
25650
25757
  */
25651
25758
  cropUrl: string().optional()
25652
25759
  });
25760
+ /**
25761
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25762
+ * are — never the bytes.
25763
+ *
25764
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25765
+ * track/event contract) is still populated because a deployed viewer requires
25766
+ * the field to parse a row at all; this method has no such reader. Its ONE
25767
+ * caller is the admin UI's detail modal, which was building
25768
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25769
+ * dialog already rendering its key FRAME from the `event-media` plane.
25770
+ *
25771
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25772
+ * media key directly, so this needed no new plane and no new access decision.
25773
+ */
25653
25774
  var MediaFileLiteSchema = object({
25654
25775
  key: string(),
25655
25776
  kind: string(),
25656
- base64: string(),
25777
+ url: string(),
25657
25778
  sizeBytes: number(),
25658
25779
  timestamp: number()
25659
25780
  });
@@ -31666,6 +31787,12 @@ Object.freeze({
31666
31787
  addonId: null,
31667
31788
  access: "view"
31668
31789
  },
31790
+ "pipelineAnalytics.listEventMedia": {
31791
+ capName: "pipeline-analytics",
31792
+ capScope: "device",
31793
+ addonId: null,
31794
+ access: "view"
31795
+ },
31669
31796
  "pipelineAnalytics.listGroups": {
31670
31797
  capName: "pipeline-analytics",
31671
31798
  capScope: "device",
@@ -35289,6 +35416,11 @@ Object.freeze({
35289
35416
  form: "array",
35290
35417
  optional: false
35291
35418
  }],
35419
+ "pipelineAnalytics.listEventMedia": [{
35420
+ name: "deviceId",
35421
+ form: "single",
35422
+ optional: false
35423
+ }],
35292
35424
  "pipelineAnalytics.listGroups": [{
35293
35425
  name: "deviceIds",
35294
35426
  form: "array",
package/dist/index.mjs CHANGED
@@ -7999,6 +7999,21 @@ var RelocateJobSchema = object({
7999
7999
  bytesMoved: number().int(),
8000
8000
  /** Total files discovered up front; null while (or when) unknown. */
8001
8001
  filesTotal: number().int().nullable(),
8002
+ /**
8003
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8004
+ * made that nobody asked for, so it is reported where the operator reads the
8005
+ * job rather than only in a log line.
8006
+ *
8007
+ * A footage segment records its byte count in its own NAME, and the durable
8008
+ * hour row derives its aggregates from those names. A file that does not
8009
+ * match its name therefore makes the ledger's sums — and with them quota and
8010
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8011
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8012
+ *
8013
+ * Absent on lanes where the question has no meaning: a media blob's size is
8014
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8015
+ */
8016
+ rowsReconciled: number().int().nonnegative().optional(),
8002
8017
  startedAt: number(),
8003
8018
  finishedAt: number().nullable(),
8004
8019
  error: string().nullable()
@@ -8067,14 +8082,42 @@ var RelocateMediaInputSchema = object({
8067
8082
  /** Omitted = `move`, the pre-existing behaviour. */
8068
8083
  mode: MediaRelocateModeSchema.optional()
8069
8084
  });
8070
- /** How many rows still carry NO `locationId` — the population a repoint would
8071
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8072
- * value that permits a non-blocking `eventMedia` cutover. */
8073
- var UnstampedEventMediaCountSchema = object({
8074
- media: number().int().nonnegative(),
8075
- retrainFrames: number().int().nonnegative(),
8076
- total: number().int().nonnegative()
8085
+ /**
8086
+ * The unstamped population of ONE collection split, because the gate and the
8087
+ * operator ask two different questions and only one of them has to be cheap.
8088
+ *
8089
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8090
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8091
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8092
+ * that matters — after a seal, when the population is empty.
8093
+ *
8094
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8095
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8096
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8097
+ * and useful answer: "there are some, and this read could not say how many"
8098
+ * still refuses the cutover, which is the whole job.
8099
+ */
8100
+ var UnstampedRowsSchema = object({
8101
+ present: boolean(),
8102
+ rows: number().int().nonnegative().nullable()
8077
8103
  });
8104
+ /**
8105
+ * How many rows still carry NO `locationId` — the population a repoint would
8106
+ * silently re-aim at a disk that does not hold their bytes.
8107
+ *
8108
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8109
+ * over. The gate opens on a measured absence and on nothing else; an unread
8110
+ * collection and an empty one are different facts, and this repo has already
8111
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8112
+ */
8113
+ var UnstampedEventMediaCountSchema = object({
8114
+ media: UnstampedRowsSchema,
8115
+ retrainFrames: UnstampedRowsSchema,
8116
+ /** True when EITHER collection holds one. The refusal reads this. */
8117
+ anyPresent: boolean(),
8118
+ /** Sum across both, or `null` when either lane could not be counted. */
8119
+ total: number().int().nonnegative().nullable()
8120
+ }).nullable();
8078
8121
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8079
8122
  /** The independently selectable logical storage classes — every class
8080
8123
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8185,6 +8228,10 @@ var StorageMigrationMoveProgressSchema = object({
8185
8228
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8186
8229
  filesTotal: number().int().nonnegative().nullable(),
8187
8230
  bytesMoved: number().int().nonnegative(),
8231
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8232
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8233
+ * rows would be the same failure as one that silently skipped them. */
8234
+ rowsReconciled: number().int().nonnegative().optional(),
8188
8235
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8189
8236
  * crash gets a new mover, and a rate computed from the migration's start
8190
8237
  * would silently average in the time nothing was running. */
@@ -12999,6 +13046,114 @@ method(object({
12999
13046
  height: number()
13000
13047
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13001
13048
  /**
13049
+ * `failure-contribution` — the capability an addon reports its OWN losses
13050
+ * through, per camera, with the denominator attached. It stores nothing.
13051
+ *
13052
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13053
+ *
13054
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13055
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13056
+ * copied: the contributor reports what it already knows, hub-main adds only
13057
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13058
+ * somebody to forget to edit.
13059
+ *
13060
+ * They are not merged, because their invariants are opposites:
13061
+ *
13062
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13063
+ * claim a camera cost nothing, which is a measurement nobody made;
13064
+ * - a `failure-contribution` zero is the **most valuable value on the
13065
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13066
+ * and it is exactly what an absent entry cannot say.
13067
+ *
13068
+ * Putting a loss counter on a cost entry would also break the reconciliation
13069
+ * that gives `load-contribution` its point: contributions are subtracted from
13070
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13071
+ * has no process.
13072
+ *
13073
+ * ## Why not a log line, since the counters already exist
13074
+ *
13075
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13076
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13077
+ * ends in a log line, and a log line is the thing the operator asked to stop
13078
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13079
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13080
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13081
+ * media blackout were both diagnosed. The counters stay; this is where they can
13082
+ * be READ.
13083
+ *
13084
+ * ## The rate is served with its denominator or not at all
13085
+ *
13086
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13087
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13088
+ * than yesterday" and was **flat across twelve hours** once divided by the
13089
+ * successes on the same path. A surface that publishes only the numerator
13090
+ * reproduces that mistake on every read.
13091
+ *
13092
+ * ## Shape
13093
+ *
13094
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13095
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13096
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13097
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13098
+ * a forked runner's entries reach hub-main over transport that already exists.
13099
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13100
+ * result through `system.getFailureContributions`.
13101
+ */
13102
+ var FailureReasonCountSchema = object({
13103
+ /**
13104
+ * Why the attempt did not land, in the contributor's own vocabulary —
13105
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13106
+ * strings that already appear in this repo's logs and, where one exists, the
13107
+ * same string the per-track `previewMissReason` records (D276): a second
13108
+ * vocabulary for the same loss would make the row and the counter
13109
+ * un-joinable.
13110
+ */
13111
+ reason: string(),
13112
+ count: number().int().nonnegative()
13113
+ });
13114
+ var FailureContributionSchema = object({
13115
+ /**
13116
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13117
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13118
+ * `unit` free: the families are owned by different addons and a shared enum
13119
+ * is a central list that rots invisibly.
13120
+ */
13121
+ family: string(),
13122
+ /**
13123
+ * The NUMERIC device id — the same value every log line carries as
13124
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13125
+ * cannot name the camera must not emit the entry, because a fleet total
13126
+ * cannot answer the only question anybody asks of this surface.
13127
+ */
13128
+ deviceId: number().int().positive(),
13129
+ /**
13130
+ * A second dimension inside the family: the model / step id for an inference
13131
+ * timeout, so "which camera AND which model" is one read. Absent when the
13132
+ * family has a single variant.
13133
+ */
13134
+ variant: string().optional(),
13135
+ /**
13136
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13137
+ * differencing two reads must drop the interval when it changes, because the
13138
+ * counter restarted from zero in a respawned runner. Same discipline as
13139
+ * `LoadContribution.startedAtMs`.
13140
+ */
13141
+ sinceMs: number(),
13142
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13143
+ atMs: number(),
13144
+ /**
13145
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13146
+ * window. A failure count published without it is the mistake this schema
13147
+ * exists to make impossible.
13148
+ */
13149
+ attempts: number().int().nonnegative(),
13150
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13151
+ succeeded: number().int().nonnegative(),
13152
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13153
+ reasons: array(FailureReasonCountSchema).readonly()
13154
+ });
13155
+ method(_void(), array(FailureContributionSchema).readonly());
13156
+ /**
13002
13157
  * filesystem-browse — per-node capability for browsing the node's local
13003
13158
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13004
13159
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13520,6 +13675,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13520
13675
  kind: "mutation",
13521
13676
  auth: "admin"
13522
13677
  });
13678
+ var LoadContributionSchema = object({
13679
+ role: _enum([
13680
+ "decode",
13681
+ "transcode",
13682
+ "recording",
13683
+ "streaming",
13684
+ "detection"
13685
+ ]),
13686
+ /**
13687
+ * The NUMERIC device id — the same value every log line carries as
13688
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13689
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13690
+ * contributor that cannot name its camera must not emit the entry at all,
13691
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13692
+ * and would quietly turn one camera's cost into everybody's.
13693
+ */
13694
+ deviceId: number().int().positive().nullable(),
13695
+ attribution: _enum([
13696
+ "measured",
13697
+ "accounted",
13698
+ "unattributable"
13699
+ ]),
13700
+ /**
13701
+ * What ONE entry is, in the contributor's own words — `615/high`,
13702
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13703
+ * family and inventing a common one would lose the only information that
13704
+ * makes two entries for the same camera distinguishable.
13705
+ */
13706
+ unit: string(),
13707
+ /**
13708
+ * The OS process this cost lives in, when there is one. Present so a
13709
+ * consumer can (a) tell two generations of the same unit apart across a
13710
+ * restart, and (b) subtract claimed processes from the node's process
13711
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13712
+ * process of its own.
13713
+ */
13714
+ pid: number().int().positive().optional(),
13715
+ /**
13716
+ * When this generation started. The pid's incarnation marker: a consumer
13717
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13718
+ * window when this changes, because the counter restarted from zero in a new
13719
+ * process.
13720
+ */
13721
+ startedAtMs: number().optional(),
13722
+ /**
13723
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13724
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13725
+ * contribution is asked for.
13726
+ *
13727
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13728
+ * needs a sampler, and a new per-node sampler is the defect half of
13729
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13730
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13731
+ *
13732
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13733
+ * an entry with no process.
13734
+ */
13735
+ cpuSeconds: number().optional(),
13736
+ /** Resident bytes of this unit's process, same source and same rules. */
13737
+ rssBytes: number().optional()
13738
+ });
13739
+ method(_void(), array(LoadContributionSchema).readonly());
13523
13740
  /**
13524
13741
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13525
13742
  * through. It stores nothing.
@@ -13596,176 +13813,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13596
13813
  tags: record(string(), string()).optional()
13597
13814
  }), array(LogEntrySchema).readonly());
13598
13815
  /**
13599
- * `failure-contribution` — the capability an addon reports its OWN losses
13600
- * through, per camera, with the denominator attached. It stores nothing.
13601
- *
13602
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13603
- *
13604
- * `load-contribution` answers *what did this camera COST*. This answers *what
13605
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13606
- * copied: the contributor reports what it already knows, hub-main adds only
13607
- * `addonId`, nothing needs global knowledge, and there is no central list for
13608
- * somebody to forget to edit.
13609
- *
13610
- * They are not merged, because their invariants are opposites:
13611
- *
13612
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13613
- * claim a camera cost nothing, which is a measurement nobody made;
13614
- * - a `failure-contribution` zero is the **most valuable value on the
13615
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13616
- * and it is exactly what an absent entry cannot say.
13617
- *
13618
- * Putting a loss counter on a cost entry would also break the reconciliation
13619
- * that gives `load-contribution` its point: contributions are subtracted from
13620
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13621
- * has no process.
13622
- *
13623
- * ## Why not a log line, since the counters already exist
13624
- *
13625
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13626
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13627
- * ends in a log line, and a log line is the thing the operator asked to stop
13628
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13629
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13630
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13631
- * media blackout were both diagnosed. The counters stay; this is where they can
13632
- * be READ.
13633
- *
13634
- * ## The rate is served with its denominator or not at all
13635
- *
13636
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13637
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13638
- * than yesterday" and was **flat across twelve hours** once divided by the
13639
- * successes on the same path. A surface that publishes only the numerator
13640
- * reproduces that mistake on every read.
13641
- *
13642
- * ## Shape
13643
- *
13644
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13645
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13646
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13647
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13648
- * a forked runner's entries reach hub-main over transport that already exists.
13649
- * No new UDS message, no second registry (D3). The operator reads the assembled
13650
- * result through `system.getFailureContributions`.
13651
- */
13652
- var FailureReasonCountSchema = object({
13653
- /**
13654
- * Why the attempt did not land, in the contributor's own vocabulary —
13655
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13656
- * strings that already appear in this repo's logs and, where one exists, the
13657
- * same string the per-track `previewMissReason` records (D276): a second
13658
- * vocabulary for the same loss would make the row and the counter
13659
- * un-joinable.
13660
- */
13661
- reason: string(),
13662
- count: number().int().nonnegative()
13663
- });
13664
- var FailureContributionSchema = object({
13665
- /**
13666
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13667
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13668
- * `unit` free: the families are owned by different addons and a shared enum
13669
- * is a central list that rots invisibly.
13670
- */
13671
- family: string(),
13672
- /**
13673
- * The NUMERIC device id — the same value every log line carries as
13674
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13675
- * cannot name the camera must not emit the entry, because a fleet total
13676
- * cannot answer the only question anybody asks of this surface.
13677
- */
13678
- deviceId: number().int().positive(),
13679
- /**
13680
- * A second dimension inside the family: the model / step id for an inference
13681
- * timeout, so "which camera AND which model" is one read. Absent when the
13682
- * family has a single variant.
13683
- */
13684
- variant: string().optional(),
13685
- /**
13686
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13687
- * differencing two reads must drop the interval when it changes, because the
13688
- * counter restarted from zero in a respawned runner. Same discipline as
13689
- * `LoadContribution.startedAtMs`.
13690
- */
13691
- sinceMs: number(),
13692
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13693
- atMs: number(),
13694
- /**
13695
- * THE DENOMINATOR — every attempt on this path for this camera in the
13696
- * window. A failure count published without it is the mistake this schema
13697
- * exists to make impossible.
13698
- */
13699
- attempts: number().int().nonnegative(),
13700
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13701
- succeeded: number().int().nonnegative(),
13702
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13703
- reasons: array(FailureReasonCountSchema).readonly()
13704
- });
13705
- method(_void(), array(FailureContributionSchema).readonly());
13706
- var LoadContributionSchema = object({
13707
- role: _enum([
13708
- "decode",
13709
- "transcode",
13710
- "recording",
13711
- "streaming",
13712
- "detection"
13713
- ]),
13714
- /**
13715
- * The NUMERIC device id — the same value every log line carries as
13716
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13717
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13718
- * contributor that cannot name its camera must not emit the entry at all,
13719
- * because an unnamed per-camera entry is indistinguishable from a shared one
13720
- * and would quietly turn one camera's cost into everybody's.
13721
- */
13722
- deviceId: number().int().positive().nullable(),
13723
- attribution: _enum([
13724
- "measured",
13725
- "accounted",
13726
- "unattributable"
13727
- ]),
13728
- /**
13729
- * What ONE entry is, in the contributor's own words — `615/high`,
13730
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13731
- * family and inventing a common one would lose the only information that
13732
- * makes two entries for the same camera distinguishable.
13733
- */
13734
- unit: string(),
13735
- /**
13736
- * The OS process this cost lives in, when there is one. Present so a
13737
- * consumer can (a) tell two generations of the same unit apart across a
13738
- * restart, and (b) subtract claimed processes from the node's process
13739
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13740
- * process of its own.
13741
- */
13742
- pid: number().int().positive().optional(),
13743
- /**
13744
- * When this generation started. The pid's incarnation marker: a consumer
13745
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13746
- * window when this changes, because the counter restarted from zero in a new
13747
- * process.
13748
- */
13749
- startedAtMs: number().optional(),
13750
- /**
13751
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13752
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13753
- * contribution is asked for.
13754
- *
13755
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13756
- * needs a sampler, and a new per-node sampler is the defect half of
13757
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13758
- * by whoever already keeps a history; a rate cannot be un-averaged.
13759
- *
13760
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13761
- * an entry with no process.
13762
- */
13763
- cpuSeconds: number().optional(),
13764
- /** Resident bytes of this unit's process, same source and same rules. */
13765
- rssBytes: number().optional()
13766
- });
13767
- method(_void(), array(LoadContributionSchema).readonly());
13768
- /**
13769
13816
  * `login-method` — collection cap through which auth addons contribute
13770
13817
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13771
13818
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18425,12 +18472,53 @@ var MediaFileKindEnum = _enum([
18425
18472
  "keyFrameSmall",
18426
18473
  "thumbnailSmall"
18427
18474
  ]);
18475
+ /**
18476
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18477
+ * ARE — never the bytes themselves.
18478
+ *
18479
+ * ## Why `url` and not `base64`
18480
+ *
18481
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18482
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18483
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18484
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18485
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18486
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18487
+ *
18488
+ * `url` points at the `event-media` data plane
18489
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18490
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18491
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18492
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18493
+ * no less protected than they were inside a `view`-level cap response — see
18494
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18495
+ * (per-device scoping).
18496
+ *
18497
+ * The URL is built from the row's **stored** key, which is not always its
18498
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18499
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18500
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18501
+ *
18502
+ * ## `base64` is TRANSITIONAL and is going away
18503
+ *
18504
+ * It is still populated for one reason: the deployed viewer's track-detail
18505
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18506
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18507
+ * triangle — not as absence. Removing the field before that viewer ships is an
18508
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18509
+ * delete this line and the `withBytes` pass-through in
18510
+ * `analytics-query-facade.ts`; nothing else reads it.
18511
+ */
18428
18512
  var MediaFileSchema = object({
18429
18513
  key: string(),
18430
18514
  kind: MediaFileKindEnum,
18431
- base64: string(),
18432
18515
  sizeBytes: number(),
18433
18516
  timestamp: number()
18517
+ }).extend({
18518
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18519
+ url: string(),
18520
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18521
+ base64: string()
18434
18522
  });
18435
18523
  /**
18436
18524
  * One media row WITHOUT its bytes.
@@ -18442,7 +18530,9 @@ var MediaFileSchema = object({
18442
18530
  * blocks the whole view.
18443
18531
  *
18444
18532
  * `sizeBytes` is carried because it is what lets a client decide between the
18445
- * stored blob and a `?variant=thumb` rendering without fetching either.
18533
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18534
+ * `url` because a client that had to build the plane path itself is a second
18535
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18446
18536
  */
18447
18537
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18448
18538
  /**
@@ -19131,6 +19221,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19131
19221
  }), array(MediaFileSchema).readonly()), method(object({
19132
19222
  trackId: string(),
19133
19223
  deviceId: number()
19224
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19225
+ eventId: string(),
19226
+ deviceId: number()
19134
19227
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19135
19228
  kind: "mutation",
19136
19229
  auth: "admin"
@@ -23391,10 +23484,24 @@ var FaceClusterSchema = object({
23391
23484
  size: number().int(),
23392
23485
  cohesion: number()
23393
23486
  });
23487
+ /**
23488
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23489
+ * are — never the bytes.
23490
+ *
23491
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23492
+ * track/event contract) is still populated because a deployed viewer requires
23493
+ * the field to parse a row at all; this method has no such reader. Its ONE
23494
+ * caller is the admin UI's detail modal, which was building
23495
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23496
+ * dialog already rendering its key FRAME from the `event-media` plane.
23497
+ *
23498
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23499
+ * media key directly, so this needed no new plane and no new access decision.
23500
+ */
23394
23501
  var MediaFileLiteSchema$1 = object({
23395
23502
  key: string(),
23396
23503
  kind: string(),
23397
- base64: string(),
23504
+ url: string(),
23398
23505
  sizeBytes: number(),
23399
23506
  timestamp: number()
23400
23507
  });
@@ -25646,10 +25753,24 @@ var PlateInfoSchema = object({
25646
25753
  */
25647
25754
  cropUrl: string().optional()
25648
25755
  });
25756
+ /**
25757
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25758
+ * are — never the bytes.
25759
+ *
25760
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25761
+ * track/event contract) is still populated because a deployed viewer requires
25762
+ * the field to parse a row at all; this method has no such reader. Its ONE
25763
+ * caller is the admin UI's detail modal, which was building
25764
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25765
+ * dialog already rendering its key FRAME from the `event-media` plane.
25766
+ *
25767
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25768
+ * media key directly, so this needed no new plane and no new access decision.
25769
+ */
25649
25770
  var MediaFileLiteSchema = object({
25650
25771
  key: string(),
25651
25772
  kind: string(),
25652
- base64: string(),
25773
+ url: string(),
25653
25774
  sizeBytes: number(),
25654
25775
  timestamp: number()
25655
25776
  });
@@ -31662,6 +31783,12 @@ Object.freeze({
31662
31783
  addonId: null,
31663
31784
  access: "view"
31664
31785
  },
31786
+ "pipelineAnalytics.listEventMedia": {
31787
+ capName: "pipeline-analytics",
31788
+ capScope: "device",
31789
+ addonId: null,
31790
+ access: "view"
31791
+ },
31665
31792
  "pipelineAnalytics.listGroups": {
31666
31793
  capName: "pipeline-analytics",
31667
31794
  capScope: "device",
@@ -35285,6 +35412,11 @@ Object.freeze({
35285
35412
  form: "array",
35286
35413
  optional: false
35287
35414
  }],
35415
+ "pipelineAnalytics.listEventMedia": [{
35416
+ name: "deviceId",
35417
+ form: "single",
35418
+ optional: false
35419
+ }],
35288
35420
  "pipelineAnalytics.listGroups": [{
35289
35421
  name: "deviceIds",
35290
35422
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.45",
3
+ "version": "1.2.47",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",