@camstack/addon-ai 0.4.42 → 0.4.44

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 +331 -174
  2. package/dist/addon.mjs +331 -174
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -13123,6 +13123,114 @@ method(object({
13123
13123
  height: number$1()
13124
13124
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13125
13125
  /**
13126
+ * `failure-contribution` — the capability an addon reports its OWN losses
13127
+ * through, per camera, with the denominator attached. It stores nothing.
13128
+ *
13129
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13130
+ *
13131
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13132
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13133
+ * copied: the contributor reports what it already knows, hub-main adds only
13134
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13135
+ * somebody to forget to edit.
13136
+ *
13137
+ * They are not merged, because their invariants are opposites:
13138
+ *
13139
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13140
+ * claim a camera cost nothing, which is a measurement nobody made;
13141
+ * - a `failure-contribution` zero is the **most valuable value on the
13142
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13143
+ * and it is exactly what an absent entry cannot say.
13144
+ *
13145
+ * Putting a loss counter on a cost entry would also break the reconciliation
13146
+ * that gives `load-contribution` its point: contributions are subtracted from
13147
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13148
+ * has no process.
13149
+ *
13150
+ * ## Why not a log line, since the counters already exist
13151
+ *
13152
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13153
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13154
+ * ends in a log line, and a log line is the thing the operator asked to stop
13155
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13156
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13157
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13158
+ * media blackout were both diagnosed. The counters stay; this is where they can
13159
+ * be READ.
13160
+ *
13161
+ * ## The rate is served with its denominator or not at all
13162
+ *
13163
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13164
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13165
+ * than yesterday" and was **flat across twelve hours** once divided by the
13166
+ * successes on the same path. A surface that publishes only the numerator
13167
+ * reproduces that mistake on every read.
13168
+ *
13169
+ * ## Shape
13170
+ *
13171
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13172
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13173
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13174
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13175
+ * a forked runner's entries reach hub-main over transport that already exists.
13176
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13177
+ * result through `system.getFailureContributions`.
13178
+ */
13179
+ var FailureReasonCountSchema = object({
13180
+ /**
13181
+ * Why the attempt did not land, in the contributor's own vocabulary —
13182
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13183
+ * strings that already appear in this repo's logs and, where one exists, the
13184
+ * same string the per-track `previewMissReason` records (D276): a second
13185
+ * vocabulary for the same loss would make the row and the counter
13186
+ * un-joinable.
13187
+ */
13188
+ reason: string(),
13189
+ count: number$1().int().nonnegative()
13190
+ });
13191
+ var FailureContributionSchema = object({
13192
+ /**
13193
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13194
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13195
+ * `unit` free: the families are owned by different addons and a shared enum
13196
+ * is a central list that rots invisibly.
13197
+ */
13198
+ family: string(),
13199
+ /**
13200
+ * The NUMERIC device id — the same value every log line carries as
13201
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13202
+ * cannot name the camera must not emit the entry, because a fleet total
13203
+ * cannot answer the only question anybody asks of this surface.
13204
+ */
13205
+ deviceId: number$1().int().positive(),
13206
+ /**
13207
+ * A second dimension inside the family: the model / step id for an inference
13208
+ * timeout, so "which camera AND which model" is one read. Absent when the
13209
+ * family has a single variant.
13210
+ */
13211
+ variant: string().optional(),
13212
+ /**
13213
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13214
+ * differencing two reads must drop the interval when it changes, because the
13215
+ * counter restarted from zero in a respawned runner. Same discipline as
13216
+ * `LoadContribution.startedAtMs`.
13217
+ */
13218
+ sinceMs: number$1(),
13219
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13220
+ atMs: number$1(),
13221
+ /**
13222
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13223
+ * window. A failure count published without it is the mistake this schema
13224
+ * exists to make impossible.
13225
+ */
13226
+ attempts: number$1().int().nonnegative(),
13227
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13228
+ succeeded: number$1().int().nonnegative(),
13229
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13230
+ reasons: array(FailureReasonCountSchema).readonly()
13231
+ });
13232
+ method(_void(), array(FailureContributionSchema).readonly());
13233
+ /**
13126
13234
  * filesystem-browse — per-node capability for browsing the node's local
13127
13235
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13128
13236
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13738,6 +13846,68 @@ var llmCapability = {
13738
13846
  })
13739
13847
  }
13740
13848
  };
13849
+ var LoadContributionSchema = object({
13850
+ role: _enum([
13851
+ "decode",
13852
+ "transcode",
13853
+ "recording",
13854
+ "streaming",
13855
+ "detection"
13856
+ ]),
13857
+ /**
13858
+ * The NUMERIC device id — the same value every log line carries as
13859
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13860
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13861
+ * contributor that cannot name its camera must not emit the entry at all,
13862
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13863
+ * and would quietly turn one camera's cost into everybody's.
13864
+ */
13865
+ deviceId: number$1().int().positive().nullable(),
13866
+ attribution: _enum([
13867
+ "measured",
13868
+ "accounted",
13869
+ "unattributable"
13870
+ ]),
13871
+ /**
13872
+ * What ONE entry is, in the contributor's own words — `615/high`,
13873
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13874
+ * family and inventing a common one would lose the only information that
13875
+ * makes two entries for the same camera distinguishable.
13876
+ */
13877
+ unit: string(),
13878
+ /**
13879
+ * The OS process this cost lives in, when there is one. Present so a
13880
+ * consumer can (a) tell two generations of the same unit apart across a
13881
+ * restart, and (b) subtract claimed processes from the node's process
13882
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13883
+ * process of its own.
13884
+ */
13885
+ pid: number$1().int().positive().optional(),
13886
+ /**
13887
+ * When this generation started. The pid's incarnation marker: a consumer
13888
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13889
+ * window when this changes, because the counter restarted from zero in a new
13890
+ * process.
13891
+ */
13892
+ startedAtMs: number$1().optional(),
13893
+ /**
13894
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13895
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13896
+ * contribution is asked for.
13897
+ *
13898
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13899
+ * needs a sampler, and a new per-node sampler is the defect half of
13900
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13901
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13902
+ *
13903
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13904
+ * an entry with no process.
13905
+ */
13906
+ cpuSeconds: number$1().optional(),
13907
+ /** Resident bytes of this unit's process, same source and same rules. */
13908
+ rssBytes: number$1().optional()
13909
+ });
13910
+ method(_void(), array(LoadContributionSchema).readonly());
13741
13911
  /**
13742
13912
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13743
13913
  * through. It stores nothing.
@@ -13814,176 +13984,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13814
13984
  tags: record(string(), string()).optional()
13815
13985
  }), array(LogEntrySchema).readonly());
13816
13986
  /**
13817
- * `failure-contribution` — the capability an addon reports its OWN losses
13818
- * through, per camera, with the denominator attached. It stores nothing.
13819
- *
13820
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13821
- *
13822
- * `load-contribution` answers *what did this camera COST*. This answers *what
13823
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13824
- * copied: the contributor reports what it already knows, hub-main adds only
13825
- * `addonId`, nothing needs global knowledge, and there is no central list for
13826
- * somebody to forget to edit.
13827
- *
13828
- * They are not merged, because their invariants are opposites:
13829
- *
13830
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13831
- * claim a camera cost nothing, which is a measurement nobody made;
13832
- * - a `failure-contribution` zero is the **most valuable value on the
13833
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13834
- * and it is exactly what an absent entry cannot say.
13835
- *
13836
- * Putting a loss counter on a cost entry would also break the reconciliation
13837
- * that gives `load-contribution` its point: contributions are subtracted from
13838
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13839
- * has no process.
13840
- *
13841
- * ## Why not a log line, since the counters already exist
13842
- *
13843
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13844
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13845
- * ends in a log line, and a log line is the thing the operator asked to stop
13846
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13847
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13848
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13849
- * media blackout were both diagnosed. The counters stay; this is where they can
13850
- * be READ.
13851
- *
13852
- * ## The rate is served with its denominator or not at all
13853
- *
13854
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13855
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13856
- * than yesterday" and was **flat across twelve hours** once divided by the
13857
- * successes on the same path. A surface that publishes only the numerator
13858
- * reproduces that mistake on every read.
13859
- *
13860
- * ## Shape
13861
- *
13862
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13863
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13864
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13865
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13866
- * a forked runner's entries reach hub-main over transport that already exists.
13867
- * No new UDS message, no second registry (D3). The operator reads the assembled
13868
- * result through `system.getFailureContributions`.
13869
- */
13870
- var FailureReasonCountSchema = object({
13871
- /**
13872
- * Why the attempt did not land, in the contributor's own vocabulary —
13873
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13874
- * strings that already appear in this repo's logs and, where one exists, the
13875
- * same string the per-track `previewMissReason` records (D276): a second
13876
- * vocabulary for the same loss would make the row and the counter
13877
- * un-joinable.
13878
- */
13879
- reason: string(),
13880
- count: number$1().int().nonnegative()
13881
- });
13882
- var FailureContributionSchema = object({
13883
- /**
13884
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13885
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13886
- * `unit` free: the families are owned by different addons and a shared enum
13887
- * is a central list that rots invisibly.
13888
- */
13889
- family: string(),
13890
- /**
13891
- * The NUMERIC device id — the same value every log line carries as
13892
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13893
- * cannot name the camera must not emit the entry, because a fleet total
13894
- * cannot answer the only question anybody asks of this surface.
13895
- */
13896
- deviceId: number$1().int().positive(),
13897
- /**
13898
- * A second dimension inside the family: the model / step id for an inference
13899
- * timeout, so "which camera AND which model" is one read. Absent when the
13900
- * family has a single variant.
13901
- */
13902
- variant: string().optional(),
13903
- /**
13904
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13905
- * differencing two reads must drop the interval when it changes, because the
13906
- * counter restarted from zero in a respawned runner. Same discipline as
13907
- * `LoadContribution.startedAtMs`.
13908
- */
13909
- sinceMs: number$1(),
13910
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13911
- atMs: number$1(),
13912
- /**
13913
- * THE DENOMINATOR — every attempt on this path for this camera in the
13914
- * window. A failure count published without it is the mistake this schema
13915
- * exists to make impossible.
13916
- */
13917
- attempts: number$1().int().nonnegative(),
13918
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13919
- succeeded: number$1().int().nonnegative(),
13920
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13921
- reasons: array(FailureReasonCountSchema).readonly()
13922
- });
13923
- method(_void(), array(FailureContributionSchema).readonly());
13924
- var LoadContributionSchema = object({
13925
- role: _enum([
13926
- "decode",
13927
- "transcode",
13928
- "recording",
13929
- "streaming",
13930
- "detection"
13931
- ]),
13932
- /**
13933
- * The NUMERIC device id — the same value every log line carries as
13934
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13935
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13936
- * contributor that cannot name its camera must not emit the entry at all,
13937
- * because an unnamed per-camera entry is indistinguishable from a shared one
13938
- * and would quietly turn one camera's cost into everybody's.
13939
- */
13940
- deviceId: number$1().int().positive().nullable(),
13941
- attribution: _enum([
13942
- "measured",
13943
- "accounted",
13944
- "unattributable"
13945
- ]),
13946
- /**
13947
- * What ONE entry is, in the contributor's own words — `615/high`,
13948
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13949
- * family and inventing a common one would lose the only information that
13950
- * makes two entries for the same camera distinguishable.
13951
- */
13952
- unit: string(),
13953
- /**
13954
- * The OS process this cost lives in, when there is one. Present so a
13955
- * consumer can (a) tell two generations of the same unit apart across a
13956
- * restart, and (b) subtract claimed processes from the node's process
13957
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13958
- * process of its own.
13959
- */
13960
- pid: number$1().int().positive().optional(),
13961
- /**
13962
- * When this generation started. The pid's incarnation marker: a consumer
13963
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13964
- * window when this changes, because the counter restarted from zero in a new
13965
- * process.
13966
- */
13967
- startedAtMs: number$1().optional(),
13968
- /**
13969
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13970
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13971
- * contribution is asked for.
13972
- *
13973
- * Cumulative and not a rate on purpose: a rate needs a window, a window
13974
- * needs a sampler, and a new per-node sampler is the defect half of
13975
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13976
- * by whoever already keeps a history; a rate cannot be un-averaged.
13977
- *
13978
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13979
- * an entry with no process.
13980
- */
13981
- cpuSeconds: number$1().optional(),
13982
- /** Resident bytes of this unit's process, same source and same rules. */
13983
- rssBytes: number$1().optional()
13984
- });
13985
- method(_void(), array(LoadContributionSchema).readonly());
13986
- /**
13987
13987
  * `login-method` — collection cap through which auth addons contribute
13988
13988
  * their pre-auth login surfaces to the login page. This is the SINGLE,
13989
13989
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18643,12 +18643,53 @@ var MediaFileKindEnum = _enum([
18643
18643
  "keyFrameSmall",
18644
18644
  "thumbnailSmall"
18645
18645
  ]);
18646
+ /**
18647
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18648
+ * ARE — never the bytes themselves.
18649
+ *
18650
+ * ## Why `url` and not `base64`
18651
+ *
18652
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18653
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18654
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18655
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18656
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18657
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18658
+ *
18659
+ * `url` points at the `event-media` data plane
18660
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18661
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18662
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18663
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18664
+ * no less protected than they were inside a `view`-level cap response — see
18665
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18666
+ * (per-device scoping).
18667
+ *
18668
+ * The URL is built from the row's **stored** key, which is not always its
18669
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18670
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18671
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18672
+ *
18673
+ * ## `base64` is TRANSITIONAL and is going away
18674
+ *
18675
+ * It is still populated for one reason: the deployed viewer's track-detail
18676
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18677
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18678
+ * triangle — not as absence. Removing the field before that viewer ships is an
18679
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18680
+ * delete this line and the `withBytes` pass-through in
18681
+ * `analytics-query-facade.ts`; nothing else reads it.
18682
+ */
18646
18683
  var MediaFileSchema = object({
18647
18684
  key: string(),
18648
18685
  kind: MediaFileKindEnum,
18649
- base64: string(),
18650
18686
  sizeBytes: number$1(),
18651
18687
  timestamp: number$1()
18688
+ }).extend({
18689
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18690
+ url: string(),
18691
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18692
+ base64: string()
18652
18693
  });
18653
18694
  /**
18654
18695
  * One media row WITHOUT its bytes.
@@ -18660,7 +18701,9 @@ var MediaFileSchema = object({
18660
18701
  * blocks the whole view.
18661
18702
  *
18662
18703
  * `sizeBytes` is carried because it is what lets a client decide between the
18663
- * stored blob and a `?variant=thumb` rendering without fetching either.
18704
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18705
+ * `url` because a client that had to build the plane path itself is a second
18706
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18664
18707
  */
18665
18708
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18666
18709
  /**
@@ -19007,6 +19050,50 @@ var EventStoreFootprintSchema = object({
19007
19050
  totalBytes: number$1().int(),
19008
19051
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19009
19052
  });
19053
+ /** Event-media footprint for one {@link MediaFileKind}. */
19054
+ var EventMediaKindFootprintSchema = object({
19055
+ kind: MediaFileKindEnum,
19056
+ /** Media rows of this kind. */
19057
+ rows: number$1().int(),
19058
+ /** Bytes on disk held by those rows. */
19059
+ bytes: number$1().int()
19060
+ });
19061
+ /**
19062
+ * The media footprint broken down by KIND — the axis a deletion decision
19063
+ * actually turns on.
19064
+ *
19065
+ * A byte total says how much there is; it cannot say what is safe to remove.
19066
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19067
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19068
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19069
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19070
+ * nothing else, so sizing a deletion means summing per kind.
19071
+ *
19072
+ * ## Why `unaccounted*` exists
19073
+ *
19074
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19075
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19076
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19077
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19078
+ * retired code path, or by a version that knew a kind this one does not) would
19079
+ * otherwise vanish from the total silently, and an operator would delete
19080
+ * against a denominator smaller than the disk.
19081
+ *
19082
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19083
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19084
+ */
19085
+ var EventMediaKindBreakdownSchema = object({
19086
+ /** Every media row in scope, from one unfiltered aggregate. */
19087
+ totalRows: number$1().int(),
19088
+ /** Every media byte in scope, from that same aggregate. */
19089
+ totalBytes: number$1().int(),
19090
+ /** Per-kind footprint, ordered by bytes descending. */
19091
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19092
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19093
+ unaccountedRows: number$1().int(),
19094
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19095
+ unaccountedBytes: number$1().int()
19096
+ });
19010
19097
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19011
19098
  var EventPruneCountsSchema = object({
19012
19099
  motion: number$1().int(),
@@ -19210,6 +19297,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19210
19297
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19211
19298
  kind: "query",
19212
19299
  auth: "admin"
19300
+ }), method(object({ deviceId: number$1().int().optional() }), EventMediaKindBreakdownSchema, {
19301
+ kind: "query",
19302
+ auth: "admin"
19213
19303
  }), method(object({
19214
19304
  olderThanMs: number$1(),
19215
19305
  reason: OpsLogReasonSchema.optional()
@@ -19349,6 +19439,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19349
19439
  }), array(MediaFileSchema).readonly()), method(object({
19350
19440
  trackId: string(),
19351
19441
  deviceId: number$1()
19442
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19443
+ eventId: string(),
19444
+ deviceId: number$1()
19352
19445
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19353
19446
  kind: "mutation",
19354
19447
  auth: "admin"
@@ -21158,6 +21251,20 @@ method(object({
21158
21251
  error: string().optional()
21159
21252
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21160
21253
  providerId: string(),
21254
+ /**
21255
+ * The location this config is an UNSAVED edit of, when there is one.
21256
+ *
21257
+ * `listLocations` replaces every declared secret with the redaction
21258
+ * sentinel, so the edit modal's form state holds the sentinel for any
21259
+ * credential the operator did not retype — and posting that here
21260
+ * without a way to resolve it makes the provider try to authenticate
21261
+ * as `__camstack_redacted__` and report the operator's own working
21262
+ * password as wrong. Given this id, the orchestrator restores each
21263
+ * sentinel from the stored config (same rule as `upsertLocation`)
21264
+ * before dispatching. Omitted by the "Add location" wizard, where
21265
+ * every value was typed just now and nothing is stored yet.
21266
+ */
21267
+ locationId: string().optional(),
21161
21268
  config: record(string(), unknown())
21162
21269
  }), object({
21163
21270
  ok: boolean(),
@@ -23609,10 +23716,24 @@ var FaceClusterSchema = object({
23609
23716
  size: number$1().int(),
23610
23717
  cohesion: number$1()
23611
23718
  });
23719
+ /**
23720
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23721
+ * are — never the bytes.
23722
+ *
23723
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23724
+ * track/event contract) is still populated because a deployed viewer requires
23725
+ * the field to parse a row at all; this method has no such reader. Its ONE
23726
+ * caller is the admin UI's detail modal, which was building
23727
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23728
+ * dialog already rendering its key FRAME from the `event-media` plane.
23729
+ *
23730
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23731
+ * media key directly, so this needed no new plane and no new access decision.
23732
+ */
23612
23733
  var MediaFileLiteSchema$1 = object({
23613
23734
  key: string(),
23614
23735
  kind: string(),
23615
- base64: string(),
23736
+ url: string(),
23616
23737
  sizeBytes: number$1(),
23617
23738
  timestamp: number$1()
23618
23739
  });
@@ -25864,10 +25985,24 @@ var PlateInfoSchema = object({
25864
25985
  */
25865
25986
  cropUrl: string().optional()
25866
25987
  });
25988
+ /**
25989
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
25990
+ * are — never the bytes.
25991
+ *
25992
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
25993
+ * track/event contract) is still populated because a deployed viewer requires
25994
+ * the field to parse a row at all; this method has no such reader. Its ONE
25995
+ * caller is the admin UI's detail modal, which was building
25996
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
25997
+ * dialog already rendering its key FRAME from the `event-media` plane.
25998
+ *
25999
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26000
+ * media key directly, so this needed no new plane and no new access decision.
26001
+ */
25867
26002
  var MediaFileLiteSchema = object({
25868
26003
  key: string(),
25869
26004
  kind: string(),
25870
- base64: string(),
26005
+ url: string(),
25871
26006
  sizeBytes: number$1(),
25872
26007
  timestamp: number$1()
25873
26008
  });
@@ -31784,6 +31919,12 @@ Object.freeze({
31784
31919
  addonId: null,
31785
31920
  access: "view"
31786
31921
  },
31922
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31923
+ capName: "pipeline-analytics",
31924
+ capScope: "device",
31925
+ addonId: null,
31926
+ access: "view"
31927
+ },
31787
31928
  "pipelineAnalytics.getEventStoreFootprint": {
31788
31929
  capName: "pipeline-analytics",
31789
31930
  capScope: "device",
@@ -31880,6 +32021,12 @@ Object.freeze({
31880
32021
  addonId: null,
31881
32022
  access: "view"
31882
32023
  },
32024
+ "pipelineAnalytics.listEventMedia": {
32025
+ capName: "pipeline-analytics",
32026
+ capScope: "device",
32027
+ addonId: null,
32028
+ access: "view"
32029
+ },
31883
32030
  "pipelineAnalytics.listGroups": {
31884
32031
  capName: "pipeline-analytics",
31885
32032
  capScope: "device",
@@ -35443,6 +35590,11 @@ Object.freeze({
35443
35590
  form: "single",
35444
35591
  optional: false
35445
35592
  }],
35593
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35594
+ name: "deviceId",
35595
+ form: "single",
35596
+ optional: true
35597
+ }],
35446
35598
  "pipelineAnalytics.getGroup": [{
35447
35599
  name: "deviceId",
35448
35600
  form: "single",
@@ -35503,6 +35655,11 @@ Object.freeze({
35503
35655
  form: "array",
35504
35656
  optional: false
35505
35657
  }],
35658
+ "pipelineAnalytics.listEventMedia": [{
35659
+ name: "deviceId",
35660
+ form: "single",
35661
+ optional: false
35662
+ }],
35506
35663
  "pipelineAnalytics.listGroups": [{
35507
35664
  name: "deviceIds",
35508
35665
  form: "array",
package/dist/addon.mjs CHANGED
@@ -13150,6 +13150,114 @@ method(object({
13150
13150
  height: number$1()
13151
13151
  }), EmbeddingResultSchema, { auth: "admin" }), method(object({ text: string() }), EmbeddingResultSchema, { auth: "admin" }), method(_void(), EmbeddingInfoSchema, { auth: "admin" });
13152
13152
  /**
13153
+ * `failure-contribution` — the capability an addon reports its OWN losses
13154
+ * through, per camera, with the denominator attached. It stores nothing.
13155
+ *
13156
+ * ## The twin of `load-contribution`, and why it is a twin and not a field
13157
+ *
13158
+ * `load-contribution` answers *what did this camera COST*. This answers *what
13159
+ * did this camera LOSE*. The reporting discipline is identical and deliberately
13160
+ * copied: the contributor reports what it already knows, hub-main adds only
13161
+ * `addonId`, nothing needs global knowledge, and there is no central list for
13162
+ * somebody to forget to edit.
13163
+ *
13164
+ * They are not merged, because their invariants are opposites:
13165
+ *
13166
+ * - a `load-contribution` measurement is **absent, never zero** — a zero would
13167
+ * claim a camera cost nothing, which is a measurement nobody made;
13168
+ * - a `failure-contribution` zero is the **most valuable value on the
13169
+ * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13170
+ * and it is exactly what an absent entry cannot say.
13171
+ *
13172
+ * Putting a loss counter on a cost entry would also break the reconciliation
13173
+ * that gives `load-contribution` its point: contributions are subtracted from
13174
+ * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13175
+ * has no process.
13176
+ *
13177
+ * ## Why not a log line, since the counters already exist
13178
+ *
13179
+ * Several of these paths already counted themselves — `CaptureScheduler`'s
13180
+ * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13181
+ * ends in a log line, and a log line is the thing the operator asked to stop
13182
+ * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13183
+ * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13184
+ * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13185
+ * media blackout were both diagnosed. The counters stay; this is where they can
13186
+ * be READ.
13187
+ *
13188
+ * ## The rate is served with its denominator or not at all
13189
+ *
13190
+ * Every entry carries `attempts` and `succeeded`. A miss count alone is
13191
+ * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13192
+ * than yesterday" and was **flat across twelve hours** once divided by the
13193
+ * successes on the same path. A surface that publishes only the numerator
13194
+ * reproduces that mistake on every read.
13195
+ *
13196
+ * ## Shape
13197
+ *
13198
+ * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13199
+ * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13200
+ * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13201
+ * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13202
+ * a forked runner's entries reach hub-main over transport that already exists.
13203
+ * No new UDS message, no second registry (D3). The operator reads the assembled
13204
+ * result through `system.getFailureContributions`.
13205
+ */
13206
+ var FailureReasonCountSchema = object({
13207
+ /**
13208
+ * Why the attempt did not land, in the contributor's own vocabulary —
13209
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13210
+ * strings that already appear in this repo's logs and, where one exists, the
13211
+ * same string the per-track `previewMissReason` records (D276): a second
13212
+ * vocabulary for the same loss would make the row and the counter
13213
+ * un-joinable.
13214
+ */
13215
+ reason: string(),
13216
+ count: number$1().int().nonnegative()
13217
+ });
13218
+ var FailureContributionSchema = object({
13219
+ /**
13220
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13221
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13222
+ * `unit` free: the families are owned by different addons and a shared enum
13223
+ * is a central list that rots invisibly.
13224
+ */
13225
+ family: string(),
13226
+ /**
13227
+ * The NUMERIC device id — the same value every log line carries as
13228
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
13229
+ * cannot name the camera must not emit the entry, because a fleet total
13230
+ * cannot answer the only question anybody asks of this surface.
13231
+ */
13232
+ deviceId: number$1().int().positive(),
13233
+ /**
13234
+ * A second dimension inside the family: the model / step id for an inference
13235
+ * timeout, so "which camera AND which model" is one read. Absent when the
13236
+ * family has a single variant.
13237
+ */
13238
+ variant: string().optional(),
13239
+ /**
13240
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13241
+ * differencing two reads must drop the interval when it changes, because the
13242
+ * counter restarted from zero in a respawned runner. Same discipline as
13243
+ * `LoadContribution.startedAtMs`.
13244
+ */
13245
+ sinceMs: number$1(),
13246
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13247
+ atMs: number$1(),
13248
+ /**
13249
+ * THE DENOMINATOR — every attempt on this path for this camera in the
13250
+ * window. A failure count published without it is the mistake this schema
13251
+ * exists to make impossible.
13252
+ */
13253
+ attempts: number$1().int().nonnegative(),
13254
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
13255
+ succeeded: number$1().int().nonnegative(),
13256
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
13257
+ reasons: array(FailureReasonCountSchema).readonly()
13258
+ });
13259
+ method(_void(), array(FailureContributionSchema).readonly());
13260
+ /**
13153
13261
  * filesystem-browse — per-node capability for browsing the node's local
13154
13262
  * filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
13155
13263
  * are sandboxed to operator-configured allowed roots (D115). Used by the
@@ -13765,6 +13873,68 @@ var llmCapability = {
13765
13873
  })
13766
13874
  }
13767
13875
  };
13876
+ var LoadContributionSchema = object({
13877
+ role: _enum([
13878
+ "decode",
13879
+ "transcode",
13880
+ "recording",
13881
+ "streaming",
13882
+ "detection"
13883
+ ]),
13884
+ /**
13885
+ * The NUMERIC device id — the same value every log line carries as
13886
+ * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13887
+ * camera (a shared pool), NOT that the contributor forgot to look it up: a
13888
+ * contributor that cannot name its camera must not emit the entry at all,
13889
+ * because an unnamed per-camera entry is indistinguishable from a shared one
13890
+ * and would quietly turn one camera's cost into everybody's.
13891
+ */
13892
+ deviceId: number$1().int().positive().nullable(),
13893
+ attribution: _enum([
13894
+ "measured",
13895
+ "accounted",
13896
+ "unattributable"
13897
+ ]),
13898
+ /**
13899
+ * What ONE entry is, in the contributor's own words — `615/high`,
13900
+ * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13901
+ * family and inventing a common one would lose the only information that
13902
+ * makes two entries for the same camera distinguishable.
13903
+ */
13904
+ unit: string(),
13905
+ /**
13906
+ * The OS process this cost lives in, when there is one. Present so a
13907
+ * consumer can (a) tell two generations of the same unit apart across a
13908
+ * restart, and (b) subtract claimed processes from the node's process
13909
+ * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13910
+ * process of its own.
13911
+ */
13912
+ pid: number$1().int().positive().optional(),
13913
+ /**
13914
+ * When this generation started. The pid's incarnation marker: a consumer
13915
+ * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13916
+ * window when this changes, because the counter restarted from zero in a new
13917
+ * process.
13918
+ */
13919
+ startedAtMs: number$1().optional(),
13920
+ /**
13921
+ * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13922
+ * system, read from the child's own `/proc/<pid>/stat` at the moment the
13923
+ * contribution is asked for.
13924
+ *
13925
+ * Cumulative and not a rate on purpose: a rate needs a window, a window
13926
+ * needs a sampler, and a new per-node sampler is the defect half of
13927
+ * `docs/architecture/load-ledger.md` documents. A counter can be differenced
13928
+ * by whoever already keeps a history; a rate cannot be un-averaged.
13929
+ *
13930
+ * Absent — never zero — on a node with no `/proc`, on a read failure, and on
13931
+ * an entry with no process.
13932
+ */
13933
+ cpuSeconds: number$1().optional(),
13934
+ /** Resident bytes of this unit's process, same source and same rules. */
13935
+ rssBytes: number$1().optional()
13936
+ });
13937
+ method(_void(), array(LoadContributionSchema).readonly());
13768
13938
  /**
13769
13939
  * `log-channels` — the capability an addon DECLARES its diagnostic channels
13770
13940
  * through. It stores nothing.
@@ -13841,176 +14011,6 @@ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
13841
14011
  tags: record(string(), string()).optional()
13842
14012
  }), array(LogEntrySchema).readonly());
13843
14013
  /**
13844
- * `failure-contribution` — the capability an addon reports its OWN losses
13845
- * through, per camera, with the denominator attached. It stores nothing.
13846
- *
13847
- * ## The twin of `load-contribution`, and why it is a twin and not a field
13848
- *
13849
- * `load-contribution` answers *what did this camera COST*. This answers *what
13850
- * did this camera LOSE*. The reporting discipline is identical and deliberately
13851
- * copied: the contributor reports what it already knows, hub-main adds only
13852
- * `addonId`, nothing needs global knowledge, and there is no central list for
13853
- * somebody to forget to edit.
13854
- *
13855
- * They are not merged, because their invariants are opposites:
13856
- *
13857
- * - a `load-contribution` measurement is **absent, never zero** — a zero would
13858
- * claim a camera cost nothing, which is a measurement nobody made;
13859
- * - a `failure-contribution` zero is the **most valuable value on the
13860
- * surface** — `attempts: 400, succeeded: 400` is the proof a fix landed,
13861
- * and it is exactly what an absent entry cannot say.
13862
- *
13863
- * Putting a loss counter on a cost entry would also break the reconciliation
13864
- * that gives `load-contribution` its point: contributions are subtracted from
13865
- * `metrics.node-processes-snapshot` to find processes nobody claims. A failure
13866
- * has no process.
13867
- *
13868
- * ## Why not a log line, since the counters already exist
13869
- *
13870
- * Several of these paths already counted themselves — `CaptureScheduler`'s
13871
- * per-device window, `KeyFrameCaptureLog`, `bumpCropMetric`. Every one of them
13872
- * ends in a log line, and a log line is the thing the operator asked to stop
13873
- * needing: *"possiamo armare questi errori intanto? Così al prossimo giro
13874
- * ricontrolliamo tutti questi punti"*. Reading them meant grepping Loki and
13875
- * hand-correlating timestamps, which is how a 22% thumbnail gap and a 3-hour
13876
- * media blackout were both diagnosed. The counters stay; this is where they can
13877
- * be READ.
13878
- *
13879
- * ## The rate is served with its denominator or not at all
13880
- *
13881
- * Every entry carries `attempts` and `succeeded`. A miss count alone is
13882
- * unreadable: on 2026-08-28 the enrichment-crop miss count read as "35x worse
13883
- * than yesterday" and was **flat across twelve hours** once divided by the
13884
- * successes on the same path. A surface that publishes only the numerator
13885
- * reproduces that mistake on every read.
13886
- *
13887
- * ## Shape
13888
- *
13889
- * Copied from `load-contribution.cap.ts` (`mode: 'collection'`,
13890
- * `internal: true`, `mount: { kind: 'skip' }`): no tRPC route of its own and no
13891
- * generated hooks, while `addons.listCapabilityProviders` still enumerates it
13892
- * and the hub's `CapabilityRegistry` still holds an RPC proxy per provider — so
13893
- * a forked runner's entries reach hub-main over transport that already exists.
13894
- * No new UDS message, no second registry (D3). The operator reads the assembled
13895
- * result through `system.getFailureContributions`.
13896
- */
13897
- var FailureReasonCountSchema = object({
13898
- /**
13899
- * Why the attempt did not land, in the contributor's own vocabulary —
13900
- * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
13901
- * strings that already appear in this repo's logs and, where one exists, the
13902
- * same string the per-track `previewMissReason` records (D276): a second
13903
- * vocabulary for the same loss would make the row and the counter
13904
- * un-joinable.
13905
- */
13906
- reason: string(),
13907
- count: number$1().int().nonnegative()
13908
- });
13909
- var FailureContributionSchema = object({
13910
- /**
13911
- * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
13912
- * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
13913
- * `unit` free: the families are owned by different addons and a shared enum
13914
- * is a central list that rots invisibly.
13915
- */
13916
- family: string(),
13917
- /**
13918
- * The NUMERIC device id — the same value every log line carries as
13919
- * `tags.deviceId`. Never nullable and never absent: a contributor that
13920
- * cannot name the camera must not emit the entry, because a fleet total
13921
- * cannot answer the only question anybody asks of this surface.
13922
- */
13923
- deviceId: number$1().int().positive(),
13924
- /**
13925
- * A second dimension inside the family: the model / step id for an inference
13926
- * timeout, so "which camera AND which model" is one read. Absent when the
13927
- * family has a single variant.
13928
- */
13929
- variant: string().optional(),
13930
- /**
13931
- * Epoch ms this counter started — the INCARNATION MARKER. A consumer
13932
- * differencing two reads must drop the interval when it changes, because the
13933
- * counter restarted from zero in a respawned runner. Same discipline as
13934
- * `LoadContribution.startedAtMs`.
13935
- */
13936
- sinceMs: number$1(),
13937
- /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
13938
- atMs: number$1(),
13939
- /**
13940
- * THE DENOMINATOR — every attempt on this path for this camera in the
13941
- * window. A failure count published without it is the mistake this schema
13942
- * exists to make impossible.
13943
- */
13944
- attempts: number$1().int().nonnegative(),
13945
- /** Attempts that landed. `attempts - succeeded` is the loss. */
13946
- succeeded: number$1().int().nonnegative(),
13947
- /** The loss, partitioned. Sums to `attempts - succeeded`. */
13948
- reasons: array(FailureReasonCountSchema).readonly()
13949
- });
13950
- method(_void(), array(FailureContributionSchema).readonly());
13951
- var LoadContributionSchema = object({
13952
- role: _enum([
13953
- "decode",
13954
- "transcode",
13955
- "recording",
13956
- "streaming",
13957
- "detection"
13958
- ]),
13959
- /**
13960
- * The NUMERIC device id — the same value every log line carries as
13961
- * `tags.deviceId`. `null` means this cost genuinely belongs to no single
13962
- * camera (a shared pool), NOT that the contributor forgot to look it up: a
13963
- * contributor that cannot name its camera must not emit the entry at all,
13964
- * because an unnamed per-camera entry is indistinguishable from a shared one
13965
- * and would quietly turn one camera's cost into everybody's.
13966
- */
13967
- deviceId: number$1().int().positive().nullable(),
13968
- attribution: _enum([
13969
- "measured",
13970
- "accounted",
13971
- "unattributable"
13972
- ]),
13973
- /**
13974
- * What ONE entry is, in the contributor's own words — `615/high`,
13975
- * `617/native`, `cuda:0 shared pool`. Free text because the unit differs per
13976
- * family and inventing a common one would lose the only information that
13977
- * makes two entries for the same camera distinguishable.
13978
- */
13979
- unit: string(),
13980
- /**
13981
- * The OS process this cost lives in, when there is one. Present so a
13982
- * consumer can (a) tell two generations of the same unit apart across a
13983
- * restart, and (b) subtract claimed processes from the node's process
13984
- * snapshot to see what NOBODY claimed. Absent for an entry that owns no
13985
- * process of its own.
13986
- */
13987
- pid: number$1().int().positive().optional(),
13988
- /**
13989
- * When this generation started. The pid's incarnation marker: a consumer
13990
- * differencing {@link LoadContributionSchema.shape.cpuSeconds} must drop the
13991
- * window when this changes, because the counter restarted from zero in a new
13992
- * process.
13993
- */
13994
- startedAtMs: number$1().optional(),
13995
- /**
13996
- * CUMULATIVE CPU seconds this unit has consumed since it started — user +
13997
- * system, read from the child's own `/proc/<pid>/stat` at the moment the
13998
- * contribution is asked for.
13999
- *
14000
- * Cumulative and not a rate on purpose: a rate needs a window, a window
14001
- * needs a sampler, and a new per-node sampler is the defect half of
14002
- * `docs/architecture/load-ledger.md` documents. A counter can be differenced
14003
- * by whoever already keeps a history; a rate cannot be un-averaged.
14004
- *
14005
- * Absent — never zero — on a node with no `/proc`, on a read failure, and on
14006
- * an entry with no process.
14007
- */
14008
- cpuSeconds: number$1().optional(),
14009
- /** Resident bytes of this unit's process, same source and same rules. */
14010
- rssBytes: number$1().optional()
14011
- });
14012
- method(_void(), array(LoadContributionSchema).readonly());
14013
- /**
14014
14014
  * `login-method` — collection cap through which auth addons contribute
14015
14015
  * their pre-auth login surfaces to the login page. This is the SINGLE,
14016
14016
  * generic mechanism that supersedes the dead `auth.listProviders` reader:
@@ -18670,12 +18670,53 @@ var MediaFileKindEnum = _enum([
18670
18670
  "keyFrameSmall",
18671
18671
  "thumbnailSmall"
18672
18672
  ]);
18673
+ /**
18674
+ * One media row ON THE WIRE: what it is, how big it is, and WHERE ITS BYTES
18675
+ * ARE — never the bytes themselves.
18676
+ *
18677
+ * ## Why `url` and not `base64`
18678
+ *
18679
+ * Measured on the live hub 2026-08-30: `getTrackMedia {trackId, deviceId}`
18680
+ * with no `kinds` returned 6 rows / **3 597 219 B**, of which `keyFrame` alone
18681
+ * was **2 824 077 B** — one full-resolution frame, base64, so +33 % on the
18682
+ * wire. Forty events is ~144 MB. Every byte of it was read off disk,
18683
+ * base64-encoded, held whole in a unary tRPC envelope, and materialised in
18684
+ * hub-main's heap on the way past — for an `<img>` that would have cached it.
18685
+ *
18686
+ * `url` points at the `event-media` data plane
18687
+ * (`/addon/<addonId>/event-media/<storedKey>`), which serves the same blob
18688
+ * with an ETag and `Cache-Control: immutable`, honours conditional GETs, can
18689
+ * render a `?variant=thumb`, and streams. The hub gate in front of it requires
18690
+ * a bearer or the session cookie (`access: 'authenticated'`), so the bytes are
18691
+ * no less protected than they were inside a `view`-level cap response — see
18692
+ * `data-plane-access.ts` for the rule and the one gap it does not close
18693
+ * (per-device scoping).
18694
+ *
18695
+ * The URL is built from the row's **stored** key, which is not always its
18696
+ * published `kind`: a track's face/plate crop is stored as `crop` under
18697
+ * `('face'|'plate', '<prefix>-<trackId>')` and published as
18698
+ * `faceCrop`/`plateCrop`. `MediaStore.getByKey` knows only the stored key.
18699
+ *
18700
+ * ## `base64` is TRANSITIONAL and is going away
18701
+ *
18702
+ * It is still populated for one reason: the deployed viewer's track-detail
18703
+ * HERO tile reads it (`use-track-media-entry.ts` → `parseMediaFiles`, which
18704
+ * REQUIRES the field), and a row without it parses as a FAILED read — the red
18705
+ * triangle — not as absence. Removing the field before that viewer ships is an
18706
+ * outage, not a cleanup. Once the viewer takes its hero bytes from `url`,
18707
+ * delete this line and the `withBytes` pass-through in
18708
+ * `analytics-query-facade.ts`; nothing else reads it.
18709
+ */
18673
18710
  var MediaFileSchema = object({
18674
18711
  key: string(),
18675
18712
  kind: MediaFileKindEnum,
18676
- base64: string(),
18677
18713
  sizeBytes: number$1(),
18678
18714
  timestamp: number$1()
18715
+ }).extend({
18716
+ /** `/addon/<addonId>/event-media/<encoded stored key>`. Always present. */
18717
+ url: string(),
18718
+ /** @deprecated Transitional — see the schema docblock. Use {@link url}. */
18719
+ base64: string()
18679
18720
  });
18680
18721
  /**
18681
18722
  * One media row WITHOUT its bytes.
@@ -18687,7 +18728,9 @@ var MediaFileSchema = object({
18687
18728
  * blocks the whole view.
18688
18729
  *
18689
18730
  * `sizeBytes` is carried because it is what lets a client decide between the
18690
- * stored blob and a `?variant=thumb` rendering without fetching either.
18731
+ * stored blob and a `?variant=thumb` rendering without fetching either, and
18732
+ * `url` because a client that had to build the plane path itself is a second
18733
+ * copy of a route — the embed, the viewer and the admin UI each grew one.
18691
18734
  */
18692
18735
  var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18693
18736
  /**
@@ -19034,6 +19077,50 @@ var EventStoreFootprintSchema = object({
19034
19077
  totalBytes: number$1().int(),
19035
19078
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19036
19079
  });
19080
+ /** Event-media footprint for one {@link MediaFileKind}. */
19081
+ var EventMediaKindFootprintSchema = object({
19082
+ kind: MediaFileKindEnum,
19083
+ /** Media rows of this kind. */
19084
+ rows: number$1().int(),
19085
+ /** Bytes on disk held by those rows. */
19086
+ bytes: number$1().int()
19087
+ });
19088
+ /**
19089
+ * The media footprint broken down by KIND — the axis a deletion decision
19090
+ * actually turns on.
19091
+ *
19092
+ * A byte total says how much there is; it cannot say what is safe to remove.
19093
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19094
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19095
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19096
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19097
+ * nothing else, so sizing a deletion means summing per kind.
19098
+ *
19099
+ * ## Why `unaccounted*` exists
19100
+ *
19101
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19102
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19103
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19104
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19105
+ * retired code path, or by a version that knew a kind this one does not) would
19106
+ * otherwise vanish from the total silently, and an operator would delete
19107
+ * against a denominator smaller than the disk.
19108
+ *
19109
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19110
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19111
+ */
19112
+ var EventMediaKindBreakdownSchema = object({
19113
+ /** Every media row in scope, from one unfiltered aggregate. */
19114
+ totalRows: number$1().int(),
19115
+ /** Every media byte in scope, from that same aggregate. */
19116
+ totalBytes: number$1().int(),
19117
+ /** Per-kind footprint, ordered by bytes descending. */
19118
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19119
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19120
+ unaccountedRows: number$1().int(),
19121
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19122
+ unaccountedBytes: number$1().int()
19123
+ });
19037
19124
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19038
19125
  var EventPruneCountsSchema = object({
19039
19126
  motion: number$1().int(),
@@ -19237,6 +19324,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19237
19324
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19238
19325
  kind: "query",
19239
19326
  auth: "admin"
19327
+ }), method(object({ deviceId: number$1().int().optional() }), EventMediaKindBreakdownSchema, {
19328
+ kind: "query",
19329
+ auth: "admin"
19240
19330
  }), method(object({
19241
19331
  olderThanMs: number$1(),
19242
19332
  reason: OpsLogReasonSchema.optional()
@@ -19376,6 +19466,9 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), array(TrackSchema).r
19376
19466
  }), array(MediaFileSchema).readonly()), method(object({
19377
19467
  trackId: string(),
19378
19468
  deviceId: number$1()
19469
+ }), array(MediaFileInfoSchema).readonly()), method(object({
19470
+ eventId: string(),
19471
+ deviceId: number$1()
19379
19472
  }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
19380
19473
  kind: "mutation",
19381
19474
  auth: "admin"
@@ -21185,6 +21278,20 @@ method(object({
21185
21278
  error: string().optional()
21186
21279
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21187
21280
  providerId: string(),
21281
+ /**
21282
+ * The location this config is an UNSAVED edit of, when there is one.
21283
+ *
21284
+ * `listLocations` replaces every declared secret with the redaction
21285
+ * sentinel, so the edit modal's form state holds the sentinel for any
21286
+ * credential the operator did not retype — and posting that here
21287
+ * without a way to resolve it makes the provider try to authenticate
21288
+ * as `__camstack_redacted__` and report the operator's own working
21289
+ * password as wrong. Given this id, the orchestrator restores each
21290
+ * sentinel from the stored config (same rule as `upsertLocation`)
21291
+ * before dispatching. Omitted by the "Add location" wizard, where
21292
+ * every value was typed just now and nothing is stored yet.
21293
+ */
21294
+ locationId: string().optional(),
21188
21295
  config: record(string(), unknown())
21189
21296
  }), object({
21190
21297
  ok: boolean(),
@@ -23636,10 +23743,24 @@ var FaceClusterSchema = object({
23636
23743
  size: number$1().int(),
23637
23744
  cohesion: number$1()
23638
23745
  });
23746
+ /**
23747
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
23748
+ * are — never the bytes.
23749
+ *
23750
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
23751
+ * track/event contract) is still populated because a deployed viewer requires
23752
+ * the field to parse a row at all; this method has no such reader. Its ONE
23753
+ * caller is the admin UI's detail modal, which was building
23754
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
23755
+ * dialog already rendering its key FRAME from the `event-media` plane.
23756
+ *
23757
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
23758
+ * media key directly, so this needed no new plane and no new access decision.
23759
+ */
23639
23760
  var MediaFileLiteSchema$1 = object({
23640
23761
  key: string(),
23641
23762
  kind: string(),
23642
- base64: string(),
23763
+ url: string(),
23643
23764
  sizeBytes: number$1(),
23644
23765
  timestamp: number$1()
23645
23766
  });
@@ -25891,10 +26012,24 @@ var PlateInfoSchema = object({
25891
26012
  */
25892
26013
  cropUrl: string().optional()
25893
26014
  });
26015
+ /**
26016
+ * One gallery media row: what the crop is, how big it is, and WHERE its bytes
26017
+ * are — never the bytes.
26018
+ *
26019
+ * `base64` was deleted here rather than deprecated. `MediaFile.base64` (the
26020
+ * track/event contract) is still populated because a deployed viewer requires
26021
+ * the field to parse a row at all; this method has no such reader. Its ONE
26022
+ * caller is the admin UI's detail modal, which was building
26023
+ * `data:image/jpeg;base64,…` from a row whose `key` sat right beside it, in a
26024
+ * dialog already rendering its key FRAME from the `event-media` plane.
26025
+ *
26026
+ * `url` is `/addon/<addonId>/event-media/<encoded key>`. The plane resolves a
26027
+ * media key directly, so this needed no new plane and no new access decision.
26028
+ */
25894
26029
  var MediaFileLiteSchema = object({
25895
26030
  key: string(),
25896
26031
  kind: string(),
25897
- base64: string(),
26032
+ url: string(),
25898
26033
  sizeBytes: number$1(),
25899
26034
  timestamp: number$1()
25900
26035
  });
@@ -31811,6 +31946,12 @@ Object.freeze({
31811
31946
  addonId: null,
31812
31947
  access: "view"
31813
31948
  },
31949
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
31950
+ capName: "pipeline-analytics",
31951
+ capScope: "device",
31952
+ addonId: null,
31953
+ access: "view"
31954
+ },
31814
31955
  "pipelineAnalytics.getEventStoreFootprint": {
31815
31956
  capName: "pipeline-analytics",
31816
31957
  capScope: "device",
@@ -31907,6 +32048,12 @@ Object.freeze({
31907
32048
  addonId: null,
31908
32049
  access: "view"
31909
32050
  },
32051
+ "pipelineAnalytics.listEventMedia": {
32052
+ capName: "pipeline-analytics",
32053
+ capScope: "device",
32054
+ addonId: null,
32055
+ access: "view"
32056
+ },
31910
32057
  "pipelineAnalytics.listGroups": {
31911
32058
  capName: "pipeline-analytics",
31912
32059
  capScope: "device",
@@ -35470,6 +35617,11 @@ Object.freeze({
35470
35617
  form: "single",
35471
35618
  optional: false
35472
35619
  }],
35620
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35621
+ name: "deviceId",
35622
+ form: "single",
35623
+ optional: true
35624
+ }],
35473
35625
  "pipelineAnalytics.getGroup": [{
35474
35626
  name: "deviceId",
35475
35627
  form: "single",
@@ -35530,6 +35682,11 @@ Object.freeze({
35530
35682
  form: "array",
35531
35683
  optional: false
35532
35684
  }],
35685
+ "pipelineAnalytics.listEventMedia": [{
35686
+ name: "deviceId",
35687
+ form: "single",
35688
+ optional: false
35689
+ }],
35533
35690
  "pipelineAnalytics.listGroups": [{
35534
35691
  name: "deviceIds",
35535
35692
  form: "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.42",
3
+ "version": "0.4.44",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",