@camstack/addon-tailscale 1.2.45 → 1.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_dist = require("../dist-ki6Kt6cL.js");
27
+ const require_dist = require("../dist-CJS8Mo7l.js");
28
28
  let node_child_process = require("node:child_process");
29
29
  let node_util = require("node:util");
30
30
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { a as EventCategory, i as BaseAddon, n as networkAccessCapability, r as BOOT_RECOVERY_BACKOFF_MS, t as meshNetworkCapability } from "../dist-RluxJbgF.mjs";
1
+ import { a as EventCategory, i as BaseAddon, n as networkAccessCapability, r as BOOT_RECOVERY_BACKOFF_MS, t as meshNetworkCapability } from "../dist-BO7-LQSt.mjs";
2
2
  import { execFile, spawn } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import * as fs from "node:fs";
@@ -8020,6 +8020,21 @@ var RelocateJobSchema = object({
8020
8020
  bytesMoved: number().int(),
8021
8021
  /** Total files discovered up front; null while (or when) unknown. */
8022
8022
  filesTotal: number().int().nullable(),
8023
+ /**
8024
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8025
+ * made that nobody asked for, so it is reported where the operator reads the
8026
+ * job rather than only in a log line.
8027
+ *
8028
+ * A footage segment records its byte count in its own NAME, and the durable
8029
+ * hour row derives its aggregates from those names. A file that does not
8030
+ * match its name therefore makes the ledger's sums — and with them quota and
8031
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8032
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8033
+ *
8034
+ * Absent on lanes where the question has no meaning: a media blob's size is
8035
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8036
+ */
8037
+ rowsReconciled: number().int().nonnegative().optional(),
8023
8038
  startedAt: number(),
8024
8039
  finishedAt: number().nullable(),
8025
8040
  error: string().nullable()
@@ -8088,14 +8103,42 @@ var RelocateMediaInputSchema = object({
8088
8103
  /** Omitted = `move`, the pre-existing behaviour. */
8089
8104
  mode: MediaRelocateModeSchema.optional()
8090
8105
  });
8091
- /** How many rows still carry NO `locationId` — the population a repoint would
8092
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8093
- * value that permits a non-blocking `eventMedia` cutover. */
8094
- var UnstampedEventMediaCountSchema = object({
8095
- media: number().int().nonnegative(),
8096
- retrainFrames: number().int().nonnegative(),
8097
- total: number().int().nonnegative()
8106
+ /**
8107
+ * The unstamped population of ONE collection — split, because the gate and the
8108
+ * operator ask two different questions and only one of them has to be cheap.
8109
+ *
8110
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8111
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8112
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8113
+ * that matters — after a seal, when the population is empty.
8114
+ *
8115
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8116
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8117
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8118
+ * and useful answer: "there are some, and this read could not say how many"
8119
+ * still refuses the cutover, which is the whole job.
8120
+ */
8121
+ var UnstampedRowsSchema = object({
8122
+ present: boolean(),
8123
+ rows: number().int().nonnegative().nullable()
8098
8124
  });
8125
+ /**
8126
+ * How many rows still carry NO `locationId` — the population a repoint would
8127
+ * silently re-aim at a disk that does not hold their bytes.
8128
+ *
8129
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8130
+ * over. The gate opens on a measured absence and on nothing else; an unread
8131
+ * collection and an empty one are different facts, and this repo has already
8132
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8133
+ */
8134
+ var UnstampedEventMediaCountSchema = object({
8135
+ media: UnstampedRowsSchema,
8136
+ retrainFrames: UnstampedRowsSchema,
8137
+ /** True when EITHER collection holds one. The refusal reads this. */
8138
+ anyPresent: boolean(),
8139
+ /** Sum across both, or `null` when either lane could not be counted. */
8140
+ total: number().int().nonnegative().nullable()
8141
+ }).nullable();
8099
8142
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8100
8143
  /** The independently selectable logical storage classes — every class
8101
8144
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8206,6 +8249,10 @@ var StorageMigrationMoveProgressSchema = object({
8206
8249
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8207
8250
  filesTotal: number().int().nonnegative().nullable(),
8208
8251
  bytesMoved: number().int().nonnegative(),
8252
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8253
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8254
+ * rows would be the same failure as one that silently skipped them. */
8255
+ rowsReconciled: number().int().nonnegative().optional(),
8209
8256
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8210
8257
  * crash gets a new mover, and a rate computed from the migration's start
8211
8258
  * would silently average in the time nothing was running. */
@@ -12890,6 +12937,114 @@ method(object({
12890
12937
  height: number()
12891
12938
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12892
12939
  /**
12940
+ * `failure-contribution` — the capability an addon reports its OWN losses
12941
+ * through, per camera, with the denominator attached. It stores nothing.
12942
+ *
12943
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12944
+ *
12945
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12946
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12947
+ * copied: the contributor reports what it already knows, hub-main adds only
12948
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12949
+ * somebody to forget to edit.
12950
+ *
12951
+ * They are not merged, because their invariants are opposites:
12952
+ *
12953
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12954
+ * claim a camera cost nothing, which is a measurement nobody made;
12955
+ * - a `failure-contribution` zero is the **most valuable value on the
12956
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12957
+ * and it is exactly what an absent entry cannot say.
12958
+ *
12959
+ * Putting a loss counter on a cost entry would also break the reconciliation
12960
+ * that gives `load-contribution` its point: contributions are subtracted from
12961
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12962
+ * has no process.
12963
+ *
12964
+ * ## Why not a log line, since the counters already exist
12965
+ *
12966
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12967
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12968
+ * ends in a log line, and a log line is the thing the operator asked to stop
12969
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12970
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12971
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12972
+ * media blackout were both diagnosed. The counters stay; this is where they can
12973
+ * be READ.
12974
+ *
12975
+ * ## The rate is served with its denominator or not at all
12976
+ *
12977
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12978
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12979
+ * than yesterday" and was **flat across twelve hours** once divided by the
12980
+ * successes on the same path. A surface that publishes only the numerator
12981
+ * reproduces that mistake on every read.
12982
+ *
12983
+ * ## Shape
12984
+ *
12985
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12986
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12987
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12988
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12989
+ * a forked runner's entries reach hub-main over transport that already exists.
12990
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12991
+ * result through `system.getFailureContributions`.
12992
+ */
12993
+ var FailureReasonCountSchema = object({
12994
+ /**
12995
+ * Why the attempt did not land, in the contributor's own vocabulary —
12996
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12997
+ * strings that already appear in this repo's logs and, where one exists, the
12998
+ * same string the per-track `previewMissReason` records (D276): a second
12999
+ * vocabulary for the same loss would make the row and the counter
13000
+ * un-joinable.
13001
+ */
13002
+ reason: string(),
13003
+ count: number().int().nonnegative()
13004
+ });
13005
+ var FailureContributionSchema = object({
13006
+ /**
13007
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13008
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13009
+ * `unit` free: the families are owned by different addons and a shared enum
13010
+ * is a central list that rots invisibly.
13011
+ */
13012
+ family: string(),
13013
+ /**
13014
+ * The NUMERIC device id — the same value every log line carries as
13015
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13016
+ * cannot name the camera must not emit the entry, because a fleet total
13017
+ * cannot answer the only question anybody asks of this surface.
13018
+ */
13019
+ deviceId: number().int().positive(),
13020
+ /**
13021
+ * A second dimension inside the family: the model / step id for an inference
13022
+ * timeout, so "which camera AND which model" is one read. Absent when the
13023
+ * family has a single variant.
13024
+ */
13025
+ variant: string().optional(),
13026
+ /**
13027
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13028
+ * differencing two reads must drop the interval when it changes, because the
13029
+ * counter restarted from zero in a respawned runner. Same discipline as
13030
+ * `LoadContribution.startedAtMs`.
13031
+ */
13032
+ sinceMs: number(),
13033
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13034
+ atMs: number(),
13035
+ /**
13036
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13037
+ * window. A failure count published without it is the mistake this schema
13038
+ * exists to make impossible.
13039
+ */
13040
+ attempts: number().int().nonnegative(),
13041
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13042
+ succeeded: number().int().nonnegative(),
13043
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13044
+ reasons: array(FailureReasonCountSchema).readonly()
13045
+ });
13046
+ method(_void(), array(FailureContributionSchema).readonly());
13047
+ /**
12893
13048
  * filesystem-browse — per-node capability for browsing the node's local
12894
13049
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12895
13050
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13411,6 +13566,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13411
13566
  kind: "mutation",
13412
13567
  auth: "admin"
13413
13568
  });
13569
+ var LoadContributionSchema = object({
13570
+ role: _enum([
13571
+ "decode",
13572
+ "transcode",
13573
+ "recording",
13574
+ "streaming",
13575
+ "detection"
13576
+ ]),
13577
+ /**
13578
+ * The NUMERIC device id — the same value every log line carries as
13579
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13580
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13581
+ * contributor that cannot name its camera must not emit the entry at all,
13582
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13583
+ * and would quietly turn one camera's cost into everybody's.
13584
+ */
13585
+ deviceId: number().int().positive().nullable(),
13586
+ attribution: _enum([
13587
+ "measured",
13588
+ "accounted",
13589
+ "unattributable"
13590
+ ]),
13591
+ /**
13592
+ * What ONE entry is, in the contributor's own words — `615/high`,
13593
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13594
+ * family and inventing a common one would lose the only information that
13595
+ * makes two entries for the same camera distinguishable.
13596
+ */
13597
+ unit: string(),
13598
+ /**
13599
+ * The OS process this cost lives in, when there is one. Present so a
13600
+ * consumer can (a) tell two generations of the same unit apart across a
13601
+ * restart, and (b) subtract claimed processes from the node's process
13602
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13603
+ * process of its own.
13604
+ */
13605
+ pid: number().int().positive().optional(),
13606
+ /**
13607
+ * When this generation started. The pid's incarnation marker: a consumer
13608
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13609
+ * window when this changes, because the counter restarted from zero in a new
13610
+ * process.
13611
+ */
13612
+ startedAtMs: number().optional(),
13613
+ /**
13614
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13615
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13616
+ * contribution is asked for.
13617
+ *
13618
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13619
+ * needs a sampler, and a new per-node sampler is the defect half of
13620
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13621
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13622
+ *
13623
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13624
+ * an entry with no process.
13625
+ */
13626
+ cpuSeconds: number().optional(),
13627
+ /** Resident bytes of this unit's process, same source and same rules. */
13628
+ rssBytes: number().optional()
13629
+ });
13630
+ method(_void(), array(LoadContributionSchema).readonly());
13414
13631
  /**
13415
13632
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13416
13633
  * through. It stores nothing.
@@ -13487,176 +13704,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13487
13704
  tags: record(string(), string()).optional()
13488
13705
  }), array(LogEntrySchema).readonly());
13489
13706
  /**
13490
- * `failure-contribution` — the capability an addon reports its OWN losses
13491
- * through, per camera, with the denominator attached. It stores nothing.
13492
- *
13493
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13494
- *
13495
- * `load-contribution` answers *what did this camera COST*. This answers *what
13496
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13497
- * copied: the contributor reports what it already knows, hub-main adds only
13498
- * `addonId`, nothing needs global knowledge, and there is no central list for
13499
- * somebody to forget to edit.
13500
- *
13501
- * They are not merged, because their invariants are opposites:
13502
- *
13503
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13504
- * claim a camera cost nothing, which is a measurement nobody made;
13505
- * - a `failure-contribution` zero is the **most valuable value on the
13506
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13507
- * and it is exactly what an absent entry cannot say.
13508
- *
13509
- * Putting a loss counter on a cost entry would also break the reconciliation
13510
- * that gives `load-contribution` its point: contributions are subtracted from
13511
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13512
- * has no process.
13513
- *
13514
- * ## Why not a log line, since the counters already exist
13515
- *
13516
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13517
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13518
- * ends in a log line, and a log line is the thing the operator asked to stop
13519
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13520
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13521
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13522
- * media blackout were both diagnosed. The counters stay; this is where they can
13523
- * be READ.
13524
- *
13525
- * ## The rate is served with its denominator or not at all
13526
- *
13527
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13528
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13529
- * than yesterday" and was **flat across twelve hours** once divided by the
13530
- * successes on the same path. A surface that publishes only the numerator
13531
- * reproduces that mistake on every read.
13532
- *
13533
- * ## Shape
13534
- *
13535
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13536
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13537
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13538
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13539
- * a forked runner's entries reach hub-main over transport that already exists.
13540
- * No new UDS message, no second registry (D3). The operator reads the assembled
13541
- * result through `system.getFailureContributions`.
13542
- */
13543
- var FailureReasonCountSchema = object({
13544
- /**
13545
- * Why the attempt did not land, in the contributor's own vocabulary —
13546
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13547
- * strings that already appear in this repo's logs and, where one exists, the
13548
- * same string the per-track `previewMissReason` records (D276): a second
13549
- * vocabulary for the same loss would make the row and the counter
13550
- * un-joinable.
13551
- */
13552
- reason: string(),
13553
- count: number().int().nonnegative()
13554
- });
13555
- var FailureContributionSchema = object({
13556
- /**
13557
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13558
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13559
- * `unit` free: the families are owned by different addons and a shared enum
13560
- * is a central list that rots invisibly.
13561
- */
13562
- family: string(),
13563
- /**
13564
- * The NUMERIC device id — the same value every log line carries as
13565
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13566
- * cannot name the camera must not emit the entry, because a fleet total
13567
- * cannot answer the only question anybody asks of this surface.
13568
- */
13569
- deviceId: number().int().positive(),
13570
- /**
13571
- * A second dimension inside the family: the model / step id for an inference
13572
- * timeout, so "which camera AND which model" is one read. Absent when the
13573
- * family has a single variant.
13574
- */
13575
- variant: string().optional(),
13576
- /**
13577
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13578
- * differencing two reads must drop the interval when it changes, because the
13579
- * counter restarted from zero in a respawned runner. Same discipline as
13580
- * `LoadContribution.startedAtMs`.
13581
- */
13582
- sinceMs: number(),
13583
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13584
- atMs: number(),
13585
- /**
13586
- * THE DENOMINATOR — every attempt on this path for this camera in the
13587
- * window. A failure count published without it is the mistake this schema
13588
- * exists to make impossible.
13589
- */
13590
- attempts: number().int().nonnegative(),
13591
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13592
- succeeded: number().int().nonnegative(),
13593
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13594
- reasons: array(FailureReasonCountSchema).readonly()
13595
- });
13596
- method(_void(), array(FailureContributionSchema).readonly());
13597
- var LoadContributionSchema = object({
13598
- role: _enum([
13599
- "decode",
13600
- "transcode",
13601
- "recording",
13602
- "streaming",
13603
- "detection"
13604
- ]),
13605
- /**
13606
- * The NUMERIC device id — the same value every log line carries as
13607
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13608
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13609
- * contributor that cannot name its camera must not emit the entry at all,
13610
- * because an unnamed per-camera entry is indistinguishable from a shared one
13611
- * and would quietly turn one camera's cost into everybody's.
13612
- */
13613
- deviceId: number().int().positive().nullable(),
13614
- attribution: _enum([
13615
- "measured",
13616
- "accounted",
13617
- "unattributable"
13618
- ]),
13619
- /**
13620
- * What ONE entry is, in the contributor's own words — `615/high`,
13621
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13622
- * family and inventing a common one would lose the only information that
13623
- * makes two entries for the same camera distinguishable.
13624
- */
13625
- unit: string(),
13626
- /**
13627
- * The OS process this cost lives in, when there is one. Present so a
13628
- * consumer can (a) tell two generations of the same unit apart across a
13629
- * restart, and (b) subtract claimed processes from the node's process
13630
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13631
- * process of its own.
13632
- */
13633
- pid: number().int().positive().optional(),
13634
- /**
13635
- * When this generation started. The pid's incarnation marker: a consumer
13636
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13637
- * window when this changes, because the counter restarted from zero in a new
13638
- * process.
13639
- */
13640
- startedAtMs: number().optional(),
13641
- /**
13642
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13643
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13644
- * contribution is asked for.
13645
- *
13646
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13647
- * needs a sampler, and a new per-node sampler is the defect half of
13648
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13649
- * by whoever already keeps a history; a rate cannot be un-averaged.
13650
- *
13651
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13652
- * an entry with no process.
13653
- */
13654
- cpuSeconds: number().optional(),
13655
- /** Resident bytes of this unit's process, same source and same rules. */
13656
- rssBytes: number().optional()
13657
- });
13658
- method(_void(), array(LoadContributionSchema).readonly());
13659
- /**
13660
13707
  * `login-method` — collection cap through which auth addons contribute
13661
13708
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13662
13709
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18333,12 +18380,53 @@ var MediaFileKindEnum = _enum([
18333
18380
  "keyFrameSmall",
18334
18381
  "thumbnailSmall"
18335
18382
  ]);
18383
+ /**
18384
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18385
+ * ARE — never the bytes themselves.
18386
+ *
18387
+ * ## Why `url` and not `base64`
18388
+ *
18389
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18390
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18391
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18392
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18393
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18394
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18395
+ *
18396
+ * `url` points at the `event-media` data plane
18397
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18398
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18399
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18400
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18401
+ * no less protected than they were inside a `view`-level cap response — see
18402
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18403
+ * (per-device scoping).
18404
+ *
18405
+ * The URL is built from the row's **stored** key, which is not always its
18406
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18407
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18408
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18409
+ *
18410
+ * ## `base64` is TRANSITIONAL and is going away
18411
+ *
18412
+ * It is still populated for one reason: the deployed viewer's track-detail
18413
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18414
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18415
+ * triangle — not as absence. Removing the field before that viewer ships is an
18416
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18417
+ * delete this line and the `withBytes` pass-through in
18418
+ * `analytics-query-facade.ts`; nothing else reads it.
18419
+ */
18336
18420
  var MediaFileSchema = object({
18337
18421
  key: string(),
18338
18422
  kind: MediaFileKindEnum,
18339
- base64: string(),
18340
18423
  sizeBytes: number(),
18341
18424
  timestamp: number()
18425
+ }).extend({
18426
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18427
+ url: string(),
18428
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18429
+ base64: string()
18342
18430
  });
18343
18431
  /**
18344
18432
  * One media row WITHOUT its bytes.
@@ -18350,7 +18438,9 @@ var MediaFileSchema = object({
18350
18438
  * blocks the whole view.
18351
18439
  *
18352
18440
  * `sizeBytes` is carried because it is what lets a client decide between the
18353
- * stored blob and a `?variant=thumb` rendering without fetching either.
18441
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18442
+ * `url` because a client that had to build the plane path itself is a second
18443
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18354
18444
  */
18355
18445
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18356
18446
  /**
@@ -19039,6 +19129,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19039
19129
  }), array(MediaFileSchema).readonly()), method(object({
19040
19130
  trackId: string(),
19041
19131
  deviceId: number()
19132
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19133
+ eventId: string(),
19134
+ deviceId: number()
19042
19135
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19043
19136
  kind: "mutation",
19044
19137
  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
  });
@@ -25615,10 +25722,24 @@ var PlateInfoSchema = object({
25615
25722
  */
25616
25723
  cropUrl: string().optional()
25617
25724
  });
25725
+ /**
25726
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25727
+ * are — never the bytes.
25728
+ *
25729
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25730
+ * track/event contract) is still populated because a deployed viewer requires
25731
+ * the field to parse a row at all; this method has no such reader. Its ONE
25732
+ * caller is the admin UI's detail modal, which was building
25733
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25734
+ * dialog already rendering its key FRAME from the `event-media` plane.
25735
+ *
25736
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25737
+ * media key directly, so this needed no new plane and no new access decision.
25738
+ */
25618
25739
  var MediaFileLiteSchema = object({
25619
25740
  key: string(),
25620
25741
  kind: string(),
25621
- base64: string(),
25742
+ url: string(),
25622
25743
  sizeBytes: number(),
25623
25744
  timestamp: number()
25624
25745
  });
@@ -31631,6 +31752,12 @@ Object.freeze({
31631
31752
  addonId: null,
31632
31753
  access: "view"
31633
31754
  },
31755
+ "pipelineAnalytics.listEventMedia": {
31756
+ capName: "pipeline-analytics",
31757
+ capScope: "device",
31758
+ addonId: null,
31759
+ access: "view"
31760
+ },
31634
31761
  "pipelineAnalytics.listGroups": {
31635
31762
  capName: "pipeline-analytics",
31636
31763
  capScope: "device",
@@ -35254,6 +35381,11 @@ Object.freeze({
35254
35381
  form: "array",
35255
35382
  optional: false
35256
35383
  }],
35384
+ "pipelineAnalytics.listEventMedia": [{
35385
+ name: "deviceId",
35386
+ form: "single",
35387
+ optional: false
35388
+ }],
35257
35389
  "pipelineAnalytics.listGroups": [{
35258
35390
  name: "deviceIds",
35259
35391
  form: "array",
@@ -8020,6 +8020,21 @@ var RelocateJobSchema = object({
8020
8020
  bytesMoved: number().int(),
8021
8021
  /** Total files discovered up front; null while (or when) unknown. */
8022
8022
  filesTotal: number().int().nullable(),
8023
+ /**
8024
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8025
+ * made that nobody asked for, so it is reported where the operator reads the
8026
+ * job rather than only in a log line.
8027
+ *
8028
+ * A footage segment records its byte count in its own NAME, and the durable
8029
+ * hour row derives its aggregates from those names. A file that does not
8030
+ * match its name therefore makes the ledger's sums — and with them quota and
8031
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8032
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8033
+ *
8034
+ * Absent on lanes where the question has no meaning: a media blob's size is
8035
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8036
+ */
8037
+ rowsReconciled: number().int().nonnegative().optional(),
8023
8038
  startedAt: number(),
8024
8039
  finishedAt: number().nullable(),
8025
8040
  error: string().nullable()
@@ -8088,14 +8103,42 @@ var RelocateMediaInputSchema = object({
8088
8103
  /** Omitted = `move`, the pre-existing behaviour. */
8089
8104
  mode: MediaRelocateModeSchema.optional()
8090
8105
  });
8091
- /** How many rows still carry NO `locationId` — the population a repoint would
8092
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8093
- * value that permits a non-blocking `eventMedia` cutover. */
8094
- var UnstampedEventMediaCountSchema = object({
8095
- media: number().int().nonnegative(),
8096
- retrainFrames: number().int().nonnegative(),
8097
- total: number().int().nonnegative()
8106
+ /**
8107
+ * The unstamped population of ONE collection — split, because the gate and the
8108
+ * operator ask two different questions and only one of them has to be cheap.
8109
+ *
8110
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8111
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8112
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8113
+ * that matters — after a seal, when the population is empty.
8114
+ *
8115
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8116
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8117
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8118
+ * and useful answer: "there are some, and this read could not say how many"
8119
+ * still refuses the cutover, which is the whole job.
8120
+ */
8121
+ var UnstampedRowsSchema = object({
8122
+ present: boolean(),
8123
+ rows: number().int().nonnegative().nullable()
8098
8124
  });
8125
+ /**
8126
+ * How many rows still carry NO `locationId` — the population a repoint would
8127
+ * silently re-aim at a disk that does not hold their bytes.
8128
+ *
8129
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8130
+ * over. The gate opens on a measured absence and on nothing else; an unread
8131
+ * collection and an empty one are different facts, and this repo has already
8132
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8133
+ */
8134
+ var UnstampedEventMediaCountSchema = object({
8135
+ media: UnstampedRowsSchema,
8136
+ retrainFrames: UnstampedRowsSchema,
8137
+ /** True when EITHER collection holds one. The refusal reads this. */
8138
+ anyPresent: boolean(),
8139
+ /** Sum across both, or `null` when either lane could not be counted. */
8140
+ total: number().int().nonnegative().nullable()
8141
+ }).nullable();
8099
8142
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8100
8143
  /** The independently selectable logical storage classes — every class
8101
8144
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8206,6 +8249,10 @@ var StorageMigrationMoveProgressSchema = object({
8206
8249
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8207
8250
  filesTotal: number().int().nonnegative().nullable(),
8208
8251
  bytesMoved: number().int().nonnegative(),
8252
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8253
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8254
+ * rows would be the same failure as one that silently skipped them. */
8255
+ rowsReconciled: number().int().nonnegative().optional(),
8209
8256
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8210
8257
  * crash gets a new mover, and a rate computed from the migration's start
8211
8258
  * would silently average in the time nothing was running. */
@@ -12890,6 +12937,114 @@ method(object({
12890
12937
  height: number()
12891
12938
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
12892
12939
  /**
12940
+ * `failure-contribution` — the capability an addon reports its OWN losses
12941
+ * through, per camera, with the denominator attached. It stores nothing.
12942
+ *
12943
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
12944
+ *
12945
+ * `load-contribution` answers *what did this camera COST*. This answers *what
12946
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
12947
+ * copied: the contributor reports what it already knows, hub-main adds only
12948
+ * `addonId`, nothing needs global knowledge, and there is no central list for
12949
+ * somebody to forget to edit.
12950
+ *
12951
+ * They are not merged, because their invariants are opposites:
12952
+ *
12953
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
12954
+ * claim a camera cost nothing, which is a measurement nobody made;
12955
+ * - a `failure-contribution` zero is the **most valuable value on the
12956
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
12957
+ * and it is exactly what an absent entry cannot say.
12958
+ *
12959
+ * Putting a loss counter on a cost entry would also break the reconciliation
12960
+ * that gives `load-contribution` its point: contributions are subtracted from
12961
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
12962
+ * has no process.
12963
+ *
12964
+ * ## Why not a log line, since the counters already exist
12965
+ *
12966
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
12967
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
12968
+ * ends in a log line, and a log line is the thing the operator asked to stop
12969
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
12970
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
12971
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
12972
+ * media blackout were both diagnosed. The counters stay; this is where they can
12973
+ * be READ.
12974
+ *
12975
+ * ## The rate is served with its denominator or not at all
12976
+ *
12977
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
12978
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
12979
+ * than yesterday" and was **flat across twelve hours** once divided by the
12980
+ * successes on the same path. A surface that publishes only the numerator
12981
+ * reproduces that mistake on every read.
12982
+ *
12983
+ * ## Shape
12984
+ *
12985
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
12986
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
12987
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
12988
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
12989
+ * a forked runner's entries reach hub-main over transport that already exists.
12990
+ * No new UDS message, no second registry (D3). The operator reads the assembled
12991
+ * result through `system.getFailureContributions`.
12992
+ */
12993
+ var FailureReasonCountSchema = object({
12994
+ /**
12995
+ * Why the attempt did not land, in the contributor's own vocabulary —
12996
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
12997
+ * strings that already appear in this repo's logs and, where one exists, the
12998
+ * same string the per-track `previewMissReason` records (D276): a second
12999
+ * vocabulary for the same loss would make the row and the counter
13000
+ * un-joinable.
13001
+ */
13002
+ reason: string(),
13003
+ count: number().int().nonnegative()
13004
+ });
13005
+ var FailureContributionSchema = object({
13006
+ /**
13007
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13008
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13009
+ * `unit` free: the families are owned by different addons and a shared enum
13010
+ * is a central list that rots invisibly.
13011
+ */
13012
+ family: string(),
13013
+ /**
13014
+ * The NUMERIC device id — the same value every log line carries as
13015
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13016
+ * cannot name the camera must not emit the entry, because a fleet total
13017
+ * cannot answer the only question anybody asks of this surface.
13018
+ */
13019
+ deviceId: number().int().positive(),
13020
+ /**
13021
+ * A second dimension inside the family: the model / step id for an inference
13022
+ * timeout, so "which camera AND which model" is one read. Absent when the
13023
+ * family has a single variant.
13024
+ */
13025
+ variant: string().optional(),
13026
+ /**
13027
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13028
+ * differencing two reads must drop the interval when it changes, because the
13029
+ * counter restarted from zero in a respawned runner. Same discipline as
13030
+ * `LoadContribution.startedAtMs`.
13031
+ */
13032
+ sinceMs: number(),
13033
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13034
+ atMs: number(),
13035
+ /**
13036
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13037
+ * window. A failure count published without it is the mistake this schema
13038
+ * exists to make impossible.
13039
+ */
13040
+ attempts: number().int().nonnegative(),
13041
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13042
+ succeeded: number().int().nonnegative(),
13043
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13044
+ reasons: array(FailureReasonCountSchema).readonly()
13045
+ });
13046
+ method(_void(), array(FailureContributionSchema).readonly());
13047
+ /**
12893
13048
  * filesystem-browse — per-node capability for browsing the node's local
12894
13049
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
12895
13050
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13411,6 +13566,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13411
13566
  kind: "mutation",
13412
13567
  auth: "admin"
13413
13568
  });
13569
+ var LoadContributionSchema = object({
13570
+ role: _enum([
13571
+ "decode",
13572
+ "transcode",
13573
+ "recording",
13574
+ "streaming",
13575
+ "detection"
13576
+ ]),
13577
+ /**
13578
+ * The NUMERIC device id — the same value every log line carries as
13579
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13580
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13581
+ * contributor that cannot name its camera must not emit the entry at all,
13582
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13583
+ * and would quietly turn one camera's cost into everybody's.
13584
+ */
13585
+ deviceId: number().int().positive().nullable(),
13586
+ attribution: _enum([
13587
+ "measured",
13588
+ "accounted",
13589
+ "unattributable"
13590
+ ]),
13591
+ /**
13592
+ * What ONE entry is, in the contributor's own words — `615/high`,
13593
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13594
+ * family and inventing a common one would lose the only information that
13595
+ * makes two entries for the same camera distinguishable.
13596
+ */
13597
+ unit: string(),
13598
+ /**
13599
+ * The OS process this cost lives in, when there is one. Present so a
13600
+ * consumer can (a) tell two generations of the same unit apart across a
13601
+ * restart, and (b) subtract claimed processes from the node's process
13602
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13603
+ * process of its own.
13604
+ */
13605
+ pid: number().int().positive().optional(),
13606
+ /**
13607
+ * When this generation started. The pid's incarnation marker: a consumer
13608
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13609
+ * window when this changes, because the counter restarted from zero in a new
13610
+ * process.
13611
+ */
13612
+ startedAtMs: number().optional(),
13613
+ /**
13614
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13615
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13616
+ * contribution is asked for.
13617
+ *
13618
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13619
+ * needs a sampler, and a new per-node sampler is the defect half of
13620
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13621
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13622
+ *
13623
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13624
+ * an entry with no process.
13625
+ */
13626
+ cpuSeconds: number().optional(),
13627
+ /** Resident bytes of this unit's process, same source and same rules. */
13628
+ rssBytes: number().optional()
13629
+ });
13630
+ method(_void(), array(LoadContributionSchema).readonly());
13414
13631
  /**
13415
13632
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13416
13633
  * through. It stores nothing.
@@ -13487,176 +13704,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13487
13704
  tags: record(string(), string()).optional()
13488
13705
  }), array(LogEntrySchema).readonly());
13489
13706
  /**
13490
- * `failure-contribution` — the capability an addon reports its OWN losses
13491
- * through, per camera, with the denominator attached. It stores nothing.
13492
- *
13493
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13494
- *
13495
- * `load-contribution` answers *what did this camera COST*. This answers *what
13496
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13497
- * copied: the contributor reports what it already knows, hub-main adds only
13498
- * `addonId`, nothing needs global knowledge, and there is no central list for
13499
- * somebody to forget to edit.
13500
- *
13501
- * They are not merged, because their invariants are opposites:
13502
- *
13503
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13504
- * claim a camera cost nothing, which is a measurement nobody made;
13505
- * - a `failure-contribution` zero is the **most valuable value on the
13506
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13507
- * and it is exactly what an absent entry cannot say.
13508
- *
13509
- * Putting a loss counter on a cost entry would also break the reconciliation
13510
- * that gives `load-contribution` its point: contributions are subtracted from
13511
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13512
- * has no process.
13513
- *
13514
- * ## Why not a log line, since the counters already exist
13515
- *
13516
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13517
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13518
- * ends in a log line, and a log line is the thing the operator asked to stop
13519
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13520
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13521
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13522
- * media blackout were both diagnosed. The counters stay; this is where they can
13523
- * be READ.
13524
- *
13525
- * ## The rate is served with its denominator or not at all
13526
- *
13527
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13528
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13529
- * than yesterday" and was **flat across twelve hours** once divided by the
13530
- * successes on the same path. A surface that publishes only the numerator
13531
- * reproduces that mistake on every read.
13532
- *
13533
- * ## Shape
13534
- *
13535
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13536
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13537
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13538
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13539
- * a forked runner's entries reach hub-main over transport that already exists.
13540
- * No new UDS message, no second registry (D3). The operator reads the assembled
13541
- * result through `system.getFailureContributions`.
13542
- */
13543
- var FailureReasonCountSchema = object({
13544
- /**
13545
- * Why the attempt did not land, in the contributor's own vocabulary —
13546
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13547
- * strings that already appear in this repo's logs and, where one exists, the
13548
- * same string the per-track `previewMissReason` records (D276): a second
13549
- * vocabulary for the same loss would make the row and the counter
13550
- * un-joinable.
13551
- */
13552
- reason: string(),
13553
- count: number().int().nonnegative()
13554
- });
13555
- var FailureContributionSchema = object({
13556
- /**
13557
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13558
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13559
- * `unit` free: the families are owned by different addons and a shared enum
13560
- * is a central list that rots invisibly.
13561
- */
13562
- family: string(),
13563
- /**
13564
- * The NUMERIC device id — the same value every log line carries as
13565
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13566
- * cannot name the camera must not emit the entry, because a fleet total
13567
- * cannot answer the only question anybody asks of this surface.
13568
- */
13569
- deviceId: number().int().positive(),
13570
- /**
13571
- * A second dimension inside the family: the model / step id for an inference
13572
- * timeout, so "which camera AND which model" is one read. Absent when the
13573
- * family has a single variant.
13574
- */
13575
- variant: string().optional(),
13576
- /**
13577
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13578
- * differencing two reads must drop the interval when it changes, because the
13579
- * counter restarted from zero in a respawned runner. Same discipline as
13580
- * `LoadContribution.startedAtMs`.
13581
- */
13582
- sinceMs: number(),
13583
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13584
- atMs: number(),
13585
- /**
13586
- * THE DENOMINATOR — every attempt on this path for this camera in the
13587
- * window. A failure count published without it is the mistake this schema
13588
- * exists to make impossible.
13589
- */
13590
- attempts: number().int().nonnegative(),
13591
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13592
- succeeded: number().int().nonnegative(),
13593
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13594
- reasons: array(FailureReasonCountSchema).readonly()
13595
- });
13596
- method(_void(), array(FailureContributionSchema).readonly());
13597
- var LoadContributionSchema = object({
13598
- role: _enum([
13599
- "decode",
13600
- "transcode",
13601
- "recording",
13602
- "streaming",
13603
- "detection"
13604
- ]),
13605
- /**
13606
- * The NUMERIC device id — the same value every log line carries as
13607
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13608
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13609
- * contributor that cannot name its camera must not emit the entry at all,
13610
- * because an unnamed per-camera entry is indistinguishable from a shared one
13611
- * and would quietly turn one camera's cost into everybody's.
13612
- */
13613
- deviceId: number().int().positive().nullable(),
13614
- attribution: _enum([
13615
- "measured",
13616
- "accounted",
13617
- "unattributable"
13618
- ]),
13619
- /**
13620
- * What ONE entry is, in the contributor's own words — `615/high`,
13621
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13622
- * family and inventing a common one would lose the only information that
13623
- * makes two entries for the same camera distinguishable.
13624
- */
13625
- unit: string(),
13626
- /**
13627
- * The OS process this cost lives in, when there is one. Present so a
13628
- * consumer can (a) tell two generations of the same unit apart across a
13629
- * restart, and (b) subtract claimed processes from the node's process
13630
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13631
- * process of its own.
13632
- */
13633
- pid: number().int().positive().optional(),
13634
- /**
13635
- * When this generation started. The pid's incarnation marker: a consumer
13636
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13637
- * window when this changes, because the counter restarted from zero in a new
13638
- * process.
13639
- */
13640
- startedAtMs: number().optional(),
13641
- /**
13642
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13643
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13644
- * contribution is asked for.
13645
- *
13646
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13647
- * needs a sampler, and a new per-node sampler is the defect half of
13648
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13649
- * by whoever already keeps a history; a rate cannot be un-averaged.
13650
- *
13651
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13652
- * an entry with no process.
13653
- */
13654
- cpuSeconds: number().optional(),
13655
- /** Resident bytes of this unit's process, same source and same rules. */
13656
- rssBytes: number().optional()
13657
- });
13658
- method(_void(), array(LoadContributionSchema).readonly());
13659
- /**
13660
13707
  * `login-method` — collection cap through which auth addons contribute
13661
13708
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13662
13709
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18333,12 +18380,53 @@ var MediaFileKindEnum = _enum([
18333
18380
  "keyFrameSmall",
18334
18381
  "thumbnailSmall"
18335
18382
  ]);
18383
+ /**
18384
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18385
+ * ARE — never the bytes themselves.
18386
+ *
18387
+ * ## Why `url` and not `base64`
18388
+ *
18389
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18390
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18391
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18392
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18393
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18394
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18395
+ *
18396
+ * `url` points at the `event-media` data plane
18397
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18398
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18399
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18400
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18401
+ * no less protected than they were inside a `view`-level cap response — see
18402
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18403
+ * (per-device scoping).
18404
+ *
18405
+ * The URL is built from the row's **stored** key, which is not always its
18406
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18407
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18408
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18409
+ *
18410
+ * ## `base64` is TRANSITIONAL and is going away
18411
+ *
18412
+ * It is still populated for one reason: the deployed viewer's track-detail
18413
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18414
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18415
+ * triangle — not as absence. Removing the field before that viewer ships is an
18416
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18417
+ * delete this line and the `withBytes` pass-through in
18418
+ * `analytics-query-facade.ts`; nothing else reads it.
18419
+ */
18336
18420
  var MediaFileSchema = object({
18337
18421
  key: string(),
18338
18422
  kind: MediaFileKindEnum,
18339
- base64: string(),
18340
18423
  sizeBytes: number(),
18341
18424
  timestamp: number()
18425
+ }).extend({
18426
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18427
+ url: string(),
18428
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18429
+ base64: string()
18342
18430
  });
18343
18431
  /**
18344
18432
  * One media row WITHOUT its bytes.
@@ -18350,7 +18438,9 @@ var MediaFileSchema = object({
18350
18438
  * blocks the whole view.
18351
18439
  *
18352
18440
  * `sizeBytes` is carried because it is what lets a client decide between the
18353
- * stored blob and a `?variant=thumb` rendering without fetching either.
18441
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18442
+ * `url` because a client that had to build the plane path itself is a second
18443
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18354
18444
  */
18355
18445
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18356
18446
  /**
@@ -19039,6 +19129,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19039
19129
  }), array(MediaFileSchema).readonly()), method(object({
19040
19130
  trackId: string(),
19041
19131
  deviceId: number()
19132
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19133
+ eventId: string(),
19134
+ deviceId: number()
19042
19135
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19043
19136
  kind: "mutation",
19044
19137
  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
  });
@@ -25615,10 +25722,24 @@ var PlateInfoSchema = object({
25615
25722
  */
25616
25723
  cropUrl: string().optional()
25617
25724
  });
25725
+ /**
25726
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25727
+ * are — never the bytes.
25728
+ *
25729
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25730
+ * track/event contract) is still populated because a deployed viewer requires
25731
+ * the field to parse a row at all; this method has no such reader. Its ONE
25732
+ * caller is the admin UI's detail modal, which was building
25733
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25734
+ * dialog already rendering its key FRAME from the `event-media` plane.
25735
+ *
25736
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
25737
+ * media key directly, so this needed no new plane and no new access decision.
25738
+ */
25618
25739
  var MediaFileLiteSchema = object({
25619
25740
  key: string(),
25620
25741
  kind: string(),
25621
- base64: string(),
25742
+ url: string(),
25622
25743
  sizeBytes: number(),
25623
25744
  timestamp: number()
25624
25745
  });
@@ -31631,6 +31752,12 @@ Object.freeze({
31631
31752
  addonId: null,
31632
31753
  access: "view"
31633
31754
  },
31755
+ "pipelineAnalytics.listEventMedia": {
31756
+ capName: "pipeline-analytics",
31757
+ capScope: "device",
31758
+ addonId: null,
31759
+ access: "view"
31760
+ },
31634
31761
  "pipelineAnalytics.listGroups": {
31635
31762
  capName: "pipeline-analytics",
31636
31763
  capScope: "device",
@@ -35254,6 +35381,11 @@ Object.freeze({
35254
35381
  form: "array",
35255
35382
  optional: false
35256
35383
  }],
35384
+ "pipelineAnalytics.listEventMedia": [{
35385
+ name: "deviceId",
35386
+ form: "single",
35387
+ optional: false
35388
+ }],
35257
35389
  "pipelineAnalytics.listGroups": [{
35258
35390
  name: "deviceIds",
35259
35391
  form: "array",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-ki6Kt6cL.js");
5
+ const require_dist = require("../dist-CJS8Mo7l.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/ingress/tailscale-cli.ts
@@ -1,4 +1,4 @@
1
- import { a as EventCategory, i as BaseAddon, n as networkAccessCapability } from "../dist-RluxJbgF.mjs";
1
+ import { a as EventCategory, i as BaseAddon, n as networkAccessCapability } from "../dist-BO7-LQSt.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-tailscale",
3
- "version": "1.2.45",
3
+ "version": "1.2.47",
4
4
  "description": "Tailscale bundle for CamStack — joins the host to a tailnet (client) and exposes the hub via Serve/Funnel (ingress). Multi-entry npm package shipping 2 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",