@camstack/addon-pipeline 1.2.375 → 1.2.601

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 (34) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-DF5qs8u4.mjs → custom-model-catalog-CK9GK3N5.mjs} +2 -2
  4. package/dist/{custom-model-catalog-gbLoUFki.js → custom-model-catalog-DgyafyuW.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-QyeZXkAO.mjs → dist-BaHsDOWa.mjs} +391 -22
  8. package/dist/{dist-C0Mg8e3I.js → dist-BqI10eLV.js} +396 -21
  9. package/dist/{model-catalogs-CIE4Dz4x.mjs → model-catalogs-DM5sVj1T.mjs} +1 -1
  10. package/dist/{model-catalogs-BTuX-P0k.js → model-catalogs-DhGVt_sH.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-ndjp9DQO.mjs → node-B0a0dXhg.mjs} +38 -3
  14. package/dist/{node-CifGKjsd.js → node-CgwqjGz0.js} +42 -1
  15. package/dist/pipeline-runner/index.js +4 -4
  16. package/dist/pipeline-runner/index.mjs +4 -4
  17. package/dist/{process-memory-BMfSP5-i.js → process-memory-C0zbWvwD.js} +1 -1
  18. package/dist/{process-memory-CaMI-Ry2.mjs → process-memory-sfdcLVkW.mjs} +1 -1
  19. package/dist/recorder/index.js +343 -516
  20. package/dist/recorder/index.mjs +343 -516
  21. package/dist/{segment-demux-js-LhwVw18z.js → segment-demux-js-B9lL2VCC.js} +1 -1
  22. package/dist/{segment-demux-js-CLXBgUTo.mjs → segment-demux-js-BB4CUAB5.mjs} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4MD_B4k.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbN9DjoK.mjs} +3 -3
  25. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BvMrdOCD.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boq8JqyD.mjs} +1 -1
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B25q1OKG.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-DBDp5YHD.mjs → hostInit-BSYHAfPt.mjs} +3 -3
  30. package/dist/stream-broker/index.js +10 -9
  31. package/dist/stream-broker/index.mjs +10 -9
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JVFs9LgI.mjs +0 -26
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-C0Mg8e3I.js");
7
- const require_node = require("../node-CifGKjsd.js");
6
+ const require_dist = require("../dist-BqI10eLV.js");
7
+ const require_node = require("../node-CgwqjGz0.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
10
10
  let node_crypto = require("node:crypto");
@@ -5203,29 +5203,6 @@ function activeModeForConfig(config) {
5203
5203
  * pays for ten minutes, not until the next restart.
5204
5204
  */
5205
5205
  var CENSUS_MAX_WINDOW_MS = 10 * 6e4;
5206
- /** Store procedures worth attributing. Reads only: a write is an operator
5207
- * gesture or a pipeline product, and neither is a mystery. */
5208
- var CENSUSED_PROCEDURES = new Set([
5209
- "query",
5210
- "count",
5211
- "aggregate",
5212
- "get",
5213
- "histogram",
5214
- "getAll"
5215
- ]);
5216
- /**
5217
- * Can `new Proxy` wrap this, and is it worth wrapping?
5218
- *
5219
- * Functions count. The generated store client is a **tRPC proxy** and every
5220
- * intermediate node of one is CALLABLE — `typeof client.query` is `'function'`,
5221
- * not `'object'`. A wrapper that tested only for `'object'` handed the whole
5222
- * client back unwrapped, deployed, and reported zero `query` and zero `count`
5223
- * call sites while the engine was recording thousands a minute against the same
5224
- * tables. Silence read as "nobody in this addon does it".
5225
- */
5226
- function isProxyable(value) {
5227
- return typeof value === "object" && value !== null || typeof value === "function";
5228
- }
5229
5206
  function rowsOf(value) {
5230
5207
  if (Array.isArray(value)) return value.length;
5231
5208
  if (typeof value === "number") return value;
@@ -5349,34 +5326,6 @@ var StoreReadCensus = class {
5349
5326
  })).sort((a, b) => b.calls - a.calls);
5350
5327
  }
5351
5328
  };
5352
- /**
5353
- * Wrap a settings-store client so every read is counted.
5354
- *
5355
- * A `Proxy` over the generated tRPC client rather than a hand-written facade:
5356
- * the client has a procedure per cap method and a facade would need updating
5357
- * every time the cap gains one - which is how a diagnostic quietly stops seeing
5358
- * half the traffic. Unrecognised procedures pass straight through.
5359
- */
5360
- function censusStoreClient(client, census) {
5361
- return new Proxy(client, { get(target, prop, receiver) {
5362
- const procedure = Reflect.get(target, prop, receiver);
5363
- if (typeof prop !== "string" || !CENSUSED_PROCEDURES.has(prop)) return procedure;
5364
- if (!isProxyable(procedure)) return procedure;
5365
- const op = prop;
5366
- return new Proxy(procedure, { get(procTarget, procProp, procReceiver) {
5367
- const call = Reflect.get(procTarget, procProp, procReceiver);
5368
- if (procProp !== "query" && procProp !== "mutate") return call;
5369
- if (typeof call !== "function") return call;
5370
- const wrapped = async (...args) => {
5371
- const site = census.siteOf(wrapped);
5372
- const result = await Reflect.apply(call, procTarget, args);
5373
- census.record(op, site, result);
5374
- return result;
5375
- };
5376
- return wrapped;
5377
- } });
5378
- } });
5379
- }
5380
5329
  /** Log the census as one line, including when it collected nothing - silence
5381
5330
  * after an arm is indistinguishable from a census that never armed. */
5382
5331
  function logCensus(census, logger, topN) {
@@ -8369,259 +8318,76 @@ function buildExportArgs(input) {
8369
8318
  outPath
8370
8319
  ];
8371
8320
  }
8372
- var ROW_MAP_VALUE_COLUMN = "value";
8373
- var RowMapStore = class RowMapStore {
8374
- spec;
8375
- store;
8376
- logger;
8377
- constructor(deps) {
8378
- this.spec = deps.spec;
8379
- this.store = deps.store;
8380
- this.logger = deps.logger;
8381
- }
8382
- /** The collection this store owns — for the caller's own log lines. */
8383
- get collection() {
8384
- return this.spec.collection;
8385
- }
8386
- /**
8387
- * Register the collection. MUST run at boot, before any read or write: the
8388
- * SQLite backend answers 412 for an undeclared collection.
8389
- */
8390
- static declare(store, spec) {
8391
- return store.declareCollection.mutate({
8392
- collection: spec.collection,
8393
- columns: [
8394
- {
8395
- name: "id",
8396
- type: "TEXT",
8397
- primaryKey: true,
8398
- notNull: true
8399
- },
8400
- ...spec.columns,
8401
- {
8402
- name: ROW_MAP_VALUE_COLUMN,
8403
- type: "JSON",
8404
- notNull: true
8405
- }
8406
- ],
8407
- ...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
8408
- });
8409
- }
8410
- declare() {
8411
- return RowMapStore.declare(this.store, this.spec);
8412
- }
8413
- /** One entry, by key. `null` when absent OR unparseable (which is said aloud). */
8414
- async read(key) {
8415
- const raw = await this.store.get.query({
8416
- collection: this.spec.collection,
8417
- key
8418
- });
8419
- if (!isRecord(raw)) return null;
8420
- const row = raw;
8421
- const parsed = this.spec.schema.safeParse(row[ROW_MAP_VALUE_COLUMN]);
8422
- if (!parsed.success) {
8423
- this.logger.warn("row-map row skipped as unparseable", {
8424
- ...this.deviceTags(row),
8425
- meta: {
8426
- collection: this.spec.collection,
8427
- key,
8428
- error: parsed.error.message
8429
- }
8430
- });
8431
- return null;
8432
- }
8433
- return parsed.data;
8434
- }
8435
- /**
8436
- * Every entry, keyed. Unparseable rows are skipped — one bad entry costs that
8437
- * entry, never the map.
8438
- */
8439
- async readAll() {
8440
- const records = await this.store.query.query({
8441
- collection: this.spec.collection,
8442
- filter: { limit: this.spec.loadLimit }
8443
- });
8444
- const out = /* @__PURE__ */ new Map();
8445
- let skipped = 0;
8446
- for (const record of records) {
8447
- const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
8448
- if (!parsed.success) {
8449
- skipped += 1;
8450
- continue;
8451
- }
8452
- out.set(record.id, parsed.data);
8453
- }
8454
- if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
8455
- collection: this.spec.collection,
8456
- skipped,
8457
- kept: out.size
8458
- } });
8459
- if (records.length >= this.spec.loadLimit) this.logger.warn("row-map read hit its own bound — the map may be truncated", { meta: {
8460
- collection: this.spec.collection,
8461
- loadLimit: this.spec.loadLimit
8462
- } });
8463
- return out;
8464
- }
8465
- /**
8466
- * Every entry as it was WRITTEN, unvalidated and unparsed — the read a
8467
- * MIGRATION needs.
8468
- *
8469
- * {@link readAll} answers through the CURRENT schema, which is exactly what a
8470
- * migration must not do: a retired key is stripped by a non-strict `z.object`
8471
- * before the migration could see it, so the row it is there to rescue looks
8472
- * like a row that never needed rescuing. Only a migration should call this;
8473
- * everything else wants the validated read.
8474
- */
8475
- async readAllRaw() {
8476
- const records = await this.store.query.query({
8477
- collection: this.spec.collection,
8478
- filter: { limit: this.spec.loadLimit }
8479
- });
8480
- const out = /* @__PURE__ */ new Map();
8481
- for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
8482
- return out;
8483
- }
8484
- /** Insert or replace ONE entry. Validates at the boundary. */
8485
- async write(key, value) {
8486
- const validated = this.spec.schema.parse(value);
8487
- const columns = this.spec.project(key, validated);
8488
- await this.store.set.mutate({
8489
- collection: this.spec.collection,
8490
- key,
8491
- value: {
8492
- ...columns,
8493
- [ROW_MAP_VALUE_COLUMN]: validated
8494
- }
8495
- });
8496
- }
8497
- /** Drop ONE entry. Idempotent. */
8498
- async remove(key) {
8499
- await this.store.delete.mutate({
8500
- collection: this.spec.collection,
8501
- key
8502
- });
8503
- }
8321
+ var recorderRecordKinds = require_dist.defineRecordKinds({
8504
8322
  /**
8505
- * Make the collection hold exactly `next` — the write path for an owner whose
8506
- * in-memory map IS the authority (a persister handed the complete set).
8323
+ * One row per export job, inserted by `createExport` and updated as the
8324
+ * render progresses. The system NEVER deletes a row — it is the export AUDIT.
8325
+ * `deleteExport` and the janitor flip `state` and clear the file, keeping
8326
+ * the row. The record's `at` field maps to `createdAt`.
8507
8327
  *
8508
- * Returns how many rows were DELETED, and says so out loud when any were: a
8509
- * replace that silently drops rows is indistinguishable from one that never
8510
- * had them. Only an owner holding an authoritative set may call this — a
8511
- * prune driven by a fallible read is work destroyed (D130, D49).
8512
- */
8513
- async replaceAll(next) {
8514
- const held = await this.store.query.query({
8515
- collection: this.spec.collection,
8516
- filter: { limit: this.spec.loadLimit }
8517
- });
8518
- for (const [key, value] of next) await this.write(key, value);
8519
- const doomed = held.filter((record) => !next.has(record.id));
8520
- for (const record of doomed) await this.remove(record.id);
8521
- if (doomed.length > 0) this.logger.warn("row-map replaceAll dropped rows", { meta: {
8522
- collection: this.spec.collection,
8523
- dropped: doomed.length,
8524
- keys: doomed.slice(0, 20).map((r) => r.id),
8525
- remaining: next.size
8526
- } });
8527
- return doomed.length;
8528
- }
8328
+ * Indexed by `deviceId` to serve the per-device listing efficiently.
8329
+ */
8330
+ exports: {
8331
+ schema: require_dist.ExportRecordSchema,
8332
+ class: "ledger",
8333
+ writeTrigger: "one row per export job, inserted by createExport; rewritten by the render engine (progress / state patches) and the download hook",
8334
+ device: "none",
8335
+ at: "required",
8336
+ indexes: [{
8337
+ name: "device",
8338
+ fields: ["deviceId"]
8339
+ }],
8340
+ retention: { none: "operator intent — a row is the export AUDIT and survives forever; deleteExport and the janitor flip state and remove the file without removing the row; removeExport is a deliberate operator purge" },
8341
+ adoptsFrom: "recorder:exports"
8342
+ },
8529
8343
  /**
8530
- * The entries matching a projected-column predicate, in the order asked for.
8344
+ * A bounded ring of per-operation audit rows. One row per recordings
8345
+ * operation (prune, evict, relocate, export, …), appended best-effort by
8346
+ * `createRecordingOpsLogSink().append`. The ring is trimmed to the newest
8347
+ * `OPS_LOG_RING_DEFAULT_MAX` (500) rows after every append.
8531
8348
  *
8532
- * This is what projecting columns BUYS: "every export for camera 617, newest
8533
- * first, capped at 50" is an indexed `WHERE` + `ORDER BY` + `LIMIT`, not a
8534
- * parse of the whole map followed by a filter in JS.
8535
- *
8536
- * A `limit` is REQUIRED — `settings-store.query` silently caps an unbounded
8537
- * read at 2 000 rows and answers as if that were everything.
8538
- */
8539
- async readWhere(filter) {
8540
- const records = await this.store.query.query({
8541
- collection: this.spec.collection,
8542
- filter: {
8543
- ...filter.where !== void 0 ? { where: filter.where } : {},
8544
- ...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
8545
- ...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {},
8546
- ...filter.orderBy !== void 0 ? { orderBy: filter.orderBy } : {},
8547
- ...filter.offset !== void 0 ? { offset: filter.offset } : {},
8548
- limit: filter.limit ?? this.spec.loadLimit
8549
- }
8550
- });
8551
- const out = [];
8552
- let skipped = 0;
8553
- for (const record of records) {
8554
- const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
8555
- if (!parsed.success) {
8556
- skipped += 1;
8557
- continue;
8558
- }
8559
- out.push({
8560
- key: record.id,
8561
- value: parsed.data
8562
- });
8563
- }
8564
- if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
8565
- collection: this.spec.collection,
8566
- skipped,
8567
- kept: out.length
8568
- } });
8569
- return out;
8570
- }
8571
- /** How many rows match, without materialising one. */
8572
- async count(filter) {
8573
- return this.store.count.query({
8574
- collection: this.spec.collection,
8575
- ...filter !== void 0 ? { filter: {
8576
- ...filter.where !== void 0 ? { where: filter.where } : {},
8577
- ...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
8578
- ...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {}
8579
- } } : {}
8580
- });
8581
- }
8582
- /**
8583
- * Delete every row matching a predicate, in ONE statement. Returns how many
8584
- * went, and says so — a retention trim that silently drops rows is
8585
- * indistinguishable from one that had nothing to do.
8349
+ * Indexed by `deviceId` so a per-device listing is an indexed query, not a
8350
+ * full scan of the ring.
8586
8351
  *
8587
- * The predicate is REQUIRED and the engine REFUSES one that compiles to
8588
- * nothing: deleting a whole collection is a legitimate intent, but it must be
8589
- * asked for by name rather than reached through an empty object.
8590
- */
8591
- async deleteWhere(filter) {
8592
- const { deleted } = await this.store.deleteWhere.mutate({
8593
- collection: this.spec.collection,
8594
- filter
8595
- });
8596
- if (deleted > 0) this.logger.warn("row-map deleteWhere dropped rows", { meta: {
8597
- collection: this.spec.collection,
8598
- deleted,
8599
- filter
8600
- } });
8601
- return deleted;
8602
- }
8603
- /** `tags: { deviceId }` off the PROJECTED column, when the spec names one. */
8604
- deviceTags(row) {
8605
- const column = this.spec.deviceIdColumn;
8606
- if (column === void 0) return {};
8607
- const raw = row[column];
8608
- return typeof raw === "number" && Number.isInteger(raw) ? { tags: { deviceId: raw } } : {};
8352
+ * The record's `at` field maps to `OpsLogEntry.at`.
8353
+ */
8354
+ "ops-log": {
8355
+ schema: require_dist.OpsLogEntrySchema,
8356
+ class: "ledger",
8357
+ writeTrigger: "one row per recordings operation (prune, evict, relocate, export, …), appended best-effort by createRecordingOpsLogSink().append; the ring is trimmed to the newest 500 rows in application code after every write",
8358
+ device: "none",
8359
+ at: "required",
8360
+ indexes: [{
8361
+ name: "device",
8362
+ fields: ["deviceId"]
8363
+ }],
8364
+ retention: { none: "a bounded ring of the newest 500 rows; older rows are removed in application code (trimOpsLog) after each append — owner-pruned" },
8365
+ adoptsFrom: "recorder:ops-log"
8609
8366
  }
8610
- };
8611
- /** A store payload that is a plain object, or `null`. A type guard, not a cast. */
8612
- function isRecord(value) {
8613
- return value !== null && typeof value === "object" && !Array.isArray(value);
8614
- }
8367
+ });
8615
8368
  //#endregion
8616
8369
  //#region src/recorder/addon/export-store.ts
8617
8370
  /**
8618
8371
  * Recording-export persistence for the recorder addon.
8619
8372
  *
8620
- * ONE ROW PER EXPORT in `recorder:exports`, through {@link RowMapStore}. History
8621
- * rows are never removed by the system (audit) — `deleteExport` / the janitor
8622
- * flip `state` and clear the file, keeping the row.
8373
+ * ONE ROW PER EXPORT in the `recorder` / `exports` record kind, through a
8374
+ * {@link RecordKindHandle}. History rows are never removed by the system
8375
+ * (audit) — `deleteExport` / the janitor flip `state` and clear the file,
8376
+ * keeping the row.
8377
+ *
8378
+ * ## Records migration (phase 2, step 25)
8379
+ *
8380
+ * The store was previously backed by a {@link RowMapStore} over a SettingsStore
8381
+ * collection named `recorder:exports`. It is now backed by a
8382
+ * `RecordKindHandle<ExportRecord>` obtained from `ctx.records`. The record's
8383
+ * `at` field maps to `ExportRecord.createdAt`.
8384
+ *
8385
+ * The per-ID serialiser ({@link serialize}) is preserved: two overlapping
8386
+ * patches on the SAME export row are still read-modify-write, so they must
8387
+ * be ordered. The `RecordKindHandle` (an object) is used as the WeakMap key,
8388
+ * keeping the same compaction semantics.
8623
8389
  *
8624
- * ## What this replaced, and why it mattered here more than anywhere
8390
+ * ## What the RowMapStore replaced, and why it mattered
8625
8391
  *
8626
8392
  * Every job used to live in one key of the recorder's `addon-settings` / `root`
8627
8393
  * blob — 201 833 bytes on the reference hub, of which the export history was
@@ -8636,11 +8402,6 @@ function isRecord(value) {
8636
8402
  * erased, and the poll answered `unknown export "547ea182-…"`; the scheduler
8637
8403
  * read the vanished row as a failed render and re-queued forever. A per-row
8638
8404
  * upsert makes that class of loss unrepresentable — a write names ONE row.
8639
- *
8640
- * What is still needed is a per-ID serialiser: two patches to the SAME export
8641
- * still read-modify-write the same row. That is what {@link serialize} bounds,
8642
- * and it is now per (store, id) rather than per store, so two exports never
8643
- * queue behind each other.
8644
8405
  */
8645
8406
  /**
8646
8407
  * The storage class export MP4s live in: `artifacts`, SHARED with the
@@ -8685,76 +8446,26 @@ function exportOutPathFor(root, deviceId, exportId) {
8685
8446
  return node_path.default.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
8686
8447
  }
8687
8448
  /**
8688
- * @durable class=ledger owner=recorder
8689
- * write="one row per export job, inserted by `createExport` and rewritten by the
8690
- * render engine's progress/state patches and by the download hook"
8691
- * retention="the SYSTEM never deletes a row — it is the export AUDIT. `deleteExport`
8692
- * and the janitor flip `state` to 'deleted'/'expired' and remove the FILE, keeping
8693
- * the row. `removeExport` exists for an operator-driven purge and nothing calls it
8694
- * on a schedule."
8449
+ * Bound for the whole-history read. Explicit because a records query without
8450
+ * an explicit limit uses the default cap (100); an export list that silently
8451
+ * loses its oldest entries looks exactly like a purge.
8695
8452
  */
8696
- var RECORDING_EXPORTS_COLLECTION = "recorder:exports";
8453
+ var RECORDING_EXPORTS_LIMIT = 2e4;
8697
8454
  /**
8698
- * Bound for the whole-history read. Explicit because `settings-store.query`
8699
- * caps an unbounded read at 2 000 rows SILENTLY, and an export list that
8700
- * silently loses its oldest half looks exactly like a purge.
8455
+ * Obtain the `recorder` / `exports` record kind handle.
8456
+ * `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
8457
+ * write (called at boot in `onInitialize`).
8701
8458
  */
8702
- var RECORDING_EXPORTS_LIMIT = 2e4;
8703
- var RECORDING_EXPORTS_SPEC = {
8704
- collection: RECORDING_EXPORTS_COLLECTION,
8705
- schema: require_dist.ExportRecordSchema,
8706
- columns: [
8707
- {
8708
- name: "deviceId",
8709
- type: "INTEGER",
8710
- notNull: true
8711
- },
8712
- {
8713
- name: "createdAt",
8714
- type: "INTEGER",
8715
- notNull: true
8716
- },
8717
- {
8718
- name: "state",
8719
- type: "TEXT",
8720
- notNull: true
8721
- }
8722
- ],
8723
- indexes: [{
8724
- name: "idx_recorder_exports_device",
8725
- columns: ["deviceId"]
8726
- }, {
8727
- name: "idx_recorder_exports_created",
8728
- columns: ["createdAt"]
8729
- }],
8730
- project: (_key, value) => ({
8731
- deviceId: value.deviceId,
8732
- createdAt: value.createdAt,
8733
- state: value.state
8734
- }),
8735
- deviceIdColumn: "deviceId",
8736
- loadLimit: RECORDING_EXPORTS_LIMIT
8737
- };
8738
- /** Build the handle. `declare()` MUST run once at boot before any read/write. */
8739
- function recordingExportsState(store, logger) {
8740
- return new RowMapStore({
8741
- spec: RECORDING_EXPORTS_SPEC,
8742
- store,
8743
- logger
8744
- });
8459
+ function recordingExportsState(records) {
8460
+ return records.kind(recorderRecordKinds["exports"]);
8745
8461
  }
8746
8462
  /**
8747
8463
  * One in-flight mutation chain per (handle, export id).
8748
8464
  *
8749
- * A row upsert removed the cross-export clobber; it does not order two
8750
- * read-modify-writes of the SAME row. `patchExport` reads the row, merges and
8751
- * writes — so two overlapping patches on one export still race, and the loser's
8752
- * fields vanish. Serialising per ID keeps those pairs atomic while leaving
8753
- * unrelated exports fully concurrent (the old chain was per HANDLE, so a slow
8754
- * patch queued every other export behind it).
8755
- *
8756
- * A `WeakMap` keyed by the handle keeps a disposed store collectable; the inner
8757
- * map is keyed by export id.
8465
+ * Two overlapping patches on the SAME export still read-modify-write one row.
8466
+ * Serialising per id keeps them atomic while leaving unrelated exports fully
8467
+ * concurrent. A `WeakMap` keyed by the handle keeps a disposed handle
8468
+ * collectable; the inner map is keyed by export id.
8758
8469
  */
8759
8470
  var mutationChains = /* @__PURE__ */ new WeakMap();
8760
8471
  /** Run `work` after every mutation already queued for `(state, id)`, never before. */
@@ -8774,7 +8485,11 @@ async function serialize(state, id, work) {
8774
8485
  async function upsertExport(state, record) {
8775
8486
  const validated = require_dist.ExportRecordSchema.parse(record);
8776
8487
  return serialize(state, validated.id, async () => {
8777
- await state.write(validated.id, validated);
8488
+ await state.put({
8489
+ id: validated.id,
8490
+ at: validated.createdAt,
8491
+ data: validated
8492
+ });
8778
8493
  return validated;
8779
8494
  });
8780
8495
  }
@@ -8784,13 +8499,17 @@ async function upsertExport(state, record) {
8784
8499
  */
8785
8500
  async function patchExport(state, id, patch) {
8786
8501
  return serialize(state, id, async () => {
8787
- const existing = await state.read(id);
8788
- if (existing === null) return null;
8502
+ const row = await state.get(id);
8503
+ if (row === null) return null;
8789
8504
  const merged = require_dist.ExportRecordSchema.parse({
8790
- ...existing,
8505
+ ...row.data,
8791
8506
  ...patch
8792
8507
  });
8793
- await state.write(id, merged);
8508
+ await state.put({
8509
+ id,
8510
+ at: merged.createdAt,
8511
+ data: merged
8512
+ });
8794
8513
  return merged;
8795
8514
  });
8796
8515
  }
@@ -8801,17 +8520,21 @@ async function patchExport(state, id, patch) {
8801
8520
  */
8802
8521
  async function patchExportUnless(state, id, patch, refuse) {
8803
8522
  return serialize(state, id, async () => {
8804
- const existing = await state.read(id);
8805
- if (existing === null) return { kind: "missing" };
8806
- if (refuse(existing)) return {
8523
+ const row = await state.get(id);
8524
+ if (row === null) return { kind: "missing" };
8525
+ if (refuse(row.data)) return {
8807
8526
  kind: "refused",
8808
- record: existing
8527
+ record: row.data
8809
8528
  };
8810
8529
  const merged = require_dist.ExportRecordSchema.parse({
8811
- ...existing,
8530
+ ...row.data,
8812
8531
  ...patch
8813
8532
  });
8814
- await state.write(id, merged);
8533
+ await state.put({
8534
+ id,
8535
+ at: merged.createdAt,
8536
+ data: merged
8537
+ });
8815
8538
  return {
8816
8539
  kind: "patched",
8817
8540
  record: merged
@@ -8820,24 +8543,22 @@ async function patchExportUnless(state, id, patch, refuse) {
8820
8543
  }
8821
8544
  /** Read one export row by id — ONE keyed row read. */
8822
8545
  async function getExportRecord(state, id) {
8823
- return state.read(id);
8546
+ const row = await state.get(id);
8547
+ return row === null ? null : row.data;
8824
8548
  }
8825
8549
  /**
8826
- * List export rows, newest-first, optionally scoped to one device.
8550
+ * List export rows, newest-first (by `createdAt` / record `at`), optionally
8551
+ * scoped to one device.
8827
8552
  *
8828
- * Scoped to a device this is an INDEXED query on the projected `deviceId`
8829
- * column — not a parse of every export in the installation followed by a filter
8830
- * in JS.
8553
+ * Scoped to a device this is an INDEXED query on the `device` index — not a
8554
+ * parse of every export in the installation followed by a filter in JS.
8831
8555
  */
8832
8556
  async function listExportsFor(state, deviceId) {
8833
- return (await state.readWhere({
8557
+ return (await state.query({
8834
8558
  ...deviceId === void 0 ? {} : { where: { deviceId } },
8835
- orderBy: {
8836
- field: "createdAt",
8837
- direction: "desc"
8838
- },
8559
+ desc: true,
8839
8560
  limit: RECORDING_EXPORTS_LIMIT
8840
- })).map((entry) => entry.value);
8561
+ })).records.map((r) => r.data);
8841
8562
  }
8842
8563
  //#endregion
8843
8564
  //#region src/recorder/addon/export-timelapse-chunks.ts
@@ -11795,36 +11516,125 @@ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
11795
11516
  });
11796
11517
  }
11797
11518
  //#endregion
11519
+ //#region src/durable/durable-legacy-kind-handle.ts
11520
+ function notImpl(method) {
11521
+ throw new Error(`DurableLegacyKindHandle.${method}() is not available on the legacy (agent) path`);
11522
+ }
11523
+ function isRecord(value) {
11524
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11525
+ }
11526
+ function toRecordRow(id, data) {
11527
+ return {
11528
+ id,
11529
+ data,
11530
+ deviceId: null,
11531
+ at: null,
11532
+ expiresAt: null,
11533
+ rev: 0,
11534
+ updatedAt: 0
11535
+ };
11536
+ }
11537
+ var DurableLegacyKindHandle = class {
11538
+ name;
11539
+ spec;
11540
+ store;
11541
+ constructor(opts) {
11542
+ this.name = opts.spec.collection;
11543
+ this.spec = opts.spec;
11544
+ this.store = opts.store;
11545
+ }
11546
+ async get(id) {
11547
+ const row = (await this.store.query.query({
11548
+ collection: this.spec.collection,
11549
+ filter: {
11550
+ where: { [this.pkColumn()]: id },
11551
+ limit: 1
11552
+ }
11553
+ }))[0];
11554
+ if (row === void 0 || !isRecord(row.data)) return null;
11555
+ const parsed = this.spec.fromRecord(row.id, row.data);
11556
+ if (parsed === null) return null;
11557
+ return toRecordRow(row.id, this.spec.toValue(parsed));
11558
+ }
11559
+ async query(q) {
11560
+ const rows = await this.store.query.query({
11561
+ collection: this.spec.collection,
11562
+ filter: { limit: q.limit ?? 1e4 }
11563
+ });
11564
+ const records = [];
11565
+ for (const row of rows) {
11566
+ if (!isRecord(row.data)) continue;
11567
+ const parsed = this.spec.fromRecord(row.id, row.data);
11568
+ if (parsed === null) continue;
11569
+ records.push(toRecordRow(row.id, this.spec.toValue(parsed)));
11570
+ }
11571
+ return {
11572
+ records,
11573
+ nextCursor: null
11574
+ };
11575
+ }
11576
+ async put(input) {
11577
+ await this.store.set.mutate({
11578
+ collection: this.spec.collection,
11579
+ key: input.id,
11580
+ value: input.data
11581
+ });
11582
+ return { rev: 0 };
11583
+ }
11584
+ async delete(id) {
11585
+ await this.store.delete.mutate({
11586
+ collection: this.spec.collection,
11587
+ key: id
11588
+ });
11589
+ return true;
11590
+ }
11591
+ getMany = () => notImpl("getMany");
11592
+ getResolved = () => notImpl("getResolved");
11593
+ putMany = () => notImpl("putMany");
11594
+ patch = () => notImpl("patch");
11595
+ deleteMany = () => notImpl("deleteMany");
11596
+ count = () => notImpl("count");
11597
+ histogram = () => notImpl("histogram");
11598
+ aggregate = () => notImpl("aggregate");
11599
+ deleteWhere = () => notImpl("deleteWhere");
11600
+ allocateId = () => notImpl("allocateId");
11601
+ pkColumn() {
11602
+ return this.spec.columns.find((c) => c.primaryKey === true)?.name ?? "id";
11603
+ }
11604
+ };
11605
+ /** Legacy settings-store handle for one {@link DurableLedgerSpec}. */
11606
+ function durableLegacyKind(spec, store) {
11607
+ return new DurableLegacyKindHandle({
11608
+ spec,
11609
+ store
11610
+ });
11611
+ }
11612
+ /** Agent boot: declare the SQLite collection behind a durable ledger spec. */
11613
+ function declareDurableLegacyCollection(store, spec) {
11614
+ return store.declareCollection.mutate({
11615
+ collection: spec.collection,
11616
+ columns: [...spec.columns],
11617
+ ...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
11618
+ });
11619
+ }
11620
+ //#endregion
11798
11621
  //#region src/durable/durable-ledger.ts
11799
11622
  /** Default reseed cap — every current consumer's row set is installation-bounded. */
11800
11623
  var DEFAULT_LOAD_LIMIT = 1e5;
11801
- var DurableLedger = class DurableLedger {
11624
+ /** Page size for records reseed queries. */
11625
+ var QUERY_PAGE = 1e3;
11626
+ var DurableLedger = class {
11802
11627
  mirror = /* @__PURE__ */ new Map();
11803
11628
  /** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
11804
11629
  answered = false;
11805
11630
  spec;
11806
- store;
11631
+ kind;
11807
11632
  logger;
11808
11633
  constructor(deps) {
11809
11634
  this.spec = deps.spec;
11810
- this.store = deps.store;
11635
+ this.kind = deps.kind;
11811
11636
  this.logger = deps.logger;
11812
11637
  }
11813
- /**
11814
- * Register the collection. MUST run at boot, before any read or write: the
11815
- * SQLite backend answers 412 for an undeclared collection and takes the whole
11816
- * runner down with it (the addon-ai boot-crash lesson).
11817
- */
11818
- static declare(store, spec) {
11819
- return store.declareCollection.mutate({
11820
- collection: spec.collection,
11821
- columns: [...spec.columns],
11822
- ...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
11823
- });
11824
- }
11825
- declare() {
11826
- return DurableLedger.declare(this.store, this.spec);
11827
- }
11828
11638
  /** The collection this ledger owns — for the caller's own log lines. */
11829
11639
  get collection() {
11830
11640
  return this.spec.collection;
@@ -11841,31 +11651,43 @@ var DurableLedger = class DurableLedger {
11841
11651
  */
11842
11652
  async load() {
11843
11653
  try {
11844
- const records = await this.store.query.query({
11845
- collection: this.spec.collection,
11846
- filter: { limit: this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT }
11847
- });
11848
11654
  const next = /* @__PURE__ */ new Map();
11849
11655
  let skipped = 0;
11850
- for (const record of records) {
11851
- const row = this.spec.fromRecord(record.id, record.data);
11852
- if (row === null) {
11853
- skipped += 1;
11854
- continue;
11656
+ let seen = 0;
11657
+ const cap = this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT;
11658
+ let cursor;
11659
+ for (;;) {
11660
+ const limit = Math.min(QUERY_PAGE, cap - seen);
11661
+ if (limit <= 0) break;
11662
+ const page = await this.kind.query({
11663
+ limit,
11664
+ ...cursor === void 0 ? {} : { cursor }
11665
+ });
11666
+ seen += page.records.length;
11667
+ for (const record of page.records) {
11668
+ const row = this.spec.fromRecord(record.id, record.data);
11669
+ if (row === null) {
11670
+ skipped += 1;
11671
+ continue;
11672
+ }
11673
+ next.set(this.spec.keyOf(row), row);
11855
11674
  }
11856
- next.set(this.spec.keyOf(row), row);
11675
+ if (page.nextCursor === null) break;
11676
+ cursor = page.nextCursor;
11857
11677
  }
11858
11678
  this.mirror.clear();
11859
11679
  for (const [key, row] of next) this.mirror.set(key, row);
11860
11680
  this.answered = true;
11861
11681
  if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
11862
11682
  collection: this.spec.collection,
11683
+ kind: this.kind.name,
11863
11684
  skipped
11864
11685
  } });
11865
11686
  return [...this.mirror.values()];
11866
11687
  } catch (err) {
11867
11688
  this.logger.warn("durable load failed — keeping the state already in memory", { meta: {
11868
11689
  collection: this.spec.collection,
11690
+ kind: this.kind.name,
11869
11691
  error: String(err),
11870
11692
  held: this.mirror.size
11871
11693
  } });
@@ -11930,15 +11752,15 @@ var DurableLedger = class DurableLedger {
11930
11752
  */
11931
11753
  async put(row) {
11932
11754
  const key = this.spec.keyOf(row);
11755
+ const body = this.spec.toValue(row);
11933
11756
  if (this.spec.writeMode === "write-behind") {
11934
11757
  this.mirror.set(key, row);
11935
- await this.persist(row);
11758
+ await this.persistRow(key, body);
11936
11759
  return;
11937
11760
  }
11938
- await this.store.set.mutate({
11939
- collection: this.spec.collection,
11940
- key,
11941
- value: this.spec.toValue(row)
11761
+ await this.kind.put({
11762
+ id: key,
11763
+ data: body
11942
11764
  });
11943
11765
  this.mirror.set(key, row);
11944
11766
  }
@@ -11979,13 +11801,11 @@ var DurableLedger = class DurableLedger {
11979
11801
  async forget(key) {
11980
11802
  this.mirror.delete(key);
11981
11803
  try {
11982
- await this.store.delete.mutate({
11983
- collection: this.spec.collection,
11984
- key
11985
- });
11804
+ await this.kind.delete(key);
11986
11805
  } catch (err) {
11987
11806
  this.logger.debug("durable delete failed", { meta: {
11988
11807
  collection: this.spec.collection,
11808
+ kind: this.kind.name,
11989
11809
  key,
11990
11810
  error: String(err)
11991
11811
  } });
@@ -12021,15 +11841,13 @@ var DurableLedger = class DurableLedger {
12021
11841
  for (const key of [...this.mirror.keys()]) {
12022
11842
  if (activeKeys.has(key)) continue;
12023
11843
  try {
12024
- await this.store.delete.mutate({
12025
- collection: this.spec.collection,
12026
- key
12027
- });
11844
+ await this.kind.delete(key);
12028
11845
  this.mirror.delete(key);
12029
11846
  pruned += 1;
12030
11847
  } catch (err) {
12031
11848
  this.logger.debug("durable prune delete failed", { meta: {
12032
11849
  collection: this.spec.collection,
11850
+ kind: this.kind.name,
12033
11851
  key,
12034
11852
  error: String(err)
12035
11853
  } });
@@ -12042,25 +11860,34 @@ var DurableLedger = class DurableLedger {
12042
11860
  * decision path: the mirror already holds the truth for this process, and the
12043
11861
  * worst a lost write can do is one silent re-seed after the next restart.
12044
11862
  */
12045
- async persist(row) {
12046
- const deviceId = this.spec.deviceIdOf?.(row);
11863
+ persist(row) {
11864
+ const key = this.spec.keyOf(row);
11865
+ this.persistRow(key, this.spec.toValue(row));
11866
+ }
11867
+ async persistRow(key, body) {
11868
+ const deviceId = this.deviceIdFromBody(body);
12047
11869
  try {
12048
- await this.store.set.mutate({
12049
- collection: this.spec.collection,
12050
- key: this.spec.keyOf(row),
12051
- value: this.spec.toValue(row)
11870
+ await this.kind.put({
11871
+ id: key,
11872
+ data: body
12052
11873
  });
12053
11874
  } catch (err) {
12054
11875
  this.logger.warn("durable persist failed — this key may re-seed on boot", {
12055
11876
  ...deviceId !== void 0 ? { tags: { deviceId } } : {},
12056
11877
  meta: {
12057
11878
  collection: this.spec.collection,
12058
- key: this.spec.keyOf(row),
11879
+ kind: this.kind.name,
11880
+ key,
12059
11881
  error: String(err)
12060
11882
  }
12061
11883
  });
12062
11884
  }
12063
11885
  }
11886
+ deviceIdFromBody(body) {
11887
+ const raw = body["deviceId"];
11888
+ const n = typeof raw === "number" ? raw : Number(raw);
11889
+ return Number.isFinite(n) ? n : void 0;
11890
+ }
12064
11891
  };
12065
11892
  //#endregion
12066
11893
  //#region src/recorder/addon/segment-hour-ledger.ts
@@ -12621,15 +12448,15 @@ var SegmentHourLedger = class SegmentHourLedger {
12621
12448
  this.rowCap = deps.loadPacing?.rowCap ?? 25e4;
12622
12449
  this.ledger = new DurableLedger({
12623
12450
  spec: SPEC,
12624
- store: deps.store,
12451
+ kind: durableLegacyKind(SPEC, deps.store),
12625
12452
  logger: deps.logger
12626
12453
  });
12627
12454
  }
12628
12455
  static declare(store) {
12629
- return DurableLedger.declare(store, SPEC);
12456
+ return declareDurableLegacyCollection(store, SPEC);
12630
12457
  }
12631
12458
  declare() {
12632
- return this.ledger.declare();
12459
+ return declareDurableLegacyCollection(this.store, SPEC);
12633
12460
  }
12634
12461
  /**
12635
12462
  * Boot load — every hour row, in one query.
@@ -15040,63 +14867,61 @@ var LocationRefresher = class {
15040
14867
  }
15041
14868
  }
15042
14869
  };
15043
- var RECORDING_OPS_LOG_SPEC = {
15044
- collection: "recorder:ops-log",
15045
- schema: require_dist.OpsLogEntrySchema,
15046
- columns: [{
15047
- name: "at",
15048
- type: "INTEGER",
15049
- notNull: true
15050
- }, {
15051
- name: "deviceId",
15052
- type: "INTEGER"
15053
- }],
15054
- indexes: [{
15055
- name: "idx_recorder_ops_log_at",
15056
- columns: ["at"]
15057
- }, {
15058
- name: "idx_recorder_ops_log_device",
15059
- columns: ["deviceId"]
15060
- }],
15061
- project: (_key, value) => ({
15062
- at: value.at,
15063
- deviceId: value.deviceId
15064
- }),
15065
- deviceIdColumn: "deviceId",
15066
- loadLimit: 2e4
15067
- };
15068
- /** Build the handle. `declare()` MUST run once at boot before any read/write. */
15069
- function recordingOpsLogState(store, logger) {
15070
- return new RowMapStore({
15071
- spec: RECORDING_OPS_LOG_SPEC,
15072
- store,
15073
- logger
15074
- });
14870
+ //#endregion
14871
+ //#region src/recorder/addon/ops-log-store.ts
14872
+ /**
14873
+ * Recordings ops-log persistence for the recorder addon.
14874
+ *
14875
+ * ONE ROW PER OPERATION in the `recorder` / `ops-log` record kind, through a
14876
+ * {@link RecordKindHandle}. The ring is bounded — `maxEntries` newest rows
14877
+ * survive — trimmed with one `deleteWhere({ atBefore })` per append.
14878
+ *
14879
+ * ## Records migration (phase 2, step 26)
14880
+ *
14881
+ * The store was previously backed by a {@link RowMapStore} over a SettingsStore
14882
+ * collection named `recorder:ops-log`. It is now backed by a
14883
+ * `RecordKindHandle<OpsLogEntry>` obtained from `ctx.records`. The record's
14884
+ * `at` field maps to `OpsLogEntry.at`.
14885
+ *
14886
+ * ## What the RowMapStore replaced
14887
+ *
14888
+ * The ring lived in ONE key of the recorder's `addon-settings` / `root` blob,
14889
+ * as a flat JSON array. On the reference hub it was 147 437 bytes of that row's
14890
+ * 201 833 — so appending one audit line read, parsed, re-serialised and rewrote
14891
+ * ~200 KB, and so did every unrelated reader of that row. The ops log is the
14892
+ * CHEAPEST thing in the recorder and it was paying for the most expensive.
14893
+ *
14894
+ * Writes stay BEST-EFFORT: `createRecordingOpsLogSink().append` never throws —
14895
+ * a failed audit write must not fail the operation it records.
14896
+ */
14897
+ /**
14898
+ * Obtain the `recorder` / `ops-log` record kind handle.
14899
+ * `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
14900
+ * write (called at boot in `onInitialize`).
14901
+ */
14902
+ function recordingOpsLogState(records) {
14903
+ return records.kind(recorderRecordKinds["ops-log"]);
15075
14904
  }
15076
14905
  /**
15077
14906
  * Trim the ring to the newest `maxEntries` rows. Returns how many went.
15078
14907
  *
15079
14908
  * The cursor is the OLDEST row that must SURVIVE — offset `maxEntries - 1`
15080
- * newest-first — and the delete is strictly older than it (`[0, at - 1]`).
14909
+ * newest-first — and the delete removes everything strictly BEFORE its `at`
14910
+ * (`atBefore: cursor.at` deletes rows where `at < cursor.at`).
15081
14911
  *
15082
- * Strictly, because `whereBetween` is inclusive and the filter surface has no
15083
- * `<`: including the cursor's own millisecond would take every row tied on it,
15084
- * and a ring whose rows all shared one millisecond would be deleted whole.
15085
- * Erring this way keeps a few EXTRA audit rows on a tie and never fewer than
15086
- * the cap.
14912
+ * Strictly, to avoid dropping the cursor row itself and any rows tied on its
14913
+ * millisecond. The ring may hold a few MORE than the cap on a millisecond tie
14914
+ * and never FEWER.
15087
14915
  */
15088
14916
  async function trimOpsLog(state, maxEntries = 500) {
15089
- if (await state.count() <= maxEntries) return 0;
15090
- const oldestKept = (await state.readWhere({
15091
- orderBy: {
15092
- field: "at",
15093
- direction: "desc"
15094
- },
15095
- limit: 1,
15096
- offset: maxEntries - 1
15097
- }))[0];
15098
- if (oldestKept === void 0) return 0;
15099
- return state.deleteWhere({ whereBetween: { at: [0, oldestKept.value.at - 1] } });
14917
+ if (await state.count({}) <= maxEntries) return 0;
14918
+ const page = await state.query({
14919
+ desc: true,
14920
+ limit: maxEntries
14921
+ });
14922
+ const oldest = page.records[page.records.length - 1];
14923
+ if (oldest === void 0) return 0;
14924
+ return state.deleteWhere({ atBefore: oldest.at ?? 0 });
15100
14925
  }
15101
14926
  /**
15102
14927
  * Append one row and persist, then trim the ring. Validates at the boundary.
@@ -15104,7 +14929,11 @@ async function trimOpsLog(state, maxEntries = 500) {
15104
14929
  */
15105
14930
  async function appendOpsLog(state, entry, maxEntries = 500) {
15106
14931
  const validated = require_dist.OpsLogEntrySchema.parse(entry);
15107
- await state.write(validated.id, validated);
14932
+ await state.put({
14933
+ id: validated.id,
14934
+ at: validated.at,
14935
+ data: validated
14936
+ });
15108
14937
  await trimOpsLog(state, maxEntries);
15109
14938
  return validated;
15110
14939
  }
@@ -15112,18 +14941,15 @@ async function appendOpsLog(state, entry, maxEntries = 500) {
15112
14941
  * List ops-log rows newest-first, optionally scoped to one device, capped at
15113
14942
  * `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
15114
14943
  *
15115
- * Scoped to a device this is an INDEXED query, not a read of the whole ring
15116
- * followed by a filter and a sort in JS.
14944
+ * Scoped to a device this is an INDEXED query on the `device` index — not a
14945
+ * read of the whole ring followed by a filter and a sort in JS.
15117
14946
  */
15118
14947
  async function listOpsLog(state, query) {
15119
- return (await state.readWhere({
14948
+ return (await state.query({
15120
14949
  ...query.deviceId === void 0 ? {} : { where: { deviceId: query.deviceId } },
15121
- orderBy: {
15122
- field: "at",
15123
- direction: "desc"
15124
- },
14950
+ desc: true,
15125
14951
  limit: query.limit ?? 200
15126
- })).map((entry) => entry.value);
14952
+ })).records.map((r) => r.data);
15127
14953
  }
15128
14954
  /**
15129
14955
  * Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
@@ -23581,7 +23407,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
23581
23407
  readCensus = new StoreReadCensus();
23582
23408
  /** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
23583
23409
  * call shares ONE census instead of each wrapping the raw client afresh. */
23584
- censusedSettingsStoreClient = null;
23585
23410
  constructor() {
23586
23411
  super({ ...DEFAULT_CONFIG });
23587
23412
  }
@@ -23618,6 +23443,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
23618
23443
  } });
23619
23444
  return { providers: [] };
23620
23445
  }
23446
+ const records = this.ctx.records;
23447
+ if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
23448
+ await records.declare(recorderRecordKinds);
23621
23449
  await this.declareRowCollections();
23622
23450
  this.segmentHours = new SegmentHourLedger({
23623
23451
  store: this.ctx.api.settingsStore,
@@ -25438,26 +25266,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
25438
25266
  } });
25439
25267
  }
25440
25268
  /**
25441
- * Declare the three row collections that replaced the `recordingConfigs`,
25442
- * `recordingExports` and `recordingOpsLog` blob keys. Idempotent — a
25443
- * re-declaration of the same shape is a no-op in the engine.
25269
+ * Declare the remaining RowMapStore collection that replaced the
25270
+ * `recordingConfigs` blob key. Idempotent — a re-declaration of the same
25271
+ * shape is a no-op in the engine.
25272
+ *
25273
+ * `recorder:exports` and `recorder:ops-log` are now record kinds declared
25274
+ * via `ctx.records.declare(recorderRecordKinds)` at the top of
25275
+ * `onInitialize`.
25444
25276
  */
25445
25277
  async declareRowCollections() {
25446
25278
  await this.configStore().declare();
25447
- await this.exportsStore().declare();
25448
- await this.opsLogStore().declare();
25449
- }
25450
- /**
25451
- * `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
25452
- *
25453
- * LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
25454
- * runs, so this cannot be a field initialiser. The wrap is permanent and
25455
- * cheap when disarmed (one `get` trap and a comparison) — what is armed and
25456
- * disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
25457
- */
25458
- censusedSettingsStore() {
25459
- this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
25460
- return this.censusedSettingsStoreClient;
25461
25279
  }
25462
25280
  configsState = null;
25463
25281
  /** A camera's recording config — its per-device settings (D865), migrated on read. */
@@ -25470,14 +25288,23 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
25470
25288
  });
25471
25289
  return this.configsState;
25472
25290
  }
25473
- /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
25474
- * wrapper as the config store). */
25291
+ /** Record kind handle over the `recorder` / `exports` kind (hub recording node only). */
25475
25292
  exportsStore() {
25476
- return recordingExportsState(this.censusedSettingsStore(), this.ctx.logger);
25293
+ return recordingExportsState(this.requireRecords());
25477
25294
  }
25478
- /** Durable handle over the bounded recordings ops-log ring. */
25295
+ /** Record kind handle over the `recorder` / `ops-log` kind (hub recording node only). */
25479
25296
  opsLogStore() {
25480
- return recordingOpsLogState(this.censusedSettingsStore(), this.ctx.logger);
25297
+ return recordingOpsLogState(this.requireRecords());
25298
+ }
25299
+ /**
25300
+ * Returns `ctx.records`, throwing clearly when it is absent.
25301
+ * Only called on the designated recording node (hub by default); an agent
25302
+ * exits before `onInitialize` reaches any store call.
25303
+ */
25304
+ requireRecords() {
25305
+ const records = this.ctx.records;
25306
+ if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
25307
+ return records;
25481
25308
  }
25482
25309
  /** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
25483
25310
  placementStore() {