@camstack/system 1.2.315 → 1.2.317

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-yonsRneP.mjs → composition-sources-DVClu006.mjs} +1 -1
  69. package/dist/{composition-sources-DYqQLRzF.js → composition-sources-Dep7nBwu.js} +1 -1
  70. package/dist/{dist-MzYJVCLE.mjs → dist-Dori4pf3.mjs} +99 -11
  71. package/dist/{dist-DVKv5i-e.js → dist-a6p0V-IL.js} +99 -11
  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-iY5I0nKM.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
  86. package/dist/{retired-settings-keys-8yY2sTc-.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
  87. package/package.json +1 -1
@@ -4,6 +4,16 @@ import { MigrationRefusedError } from './migration-refused.js';
4
4
  import { RuntimeStatePolicyLookup } from './runtime-state-persist-gate.js';
5
5
  /** A device's row as it may reach disk: cap name → persistable slice (plus `$owned`). */
6
6
  export type PersistableRow = Record<string, Record<string, unknown>>;
7
+ /** Which path wrote (or tried to write) a row — named on every line it logs. */
8
+ export type RowWritePath = 'debounced' | 'shutdown-flush' | 'drain' | 'claim' | 'release';
9
+ /**
10
+ * A write-through refused because the device's row could not be read first
11
+ * (D677). Nothing was written; the caller rolls its memory change back.
12
+ */
13
+ export declare class RuntimeStateRowUnreadError extends Error {
14
+ readonly deviceId: number;
15
+ constructor(deviceId: number);
16
+ }
7
17
  /**
8
18
  * The drain before a migration could not vouch for a device's row. Nothing has
9
19
  * moved yet, so it is a {@link MigrationRefusedError}: the migration refuses
@@ -25,10 +35,21 @@ export interface MirrorRowWriterDeps {
25
35
  readonly policyFor: RuntimeStatePolicyLookup;
26
36
  /** The device's row as memory holds it NOW — taken when a write RUNS. */
27
37
  readonly persistableFor: (deviceId: number) => PersistableRow;
38
+ /** The device's row was read into memory in this process (D677). */
39
+ readonly isRowRead: (deviceId: number) => boolean;
40
+ /**
41
+ * Read the row into memory — single-flight, `fill` (memory wins per cap).
42
+ * Rejects when the row cannot be read; memory is then unchanged.
43
+ */
44
+ readonly readRowFirst: (deviceId: number, settings: DeviceManagerSettings) => Promise<void>;
28
45
  }
29
46
  export declare class MirrorRowWriter {
30
47
  private readonly deps;
31
48
  static readonly RUNTIME_STATE_DEBOUNCE_MS = 1000;
49
+ /** Ceiling of the re-arm backoff while a device's row cannot be read (D677, D3). */
50
+ static readonly UNREAD_REARM_MAX_MS = 60000;
51
+ /** The re-arm delay after `refusals` consecutive unread refusals: 1 s, 2 s, 4 s … capped at 60 s. */
52
+ static unreadRearmDelayMs(refusals: number): number;
32
53
  /**
33
54
  * Per-device disk-write debouncer for runtime-state. `setCapSlice`
34
55
  * updates the in-memory mirror synchronously and emits the change
@@ -57,6 +78,8 @@ export declare class MirrorRowWriter {
57
78
  * that drops work silently reads as "never happened".
58
79
  */
59
80
  private readonly skippedSinceWrite;
81
+ /** Devices whose unreadable-row refusal is already logged — once per episode. */
82
+ private readonly rowUnreadLogged;
60
83
  constructor(deps: MirrorRowWriterDeps);
61
84
  /** What is believed to be on the device's row; undefined before any read or write. */
62
85
  persisted(deviceId: number): PersistableRow | undefined;
@@ -92,8 +115,43 @@ export declare class MirrorRowWriter {
92
115
  schedule(deviceId: number, settings: DeviceManagerSettings, changedCap: string): void;
93
116
  /** Arm the debounce timer; at fire, queue a write of the live blob unless disk already has it. */
94
117
  private armDiskWrite;
95
- /** The debounced write itself: the blob as memory holds it NOW, behind the effective-change gate. Never rejects. */
118
+ /**
119
+ * Forget a removed device: cancel its armed write, await one in flight, and
120
+ * drop its baseline and counters — nothing writes its row after this.
121
+ */
122
+ forget(deviceId: number): Promise<void>;
123
+ /**
124
+ * The row is in memory, or is read now (D677). False when the read failed:
125
+ * the caller writes nothing. Logged once per episode, tagged; the episode
126
+ * ends at the next successful read.
127
+ */
128
+ private rowReadOrRefuse;
129
+ /**
130
+ * The ONE call of `writeDeviceRuntimeState`. A blob that lacks a cap the
131
+ * stored row holds is written (memory is the authority once the row was
132
+ * read) but never silently: the dropped caps are named at warn (D677).
133
+ * `$owned` is excluded — a release that empties it is the claim path's own
134
+ * logged business.
135
+ */
136
+ private writeRow;
137
+ /**
138
+ * A claim's write-through (D663): queued behind the device's writes, the row
139
+ * read first, the blob taken when it runs. Rejects with
140
+ * {@link RuntimeStateRowUnreadError} when the row cannot be read, or with the
141
+ * write's own error; resolves to what landed.
142
+ */
143
+ writeThroughRow(deviceId: number, settings: DeviceManagerSettings, path: RowWritePath): Promise<PersistableRow>;
144
+ /**
145
+ * The debounced write itself: the row read first (D677), then the blob as
146
+ * memory holds it NOW, behind the effective-change gate. Never rejects. An
147
+ * unreadable row writes nothing; the debounced path re-arms with a backoff
148
+ * (1 s doubling to 60 s — never a 1 Hz loop, D3), and the armed timer is
149
+ * what a migration drain sees as owed, so a row that has become readable
150
+ * drains normally. The shutdown flush does not re-arm.
151
+ */
96
152
  private writeLiveBlob;
153
+ /** Re-arm an unread refusal with its backoff, or remember it for `release` while held. */
154
+ private rearmAfterUnread;
97
155
  /**
98
156
  * Hold the debounced writer off these devices' rows while a migration swaps
99
157
  * them (D663 re-review N-6). Resolves once each device is DRAINED: a write
@@ -1,68 +1,4 @@
1
- /**
2
- * The virtual-doorbell wrapper's in-memory mirror of ONE durable fact per
3
- * camera: is this wrapper still the active `doorbell` binding for it?
4
- *
5
- * WHY A MIRROR AND NOT A READ. A doorbell press is a one-shot event: there
6
- * is no retry, no reconcile, nothing downstream that can notice it went
7
- * missing. The wrapper used to re-derive the binding on the ring path via
8
- * `deviceManager.getBindings` and treat ANY answer that omitted its own
9
- * activation as "the operator unbound this". On 2026-08-04/05 camera 615
10
- * rang zero times out of three real presses: the binding was persisted (a
11
- * same-day backup taken before the presses proves it, and the same process
12
- * later returned it correctly to a tRPC query), but the store answered
13
- * transiently without it — `getBindings` reads the device-manager addon
14
- * store, which is exactly the surface that can silently answer `{}` (D44,
15
- * `addon-registry.service.ts`). Three presses, three drops, at `debug`.
16
- *
17
- * WHY CORROBORATION AND NOT A BETTER DISCRIMINATOR. There isn't one. A
18
- * genuinely unbound pure-wrapper cap and a store answering from an empty
19
- * snapshot produce the SAME `getBindings` response — no `doorbell` entry,
20
- * with the camera's native entries (snapshot, motion, …) still present in
21
- * both cases because those come from the capability registry, not the
22
- * store. The response cannot be graded. What CAN be graded is repetition: a
23
- * real unbind is permanent and every later read agrees, a store blip does
24
- * not survive the next read. So a negative read ARMS the revocation and a
25
- * second consecutive negative applies it.
26
- *
27
- * ERRING. `undefined` (never read) and `unbind-pending` both ring. The cost
28
- * of erring open is a spurious ring on a camera whose wrapper is not bound
29
- * but which has trigger sources, for at most two reload cycles. The cost of
30
- * erring closed is the press.
31
- *
32
- * D655: a configured source is NO LONGER evidence of the binding. D49 leaned
33
- * on "a source can only be configured through this wrapper's own
34
- * binding-gated settings surface"; that surface is retired, and the
35
- * `doorbell-source` extension slot that replaced it is offered on every
36
- * camera, bound or not (D648). The erring-open window above is therefore
37
- * reachable by configuring sources on a never-bound camera, not only by
38
- * unbinding a configured one — still bounded by two reload cycles, and the
39
- * slot's card says `inactive` once the reads agree.
40
- *
41
- * ─────────────────────────────────────────────────────────────────────
42
- * 2026-08-07 (D72). The corroboration above was DEFEATED, and the reason
43
- * matters more than the fix. `refreshBindingMirror` matched the binding with
44
- * `entry.providerAddonId === this.ctx.id`. The two addon-context factories do
45
- * not spell `ctx.id` the same way: a forked addon gets `virtual-doorbell`, a
46
- * hub-main BUILTIN gets `addon:virtual-doorbell`. Every entry on the wire
47
- * carries the bare form. So for this builtin the comparison was false for
48
- * every entry, forever — not a blip. Corroboration only defends against a
49
- * TRANSIENT bad read; two systematically-wrong reads agree with each other,
50
- * and the mirror latched `unbound` with no way back except an operator
51
- * opening the settings page. A real press on camera 615 was dropped.
52
- *
53
- * Two things follow, and both are now load-bearing here:
54
- *
55
- * 1. The comparison goes through `isSameAddonId` ({@link entriesBindWrapper}),
56
- * never a raw `===`.
57
- * 2. The DERIVED view (`deviceManager.getBindings`) is no longer the sole
58
- * input. Before a negative is believed — and again on the ring path
59
- * before a press is destroyed — the DURABLE authority is consulted
60
- * directly ({@link readDurableWrapperBinding} over the `device-manager`
61
- * addon store's `deviceBindings` map). That read carries a discriminator
62
- * the derived view genuinely cannot offer: a store answering from an
63
- * empty snapshot (D44) has no `deviceBindings` key at all, while a
64
- * genuine unbind leaves the key present and merely omits the row.
65
- */
1
+ import { WrapperActivationRead } from '@camstack/types';
66
2
  /**
67
3
  * `bound` — a read saw the wrapper's activation.
68
4
  * `unbind-pending` — one read did not; NOT yet believed, still rings.
@@ -75,7 +11,10 @@ export type DoorbellBindingState = 'bound' | 'unbind-pending' | 'unbound';
75
11
  * and must leave the mirror exactly as it was.
76
12
  */
77
13
  export declare function nextBindingState(previous: DoorbellBindingState | undefined, boundNow: boolean): DoorbellBindingState;
78
- /** May a ring proceed? Only a corroborated unbind stops one. */
14
+ /**
15
+ * May a ring proceed on the mirror STATE alone? Only a corroborated unbind
16
+ * stops one. {@link needsDurableRead} adds the served-elsewhere case on top.
17
+ */
79
18
  export declare function allowsRing(state: DoorbellBindingState | undefined): boolean;
80
19
  /** True only when a read has positively confirmed the binding. */
81
20
  export declare function hasConfirmedBinding(state: DoorbellBindingState | undefined): boolean;
@@ -108,18 +47,24 @@ export declare function summariseEntries(entries: ReadonlyArray<BindingEntryView
108
47
  */
109
48
  export type DurableBindingVerdict = 'bound' | 'unbound' | 'unreadable';
110
49
  /**
111
- * Read the DURABLE authority — the `device-manager` addon store's
112
- * `deviceBindings` map — for "is `ownAddonId` the active wrapper of
113
- * `capName` on `deviceId`".
50
+ * The durable verdict for `ownAddonId`, from device-manager's STORED
51
+ * activation of this cap on this device (`deviceManager.getWrapperActivation`).
114
52
  *
115
- * This is the input the derived `getBindings` view is not: it is the row the
116
- * operator's toggle actually writes, with no registry resolution, no
117
- * `defaultActive` auto-binding and no cross-node cap folding in between. It
118
- * also grades its own trustworthiness, which `getBindings` cannot: the
119
- * absence of the `deviceBindings` KEY is the signature of a store answering
120
- * from an empty snapshot, whereas a genuine unbind leaves the key present.
53
+ * - `value` naming this wrapper → `bound`.
54
+ * - `value` naming another wrapper, or `null` — the operator's explicit "off"
55
+ * marker (D62) — → `unbound`.
56
+ * - `missing` → `unbound`: the store ANSWERED and holds no activation, and the
57
+ * doorbell cap is `defaultActive: false`, so a wrapper nobody activated is
58
+ * not bound.
59
+ * - `failed` → `unreadable`: it carries no information and destroys nothing.
60
+ */
61
+ export declare function durableVerdictOf(read: WrapperActivationRead, ownAddonId: string): DurableBindingVerdict;
62
+ /**
63
+ * Who, other than this wrapper, the derived view says serves `capName` —
64
+ * `composer` on a camera whose doorbell was migrated to a composition (D671).
65
+ * Non-empty means a press this wrapper does not ring is still somebody's ring.
121
66
  */
122
- export declare function readDurableWrapperBinding(record: unknown, deviceId: number, capName: string, ownAddonId: string): DurableBindingVerdict;
67
+ export declare function otherProvidersOf(entries: ReadonlyArray<BindingEntryView>, capName: string, ownAddonId: string): string[];
123
68
  /**
124
69
  * What a press on this camera would do right now.
125
70
  *
@@ -132,15 +77,34 @@ export declare function readDurableWrapperBinding(record: unknown, deviceId: num
132
77
  * store agrees with an unbound mirror: the only verdict that drops a press.
133
78
  */
134
79
  export type RingVerdict = 'rings' | 'rings-pending' | 'rings-unconfirmed' | 'unknown' | 'silent';
135
- /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
136
- export declare function needsDurableRead(state: DoorbellBindingState | undefined): boolean;
137
80
  /**
138
- * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
81
+ * What the ring gate and the card know about one camera without I/O: the
82
+ * mirror's state, and which OTHER providers the last successful bindings read
83
+ * said serve `doorbell` there (empty when none, or when no read has answered).
84
+ */
85
+ export interface DoorbellMirrorRead {
86
+ readonly state: DoorbellBindingState | undefined;
87
+ readonly servedBy: readonly string[];
88
+ }
89
+ /**
90
+ * Does this camera need the durable authority before a verdict?
91
+ *
92
+ * An `unbound` mirror does (D72). So does any mirror short of `bound` on a
93
+ * camera whose doorbell another provider serves — the D671 cutover, where
94
+ * erring open means ringing ON TOP of the composer: after every restart the
95
+ * mirror starts `undefined` and spends a reload cycle at `unbind-pending`, and
96
+ * each press in that window was a duplicate ring. Two independent reads
97
+ * agreeing — the derived view naming another provider, the store naming no
98
+ * wrapper — is the D49 "second read agrees"; an unreadable store still rings.
99
+ */
100
+ export declare function needsDurableRead(mirror: DoorbellMirrorRead): boolean;
101
+ /**
102
+ * THE decision — the ring gate (`fireDoorbell` / `ringsAfterDurableRead`)
139
103
  * and the Extensions card's activation both call it, so what the card says
140
104
  * and what a press does cannot disagree. `durable` is `null` when it was not
141
- * read, which {@link needsDurableRead} allows for every state but `unbound`.
105
+ * read, which {@link needsDurableRead} allows only where it says so.
142
106
  * Pure: it reads nothing and writes nothing.
143
107
  */
144
- export declare function ringVerdict(state: DoorbellBindingState | undefined, durable: DurableBindingVerdict | null): RingVerdict;
108
+ export declare function ringVerdict(mirror: DoorbellMirrorRead, durable: DurableBindingVerdict | null): RingVerdict;
145
109
  /** Whether a press proceeds under this verdict. */
146
110
  export declare function verdictRings(verdict: RingVerdict): boolean;
@@ -1,5 +1,5 @@
1
1
  import { ExtensionSlotActivation, IDeviceExtensionProvider, IScopedLogger } from '@camstack/types';
2
- import { DoorbellBindingState, DurableBindingVerdict, RingVerdict } from './binding-mirror.js';
2
+ import { DoorbellMirrorRead, DurableBindingVerdict, RingVerdict } from './binding-mirror.js';
3
3
  import { DoorbellSource } from './doorbell-settings.js';
4
4
  /** The routing prefix of this owner's `slotRef`s. */
5
5
  export declare const VIRTUAL_DOORBELL_EXTENSION_SOURCE = "virtual-doorbell";
@@ -27,7 +27,7 @@ export interface DoorbellSlotDeps {
27
27
  /** Several devices in ONE read. Ids no device has are simply absent. */
28
28
  readonly listDevices: (deviceIds: readonly number[]) => Promise<readonly DoorbellSlotDevice[]>;
29
29
  /** The D49 mirror's verdict for this camera, or undefined when it has none. */
30
- readonly readBindingMirror: (deviceId: number) => DoorbellBindingState | undefined;
30
+ readonly readBindingMirror: (deviceId: number) => DoorbellMirrorRead;
31
31
  /**
32
32
  * The DURABLE authority, read-only — the same read the ring gate makes
33
33
  * before it drops a press. Never throws: a store that cannot answer is
@@ -47,7 +47,8 @@ export declare function activationFromVerdict(verdict: RingVerdict): ExtensionSl
47
47
  * never writes it: the mirror gates the ring, and a view that could move it
48
48
  * would let a page refetch during a store blip silence a doorbell. The only
49
49
  * read the card may make is the durable authority, and only where the ring
50
- * gate would make it too (`needsDurableRead`: an `unbound` mirror). A camera
50
+ * gate would make it too (`needsDurableRead`: an `unbound` mirror, or one
51
+ * short of `bound` whose doorbell another provider serves). A camera
51
52
  * the mirror has never seen costs nothing — the ring path errs open there and
52
53
  * so does the card.
53
54
  */
@@ -53,6 +53,24 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
53
53
  * `binding-mirror.ts` for why a single negative read is not believed.
54
54
  */
55
55
  private readonly bindingMirror;
56
+ /**
57
+ * Per camera, the last NEGATIVE refresh this addon reported — its state,
58
+ * the authority's verdict and who else serves the doorbell. A refresh runs
59
+ * every reload cycle (about once a minute); the same answer repeated is not
60
+ * news, so it is reported once per change and forgotten on a positive read.
61
+ * Camera 615 logged the same WARN 607 times in 14 h after D671.
62
+ */
63
+ private readonly reportedNegative;
64
+ /** Cameras whose last durable-authority read THREW — reported once per outage. */
65
+ private readonly durableReadFailing;
66
+ /**
67
+ * Per camera, the OTHER providers of `doorbell` the last successful derived
68
+ * read named (the composer after D671). Written off the ring path with the
69
+ * mirror; read on it, to decide whether erring open would double a ring.
70
+ */
71
+ private readonly servedElsewhere;
72
+ /** Cameras whose store-over-derived disagreement has been reported, until a derived read agrees. */
73
+ private readonly reportedStoreOverride;
56
74
  constructor();
57
75
  protected onInitialize(): Promise<ProviderRegistration[]>;
58
76
  protected onShutdown(): Promise<void>;
@@ -76,9 +94,9 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
76
94
  * (UI toast, notifier rules, exporters) is agnostic to the origin.
77
95
  *
78
96
  * The binding gate here is a pure in-memory read on the path that RINGS:
79
- * an open mirror performs NO I/O and cannot fail. The mirror can only
80
- * DESTROY a press through the one branch below, and that branch is not
81
- * allowed to be the last word — see `ringsDespiteUnboundMirror`.
97
+ * a `bound` mirror, or an open one on a camera nobody else serves, performs
98
+ * NO I/O and cannot fail. Every other case asks the durable authority once
99
+ * before deciding — see `needsDurableRead` and `ringsAfterDurableRead`.
82
100
  *
83
101
  * `sourceDeviceId`/`capName` are the source that produced this rise —
84
102
  * carried through purely so the terminal `pressed` line can name it. Every
@@ -104,8 +122,10 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
104
122
  */
105
123
  private readDerivedBinding;
106
124
  /**
107
- * One read of the DURABLE authority: the `device-manager` addon store's
108
- * `deviceBindings` map, straight from the settings DB.
125
+ * One read of the DURABLE authority: the wrapper activation device-manager
126
+ * STORED for this camera (`deviceManager.getWrapperActivation`, D675) — the
127
+ * row the operator's bindings toggle writes. `deviceManager.getBindings` is
128
+ * a VIEW derived from it plus the capability registry.
109
129
  *
110
130
  * This is deliberately NOT `getBindings`. The derived view folds the store
111
131
  * together with the capability registry, `defaultActive` auto-binding and
@@ -114,6 +134,9 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
114
134
  * spelling mismatch) made it disagree *systematically* while looking
115
135
  * exactly like a genuine unbind. The raw row cannot: it is what the
116
136
  * bindings toggle wrote, and it grades its own trustworthiness (D44).
137
+ *
138
+ * A read that throws is reported when the outage STARTS and when it ends —
139
+ * never per read: this runs on every reload cycle.
117
140
  */
118
141
  private readDurableAuthority;
119
142
  /**
@@ -130,20 +153,39 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
130
153
  * second consecutive one (D49).
131
154
  */
132
155
  private refreshBindingMirror;
156
+ /** The ring gate's in-memory input for one camera — no I/O. */
157
+ private mirrorRead;
158
+ /**
159
+ * Report a negative refresh ONCE per change of what it says (D391: a
160
+ * repeated identical line is noise that buries the next real one).
161
+ *
162
+ * The level follows what the answer MEANS:
163
+ * - the durable store could not grade it (`unreadable`) — WARN: the
164
+ * camera's ring now depends on erring open, and nobody can say whether
165
+ * that is right;
166
+ * - the store CONFIRMS the wrapper is not bound — an operator's unbind or
167
+ * the D671 cutover to a composition. That is a state, not an anomaly:
168
+ * INFO when it is believed (`unbound`), DEBUG for the first,
169
+ * not-yet-believed read.
170
+ */
171
+ private reportNegativeRefresh;
133
172
  /**
134
- * The mirror says `unbound` and a real press just arrived. Decide, with
135
- * ONE read of the durable authority, whether to destroy it.
173
+ * A real press arrived on a camera the in-memory gate cannot clear on its
174
+ * own: the mirror is `unbound`, or it is short of `bound` while another
175
+ * provider serves the doorbell. Decide, with ONE read of the durable
176
+ * authority, whether this wrapper rings.
136
177
  *
137
178
  * D49 forbade I/O on the ring path because the read it would have made was
138
179
  * the same fallible derived read that caused the problem. This read is
139
180
  * different in both directions: it is the authority rather than a view of
140
- * it, and it is paid ONLY on the branch that would otherwise drop a press —
141
- * a ring is rare, one query is cheap, and a dropped ring has no retry, no
142
- * reconcile and nothing downstream that notices.
181
+ * it, and it is paid ONLY where erring open could be wrong — a dropped ring
182
+ * (an `unbound` mirror) or a DOUBLED one (another provider rings this
183
+ * camera). A ring is rare and one query is cheap. An unreadable store still
184
+ * rings: it carries no information.
143
185
  *
144
186
  * Returns `true` when the ring must proceed.
145
187
  */
146
- private ringsDespiteUnboundMirror;
188
+ private ringsAfterDurableRead;
147
189
  /**
148
190
  * Lazily (re)build the camera→source assignment set from every
149
191
  * camera's device store. TTL-bounded so devices/settings appearing