@camstack/addon-pipeline 1.2.257 → 1.2.259

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 (33) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-BTzxxm-z.mjs → default-detection-model-BZ54sUY2.mjs} +1 -1
  4. package/dist/{default-detection-model-C8CHpxQl.js → default-detection-model-vVWcdv0g.js} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-BMARLLlX.js → dist-CsJJPDQW.js} +154 -1
  8. package/dist/{dist-xsaGDCOu.mjs → dist-D3w2fwD4.mjs} +154 -1
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-Ca-vE0xX.js → node-1vbEWzPZ.js} +1 -1
  12. package/dist/{node-Fv4ByT36.mjs → node-IHGNrWH9.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +4 -4
  15. package/dist/{process-memory-C3Wjmvm0.js → process-memory-DynU4177.js} +1 -1
  16. package/dist/{process-memory-BgtQADAY.mjs → process-memory-jhOBJEil.mjs} +1 -1
  17. package/dist/recorder/index.js +293 -19
  18. package/dist/recorder/index.mjs +293 -19
  19. package/dist/{restream-intent-D2VT97-U.mjs → restream-intent-CW79dvFV.mjs} +118 -2
  20. package/dist/{restream-intent-VVJiUAkh.js → restream-intent-iKGCB9IR.js} +135 -1
  21. package/dist/{segment-demux-js-C7C6c-g2.mjs → segment-demux-js-BZAiu_2h.mjs} +1 -1
  22. package/dist/{segment-demux-js-DHV7EOQf.js → segment-demux-js-OQT6wqXH.js} +1 -1
  23. package/dist/stream-broker/_stub.js +1 -1
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CJWLoj3Z.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CFcO9OoY.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-CTHexfai.mjs +26 -0
  26. package/dist/stream-broker/demux-worker-child.js +1 -1
  27. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  28. package/dist/stream-broker/{hostInit-BzBcvawL.mjs → hostInit-rU23F6nl.mjs} +3 -3
  29. package/dist/stream-broker/index.js +476 -17
  30. package/dist/stream-broker/index.mjs +476 -17
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/package.json +1 -1
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ReCGmeOG.mjs +0 -26
@@ -3,10 +3,10 @@ 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-BMARLLlX.js");
7
- const require_node = require("../node-Ca-vE0xX.js");
6
+ const require_dist = require("../dist-CsJJPDQW.js");
7
+ const require_node = require("../node-1vbEWzPZ.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
- const require_restream_intent = require("../restream-intent-VVJiUAkh.js");
9
+ const require_restream_intent = require("../restream-intent-iKGCB9IR.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
11
  let node_crypto = require("node:crypto");
12
12
  let node_fs = require("node:fs");
@@ -820,14 +820,22 @@ function parseSegmentPath(relPath) {
820
820
  /** Hour bucket, the granularity the path convention (`.../YYYY/MM/DD/HH/`)
821
821
  * already gives us — so "have I walked this?" costs one Set lookup. */
822
822
  var HOUR_MS$9 = 36e5;
823
- /** A device walked end-to-end. Cheaper and clearer than inserting every hour of
824
- * the retention window into the set. */
825
- var ALL_HOURS = Symbol("all-hours");
826
823
  /** The hour buckets a half-open span touches. */
827
824
  function* hourBuckets(fromMs, toMs) {
828
825
  const first = Math.floor(fromMs / HOUR_MS$9) * HOUR_MS$9;
829
826
  for (let h = first; h < toMs; h += HOUR_MS$9) yield h;
830
827
  }
828
+ /** The hour bucket a segment belongs to — the same one its path encodes. */
829
+ function hourOf$1(startMs) {
830
+ return Math.floor(startMs / HOUR_MS$9) * HOUR_MS$9;
831
+ }
832
+ /** The production bound. See {@link RecordingIndexBudget} for the measurement. */
833
+ var DEFAULT_RECORDING_INDEX_BUDGET = {
834
+ residentRowBudget: 25e4,
835
+ residentHourBudget: 2e4,
836
+ evictToFraction: .8,
837
+ pinnedTailHours: 2
838
+ };
831
839
  /**
832
840
  * Index of the first element whose `startMs` is GREATER than `startMs` (upper
833
841
  * bound). Inserting there keeps equal-start rows in arrival order — exactly
@@ -875,10 +883,35 @@ var RecordingIndex = class {
875
883
  /** See {@link HydrateReuseStats}. */
876
884
  rowsReused = 0;
877
885
  rowsAllocated = 0;
878
- /** Hour buckets this device has been walked for; `ALL_HOURS` after a full
879
- * hydrate. Empty = nothing has been looked at, which is NOT the same as
880
- * "there is nothing" — see {@link hydrationOf}. */
886
+ /**
887
+ * Hour buckets this device has been walked for → when anything last ASKED
888
+ * about that hour ({@link useTick}, 0 = never since the walk). Empty =
889
+ * nothing has been looked at, which is NOT the same as "there is nothing" —
890
+ * see {@link hydrationOf}.
891
+ *
892
+ * The tick is the eviction order, and it lives here rather than in a side
893
+ * map because an evicted hour must lose its marker in the SAME operation
894
+ * that loses its rows: a walked marker outliving its rows would report
895
+ * `hydrated` over an hour the index no longer holds, which is the one
896
+ * failure a forgetting index must not have.
897
+ *
898
+ * There used to be an `ALL_HOURS` sentinel here for a device walked
899
+ * end-to-end. It had no production caller (2026-08-30 census) and a bounded
900
+ * index can never truthfully claim it, so it is gone.
901
+ */
881
902
  walked = /* @__PURE__ */ new Map();
903
+ /** Monotonic "somebody asked" counter. Only ever compared, never read out. */
904
+ useTick = 0;
905
+ /** `null` until {@link armResidency}. An unarmed index never evicts. */
906
+ residencyDeps = null;
907
+ evictedRows = 0;
908
+ evictedHours = 0;
909
+ evictionPasses = 0;
910
+ /** Hours dropped per device, for the life of the process. One number per
911
+ * camera — the input to {@link droppedDuringWalk}, nothing else. */
912
+ evictedHoursByDevice = /* @__PURE__ */ new Map();
913
+ /** {@link evictedHoursByDevice} as it stood when a whole-device walk began. */
914
+ walkBaseline = /* @__PURE__ */ new Map();
882
915
  /**
883
916
  * Upper bound on the `durMs` of any row the index holds — the ONLY thing that
884
917
  * turns "walk the whole sorted view" into "walk backwards a bounded number of
@@ -1052,7 +1085,9 @@ var RecordingIndex = class {
1052
1085
  * Deliberately NOT delete-all-then-reinsert: the archive walk re-reports rows
1053
1086
  * this index already holds, with freshly built path strings, and dropping the
1054
1087
  * held rows first would make those fresh strings the permanent ones — a
1055
- * second copy of the whole archive's paths. See {@link retainedPathStrings}.
1088
+ * second copy of the whole archive's paths. See {@link RecordingIndex.rowFor}
1089
+ * for the measurement (`retainedPathStrings`, which this used to point at,
1090
+ * never existed — a leftover that read as a verification, removed 2026-09-17).
1056
1091
  * @residency mutation
1057
1092
  */
1058
1093
  hydrateDevice(deviceId, locationId, relPaths) {
@@ -1066,6 +1101,7 @@ var RecordingIndex = class {
1066
1101
  }
1067
1102
  for (const [path, existing] of m) if (existing.locationId === locationId && !seen.has(path)) m.delete(path);
1068
1103
  this.invalidateSorted(deviceId);
1104
+ this.enforceResidency();
1069
1105
  }
1070
1106
  /**
1071
1107
  * Whether the index has WALKED `[fromMs, toMs)` for a device.
@@ -1079,7 +1115,6 @@ var RecordingIndex = class {
1079
1115
  hydrationOf(deviceId, fromMs, toMs) {
1080
1116
  const w = this.walked.get(deviceId);
1081
1117
  if (w === void 0) return "unknown";
1082
- if (w === ALL_HOURS) return "hydrated";
1083
1118
  let seen = 0;
1084
1119
  let total = 0;
1085
1120
  for (const h of hourBuckets(fromMs, toMs)) {
@@ -1091,15 +1126,225 @@ var RecordingIndex = class {
1091
1126
  }
1092
1127
  /** Record that `[fromMs, toMs)` has been walked, whatever it contained. @residency mutation */
1093
1128
  markHydrated(deviceId, fromMs, toMs) {
1129
+ let w = this.walked.get(deviceId);
1130
+ if (w === void 0) {
1131
+ w = /* @__PURE__ */ new Map();
1132
+ this.walked.set(deviceId, w);
1133
+ }
1134
+ const tick = ++this.useTick;
1135
+ for (const h of hourBuckets(fromMs, toMs)) w.set(h, tick);
1136
+ }
1137
+ /**
1138
+ * Bound this index, and give it somewhere to SAY what it dropped.
1139
+ *
1140
+ * Called once, from the recorder addon's initialize — `ctx.logger` does not
1141
+ * exist when the field is constructed. Until it runs the index never evicts,
1142
+ * which is honest for the pre-init window (it holds nothing) and is the one
1143
+ * state in which {@link ResidencyCensus.budget} is `null`.
1144
+ * @residency mutation
1145
+ */
1146
+ armResidency(deps) {
1147
+ this.residencyDeps = deps;
1148
+ this.enforceResidency();
1149
+ }
1150
+ /**
1151
+ * A whole-device walk is starting: remember how much this index had already
1152
+ * forgotten about that camera, so {@link droppedDuringWalk} can say whether
1153
+ * the walk's result is a COMPLETE view of what it read.
1154
+ * @residency mutation
1155
+ */
1156
+ beginWalk(deviceId) {
1157
+ this.walkBaseline.set(deviceId, this.evictedHoursByDevice.get(deviceId) ?? 0);
1158
+ }
1159
+ /**
1160
+ * Did this index drop anything for `deviceId` since {@link beginWalk}?
1161
+ *
1162
+ * The one question a PRUNE has to ask. An hour the index dropped is
1163
+ * indistinguishable, from the outside, from an hour disk no longer has — so
1164
+ * a consumer that deletes durable state on the index's silence must refuse
1165
+ * when the answer is `true`. `false` when nobody called `beginWalk`: an
1166
+ * unarmed index never drops, and a caller that never announced a walk is
1167
+ * asking about one that did not happen.
1168
+ * @residency resident-scoped
1169
+ */
1170
+ droppedDuringWalk(deviceId) {
1171
+ const baseline = this.walkBaseline.get(deviceId);
1172
+ if (baseline === void 0) return false;
1173
+ return (this.evictedHoursByDevice.get(deviceId) ?? 0) > baseline;
1174
+ }
1175
+ /** What is held, what was let go, and under which bound. @residency resident-scoped */
1176
+ residency() {
1177
+ return {
1178
+ residentRows: this.residentRowCount(),
1179
+ residentHours: this.residentHourCount(),
1180
+ evictedRows: this.evictedRows,
1181
+ evictedHours: this.evictedHours,
1182
+ evictionPasses: this.evictionPasses,
1183
+ budget: this.residencyDeps?.budget ?? null
1184
+ };
1185
+ }
1186
+ residentRowCount() {
1187
+ let n = 0;
1188
+ for (const m of this.byDevice.values()) n += m.size;
1189
+ return n;
1190
+ }
1191
+ residentHourCount() {
1192
+ let n = 0;
1193
+ for (const w of this.walked.values()) n += w.size;
1194
+ return n;
1195
+ }
1196
+ /**
1197
+ * Note that something ASKED about `[fromMs, toMs)` — the only input to the
1198
+ * eviction order.
1199
+ *
1200
+ * Walks whichever side is smaller: a read whose span is wider than the
1201
+ * device's whole walked map (a month-wide `ranges`, an availability sweep)
1202
+ * would otherwise iterate one hour at a time across the span.
1203
+ */
1204
+ touch(deviceId, fromMs, toMs) {
1094
1205
  const w = this.walked.get(deviceId);
1095
- if (w === ALL_HOURS) return;
1096
- const set = w ?? /* @__PURE__ */ new Set();
1097
- for (const h of hourBuckets(fromMs, toMs)) set.add(h);
1098
- this.walked.set(deviceId, set);
1206
+ if (w === void 0 || w.size === 0) return;
1207
+ const tick = ++this.useTick;
1208
+ if (Math.ceil(Math.max(0, toMs - fromMs) / HOUR_MS$9) > w.size) {
1209
+ for (const h of w.keys()) if (h < toMs && h + HOUR_MS$9 > fromMs) w.set(h, tick);
1210
+ return;
1211
+ }
1212
+ for (const h of hourBuckets(fromMs, toMs)) if (w.has(h)) w.set(h, tick);
1099
1213
  }
1100
- /** Record that the device was walked end-to-end (the background full walk). @residency mutation */
1101
- markHydratedAll(deviceId) {
1102
- this.walked.set(deviceId, ALL_HOURS);
1214
+ /**
1215
+ * Drop the least-recently-asked-about hours until the index is back inside
1216
+ * its budget.
1217
+ *
1218
+ * ## What a dropped hour costs, and why that is safe
1219
+ *
1220
+ * The directory is the authority for which segments exist — every row here
1221
+ * is derived from a path the walk read, and `hydrateHour` /
1222
+ * `hydrateWindowFromStorage` rebuild it from one shallow `readdir` (or one
1223
+ * ledger point read). So a drop costs a re-read, never a segment. The
1224
+ * property that makes it costs-a-read rather than loses-a-segment is that
1225
+ * the walked marker goes with the rows: `hydrationOf` then answers `unknown`
1226
+ * and the reader walks. A marker left behind would answer `hydrated` over an
1227
+ * hour holding nothing, which is exactly the silent gap this class spent
1228
+ * D295 learning not to report.
1229
+ *
1230
+ * ## What is never a candidate
1231
+ *
1232
+ * The live tail ({@link RecordingIndexBudget.pinnedTailHours}). Those hours
1233
+ * are write-through from the writer (`addSegment`), not walked, and a
1234
+ * `readdir` of an hour a writer is still filling cannot give back what it
1235
+ * had — D445's lost tail, from the other direction.
1236
+ */
1237
+ enforceResidency() {
1238
+ const deps = this.residencyDeps;
1239
+ if (deps === null) return;
1240
+ const { budget } = deps;
1241
+ let rows = this.residentRowCount();
1242
+ let hours = this.residentHourCount();
1243
+ if (rows <= budget.residentRowBudget && hours <= budget.residentHourBudget) return;
1244
+ const rowTarget = Math.floor(budget.residentRowBudget * budget.evictToFraction);
1245
+ const hourTarget = Math.floor(budget.residentHourBudget * budget.evictToFraction);
1246
+ const pinnedFrom = hourOf$1(deps.now()) - Math.max(0, budget.pinnedTailHours - 1) * HOUR_MS$9;
1247
+ const counts = this.hourHistogram();
1248
+ const candidates = [];
1249
+ for (const [deviceId, byHour] of counts) {
1250
+ const walkedHours = this.walked.get(deviceId);
1251
+ for (const [hourStartMs, count] of byHour) {
1252
+ if (hourStartMs >= pinnedFrom) continue;
1253
+ candidates.push({
1254
+ deviceId,
1255
+ hourStartMs,
1256
+ rows: count,
1257
+ lastUse: walkedHours?.get(hourStartMs) ?? 0
1258
+ });
1259
+ }
1260
+ }
1261
+ candidates.sort((a, b) => a.lastUse - b.lastUse || a.hourStartMs - b.hourStartMs);
1262
+ const victims = /* @__PURE__ */ new Map();
1263
+ let droppedRows = 0;
1264
+ let droppedHours = 0;
1265
+ for (const c of candidates) {
1266
+ if (rows <= rowTarget && hours <= hourTarget) break;
1267
+ let forDevice = victims.get(c.deviceId);
1268
+ if (!forDevice) {
1269
+ forDevice = /* @__PURE__ */ new Set();
1270
+ victims.set(c.deviceId, forDevice);
1271
+ }
1272
+ forDevice.add(c.hourStartMs);
1273
+ rows -= c.rows;
1274
+ droppedRows += c.rows;
1275
+ if (this.walked.get(c.deviceId)?.has(c.hourStartMs) === true) {
1276
+ hours -= 1;
1277
+ droppedHours += 1;
1278
+ }
1279
+ }
1280
+ if (victims.size === 0) return;
1281
+ this.evictionPasses += 1;
1282
+ this.evictedRows += droppedRows;
1283
+ this.evictedHours += droppedHours;
1284
+ for (const [deviceId, hoursToDrop] of victims) this.dropHours(deviceId, hoursToDrop, rows, budget, deps.logger);
1285
+ }
1286
+ /** `deviceId → hourStartMs → resident rows`, including hours walked and found
1287
+ * empty (count 0) — those hold no rows and would otherwise never be dropped,
1288
+ * leaving the walked map to grow without bound on its own. */
1289
+ hourHistogram() {
1290
+ const out = /* @__PURE__ */ new Map();
1291
+ for (const [deviceId, m] of this.byDevice) {
1292
+ const byHour = /* @__PURE__ */ new Map();
1293
+ for (const s of m.values()) {
1294
+ const h = hourOf$1(s.startMs);
1295
+ byHour.set(h, (byHour.get(h) ?? 0) + 1);
1296
+ }
1297
+ out.set(deviceId, byHour);
1298
+ }
1299
+ for (const [deviceId, w] of this.walked) {
1300
+ let byHour = out.get(deviceId);
1301
+ if (!byHour) {
1302
+ byHour = /* @__PURE__ */ new Map();
1303
+ out.set(deviceId, byHour);
1304
+ }
1305
+ for (const h of w.keys()) if (!byHour.has(h)) byHour.set(h, 0);
1306
+ }
1307
+ return out;
1308
+ }
1309
+ /** Delete one device's victim hours — rows and walked markers together — and
1310
+ * say so. A structure that silently forgets is how this class of bug hides. */
1311
+ dropHours(deviceId, hoursToDrop, residentRowsAfter, budget, logger) {
1312
+ const m = this.byDevice.get(deviceId);
1313
+ let droppedRows = 0;
1314
+ if (m) {
1315
+ for (const [path, s] of m) {
1316
+ if (!hoursToDrop.has(hourOf$1(s.startMs))) continue;
1317
+ m.delete(path);
1318
+ droppedRows += 1;
1319
+ }
1320
+ if (m.size === 0) this.byDevice.delete(deviceId);
1321
+ }
1322
+ const w = this.walked.get(deviceId);
1323
+ if (w) {
1324
+ for (const h of hoursToDrop) w.delete(h);
1325
+ if (w.size === 0) this.walked.delete(deviceId);
1326
+ }
1327
+ this.invalidateSorted(deviceId);
1328
+ this.evictedHoursByDevice.set(deviceId, (this.evictedHoursByDevice.get(deviceId) ?? 0) + hoursToDrop.size);
1329
+ let oldestHourMs = Infinity;
1330
+ let newestHourMs = -Infinity;
1331
+ for (const h of hoursToDrop) {
1332
+ if (h < oldestHourMs) oldestHourMs = h;
1333
+ if (h > newestHourMs) newestHourMs = h;
1334
+ }
1335
+ logger.info("recorder: index dropped resident hours — over the residency budget", {
1336
+ tags: { deviceId },
1337
+ meta: {
1338
+ hours: hoursToDrop.size,
1339
+ rows: droppedRows,
1340
+ oldestHourMs,
1341
+ newestHourMs,
1342
+ residentRowsAfter,
1343
+ rowBudget: budget.residentRowBudget,
1344
+ hourBudget: budget.residentHourBudget,
1345
+ costOfRecovery: "readdir-per-hour"
1346
+ }
1347
+ });
1103
1348
  }
1104
1349
  /**
1105
1350
  * Merge one hour's segments for a location, and mark that hour walked.
@@ -1121,6 +1366,7 @@ var RecordingIndex = class {
1121
1366
  }
1122
1367
  if (changed) this.invalidateSorted(deviceId);
1123
1368
  this.markHydrated(deviceId, hourStartMs, hourStartMs + HOUR_MS$9);
1369
+ this.enforceResidency();
1124
1370
  }
1125
1371
  /**
1126
1372
  * Make one hour of ONE location exactly `relPaths` — merge what is there and
@@ -1151,6 +1397,7 @@ var RecordingIndex = class {
1151
1397
  for (const path of dropped) m.delete(path);
1152
1398
  if (dropped.length > 0) this.invalidateSorted(deviceId);
1153
1399
  this.hydrateHour(deviceId, locationId, hourStartMs, relPaths);
1400
+ this.enforceResidency();
1154
1401
  return dropped;
1155
1402
  }
1156
1403
  /**
@@ -1175,6 +1422,7 @@ var RecordingIndex = class {
1175
1422
  return;
1176
1423
  }
1177
1424
  this.insertIntoSortedViews(s);
1425
+ this.enforceResidency();
1178
1426
  }
1179
1427
  /** Remove segments by relative path, routing each path to the correct device map via its encoded deviceId. @residency mutation */
1180
1428
  removeSegments(paths) {
@@ -1202,6 +1450,7 @@ var RecordingIndex = class {
1202
1450
  * @residency resident-scoped
1203
1451
  */
1204
1452
  segmentsStartingIn(deviceId, profile, fromMs, toMs) {
1453
+ this.touch(deviceId, fromMs, toMs);
1205
1454
  const segs = this.sortedSegments(deviceId, profile);
1206
1455
  let lo = 0;
1207
1456
  let hi = segs.length;
@@ -1230,6 +1479,7 @@ var RecordingIndex = class {
1230
1479
  * @residency resident-scoped
1231
1480
  */
1232
1481
  segmentStartingAt(deviceId, profile, startMs) {
1482
+ this.touch(deviceId, startMs, startMs + 1);
1233
1483
  const segs = this.sortedSegments(deviceId, profile);
1234
1484
  const at = segs[lowerBoundByStart(segs, startMs)];
1235
1485
  return at !== void 0 && at.startMs === startMs ? at : null;
@@ -1253,6 +1503,7 @@ var RecordingIndex = class {
1253
1503
  * @residency resident-scoped
1254
1504
  */
1255
1505
  segmentsOverlapping(deviceId, profile, fromMs, toMs) {
1506
+ this.touch(deviceId, fromMs - this.maxDurMs, toMs);
1256
1507
  const segs = this.sortedSegments(deviceId, profile);
1257
1508
  const end = lowerBoundByStart(segs, toMs);
1258
1509
  let head = lowerBoundByStart(segs, fromMs);
@@ -1414,6 +1665,7 @@ var RecordingIndex = class {
1414
1665
  }
1415
1666
  /** The segment whose [startMs, startMs+durMs) window contains `epochMs`, or null if `epochMs` falls in a gap / there is no footage. @residency resident-scoped */
1416
1667
  segmentAt(deviceId, profile, epochMs) {
1668
+ this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
1417
1669
  return this.segmentAtIn(this.sortedSegments(deviceId, profile), epochMs);
1418
1670
  }
1419
1671
  /**
@@ -1423,6 +1675,7 @@ var RecordingIndex = class {
1423
1675
  * @residency resident-scoped
1424
1676
  */
1425
1677
  nearestCoveredEdge(deviceId, profile, epochMs) {
1678
+ this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
1426
1679
  const segs = this.sortedSegments(deviceId, profile);
1427
1680
  if (segs.length === 0) return null;
1428
1681
  if (this.segmentAtIn(segs, epochMs) !== null) return epochMs;
@@ -1476,6 +1729,7 @@ var RecordingIndex = class {
1476
1729
  * @residency resident-scoped
1477
1730
  */
1478
1731
  coveredEdgeBefore(deviceId, profile, epochMs) {
1732
+ this.touch(deviceId, epochMs - this.maxDurMs, epochMs + 1);
1479
1733
  const segs = this.sortedSegments(deviceId, profile);
1480
1734
  let best = null;
1481
1735
  for (let i = upperBoundByStart(segs, epochMs) - 1; i >= 0; i--) {
@@ -1488,6 +1742,7 @@ var RecordingIndex = class {
1488
1742
  }
1489
1743
  /** @residency resident-scoped */
1490
1744
  ranges(deviceId, profile, fromMs, toMs, gapToleranceMs) {
1745
+ this.touch(deviceId, fromMs, toMs);
1491
1746
  return mergeRanges(this.sortedSegments(deviceId, profile).filter((s) => s.startMs + s.durMs > fromMs && s.startMs < toMs), gapToleranceMs).map((r) => ({
1492
1747
  startMs: Math.max(r.startMs, fromMs),
1493
1748
  endMs: Math.min(r.endMs, toMs)
@@ -1722,6 +1977,7 @@ async function resolveRecordingsLocations(api, logger) {
1722
1977
  async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger, options = {}) {
1723
1978
  const yieldBetween = options.yieldBetween ?? macrotask$3;
1724
1979
  const chunkSize = options.chunkSize ?? HYDRATE_CHUNK;
1980
+ index.beginWalk(deviceId);
1725
1981
  for (const [root, aliases] of aliasesByRoot(locations)) {
1726
1982
  const deviceDir = node_path.default.join(root, String(deviceId));
1727
1983
  const startedMs = Date.now();
@@ -10056,10 +10312,16 @@ var SegmentHourLedger = class SegmentHourLedger {
10056
10312
  else fromIndex.set(key, appendSegmentPath(existing, seg.path));
10057
10313
  }
10058
10314
  const refused = this.refusedPrunes(walked, fromIndex);
10315
+ const trimmed = /* @__PURE__ */ new Set();
10316
+ for (const deviceId of walked) {
10317
+ if (!index.droppedDuringWalk(deviceId)) continue;
10318
+ trimmed.add(deviceId);
10319
+ this.logger.warn("recorder: ledger prune REFUSED — the RAM index dropped hours during this walk, so its silence is not evidence", { tags: { deviceId } });
10320
+ }
10059
10321
  for (const row of this.ledger.snapshot()) {
10060
10322
  if (!walked.has(row.deviceId)) continue;
10061
10323
  const disk = fromIndex.get(row.key);
10062
- const additiveOnly = row.hourStartMs === currentHour || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
10324
+ const additiveOnly = row.hourStartMs === currentHour || trimmed.has(row.deviceId) || refused.devices.has(row.deviceId) || refused.locations.has(locationKey(row.deviceId, row.locationId));
10063
10325
  if (disk === void 0) {
10064
10326
  if (additiveOnly) continue;
10065
10327
  await this.ledger.forget(row.key);
@@ -17797,6 +18059,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
17797
18059
  }
17798
18060
  async onInitialize() {
17799
18061
  this.stopStallMonitor = (0, _camstack_system_addon_utils.startEventLoopStallMonitor)(this.ctx.logger);
18062
+ this.index.armResidency({
18063
+ logger: this.ctx.logger,
18064
+ now: () => Date.now(),
18065
+ budget: DEFAULT_RECORDING_INDEX_BUDGET
18066
+ });
17800
18067
  const raw = this.ctx.kernel.localNodeId ?? this.ctx.id;
17801
18068
  this.nodeId = raw.includes("/") ? raw.split("/")[0] : raw;
17802
18069
  const configuredNode = this.config.recordingNodeId;
@@ -18792,9 +19059,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
18792
19059
  const census = this.index.sortedViewCensus();
18793
19060
  const views = this.index.sortedViewStats();
18794
19061
  const reuse = this.index.hydrateReuseStats();
19062
+ const residency = this.index.residency();
18795
19063
  this.ctx.logger.info("recorder: index census", {
18796
19064
  ...census.largestDeviceId > 0 ? { tags: { deviceId: census.largestDeviceId } } : {},
18797
19065
  meta: {
19066
+ residentRows: residency.residentRows,
19067
+ residentHours: residency.residentHours,
19068
+ rowBudget: residency.budget?.residentRowBudget ?? null,
19069
+ evictedRows: residency.evictedRows,
19070
+ evictedHours: residency.evictedHours,
19071
+ evictionPasses: residency.evictionPasses,
18798
19072
  sortedViews: census.views,
18799
19073
  rowSlots: census.rowSlots,
18800
19074
  largestViewRows: census.largestRows,