@camstack/system 1.2.316 → 1.2.318

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 (87) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/auth-manager.d.ts +11 -0
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/camera-grid/index.js +1 -1
  15. package/dist/builtins/camera-grid/index.mjs +1 -1
  16. package/dist/builtins/composer/composer.addon.js +347 -181
  17. package/dist/builtins/composer/composer.addon.mjs +347 -181
  18. package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
  19. package/dist/builtins/composer/composition-runtime.d.ts +11 -52
  20. package/dist/builtins/composer/composition-target.d.ts +50 -0
  21. package/dist/builtins/composer/existing-target.d.ts +19 -1
  22. package/dist/builtins/composer/own-field-reads.d.ts +19 -0
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.js +265 -29
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
  29. package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
  30. package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
  31. package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
  32. package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
  33. package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
  34. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
  35. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
  36. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
  37. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  41. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  42. package/dist/builtins/local-auth/local-auth.addon.js +47 -3
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
  44. package/dist/builtins/local-network/local-network.addon.js +1 -1
  45. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  46. package/dist/builtins/loki-logging/index.js +1 -1
  47. package/dist/builtins/loki-logging/index.mjs +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  50. package/dist/builtins/platform-probe/index.js +1 -1
  51. package/dist/builtins/platform-probe/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +4 -4
  63. package/dist/builtins/system-config/system-config.addon.mjs +4 -4
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
  67. package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
  68. package/dist/{composition-sources-BwE1POnF.js → composition-sources-BjRPhWtU.js} +1 -1
  69. package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-Cg2MP-qf.mjs} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-CQ9vXSUh.mjs} +220 -8
  71. package/dist/{dist-RAl10EfG.js → dist-Dcy0gVHu.js} +220 -8
  72. package/dist/index.js +739 -350
  73. package/dist/index.mjs +737 -352
  74. package/dist/kernel/bootstrap-config.d.ts +8 -0
  75. package/dist/kernel/capability-registry.d.ts +30 -0
  76. package/dist/kernel/config-manager.d.ts +75 -129
  77. package/dist/kernel/index.d.ts +5 -2
  78. package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
  79. package/dist/kernel/settings-door-view.d.ts +24 -1
  80. package/dist/kernel/settings-read-error.d.ts +37 -0
  81. package/dist/kernel/settings-view-types.d.ts +105 -0
  82. package/dist/kernel/store-settings-view.d.ts +17 -0
  83. package/dist/kernel/system-settings-mirror.d.ts +34 -0
  84. package/dist/kernel/system-settings-policy.d.ts +47 -0
  85. package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DNIbyfTR.mjs} +1 -1
  86. package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-pzlrLUbA.js} +1 -1
  87. package/package.json +1 -1
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-RAl10EfG.js");
6
+ const require_dist = require("../../dist-Dcy0gVHu.js");
7
7
  const require_device_state_changed = require("../../device-state-changed-Bk7cvTnz.js");
8
8
  let zod = require("zod");
9
9
  //#region src/builtins/doorbell/trigger-engine.ts
@@ -201,11 +201,18 @@ var DoorbellTriggerEngine = class {
201
201
  * 2. The DERIVED view (`deviceManager.getBindings`) is no longer the sole
202
202
  * input. Before a negative is believed — and again on the ring path
203
203
  * before a press is destroyed — the DURABLE authority is consulted
204
- * directly ({@link readDurableWrapperBinding} over the `device-manager`
205
- * addon store's `deviceBindings` map). That read carries a discriminator
206
- * the derived view genuinely cannot offer: a store answering from an
207
- * empty snapshot (D44) has no `deviceBindings` key at all, while a
208
- * genuine unbind leaves the key present and merely omits the row.
204
+ * directly ({@link durableVerdictOf} over
205
+ * `deviceManager.getWrapperActivation`). That read carries a discriminator
206
+ * the derived view genuinely cannot offer: it is three-way, so a store
207
+ * that could not answer (`failed`) is never mistaken for a store that
208
+ * answered "nothing here" (`missing`).
209
+ *
210
+ * 2026-09-29 (D675). Until then the durable read was
211
+ * `addonSettingsRaw.getGlobal({ addonId: 'device-manager' })`, whose backend
212
+ * (`ConfigManager.getAddonConfig`) has had no store on the hub since D231
213
+ * isolated the engine: it answered `{}` on every call, so every durable read
214
+ * was `unreadable`, an operator's explicit "off" (camera 615,
215
+ * `wrapperAddonId: null`) was never honoured, and a press rang anyway.
209
216
  */
210
217
  /**
211
218
  * Fold one SUCCESSFUL bindings read into the mirror. A read that failed must
@@ -216,9 +223,12 @@ function nextBindingState(previous, boundNow) {
216
223
  if (boundNow) return "bound";
217
224
  return previous === "unbind-pending" || previous === "unbound" ? "unbound" : "unbind-pending";
218
225
  }
219
- /** May a ring proceed? Only a corroborated unbind stops one. */
226
+ /**
227
+ * May a ring proceed on the mirror STATE alone? Only a corroborated unbind
228
+ * stops one. {@link needsDurableRead} adds the served-elsewhere case on top.
229
+ */
220
230
  function allowsRing(state) {
221
- return !needsDurableRead(state);
231
+ return state !== "unbound";
222
232
  }
223
233
  /** True only when a read has positively confirmed the binding. */
224
234
  function hasConfirmedBinding(state) {
@@ -243,46 +253,62 @@ function entriesBindWrapper(entries, capName, ownAddonId) {
243
253
  function summariseEntries(entries) {
244
254
  return entries.map((entry) => `${entry.capName}:${entry.kind}:${entry.providerAddonId}`);
245
255
  }
246
- function isRecord$2(value) {
247
- return typeof value === "object" && value !== null && !Array.isArray(value);
248
- }
249
256
  /**
250
- * Read the DURABLE authority — the `device-manager` addon store's
251
- * `deviceBindings` map — for "is `ownAddonId` the active wrapper of
252
- * `capName` on `deviceId`".
257
+ * The durable verdict for `ownAddonId`, from device-manager's STORED
258
+ * activation of this cap on this device (`deviceManager.getWrapperActivation`).
253
259
  *
254
- * This is the input the derived `getBindings` view is not: it is the row the
255
- * operator's toggle actually writes, with no registry resolution, no
256
- * `defaultActive` auto-binding and no cross-node cap folding in between. It
257
- * also grades its own trustworthiness, which `getBindings` cannot: the
258
- * absence of the `deviceBindings` KEY is the signature of a store answering
259
- * from an empty snapshot, whereas a genuine unbind leaves the key present.
260
+ * - `value` naming this wrapper → `bound`.
261
+ * - `value` naming another wrapper, or `null` — the operator's explicit "off"
262
+ * marker (D62) — → `unbound`.
263
+ * - `missing` → `unbound`: the store ANSWERED and holds no activation, and the
264
+ * doorbell cap is `defaultActive: false`, so a wrapper nobody activated is
265
+ * not bound.
266
+ * - `failed` → `unreadable`: it carries no information and destroys nothing.
267
+ */
268
+ function durableVerdictOf(read, ownAddonId) {
269
+ switch (read.kind) {
270
+ case "failed": return "unreadable";
271
+ case "missing": return "unbound";
272
+ case "value": return read.wrapperAddonId !== null && require_dist.isSameAddonId(read.wrapperAddonId, ownAddonId) ? "bound" : "unbound";
273
+ }
274
+ }
275
+ /**
276
+ * Who, other than this wrapper, the derived view says serves `capName` —
277
+ * `composer` on a camera whose doorbell was migrated to a composition (D671).
278
+ * Non-empty means a press this wrapper does not ring is still somebody's ring.
260
279
  */
261
- function readDurableWrapperBinding(record, deviceId, capName, ownAddonId) {
262
- if (!isRecord$2(record)) return "unreadable";
263
- const deviceBindings = record["deviceBindings"];
264
- if (!isRecord$2(deviceBindings)) return "unreadable";
265
- const perDevice = deviceBindings[String(deviceId)];
266
- if (!isRecord$2(perDevice)) return "unbound";
267
- const activation = perDevice[capName];
268
- if (!isRecord$2(activation)) return "unbound";
269
- const wrapperAddonId = activation["wrapperAddonId"];
270
- if (typeof wrapperAddonId !== "string") return "unbound";
271
- return require_dist.isSameAddonId(wrapperAddonId, ownAddonId) ? "bound" : "unbound";
280
+ function otherProvidersOf(entries, capName, ownAddonId) {
281
+ return entries.filter((entry) => entry.capName === capName && !require_dist.isSameAddonId(entry.providerAddonId, ownAddonId)).map((entry) => entry.providerAddonId);
272
282
  }
273
- /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
274
- function needsDurableRead(state) {
275
- return state === "unbound";
283
+ /**
284
+ * Does this camera need the durable authority before a verdict?
285
+ *
286
+ * An `unbound` mirror does (D72). So does any mirror short of `bound` on a
287
+ * camera whose doorbell another provider serves — the D671 cutover, where
288
+ * erring open means ringing ON TOP of the composer: after every restart the
289
+ * mirror starts `undefined` and spends a reload cycle at `unbind-pending`, and
290
+ * each press in that window was a duplicate ring. Two independent reads
291
+ * agreeing — the derived view naming another provider, the store naming no
292
+ * wrapper — is the D49 "second read agrees"; an unreadable store still rings.
293
+ */
294
+ function needsDurableRead(mirror) {
295
+ if (!allowsRing(mirror.state)) return true;
296
+ return mirror.servedBy.length > 0 && mirror.state !== "bound";
276
297
  }
277
298
  /**
278
- * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
299
+ * THE decision — the ring gate (`fireDoorbell` / `ringsAfterDurableRead`)
279
300
  * and the Extensions card's activation both call it, so what the card says
280
301
  * and what a press does cannot disagree. `durable` is `null` when it was not
281
- * read, which {@link needsDurableRead} allows for every state but `unbound`.
302
+ * read, which {@link needsDurableRead} allows only where it says so.
282
303
  * Pure: it reads nothing and writes nothing.
283
304
  */
284
- function ringVerdict(state, durable) {
305
+ function ringVerdict(mirror, durable) {
306
+ const state = mirror.state;
285
307
  if (state === "bound") return "rings";
308
+ if (mirror.servedBy.length > 0) {
309
+ if (durable === "unbound") return "silent";
310
+ if (durable === "bound") return "rings";
311
+ }
286
312
  if (state === "unbind-pending") return "rings-pending";
287
313
  if (state === void 0) return "rings-unconfirmed";
288
314
  if (durable === "bound") return "rings";
@@ -853,13 +879,6 @@ function errorText(err) {
853
879
  }
854
880
  //#endregion
855
881
  //#region src/builtins/doorbell/virtual-doorbell.addon.ts
856
- /**
857
- * The addon whose store IS the durable authority on wrapper activations.
858
- * `deviceBindings[<deviceId>][<capName>].wrapperAddonId` is the row the
859
- * operator's bindings toggle writes; `deviceManager.getBindings` is a VIEW
860
- * derived from it plus the capability registry.
861
- */
862
- var DEVICE_MANAGER_ADDON_ID = "device-manager";
863
882
  /** Narrow a raw `doorbell` runtime-state slice to counters (defaults on absence). */
864
883
  function parseDoorbellSlice(slice) {
865
884
  const lastPressedAtRaw = slice?.["lastPressedAt"];
@@ -912,6 +931,24 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
912
931
  * `binding-mirror.ts` for why a single negative read is not believed.
913
932
  */
914
933
  bindingMirror = /* @__PURE__ */ new Map();
934
+ /**
935
+ * Per camera, the last NEGATIVE refresh this addon reported — its state,
936
+ * the authority's verdict and who else serves the doorbell. A refresh runs
937
+ * every reload cycle (about once a minute); the same answer repeated is not
938
+ * news, so it is reported once per change and forgotten on a positive read.
939
+ * Camera 615 logged the same WARN 607 times in 14 h after D671.
940
+ */
941
+ reportedNegative = /* @__PURE__ */ new Map();
942
+ /** Cameras whose last durable-authority read THREW — reported once per outage. */
943
+ durableReadFailing = /* @__PURE__ */ new Set();
944
+ /**
945
+ * Per camera, the OTHER providers of `doorbell` the last successful derived
946
+ * read named (the composer after D671). Written off the ring path with the
947
+ * mirror; read on it, to decide whether erring open would double a ring.
948
+ */
949
+ servedElsewhere = /* @__PURE__ */ new Map();
950
+ /** Cameras whose store-over-derived disagreement has been reported, until a derived read agrees. */
951
+ reportedStoreOverride = /* @__PURE__ */ new Set();
915
952
  constructor() {
916
953
  super({
917
954
  debounceMs: 2e3,
@@ -992,7 +1029,7 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
992
1029
  projection: "slim",
993
1030
  deviceIds: [...deviceIds]
994
1031
  })).map(toSlotDevice),
995
- readBindingMirror: (deviceId) => this.bindingMirror.get(deviceId),
1032
+ readBindingMirror: (deviceId) => this.mirrorRead(deviceId),
996
1033
  readDurableBinding: (deviceId) => this.readDurableAuthority(deviceId),
997
1034
  sourceCapableDeviceIds: () => this.ctx.api.deviceManager.listDeviceIdsByCaps.query({ capNames: [...require_dist.SOURCE_CAPS] }),
998
1035
  listFleet: async () => (await this.ctx.api.deviceManager.listAll.query({ projection: "slim" })).map(toSlotDevice)
@@ -1041,9 +1078,9 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1041
1078
  * (UI toast, notifier rules, exporters) is agnostic to the origin.
1042
1079
  *
1043
1080
  * The binding gate here is a pure in-memory read on the path that RINGS:
1044
- * an open mirror performs NO I/O and cannot fail. The mirror can only
1045
- * DESTROY a press through the one branch below, and that branch is not
1046
- * allowed to be the last word — see `ringsDespiteUnboundMirror`.
1081
+ * a `bound` mirror, or an open one on a camera nobody else serves, performs
1082
+ * NO I/O and cannot fail. Every other case asks the durable authority once
1083
+ * before deciding — see `needsDurableRead` and `ringsAfterDurableRead`.
1047
1084
  *
1048
1085
  * `sourceDeviceId`/`capName` are the source that produced this rise —
1049
1086
  * carried through purely so the terminal `pressed` line can name it. Every
@@ -1053,8 +1090,9 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1053
1090
  * timestamp against provider logs (proven live 2026-08-25, camera 615).
1054
1091
  */
1055
1092
  async fireDoorbell(cameraId, timestamp, sourceDeviceId, capName) {
1056
- const binding = this.bindingMirror.get(cameraId);
1057
- if (!allowsRing(binding) && !await this.ringsDespiteUnboundMirror(cameraId, binding)) return;
1093
+ const mirror = this.mirrorRead(cameraId);
1094
+ const binding = mirror.state;
1095
+ if (needsDurableRead(mirror) && !await this.ringsAfterDurableRead(cameraId, mirror)) return;
1058
1096
  if (!hasConfirmedBinding(binding)) this.ctx.logger.warn("virtual-doorbell: ringing on an unconfirmed binding — the bindings store has not corroborated an unbind, and a one-shot press is never dropped on an unproven read", {
1059
1097
  tags: { deviceId: cameraId },
1060
1098
  meta: { bindingState: binding ?? "never-read" }
@@ -1131,8 +1169,10 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1131
1169
  }
1132
1170
  }
1133
1171
  /**
1134
- * One read of the DURABLE authority: the `device-manager` addon store's
1135
- * `deviceBindings` map, straight from the settings DB.
1172
+ * One read of the DURABLE authority: the wrapper activation device-manager
1173
+ * STORED for this camera (`deviceManager.getWrapperActivation`, D675) — the
1174
+ * row the operator's bindings toggle writes. `deviceManager.getBindings` is
1175
+ * a VIEW derived from it plus the capability registry.
1136
1176
  *
1137
1177
  * This is deliberately NOT `getBindings`. The derived view folds the store
1138
1178
  * together with the capability registry, `defaultActive` auto-binding and
@@ -1141,17 +1181,35 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1141
1181
  * spelling mismatch) made it disagree *systematically* while looking
1142
1182
  * exactly like a genuine unbind. The raw row cannot: it is what the
1143
1183
  * bindings toggle wrote, and it grades its own trustworthiness (D44).
1184
+ *
1185
+ * A read that throws is reported when the outage STARTS and when it ends —
1186
+ * never per read: this runs on every reload cycle.
1144
1187
  */
1145
1188
  async readDurableAuthority(cameraId) {
1189
+ let read;
1146
1190
  try {
1147
- return readDurableWrapperBinding(await this.ctx.api.addonSettingsRaw.getGlobal.query({ addonId: DEVICE_MANAGER_ADDON_ID }), cameraId, require_dist.doorbellCapability.name, this.ownAddonId);
1148
- } catch (err) {
1149
- this.ctx.logger.warn("virtual-doorbell: the durable bindings store could not be read — it carries no information and revokes nothing", {
1150
- tags: { deviceId: cameraId },
1151
- meta: { error: require_dist.errMsg(err) }
1191
+ read = await this.ctx.api.deviceManager.getWrapperActivation.query({
1192
+ deviceId: cameraId,
1193
+ capName: require_dist.doorbellCapability.name
1152
1194
  });
1195
+ } catch (err) {
1196
+ read = {
1197
+ kind: "failed",
1198
+ error: require_dist.errMsg(err)
1199
+ };
1200
+ }
1201
+ if (read.kind === "failed") {
1202
+ if (!this.durableReadFailing.has(cameraId)) {
1203
+ this.durableReadFailing.add(cameraId);
1204
+ this.ctx.logger.warn("virtual-doorbell: the durable bindings store could not be read — it carries no information and revokes nothing (reported once until it answers again)", {
1205
+ tags: { deviceId: cameraId },
1206
+ meta: { error: read.error }
1207
+ });
1208
+ }
1153
1209
  return "unreadable";
1154
1210
  }
1211
+ if (this.durableReadFailing.delete(cameraId)) this.ctx.logger.info("virtual-doorbell: the durable bindings store answers again", { tags: { deviceId: cameraId } });
1212
+ return durableVerdictOf(read, this.ownAddonId);
1155
1213
  }
1156
1214
  /**
1157
1215
  * Reconcile `cameraId`'s mirror against the authority. Runs on the
@@ -1169,60 +1227,114 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1169
1227
  async refreshBindingMirror(cameraId) {
1170
1228
  const derived = await this.readDerivedBinding(cameraId);
1171
1229
  if (derived === null) return;
1230
+ const servedBy = otherProvidersOf(derived.entries, require_dist.doorbellCapability.name, this.ownAddonId);
1231
+ this.servedElsewhere.set(cameraId, servedBy);
1172
1232
  if (derived.bound) {
1173
1233
  this.bindingMirror.set(cameraId, nextBindingState(this.bindingMirror.get(cameraId), true));
1234
+ this.reportedNegative.delete(cameraId);
1235
+ this.reportedStoreOverride.delete(cameraId);
1174
1236
  return;
1175
1237
  }
1176
1238
  const authority = await this.readDurableAuthority(cameraId);
1177
1239
  const entries = summariseEntries(derived.entries);
1178
1240
  if (authority === "bound") {
1179
- this.ctx.logger.warn("virtual-doorbell: the derived binding list omits this wrapper but the durable bindings store still names this wrapper — trusting the store", {
1180
- tags: { deviceId: cameraId },
1181
- meta: {
1182
- ownAddonId: this.ownAddonId,
1183
- entries
1184
- }
1185
- });
1241
+ this.reportedNegative.delete(cameraId);
1242
+ if (!this.reportedStoreOverride.has(cameraId)) {
1243
+ this.reportedStoreOverride.add(cameraId);
1244
+ this.ctx.logger.warn("virtual-doorbell: the derived binding list omits this wrapper but the durable bindings store still names this wrapper — trusting the store (reported once until they agree)", {
1245
+ tags: { deviceId: cameraId },
1246
+ meta: {
1247
+ ownAddonId: this.ownAddonId,
1248
+ entries
1249
+ }
1250
+ });
1251
+ }
1186
1252
  this.bindingMirror.set(cameraId, nextBindingState(this.bindingMirror.get(cameraId), true));
1187
1253
  return;
1188
1254
  }
1255
+ this.reportedStoreOverride.delete(cameraId);
1189
1256
  const previous = this.bindingMirror.get(cameraId);
1190
1257
  const next = nextBindingState(previous, false);
1191
1258
  this.bindingMirror.set(cameraId, next);
1192
- this.ctx.logger.warn("virtual-doorbell: bindings read did not see this wrapper", {
1259
+ this.reportNegativeRefresh(cameraId, {
1260
+ from: previous ?? "never-read",
1261
+ to: next,
1262
+ authority,
1263
+ servedBy,
1264
+ entries
1265
+ });
1266
+ }
1267
+ /** The ring gate's in-memory input for one camera — no I/O. */
1268
+ mirrorRead(cameraId) {
1269
+ return {
1270
+ state: this.bindingMirror.get(cameraId),
1271
+ servedBy: this.servedElsewhere.get(cameraId) ?? []
1272
+ };
1273
+ }
1274
+ /**
1275
+ * Report a negative refresh ONCE per change of what it says (D391: a
1276
+ * repeated identical line is noise that buries the next real one).
1277
+ *
1278
+ * The level follows what the answer MEANS:
1279
+ * - the durable store could not grade it (`unreadable`) — WARN: the
1280
+ * camera's ring now depends on erring open, and nobody can say whether
1281
+ * that is right;
1282
+ * - the store CONFIRMS the wrapper is not bound — an operator's unbind or
1283
+ * the D671 cutover to a composition. That is a state, not an anomaly:
1284
+ * INFO when it is believed (`unbound`), DEBUG for the first,
1285
+ * not-yet-believed read.
1286
+ */
1287
+ reportNegativeRefresh(cameraId, report) {
1288
+ const key = `${report.to}|${report.authority}|${report.servedBy.join(",")}`;
1289
+ if (this.reportedNegative.get(cameraId) === key) return;
1290
+ this.reportedNegative.set(cameraId, key);
1291
+ const options = {
1193
1292
  tags: { deviceId: cameraId },
1194
1293
  meta: {
1195
- from: previous ?? "never-read",
1196
- to: next,
1294
+ from: report.from,
1295
+ to: report.to,
1197
1296
  ownAddonId: this.ownAddonId,
1198
- durableAuthority: authority,
1199
- entries
1297
+ durableAuthority: report.authority,
1298
+ doorbellServedBy: report.servedBy,
1299
+ entries: report.entries
1200
1300
  }
1201
- });
1301
+ };
1302
+ if (report.authority === "unreadable") {
1303
+ this.ctx.logger.warn("virtual-doorbell: bindings read did not see this wrapper and the durable store cannot confirm it — a press still rings", options);
1304
+ return;
1305
+ }
1306
+ const message = report.servedBy.length > 0 ? `virtual-doorbell: wrapper deactivated on this camera — its doorbell is served by ${report.servedBy.join(", ")}` : "virtual-doorbell: wrapper deactivated on this camera — the bindings store confirms it";
1307
+ if (report.to === "unbound") this.ctx.logger.info(message, options);
1308
+ else this.ctx.logger.debug(message, options);
1202
1309
  }
1203
1310
  /**
1204
- * The mirror says `unbound` and a real press just arrived. Decide, with
1205
- * ONE read of the durable authority, whether to destroy it.
1311
+ * A real press arrived on a camera the in-memory gate cannot clear on its
1312
+ * own: the mirror is `unbound`, or it is short of `bound` while another
1313
+ * provider serves the doorbell. Decide, with ONE read of the durable
1314
+ * authority, whether this wrapper rings.
1206
1315
  *
1207
1316
  * D49 forbade I/O on the ring path because the read it would have made was
1208
1317
  * the same fallible derived read that caused the problem. This read is
1209
1318
  * different in both directions: it is the authority rather than a view of
1210
- * it, and it is paid ONLY on the branch that would otherwise drop a press —
1211
- * a ring is rare, one query is cheap, and a dropped ring has no retry, no
1212
- * reconcile and nothing downstream that notices.
1319
+ * it, and it is paid ONLY where erring open could be wrong — a dropped ring
1320
+ * (an `unbound` mirror) or a DOUBLED one (another provider rings this
1321
+ * camera). A ring is rare and one query is cheap. An unreadable store still
1322
+ * rings: it carries no information.
1213
1323
  *
1214
1324
  * Returns `true` when the ring must proceed.
1215
1325
  */
1216
- async ringsDespiteUnboundMirror(cameraId, binding) {
1326
+ async ringsAfterDurableRead(cameraId, mirror) {
1327
+ const binding = mirror.state;
1217
1328
  const authority = await this.readDurableAuthority(cameraId);
1218
- const verdict = ringVerdict(binding, authority);
1329
+ const verdict = ringVerdict(mirror, authority);
1219
1330
  switch (verdict) {
1220
1331
  case "rings":
1221
1332
  this.bindingMirror.set(cameraId, "bound");
1222
- this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror said unbound but the durable bindings store still names this wrapper; the mirror has been repaired", {
1333
+ this.ctx.logger.warn("virtual-doorbell: ring RESCUED — the mirror did not confirm this wrapper but the durable bindings store still names it; the mirror has been repaired", {
1223
1334
  tags: { deviceId: cameraId },
1224
1335
  meta: {
1225
- bindingState: binding,
1336
+ bindingState: binding ?? "never-read",
1337
+ doorbellServedBy: mirror.servedBy,
1226
1338
  ownAddonId: this.ownAddonId
1227
1339
  }
1228
1340
  });
@@ -1238,19 +1350,32 @@ var VirtualDoorbellAddon = class extends require_dist.BaseAddon {
1238
1350
  return verdictRings(verdict);
1239
1351
  case "silent": {
1240
1352
  const derived = await this.readDerivedBinding(cameraId);
1241
- this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", {
1353
+ const servedBy = derived === null ? mirror.servedBy : otherProvidersOf(derived.entries, require_dist.doorbellCapability.name, this.ownAddonId);
1354
+ const options = {
1242
1355
  tags: { deviceId: cameraId },
1243
1356
  meta: {
1244
1357
  bindingState: binding,
1245
1358
  ownAddonId: this.ownAddonId,
1246
1359
  durableAuthority: authority,
1360
+ doorbellServedBy: servedBy,
1247
1361
  entries: derived === null ? "unreadable" : summariseEntries(derived.entries)
1248
1362
  }
1249
- });
1363
+ };
1364
+ if (servedBy.length > 0) this.ctx.logger.info(`virtual-doorbell: press left to ${servedBy.join(", ")} — this wrapper is deactivated on this camera`, options);
1365
+ else this.ctx.logger.warn("virtual-doorbell: ring DROPPED — the wrapper is not bound to this camera, but a trigger source is still configured for it", options);
1250
1366
  return verdictRings(verdict);
1251
1367
  }
1252
1368
  case "rings-pending":
1253
- case "rings-unconfirmed": return verdictRings(verdict);
1369
+ case "rings-unconfirmed":
1370
+ this.ctx.logger.warn(`virtual-doorbell: ringing although ${mirror.servedBy.join(", ")} serves this doorbell — the durable bindings store could not confirm this wrapper was deactivated`, {
1371
+ tags: { deviceId: cameraId },
1372
+ meta: {
1373
+ bindingState: binding ?? "never-read",
1374
+ durableAuthority: authority,
1375
+ ownAddonId: this.ownAddonId
1376
+ }
1377
+ });
1378
+ return verdictRings(verdict);
1254
1379
  }
1255
1380
  }
1256
1381
  /**