@camstack/addon-provider-gree 0.2.44 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +313 -181
  2. package/dist/addon.mjs +313 -181
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8006,6 +8006,21 @@ var RelocateJobSchema = object({
8006
8006
  bytesMoved: number().int(),
8007
8007
  /** Total files discovered up front; null while (or when) unknown. */
8008
8008
  filesTotal: number().int().nullable(),
8009
+ /**
8010
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8011
+ * made that nobody asked for, so it is reported where the operator reads the
8012
+ * job rather than only in a log line.
8013
+ *
8014
+ * A footage segment records its byte count in its own NAME, and the durable
8015
+ * hour row derives its aggregates from those names. A file that does not
8016
+ * match its name therefore makes the ledger's sums — and with them quota and
8017
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8018
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8019
+ *
8020
+ * Absent on lanes where the question has no meaning: a media blob's size is
8021
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8022
+ */
8023
+ rowsReconciled: number().int().nonnegative().optional(),
8009
8024
  startedAt: number(),
8010
8025
  finishedAt: number().nullable(),
8011
8026
  error: string().nullable()
@@ -8074,14 +8089,42 @@ var RelocateMediaInputSchema = object({
8074
8089
  /** Omitted = `move`, the pre-existing behaviour. */
8075
8090
  mode: MediaRelocateModeSchema.optional()
8076
8091
  });
8077
- /** How many rows still carry NO `locationId` — the population a repoint would
8078
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8079
- * value that permits a non-blocking `eventMedia` cutover. */
8080
- var UnstampedEventMediaCountSchema = object({
8081
- media: number().int().nonnegative(),
8082
- retrainFrames: number().int().nonnegative(),
8083
- total: number().int().nonnegative()
8092
+ /**
8093
+ * The unstamped population of ONE collection — split, because the gate and the
8094
+ * operator ask two different questions and only one of them has to be cheap.
8095
+ *
8096
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8097
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8098
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8099
+ * that matters — after a seal, when the population is empty.
8100
+ *
8101
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8102
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8103
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8104
+ * and useful answer: "there are some, and this read could not say how many"
8105
+ * still refuses the cutover, which is the whole job.
8106
+ */
8107
+ var UnstampedRowsSchema = object({
8108
+ present: boolean(),
8109
+ rows: number().int().nonnegative().nullable()
8084
8110
  });
8111
+ /**
8112
+ * How many rows still carry NO `locationId` — the population a repoint would
8113
+ * silently re-aim at a disk that does not hold their bytes.
8114
+ *
8115
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8116
+ * over. The gate opens on a measured absence and on nothing else; an unread
8117
+ * collection and an empty one are different facts, and this repo has already
8118
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8119
+ */
8120
+ var UnstampedEventMediaCountSchema = object({
8121
+ media: UnstampedRowsSchema,
8122
+ retrainFrames: UnstampedRowsSchema,
8123
+ /** True when EITHER collection holds one. The refusal reads this. */
8124
+ anyPresent: boolean(),
8125
+ /** Sum across both, or `null` when either lane could not be counted. */
8126
+ total: number().int().nonnegative().nullable()
8127
+ }).nullable();
8085
8128
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8086
8129
  /** The independently selectable logical storage classes — every class
8087
8130
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8192,6 +8235,10 @@ var StorageMigrationMoveProgressSchema = object({
8192
8235
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8193
8236
  filesTotal: number().int().nonnegative().nullable(),
8194
8237
  bytesMoved: number().int().nonnegative(),
8238
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8239
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8240
+ * rows would be the same failure as one that silently skipped them. */
8241
+ rowsReconciled: number().int().nonnegative().optional(),
8195
8242
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8196
8243
  * crash gets a new mover, and a rate computed from the migration's start
8197
8244
  * would silently average in the time nothing was running. */
@@ -13140,6 +13187,114 @@ method(object({
13140
13187
  height: number()
13141
13188
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13142
13189
  /**
13190
+ * `failure-contribution` — the capability an addon reports its OWN losses
13191
+ * through, per camera, with the denominator attached. It stores nothing.
13192
+ *
13193
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13194
+ *
13195
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13196
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13197
+ * copied: the contributor reports what it already knows, hub-main adds only
13198
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13199
+ * somebody to forget to edit.
13200
+ *
13201
+ * They are not merged, because their invariants are opposites:
13202
+ *
13203
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13204
+ * claim a camera cost nothing, which is a measurement nobody made;
13205
+ * - a `failure-contribution` zero is the **most valuable value on the
13206
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13207
+ * and it is exactly what an absent entry cannot say.
13208
+ *
13209
+ * Putting a loss counter on a cost entry would also break the reconciliation
13210
+ * that gives `load-contribution` its point: contributions are subtracted from
13211
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13212
+ * has no process.
13213
+ *
13214
+ * ## Why not a log line, since the counters already exist
13215
+ *
13216
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13217
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13218
+ * ends in a log line, and a log line is the thing the operator asked to stop
13219
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13220
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13221
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13222
+ * media blackout were both diagnosed. The counters stay; this is where they can
13223
+ * be READ.
13224
+ *
13225
+ * ## The rate is served with its denominator or not at all
13226
+ *
13227
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13228
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13229
+ * than yesterday" and was **flat across twelve hours** once divided by the
13230
+ * successes on the same path. A surface that publishes only the numerator
13231
+ * reproduces that mistake on every read.
13232
+ *
13233
+ * ## Shape
13234
+ *
13235
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13236
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13237
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13238
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13239
+ * a forked runner's entries reach hub-main over transport that already exists.
13240
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13241
+ * result through `system.getFailureContributions`.
13242
+ */
13243
+ var FailureReasonCountSchema = object({
13244
+ /**
13245
+ * Why the attempt did not land, in the contributor's own vocabulary —
13246
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13247
+ * strings that already appear in this repo's logs and, where one exists, the
13248
+ * same string the per-track `previewMissReason` records (D276): a second
13249
+ * vocabulary for the same loss would make the row and the counter
13250
+ * un-joinable.
13251
+ */
13252
+ reason: string(),
13253
+ count: number().int().nonnegative()
13254
+ });
13255
+ var FailureContributionSchema = object({
13256
+ /**
13257
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13258
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13259
+ * `unit` free: the families are owned by different addons and a shared enum
13260
+ * is a central list that rots invisibly.
13261
+ */
13262
+ family: string(),
13263
+ /**
13264
+ * The NUMERIC device id — the same value every log line carries as
13265
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13266
+ * cannot name the camera must not emit the entry, because a fleet total
13267
+ * cannot answer the only question anybody asks of this surface.
13268
+ */
13269
+ deviceId: number().int().positive(),
13270
+ /**
13271
+ * A second dimension inside the family: the model / step id for an inference
13272
+ * timeout, so "which camera AND which model" is one read. Absent when the
13273
+ * family has a single variant.
13274
+ */
13275
+ variant: string().optional(),
13276
+ /**
13277
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13278
+ * differencing two reads must drop the interval when it changes, because the
13279
+ * counter restarted from zero in a respawned runner. Same discipline as
13280
+ * `LoadContribution.startedAtMs`.
13281
+ */
13282
+ sinceMs: number(),
13283
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13284
+ atMs: number(),
13285
+ /**
13286
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13287
+ * window. A failure count published without it is the mistake this schema
13288
+ * exists to make impossible.
13289
+ */
13290
+ attempts: number().int().nonnegative(),
13291
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13292
+ succeeded: number().int().nonnegative(),
13293
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13294
+ reasons: array(FailureReasonCountSchema).readonly()
13295
+ });
13296
+ method(_void(), array(FailureContributionSchema).readonly());
13297
+ /**
13143
13298
  * filesystem-browse — per-node capability for browsing the node's local
13144
13299
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13145
13300
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13661,6 +13816,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13661
13816
  kind: "mutation",
13662
13817
  auth: "admin"
13663
13818
  });
13819
+ var LoadContributionSchema = object({
13820
+ role: _enum([
13821
+ "decode",
13822
+ "transcode",
13823
+ "recording",
13824
+ "streaming",
13825
+ "detection"
13826
+ ]),
13827
+ /**
13828
+ * The NUMERIC device id — the same value every log line carries as
13829
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13830
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13831
+ * contributor that cannot name its camera must not emit the entry at all,
13832
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13833
+ * and would quietly turn one camera's cost into everybody's.
13834
+ */
13835
+ deviceId: number().int().positive().nullable(),
13836
+ attribution: _enum([
13837
+ "measured",
13838
+ "accounted",
13839
+ "unattributable"
13840
+ ]),
13841
+ /**
13842
+ * What ONE entry is, in the contributor's own words — `615/high`,
13843
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13844
+ * family and inventing a common one would lose the only information that
13845
+ * makes two entries for the same camera distinguishable.
13846
+ */
13847
+ unit: string(),
13848
+ /**
13849
+ * The OS process this cost lives in, when there is one. Present so a
13850
+ * consumer can (a) tell two generations of the same unit apart across a
13851
+ * restart, and (b) subtract claimed processes from the node's process
13852
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13853
+ * process of its own.
13854
+ */
13855
+ pid: number().int().positive().optional(),
13856
+ /**
13857
+ * When this generation started. The pid's incarnation marker: a consumer
13858
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13859
+ * window when this changes, because the counter restarted from zero in a new
13860
+ * process.
13861
+ */
13862
+ startedAtMs: number().optional(),
13863
+ /**
13864
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13865
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13866
+ * contribution is asked for.
13867
+ *
13868
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13869
+ * needs a sampler, and a new per-node sampler is the defect half of
13870
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13871
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13872
+ *
13873
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13874
+ * an entry with no process.
13875
+ */
13876
+ cpuSeconds: number().optional(),
13877
+ /** Resident bytes of this unit's process, same source and same rules. */
13878
+ rssBytes: number().optional()
13879
+ });
13880
+ method(_void(), array(LoadContributionSchema).readonly());
13664
13881
  /**
13665
13882
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13666
13883
  * through. It stores nothing.
@@ -13737,176 +13954,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13737
13954
  tags: record(string(), string()).optional()
13738
13955
  }), array(LogEntrySchema).readonly());
13739
13956
  /**
13740
- * `failure-contribution` — the capability an addon reports its OWN losses
13741
- * through, per camera, with the denominator attached. It stores nothing.
13742
- *
13743
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13744
- *
13745
- * `load-contribution` answers *what did this camera COST*. This answers *what
13746
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13747
- * copied: the contributor reports what it already knows, hub-main adds only
13748
- * `addonId`, nothing needs global knowledge, and there is no central list for
13749
- * somebody to forget to edit.
13750
- *
13751
- * They are not merged, because their invariants are opposites:
13752
- *
13753
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13754
- * claim a camera cost nothing, which is a measurement nobody made;
13755
- * - a `failure-contribution` zero is the **most valuable value on the
13756
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13757
- * and it is exactly what an absent entry cannot say.
13758
- *
13759
- * Putting a loss counter on a cost entry would also break the reconciliation
13760
- * that gives `load-contribution` its point: contributions are subtracted from
13761
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13762
- * has no process.
13763
- *
13764
- * ## Why not a log line, since the counters already exist
13765
- *
13766
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13767
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13768
- * ends in a log line, and a log line is the thing the operator asked to stop
13769
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13770
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13771
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13772
- * media blackout were both diagnosed. The counters stay; this is where they can
13773
- * be READ.
13774
- *
13775
- * ## The rate is served with its denominator or not at all
13776
- *
13777
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13778
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13779
- * than yesterday" and was **flat across twelve hours** once divided by the
13780
- * successes on the same path. A surface that publishes only the numerator
13781
- * reproduces that mistake on every read.
13782
- *
13783
- * ## Shape
13784
- *
13785
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13786
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13787
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13788
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13789
- * a forked runner's entries reach hub-main over transport that already exists.
13790
- * No new UDS message, no second registry (D3). The operator reads the assembled
13791
- * result through `system.getFailureContributions`.
13792
- */
13793
- var FailureReasonCountSchema = object({
13794
- /**
13795
- * Why the attempt did not land, in the contributor's own vocabulary —
13796
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13797
- * strings that already appear in this repo's logs and, where one exists, the
13798
- * same string the per-track `previewMissReason` records (D276): a second
13799
- * vocabulary for the same loss would make the row and the counter
13800
- * un-joinable.
13801
- */
13802
- reason: string(),
13803
- count: number().int().nonnegative()
13804
- });
13805
- var FailureContributionSchema = object({
13806
- /**
13807
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13808
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13809
- * `unit` free: the families are owned by different addons and a shared enum
13810
- * is a central list that rots invisibly.
13811
- */
13812
- family: string(),
13813
- /**
13814
- * The NUMERIC device id — the same value every log line carries as
13815
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13816
- * cannot name the camera must not emit the entry, because a fleet total
13817
- * cannot answer the only question anybody asks of this surface.
13818
- */
13819
- deviceId: number().int().positive(),
13820
- /**
13821
- * A second dimension inside the family: the model / step id for an inference
13822
- * timeout, so "which camera AND which model" is one read. Absent when the
13823
- * family has a single variant.
13824
- */
13825
- variant: string().optional(),
13826
- /**
13827
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13828
- * differencing two reads must drop the interval when it changes, because the
13829
- * counter restarted from zero in a respawned runner. Same discipline as
13830
- * `LoadContribution.startedAtMs`.
13831
- */
13832
- sinceMs: number(),
13833
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13834
- atMs: number(),
13835
- /**
13836
- * THE DENOMINATOR — every attempt on this path for this camera in the
13837
- * window. A failure count published without it is the mistake this schema
13838
- * exists to make impossible.
13839
- */
13840
- attempts: number().int().nonnegative(),
13841
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13842
- succeeded: number().int().nonnegative(),
13843
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13844
- reasons: array(FailureReasonCountSchema).readonly()
13845
- });
13846
- method(_void(), array(FailureContributionSchema).readonly());
13847
- var LoadContributionSchema = object({
13848
- role: _enum([
13849
- "decode",
13850
- "transcode",
13851
- "recording",
13852
- "streaming",
13853
- "detection"
13854
- ]),
13855
- /**
13856
- * The NUMERIC device id — the same value every log line carries as
13857
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13858
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13859
- * contributor that cannot name its camera must not emit the entry at all,
13860
- * because an unnamed per-camera entry is indistinguishable from a shared one
13861
- * and would quietly turn one camera's cost into everybody's.
13862
- */
13863
- deviceId: number().int().positive().nullable(),
13864
- attribution: _enum([
13865
- "measured",
13866
- "accounted",
13867
- "unattributable"
13868
- ]),
13869
- /**
13870
- * What ONE entry is, in the contributor's own words — `615/high`,
13871
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13872
- * family and inventing a common one would lose the only information that
13873
- * makes two entries for the same camera distinguishable.
13874
- */
13875
- unit: string(),
13876
- /**
13877
- * The OS process this cost lives in, when there is one. Present so a
13878
- * consumer can (a) tell two generations of the same unit apart across a
13879
- * restart, and (b) subtract claimed processes from the node's process
13880
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13881
- * process of its own.
13882
- */
13883
- pid: number().int().positive().optional(),
13884
- /**
13885
- * When this generation started. The pid's incarnation marker: a consumer
13886
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13887
- * window when this changes, because the counter restarted from zero in a new
13888
- * process.
13889
- */
13890
- startedAtMs: number().optional(),
13891
- /**
13892
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13893
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13894
- * contribution is asked for.
13895
- *
13896
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13897
- * needs a sampler, and a new per-node sampler is the defect half of
13898
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13899
- * by whoever already keeps a history; a rate cannot be un-averaged.
13900
- *
13901
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13902
- * an entry with no process.
13903
- */
13904
- cpuSeconds: number().optional(),
13905
- /** Resident bytes of this unit's process, same source and same rules. */
13906
- rssBytes: number().optional()
13907
- });
13908
- method(_void(), array(LoadContributionSchema).readonly());
13909
- /**
13910
13957
  * `login-method` — collection cap through which auth addons contribute
13911
13958
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13912
13959
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18644,12 +18691,53 @@ var MediaFileKindEnum = _enum([
18644
18691
  "keyFrameSmall",
18645
18692
  "thumbnailSmall"
18646
18693
  ]);
18694
+ /**
18695
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18696
+ * ARE — never the bytes themselves.
18697
+ *
18698
+ * ## Why `url` and not `base64`
18699
+ *
18700
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18701
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18702
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18703
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18704
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18705
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18706
+ *
18707
+ * `url` points at the `event-media` data plane
18708
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18709
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18710
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18711
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18712
+ * no less protected than they were inside a `view`-level cap response — see
18713
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18714
+ * (per-device scoping).
18715
+ *
18716
+ * The URL is built from the row's **stored** key, which is not always its
18717
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18718
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18719
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18720
+ *
18721
+ * ## `base64` is TRANSITIONAL and is going away
18722
+ *
18723
+ * It is still populated for one reason: the deployed viewer's track-detail
18724
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18725
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18726
+ * triangle — not as absence. Removing the field before that viewer ships is an
18727
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18728
+ * delete this line and the `withBytes` pass-through in
18729
+ * `analytics-query-facade.ts`; nothing else reads it.
18730
+ */
18647
18731
  var MediaFileSchema = object({
18648
18732
  key: string(),
18649
18733
  kind: MediaFileKindEnum,
18650
- base64: string(),
18651
18734
  sizeBytes: number(),
18652
18735
  timestamp: number()
18736
+ }).extend({
18737
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18738
+ url: string(),
18739
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18740
+ base64: string()
18653
18741
  });
18654
18742
  /**
18655
18743
  * One media row WITHOUT its bytes.
@@ -18661,7 +18749,9 @@ var MediaFileSchema = object({
18661
18749
  * blocks the whole view.
18662
18750
  *
18663
18751
  * `sizeBytes` is carried because it is what lets a client decide between the
18664
- * stored blob and a `?variant=thumb` rendering without fetching either.
18752
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18753
+ * `url` because a client that had to build the plane path itself is a second
18754
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18665
18755
  */
18666
18756
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18667
18757
  /**
@@ -19350,6 +19440,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19350
19440
  }), array(MediaFileSchema).readonly()), method(object({
19351
19441
  trackId: string(),
19352
19442
  deviceId: number()
19443
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19444
+ eventId: string(),
19445
+ deviceId: number()
19353
19446
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19354
19447
  kind: "mutation",
19355
19448
  auth: "admin"
@@ -24340,10 +24433,24 @@ var FaceClusterSchema = object({
24340
24433
  size: number().int(),
24341
24434
  cohesion: number()
24342
24435
  });
24436
+ /**
24437
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24438
+ * are — never the bytes.
24439
+ *
24440
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24441
+ * track/event contract) is still populated because a deployed viewer requires
24442
+ * the field to parse a row at all; this method has no such reader. Its ONE
24443
+ * caller is the admin UI's detail modal, which was building
24444
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24445
+ * dialog already rendering its key FRAME from the `event-media` plane.
24446
+ *
24447
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24448
+ * media key directly, so this needed no new plane and no new access decision.
24449
+ */
24343
24450
  var MediaFileLiteSchema$1 = object({
24344
24451
  key: string(),
24345
24452
  kind: string(),
24346
- base64: string(),
24453
+ url: string(),
24347
24454
  sizeBytes: number(),
24348
24455
  timestamp: number()
24349
24456
  });
@@ -27365,10 +27472,24 @@ var PlateInfoSchema = object({
27365
27472
  */
27366
27473
  cropUrl: string().optional()
27367
27474
  });
27475
+ /**
27476
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27477
+ * are — never the bytes.
27478
+ *
27479
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27480
+ * track/event contract) is still populated because a deployed viewer requires
27481
+ * the field to parse a row at all; this method has no such reader. Its ONE
27482
+ * caller is the admin UI's detail modal, which was building
27483
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27484
+ * dialog already rendering its key FRAME from the `event-media` plane.
27485
+ *
27486
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27487
+ * media key directly, so this needed no new plane and no new access decision.
27488
+ */
27368
27489
  var MediaFileLiteSchema = object({
27369
27490
  key: string(),
27370
27491
  kind: string(),
27371
- base64: string(),
27492
+ url: string(),
27372
27493
  sizeBytes: number(),
27373
27494
  timestamp: number()
27374
27495
  });
@@ -35265,6 +35386,12 @@ Object.freeze({
35265
35386
  addonId: null,
35266
35387
  access: "view"
35267
35388
  },
35389
+ "pipelineAnalytics.listEventMedia": {
35390
+ capName: "pipeline-analytics",
35391
+ capScope: "device",
35392
+ addonId: null,
35393
+ access: "view"
35394
+ },
35268
35395
  "pipelineAnalytics.listGroups": {
35269
35396
  capName: "pipeline-analytics",
35270
35397
  capScope: "device",
@@ -38888,6 +39015,11 @@ Object.freeze({
38888
39015
  form: "array",
38889
39016
  optional: false
38890
39017
  }],
39018
+ "pipelineAnalytics.listEventMedia": [{
39019
+ name: "deviceId",
39020
+ form: "single",
39021
+ optional: false
39022
+ }],
38891
39023
  "pipelineAnalytics.listGroups": [{
38892
39024
  name: "deviceIds",
38893
39025
  form: "array",
package/dist/addon.mjs CHANGED
@@ -8005,6 +8005,21 @@ var RelocateJobSchema = object({
8005
8005
  bytesMoved: number().int(),
8006
8006
  /** Total files discovered up front; null while (or when) unknown. */
8007
8007
  filesTotal: number().int().nullable(),
8008
+ /**
8009
+ * Rows this run CORRECTED while moving them — a durable mutation the move
8010
+ * made that nobody asked for, so it is reported where the operator reads the
8011
+ * job rather than only in a log line.
8012
+ *
8013
+ * A footage segment records its byte count in its own NAME, and the durable
8014
+ * hour row derives its aggregates from those names. A file that does not
8015
+ * match its name therefore makes the ledger's sums — and with them quota and
8016
+ * pressure eviction — wrong by the difference, and only a rename can fix it.
8017
+ * On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
8018
+ *
8019
+ * Absent on lanes where the question has no meaning: a media blob's size is
8020
+ * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8021
+ */
8022
+ rowsReconciled: number().int().nonnegative().optional(),
8008
8023
  startedAt: number(),
8009
8024
  finishedAt: number().nullable(),
8010
8025
  error: string().nullable()
@@ -8073,14 +8088,42 @@ var RelocateMediaInputSchema = object({
8073
8088
  /** Omitted = `move`, the pre-existing behaviour. */
8074
8089
  mode: MediaRelocateModeSchema.optional()
8075
8090
  });
8076
- /** How many rows still carry NO `locationId` — the population a repoint would
8077
- * silently re-aim at a disk that does not hold their bytes. Zero is the only
8078
- * value that permits a non-blocking `eventMedia` cutover. */
8079
- var UnstampedEventMediaCountSchema = object({
8080
- media: number().int().nonnegative(),
8081
- retrainFrames: number().int().nonnegative(),
8082
- total: number().int().nonnegative()
8091
+ /**
8092
+ * The unstamped population of ONE collection — split, because the gate and the
8093
+ * operator ask two different questions and only one of them has to be cheap.
8094
+ *
8095
+ * `present` is the GATE: "is there at least one row that would be orphaned by a
8096
+ * repoint". It is a single indexed seek to the first matching row, so it stays
8097
+ * answerable on a saturated disk and answers in O(log n) precisely in the state
8098
+ * that matters — after a seal, when the population is empty.
8099
+ *
8100
+ * `rows` is the NUMBER, for the refusal message and the operator's sense of
8101
+ * scale. It is a second, indexed `COUNT(*)`, and `null` means **not
8102
+ * measurable** — never zero. `{ present: true, rows: null }` is a legitimate
8103
+ * and useful answer: "there are some, and this read could not say how many"
8104
+ * still refuses the cutover, which is the whole job.
8105
+ */
8106
+ var UnstampedRowsSchema = object({
8107
+ present: boolean(),
8108
+ rows: number().int().nonnegative().nullable()
8083
8109
  });
8110
+ /**
8111
+ * How many rows still carry NO `locationId` — the population a repoint would
8112
+ * silently re-aim at a disk that does not hold their bytes.
8113
+ *
8114
+ * **`null` = the count could not be taken**, and it is NOT permission to cut
8115
+ * over. The gate opens on a measured absence and on nothing else; an unread
8116
+ * collection and an empty one are different facts, and this repo has already
8117
+ * paid for conflating them (`RelocateResidueSchema`, D295).
8118
+ */
8119
+ var UnstampedEventMediaCountSchema = object({
8120
+ media: UnstampedRowsSchema,
8121
+ retrainFrames: UnstampedRowsSchema,
8122
+ /** True when EITHER collection holds one. The refusal reads this. */
8123
+ anyPresent: boolean(),
8124
+ /** Sum across both, or `null` when either lane could not be counted. */
8125
+ total: number().int().nonnegative().nullable()
8126
+ }).nullable();
8084
8127
  var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
8085
8128
  /** The independently selectable logical storage classes — every class
8086
8129
  * `storage.listLocationDeclarations` reports, so an operator never meets a
@@ -8191,6 +8234,10 @@ var StorageMigrationMoveProgressSchema = object({
8191
8234
  /** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
8192
8235
  filesTotal: number().int().nonnegative().nullable(),
8193
8236
  bytesMoved: number().int().nonnegative(),
8237
+ /** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
8238
+ * a lane that cannot reconcile. A migration that silently rewrote durable
8239
+ * rows would be the same failure as one that silently skipped them. */
8240
+ rowsReconciled: number().int().nonnegative().optional(),
8194
8241
  /** The MOVER's start, not the migration's: a drain restarted after an addon
8195
8242
  * crash gets a new mover, and a rate computed from the migration's start
8196
8243
  * would silently average in the time nothing was running. */
@@ -13139,6 +13186,114 @@ method(object({
13139
13186
  height: number()
13140
13187
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13141
13188
  /**
13189
+ * `failure-contribution` — the capability an addon reports its OWN losses
13190
+ * through, per camera, with the denominator attached. It stores nothing.
13191
+ *
13192
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13193
+ *
13194
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13195
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13196
+ * copied: the contributor reports what it already knows, hub-main adds only
13197
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13198
+ * somebody to forget to edit.
13199
+ *
13200
+ * They are not merged, because their invariants are opposites:
13201
+ *
13202
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13203
+ * claim a camera cost nothing, which is a measurement nobody made;
13204
+ * - a `failure-contribution` zero is the **most valuable value on the
13205
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13206
+ * and it is exactly what an absent entry cannot say.
13207
+ *
13208
+ * Putting a loss counter on a cost entry would also break the reconciliation
13209
+ * that gives `load-contribution` its point: contributions are subtracted from
13210
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13211
+ * has no process.
13212
+ *
13213
+ * ## Why not a log line, since the counters already exist
13214
+ *
13215
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13216
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13217
+ * ends in a log line, and a log line is the thing the operator asked to stop
13218
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13219
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13220
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13221
+ * media blackout were both diagnosed. The counters stay; this is where they can
13222
+ * be READ.
13223
+ *
13224
+ * ## The rate is served with its denominator or not at all
13225
+ *
13226
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13227
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13228
+ * than yesterday" and was **flat across twelve hours** once divided by the
13229
+ * successes on the same path. A surface that publishes only the numerator
13230
+ * reproduces that mistake on every read.
13231
+ *
13232
+ * ## Shape
13233
+ *
13234
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13235
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13236
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13237
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13238
+ * a forked runner's entries reach hub-main over transport that already exists.
13239
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13240
+ * result through `system.getFailureContributions`.
13241
+ */
13242
+ var FailureReasonCountSchema = object({
13243
+ /**
13244
+ * Why the attempt did not land, in the contributor's own vocabulary —
13245
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13246
+ * strings that already appear in this repo's logs and, where one exists, the
13247
+ * same string the per-track `previewMissReason` records (D276): a second
13248
+ * vocabulary for the same loss would make the row and the counter
13249
+ * un-joinable.
13250
+ */
13251
+ reason: string(),
13252
+ count: number().int().nonnegative()
13253
+ });
13254
+ var FailureContributionSchema = object({
13255
+ /**
13256
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13257
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13258
+ * `unit` free: the families are owned by different addons and a shared enum
13259
+ * is a central list that rots invisibly.
13260
+ */
13261
+ family: string(),
13262
+ /**
13263
+ * The NUMERIC device id — the same value every log line carries as
13264
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13265
+ * cannot name the camera must not emit the entry, because a fleet total
13266
+ * cannot answer the only question anybody asks of this surface.
13267
+ */
13268
+ deviceId: number().int().positive(),
13269
+ /**
13270
+ * A second dimension inside the family: the model / step id for an inference
13271
+ * timeout, so "which camera AND which model" is one read. Absent when the
13272
+ * family has a single variant.
13273
+ */
13274
+ variant: string().optional(),
13275
+ /**
13276
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13277
+ * differencing two reads must drop the interval when it changes, because the
13278
+ * counter restarted from zero in a respawned runner. Same discipline as
13279
+ * `LoadContribution.startedAtMs`.
13280
+ */
13281
+ sinceMs: number(),
13282
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13283
+ atMs: number(),
13284
+ /**
13285
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13286
+ * window. A failure count published without it is the mistake this schema
13287
+ * exists to make impossible.
13288
+ */
13289
+ attempts: number().int().nonnegative(),
13290
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13291
+ succeeded: number().int().nonnegative(),
13292
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13293
+ reasons: array(FailureReasonCountSchema).readonly()
13294
+ });
13295
+ method(_void(), array(FailureContributionSchema).readonly());
13296
+ /**
13142
13297
  * filesystem-browse — per-node capability for browsing the node's local
13143
13298
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13144
13299
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13660,6 +13815,68 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13660
13815
  kind: "mutation",
13661
13816
  auth: "admin"
13662
13817
  });
13818
+ var LoadContributionSchema = object({
13819
+ role: _enum([
13820
+ "decode",
13821
+ "transcode",
13822
+ "recording",
13823
+ "streaming",
13824
+ "detection"
13825
+ ]),
13826
+ /**
13827
+ * The NUMERIC device id — the same value every log line carries as
13828
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13829
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13830
+ * contributor that cannot name its camera must not emit the entry at all,
13831
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13832
+ * and would quietly turn one camera's cost into everybody's.
13833
+ */
13834
+ deviceId: number().int().positive().nullable(),
13835
+ attribution: _enum([
13836
+ "measured",
13837
+ "accounted",
13838
+ "unattributable"
13839
+ ]),
13840
+ /**
13841
+ * What ONE entry is, in the contributor's own words — `615/high`,
13842
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13843
+ * family and inventing a common one would lose the only information that
13844
+ * makes two entries for the same camera distinguishable.
13845
+ */
13846
+ unit: string(),
13847
+ /**
13848
+ * The OS process this cost lives in, when there is one. Present so a
13849
+ * consumer can (a) tell two generations of the same unit apart across a
13850
+ * restart, and (b) subtract claimed processes from the node's process
13851
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13852
+ * process of its own.
13853
+ */
13854
+ pid: number().int().positive().optional(),
13855
+ /**
13856
+ * When this generation started. The pid's incarnation marker: a consumer
13857
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13858
+ * window when this changes, because the counter restarted from zero in a new
13859
+ * process.
13860
+ */
13861
+ startedAtMs: number().optional(),
13862
+ /**
13863
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13864
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13865
+ * contribution is asked for.
13866
+ *
13867
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13868
+ * needs a sampler, and a new per-node sampler is the defect half of
13869
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13870
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13871
+ *
13872
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13873
+ * an entry with no process.
13874
+ */
13875
+ cpuSeconds: number().optional(),
13876
+ /** Resident bytes of this unit's process, same source and same rules. */
13877
+ rssBytes: number().optional()
13878
+ });
13879
+ method(_void(), array(LoadContributionSchema).readonly());
13663
13880
  /**
13664
13881
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13665
13882
  * through. It stores nothing.
@@ -13736,176 +13953,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13736
13953
  tags: record(string(), string()).optional()
13737
13954
  }), array(LogEntrySchema).readonly());
13738
13955
  /**
13739
- * `failure-contribution` — the capability an addon reports its OWN losses
13740
- * through, per camera, with the denominator attached. It stores nothing.
13741
- *
13742
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13743
- *
13744
- * `load-contribution` answers *what did this camera COST*. This answers *what
13745
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13746
- * copied: the contributor reports what it already knows, hub-main adds only
13747
- * `addonId`, nothing needs global knowledge, and there is no central list for
13748
- * somebody to forget to edit.
13749
- *
13750
- * They are not merged, because their invariants are opposites:
13751
- *
13752
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13753
- * claim a camera cost nothing, which is a measurement nobody made;
13754
- * - a `failure-contribution` zero is the **most valuable value on the
13755
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13756
- * and it is exactly what an absent entry cannot say.
13757
- *
13758
- * Putting a loss counter on a cost entry would also break the reconciliation
13759
- * that gives `load-contribution` its point: contributions are subtracted from
13760
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13761
- * has no process.
13762
- *
13763
- * ## Why not a log line, since the counters already exist
13764
- *
13765
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13766
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13767
- * ends in a log line, and a log line is the thing the operator asked to stop
13768
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13769
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13770
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13771
- * media blackout were both diagnosed. The counters stay; this is where they can
13772
- * be READ.
13773
- *
13774
- * ## The rate is served with its denominator or not at all
13775
- *
13776
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13777
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13778
- * than yesterday" and was **flat across twelve hours** once divided by the
13779
- * successes on the same path. A surface that publishes only the numerator
13780
- * reproduces that mistake on every read.
13781
- *
13782
- * ## Shape
13783
- *
13784
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13785
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13786
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13787
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13788
- * a forked runner's entries reach hub-main over transport that already exists.
13789
- * No new UDS message, no second registry (D3). The operator reads the assembled
13790
- * result through `system.getFailureContributions`.
13791
- */
13792
- var FailureReasonCountSchema = object({
13793
- /**
13794
- * Why the attempt did not land, in the contributor's own vocabulary —
13795
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13796
- * strings that already appear in this repo's logs and, where one exists, the
13797
- * same string the per-track `previewMissReason` records (D276): a second
13798
- * vocabulary for the same loss would make the row and the counter
13799
- * un-joinable.
13800
- */
13801
- reason: string(),
13802
- count: number().int().nonnegative()
13803
- });
13804
- var FailureContributionSchema = object({
13805
- /**
13806
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13807
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13808
- * `unit` free: the families are owned by different addons and a shared enum
13809
- * is a central list that rots invisibly.
13810
- */
13811
- family: string(),
13812
- /**
13813
- * The NUMERIC device id — the same value every log line carries as
13814
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13815
- * cannot name the camera must not emit the entry, because a fleet total
13816
- * cannot answer the only question anybody asks of this surface.
13817
- */
13818
- deviceId: number().int().positive(),
13819
- /**
13820
- * A second dimension inside the family: the model / step id for an inference
13821
- * timeout, so "which camera AND which model" is one read. Absent when the
13822
- * family has a single variant.
13823
- */
13824
- variant: string().optional(),
13825
- /**
13826
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13827
- * differencing two reads must drop the interval when it changes, because the
13828
- * counter restarted from zero in a respawned runner. Same discipline as
13829
- * `LoadContribution.startedAtMs`.
13830
- */
13831
- sinceMs: number(),
13832
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13833
- atMs: number(),
13834
- /**
13835
- * THE DENOMINATOR — every attempt on this path for this camera in the
13836
- * window. A failure count published without it is the mistake this schema
13837
- * exists to make impossible.
13838
- */
13839
- attempts: number().int().nonnegative(),
13840
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13841
- succeeded: number().int().nonnegative(),
13842
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13843
- reasons: array(FailureReasonCountSchema).readonly()
13844
- });
13845
- method(_void(), array(FailureContributionSchema).readonly());
13846
- var LoadContributionSchema = object({
13847
- role: _enum([
13848
- "decode",
13849
- "transcode",
13850
- "recording",
13851
- "streaming",
13852
- "detection"
13853
- ]),
13854
- /**
13855
- * The NUMERIC device id — the same value every log line carries as
13856
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13857
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13858
- * contributor that cannot name its camera must not emit the entry at all,
13859
- * because an unnamed per-camera entry is indistinguishable from a shared one
13860
- * and would quietly turn one camera's cost into everybody's.
13861
- */
13862
- deviceId: number().int().positive().nullable(),
13863
- attribution: _enum([
13864
- "measured",
13865
- "accounted",
13866
- "unattributable"
13867
- ]),
13868
- /**
13869
- * What ONE entry is, in the contributor's own words — `615/high`,
13870
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13871
- * family and inventing a common one would lose the only information that
13872
- * makes two entries for the same camera distinguishable.
13873
- */
13874
- unit: string(),
13875
- /**
13876
- * The OS process this cost lives in, when there is one. Present so a
13877
- * consumer can (a) tell two generations of the same unit apart across a
13878
- * restart, and (b) subtract claimed processes from the node's process
13879
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13880
- * process of its own.
13881
- */
13882
- pid: number().int().positive().optional(),
13883
- /**
13884
- * When this generation started. The pid's incarnation marker: a consumer
13885
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13886
- * window when this changes, because the counter restarted from zero in a new
13887
- * process.
13888
- */
13889
- startedAtMs: number().optional(),
13890
- /**
13891
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13892
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13893
- * contribution is asked for.
13894
- *
13895
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13896
- * needs a sampler, and a new per-node sampler is the defect half of
13897
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13898
- * by whoever already keeps a history; a rate cannot be un-averaged.
13899
- *
13900
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13901
- * an entry with no process.
13902
- */
13903
- cpuSeconds: number().optional(),
13904
- /** Resident bytes of this unit's process, same source and same rules. */
13905
- rssBytes: number().optional()
13906
- });
13907
- method(_void(), array(LoadContributionSchema).readonly());
13908
- /**
13909
13956
  * `login-method` — collection cap through which auth addons contribute
13910
13957
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13911
13958
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18643,12 +18690,53 @@ var MediaFileKindEnum = _enum([
18643
18690
  "keyFrameSmall",
18644
18691
  "thumbnailSmall"
18645
18692
  ]);
18693
+ /**
18694
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18695
+ * ARE — never the bytes themselves.
18696
+ *
18697
+ * ## Why `url` and not `base64`
18698
+ *
18699
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18700
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18701
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18702
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18703
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18704
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18705
+ *
18706
+ * `url` points at the `event-media` data plane
18707
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18708
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18709
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18710
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18711
+ * no less protected than they were inside a `view`-level cap response — see
18712
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18713
+ * (per-device scoping).
18714
+ *
18715
+ * The URL is built from the row's **stored** key, which is not always its
18716
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18717
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18718
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18719
+ *
18720
+ * ## `base64` is TRANSITIONAL and is going away
18721
+ *
18722
+ * It is still populated for one reason: the deployed viewer's track-detail
18723
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18724
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18725
+ * triangle — not as absence. Removing the field before that viewer ships is an
18726
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18727
+ * delete this line and the `withBytes` pass-through in
18728
+ * `analytics-query-facade.ts`; nothing else reads it.
18729
+ */
18646
18730
  var MediaFileSchema = object({
18647
18731
  key: string(),
18648
18732
  kind: MediaFileKindEnum,
18649
- base64: string(),
18650
18733
  sizeBytes: number(),
18651
18734
  timestamp: number()
18735
+ }).extend({
18736
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18737
+ url: string(),
18738
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18739
+ base64: string()
18652
18740
  });
18653
18741
  /**
18654
18742
  * One media row WITHOUT its bytes.
@@ -18660,7 +18748,9 @@ var MediaFileSchema = object({
18660
18748
  * blocks the whole view.
18661
18749
  *
18662
18750
  * `sizeBytes` is carried because it is what lets a client decide between the
18663
- * stored blob and a `?variant=thumb` rendering without fetching either.
18751
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18752
+ * `url` because a client that had to build the plane path itself is a second
18753
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18664
18754
  */
18665
18755
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18666
18756
  /**
@@ -19349,6 +19439,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19349
19439
  }), array(MediaFileSchema).readonly()), method(object({
19350
19440
  trackId: string(),
19351
19441
  deviceId: number()
19442
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19443
+ eventId: string(),
19444
+ deviceId: number()
19352
19445
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19353
19446
  kind: "mutation",
19354
19447
  auth: "admin"
@@ -24339,10 +24432,24 @@ var FaceClusterSchema = object({
24339
24432
  size: number().int(),
24340
24433
  cohesion: number()
24341
24434
  });
24435
+ /**
24436
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
24437
+ * are — never the bytes.
24438
+ *
24439
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
24440
+ * track/event contract) is still populated because a deployed viewer requires
24441
+ * the field to parse a row at all; this method has no such reader. Its ONE
24442
+ * caller is the admin UI's detail modal, which was building
24443
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
24444
+ * dialog already rendering its key FRAME from the `event-media` plane.
24445
+ *
24446
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
24447
+ * media key directly, so this needed no new plane and no new access decision.
24448
+ */
24342
24449
  var MediaFileLiteSchema$1 = object({
24343
24450
  key: string(),
24344
24451
  kind: string(),
24345
- base64: string(),
24452
+ url: string(),
24346
24453
  sizeBytes: number(),
24347
24454
  timestamp: number()
24348
24455
  });
@@ -27364,10 +27471,24 @@ var PlateInfoSchema = object({
27364
27471
  */
27365
27472
  cropUrl: string().optional()
27366
27473
  });
27474
+ /**
27475
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
27476
+ * are — never the bytes.
27477
+ *
27478
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
27479
+ * track/event contract) is still populated because a deployed viewer requires
27480
+ * the field to parse a row at all; this method has no such reader. Its ONE
27481
+ * caller is the admin UI's detail modal, which was building
27482
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
27483
+ * dialog already rendering its key FRAME from the `event-media` plane.
27484
+ *
27485
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
27486
+ * media key directly, so this needed no new plane and no new access decision.
27487
+ */
27367
27488
  var MediaFileLiteSchema = object({
27368
27489
  key: string(),
27369
27490
  kind: string(),
27370
- base64: string(),
27491
+ url: string(),
27371
27492
  sizeBytes: number(),
27372
27493
  timestamp: number()
27373
27494
  });
@@ -35264,6 +35385,12 @@ Object.freeze({
35264
35385
  addonId: null,
35265
35386
  access: "view"
35266
35387
  },
35388
+ "pipelineAnalytics.listEventMedia": {
35389
+ capName: "pipeline-analytics",
35390
+ capScope: "device",
35391
+ addonId: null,
35392
+ access: "view"
35393
+ },
35267
35394
  "pipelineAnalytics.listGroups": {
35268
35395
  capName: "pipeline-analytics",
35269
35396
  capScope: "device",
@@ -38887,6 +39014,11 @@ Object.freeze({
38887
39014
  form: "array",
38888
39015
  optional: false
38889
39016
  }],
39017
+ "pipelineAnalytics.listEventMedia": [{
39018
+ name: "deviceId",
39019
+ form: "single",
39020
+ optional: false
39021
+ }],
38890
39022
  "pipelineAnalytics.listGroups": [{
38891
39023
  name: "deviceIds",
38892
39024
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-gree",
3
- "version": "0.2.44",
3
+ "version": "0.2.46",
4
4
  "description": "Gree air-conditioner device-provider addon for CamStack — wraps the @apocaliss92/nodegree local-UDP client (LAN discovery + AES control), exposing climate-control and fan-control",
5
5
  "keywords": [
6
6
  "camstack",