@camstack/addon-post-analysis 1.2.277 → 1.2.279

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.
@@ -1,4 +1,4 @@
1
- import { $ as RECORDING_SOURCE_CAMSTACK_ADDON, $t as CamProfileSchema, At as isScheduleActive, B as NcRuleUpdateInputSchema, Bt as readDeviceStateFrom, C as MAX_BIRTH_DECISION_RECORDS, Ct as faceGalleryCapability, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dt as isEventOwnerType, E as MediaFileKindEnum, Et as isDetectionMacroClass, F as NcConditionDescriptorSchema, Ft as occupancyScope, G as NcSystemEventKindSchema, Gt as storageOccupancyCapability, H as NcSnoozeInputSchema, Ht as resolveLocationMode, I as NcRuleInputSchema, J as NcTemplatePreviewSchema, Jt as templateVarsFor, K as NcTaxonomySchema, Kt as subKindsOf, L as NcRulePatchSchema, Lt as pickClusterStepModels, M as NC_TEMPLATE_VARS, Mt as kebabToCamel, N as NcAlarmConfigSchema, Nt as mayWriteToLocation, O as NC_CONDITION_CATALOG, Ot as isKnownTemplateVar, P as NcAlarmSettingsPatchSchema, Pt as notificationRulesCapability, Q as RECORDING_EXPORT_MAX_READ_BYTES, Qt as BaseAddon, R as NcRuleSchema, Rt as pipelineAnalyticsCapability, S as MAX_ARCHIVED_DEBUG_NOTES, St as evictionPolicyOfLocation, T as MOTION_CLOSE_AFTER_MS, U as NcSnoozeSchema, V as NcScheduleSchema, Vt as readTimelapseGeneratedAt, W as NcSnoozeSuppressedSchema, Wt as sceneMonitorCapability, X as OpsLogEntrySchema, Xt as zoneAnalyticsCapability, Y as NcTemplateVarDescriptorSchema, Yt as vectorDimFromBase64, Zt as errMsg$1, _ as FIRST_LEVEL_MACRO_CLASSES, _n as string, _t as cosineSimilarity$1, a as BirthDecisionRecordSchema, an as isDeviceScopedCap, at as SceneMonitorSchema, b as LabelAttributionSchema, bt as encodeVectorBase64, c as CoverStateSchema, cn as _enum, ct as TimelapseRulePatchSchema, dn as discriminatedUnion, dt as addonWidgetsSourceCapability, en as DeviceType, et as RetrainStatusSchema, f as DETECTION_MACRO_CLASSES, fn as literal, ft as alarmPanelCapability, g as EVENT_OWNER_TYPES, gn as record, gt as buildEventKindDescriptor, h as EVENT_KIND_BY_CAP, hn as partialRecord, ht as audioModeOf, i as BaseDevice, in as hydrateSchema, it as SCENE_DIVERGED, j as NC_TAXONOMY, jt as isSourceCap, k as NC_DEFAULT_SNOOZE_MINUTES, kt as isMediaOwnerType, l as DEFAULT_EVENT_COLOR, ln as array, lt as TimelapseRuleSchema, m as DeclaredDevices, mn as object, mt as audioMetricsCapability, n as AlarmArmModeSchema, nn as customAction, nt as SCENE_DEFAULT_ANCHOR_THRESHOLD, o as CLUSTER_MODEL_SCOPED_STEPS, on as nodePin, ot as TIMELAPSE_DENSE_FLOOR_SEC, pn as number, pt as assertTimelapseCadences, q as NcTemplatePreviewInputSchema, qt as systemEventFilterApplies, r as ArchivedDebugNoteSchema, rn as defineCustomActions, rt as SCENE_DEFAULT_UNCOVERED_POLICY, s as COCO_TO_MACRO, sn as sleep$1, st as TimelapseRuleInputSchema, t as AUDIO_MACRO_LABELS, tn as createEvent, un as boolean, ut as TrackSourceSchema, v as FULL_IMAGE_BBOX, vn as unknown, vt as deriveRecordingMode, wt as failureContributionCapability, x as LockStateSchema, xt as evaluateSensorEdge, y as FailureCounters, yn as EventCategory, z as NcRuleTargetSchema, zt as plateGalleryCapability } from "../dist-D8G7jV1C.mjs";
1
+ import { $ as RECORDING_SOURCE_CAMSTACK_ADDON, $t as CamProfileSchema, At as isScheduleActive, B as NcRuleUpdateInputSchema, Bt as readDeviceStateFrom, C as MAX_BIRTH_DECISION_RECORDS, Ct as faceGalleryCapability, D as NC_ALARM_SYSTEM_EVENT_KINDS, Dt as isEventOwnerType, E as MediaFileKindEnum, Et as isDetectionMacroClass, F as NcConditionDescriptorSchema, Ft as occupancyScope, G as NcSystemEventKindSchema, Gt as storageOccupancyCapability, H as NcSnoozeInputSchema, Ht as resolveLocationMode, I as NcRuleInputSchema, J as NcTemplatePreviewSchema, Jt as templateVarsFor, K as NcTaxonomySchema, Kt as subKindsOf, L as NcRulePatchSchema, Lt as pickClusterStepModels, M as NC_TEMPLATE_VARS, Mt as kebabToCamel, N as NcAlarmConfigSchema, Nt as mayWriteToLocation, O as NC_CONDITION_CATALOG, Ot as isKnownTemplateVar, P as NcAlarmSettingsPatchSchema, Pt as notificationRulesCapability, Q as RECORDING_EXPORT_MAX_READ_BYTES, Qt as BaseAddon, R as NcRuleSchema, Rt as pipelineAnalyticsCapability, S as MAX_ARCHIVED_DEBUG_NOTES, St as evictionPolicyOfLocation, T as MOTION_CLOSE_AFTER_MS, U as NcSnoozeSchema, V as NcScheduleSchema, Vt as readTimelapseGeneratedAt, W as NcSnoozeSuppressedSchema, Wt as sceneMonitorCapability, X as OpsLogEntrySchema, Xt as zoneAnalyticsCapability, Y as NcTemplateVarDescriptorSchema, Yt as vectorDimFromBase64, Zt as errMsg$1, _ as FIRST_LEVEL_MACRO_CLASSES, _n as string, _t as cosineSimilarity$1, a as BirthDecisionRecordSchema, an as isDeviceScopedCap, at as SceneMonitorSchema, b as LabelAttributionSchema, bt as encodeVectorBase64, c as CoverStateSchema, cn as _enum, ct as TimelapseRulePatchSchema, dn as discriminatedUnion, dt as addonWidgetsSourceCapability, en as DeviceType, et as RetrainStatusSchema, f as DETECTION_MACRO_CLASSES, fn as literal, ft as alarmPanelCapability, g as EVENT_OWNER_TYPES, gn as record, gt as buildEventKindDescriptor, h as EVENT_KIND_BY_CAP, hn as partialRecord, ht as audioModeOf, i as BaseDevice, in as hydrateSchema, it as SCENE_DIVERGED, j as NC_TAXONOMY, jt as isSourceCap, k as NC_DEFAULT_SNOOZE_MINUTES, kt as isMediaOwnerType, l as DEFAULT_EVENT_COLOR, ln as array, lt as TimelapseRuleSchema, m as DeclaredDevices, mn as object, mt as audioMetricsCapability, n as AlarmArmModeSchema, nn as customAction, nt as SCENE_DEFAULT_ANCHOR_THRESHOLD, o as CLUSTER_MODEL_SCOPED_STEPS, on as nodePin, ot as TIMELAPSE_DENSE_FLOOR_SEC, pn as number, pt as assertTimelapseCadences, q as NcTemplatePreviewInputSchema, qt as systemEventFilterApplies, r as ArchivedDebugNoteSchema, rn as defineCustomActions, rt as SCENE_DEFAULT_UNCOVERED_POLICY, s as COCO_TO_MACRO, sn as sleep$1, st as TimelapseRuleInputSchema, t as AUDIO_MACRO_LABELS, tn as createEvent, un as boolean, ut as TrackSourceSchema, v as FULL_IMAGE_BBOX, vn as unknown, vt as deriveRecordingMode, wt as failureContributionCapability, x as LockStateSchema, xt as evaluateSensorEdge, y as FailureCounters, yn as EventCategory, z as NcRuleTargetSchema, zt as plateGalleryCapability } from "../dist-D0jvZ8dq.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -8212,6 +8212,99 @@ var NcAlarmLiveReadLog = class {
8212
8212
  }
8213
8213
  };
8214
8214
  //#endregion
8215
+ //#region src/notification-center/alerts/alert-episode.ts
8216
+ function decideAlert(input) {
8217
+ const previous = input.previous;
8218
+ if (previous === void 0) return input.alerting ? { kind: "raise" } : { kind: "seeded" };
8219
+ if (previous.alerting !== input.alerting) {
8220
+ if (input.alerting) return { kind: "raise" };
8221
+ if (previous.lastNotifiedAt <= 0) return { kind: "silent" };
8222
+ return input.notifyRecovery ? { kind: "recover" } : { kind: "silent" };
8223
+ }
8224
+ if (!input.alerting) return { kind: "silent" };
8225
+ if (input.renotifyMs <= 0) return { kind: "silent" };
8226
+ if (previous.lastNotifiedAt <= 0) return { kind: "silent" };
8227
+ if (input.nowMs - previous.lastNotifiedAt < input.renotifyMs) return { kind: "silent" };
8228
+ return {
8229
+ kind: "remind",
8230
+ reminderIndex: previous.reminders + 1
8231
+ };
8232
+ }
8233
+ /**
8234
+ * The row that survives a verdict.
8235
+ *
8236
+ * Split from {@link decideAlert} rather than returned with it because the
8237
+ * caller must be able to persist a row for a verdict it did NOT deliver — the
8238
+ * suppressed recovery is the whole reason (rule 3), and so is the silent seed.
8239
+ */
8240
+ function nextAlertRow(subject, previous, alerting, verdict, nowMs) {
8241
+ switch (verdict.kind) {
8242
+ case "seeded": return {
8243
+ subject,
8244
+ alerting,
8245
+ since: nowMs,
8246
+ lastNotifiedAt: 0,
8247
+ reminders: 0
8248
+ };
8249
+ case "raise":
8250
+ case "recover": return {
8251
+ subject,
8252
+ alerting,
8253
+ since: nowMs,
8254
+ lastNotifiedAt: nowMs,
8255
+ reminders: 0
8256
+ };
8257
+ case "remind": return {
8258
+ subject,
8259
+ alerting,
8260
+ since: previous?.since ?? nowMs,
8261
+ lastNotifiedAt: nowMs,
8262
+ reminders: verdict.reminderIndex
8263
+ };
8264
+ case "silent":
8265
+ if (previous === void 0) return {
8266
+ subject,
8267
+ alerting,
8268
+ since: nowMs,
8269
+ lastNotifiedAt: 0,
8270
+ reminders: 0
8271
+ };
8272
+ if (previous.alerting !== alerting) return {
8273
+ subject,
8274
+ alerting,
8275
+ since: nowMs,
8276
+ lastNotifiedAt: previous.lastNotifiedAt,
8277
+ reminders: 0
8278
+ };
8279
+ return previous;
8280
+ }
8281
+ }
8282
+ /**
8283
+ * The `recordId` a notification for this episode carries — and therefore, via
8284
+ * the outbox's fallback, its `dedupRef`.
8285
+ *
8286
+ * `reminderIndex: 0` is the opening notification (and the recovery, which
8287
+ * opens the `normal` episode). Every reminder appends `:r<n>`, which is what
8288
+ * stops the 7-day outbox dedup swallowing it.
8289
+ */
8290
+ function alertRecordId(subject, since, reminderIndex) {
8291
+ const base = `${subject}:${since}`;
8292
+ return reminderIndex > 0 ? `${base}:r${reminderIndex}` : base;
8293
+ }
8294
+ /**
8295
+ * The device a subject is about: `battery:3602` → 3602, and
8296
+ * `consumable:4385:sensor` → 4385. The id is the SECOND segment, never the
8297
+ * last — reading the last one turned every consumable subject into "no
8298
+ * device", which lost the `deviceId` tag on its log lines and the indexed
8299
+ * column on its durable row. `undefined` for a subject that names no device.
8300
+ */
8301
+ function alertSubjectDeviceId(subject) {
8302
+ const raw = subject.split(":")[1];
8303
+ if (raw === void 0 || raw === "") return void 0;
8304
+ const parsed = Number(raw);
8305
+ return Number.isInteger(parsed) ? parsed : void 0;
8306
+ }
8307
+ //#endregion
8215
8308
  //#region src/notification-center/alerts/alert-store.ts
8216
8309
  /**
8217
8310
  * @durable class=ledger owner=notification-center
@@ -8269,9 +8362,9 @@ var NC_ALERTS_INDEXES = [{
8269
8362
  name: "idx_nc_alerts_device",
8270
8363
  columns: ["deviceId"]
8271
8364
  }];
8365
+ /** The device a subject is about — see {@link alertSubjectDeviceId}. */
8272
8366
  function deviceIdOf(subject) {
8273
- const parsed = Number(subject.slice(subject.lastIndexOf(":") + 1));
8274
- return Number.isInteger(parsed) ? parsed : void 0;
8367
+ return alertSubjectDeviceId(subject);
8275
8368
  }
8276
8369
  var NC_ALERTS_SPEC = {
8277
8370
  collection: NC_ALERTS_COLLECTION,
@@ -8331,7 +8424,8 @@ var NcAlertStore = class {
8331
8424
  put: (row) => {
8332
8425
  this.ledger.put(row).catch(() => void 0);
8333
8426
  },
8334
- all: () => this.ledger.snapshot()
8427
+ all: () => this.ledger.snapshot(),
8428
+ remove: (subject) => this.ledger.forget(subject)
8335
8429
  };
8336
8430
  }
8337
8431
  };
@@ -9942,6 +10036,10 @@ function systemEventExclusion(condition, subject) {
9942
10036
  if (condition.deviceIds !== void 0 && applies("deviceIds")) {
9943
10037
  if (subject.deviceId === void 0 || !condition.deviceIds.includes(subject.deviceId)) return "deviceIds";
9944
10038
  }
10039
+ const excluded = condition.excludeDeviceIds;
10040
+ if (excluded !== void 0 && applies("deviceIds")) {
10041
+ if (subject.deviceId !== void 0 && excluded.includes(subject.deviceId)) return "excludeDeviceIds";
10042
+ }
9945
10043
  if (condition.deviceTypes !== void 0 && applies("deviceTypes")) {
9946
10044
  if (subject.deviceType === void 0 || !condition.deviceTypes.includes(subject.deviceType)) return "deviceTypes";
9947
10045
  }
@@ -11504,85 +11602,6 @@ function deliveryForKind(kind) {
11504
11602
  return kind;
11505
11603
  }
11506
11604
  //#endregion
11507
- //#region src/notification-center/alerts/alert-episode.ts
11508
- function decideAlert(input) {
11509
- const previous = input.previous;
11510
- if (previous === void 0) return input.alerting ? { kind: "raise" } : { kind: "seeded" };
11511
- if (previous.alerting !== input.alerting) {
11512
- if (input.alerting) return { kind: "raise" };
11513
- return input.notifyRecovery ? { kind: "recover" } : { kind: "silent" };
11514
- }
11515
- if (!input.alerting) return { kind: "silent" };
11516
- if (input.renotifyMs <= 0) return { kind: "silent" };
11517
- if (previous.lastNotifiedAt <= 0) return { kind: "silent" };
11518
- if (input.nowMs - previous.lastNotifiedAt < input.renotifyMs) return { kind: "silent" };
11519
- return {
11520
- kind: "remind",
11521
- reminderIndex: previous.reminders + 1
11522
- };
11523
- }
11524
- /**
11525
- * The row that survives a verdict.
11526
- *
11527
- * Split from {@link decideAlert} rather than returned with it because the
11528
- * caller must be able to persist a row for a verdict it did NOT deliver — the
11529
- * suppressed recovery is the whole reason (rule 3), and so is the silent seed.
11530
- */
11531
- function nextAlertRow(subject, previous, alerting, verdict, nowMs) {
11532
- switch (verdict.kind) {
11533
- case "seeded": return {
11534
- subject,
11535
- alerting,
11536
- since: nowMs,
11537
- lastNotifiedAt: 0,
11538
- reminders: 0
11539
- };
11540
- case "raise":
11541
- case "recover": return {
11542
- subject,
11543
- alerting,
11544
- since: nowMs,
11545
- lastNotifiedAt: nowMs,
11546
- reminders: 0
11547
- };
11548
- case "remind": return {
11549
- subject,
11550
- alerting,
11551
- since: previous?.since ?? nowMs,
11552
- lastNotifiedAt: nowMs,
11553
- reminders: verdict.reminderIndex
11554
- };
11555
- case "silent":
11556
- if (previous === void 0) return {
11557
- subject,
11558
- alerting,
11559
- since: nowMs,
11560
- lastNotifiedAt: 0,
11561
- reminders: 0
11562
- };
11563
- if (previous.alerting !== alerting) return {
11564
- subject,
11565
- alerting,
11566
- since: nowMs,
11567
- lastNotifiedAt: previous.lastNotifiedAt,
11568
- reminders: 0
11569
- };
11570
- return previous;
11571
- }
11572
- }
11573
- /**
11574
- * The `recordId` a notification for this episode carries — and therefore, via
11575
- * the outbox's fallback, its `dedupRef`.
11576
- *
11577
- * `reminderIndex: 0` is the opening notification (and the recovery, which
11578
- * opens the `normal` episode). Every reminder appends `:r<n>`, which is what
11579
- * stops the 7-day outbox dedup swallowing it.
11580
- */
11581
- function alertRecordId(subject, since, reminderIndex) {
11582
- const base = `${subject}:${since}`;
11583
- return reminderIndex > 0 ? `${base}:r${reminderIndex}` : base;
11584
- }
11585
- //#endregion
11586
11605
  //#region src/notification-center/alerts/battery-band.ts
11587
11606
  /**
11588
11607
  * The stand-in a provider writes for a binary NORMAL. Anything below it, on a
@@ -11644,6 +11663,18 @@ function deriveBatteryBand(input) {
11644
11663
  * that is already a FAULT — see `alert-episode.ts` rule 2 for why that is D141
11645
11664
  * rather than a loophole.
11646
11665
  */
11666
+ /** One hour — the longest a pending raise waits before it is offered again. */
11667
+ var PENDING_CADENCE_MAX_MS = 36e5;
11668
+ /**
11669
+ * How often a pending raise is offered again with the rules unchanged: the
11670
+ * re-notify interval, capped at one hour. The cap matters because the
11671
+ * cadence counts from the raise: with a 12 h interval, a rule whose schedule
11672
+ * opens for one hour a day could keep being missed. Reminders off ("say it
11673
+ * once") still has to say it once, hourly.
11674
+ */
11675
+ function pendingCadenceMs(settings) {
11676
+ return settings.renotifyMinutes > 0 ? Math.min(settings.renotifyMinutes * 6e4, PENDING_CADENCE_MAX_MS) : PENDING_CADENCE_MAX_MS;
11677
+ }
11647
11678
  var NcDeviceAlerts = class {
11648
11679
  deps;
11649
11680
  now;
@@ -11657,6 +11688,13 @@ var NcDeviceAlerts = class {
11657
11688
  lastReading = /* @__PURE__ */ new Map();
11658
11689
  /** The last reading per consumable SUBJECT, for the reminder sweep. */
11659
11690
  lastConsumable = /* @__PURE__ */ new Map();
11691
+ /**
11692
+ * Subject → WHEN its PENDING raise was last offered, and to which
11693
+ * {@link NcDeviceAlertsDeps.rulesVersion}. RAM only, on purpose: after a
11694
+ * restart every pending raise is offered once more, which is bounded by the
11695
+ * number of open episodes.
11696
+ */
11697
+ offeredAt = /* @__PURE__ */ new Map();
11660
11698
  constructor(deps) {
11661
11699
  this.deps = deps;
11662
11700
  this.now = deps.now ?? Date.now;
@@ -11742,34 +11780,94 @@ var NcDeviceAlerts = class {
11742
11780
  */
11743
11781
  tick() {
11744
11782
  const settings = this.deps.settings();
11783
+ this.retryPending(settings);
11745
11784
  if (settings.renotifyMinutes <= 0) return;
11746
11785
  for (const row of this.deps.rows.all()) {
11747
- if (!row.alerting) continue;
11748
- const consumable = this.lastConsumable.get(row.subject);
11749
- if (consumable !== void 0) {
11750
- this.apply(row.subject, true, settings, (facts) => incomingFromConsumableBand({
11751
- facts,
11752
- deviceId: consumable.deviceId,
11753
- band: "low",
11754
- item: consumable.item,
11755
- identity: this.deps.lookup(consumable.deviceId)
11756
- }));
11757
- continue;
11758
- }
11759
- const restatement = this.lastReading.get(row.subject);
11760
- if (restatement === void 0) continue;
11761
- this.apply(row.subject, true, settings, (facts) => incomingFromBatteryBand({
11762
- facts,
11763
- deviceId: restatement.deviceId,
11764
- band: "low",
11765
- reading: restatement.reading,
11766
- identity: this.deps.lookup(restatement.deviceId)
11767
- }));
11786
+ if (!row.alerting || isPending(row)) continue;
11787
+ const build = this.restatementOf(row.subject);
11788
+ if (build === null) continue;
11789
+ this.apply(row.subject, true, settings, build);
11768
11790
  }
11769
11791
  }
11770
- /** Drop a subject (device removed), so a re-adoption starts a fresh episode. */
11771
- forget(deviceId) {
11792
+ /**
11793
+ * Every device that holds an alert row — what the deleted-device reconcile
11794
+ * must look at besides the devices a rule names (a battery device no rule
11795
+ * names still has a row).
11796
+ */
11797
+ deviceIds() {
11798
+ const ids = /* @__PURE__ */ new Set();
11799
+ for (const row of this.deps.rows.all()) {
11800
+ const id = alertSubjectDeviceId(row.subject);
11801
+ if (id !== void 0) ids.add(id);
11802
+ }
11803
+ return [...ids];
11804
+ }
11805
+ /**
11806
+ * A DELETED device: drop its battery row and every consumable row, durable
11807
+ * and in RAM, so nothing restates it and a re-adoption starts a fresh
11808
+ * episode. Called only by the deleted-device prune (D633), after the second
11809
+ * read agreed the device is gone. Returns how many rows went.
11810
+ */
11811
+ async forget(deviceId) {
11812
+ const subjects = this.deps.rows.all().map((row) => row.subject).filter((subject) => alertSubjectDeviceId(subject) === deviceId);
11813
+ for (const subject of subjects) {
11814
+ this.lastReading.delete(subject);
11815
+ this.lastConsumable.delete(subject);
11816
+ this.offeredAt.delete(subject);
11817
+ await this.deps.rows.remove(subject);
11818
+ }
11772
11819
  this.lastReading.delete(`battery:${deviceId}`);
11820
+ return subjects.length;
11821
+ }
11822
+ /**
11823
+ * Offer every PENDING raise once more — when the rule set changed since its
11824
+ * last offer, OR when {@link pendingCadenceMs} has passed since it. The
11825
+ * cadence is what delivers a raise refused only by a rule's SCHEDULE (or any
11826
+ * other time-dependent gate): the rules do not change when the window
11827
+ * opens. The restatement is RAM: a row whose reading has not arrived since
11828
+ * the restart waits for it (the slice arrives on its own).
11829
+ */
11830
+ retryPending(settings) {
11831
+ const version = this.deps.rulesVersion();
11832
+ const nowMs = this.now();
11833
+ const cadenceMs = pendingCadenceMs(settings);
11834
+ for (const row of this.deps.rows.all()) {
11835
+ if (!isPending(row)) continue;
11836
+ const last = this.offeredAt.get(row.subject);
11837
+ const changed = last === void 0 || last.version !== version;
11838
+ if (!changed && nowMs - last.at < cadenceMs) continue;
11839
+ const build = this.restatementOf(row.subject);
11840
+ if (build === null) continue;
11841
+ this.logger()?.debug("device alert re-offered — no rule has matched it yet", {
11842
+ ...tagsOf(row.subject),
11843
+ meta: {
11844
+ subject: row.subject,
11845
+ since: row.since,
11846
+ reason: changed ? "rules" : "cadence"
11847
+ }
11848
+ });
11849
+ this.speak(row.subject, row, { kind: "raise" }, build);
11850
+ }
11851
+ }
11852
+ /** How to re-word a subject's alert from the last reading, or `null`. */
11853
+ restatementOf(subject) {
11854
+ const consumable = this.lastConsumable.get(subject);
11855
+ if (consumable !== void 0) return (facts) => incomingFromConsumableBand({
11856
+ facts,
11857
+ deviceId: consumable.deviceId,
11858
+ band: "low",
11859
+ item: consumable.item,
11860
+ identity: this.deps.lookup(consumable.deviceId)
11861
+ });
11862
+ const battery = this.lastReading.get(subject);
11863
+ if (battery !== void 0) return (facts) => incomingFromBatteryBand({
11864
+ facts,
11865
+ deviceId: battery.deviceId,
11866
+ band: "low",
11867
+ reading: battery.reading,
11868
+ identity: this.deps.lookup(battery.deviceId)
11869
+ });
11870
+ return null;
11773
11871
  }
11774
11872
  /**
11775
11873
  * The shared half: decide, persist, and speak only when the verdict says to.
@@ -11794,16 +11892,63 @@ var NcDeviceAlerts = class {
11794
11892
  this.report(subject, verdict, alerting);
11795
11893
  return;
11796
11894
  }
11895
+ this.speak(subject, next, verdict, (facts) => build(facts, alerting));
11896
+ }
11897
+ /**
11898
+ * Emit one verdict through the door, and — for a RAISE — hear whether any
11899
+ * rule matched it. A raise no rule matched is put back to PENDING
11900
+ * (`lastNotifiedAt: 0`), unless the row moved meanwhile; it is re-offered
11901
+ * by {@link retryPending} when the rules change. A reminder or a recovery is
11902
+ * not re-offered: the episode was already announced (or is over).
11903
+ */
11904
+ speak(subject, row, verdict, build) {
11905
+ const nowMs = this.now();
11797
11906
  const reminderIndex = verdict.kind === "remind" ? verdict.reminderIndex : 0;
11798
- this.deps.emit(build({
11799
- eventId: alertRecordId(subject, next.since, reminderIndex),
11907
+ const event = build({
11908
+ eventId: alertRecordId(subject, row.since, reminderIndex),
11800
11909
  timestamp: nowMs,
11801
11910
  category: EventCategory.DeviceStateChanged,
11802
11911
  data: {
11803
11912
  alertVerdict: verdict.kind,
11804
11913
  reminderIndex
11805
11914
  }
11806
- }, alerting));
11915
+ });
11916
+ if (verdict.kind !== "raise") {
11917
+ this.deps.emit(event).catch(() => void 0);
11918
+ return;
11919
+ }
11920
+ this.offeredAt.set(subject, {
11921
+ version: this.deps.rulesVersion(),
11922
+ at: nowMs
11923
+ });
11924
+ this.deps.emit(event).then((heard) => this.settleRaise(subject, row.since, heard, nowMs)).catch(() => void 0);
11925
+ }
11926
+ /** The door answered a raise. */
11927
+ settleRaise(subject, since, heard, nowMs) {
11928
+ const current = this.deps.rows.get(subject);
11929
+ if (current === void 0 || !current.alerting || current.since !== since) return;
11930
+ if (heard) {
11931
+ this.offeredAt.delete(subject);
11932
+ if (current.lastNotifiedAt !== nowMs) this.deps.rows.put({
11933
+ ...current,
11934
+ lastNotifiedAt: nowMs
11935
+ });
11936
+ return;
11937
+ }
11938
+ if (current.lastNotifiedAt !== 0) this.deps.rows.put({
11939
+ ...current,
11940
+ lastNotifiedAt: 0
11941
+ });
11942
+ this.logger()?.info("device alert matched NO rule — kept pending, offered again on a rule change or within the hour", {
11943
+ ...tagsOf(subject),
11944
+ meta: {
11945
+ subject,
11946
+ since
11947
+ }
11948
+ });
11949
+ }
11950
+ logger() {
11951
+ return this.deps.logger;
11807
11952
  }
11808
11953
  /**
11809
11954
  * A dropped branch is never silent. The suppressed recovery in particular:
@@ -11813,7 +11958,7 @@ var NcDeviceAlerts = class {
11813
11958
  report(subject, verdict, alerting) {
11814
11959
  const logger = this.deps.logger;
11815
11960
  if (logger === void 0) return;
11816
- const deviceId = deviceIdOfSubject(subject);
11961
+ const deviceId = alertSubjectDeviceId(subject);
11817
11962
  const tags = deviceId !== void 0 ? { deviceId } : {};
11818
11963
  if (verdict.kind === "silent" && !alerting) {
11819
11964
  logger.debug("device alert cleared without a notification", {
@@ -11837,11 +11982,16 @@ var NcDeviceAlerts = class {
11837
11982
  function speaks(verdict) {
11838
11983
  return verdict.kind === "raise" || verdict.kind === "recover" || verdict.kind === "remind";
11839
11984
  }
11840
- /** `battery:3602` → 3602. Undefined for a subject that names no device. */
11841
- function deviceIdOfSubject(subject) {
11842
- const raw = subject.slice(subject.lastIndexOf(":") + 1);
11843
- const parsed = Number(raw);
11844
- return Number.isInteger(parsed) ? parsed : void 0;
11985
+ /**
11986
+ * A raise no rule matched: alerting, and never announced. A row in the
11987
+ * alerting state is otherwise always stamped, because it was raised.
11988
+ */
11989
+ function isPending(row) {
11990
+ return row.alerting && row.lastNotifiedAt <= 0;
11991
+ }
11992
+ function tagsOf(subject) {
11993
+ const deviceId = alertSubjectDeviceId(subject);
11994
+ return deviceId !== void 0 ? { tags: { deviceId } } : {};
11845
11995
  }
11846
11996
  //#endregion
11847
11997
  //#region src/notification-center/cap-caller.ts
@@ -19062,6 +19212,7 @@ var NcDeletedSubjectPruner = class {
19062
19212
  return this.serialise(async () => {
19063
19213
  if (!this.ready("event", deviceId)) return;
19064
19214
  if (!await this.confirmDeleted(deviceId, "event")) return;
19215
+ await this.forgetAlerts(deviceId, "event");
19065
19216
  const deleted = new Set([deviceId]);
19066
19217
  await this.apply(planDevicePrune(this.snapshot(), deleted, this.guard()), "event", (fresh) => planDevicePrune(fresh, deleted, this.guard()));
19067
19218
  }, deviceId);
@@ -19071,15 +19222,17 @@ var NcDeletedSubjectPruner = class {
19071
19222
  return this.serialise(async () => {
19072
19223
  if (!this.ready("reconcile")) return;
19073
19224
  const directory = this.deps.directory;
19074
- const candidates = [...referencedDeviceIds(this.snapshot())].filter((id) => directory.absentFromLastTwoAnswers(id));
19225
+ const alerting = new Set(this.deps.alerts?.deviceIds() ?? []);
19226
+ const candidates = [...new Set([...referencedDeviceIds(this.snapshot()), ...alerting])].filter((id) => directory.absentFromLastTwoAnswers(id));
19075
19227
  if (candidates.length === 0) return;
19076
19228
  const first = planDevicePrune(this.snapshot(), new Set(candidates), this.guard());
19077
- if (isEmptyPlan(first)) return;
19078
- const subjects = [...planSubjects(first)].map(Number);
19229
+ const subjects = [...new Set([...[...planSubjects(first)].map(Number), ...candidates.filter((id) => alerting.has(id))])];
19230
+ if (subjects.length === 0) return;
19079
19231
  if (!this.withinBound("device", subjects.map(String))) return;
19080
19232
  const confirmed = /* @__PURE__ */ new Set();
19081
19233
  for (const id of subjects) if (await this.confirmDeleted(id, "reconcile")) confirmed.add(id);
19082
19234
  if (confirmed.size === 0) return;
19235
+ for (const id of confirmed) if (alerting.has(id)) await this.forgetAlerts(id, "reconcile");
19083
19236
  await this.apply(planDevicePrune(this.snapshot(), confirmed, this.guard()), "reconcile", (fresh) => planDevicePrune(fresh, confirmed, this.guard()));
19084
19237
  });
19085
19238
  }
@@ -19157,6 +19310,33 @@ var NcDeletedSubjectPruner = class {
19157
19310
  return false;
19158
19311
  }
19159
19312
  }
19313
+ /**
19314
+ * Drop a CONFIRMED-deleted device's alert rows. A failure is logged and
19315
+ * retried by the next reconcile, which still sees the row.
19316
+ */
19317
+ async forgetAlerts(deviceId, source) {
19318
+ const alerts = this.deps.alerts;
19319
+ if (alerts === void 0) return;
19320
+ try {
19321
+ const forgotten = await alerts.forget(deviceId);
19322
+ if (forgotten === 0) return;
19323
+ this.deps.logger.info("deleted device alerts forgotten", {
19324
+ tags: { deviceId },
19325
+ meta: {
19326
+ source,
19327
+ rows: forgotten
19328
+ }
19329
+ });
19330
+ } catch (err) {
19331
+ this.deps.logger.warn("deleted device alerts NOT forgotten — retried next tick", {
19332
+ tags: { deviceId },
19333
+ meta: {
19334
+ source,
19335
+ error: String(err)
19336
+ }
19337
+ });
19338
+ }
19339
+ }
19160
19340
  withinBound(subjectKind, subjects) {
19161
19341
  if (subjects.length <= 16) {
19162
19342
  this.lastAbort.delete(subjectKind);
@@ -28246,9 +28426,10 @@ var NotificationCenter = class NotificationCenter {
28246
28426
  logger: this.logger.child("device-alerts")
28247
28427
  });
28248
28428
  this.deviceAlerts = new NcDeviceAlerts({
28249
- emit: (event) => {
28250
- this.consumeEvent(event);
28429
+ emit: async (event) => {
28430
+ return (await this.consumeEventAndReport(event)).outcomes.some((outcome) => outcome.matched);
28251
28431
  },
28432
+ rulesVersion: () => this.rulesFingerprint(),
28252
28433
  settings: () => this.deviceAlertSettings(),
28253
28434
  lookup: (deviceId) => this.deviceDirectory.get(deviceId),
28254
28435
  rows: this.alertStore.port(),
@@ -28350,6 +28531,7 @@ var NotificationCenter = class NotificationCenter {
28350
28531
  summary: this.summaryRules,
28351
28532
  mutes: this.deviceMutes,
28352
28533
  alarm: () => this.alarmPanel,
28534
+ alerts: this.deviceAlerts,
28353
28535
  directory: this.deviceDirectory,
28354
28536
  isDeviceDeleted: deps.isDeviceDeleted,
28355
28537
  targetCatalog: () => this.dispatcher.catalogSnapshot()
@@ -31055,6 +31237,14 @@ var NotificationCenter = class NotificationCenter {
31055
31237
  * wiring that silently disabled the feature would be indistinguishable from
31056
31238
  * a hub with no low batteries on it.
31057
31239
  */
31240
+ /**
31241
+ * Which rules exist, and at which revision — the question "did anything
31242
+ * change that could make an undelivered alert deliverable now?". Cheap: the
31243
+ * rule mirror is in RAM and holds tens of rows.
31244
+ */
31245
+ rulesFingerprint() {
31246
+ return this.rules.list().map((rule) => `${rule.id}@${String(rule.updatedAt)}`).join("|");
31247
+ }
31058
31248
  deviceAlertSettings() {
31059
31249
  return this.deps.readDeviceAlertSettings?.() ?? {
31060
31250
  batteryLowPercent: NC_DEVICE_ALERT_FALLBACK.batteryLowPercent,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CHNFeWfB.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CxprcSXe.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.277",
3
+ "version": "1.2.279",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",