@camstack/system 1.2.316 → 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-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
  69. package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
  71. package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
  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-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
  86. package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
  87. package/package.json +1 -1
@@ -3,8 +3,8 @@ 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");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-D3rGTGq5.js");
6
+ const require_dist = require("../../dist-a6p0V-IL.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-DC1uHWPZ.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
10
10
  let _camstack_types_node = require("@camstack/types/node");
@@ -4141,6 +4141,73 @@ async function setWrapperActive(deps, input) {
4141
4141
  }
4142
4142
  });
4143
4143
  }
4144
+ function isRecord(value) {
4145
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4146
+ }
4147
+ /** The key of this addon's store that holds every wrapper activation. */
4148
+ var DEVICE_BINDINGS_KEY = "deviceBindings";
4149
+ /**
4150
+ * The STORED activation for one `(device, capName)` pair, three-way — the
4151
+ * provider behind `deviceManager.getWrapperActivation` (D675).
4152
+ *
4153
+ * Reads ONE row — this addon's `deviceBindings` key (~1.6 KB live) — through
4154
+ * the settings view's single-key three-way read, never the addon's whole store
4155
+ * (~625 KB) and never `readAddonStore()`, which folds a failed read into `{}`.
4156
+ *
4157
+ * Absence is graded by LEVEL:
4158
+ * - STORE level — the read failed, or the `deviceBindings` row does not exist,
4159
+ * or it is not a map — is `failed`. Every hub that has ever bound anything
4160
+ * holds that row, so its absence is the signature of a store answering from
4161
+ * an empty or unready snapshot (D44), and it must never silence a press
4162
+ * (D49). An install that has never bound anything lands here too; erring
4163
+ * `failed` there costs nothing, because nothing is bound to silence.
4164
+ * - PAIR level — the row was read and has no entry for this device, or none
4165
+ * for this cap, or none with `wrapperAddonId` — is `missing`: the operator
4166
+ * never touched this pair.
4167
+ */
4168
+ async function readWrapperActivation(deps, query) {
4169
+ const settings = deps.ctx.settings;
4170
+ if (settings?.readAddonStoreKeyResult === void 0) return {
4171
+ kind: "failed",
4172
+ error: "this settings view has no single-key three-way read"
4173
+ };
4174
+ const read = await settings.readAddonStoreKeyResult(DEVICE_BINDINGS_KEY);
4175
+ if (read.kind === "unreadable") return {
4176
+ kind: "failed",
4177
+ error: read.error ?? read.reason
4178
+ };
4179
+ if (read.kind === "absent") return {
4180
+ kind: "failed",
4181
+ error: "the store holds no deviceBindings row — an empty snapshot is not evidence of an unbind"
4182
+ };
4183
+ const deviceBindings = read.value;
4184
+ if (!isRecord(deviceBindings)) return {
4185
+ kind: "failed",
4186
+ error: "stored deviceBindings is not a map"
4187
+ };
4188
+ const perDevice = deviceBindings[String(query.deviceId)];
4189
+ if (perDevice === void 0) return { kind: "missing" };
4190
+ if (!isRecord(perDevice)) return {
4191
+ kind: "failed",
4192
+ error: `stored bindings of device ${query.deviceId} are not a map`
4193
+ };
4194
+ const activation = perDevice[query.capName];
4195
+ if (activation === void 0) return { kind: "missing" };
4196
+ if (!isRecord(activation)) return {
4197
+ kind: "failed",
4198
+ error: `stored "${query.capName}" activation is not a record`
4199
+ };
4200
+ const wrapperAddonId = activation["wrapperAddonId"];
4201
+ if (wrapperAddonId === void 0) return { kind: "missing" };
4202
+ if (wrapperAddonId === null || typeof wrapperAddonId === "string") return {
4203
+ kind: "value",
4204
+ wrapperAddonId
4205
+ };
4206
+ return {
4207
+ kind: "failed",
4208
+ error: `stored "${query.capName}" wrapperAddonId is not a string`
4209
+ };
4210
+ }
4144
4211
  //#endregion
4145
4212
  //#region src/builtins/device-manager/device-event-propagator.ts
4146
4213
  /**
@@ -5545,6 +5612,7 @@ async function removeDevice(pctx, input) {
5545
5612
  });
5546
5613
  });
5547
5614
  await pctx.settings.clearDeviceStore(deviceId);
5615
+ await pctx.host.forgetMirrorDevice(deviceId);
5548
5616
  await pctx.settings.clearDeviceRuntimeState(deviceId);
5549
5617
  await pctx.host.fieldClaims.pruneDevice(deviceId, pctx.settings);
5550
5618
  const bindingKey = String(deviceId);
@@ -8066,6 +8134,20 @@ function deepEqual(a, b) {
8066
8134
  }
8067
8135
  //#endregion
8068
8136
  //#region src/builtins/device-manager/mirror-row-writer.ts
8137
+ /**
8138
+ * A write-through refused because the device's row could not be read first
8139
+ * (D677). Nothing was written; the caller rolls its memory change back.
8140
+ */
8141
+ var RuntimeStateRowUnreadError = class extends Error {
8142
+ deviceId;
8143
+ constructor(deviceId) {
8144
+ super(`runtime-state row of device ${deviceId} could not be read — nothing written, a row this process has not read is never replaced (D677)`);
8145
+ this.deviceId = deviceId;
8146
+ this.name = "RuntimeStateRowUnreadError";
8147
+ }
8148
+ };
8149
+ var ROW_UNREAD_REFUSAL_MESSAGE = "runtime-state row write refused — the row could not be read first, nothing written (D677)";
8150
+ var ROW_DROPS_CAPS_MESSAGE = "runtime-state row write DROPS caps the stored row holds (D677)";
8069
8151
  function errorMessage$2(err) {
8070
8152
  return err instanceof Error ? err.message : String(err);
8071
8153
  }
@@ -8085,6 +8167,13 @@ var RuntimeStateDrainError = class extends MigrationRefusedError {
8085
8167
  var MirrorRowWriter = class MirrorRowWriter {
8086
8168
  deps;
8087
8169
  static RUNTIME_STATE_DEBOUNCE_MS = 1e3;
8170
+ /** Ceiling of the re-arm backoff while a device's row cannot be read (D677, D3). */
8171
+ static UNREAD_REARM_MAX_MS = 6e4;
8172
+ /** The re-arm delay after `refusals` consecutive unread refusals: 1 s, 2 s, 4 s … capped at 60 s. */
8173
+ static unreadRearmDelayMs(refusals) {
8174
+ const exponent = Math.max(0, refusals - 1);
8175
+ return Math.min(MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS * 2 ** exponent, MirrorRowWriter.UNREAD_REARM_MAX_MS);
8176
+ }
8088
8177
  /**
8089
8178
  * Per-device disk-write debouncer for runtime-state. `setCapSlice`
8090
8179
  * updates the in-memory mirror synchronously and emits the change
@@ -8113,6 +8202,8 @@ var MirrorRowWriter = class MirrorRowWriter {
8113
8202
  * that drops work silently reads as "never happened".
8114
8203
  */
8115
8204
  skippedSinceWrite = /* @__PURE__ */ new Map();
8205
+ /** Devices whose unreadable-row refusal is already logged — once per episode. */
8206
+ rowUnreadLogged = /* @__PURE__ */ new Set();
8116
8207
  constructor(deps) {
8117
8208
  this.deps = deps;
8118
8209
  }
@@ -8149,7 +8240,8 @@ var MirrorRowWriter = class MirrorRowWriter {
8149
8240
  slot = {
8150
8241
  timer: null,
8151
8242
  inFlight: null,
8152
- lastWriteFailed: false
8243
+ lastWriteFailed: false,
8244
+ unreadRefusals: 0
8153
8245
  };
8154
8246
  this.runtimeStateDebounce.set(deviceId, slot);
8155
8247
  }
@@ -8184,23 +8276,116 @@ var MirrorRowWriter = class MirrorRowWriter {
8184
8276
  this.armOrHold(deviceId, settings);
8185
8277
  }
8186
8278
  /** Arm the debounce timer; at fire, queue a write of the live blob unless disk already has it. */
8187
- armDiskWrite(deviceId, settings) {
8279
+ armDiskWrite(deviceId, settings, delayMs = MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS) {
8188
8280
  const slot = this.slotFor(deviceId);
8189
8281
  if (slot.timer) return;
8190
8282
  slot.timer = setTimeout(() => {
8191
8283
  slot.timer = null;
8192
- this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot));
8193
- }, MirrorRowWriter.RUNTIME_STATE_DEBOUNCE_MS);
8284
+ this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "debounced"));
8285
+ }, delayMs);
8286
+ }
8287
+ /**
8288
+ * Forget a removed device: cancel its armed write, await one in flight, and
8289
+ * drop its baseline and counters — nothing writes its row after this.
8290
+ */
8291
+ async forget(deviceId) {
8292
+ const slot = this.runtimeStateDebounce.get(deviceId);
8293
+ if (slot?.timer) {
8294
+ clearTimeout(slot.timer);
8295
+ slot.timer = null;
8296
+ }
8297
+ if (slot?.inFlight) await slot.inFlight;
8298
+ this.runtimeStateDebounce.delete(deviceId);
8299
+ this.lastPersisted.delete(deviceId);
8300
+ this.skippedSinceWrite.delete(deviceId);
8301
+ this.rowUnreadLogged.delete(deviceId);
8302
+ this.heldDirty.delete(deviceId);
8303
+ }
8304
+ /**
8305
+ * The row is in memory, or is read now (D677). False when the read failed:
8306
+ * the caller writes nothing. Logged once per episode, tagged; the episode
8307
+ * ends at the next successful read.
8308
+ */
8309
+ async rowReadOrRefuse(deviceId, settings, path) {
8310
+ if (!this.deps.isRowRead(deviceId)) try {
8311
+ await this.deps.readRowFirst(deviceId, settings);
8312
+ } catch (err) {
8313
+ if (!this.rowUnreadLogged.has(deviceId)) {
8314
+ this.rowUnreadLogged.add(deviceId);
8315
+ this.deps.logger.warn(ROW_UNREAD_REFUSAL_MESSAGE, {
8316
+ tags: { deviceId },
8317
+ meta: {
8318
+ path,
8319
+ error: errorMessage$2(err)
8320
+ }
8321
+ });
8322
+ }
8323
+ return false;
8324
+ }
8325
+ this.rowUnreadLogged.delete(deviceId);
8326
+ return true;
8194
8327
  }
8195
- /** The debounced write itself: the blob as memory holds it NOW, behind the effective-change gate. Never rejects. */
8196
- async writeLiveBlob(deviceId, settings, slot) {
8328
+ /**
8329
+ * The ONE call of `writeDeviceRuntimeState`. A blob that lacks a cap the
8330
+ * stored row holds is written (memory is the authority once the row was
8331
+ * read) but never silently: the dropped caps are named at warn (D677).
8332
+ * `$owned` is excluded — a release that empties it is the claim path's own
8333
+ * logged business.
8334
+ */
8335
+ async writeRow(deviceId, settings, blob, path) {
8336
+ const onDisk = this.lastPersisted.get(deviceId);
8337
+ if (onDisk) {
8338
+ const dropped = Object.keys(onDisk).filter((cap) => cap !== "$owned" && !Object.hasOwn(blob, cap));
8339
+ if (dropped.length > 0) this.deps.logger.warn(ROW_DROPS_CAPS_MESSAGE, {
8340
+ tags: { deviceId },
8341
+ meta: {
8342
+ path,
8343
+ dropped,
8344
+ storedCaps: Object.keys(onDisk).length,
8345
+ writtenCaps: Object.keys(blob).length
8346
+ }
8347
+ });
8348
+ }
8349
+ await settings.writeDeviceRuntimeState(deviceId, blob);
8350
+ this.lastPersisted.set(deviceId, blob);
8351
+ }
8352
+ /**
8353
+ * A claim's write-through (D663): queued behind the device's writes, the row
8354
+ * read first, the blob taken when it runs. Rejects with
8355
+ * {@link RuntimeStateRowUnreadError} when the row cannot be read, or with the
8356
+ * write's own error; resolves to what landed.
8357
+ */
8358
+ writeThroughRow(deviceId, settings, path) {
8359
+ return this.serialiseWrite(deviceId, async () => {
8360
+ if (!await this.rowReadOrRefuse(deviceId, settings, path)) throw new RuntimeStateRowUnreadError(deviceId);
8361
+ const live = this.deps.persistableFor(deviceId);
8362
+ await this.writeRow(deviceId, settings, live, path);
8363
+ return live;
8364
+ });
8365
+ }
8366
+ /**
8367
+ * The debounced write itself: the row read first (D677), then the blob as
8368
+ * memory holds it NOW, behind the effective-change gate. Never rejects. An
8369
+ * unreadable row writes nothing; the debounced path re-arms with a backoff
8370
+ * (1 s doubling to 60 s — never a 1 Hz loop, D3), and the armed timer is
8371
+ * what a migration drain sees as owed, so a row that has become readable
8372
+ * drains normally. The shutdown flush does not re-arm.
8373
+ */
8374
+ async writeLiveBlob(deviceId, settings, slot, path) {
8375
+ if (!await this.rowReadOrRefuse(deviceId, settings, path)) {
8376
+ if (path === "debounced") {
8377
+ slot.unreadRefusals += 1;
8378
+ this.rearmAfterUnread(deviceId, settings, slot);
8379
+ }
8380
+ return;
8381
+ }
8382
+ slot.unreadRefusals = 0;
8197
8383
  const blob = this.deps.persistableFor(deviceId);
8198
8384
  if (this.isAlreadyPersisted(deviceId, blob)) return;
8199
8385
  const skipped = this.skippedSinceWrite.get(deviceId) ?? 0;
8200
8386
  this.skippedSinceWrite.delete(deviceId);
8201
8387
  try {
8202
- await settings.writeDeviceRuntimeState(deviceId, blob);
8203
- this.lastPersisted.set(deviceId, blob);
8388
+ await this.writeRow(deviceId, settings, blob, path);
8204
8389
  slot.lastWriteFailed = false;
8205
8390
  if (skipped > 0) this.deps.logger.debug("runtime state persisted", {
8206
8391
  tags: { deviceId },
@@ -8217,6 +8402,14 @@ var MirrorRowWriter = class MirrorRowWriter {
8217
8402
  });
8218
8403
  }
8219
8404
  }
8405
+ /** Re-arm an unread refusal with its backoff, or remember it for `release` while held. */
8406
+ rearmAfterUnread(deviceId, settings, slot) {
8407
+ if (this.writesHeld.has(deviceId)) {
8408
+ this.heldDirty.add(deviceId);
8409
+ return;
8410
+ }
8411
+ this.armDiskWrite(deviceId, settings, MirrorRowWriter.unreadRearmDelayMs(slot.unreadRefusals));
8412
+ }
8220
8413
  /**
8221
8414
  * Hold the debounced writer off these devices' rows while a migration swaps
8222
8415
  * them (D663 re-review N-6). Resolves once each device is DRAINED: a write
@@ -8270,6 +8463,7 @@ var MirrorRowWriter = class MirrorRowWriter {
8270
8463
  if (slot?.inFlight) await slot.inFlight;
8271
8464
  const lastFailed = slot?.lastWriteFailed ?? false;
8272
8465
  if (!pending && !lastFailed) return;
8466
+ if (!await this.rowReadOrRefuse(deviceId, settings, "drain")) this.refuseDrain(deviceId, "its runtime-state row could not be read first", null);
8273
8467
  const blob = this.deps.persistableFor(deviceId);
8274
8468
  if (this.isAlreadyPersisted(deviceId, blob)) {
8275
8469
  if (slot) slot.lastWriteFailed = false;
@@ -8277,11 +8471,10 @@ var MirrorRowWriter = class MirrorRowWriter {
8277
8471
  }
8278
8472
  if (lastFailed) this.refuseDrain(deviceId, "its last runtime-state write did not land", null);
8279
8473
  try {
8280
- await this.serialiseWrite(deviceId, () => settings.writeDeviceRuntimeState(deviceId, blob));
8474
+ await this.serialiseWrite(deviceId, () => this.writeRow(deviceId, settings, blob, "drain"));
8281
8475
  } catch (err) {
8282
8476
  this.refuseDrain(deviceId, `the pending runtime-state write failed: ${errorMessage$2(err)}`, err);
8283
8477
  }
8284
- this.lastPersisted.set(deviceId, blob);
8285
8478
  }
8286
8479
  /** A drain that cannot vouch for the row: still owed (re-armed on release), logged, refused. */
8287
8480
  refuseDrain(deviceId, reason, cause) {
@@ -8314,7 +8507,7 @@ var MirrorRowWriter = class MirrorRowWriter {
8314
8507
  clearTimeout(slot.timer);
8315
8508
  slot.timer = null;
8316
8509
  if (settings) {
8317
- pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot)));
8510
+ pending.push(this.serialiseWrite(deviceId, () => this.writeLiveBlob(deviceId, settings, slot, "shutdown-flush")));
8318
8511
  continue;
8319
8512
  }
8320
8513
  }
@@ -8371,9 +8564,18 @@ var DeviceStateMirror = class {
8371
8564
  stateMirror = /* @__PURE__ */ new Map();
8372
8565
  /** The claims and the merged view of every claimed cap (D663). */
8373
8566
  claims;
8374
- /** Devices whose row has been consulted (or ruled empty by the index). */
8567
+ /**
8568
+ * Devices whose CLAIMS are known: the row was read, or a loaded claims index
8569
+ * vouched that it names none (D447). This is a READ-path fact — what a
8570
+ * status read may answer from. It never licenses a row write (D677).
8571
+ */
8375
8572
  seeded = /* @__PURE__ */ new Set();
8376
- /** Devices whose row was actually READ into memory — a write-through needs this. */
8573
+ /**
8574
+ * Devices whose row was actually READ into memory in THIS process. Every row
8575
+ * write of a device requires it (D677): the write replaces the whole row, so
8576
+ * memory must hold what the row held — an index that vouches for the claims
8577
+ * says nothing about the native slices beside them.
8578
+ */
8377
8579
  rowRead = /* @__PURE__ */ new Set();
8378
8580
  /** One row read per device, however many writes arrive during it. */
8379
8581
  seeding = /* @__PURE__ */ new Map();
@@ -8392,7 +8594,9 @@ var DeviceStateMirror = class {
8392
8594
  this.writer = new MirrorRowWriter({
8393
8595
  logger: ctx.logger,
8394
8596
  policyFor,
8395
- persistableFor: (deviceId) => this.persistableForDevice(deviceId)
8597
+ persistableFor: (deviceId) => this.persistableForDevice(deviceId),
8598
+ isRowRead: (deviceId) => this.rowRead.has(deviceId),
8599
+ readRowFirst: (deviceId, settings) => this.ensureRowRead(deviceId, settings)
8396
8600
  });
8397
8601
  }
8398
8602
  /**
@@ -8592,8 +8796,28 @@ var DeviceStateMirror = class {
8592
8796
  return this.seeded.has(deviceId);
8593
8797
  }
8594
8798
  /**
8799
+ * Forget a removed device (D677): its armed row write is cancelled and one
8800
+ * in flight awaited BEFORE its memory goes, so nothing writes its row back
8801
+ * after `removeDevice` clears it.
8802
+ */
8803
+ async forgetDevice(deviceId) {
8804
+ await this.seeding.get(deviceId)?.catch(() => void 0);
8805
+ await this.writer.forget(deviceId);
8806
+ this.stateMirror.delete(deviceId);
8807
+ this.claims.clearDevice(deviceId);
8808
+ this.seeded.delete(deviceId);
8809
+ this.rowRead.delete(deviceId);
8810
+ this.seedRefusalLogged.delete(deviceId);
8811
+ }
8812
+ /** The device's row was read in this process — the precondition of every row write (D677). */
8813
+ isRowRead(deviceId) {
8814
+ return this.rowRead.has(deviceId);
8815
+ }
8816
+ /**
8595
8817
  * The SYNCHRONOUS half of {@link ensureSeeded}, branch (c): a LOADED claims
8596
- * index that does not name the device vouches for it — seeded, no read.
8818
+ * index that does not name the device vouches for its CLAIMS — seeded, no
8819
+ * read. That is all it vouches for: the device is still not `rowRead`, and
8820
+ * the writer reads the row before its first write (D677).
8597
8821
  * Refused (false) while a read is in flight, because that read may be
8598
8822
  * landing a claim the index does not know yet; refused when the index cannot
8599
8823
  * vouch. The overlay's `peek` calls this instead of `ensureSeeded`: a sync
@@ -8608,14 +8832,21 @@ var DeviceStateMirror = class {
8608
8832
  return true;
8609
8833
  }
8610
8834
  /**
8611
- * Make sure the device's claims are known before a native write is applied
8612
- * (D49). Index first, single-flight (ruling S6):
8835
+ * Make sure the device's CLAIMS are known before a status read is answered
8836
+ * or a native slice applied (D49). Index first, single-flight (ruling S6):
8613
8837
  * (a) already seeded → nothing;
8614
8838
  * (b) a seed in flight → await it;
8615
8839
  * (c) a LOADED index that does not name the device → seeded, no read;
8616
8840
  * (d) otherwise read the row once and seed it.
8617
- * A read that throws propagates — the write is refused — and is logged once
8618
- * per device transition (D391).
8841
+ * A read that throws propagates and is logged once per device transition
8842
+ * (D391).
8843
+ *
8844
+ * Also the native-write RPC's gate: a slice may be APPLIED once its claims
8845
+ * are known. Never the ROW write's: branch (c) knows the claims and nothing
8846
+ * about the native slices on the row; a writer that trusted it replaced a
8847
+ * row it never read with the one slice it held, and every restored cap
8848
+ * nobody re-wrote soon after boot was erased (D677, superseding S6(c) for
8849
+ * writes). The writer calls {@link ensureRowRead}.
8619
8850
  */
8620
8851
  async ensureSeeded(deviceId, settings, index) {
8621
8852
  if (this.seeded.has(deviceId)) return;
@@ -8624,7 +8855,13 @@ var DeviceStateMirror = class {
8624
8855
  if (this.seedIfIndexClears(deviceId, index)) return;
8625
8856
  return this.readRowOnce(deviceId, settings);
8626
8857
  }
8627
- /** A write-through replaces the whole row: memory must hold what the row held. */
8858
+ /**
8859
+ * A row write replaces the whole row: memory must hold what the row held
8860
+ * (D677). Reads the row once, single-flight, and fills memory's gaps from it
8861
+ * (`seedMirror('fill')`: a slice memory already holds is newer and wins).
8862
+ * A read that throws propagates — nothing is written — and is logged once
8863
+ * per device transition (D49, D391).
8864
+ */
8628
8865
  async ensureRowRead(deviceId, settings) {
8629
8866
  if (this.rowRead.has(deviceId)) return;
8630
8867
  const inFlight = this.seeding.get(deviceId);
@@ -8678,13 +8915,8 @@ var DeviceStateMirror = class {
8678
8915
  * `changed: false`, and consumers are handed the truth that stands.
8679
8916
  */
8680
8917
  async writeThrough(deviceId, settings, op, rollback) {
8681
- let blob;
8682
8918
  try {
8683
- blob = await this.writer.serialiseWrite(deviceId, async () => {
8684
- const live = this.persistableForDevice(deviceId);
8685
- await settings.writeDeviceRuntimeState(deviceId, live);
8686
- return live;
8687
- });
8919
+ await this.writer.writeThroughRow(deviceId, settings, op.op);
8688
8920
  } catch (err) {
8689
8921
  const extras = {
8690
8922
  tags: { deviceId },
@@ -8703,7 +8935,6 @@ var DeviceStateMirror = class {
8703
8935
  }
8704
8936
  throw err;
8705
8937
  }
8706
- this.writer.markPersisted(deviceId, blob);
8707
8938
  }
8708
8939
  /** After a rollback, re-emit what is true now: the restored claim's merge, or the native. */
8709
8940
  reemitAfterRollback(deviceId, capName) {
@@ -9331,6 +9562,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
9331
9562
  },
9332
9563
  holdMirrorWrites: (deviceIds, settings) => this.stateMirror.holdDiskWrites(deviceIds, settings),
9333
9564
  mirrorOwnedCaps: (deviceId) => this.stateMirror.ownedCaps(deviceId),
9565
+ forgetMirrorDevice: (deviceId) => this.stateMirror.forgetDevice(deviceId),
9334
9566
  resolveDeviceOnline: (deviceId, fallbackOnline) => this.stateMirror.resolveDeviceOnline(deviceId, fallbackOnline),
9335
9567
  resolveDeviceProbed: (deviceId) => this.stateMirror.resolveDeviceProbed(deviceId),
9336
9568
  waitDeviceProvider: (addonId, timeoutMs) => this.waitDeviceProvider(addonId, timeoutMs),
@@ -9673,6 +9905,10 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
9673
9905
  active: input.active
9674
9906
  });
9675
9907
  },
9908
+ getWrapperActivation: (input) => readWrapperActivation(this.wrapperActivationDeps, {
9909
+ deviceId: input.deviceId,
9910
+ capName: input.capName
9911
+ }),
9676
9912
  listWrappersForCap: async (input) => this.listWrappersForCap(input),
9677
9913
  listBindableCapsForDeviceType: async (input) => this.listBindableCapsForDeviceType(input),
9678
9914
  getDeviceSettingsAggregate: async (input) => {