@camstack/addon-smtp-nodemailer 1.2.44 → 1.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8024,6 +8024,21 @@ var RelocateJobSchema = object({
8024
8024
  bytesMoved: number().int(),
8025
8025
  /** Total files discovered up front; null while (or when) unknown. */
8026
8026
  filesTotal: number().int().nullable(),
8027
+ /**
8028
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8029
+ * made that nobody asked for, so it is reported where the operator reads the
8030
+ * job rather than only in a log line.
8031
+ *
8032
+ * A footage segment records its byte count in its own NAME, and the durable
8033
+ * hour row derives its aggregates from those names. A file that does not
8034
+ * match its name therefore makes the ledger's sums — and with them quota and
8035
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8036
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8037
+ *
8038
+ * Absent on lanes where the question has no meaning: a media blob's size is
8039
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8040
+ */
8041
+ rowsReconciled: number().int().nonnegative().optional(),
8027
8042
  startedAt: number(),
8028
8043
  finishedAt: number().nullable(),
8029
8044
  error: string().nullable()
@@ -8092,14 +8107,42 @@ var RelocateMediaInputSchema = object({
8092
8107
  /** Omitted = `move`, the pre-existing behaviour. */
8093
8108
  mode: MediaRelocateModeSchema.optional()
8094
8109
  });
8095
- /** How many rows still carry NO `locationId` — the population a repoint would
8096
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8097
- * value that permits a non-blocking `eventMedia` cutover. */
8098
- var UnstampedEventMediaCountSchema = object({
8099
- media: number().int().nonnegative(),
8100
- retrainFrames: number().int().nonnegative(),
8101
- total: number().int().nonnegative()
8110
+ /**
8111
+ * The unstamped population of ONE collection split, because the gate and the
8112
+ * operator ask two different questions and only one of them has to be cheap.
8113
+ *
8114
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8115
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8116
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8117
+ * that matters — after a seal, when the population is empty.
8118
+ *
8119
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8120
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8121
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8122
+ * and useful answer: "there are some, and this read could not say how many"
8123
+ * still refuses the cutover, which is the whole job.
8124
+ */
8125
+ var UnstampedRowsSchema = object({
8126
+ present: boolean(),
8127
+ rows: number().int().nonnegative().nullable()
8102
8128
  });
8129
+ /**
8130
+ * How many rows still carry NO `locationId` — the population a repoint would
8131
+ * silently re-aim at a disk that does not hold their bytes.
8132
+ *
8133
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8134
+ * over. The gate opens on a measured absence and on nothing else; an unread
8135
+ * collection and an empty one are different facts, and this repo has already
8136
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8137
+ */
8138
+ var UnstampedEventMediaCountSchema = object({
8139
+ media: UnstampedRowsSchema,
8140
+ retrainFrames: UnstampedRowsSchema,
8141
+ /** True when EITHER collection holds one. The refusal reads this. */
8142
+ anyPresent: boolean(),
8143
+ /** Sum across both, or `null` when either lane could not be counted. */
8144
+ total: number().int().nonnegative().nullable()
8145
+ }).nullable();
8103
8146
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8104
8147
  /** The independently selectable logical storage classes — every class
8105
8148
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8210,6 +8253,10 @@ var StorageMigrationMoveProgressSchema = object({
8210
8253
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8211
8254
  filesTotal: number().int().nonnegative().nullable(),
8212
8255
  bytesMoved: number().int().nonnegative(),
8256
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8257
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8258
+ * rows would be the same failure as one that silently skipped them. */
8259
+ rowsReconciled: number().int().nonnegative().optional(),
8213
8260
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8214
8261
  * crash gets a new mover, and a rate computed from the migration's start
8215
8262
  * would silently average in the time nothing was running. */
@@ -12894,6 +12941,114 @@ method(object({
12894
12941
  height: number()
12895
12942
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12896
12943
  /**
12944
+ * `failure-contribution` — the capability an addon reports its OWN losses
12945
+ * through, per camera, with the denominator attached. It stores nothing.
12946
+ *
12947
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12948
+ *
12949
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12950
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12951
+ * copied: the contributor reports what it already knows, hub-main adds only
12952
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12953
+ * somebody to forget to edit.
12954
+ *
12955
+ * They are not merged, because their invariants are opposites:
12956
+ *
12957
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12958
+ * claim a camera cost nothing, which is a measurement nobody made;
12959
+ * - a `failure-contribution` zero is the **most valuable value on the
12960
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12961
+ * and it is exactly what an absent entry cannot say.
12962
+ *
12963
+ * Putting a loss counter on a cost entry would also break the reconciliation
12964
+ * that gives `load-contribution` its point: contributions are subtracted from
12965
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12966
+ * has no process.
12967
+ *
12968
+ * ## Why not a log line, since the counters already exist
12969
+ *
12970
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12971
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12972
+ * ends in a log line, and a log line is the thing the operator asked to stop
12973
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12974
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12975
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12976
+ * media blackout were both diagnosed. The counters stay; this is where they can
12977
+ * be READ.
12978
+ *
12979
+ * ## The rate is served with its denominator or not at all
12980
+ *
12981
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12982
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12983
+ * than yesterday" and was **flat across twelve hours** once divided by the
12984
+ * successes on the same path. A surface that publishes only the numerator
12985
+ * reproduces that mistake on every read.
12986
+ *
12987
+ * ## Shape
12988
+ *
12989
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12990
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12991
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12992
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12993
+ * a forked runner's entries reach hub-main over transport that already exists.
12994
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12995
+ * result through `system.getFailureContributions`.
12996
+ */
12997
+ var FailureReasonCountSchema = object({
12998
+ /**
12999
+ * Why the attempt did not land, in the contributor's own vocabulary —
13000
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13001
+ * strings that already appear in this repo's logs and, where one exists, the
13002
+ * same string the per-track `previewMissReason` records (D276): a second
13003
+ * vocabulary for the same loss would make the row and the counter
13004
+ * un-joinable.
13005
+ */
13006
+ reason: string(),
13007
+ count: number().int().nonnegative()
13008
+ });
13009
+ var FailureContributionSchema = object({
13010
+ /**
13011
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13012
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13013
+ * `unit` free: the families are owned by different addons and a shared enum
13014
+ * is a central list that rots invisibly.
13015
+ */
13016
+ family: string(),
13017
+ /**
13018
+ * The NUMERIC device id — the same value every log line carries as
13019
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13020
+ * cannot name the camera must not emit the entry, because a fleet total
13021
+ * cannot answer the only question anybody asks of this surface.
13022
+ */
13023
+ deviceId: number().int().positive(),
13024
+ /**
13025
+ * A second dimension inside the family: the model / step id for an inference
13026
+ * timeout, so "which camera AND which model" is one read. Absent when the
13027
+ * family has a single variant.
13028
+ */
13029
+ variant: string().optional(),
13030
+ /**
13031
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13032
+ * differencing two reads must drop the interval when it changes, because the
13033
+ * counter restarted from zero in a respawned runner. Same discipline as
13034
+ * `LoadContribution.startedAtMs`.
13035
+ */
13036
+ sinceMs: number(),
13037
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13038
+ atMs: number(),
13039
+ /**
13040
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13041
+ * window. A failure count published without it is the mistake this schema
13042
+ * exists to make impossible.
13043
+ */
13044
+ attempts: number().int().nonnegative(),
13045
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13046
+ succeeded: number().int().nonnegative(),
13047
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13048
+ reasons: array(FailureReasonCountSchema).readonly()
13049
+ });
13050
+ method(_void(), array(FailureContributionSchema).readonly());
13051
+ /**
12897
13052
  * filesystem-browse — per-node capability for browsing the node's local
12898
13053
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12899
13054
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13415,6 +13570,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13415
13570
  kind: "mutation",
13416
13571
  auth: "admin"
13417
13572
  });
13573
+ var LoadContributionSchema = object({
13574
+ role: _enum([
13575
+ "decode",
13576
+ "transcode",
13577
+ "recording",
13578
+ "streaming",
13579
+ "detection"
13580
+ ]),
13581
+ /**
13582
+ * The NUMERIC device id — the same value every log line carries as
13583
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13584
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13585
+ * contributor that cannot name its camera must not emit the entry at all,
13586
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13587
+ * and would quietly turn one camera's cost into everybody's.
13588
+ */
13589
+ deviceId: number().int().positive().nullable(),
13590
+ attribution: _enum([
13591
+ "measured",
13592
+ "accounted",
13593
+ "unattributable"
13594
+ ]),
13595
+ /**
13596
+ * What ONE entry is, in the contributor's own words — `615/high`,
13597
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13598
+ * family and inventing a common one would lose the only information that
13599
+ * makes two entries for the same camera distinguishable.
13600
+ */
13601
+ unit: string(),
13602
+ /**
13603
+ * The OS process this cost lives in, when there is one. Present so a
13604
+ * consumer can (a) tell two generations of the same unit apart across a
13605
+ * restart, and (b) subtract claimed processes from the node's process
13606
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13607
+ * process of its own.
13608
+ */
13609
+ pid: number().int().positive().optional(),
13610
+ /**
13611
+ * When this generation started. The pid's incarnation marker: a consumer
13612
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13613
+ * window when this changes, because the counter restarted from zero in a new
13614
+ * process.
13615
+ */
13616
+ startedAtMs: number().optional(),
13617
+ /**
13618
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13619
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13620
+ * contribution is asked for.
13621
+ *
13622
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13623
+ * needs a sampler, and a new per-node sampler is the defect half of
13624
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13625
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13626
+ *
13627
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13628
+ * an entry with no process.
13629
+ */
13630
+ cpuSeconds: number().optional(),
13631
+ /** Resident bytes of this unit's process, same source and same rules. */
13632
+ rssBytes: number().optional()
13633
+ });
13634
+ method(_void(), array(LoadContributionSchema).readonly());
13418
13635
  /**
13419
13636
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13420
13637
  * through. It stores nothing.
@@ -13491,176 +13708,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13491
13708
  tags: record(string(), string()).optional()
13492
13709
  }), array(LogEntrySchema).readonly());
13493
13710
  /**
13494
- * `failure-contribution` — the capability an addon reports its OWN losses
13495
- * through, per camera, with the denominator attached. It stores nothing.
13496
- *
13497
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13498
- *
13499
- * `load-contribution` answers *what did this camera COST*. This answers *what
13500
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13501
- * copied: the contributor reports what it already knows, hub-main adds only
13502
- * `addonId`, nothing needs global knowledge, and there is no central list for
13503
- * somebody to forget to edit.
13504
- *
13505
- * They are not merged, because their invariants are opposites:
13506
- *
13507
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13508
- * claim a camera cost nothing, which is a measurement nobody made;
13509
- * - a `failure-contribution` zero is the **most valuable value on the
13510
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13511
- * and it is exactly what an absent entry cannot say.
13512
- *
13513
- * Putting a loss counter on a cost entry would also break the reconciliation
13514
- * that gives `load-contribution` its point: contributions are subtracted from
13515
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13516
- * has no process.
13517
- *
13518
- * ## Why not a log line, since the counters already exist
13519
- *
13520
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13521
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13522
- * ends in a log line, and a log line is the thing the operator asked to stop
13523
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13524
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13525
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13526
- * media blackout were both diagnosed. The counters stay; this is where they can
13527
- * be READ.
13528
- *
13529
- * ## The rate is served with its denominator or not at all
13530
- *
13531
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13532
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13533
- * than yesterday" and was **flat across twelve hours** once divided by the
13534
- * successes on the same path. A surface that publishes only the numerator
13535
- * reproduces that mistake on every read.
13536
- *
13537
- * ## Shape
13538
- *
13539
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13540
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13541
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13542
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13543
- * a forked runner's entries reach hub-main over transport that already exists.
13544
- * No new UDS message, no second registry (D3). The operator reads the assembled
13545
- * result through `system.getFailureContributions`.
13546
- */
13547
- var FailureReasonCountSchema = object({
13548
- /**
13549
- * Why the attempt did not land, in the contributor's own vocabulary —
13550
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13551
- * strings that already appear in this repo's logs and, where one exists, the
13552
- * same string the per-track `previewMissReason` records (D276): a second
13553
- * vocabulary for the same loss would make the row and the counter
13554
- * un-joinable.
13555
- */
13556
- reason: string(),
13557
- count: number().int().nonnegative()
13558
- });
13559
- var FailureContributionSchema = object({
13560
- /**
13561
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13562
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13563
- * `unit` free: the families are owned by different addons and a shared enum
13564
- * is a central list that rots invisibly.
13565
- */
13566
- family: string(),
13567
- /**
13568
- * The NUMERIC device id — the same value every log line carries as
13569
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13570
- * cannot name the camera must not emit the entry, because a fleet total
13571
- * cannot answer the only question anybody asks of this surface.
13572
- */
13573
- deviceId: number().int().positive(),
13574
- /**
13575
- * A second dimension inside the family: the model / step id for an inference
13576
- * timeout, so "which camera AND which model" is one read. Absent when the
13577
- * family has a single variant.
13578
- */
13579
- variant: string().optional(),
13580
- /**
13581
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13582
- * differencing two reads must drop the interval when it changes, because the
13583
- * counter restarted from zero in a respawned runner. Same discipline as
13584
- * `LoadContribution.startedAtMs`.
13585
- */
13586
- sinceMs: number(),
13587
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13588
- atMs: number(),
13589
- /**
13590
- * THE DENOMINATOR — every attempt on this path for this camera in the
13591
- * window. A failure count published without it is the mistake this schema
13592
- * exists to make impossible.
13593
- */
13594
- attempts: number().int().nonnegative(),
13595
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13596
- succeeded: number().int().nonnegative(),
13597
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13598
- reasons: array(FailureReasonCountSchema).readonly()
13599
- });
13600
- method(_void(), array(FailureContributionSchema).readonly());
13601
- var LoadContributionSchema = object({
13602
- role: _enum([
13603
- "decode",
13604
- "transcode",
13605
- "recording",
13606
- "streaming",
13607
- "detection"
13608
- ]),
13609
- /**
13610
- * The NUMERIC device id — the same value every log line carries as
13611
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13612
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13613
- * contributor that cannot name its camera must not emit the entry at all,
13614
- * because an unnamed per-camera entry is indistinguishable from a shared one
13615
- * and would quietly turn one camera's cost into everybody's.
13616
- */
13617
- deviceId: number().int().positive().nullable(),
13618
- attribution: _enum([
13619
- "measured",
13620
- "accounted",
13621
- "unattributable"
13622
- ]),
13623
- /**
13624
- * What ONE entry is, in the contributor's own words — `615/high`,
13625
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13626
- * family and inventing a common one would lose the only information that
13627
- * makes two entries for the same camera distinguishable.
13628
- */
13629
- unit: string(),
13630
- /**
13631
- * The OS process this cost lives in, when there is one. Present so a
13632
- * consumer can (a) tell two generations of the same unit apart across a
13633
- * restart, and (b) subtract claimed processes from the node's process
13634
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13635
- * process of its own.
13636
- */
13637
- pid: number().int().positive().optional(),
13638
- /**
13639
- * When this generation started. The pid's incarnation marker: a consumer
13640
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13641
- * window when this changes, because the counter restarted from zero in a new
13642
- * process.
13643
- */
13644
- startedAtMs: number().optional(),
13645
- /**
13646
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13647
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13648
- * contribution is asked for.
13649
- *
13650
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13651
- * needs a sampler, and a new per-node sampler is the defect half of
13652
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13653
- * by whoever already keeps a history; a rate cannot be un-averaged.
13654
- *
13655
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13656
- * an entry with no process.
13657
- */
13658
- cpuSeconds: number().optional(),
13659
- /** Resident bytes of this unit's process, same source and same rules. */
13660
- rssBytes: number().optional()
13661
- });
13662
- method(_void(), array(LoadContributionSchema).readonly());
13663
- /**
13664
13711
  * `login-method` — collection cap through which auth addons contribute
13665
13712
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13666
13713
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18320,12 +18367,53 @@ var MediaFileKindEnum = _enum([
18320
18367
  "keyFrameSmall",
18321
18368
  "thumbnailSmall"
18322
18369
  ]);
18370
+ /**
18371
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18372
+ * ARE — never the bytes themselves.
18373
+ *
18374
+ * ## Why `url` and not `base64`
18375
+ *
18376
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18377
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18378
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18379
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18380
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18381
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18382
+ *
18383
+ * `url` points at the `event-media` data plane
18384
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18385
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18386
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18387
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18388
+ * no less protected than they were inside a `view`-level cap response — see
18389
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18390
+ * (per-device scoping).
18391
+ *
18392
+ * The URL is built from the row's **stored** key, which is not always its
18393
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18394
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18395
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18396
+ *
18397
+ * ## `base64` is TRANSITIONAL and is going away
18398
+ *
18399
+ * It is still populated for one reason: the deployed viewer's track-detail
18400
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18401
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18402
+ * triangle — not as absence. Removing the field before that viewer ships is an
18403
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18404
+ * delete this line and the `withBytes` pass-through in
18405
+ * `analytics-query-facade.ts`; nothing else reads it.
18406
+ */
18323
18407
  var MediaFileSchema = object({
18324
18408
  key: string(),
18325
18409
  kind: MediaFileKindEnum,
18326
- base64: string(),
18327
18410
  sizeBytes: number(),
18328
18411
  timestamp: number()
18412
+ }).extend({
18413
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18414
+ url: string(),
18415
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18416
+ base64: string()
18329
18417
  });
18330
18418
  /**
18331
18419
  * One media row WITHOUT its bytes.
@@ -18337,7 +18425,9 @@ var MediaFileSchema = object({
18337
18425
  * blocks the whole view.
18338
18426
  *
18339
18427
  * `sizeBytes` is carried because it is what lets a client decide between the
18340
- * stored blob and a `?variant=thumb` rendering without fetching either.
18428
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18429
+ * `url` because a client that had to build the plane path itself is a second
18430
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18341
18431
  */
18342
18432
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18343
18433
  /**
@@ -19026,6 +19116,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19026
19116
  }), array(MediaFileSchema).readonly()), method(object({
19027
19117
  trackId: string(),
19028
19118
  deviceId: number()
19119
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19120
+ eventId: string(),
19121
+ deviceId: number()
19029
19122
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19030
19123
  kind: "mutation",
19031
19124
  auth: "admin"
@@ -23299,10 +23392,24 @@ var FaceClusterSchema = object({
23299
23392
  size: number().int(),
23300
23393
  cohesion: number()
23301
23394
  });
23395
+ /**
23396
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23397
+ * are — never the bytes.
23398
+ *
23399
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23400
+ * track/event contract) is still populated because a deployed viewer requires
23401
+ * the field to parse a row at all; this method has no such reader. Its ONE
23402
+ * caller is the admin UI's detail modal, which was building
23403
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23404
+ * dialog already rendering its key FRAME from the `event-media` plane.
23405
+ *
23406
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23407
+ * media key directly, so this needed no new plane and no new access decision.
23408
+ */
23302
23409
  var MediaFileLiteSchema$1 = object({
23303
23410
  key: string(),
23304
23411
  kind: string(),
23305
- base64: string(),
23412
+ url: string(),
23306
23413
  sizeBytes: number(),
23307
23414
  timestamp: number()
23308
23415
  });
@@ -25554,10 +25661,24 @@ var PlateInfoSchema = object({
25554
25661
  */
25555
25662
  cropUrl: string().optional()
25556
25663
  });
25664
+ /**
25665
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25666
+ * are — never the bytes.
25667
+ *
25668
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25669
+ * track/event contract) is still populated because a deployed viewer requires
25670
+ * the field to parse a row at all; this method has no such reader. Its ONE
25671
+ * caller is the admin UI's detail modal, which was building
25672
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25673
+ * dialog already rendering its key FRAME from the `event-media` plane.
25674
+ *
25675
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25676
+ * media key directly, so this needed no new plane and no new access decision.
25677
+ */
25557
25678
  var MediaFileLiteSchema = object({
25558
25679
  key: string(),
25559
25680
  kind: string(),
25560
- base64: string(),
25681
+ url: string(),
25561
25682
  sizeBytes: number(),
25562
25683
  timestamp: number()
25563
25684
  });
@@ -31570,6 +31691,12 @@ Object.freeze({
31570
31691
  addonId: null,
31571
31692
  access: "view"
31572
31693
  },
31694
+ "pipelineAnalytics.listEventMedia": {
31695
+ capName: "pipeline-analytics",
31696
+ capScope: "device",
31697
+ addonId: null,
31698
+ access: "view"
31699
+ },
31573
31700
  "pipelineAnalytics.listGroups": {
31574
31701
  capName: "pipeline-analytics",
31575
31702
  capScope: "device",
@@ -35193,6 +35320,11 @@ Object.freeze({
35193
35320
  form: "array",
35194
35321
  optional: false
35195
35322
  }],
35323
+ "pipelineAnalytics.listEventMedia": [{
35324
+ name: "deviceId",
35325
+ form: "single",
35326
+ optional: false
35327
+ }],
35196
35328
  "pipelineAnalytics.listGroups": [{
35197
35329
  name: "deviceIds",
35198
35330
  form: "array",
@@ -8022,6 +8022,21 @@ var RelocateJobSchema = object({
8022
8022
  bytesMoved: number().int(),
8023
8023
  /** Total files discovered up front; null while (or when) unknown. */
8024
8024
  filesTotal: number().int().nullable(),
8025
+ /**
8026
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8027
+ * made that nobody asked for, so it is reported where the operator reads the
8028
+ * job rather than only in a log line.
8029
+ *
8030
+ * A footage segment records its byte count in its own NAME, and the durable
8031
+ * hour row derives its aggregates from those names. A file that does not
8032
+ * match its name therefore makes the ledger's sums — and with them quota and
8033
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8034
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8035
+ *
8036
+ * Absent on lanes where the question has no meaning: a media blob's size is
8037
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8038
+ */
8039
+ rowsReconciled: number().int().nonnegative().optional(),
8025
8040
  startedAt: number(),
8026
8041
  finishedAt: number().nullable(),
8027
8042
  error: string().nullable()
@@ -8090,14 +8105,42 @@ var RelocateMediaInputSchema = object({
8090
8105
  /** Omitted = `move`, the pre-existing behaviour. */
8091
8106
  mode: MediaRelocateModeSchema.optional()
8092
8107
  });
8093
- /** How many rows still carry NO `locationId` — the population a repoint would
8094
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8095
- * value that permits a non-blocking `eventMedia` cutover. */
8096
- var UnstampedEventMediaCountSchema = object({
8097
- media: number().int().nonnegative(),
8098
- retrainFrames: number().int().nonnegative(),
8099
- total: number().int().nonnegative()
8108
+ /**
8109
+ * The unstamped population of ONE collection split, because the gate and the
8110
+ * operator ask two different questions and only one of them has to be cheap.
8111
+ *
8112
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8113
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8114
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8115
+ * that matters — after a seal, when the population is empty.
8116
+ *
8117
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8118
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8119
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8120
+ * and useful answer: "there are some, and this read could not say how many"
8121
+ * still refuses the cutover, which is the whole job.
8122
+ */
8123
+ var UnstampedRowsSchema = object({
8124
+ present: boolean(),
8125
+ rows: number().int().nonnegative().nullable()
8100
8126
  });
8127
+ /**
8128
+ * How many rows still carry NO `locationId` — the population a repoint would
8129
+ * silently re-aim at a disk that does not hold their bytes.
8130
+ *
8131
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8132
+ * over. The gate opens on a measured absence and on nothing else; an unread
8133
+ * collection and an empty one are different facts, and this repo has already
8134
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8135
+ */
8136
+ var UnstampedEventMediaCountSchema = object({
8137
+ media: UnstampedRowsSchema,
8138
+ retrainFrames: UnstampedRowsSchema,
8139
+ /** True when EITHER collection holds one. The refusal reads this. */
8140
+ anyPresent: boolean(),
8141
+ /** Sum across both, or `null` when either lane could not be counted. */
8142
+ total: number().int().nonnegative().nullable()
8143
+ }).nullable();
8101
8144
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8102
8145
  /** The independently selectable logical storage classes — every class
8103
8146
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8208,6 +8251,10 @@ var StorageMigrationMoveProgressSchema = object({
8208
8251
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8209
8252
  filesTotal: number().int().nonnegative().nullable(),
8210
8253
  bytesMoved: number().int().nonnegative(),
8254
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8255
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8256
+ * rows would be the same failure as one that silently skipped them. */
8257
+ rowsReconciled: number().int().nonnegative().optional(),
8211
8258
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8212
8259
  * crash gets a new mover, and a rate computed from the migration's start
8213
8260
  * would silently average in the time nothing was running. */
@@ -12892,6 +12939,114 @@ method(object({
12892
12939
  height: number()
12893
12940
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12894
12941
  /**
12942
+ * `failure-contribution` — the capability an addon reports its OWN losses
12943
+ * through, per camera, with the denominator attached. It stores nothing.
12944
+ *
12945
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12946
+ *
12947
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12948
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12949
+ * copied: the contributor reports what it already knows, hub-main adds only
12950
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12951
+ * somebody to forget to edit.
12952
+ *
12953
+ * They are not merged, because their invariants are opposites:
12954
+ *
12955
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12956
+ * claim a camera cost nothing, which is a measurement nobody made;
12957
+ * - a `failure-contribution` zero is the **most valuable value on the
12958
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12959
+ * and it is exactly what an absent entry cannot say.
12960
+ *
12961
+ * Putting a loss counter on a cost entry would also break the reconciliation
12962
+ * that gives `load-contribution` its point: contributions are subtracted from
12963
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12964
+ * has no process.
12965
+ *
12966
+ * ## Why not a log line, since the counters already exist
12967
+ *
12968
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12969
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12970
+ * ends in a log line, and a log line is the thing the operator asked to stop
12971
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12972
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12973
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12974
+ * media blackout were both diagnosed. The counters stay; this is where they can
12975
+ * be READ.
12976
+ *
12977
+ * ## The rate is served with its denominator or not at all
12978
+ *
12979
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12980
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12981
+ * than yesterday" and was **flat across twelve hours** once divided by the
12982
+ * successes on the same path. A surface that publishes only the numerator
12983
+ * reproduces that mistake on every read.
12984
+ *
12985
+ * ## Shape
12986
+ *
12987
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12988
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12989
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12990
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12991
+ * a forked runner's entries reach hub-main over transport that already exists.
12992
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12993
+ * result through `system.getFailureContributions`.
12994
+ */
12995
+ var FailureReasonCountSchema = object({
12996
+ /**
12997
+ * Why the attempt did not land, in the contributor's own vocabulary —
12998
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12999
+ * strings that already appear in this repo's logs and, where one exists, the
13000
+ * same string the per-track `previewMissReason` records (D276): a second
13001
+ * vocabulary for the same loss would make the row and the counter
13002
+ * un-joinable.
13003
+ */
13004
+ reason: string(),
13005
+ count: number().int().nonnegative()
13006
+ });
13007
+ var FailureContributionSchema = object({
13008
+ /**
13009
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13010
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13011
+ * `unit` free: the families are owned by different addons and a shared enum
13012
+ * is a central list that rots invisibly.
13013
+ */
13014
+ family: string(),
13015
+ /**
13016
+ * The NUMERIC device id — the same value every log line carries as
13017
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13018
+ * cannot name the camera must not emit the entry, because a fleet total
13019
+ * cannot answer the only question anybody asks of this surface.
13020
+ */
13021
+ deviceId: number().int().positive(),
13022
+ /**
13023
+ * A second dimension inside the family: the model / step id for an inference
13024
+ * timeout, so "which camera AND which model" is one read. Absent when the
13025
+ * family has a single variant.
13026
+ */
13027
+ variant: string().optional(),
13028
+ /**
13029
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13030
+ * differencing two reads must drop the interval when it changes, because the
13031
+ * counter restarted from zero in a respawned runner. Same discipline as
13032
+ * `LoadContribution.startedAtMs`.
13033
+ */
13034
+ sinceMs: number(),
13035
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13036
+ atMs: number(),
13037
+ /**
13038
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13039
+ * window. A failure count published without it is the mistake this schema
13040
+ * exists to make impossible.
13041
+ */
13042
+ attempts: number().int().nonnegative(),
13043
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13044
+ succeeded: number().int().nonnegative(),
13045
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13046
+ reasons: array(FailureReasonCountSchema).readonly()
13047
+ });
13048
+ method(_void(), array(FailureContributionSchema).readonly());
13049
+ /**
12895
13050
  * filesystem-browse — per-node capability for browsing the node's local
12896
13051
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12897
13052
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13413,6 +13568,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13413
13568
  kind: "mutation",
13414
13569
  auth: "admin"
13415
13570
  });
13571
+ var LoadContributionSchema = object({
13572
+ role: _enum([
13573
+ "decode",
13574
+ "transcode",
13575
+ "recording",
13576
+ "streaming",
13577
+ "detection"
13578
+ ]),
13579
+ /**
13580
+ * The NUMERIC device id — the same value every log line carries as
13581
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13582
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13583
+ * contributor that cannot name its camera must not emit the entry at all,
13584
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13585
+ * and would quietly turn one camera's cost into everybody's.
13586
+ */
13587
+ deviceId: number().int().positive().nullable(),
13588
+ attribution: _enum([
13589
+ "measured",
13590
+ "accounted",
13591
+ "unattributable"
13592
+ ]),
13593
+ /**
13594
+ * What ONE entry is, in the contributor's own words — `615/high`,
13595
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13596
+ * family and inventing a common one would lose the only information that
13597
+ * makes two entries for the same camera distinguishable.
13598
+ */
13599
+ unit: string(),
13600
+ /**
13601
+ * The OS process this cost lives in, when there is one. Present so a
13602
+ * consumer can (a) tell two generations of the same unit apart across a
13603
+ * restart, and (b) subtract claimed processes from the node's process
13604
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13605
+ * process of its own.
13606
+ */
13607
+ pid: number().int().positive().optional(),
13608
+ /**
13609
+ * When this generation started. The pid's incarnation marker: a consumer
13610
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13611
+ * window when this changes, because the counter restarted from zero in a new
13612
+ * process.
13613
+ */
13614
+ startedAtMs: number().optional(),
13615
+ /**
13616
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13617
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13618
+ * contribution is asked for.
13619
+ *
13620
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13621
+ * needs a sampler, and a new per-node sampler is the defect half of
13622
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13623
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13624
+ *
13625
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13626
+ * an entry with no process.
13627
+ */
13628
+ cpuSeconds: number().optional(),
13629
+ /** Resident bytes of this unit's process, same source and same rules. */
13630
+ rssBytes: number().optional()
13631
+ });
13632
+ method(_void(), array(LoadContributionSchema).readonly());
13416
13633
  /**
13417
13634
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13418
13635
  * through. It stores nothing.
@@ -13489,176 +13706,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13489
13706
  tags: record(string(), string()).optional()
13490
13707
  }), array(LogEntrySchema).readonly());
13491
13708
  /**
13492
- * `failure-contribution` — the capability an addon reports its OWN losses
13493
- * through, per camera, with the denominator attached. It stores nothing.
13494
- *
13495
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13496
- *
13497
- * `load-contribution` answers *what did this camera COST*. This answers *what
13498
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13499
- * copied: the contributor reports what it already knows, hub-main adds only
13500
- * `addonId`, nothing needs global knowledge, and there is no central list for
13501
- * somebody to forget to edit.
13502
- *
13503
- * They are not merged, because their invariants are opposites:
13504
- *
13505
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13506
- * claim a camera cost nothing, which is a measurement nobody made;
13507
- * - a `failure-contribution` zero is the **most valuable value on the
13508
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13509
- * and it is exactly what an absent entry cannot say.
13510
- *
13511
- * Putting a loss counter on a cost entry would also break the reconciliation
13512
- * that gives `load-contribution` its point: contributions are subtracted from
13513
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13514
- * has no process.
13515
- *
13516
- * ## Why not a log line, since the counters already exist
13517
- *
13518
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13519
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13520
- * ends in a log line, and a log line is the thing the operator asked to stop
13521
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13522
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13523
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13524
- * media blackout were both diagnosed. The counters stay; this is where they can
13525
- * be READ.
13526
- *
13527
- * ## The rate is served with its denominator or not at all
13528
- *
13529
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13530
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13531
- * than yesterday" and was **flat across twelve hours** once divided by the
13532
- * successes on the same path. A surface that publishes only the numerator
13533
- * reproduces that mistake on every read.
13534
- *
13535
- * ## Shape
13536
- *
13537
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13538
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13539
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13540
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13541
- * a forked runner's entries reach hub-main over transport that already exists.
13542
- * No new UDS message, no second registry (D3). The operator reads the assembled
13543
- * result through `system.getFailureContributions`.
13544
- */
13545
- var FailureReasonCountSchema = object({
13546
- /**
13547
- * Why the attempt did not land, in the contributor's own vocabulary —
13548
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13549
- * strings that already appear in this repo's logs and, where one exists, the
13550
- * same string the per-track `previewMissReason` records (D276): a second
13551
- * vocabulary for the same loss would make the row and the counter
13552
- * un-joinable.
13553
- */
13554
- reason: string(),
13555
- count: number().int().nonnegative()
13556
- });
13557
- var FailureContributionSchema = object({
13558
- /**
13559
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13560
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13561
- * `unit` free: the families are owned by different addons and a shared enum
13562
- * is a central list that rots invisibly.
13563
- */
13564
- family: string(),
13565
- /**
13566
- * The NUMERIC device id — the same value every log line carries as
13567
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13568
- * cannot name the camera must not emit the entry, because a fleet total
13569
- * cannot answer the only question anybody asks of this surface.
13570
- */
13571
- deviceId: number().int().positive(),
13572
- /**
13573
- * A second dimension inside the family: the model / step id for an inference
13574
- * timeout, so "which camera AND which model" is one read. Absent when the
13575
- * family has a single variant.
13576
- */
13577
- variant: string().optional(),
13578
- /**
13579
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13580
- * differencing two reads must drop the interval when it changes, because the
13581
- * counter restarted from zero in a respawned runner. Same discipline as
13582
- * `LoadContribution.startedAtMs`.
13583
- */
13584
- sinceMs: number(),
13585
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13586
- atMs: number(),
13587
- /**
13588
- * THE DENOMINATOR — every attempt on this path for this camera in the
13589
- * window. A failure count published without it is the mistake this schema
13590
- * exists to make impossible.
13591
- */
13592
- attempts: number().int().nonnegative(),
13593
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13594
- succeeded: number().int().nonnegative(),
13595
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13596
- reasons: array(FailureReasonCountSchema).readonly()
13597
- });
13598
- method(_void(), array(FailureContributionSchema).readonly());
13599
- var LoadContributionSchema = object({
13600
- role: _enum([
13601
- "decode",
13602
- "transcode",
13603
- "recording",
13604
- "streaming",
13605
- "detection"
13606
- ]),
13607
- /**
13608
- * The NUMERIC device id — the same value every log line carries as
13609
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13610
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13611
- * contributor that cannot name its camera must not emit the entry at all,
13612
- * because an unnamed per-camera entry is indistinguishable from a shared one
13613
- * and would quietly turn one camera's cost into everybody's.
13614
- */
13615
- deviceId: number().int().positive().nullable(),
13616
- attribution: _enum([
13617
- "measured",
13618
- "accounted",
13619
- "unattributable"
13620
- ]),
13621
- /**
13622
- * What ONE entry is, in the contributor's own words — `615/high`,
13623
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13624
- * family and inventing a common one would lose the only information that
13625
- * makes two entries for the same camera distinguishable.
13626
- */
13627
- unit: string(),
13628
- /**
13629
- * The OS process this cost lives in, when there is one. Present so a
13630
- * consumer can (a) tell two generations of the same unit apart across a
13631
- * restart, and (b) subtract claimed processes from the node's process
13632
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13633
- * process of its own.
13634
- */
13635
- pid: number().int().positive().optional(),
13636
- /**
13637
- * When this generation started. The pid's incarnation marker: a consumer
13638
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13639
- * window when this changes, because the counter restarted from zero in a new
13640
- * process.
13641
- */
13642
- startedAtMs: number().optional(),
13643
- /**
13644
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13645
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13646
- * contribution is asked for.
13647
- *
13648
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13649
- * needs a sampler, and a new per-node sampler is the defect half of
13650
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13651
- * by whoever already keeps a history; a rate cannot be un-averaged.
13652
- *
13653
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13654
- * an entry with no process.
13655
- */
13656
- cpuSeconds: number().optional(),
13657
- /** Resident bytes of this unit's process, same source and same rules. */
13658
- rssBytes: number().optional()
13659
- });
13660
- method(_void(), array(LoadContributionSchema).readonly());
13661
- /**
13662
13709
  * `login-method` — collection cap through which auth addons contribute
13663
13710
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13664
13711
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18318,12 +18365,53 @@ var MediaFileKindEnum = _enum([
18318
18365
  "keyFrameSmall",
18319
18366
  "thumbnailSmall"
18320
18367
  ]);
18368
+ /**
18369
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18370
+ * ARE — never the bytes themselves.
18371
+ *
18372
+ * ## Why `url` and not `base64`
18373
+ *
18374
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18375
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18376
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18377
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18378
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18379
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18380
+ *
18381
+ * `url` points at the `event-media` data plane
18382
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18383
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18384
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18385
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18386
+ * no less protected than they were inside a `view`-level cap response — see
18387
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18388
+ * (per-device scoping).
18389
+ *
18390
+ * The URL is built from the row's **stored** key, which is not always its
18391
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18392
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18393
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18394
+ *
18395
+ * ## `base64` is TRANSITIONAL and is going away
18396
+ *
18397
+ * It is still populated for one reason: the deployed viewer's track-detail
18398
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18399
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18400
+ * triangle — not as absence. Removing the field before that viewer ships is an
18401
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18402
+ * delete this line and the `withBytes` pass-through in
18403
+ * `analytics-query-facade.ts`; nothing else reads it.
18404
+ */
18321
18405
  var MediaFileSchema = object({
18322
18406
  key: string(),
18323
18407
  kind: MediaFileKindEnum,
18324
- base64: string(),
18325
18408
  sizeBytes: number(),
18326
18409
  timestamp: number()
18410
+ }).extend({
18411
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18412
+ url: string(),
18413
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18414
+ base64: string()
18327
18415
  });
18328
18416
  /**
18329
18417
  * One media row WITHOUT its bytes.
@@ -18335,7 +18423,9 @@ var MediaFileSchema = object({
18335
18423
  * blocks the whole view.
18336
18424
  *
18337
18425
  * `sizeBytes` is carried because it is what lets a client decide between the
18338
- * stored blob and a `?variant=thumb` rendering without fetching either.
18426
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18427
+ * `url` because a client that had to build the plane path itself is a second
18428
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18339
18429
  */
18340
18430
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18341
18431
  /**
@@ -19024,6 +19114,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19024
19114
  }), array(MediaFileSchema).readonly()), method(object({
19025
19115
  trackId: string(),
19026
19116
  deviceId: number()
19117
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19118
+ eventId: string(),
19119
+ deviceId: number()
19027
19120
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19028
19121
  kind: "mutation",
19029
19122
  auth: "admin"
@@ -23297,10 +23390,24 @@ var FaceClusterSchema = object({
23297
23390
  size: number().int(),
23298
23391
  cohesion: number()
23299
23392
  });
23393
+ /**
23394
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23395
+ * are — never the bytes.
23396
+ *
23397
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23398
+ * track/event contract) is still populated because a deployed viewer requires
23399
+ * the field to parse a row at all; this method has no such reader. Its ONE
23400
+ * caller is the admin UI's detail modal, which was building
23401
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23402
+ * dialog already rendering its key FRAME from the `event-media` plane.
23403
+ *
23404
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23405
+ * media key directly, so this needed no new plane and no new access decision.
23406
+ */
23300
23407
  var MediaFileLiteSchema$1 = object({
23301
23408
  key: string(),
23302
23409
  kind: string(),
23303
- base64: string(),
23410
+ url: string(),
23304
23411
  sizeBytes: number(),
23305
23412
  timestamp: number()
23306
23413
  });
@@ -25552,10 +25659,24 @@ var PlateInfoSchema = object({
25552
25659
  */
25553
25660
  cropUrl: string().optional()
25554
25661
  });
25662
+ /**
25663
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25664
+ * are — never the bytes.
25665
+ *
25666
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25667
+ * track/event contract) is still populated because a deployed viewer requires
25668
+ * the field to parse a row at all; this method has no such reader. Its ONE
25669
+ * caller is the admin UI's detail modal, which was building
25670
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25671
+ * dialog already rendering its key FRAME from the `event-media` plane.
25672
+ *
25673
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25674
+ * media key directly, so this needed no new plane and no new access decision.
25675
+ */
25555
25676
  var MediaFileLiteSchema = object({
25556
25677
  key: string(),
25557
25678
  kind: string(),
25558
- base64: string(),
25679
+ url: string(),
25559
25680
  sizeBytes: number(),
25560
25681
  timestamp: number()
25561
25682
  });
@@ -31568,6 +31689,12 @@ Object.freeze({
31568
31689
  addonId: null,
31569
31690
  access: "view"
31570
31691
  },
31692
+ "pipelineAnalytics.listEventMedia": {
31693
+ capName: "pipeline-analytics",
31694
+ capScope: "device",
31695
+ addonId: null,
31696
+ access: "view"
31697
+ },
31571
31698
  "pipelineAnalytics.listGroups": {
31572
31699
  capName: "pipeline-analytics",
31573
31700
  capScope: "device",
@@ -35191,6 +35318,11 @@ Object.freeze({
35191
35318
  form: "array",
35192
35319
  optional: false
35193
35320
  }],
35321
+ "pipelineAnalytics.listEventMedia": [{
35322
+ name: "deviceId",
35323
+ form: "single",
35324
+ optional: false
35325
+ }],
35194
35326
  "pipelineAnalytics.listGroups": [{
35195
35327
  name: "deviceIds",
35196
35328
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.44",
3
+ "version": "1.2.46",
4
4
  "description": "SMTP email provider addon for CamStack — wraps `nodemailer` and registers a `smtp-provider` cap collection entry. Used by magic-link login + notifier addons.",
5
5
  "keywords": [
6
6
  "camstack",