@camstack/addon-provider-reolink 1.2.68 → 1.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +332 -200
  2. package/dist/addon.mjs +332 -200
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8293,6 +8293,21 @@ var RelocateJobSchema = object({
8293
8293
  bytesMoved: number().int(),
8294
8294
  /** Total files discovered up front; null while (or when) unknown. */
8295
8295
  filesTotal: number().int().nullable(),
8296
+ /**
8297
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8298
+ * made that nobody asked for, so it is reported where the operator reads the
8299
+ * job rather than only in a log line.
8300
+ *
8301
+ * A footage segment records its byte count in its own NAME, and the durable
8302
+ * hour row derives its aggregates from those names. A file that does not
8303
+ * match its name therefore makes the ledger's sums — and with them quota and
8304
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8305
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8306
+ *
8307
+ * Absent on lanes where the question has no meaning: a media blob's size is
8308
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8309
+ */
8310
+ rowsReconciled: number().int().nonnegative().optional(),
8296
8311
  startedAt: number(),
8297
8312
  finishedAt: number().nullable(),
8298
8313
  error: string().nullable()
@@ -8361,14 +8376,42 @@ var RelocateMediaInputSchema = object({
8361
8376
  /** Omitted = `move`, the pre-existing behaviour. */
8362
8377
  mode: MediaRelocateModeSchema.optional()
8363
8378
  });
8364
- /** How many rows still carry NO `locationId` — the population a repoint would
8365
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8366
- * value that permits a non-blocking `eventMedia` cutover. */
8367
- var UnstampedEventMediaCountSchema = object({
8368
- media: number().int().nonnegative(),
8369
- retrainFrames: number().int().nonnegative(),
8370
- total: number().int().nonnegative()
8379
+ /**
8380
+ * The unstamped population of ONE collection split, because the gate and the
8381
+ * operator ask two different questions and only one of them has to be cheap.
8382
+ *
8383
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8384
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8385
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8386
+ * that matters — after a seal, when the population is empty.
8387
+ *
8388
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8389
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8390
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8391
+ * and useful answer: "there are some, and this read could not say how many"
8392
+ * still refuses the cutover, which is the whole job.
8393
+ */
8394
+ var UnstampedRowsSchema = object({
8395
+ present: boolean(),
8396
+ rows: number().int().nonnegative().nullable()
8371
8397
  });
8398
+ /**
8399
+ * How many rows still carry NO `locationId` — the population a repoint would
8400
+ * silently re-aim at a disk that does not hold their bytes.
8401
+ *
8402
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8403
+ * over. The gate opens on a measured absence and on nothing else; an unread
8404
+ * collection and an empty one are different facts, and this repo has already
8405
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8406
+ */
8407
+ var UnstampedEventMediaCountSchema = object({
8408
+ media: UnstampedRowsSchema,
8409
+ retrainFrames: UnstampedRowsSchema,
8410
+ /** True when EITHER collection holds one. The refusal reads this. */
8411
+ anyPresent: boolean(),
8412
+ /** Sum across both, or `null` when either lane could not be counted. */
8413
+ total: number().int().nonnegative().nullable()
8414
+ }).nullable();
8372
8415
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8373
8416
  /** The independently selectable logical storage classes — every class
8374
8417
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8479,6 +8522,10 @@ var StorageMigrationMoveProgressSchema = object({
8479
8522
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8480
8523
  filesTotal: number().int().nonnegative().nullable(),
8481
8524
  bytesMoved: number().int().nonnegative(),
8525
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8526
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8527
+ * rows would be the same failure as one that silently skipped them. */
8528
+ rowsReconciled: number().int().nonnegative().optional(),
8482
8529
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8483
8530
  * crash gets a new mover, and a rate computed from the migration's start
8484
8531
  * would silently average in the time nothing was running. */
@@ -13720,6 +13767,133 @@ method(object({
13720
13767
  height: number()
13721
13768
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13722
13769
  /**
13770
+ * `failure-contribution` — the capability an addon reports its OWN losses
13771
+ * through, per camera, with the denominator attached. It stores nothing.
13772
+ *
13773
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13774
+ *
13775
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13776
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13777
+ * copied: the contributor reports what it already knows, hub-main adds only
13778
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13779
+ * somebody to forget to edit.
13780
+ *
13781
+ * They are not merged, because their invariants are opposites:
13782
+ *
13783
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13784
+ * claim a camera cost nothing, which is a measurement nobody made;
13785
+ * - a `failure-contribution` zero is the **most valuable value on the
13786
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13787
+ * and it is exactly what an absent entry cannot say.
13788
+ *
13789
+ * Putting a loss counter on a cost entry would also break the reconciliation
13790
+ * that gives `load-contribution` its point: contributions are subtracted from
13791
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13792
+ * has no process.
13793
+ *
13794
+ * ## Why not a log line, since the counters already exist
13795
+ *
13796
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13797
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13798
+ * ends in a log line, and a log line is the thing the operator asked to stop
13799
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13800
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13801
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13802
+ * media blackout were both diagnosed. The counters stay; this is where they can
13803
+ * be READ.
13804
+ *
13805
+ * ## The rate is served with its denominator or not at all
13806
+ *
13807
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13808
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13809
+ * than yesterday" and was **flat across twelve hours** once divided by the
13810
+ * successes on the same path. A surface that publishes only the numerator
13811
+ * reproduces that mistake on every read.
13812
+ *
13813
+ * ## Shape
13814
+ *
13815
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13816
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13817
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13818
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13819
+ * a forked runner's entries reach hub-main over transport that already exists.
13820
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13821
+ * result through `system.getFailureContributions`.
13822
+ */
13823
+ var FailureReasonCountSchema = object({
13824
+ /**
13825
+ * Why the attempt did not land, in the contributor's own vocabulary —
13826
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13827
+ * strings that already appear in this repo's logs and, where one exists, the
13828
+ * same string the per-track `previewMissReason` records (D276): a second
13829
+ * vocabulary for the same loss would make the row and the counter
13830
+ * un-joinable.
13831
+ */
13832
+ reason: string(),
13833
+ count: number().int().nonnegative()
13834
+ });
13835
+ var FailureContributionSchema = object({
13836
+ /**
13837
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13838
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13839
+ * `unit` free: the families are owned by different addons and a shared enum
13840
+ * is a central list that rots invisibly.
13841
+ */
13842
+ family: string(),
13843
+ /**
13844
+ * The NUMERIC device id — the same value every log line carries as
13845
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13846
+ * cannot name the camera must not emit the entry, because a fleet total
13847
+ * cannot answer the only question anybody asks of this surface.
13848
+ */
13849
+ deviceId: number().int().positive(),
13850
+ /**
13851
+ * A second dimension inside the family: the model / step id for an inference
13852
+ * timeout, so "which camera AND which model" is one read. Absent when the
13853
+ * family has a single variant.
13854
+ */
13855
+ variant: string().optional(),
13856
+ /**
13857
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13858
+ * differencing two reads must drop the interval when it changes, because the
13859
+ * counter restarted from zero in a respawned runner. Same discipline as
13860
+ * `LoadContribution.startedAtMs`.
13861
+ */
13862
+ sinceMs: number(),
13863
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13864
+ atMs: number(),
13865
+ /**
13866
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13867
+ * window. A failure count published without it is the mistake this schema
13868
+ * exists to make impossible.
13869
+ */
13870
+ attempts: number().int().nonnegative(),
13871
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13872
+ succeeded: number().int().nonnegative(),
13873
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13874
+ reasons: array(FailureReasonCountSchema).readonly()
13875
+ });
13876
+ var failureContributionCapability = {
13877
+ name: "failure-contribution",
13878
+ scope: "system",
13879
+ mode: "collection",
13880
+ internal: true,
13881
+ methods: {
13882
+ /**
13883
+ * This addon's per-camera failure counters, read live from bounded in-RAM
13884
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
13885
+ *
13886
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
13887
+ * consumer that wants a rate differences two reads. A draining read would
13888
+ * make two operators with the page open each destroy half of the other's
13889
+ * numbers, and `load-contribution` already settled the same question the
13890
+ * same way for `cpuSeconds`.
13891
+ */
13892
+ list: method(_void(), array(FailureContributionSchema).readonly()) },
13893
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
13894
+ mount: { kind: "skip" }
13895
+ };
13896
+ /**
13723
13897
  * filesystem-browse — per-node capability for browsing the node's local
13724
13898
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13725
13899
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14241,6 +14415,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14241
14415
  kind: "mutation",
14242
14416
  auth: "admin"
14243
14417
  });
14418
+ var LoadContributionSchema = object({
14419
+ role: _enum([
14420
+ "decode",
14421
+ "transcode",
14422
+ "recording",
14423
+ "streaming",
14424
+ "detection"
14425
+ ]),
14426
+ /**
14427
+ * The NUMERIC device id — the same value every log line carries as
14428
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14429
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14430
+ * contributor that cannot name its camera must not emit the entry at all,
14431
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14432
+ * and would quietly turn one camera's cost into everybody's.
14433
+ */
14434
+ deviceId: number().int().positive().nullable(),
14435
+ attribution: _enum([
14436
+ "measured",
14437
+ "accounted",
14438
+ "unattributable"
14439
+ ]),
14440
+ /**
14441
+ * What ONE entry is, in the contributor's own words — `615/high`,
14442
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14443
+ * family and inventing a common one would lose the only information that
14444
+ * makes two entries for the same camera distinguishable.
14445
+ */
14446
+ unit: string(),
14447
+ /**
14448
+ * The OS process this cost lives in, when there is one. Present so a
14449
+ * consumer can (a) tell two generations of the same unit apart across a
14450
+ * restart, and (b) subtract claimed processes from the node's process
14451
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14452
+ * process of its own.
14453
+ */
14454
+ pid: number().int().positive().optional(),
14455
+ /**
14456
+ * When this generation started. The pid's incarnation marker: a consumer
14457
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14458
+ * window when this changes, because the counter restarted from zero in a new
14459
+ * process.
14460
+ */
14461
+ startedAtMs: number().optional(),
14462
+ /**
14463
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14464
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14465
+ * contribution is asked for.
14466
+ *
14467
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14468
+ * needs a sampler, and a new per-node sampler is the defect half of
14469
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14470
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14471
+ *
14472
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14473
+ * an entry with no process.
14474
+ */
14475
+ cpuSeconds: number().optional(),
14476
+ /** Resident bytes of this unit's process, same source and same rules. */
14477
+ rssBytes: number().optional()
14478
+ });
14479
+ method(_void(), array(LoadContributionSchema).readonly());
14244
14480
  /**
14245
14481
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14246
14482
  * through. It stores nothing.
@@ -14338,195 +14574,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14338
14574
  tags: record(string(), string()).optional()
14339
14575
  }), array(LogEntrySchema).readonly());
14340
14576
  /**
14341
- * `failure-contribution` — the capability an addon reports its OWN losses
14342
- * through, per camera, with the denominator attached. It stores nothing.
14343
- *
14344
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14345
- *
14346
- * `load-contribution` answers *what did this camera COST*. This answers *what
14347
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14348
- * copied: the contributor reports what it already knows, hub-main adds only
14349
- * `addonId`, nothing needs global knowledge, and there is no central list for
14350
- * somebody to forget to edit.
14351
- *
14352
- * They are not merged, because their invariants are opposites:
14353
- *
14354
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14355
- * claim a camera cost nothing, which is a measurement nobody made;
14356
- * - a `failure-contribution` zero is the **most valuable value on the
14357
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14358
- * and it is exactly what an absent entry cannot say.
14359
- *
14360
- * Putting a loss counter on a cost entry would also break the reconciliation
14361
- * that gives `load-contribution` its point: contributions are subtracted from
14362
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14363
- * has no process.
14364
- *
14365
- * ## Why not a log line, since the counters already exist
14366
- *
14367
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14368
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14369
- * ends in a log line, and a log line is the thing the operator asked to stop
14370
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14371
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14372
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14373
- * media blackout were both diagnosed. The counters stay; this is where they can
14374
- * be READ.
14375
- *
14376
- * ## The rate is served with its denominator or not at all
14377
- *
14378
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14379
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14380
- * than yesterday" and was **flat across twelve hours** once divided by the
14381
- * successes on the same path. A surface that publishes only the numerator
14382
- * reproduces that mistake on every read.
14383
- *
14384
- * ## Shape
14385
- *
14386
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14387
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14388
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14389
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14390
- * a forked runner's entries reach hub-main over transport that already exists.
14391
- * No new UDS message, no second registry (D3). The operator reads the assembled
14392
- * result through `system.getFailureContributions`.
14393
- */
14394
- var FailureReasonCountSchema = object({
14395
- /**
14396
- * Why the attempt did not land, in the contributor's own vocabulary —
14397
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14398
- * strings that already appear in this repo's logs and, where one exists, the
14399
- * same string the per-track `previewMissReason` records (D276): a second
14400
- * vocabulary for the same loss would make the row and the counter
14401
- * un-joinable.
14402
- */
14403
- reason: string(),
14404
- count: number().int().nonnegative()
14405
- });
14406
- var FailureContributionSchema = object({
14407
- /**
14408
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14409
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14410
- * `unit` free: the families are owned by different addons and a shared enum
14411
- * is a central list that rots invisibly.
14412
- */
14413
- family: string(),
14414
- /**
14415
- * The NUMERIC device id — the same value every log line carries as
14416
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14417
- * cannot name the camera must not emit the entry, because a fleet total
14418
- * cannot answer the only question anybody asks of this surface.
14419
- */
14420
- deviceId: number().int().positive(),
14421
- /**
14422
- * A second dimension inside the family: the model / step id for an inference
14423
- * timeout, so "which camera AND which model" is one read. Absent when the
14424
- * family has a single variant.
14425
- */
14426
- variant: string().optional(),
14427
- /**
14428
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14429
- * differencing two reads must drop the interval when it changes, because the
14430
- * counter restarted from zero in a respawned runner. Same discipline as
14431
- * `LoadContribution.startedAtMs`.
14432
- */
14433
- sinceMs: number(),
14434
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14435
- atMs: number(),
14436
- /**
14437
- * THE DENOMINATOR — every attempt on this path for this camera in the
14438
- * window. A failure count published without it is the mistake this schema
14439
- * exists to make impossible.
14440
- */
14441
- attempts: number().int().nonnegative(),
14442
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14443
- succeeded: number().int().nonnegative(),
14444
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14445
- reasons: array(FailureReasonCountSchema).readonly()
14446
- });
14447
- var failureContributionCapability = {
14448
- name: "failure-contribution",
14449
- scope: "system",
14450
- mode: "collection",
14451
- internal: true,
14452
- methods: {
14453
- /**
14454
- * This addon's per-camera failure counters, read live from bounded in-RAM
14455
- * state it already keeps. Inert: no persistence, no sampling, no timer.
14456
- *
14457
- * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
14458
- * consumer that wants a rate differences two reads. A draining read would
14459
- * make two operators with the page open each destroy half of the other's
14460
- * numbers, and `load-contribution` already settled the same question the
14461
- * same way for `cpuSeconds`.
14462
- */
14463
- list: method(_void(), array(FailureContributionSchema).readonly()) },
14464
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
14465
- mount: { kind: "skip" }
14466
- };
14467
- var LoadContributionSchema = object({
14468
- role: _enum([
14469
- "decode",
14470
- "transcode",
14471
- "recording",
14472
- "streaming",
14473
- "detection"
14474
- ]),
14475
- /**
14476
- * The NUMERIC device id — the same value every log line carries as
14477
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14478
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14479
- * contributor that cannot name its camera must not emit the entry at all,
14480
- * because an unnamed per-camera entry is indistinguishable from a shared one
14481
- * and would quietly turn one camera's cost into everybody's.
14482
- */
14483
- deviceId: number().int().positive().nullable(),
14484
- attribution: _enum([
14485
- "measured",
14486
- "accounted",
14487
- "unattributable"
14488
- ]),
14489
- /**
14490
- * What ONE entry is, in the contributor's own words — `615/high`,
14491
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14492
- * family and inventing a common one would lose the only information that
14493
- * makes two entries for the same camera distinguishable.
14494
- */
14495
- unit: string(),
14496
- /**
14497
- * The OS process this cost lives in, when there is one. Present so a
14498
- * consumer can (a) tell two generations of the same unit apart across a
14499
- * restart, and (b) subtract claimed processes from the node's process
14500
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14501
- * process of its own.
14502
- */
14503
- pid: number().int().positive().optional(),
14504
- /**
14505
- * When this generation started. The pid's incarnation marker: a consumer
14506
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14507
- * window when this changes, because the counter restarted from zero in a new
14508
- * process.
14509
- */
14510
- startedAtMs: number().optional(),
14511
- /**
14512
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14513
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14514
- * contribution is asked for.
14515
- *
14516
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14517
- * needs a sampler, and a new per-node sampler is the defect half of
14518
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14519
- * by whoever already keeps a history; a rate cannot be un-averaged.
14520
- *
14521
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14522
- * an entry with no process.
14523
- */
14524
- cpuSeconds: number().optional(),
14525
- /** Resident bytes of this unit's process, same source and same rules. */
14526
- rssBytes: number().optional()
14527
- });
14528
- method(_void(), array(LoadContributionSchema).readonly());
14529
- /**
14530
14577
  * `login-method` — collection cap through which auth addons contribute
14531
14578
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14532
14579
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19264,12 +19311,53 @@ var MediaFileKindEnum = _enum([
19264
19311
  "keyFrameSmall",
19265
19312
  "thumbnailSmall"
19266
19313
  ]);
19314
+ /**
19315
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19316
+ * ARE — never the bytes themselves.
19317
+ *
19318
+ * ## Why `url` and not `base64`
19319
+ *
19320
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19321
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19322
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19323
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19324
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19325
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19326
+ *
19327
+ * `url` points at the `event-media` data plane
19328
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19329
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19330
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19331
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19332
+ * no less protected than they were inside a `view`-level cap response — see
19333
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19334
+ * (per-device scoping).
19335
+ *
19336
+ * The URL is built from the row's **stored** key, which is not always its
19337
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19338
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19339
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19340
+ *
19341
+ * ## `base64` is TRANSITIONAL and is going away
19342
+ *
19343
+ * It is still populated for one reason: the deployed viewer's track-detail
19344
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19345
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19346
+ * triangle — not as absence. Removing the field before that viewer ships is an
19347
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19348
+ * delete this line and the `withBytes` pass-through in
19349
+ * `analytics-query-facade.ts`; nothing else reads it.
19350
+ */
19267
19351
  var MediaFileSchema = object({
19268
19352
  key: string(),
19269
19353
  kind: MediaFileKindEnum,
19270
- base64: string(),
19271
19354
  sizeBytes: number(),
19272
19355
  timestamp: number()
19356
+ }).extend({
19357
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19358
+ url: string(),
19359
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19360
+ base64: string()
19273
19361
  });
19274
19362
  /**
19275
19363
  * One media row WITHOUT its bytes.
@@ -19281,7 +19369,9 @@ var MediaFileSchema = object({
19281
19369
  * blocks the whole view.
19282
19370
  *
19283
19371
  * `sizeBytes` is carried because it is what lets a client decide between the
19284
- * stored blob and a `?variant=thumb` rendering without fetching either.
19372
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19373
+ * `url` because a client that had to build the plane path itself is a second
19374
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19285
19375
  */
19286
19376
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19287
19377
  /**
@@ -19970,6 +20060,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19970
20060
  }), array(MediaFileSchema).readonly()), method(object({
19971
20061
  trackId: string(),
19972
20062
  deviceId: number()
20063
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20064
+ eventId: string(),
20065
+ deviceId: number()
19973
20066
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19974
20067
  kind: "mutation",
19975
20068
  auth: "admin"
@@ -25064,10 +25157,24 @@ var FaceClusterSchema = object({
25064
25157
  size: number().int(),
25065
25158
  cohesion: number()
25066
25159
  });
25160
+ /**
25161
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25162
+ * are — never the bytes.
25163
+ *
25164
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25165
+ * track/event contract) is still populated because a deployed viewer requires
25166
+ * the field to parse a row at all; this method has no such reader. Its ONE
25167
+ * caller is the admin UI's detail modal, which was building
25168
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25169
+ * dialog already rendering its key FRAME from the `event-media` plane.
25170
+ *
25171
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25172
+ * media key directly, so this needed no new plane and no new access decision.
25173
+ */
25067
25174
  var MediaFileLiteSchema$1 = object({
25068
25175
  key: string(),
25069
25176
  kind: string(),
25070
- base64: string(),
25177
+ url: string(),
25071
25178
  sizeBytes: number(),
25072
25179
  timestamp: number()
25073
25180
  });
@@ -28089,10 +28196,24 @@ var PlateInfoSchema = object({
28089
28196
  */
28090
28197
  cropUrl: string().optional()
28091
28198
  });
28199
+ /**
28200
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28201
+ * are — never the bytes.
28202
+ *
28203
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28204
+ * track/event contract) is still populated because a deployed viewer requires
28205
+ * the field to parse a row at all; this method has no such reader. Its ONE
28206
+ * caller is the admin UI's detail modal, which was building
28207
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28208
+ * dialog already rendering its key FRAME from the `event-media` plane.
28209
+ *
28210
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28211
+ * media key directly, so this needed no new plane and no new access decision.
28212
+ */
28092
28213
  var MediaFileLiteSchema = object({
28093
28214
  key: string(),
28094
28215
  kind: string(),
28095
- base64: string(),
28216
+ url: string(),
28096
28217
  sizeBytes: number(),
28097
28218
  timestamp: number()
28098
28219
  });
@@ -36441,6 +36562,12 @@ Object.freeze({
36441
36562
  addonId: null,
36442
36563
  access: "view"
36443
36564
  },
36565
+ "pipelineAnalytics.listEventMedia": {
36566
+ capName: "pipeline-analytics",
36567
+ capScope: "device",
36568
+ addonId: null,
36569
+ access: "view"
36570
+ },
36444
36571
  "pipelineAnalytics.listGroups": {
36445
36572
  capName: "pipeline-analytics",
36446
36573
  capScope: "device",
@@ -40064,6 +40191,11 @@ Object.freeze({
40064
40191
  form: "array",
40065
40192
  optional: false
40066
40193
  }],
40194
+ "pipelineAnalytics.listEventMedia": [{
40195
+ name: "deviceId",
40196
+ form: "single",
40197
+ optional: false
40198
+ }],
40067
40199
  "pipelineAnalytics.listGroups": [{
40068
40200
  name: "deviceIds",
40069
40201
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8288,6 +8288,21 @@ var RelocateJobSchema = object({
8288
8288
  bytesMoved: number().int(),
8289
8289
  /** Total files discovered up front; null while (or when) unknown. */
8290
8290
  filesTotal: number().int().nullable(),
8291
+ /**
8292
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8293
+ * made that nobody asked for, so it is reported where the operator reads the
8294
+ * job rather than only in a log line.
8295
+ *
8296
+ * A footage segment records its byte count in its own NAME, and the durable
8297
+ * hour row derives its aggregates from those names. A file that does not
8298
+ * match its name therefore makes the ledger's sums — and with them quota and
8299
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8300
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8301
+ *
8302
+ * Absent on lanes where the question has no meaning: a media blob's size is
8303
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8304
+ */
8305
+ rowsReconciled: number().int().nonnegative().optional(),
8291
8306
  startedAt: number(),
8292
8307
  finishedAt: number().nullable(),
8293
8308
  error: string().nullable()
@@ -8356,14 +8371,42 @@ var RelocateMediaInputSchema = object({
8356
8371
  /** Omitted = `move`, the pre-existing behaviour. */
8357
8372
  mode: MediaRelocateModeSchema.optional()
8358
8373
  });
8359
- /** How many rows still carry NO `locationId` — the population a repoint would
8360
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8361
- * value that permits a non-blocking `eventMedia` cutover. */
8362
- var UnstampedEventMediaCountSchema = object({
8363
- media: number().int().nonnegative(),
8364
- retrainFrames: number().int().nonnegative(),
8365
- total: number().int().nonnegative()
8374
+ /**
8375
+ * The unstamped population of ONE collection split, because the gate and the
8376
+ * operator ask two different questions and only one of them has to be cheap.
8377
+ *
8378
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8379
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8380
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8381
+ * that matters — after a seal, when the population is empty.
8382
+ *
8383
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8384
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8385
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8386
+ * and useful answer: "there are some, and this read could not say how many"
8387
+ * still refuses the cutover, which is the whole job.
8388
+ */
8389
+ var UnstampedRowsSchema = object({
8390
+ present: boolean(),
8391
+ rows: number().int().nonnegative().nullable()
8366
8392
  });
8393
+ /**
8394
+ * How many rows still carry NO `locationId` — the population a repoint would
8395
+ * silently re-aim at a disk that does not hold their bytes.
8396
+ *
8397
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8398
+ * over. The gate opens on a measured absence and on nothing else; an unread
8399
+ * collection and an empty one are different facts, and this repo has already
8400
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8401
+ */
8402
+ var UnstampedEventMediaCountSchema = object({
8403
+ media: UnstampedRowsSchema,
8404
+ retrainFrames: UnstampedRowsSchema,
8405
+ /** True when EITHER collection holds one. The refusal reads this. */
8406
+ anyPresent: boolean(),
8407
+ /** Sum across both, or `null` when either lane could not be counted. */
8408
+ total: number().int().nonnegative().nullable()
8409
+ }).nullable();
8367
8410
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8368
8411
  /** The independently selectable logical storage classes — every class
8369
8412
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8474,6 +8517,10 @@ var StorageMigrationMoveProgressSchema = object({
8474
8517
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8475
8518
  filesTotal: number().int().nonnegative().nullable(),
8476
8519
  bytesMoved: number().int().nonnegative(),
8520
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8521
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8522
+ * rows would be the same failure as one that silently skipped them. */
8523
+ rowsReconciled: number().int().nonnegative().optional(),
8477
8524
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8478
8525
  * crash gets a new mover, and a rate computed from the migration's start
8479
8526
  * would silently average in the time nothing was running. */
@@ -13715,6 +13762,133 @@ method(object({
13715
13762
  height: number()
13716
13763
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13717
13764
  /**
13765
+ * `failure-contribution` — the capability an addon reports its OWN losses
13766
+ * through, per camera, with the denominator attached. It stores nothing.
13767
+ *
13768
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13769
+ *
13770
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13771
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13772
+ * copied: the contributor reports what it already knows, hub-main adds only
13773
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13774
+ * somebody to forget to edit.
13775
+ *
13776
+ * They are not merged, because their invariants are opposites:
13777
+ *
13778
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13779
+ * claim a camera cost nothing, which is a measurement nobody made;
13780
+ * - a `failure-contribution` zero is the **most valuable value on the
13781
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13782
+ * and it is exactly what an absent entry cannot say.
13783
+ *
13784
+ * Putting a loss counter on a cost entry would also break the reconciliation
13785
+ * that gives `load-contribution` its point: contributions are subtracted from
13786
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13787
+ * has no process.
13788
+ *
13789
+ * ## Why not a log line, since the counters already exist
13790
+ *
13791
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13792
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13793
+ * ends in a log line, and a log line is the thing the operator asked to stop
13794
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13795
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13796
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13797
+ * media blackout were both diagnosed. The counters stay; this is where they can
13798
+ * be READ.
13799
+ *
13800
+ * ## The rate is served with its denominator or not at all
13801
+ *
13802
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13803
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13804
+ * than yesterday" and was **flat across twelve hours** once divided by the
13805
+ * successes on the same path. A surface that publishes only the numerator
13806
+ * reproduces that mistake on every read.
13807
+ *
13808
+ * ## Shape
13809
+ *
13810
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13811
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13812
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13813
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13814
+ * a forked runner's entries reach hub-main over transport that already exists.
13815
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13816
+ * result through `system.getFailureContributions`.
13817
+ */
13818
+ var FailureReasonCountSchema = object({
13819
+ /**
13820
+ * Why the attempt did not land, in the contributor's own vocabulary —
13821
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13822
+ * strings that already appear in this repo's logs and, where one exists, the
13823
+ * same string the per-track `previewMissReason` records (D276): a second
13824
+ * vocabulary for the same loss would make the row and the counter
13825
+ * un-joinable.
13826
+ */
13827
+ reason: string(),
13828
+ count: number().int().nonnegative()
13829
+ });
13830
+ var FailureContributionSchema = object({
13831
+ /**
13832
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13833
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13834
+ * `unit` free: the families are owned by different addons and a shared enum
13835
+ * is a central list that rots invisibly.
13836
+ */
13837
+ family: string(),
13838
+ /**
13839
+ * The NUMERIC device id — the same value every log line carries as
13840
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13841
+ * cannot name the camera must not emit the entry, because a fleet total
13842
+ * cannot answer the only question anybody asks of this surface.
13843
+ */
13844
+ deviceId: number().int().positive(),
13845
+ /**
13846
+ * A second dimension inside the family: the model / step id for an inference
13847
+ * timeout, so "which camera AND which model" is one read. Absent when the
13848
+ * family has a single variant.
13849
+ */
13850
+ variant: string().optional(),
13851
+ /**
13852
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13853
+ * differencing two reads must drop the interval when it changes, because the
13854
+ * counter restarted from zero in a respawned runner. Same discipline as
13855
+ * `LoadContribution.startedAtMs`.
13856
+ */
13857
+ sinceMs: number(),
13858
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13859
+ atMs: number(),
13860
+ /**
13861
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13862
+ * window. A failure count published without it is the mistake this schema
13863
+ * exists to make impossible.
13864
+ */
13865
+ attempts: number().int().nonnegative(),
13866
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13867
+ succeeded: number().int().nonnegative(),
13868
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13869
+ reasons: array(FailureReasonCountSchema).readonly()
13870
+ });
13871
+ var failureContributionCapability = {
13872
+ name: "failure-contribution",
13873
+ scope: "system",
13874
+ mode: "collection",
13875
+ internal: true,
13876
+ methods: {
13877
+ /**
13878
+ * This addon's per-camera failure counters, read live from bounded in-RAM
13879
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
13880
+ *
13881
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
13882
+ * consumer that wants a rate differences two reads. A draining read would
13883
+ * make two operators with the page open each destroy half of the other's
13884
+ * numbers, and `load-contribution` already settled the same question the
13885
+ * same way for `cpuSeconds`.
13886
+ */
13887
+ list: method(_void(), array(FailureContributionSchema).readonly()) },
13888
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
13889
+ mount: { kind: "skip" }
13890
+ };
13891
+ /**
13718
13892
  * filesystem-browse — per-node capability for browsing the node's local
13719
13893
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13720
13894
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -14236,6 +14410,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
14236
14410
  kind: "mutation",
14237
14411
  auth: "admin"
14238
14412
  });
14413
+ var LoadContributionSchema = object({
14414
+ role: _enum([
14415
+ "decode",
14416
+ "transcode",
14417
+ "recording",
14418
+ "streaming",
14419
+ "detection"
14420
+ ]),
14421
+ /**
14422
+ * The NUMERIC device id — the same value every log line carries as
14423
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14424
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
14425
+ * contributor that cannot name its camera must not emit the entry at all,
14426
+ * because an unnamed per-camera entry is indistinguishable from a shared one
14427
+ * and would quietly turn one camera's cost into everybody's.
14428
+ */
14429
+ deviceId: number().int().positive().nullable(),
14430
+ attribution: _enum([
14431
+ "measured",
14432
+ "accounted",
14433
+ "unattributable"
14434
+ ]),
14435
+ /**
14436
+ * What ONE entry is, in the contributor's own words — `615/high`,
14437
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14438
+ * family and inventing a common one would lose the only information that
14439
+ * makes two entries for the same camera distinguishable.
14440
+ */
14441
+ unit: string(),
14442
+ /**
14443
+ * The OS process this cost lives in, when there is one. Present so a
14444
+ * consumer can (a) tell two generations of the same unit apart across a
14445
+ * restart, and (b) subtract claimed processes from the node's process
14446
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14447
+ * process of its own.
14448
+ */
14449
+ pid: number().int().positive().optional(),
14450
+ /**
14451
+ * When this generation started. The pid's incarnation marker: a consumer
14452
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14453
+ * window when this changes, because the counter restarted from zero in a new
14454
+ * process.
14455
+ */
14456
+ startedAtMs: number().optional(),
14457
+ /**
14458
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14459
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
14460
+ * contribution is asked for.
14461
+ *
14462
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
14463
+ * needs a sampler, and a new per-node sampler is the defect half of
14464
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14465
+ * by whoever already keeps a history; a rate cannot be un-averaged.
14466
+ *
14467
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14468
+ * an entry with no process.
14469
+ */
14470
+ cpuSeconds: number().optional(),
14471
+ /** Resident bytes of this unit's process, same source and same rules. */
14472
+ rssBytes: number().optional()
14473
+ });
14474
+ method(_void(), array(LoadContributionSchema).readonly());
14239
14475
  /**
14240
14476
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
14241
14477
  * through. It stores nothing.
@@ -14333,195 +14569,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
14333
14569
  tags: record(string(), string()).optional()
14334
14570
  }), array(LogEntrySchema).readonly());
14335
14571
  /**
14336
- * `failure-contribution` — the capability an addon reports its OWN losses
14337
- * through, per camera, with the denominator attached. It stores nothing.
14338
- *
14339
- * ## The twin of `load-contribution`, and why it is a twin and not a field
14340
- *
14341
- * `load-contribution` answers *what did this camera COST*. This answers *what
14342
- * did this camera LOSE*. The reporting discipline is identical and deliberately
14343
- * copied: the contributor reports what it already knows, hub-main adds only
14344
- * `addonId`, nothing needs global knowledge, and there is no central list for
14345
- * somebody to forget to edit.
14346
- *
14347
- * They are not merged, because their invariants are opposites:
14348
- *
14349
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
14350
- * claim a camera cost nothing, which is a measurement nobody made;
14351
- * - a `failure-contribution` zero is the **most valuable value on the
14352
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
14353
- * and it is exactly what an absent entry cannot say.
14354
- *
14355
- * Putting a loss counter on a cost entry would also break the reconciliation
14356
- * that gives `load-contribution` its point: contributions are subtracted from
14357
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
14358
- * has no process.
14359
- *
14360
- * ## Why not a log line, since the counters already exist
14361
- *
14362
- * Several of these paths already counted themselves — `CaptureScheduler`'s
14363
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
14364
- * ends in a log line, and a log line is the thing the operator asked to stop
14365
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
14366
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
14367
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
14368
- * media blackout were both diagnosed. The counters stay; this is where they can
14369
- * be READ.
14370
- *
14371
- * ## The rate is served with its denominator or not at all
14372
- *
14373
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
14374
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
14375
- * than yesterday" and was **flat across twelve hours** once divided by the
14376
- * successes on the same path. A surface that publishes only the numerator
14377
- * reproduces that mistake on every read.
14378
- *
14379
- * ## Shape
14380
- *
14381
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
14382
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
14383
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
14384
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
14385
- * a forked runner's entries reach hub-main over transport that already exists.
14386
- * No new UDS message, no second registry (D3). The operator reads the assembled
14387
- * result through `system.getFailureContributions`.
14388
- */
14389
- var FailureReasonCountSchema = object({
14390
- /**
14391
- * Why the attempt did not land, in the contributor's own vocabulary —
14392
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
14393
- * strings that already appear in this repo's logs and, where one exists, the
14394
- * same string the per-track `previewMissReason` records (D276): a second
14395
- * vocabulary for the same loss would make the row and the counter
14396
- * un-joinable.
14397
- */
14398
- reason: string(),
14399
- count: number().int().nonnegative()
14400
- });
14401
- var FailureContributionSchema = object({
14402
- /**
14403
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
14404
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
14405
- * `unit` free: the families are owned by different addons and a shared enum
14406
- * is a central list that rots invisibly.
14407
- */
14408
- family: string(),
14409
- /**
14410
- * The NUMERIC device id — the same value every log line carries as
14411
- * `tags.deviceId`. Never nullable and never absent: a contributor that
14412
- * cannot name the camera must not emit the entry, because a fleet total
14413
- * cannot answer the only question anybody asks of this surface.
14414
- */
14415
- deviceId: number().int().positive(),
14416
- /**
14417
- * A second dimension inside the family: the model / step id for an inference
14418
- * timeout, so "which camera AND which model" is one read. Absent when the
14419
- * family has a single variant.
14420
- */
14421
- variant: string().optional(),
14422
- /**
14423
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
14424
- * differencing two reads must drop the interval when it changes, because the
14425
- * counter restarted from zero in a respawned runner. Same discipline as
14426
- * `LoadContribution.startedAtMs`.
14427
- */
14428
- sinceMs: number(),
14429
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
14430
- atMs: number(),
14431
- /**
14432
- * THE DENOMINATOR — every attempt on this path for this camera in the
14433
- * window. A failure count published without it is the mistake this schema
14434
- * exists to make impossible.
14435
- */
14436
- attempts: number().int().nonnegative(),
14437
- /** Attempts that landed. `attempts - succeeded` is the loss. */
14438
- succeeded: number().int().nonnegative(),
14439
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
14440
- reasons: array(FailureReasonCountSchema).readonly()
14441
- });
14442
- var failureContributionCapability = {
14443
- name: "failure-contribution",
14444
- scope: "system",
14445
- mode: "collection",
14446
- internal: true,
14447
- methods: {
14448
- /**
14449
- * This addon's per-camera failure counters, read live from bounded in-RAM
14450
- * state it already keeps. Inert: no persistence, no sampling, no timer.
14451
- *
14452
- * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
14453
- * consumer that wants a rate differences two reads. A draining read would
14454
- * make two operators with the page open each destroy half of the other's
14455
- * numbers, and `load-contribution` already settled the same question the
14456
- * same way for `cpuSeconds`.
14457
- */
14458
- list: method(_void(), array(FailureContributionSchema).readonly()) },
14459
- /** In-process only — enumerated through `addons.listCapabilityProviders`. */
14460
- mount: { kind: "skip" }
14461
- };
14462
- var LoadContributionSchema = object({
14463
- role: _enum([
14464
- "decode",
14465
- "transcode",
14466
- "recording",
14467
- "streaming",
14468
- "detection"
14469
- ]),
14470
- /**
14471
- * The NUMERIC device id — the same value every log line carries as
14472
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
14473
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
14474
- * contributor that cannot name its camera must not emit the entry at all,
14475
- * because an unnamed per-camera entry is indistinguishable from a shared one
14476
- * and would quietly turn one camera's cost into everybody's.
14477
- */
14478
- deviceId: number().int().positive().nullable(),
14479
- attribution: _enum([
14480
- "measured",
14481
- "accounted",
14482
- "unattributable"
14483
- ]),
14484
- /**
14485
- * What ONE entry is, in the contributor's own words — `615/high`,
14486
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
14487
- * family and inventing a common one would lose the only information that
14488
- * makes two entries for the same camera distinguishable.
14489
- */
14490
- unit: string(),
14491
- /**
14492
- * The OS process this cost lives in, when there is one. Present so a
14493
- * consumer can (a) tell two generations of the same unit apart across a
14494
- * restart, and (b) subtract claimed processes from the node's process
14495
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
14496
- * process of its own.
14497
- */
14498
- pid: number().int().positive().optional(),
14499
- /**
14500
- * When this generation started. The pid's incarnation marker: a consumer
14501
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
14502
- * window when this changes, because the counter restarted from zero in a new
14503
- * process.
14504
- */
14505
- startedAtMs: number().optional(),
14506
- /**
14507
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
14508
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
14509
- * contribution is asked for.
14510
- *
14511
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14512
- * needs a sampler, and a new per-node sampler is the defect half of
14513
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14514
- * by whoever already keeps a history; a rate cannot be un-averaged.
14515
- *
14516
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14517
- * an entry with no process.
14518
- */
14519
- cpuSeconds: number().optional(),
14520
- /** Resident bytes of this unit's process, same source and same rules. */
14521
- rssBytes: number().optional()
14522
- });
14523
- method(_void(), array(LoadContributionSchema).readonly());
14524
- /**
14525
14572
  * `login-method` — collection cap through which auth addons contribute
14526
14573
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14527
14574
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -19259,12 +19306,53 @@ var MediaFileKindEnum = _enum([
19259
19306
  "keyFrameSmall",
19260
19307
  "thumbnailSmall"
19261
19308
  ]);
19309
+ /**
19310
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
19311
+ * ARE — never the bytes themselves.
19312
+ *
19313
+ * ## Why `url` and not `base64`
19314
+ *
19315
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
19316
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
19317
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
19318
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
19319
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
19320
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
19321
+ *
19322
+ * `url` points at the `event-media` data plane
19323
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
19324
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
19325
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
19326
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
19327
+ * no less protected than they were inside a `view`-level cap response — see
19328
+ * `data-plane-access.ts` for the rule and the one gap it does not close
19329
+ * (per-device scoping).
19330
+ *
19331
+ * The URL is built from the row's **stored** key, which is not always its
19332
+ * published `kind`: a track's face/plate crop is stored as `crop` under
19333
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
19334
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
19335
+ *
19336
+ * ## `base64` is TRANSITIONAL and is going away
19337
+ *
19338
+ * It is still populated for one reason: the deployed viewer's track-detail
19339
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
19340
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
19341
+ * triangle — not as absence. Removing the field before that viewer ships is an
19342
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
19343
+ * delete this line and the `withBytes` pass-through in
19344
+ * `analytics-query-facade.ts`; nothing else reads it.
19345
+ */
19262
19346
  var MediaFileSchema = object({
19263
19347
  key: string(),
19264
19348
  kind: MediaFileKindEnum,
19265
- base64: string(),
19266
19349
  sizeBytes: number(),
19267
19350
  timestamp: number()
19351
+ }).extend({
19352
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
19353
+ url: string(),
19354
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
19355
+ base64: string()
19268
19356
  });
19269
19357
  /**
19270
19358
  * One media row WITHOUT its bytes.
@@ -19276,7 +19364,9 @@ var MediaFileSchema = object({
19276
19364
  * blocks the whole view.
19277
19365
  *
19278
19366
  * `sizeBytes` is carried because it is what lets a client decide between the
19279
- * stored blob and a `?variant=thumb` rendering without fetching either.
19367
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
19368
+ * `url` because a client that had to build the plane path itself is a second
19369
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
19280
19370
  */
19281
19371
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
19282
19372
  /**
@@ -19965,6 +20055,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19965
20055
  }), array(MediaFileSchema).readonly()), method(object({
19966
20056
  trackId: string(),
19967
20057
  deviceId: number()
20058
+ }), array(MediaFileInfoSchema).readonly()), method(object({
20059
+ eventId: string(),
20060
+ deviceId: number()
19968
20061
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19969
20062
  kind: "mutation",
19970
20063
  auth: "admin"
@@ -25059,10 +25152,24 @@ var FaceClusterSchema = object({
25059
25152
  size: number().int(),
25060
25153
  cohesion: number()
25061
25154
  });
25155
+ /**
25156
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25157
+ * are — never the bytes.
25158
+ *
25159
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25160
+ * track/event contract) is still populated because a deployed viewer requires
25161
+ * the field to parse a row at all; this method has no such reader. Its ONE
25162
+ * caller is the admin UI's detail modal, which was building
25163
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25164
+ * dialog already rendering its key FRAME from the `event-media` plane.
25165
+ *
25166
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25167
+ * media key directly, so this needed no new plane and no new access decision.
25168
+ */
25062
25169
  var MediaFileLiteSchema$1 = object({
25063
25170
  key: string(),
25064
25171
  kind: string(),
25065
- base64: string(),
25172
+ url: string(),
25066
25173
  sizeBytes: number(),
25067
25174
  timestamp: number()
25068
25175
  });
@@ -28084,10 +28191,24 @@ var PlateInfoSchema = object({
28084
28191
  */
28085
28192
  cropUrl: string().optional()
28086
28193
  });
28194
+ /**
28195
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
28196
+ * are — never the bytes.
28197
+ *
28198
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
28199
+ * track/event contract) is still populated because a deployed viewer requires
28200
+ * the field to parse a row at all; this method has no such reader. Its ONE
28201
+ * caller is the admin UI's detail modal, which was building
28202
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
28203
+ * dialog already rendering its key FRAME from the `event-media` plane.
28204
+ *
28205
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
28206
+ * media key directly, so this needed no new plane and no new access decision.
28207
+ */
28087
28208
  var MediaFileLiteSchema = object({
28088
28209
  key: string(),
28089
28210
  kind: string(),
28090
- base64: string(),
28211
+ url: string(),
28091
28212
  sizeBytes: number(),
28092
28213
  timestamp: number()
28093
28214
  });
@@ -36436,6 +36557,12 @@ Object.freeze({
36436
36557
  addonId: null,
36437
36558
  access: "view"
36438
36559
  },
36560
+ "pipelineAnalytics.listEventMedia": {
36561
+ capName: "pipeline-analytics",
36562
+ capScope: "device",
36563
+ addonId: null,
36564
+ access: "view"
36565
+ },
36439
36566
  "pipelineAnalytics.listGroups": {
36440
36567
  capName: "pipeline-analytics",
36441
36568
  capScope: "device",
@@ -40059,6 +40186,11 @@ Object.freeze({
40059
40186
  form: "array",
40060
40187
  optional: false
40061
40188
  }],
40189
+ "pipelineAnalytics.listEventMedia": [{
40190
+ name: "deviceId",
40191
+ form: "single",
40192
+ optional: false
40193
+ }],
40062
40194
  "pipelineAnalytics.listGroups": [{
40063
40195
  name: "deviceIds",
40064
40196
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.2.68",
3
+ "version": "1.2.70",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",