camstack 1.2.34 → 1.2.36

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.
@@ -23631,9 +23631,9 @@ var require_zod = __commonJS({
23631
23631
  }
23632
23632
  });
23633
23633
 
23634
- // ../system/dist/dist-Ctfhlrtl.js
23635
- var require_dist_Ctfhlrtl = __commonJS({
23636
- "../system/dist/dist-Ctfhlrtl.js"(exports) {
23634
+ // ../system/dist/dist-B2CtwVW2.js
23635
+ var require_dist_B2CtwVW2 = __commonJS({
23636
+ "../system/dist/dist-B2CtwVW2.js"(exports) {
23637
23637
  "use strict";
23638
23638
  var zod = require_zod();
23639
23639
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -25933,7 +25933,7 @@ var require_dist_Ctfhlrtl = __commonJS({
25933
25933
  postBufferSec: zod.z.number().min(0).optional()
25934
25934
  });
25935
25935
  ({
25936
- preBufferSec: 10,
25936
+ preBufferSec: 15,
25937
25937
  postBufferSec: 30
25938
25938
  }).postBufferSec * 1e3;
25939
25939
  var RecordingRetentionSchema = zod.z.object({
@@ -32442,128 +32442,103 @@ var require_dist_Ctfhlrtl = __commonJS({
32442
32442
  var AUDIO_MACRO_LABELS = [
32443
32443
  {
32444
32444
  id: "speech",
32445
- name: "Speech",
32446
- icon: "\u{1F5E3}\uFE0F"
32445
+ name: "Speech"
32447
32446
  },
32448
32447
  {
32449
32448
  id: "scream",
32450
- name: "Scream / Shout",
32451
- icon: "\u{1F631}"
32449
+ name: "Scream / Shout"
32452
32450
  },
32453
32451
  {
32454
32452
  id: "crying",
32455
- name: "Crying / Baby",
32456
- icon: "\u{1F622}"
32453
+ name: "Crying / Baby"
32457
32454
  },
32458
32455
  {
32459
32456
  id: "laughter",
32460
- name: "Laughter",
32461
- icon: "\u{1F602}"
32457
+ name: "Laughter"
32462
32458
  },
32463
32459
  {
32464
32460
  id: "music",
32465
- name: "Music",
32466
- icon: "\u{1F3B5}"
32461
+ name: "Music"
32467
32462
  },
32468
32463
  {
32469
32464
  id: "dog",
32470
- name: "Dog",
32471
- icon: "\u{1F415}"
32465
+ name: "Dog"
32472
32466
  },
32473
32467
  {
32474
32468
  id: "cat",
32475
- name: "Cat",
32476
- icon: "\u{1F408}"
32469
+ name: "Cat"
32477
32470
  },
32478
32471
  {
32479
32472
  id: "bird",
32480
- name: "Bird",
32481
- icon: "\u{1F426}"
32473
+ name: "Bird"
32482
32474
  },
32483
32475
  {
32484
32476
  id: "animal",
32485
- name: "Animal (other)",
32486
- icon: "\u{1F43E}"
32477
+ name: "Animal (other)"
32487
32478
  },
32488
32479
  {
32489
32480
  id: "alarm",
32490
- name: "Alarm / Siren",
32491
- icon: "\u{1F6A8}"
32481
+ name: "Alarm / Siren"
32492
32482
  },
32493
32483
  {
32494
32484
  id: "doorbell",
32495
- name: "Doorbell / Knock",
32496
- icon: "\u{1F514}"
32485
+ name: "Doorbell / Knock"
32497
32486
  },
32498
32487
  {
32499
32488
  id: "glass_breaking",
32500
- name: "Glass Breaking",
32501
- icon: "\u{1F4A5}"
32489
+ name: "Glass Breaking"
32502
32490
  },
32503
32491
  {
32504
32492
  id: "gunshot",
32505
- name: "Gunshot / Explosion",
32506
- icon: "\u{1F4A3}"
32493
+ name: "Gunshot / Explosion"
32507
32494
  },
32508
32495
  {
32509
32496
  id: "vehicle",
32510
- name: "Vehicle",
32511
- icon: "\u{1F697}"
32497
+ name: "Vehicle"
32512
32498
  },
32513
32499
  {
32514
32500
  id: "siren",
32515
- name: "Emergency Siren",
32516
- icon: "\u{1F691}"
32501
+ name: "Emergency Siren"
32517
32502
  },
32518
32503
  {
32519
32504
  id: "fire",
32520
- name: "Fire / Smoke",
32521
- icon: "\u{1F525}"
32505
+ name: "Fire / Smoke"
32522
32506
  },
32523
32507
  {
32524
32508
  id: "water",
32525
- name: "Water",
32526
- icon: "\u{1F4A7}"
32509
+ name: "Water"
32527
32510
  },
32528
32511
  {
32529
32512
  id: "wind",
32530
- name: "Wind / Weather",
32531
- icon: "\u{1F32C}\uFE0F"
32513
+ name: "Wind / Weather"
32532
32514
  },
32533
32515
  {
32534
32516
  id: "door",
32535
- name: "Door",
32536
- icon: "\u{1F6AA}"
32517
+ name: "Door"
32537
32518
  },
32538
32519
  {
32539
32520
  id: "footsteps",
32540
- name: "Footsteps",
32541
- icon: "\u{1F463}"
32521
+ name: "Footsteps"
32542
32522
  },
32543
32523
  {
32544
32524
  id: "crowd",
32545
- name: "Crowd / Chatter",
32546
- icon: "\u{1F465}"
32525
+ name: "Crowd / Chatter"
32547
32526
  },
32548
32527
  {
32549
32528
  id: "telephone",
32550
- name: "Telephone",
32551
- icon: "\u{1F4DE}"
32529
+ name: "Telephone"
32552
32530
  },
32553
32531
  {
32554
32532
  id: "engine",
32555
- name: "Engine / Motor",
32556
- icon: "\u2699\uFE0F"
32533
+ name: "Engine / Motor"
32557
32534
  },
32558
32535
  {
32559
32536
  id: "tools",
32560
- name: "Tools / Construction",
32561
- icon: "\u{1F528}"
32537
+ name: "Tools / Construction"
32562
32538
  },
32563
32539
  {
32564
32540
  id: "silence",
32565
- name: "Silence",
32566
- icon: "\u{1F92B}"
32541
+ name: "Silence"
32567
32542
  }
32568
32543
  ];
32569
32544
  var YAMNET_TO_MACRO = {
@@ -34677,6 +34652,35 @@ var require_dist_Ctfhlrtl = __commonJS({
34677
34652
  */
34678
34653
  hasFace: zod.z.boolean().optional(),
34679
34654
  /**
34655
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
34656
+ * face an operator could ASSIGN to an identity.
34657
+ *
34658
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
34659
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
34660
+ * branch, before every gate, and means no more than "a face detector produced
34661
+ * a face detail". This one is stamped at the single moment the gallery row
34662
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
34663
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
34664
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
34665
+ * the crop-store drop. Everything between the detector and that insert can
34666
+ * legitimately refuse the face, so a flag written any earlier promises the
34667
+ * operator something to assign and delivers nothing.
34668
+ *
34669
+ * **Independent of recognition.** A face collected but never auto-matched is
34670
+ * still assignable — it is in fact the face an operator most wants to reach —
34671
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
34672
+ * `subLabel`; this says only that the raw material exists.
34673
+ *
34674
+ * **Set once, never cleared.** A track that produced a gallery row produced
34675
+ * one; deleting the row later is the gallery's business, not this flag's.
34676
+ *
34677
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
34678
+ * before the column omits it, and so does every server that predates the
34679
+ * field. A consumer must test `=== true` and render nothing otherwise —
34680
+ * never infer "no assignable face".
34681
+ */
34682
+ hasEmbeddedFace: zod.z.boolean().optional(),
34683
+ /**
34680
34684
  * This subject CONTAINS a folded rider — a person the rider-pairing step
34681
34685
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
34682
34686
  * so the passage is tracked once and as a VEHICLE.
@@ -36437,6 +36441,32 @@ var require_dist_Ctfhlrtl = __commonJS({
36437
36441
  /** FRAME-space bbox (already mapped back from crop space). */
36438
36442
  bbox: NativeCropBboxSchema.optional(),
36439
36443
  embedding: zod.z.string().optional(),
36444
+ /**
36445
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
36446
+ * so an older runner that does not send it degrades to "unmeasurable", which
36447
+ * the consuming gate treats as accept — never as reject.
36448
+ */
36449
+ embeddingMagnitude: zod.z.number().optional(),
36450
+ /**
36451
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
36452
+ * `label`, and for the same reason: the producing node is the only side that
36453
+ * knows which model actually ran, and re-deriving it downstream is exactly
36454
+ * the inference this model exists to forbid.
36455
+ *
36456
+ * Additive and optional, so a runner that predates it degrades to
36457
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
36458
+ * its previous assumption instead of refusing.
36459
+ *
36460
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
36461
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
36462
+ * with nothing on the wire it took the id from the GALLERY, so the gate
36463
+ * compared the gallery against itself and could never fire. That turned the
36464
+ * one guard protecting the face feature space into a tautology, and it is
36465
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
36466
+ * gallery as the catalog comment predicts — it would have scored the new
36467
+ * model's vectors against the old model's samples in one cosine space.
36468
+ */
36469
+ embeddingModelId: zod.z.string().optional(),
36440
36470
  label: zod.z.string().optional(),
36441
36471
  /**
36442
36472
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -38098,6 +38128,7 @@ var require_dist_Ctfhlrtl = __commonJS({
38098
38128
  isBattery: zod.z.boolean(),
38099
38129
  reason: zod.z.enum([
38100
38130
  "disabled",
38131
+ "offline",
38101
38132
  "sleeping",
38102
38133
  "unreachable",
38103
38134
  "waking"
@@ -38188,9 +38219,17 @@ var require_dist_Ctfhlrtl = __commonJS({
38188
38219
  sleeping: zod.z.boolean(),
38189
38220
  /** Current device state rendered over the cached frame. State images
38190
38221
  * remain authoritative even when their photographic background is
38191
- * old; null means the link must carry a current camera frame. */
38222
+ * old; null means the link must carry a current camera frame.
38223
+ *
38224
+ * **This is the ONLY source a surface may derive an inactive tile
38225
+ * from.** A client that reads a device's `online`/`disabled` flags and
38226
+ * draws its own verdict will disagree with the pixels it is painting
38227
+ * — the frame comes from here, and so must the sentence over it
38228
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
38229
+ * state. */
38192
38230
  stateReason: zod.z.enum([
38193
38231
  "disabled",
38232
+ "offline",
38194
38233
  "sleeping",
38195
38234
  "unreachable",
38196
38235
  "waking"
@@ -55173,7 +55212,6 @@ var require_dist_Ctfhlrtl = __commonJS({
55173
55212
  hitPercent: 60,
55174
55213
  samplingSeconds: 10
55175
55214
  };
55176
- new Map(AUDIO_MACRO_LABELS.flatMap((macro2) => macro2.icon === void 0 ? [] : [[macro2.id, macro2.icon]]));
55177
55215
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
55178
55216
  var TimelapseTemplateSchema = zod.z.object({
55179
55217
  title: zod.z.string().max(500).optional(),
@@ -55284,6 +55322,39 @@ var require_dist_Ctfhlrtl = __commonJS({
55284
55322
  createdAt: zod.z.number(),
55285
55323
  updatedAt: zod.z.number()
55286
55324
  });
55325
+ Object.freeze(Object.fromEntries([{
55326
+ stepId: "face-embedding",
55327
+ label: "Face recognition model",
55328
+ defaultModelId: "arcface-r100",
55329
+ indexName: "the enrolled face gallery",
55330
+ options: [
55331
+ {
55332
+ id: "arcface-r100",
55333
+ label: "ArcFace (ResNet34)"
55334
+ },
55335
+ {
55336
+ id: "auraface-r100",
55337
+ label: "AuraFace R100"
55338
+ },
55339
+ {
55340
+ id: "inception-resnet-v1",
55341
+ label: "Inception ResNet V1"
55342
+ }
55343
+ ]
55344
+ }, {
55345
+ stepId: "clip-embedding",
55346
+ label: "Semantic search model",
55347
+ defaultModelId: "mobileclip-s1",
55348
+ indexName: "the object semantic-search index",
55349
+ options: [{
55350
+ id: "mobileclip-s1",
55351
+ label: "MobileCLIP S1"
55352
+ }, {
55353
+ id: "mobileclip-s2",
55354
+ label: "MobileCLIP S2"
55355
+ }]
55356
+ }].map((s) => [s.stepId, s.defaultModelId])));
55357
+ zod.z.string().min(1);
55287
55358
  zod.z.object({
55288
55359
  /**
55289
55360
  * Fraction of the box's own size added on EACH side before cutting.
@@ -55304,10 +55375,17 @@ var require_dist_Ctfhlrtl = __commonJS({
55304
55375
  */
55305
55376
  square: zod.z.boolean()
55306
55377
  });
55307
- ({
55378
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
55308
55379
  paddingRatio: 0.15,
55309
55380
  square: false
55310
- }).paddingRatio;
55381
+ };
55382
+ Object.freeze({
55383
+ x: 0,
55384
+ y: 0,
55385
+ w: 1,
55386
+ h: 1
55387
+ });
55388
+ DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
55311
55389
  var NativeLeaseAdmissionSchema = zod.z.enum(["all", "inferred"]);
55312
55390
  zod.z.object({
55313
55391
  /**
@@ -56108,7 +56186,7 @@ var require_alerts_addon = __commonJS({
56108
56186
  [Symbol.toStringTag]: { value: "Module" }
56109
56187
  });
56110
56188
  require_chunk_Cek0wNdY();
56111
- var require_dist10 = require_dist_Ctfhlrtl();
56189
+ var require_dist10 = require_dist_B2CtwVW2();
56112
56190
  function selectExpired(alerts, cutoffMs) {
56113
56191
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
56114
56192
  }
@@ -56921,7 +56999,7 @@ var require_console_logging = __commonJS({
56921
56999
  [Symbol.toStringTag]: { value: "Module" }
56922
57000
  });
56923
57001
  require_chunk_Cek0wNdY();
56924
- var require_dist10 = require_dist_Ctfhlrtl();
57002
+ var require_dist10 = require_dist_B2CtwVW2();
56925
57003
  var require_formatter = require_formatter_DqAKDlvN();
56926
57004
  var LEVEL_RANK = {
56927
57005
  debug: 0,
@@ -57015,7 +57093,7 @@ var require_core_blocks_addon = __commonJS({
57015
57093
  "use strict";
57016
57094
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
57017
57095
  var require_chunk = require_chunk_Cek0wNdY();
57018
- var require_dist10 = require_dist_Ctfhlrtl();
57096
+ var require_dist10 = require_dist_B2CtwVW2();
57019
57097
  var node_crypto = __require("crypto");
57020
57098
  var node_fs_promises = __require("fs/promises");
57021
57099
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -57912,6 +57990,230 @@ var require_core_blocks = __commonJS({
57912
57990
  }
57913
57991
  });
57914
57992
 
57993
+ // ../system/dist/retired-settings-keys-CYDGGkyM.js
57994
+ var require_retired_settings_keys_CYDGGkyM = __commonJS({
57995
+ "../system/dist/retired-settings-keys-CYDGGkyM.js"(exports) {
57996
+ "use strict";
57997
+ var require_dist10 = require_dist_B2CtwVW2();
57998
+ function settingsStoreIsAuthoritativeHere(env) {
57999
+ const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
58000
+ return raw === "" || raw === "hub";
58001
+ }
58002
+ var RETIRED_SETTINGS_KEYS = [{
58003
+ namespace: "detection-pipeline",
58004
+ collection: "addon-settings",
58005
+ row: "root",
58006
+ keys: ["pipelineSteps", "pipelineEngine"],
58007
+ perNodeKeys: [
58008
+ "engineRuntime",
58009
+ "engineBackend",
58010
+ "engineDevice",
58011
+ "probedBestEngine"
58012
+ ],
58013
+ reason: "global step/engine seed and the per-node engine cascade \u2014 both removed; the live per-camera dispatch path never read either"
58014
+ }];
58015
+ var RETIRED_SETTINGS_ROWS = [
58016
+ {
58017
+ collection: "addon-settings",
58018
+ row: "device-manager.deviceMeta",
58019
+ owner: "device-manager",
58020
+ evidence: { collection: "device-manager:devices" },
58021
+ reason: "the fleet is one row per device in `device-manager:devices` since the flatten; this blob was 467 KB read and JSON.parsed on hub-main per projection"
58022
+ },
58023
+ {
58024
+ collection: "addon-settings",
58025
+ row: "device-manager.deviceMetadata",
58026
+ owner: "device-manager",
58027
+ evidence: { collection: "device-manager:devices" },
58028
+ reason: "hardware-identity metadata is the `metadata` column of the device row; the separate blob existed only because a blob write is whole-value"
58029
+ },
58030
+ {
58031
+ collection: "addon-settings",
58032
+ row: "device-manager.deviceIndex",
58033
+ owner: "device-manager",
58034
+ evidence: { collection: "device-manager:devices" },
58035
+ reason: "derivable: `addonId`/`stableId` are indexed columns, and index membership is the `registered` column"
58036
+ }
58037
+ ];
58038
+ function retiredKeyStoreOf(backend) {
58039
+ return {
58040
+ async read(spec) {
58041
+ return require_dist10.asJsonObject(await backend.get({
58042
+ namespace: spec.namespace,
58043
+ collection: spec.collection,
58044
+ key: spec.row
58045
+ }));
58046
+ },
58047
+ async write(spec, value) {
58048
+ await backend.set({
58049
+ namespace: spec.namespace,
58050
+ collection: spec.collection,
58051
+ key: spec.row,
58052
+ value
58053
+ });
58054
+ }
58055
+ };
58056
+ }
58057
+ function planNestedRetiredKeyPurge(blob, spec) {
58058
+ const nestedKeys = spec.mapValueKeys ?? [];
58059
+ if (nestedKeys.length === 0) return [];
58060
+ const found = [];
58061
+ for (const [mapKey, value] of Object.entries(blob)) {
58062
+ const inner = require_dist10.asJsonObject(value);
58063
+ if (inner === null) continue;
58064
+ for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
58065
+ }
58066
+ return found;
58067
+ }
58068
+ function planRetiredKeyPurge(blob, spec) {
58069
+ const exact = new Set(spec.keys);
58070
+ const found = [];
58071
+ for (const key of Object.keys(blob)) {
58072
+ if (exact.has(key)) {
58073
+ found.push(key);
58074
+ continue;
58075
+ }
58076
+ for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
58077
+ found.push(key);
58078
+ break;
58079
+ }
58080
+ }
58081
+ return found;
58082
+ }
58083
+ async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
58084
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
58085
+ const results = [];
58086
+ for (const spec of specs) {
58087
+ let blob;
58088
+ try {
58089
+ blob = await store.read(spec);
58090
+ } catch (err) {
58091
+ logger.warn("retired settings keys \u2014 read failed, row left untouched", { meta: {
58092
+ addon: spec.namespace,
58093
+ collection: spec.collection,
58094
+ row: spec.row,
58095
+ error: err instanceof Error ? err.message : String(err)
58096
+ } });
58097
+ continue;
58098
+ }
58099
+ if (blob === null) continue;
58100
+ const doomed = planRetiredKeyPurge(blob, spec);
58101
+ const doomedNested = planNestedRetiredKeyPurge(blob, spec);
58102
+ if (doomed.length === 0 && doomedNested.length === 0) continue;
58103
+ const doomedSet = new Set(doomed);
58104
+ const nestedByMapKey = /* @__PURE__ */ new Map();
58105
+ for (const path of doomedNested) {
58106
+ const cut = path.indexOf(".");
58107
+ const mapKey = path.slice(0, cut);
58108
+ const inner = path.slice(cut + 1);
58109
+ const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
58110
+ bucket.add(inner);
58111
+ nestedByMapKey.set(mapKey, bucket);
58112
+ }
58113
+ const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
58114
+ const retired = nestedByMapKey.get(key);
58115
+ if (retired === void 0) return [key, value];
58116
+ const inner = require_dist10.asJsonObject(value);
58117
+ if (inner === null) return [key, value];
58118
+ return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
58119
+ }));
58120
+ await store.write(spec, next);
58121
+ const result = {
58122
+ addon: spec.namespace,
58123
+ collection: spec.collection,
58124
+ row: spec.row,
58125
+ keys: doomed,
58126
+ nested: doomedNested
58127
+ };
58128
+ results.push(result);
58129
+ logger.info(`purged ${doomed.length + doomedNested.length} retired settings keys`, { meta: {
58130
+ ...result,
58131
+ reason: spec.reason
58132
+ } });
58133
+ }
58134
+ return results;
58135
+ }
58136
+ async function purgeRetiredSettingsRows(store, logger, specs = RETIRED_SETTINGS_ROWS, env = process.env) {
58137
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
58138
+ const results = [];
58139
+ const evidence = /* @__PURE__ */ new Map();
58140
+ for (const spec of specs) {
58141
+ let present;
58142
+ try {
58143
+ present = await store.hasRow(spec);
58144
+ } catch (err) {
58145
+ logger.warn("retired settings row \u2014 read failed, row left untouched", { meta: {
58146
+ addon: spec.owner,
58147
+ collection: spec.collection,
58148
+ row: spec.row,
58149
+ error: errText(err)
58150
+ } });
58151
+ continue;
58152
+ }
58153
+ if (!present) continue;
58154
+ const successor = spec.evidence.collection;
58155
+ let count = evidence.get(successor);
58156
+ if (count === void 0) {
58157
+ try {
58158
+ count = await store.countRows(successor);
58159
+ } catch (err) {
58160
+ logger.warn("retired settings row \u2014 successor unreadable, row left untouched", { meta: {
58161
+ addon: spec.owner,
58162
+ row: spec.row,
58163
+ successor,
58164
+ error: errText(err)
58165
+ } });
58166
+ count = 0;
58167
+ }
58168
+ evidence.set(successor, count);
58169
+ }
58170
+ if (count === 0) {
58171
+ logger.warn("retired settings row kept \u2014 its successor is empty, migration has not run", { meta: {
58172
+ addon: spec.owner,
58173
+ collection: spec.collection,
58174
+ row: spec.row,
58175
+ successor
58176
+ } });
58177
+ continue;
58178
+ }
58179
+ await store.deleteRow(spec);
58180
+ const result = {
58181
+ owner: spec.owner,
58182
+ collection: spec.collection,
58183
+ row: spec.row
58184
+ };
58185
+ results.push(result);
58186
+ logger.info("purged a retired settings row", { meta: {
58187
+ ...result,
58188
+ reason: spec.reason
58189
+ } });
58190
+ }
58191
+ return results;
58192
+ }
58193
+ function errText(err) {
58194
+ return err instanceof Error ? err.message : String(err);
58195
+ }
58196
+ Object.defineProperty(exports, "purgeRetiredSettingsKeys", {
58197
+ enumerable: true,
58198
+ get: function() {
58199
+ return purgeRetiredSettingsKeys;
58200
+ }
58201
+ });
58202
+ Object.defineProperty(exports, "purgeRetiredSettingsRows", {
58203
+ enumerable: true,
58204
+ get: function() {
58205
+ return purgeRetiredSettingsRows;
58206
+ }
58207
+ });
58208
+ Object.defineProperty(exports, "retiredKeyStoreOf", {
58209
+ enumerable: true,
58210
+ get: function() {
58211
+ return retiredKeyStoreOf;
58212
+ }
58213
+ });
58214
+ }
58215
+ });
58216
+
57915
58217
  // ../types/dist/canonical-hash-DNV8S5ET.js
57916
58218
  var require_canonical_hash_DNV8S5ET = __commonJS({
57917
58219
  "../types/dist/canonical-hash-DNV8S5ET.js"(exports) {
@@ -59735,7 +60037,8 @@ var require_device_manager_addon = __commonJS({
59735
60037
  [Symbol.toStringTag]: { value: "Module" }
59736
60038
  });
59737
60039
  require_chunk_Cek0wNdY();
59738
- var require_dist10 = require_dist_Ctfhlrtl();
60040
+ var require_dist10 = require_dist_B2CtwVW2();
60041
+ var require_retired_settings_keys = require_retired_settings_keys_CYDGGkyM();
59739
60042
  var node_crypto = __require("crypto");
59740
60043
  var _camstack_types_node = require_node();
59741
60044
  var JOB_HISTORY = 20;
@@ -60023,6 +60326,311 @@ var require_device_manager_addon = __commonJS({
60023
60326
  }
60024
60327
  }
60025
60328
  };
60329
+ function tagContribution(contribution, capName, addonId, kind) {
60330
+ const source = kind === "settings" ? "settings" : "live";
60331
+ return {
60332
+ ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
60333
+ sections: contribution.sections.map((section) => ({
60334
+ ...section,
60335
+ fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
60336
+ }))
60337
+ };
60338
+ }
60339
+ function isFieldRecord(value) {
60340
+ return value !== null && typeof value === "object" && !Array.isArray(value);
60341
+ }
60342
+ function toWireShape(input) {
60343
+ const out = { sections: input.sections.map((s) => ({
60344
+ id: s.id,
60345
+ title: s.title,
60346
+ ...s.description !== void 0 ? { description: s.description } : {},
60347
+ ...s.style !== void 0 ? { style: s.style } : {},
60348
+ ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
60349
+ ...s.columns !== void 0 ? { columns: s.columns } : {},
60350
+ ...s.tab !== void 0 ? { tab: s.tab } : {},
60351
+ ...s.location !== void 0 ? { location: s.location } : {},
60352
+ ...s.order !== void 0 ? { order: s.order } : {},
60353
+ fields: [...s.fields]
60354
+ })) };
60355
+ if (input.tabs) out.tabs = [...input.tabs];
60356
+ return out;
60357
+ }
60358
+ function tagField(field, capName, addonId, source, kind) {
60359
+ if (!isFieldRecord(field)) return field;
60360
+ const f = field;
60361
+ const structuralTypes = /* @__PURE__ */ new Set([
60362
+ "separator",
60363
+ "info",
60364
+ "button"
60365
+ ]);
60366
+ if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
60367
+ const tagged = {
60368
+ ...f,
60369
+ source
60370
+ };
60371
+ if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
60372
+ else {
60373
+ tagged.writerCapName = capName;
60374
+ tagged.writerAddonId = addonId;
60375
+ }
60376
+ if (f.type === "group") {
60377
+ const children = Array.isArray(f.fields) ? f.fields : [];
60378
+ if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
60379
+ } else if (f.type === "sub-tabs") {
60380
+ const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
60381
+ if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
60382
+ if (!isFieldRecord(tab)) return tab;
60383
+ const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
60384
+ return {
60385
+ ...tab,
60386
+ fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
60387
+ };
60388
+ });
60389
+ }
60390
+ return tagged;
60391
+ }
60392
+ function mergeAggregates(parts) {
60393
+ const tabDecls = /* @__PURE__ */ new Map();
60394
+ const sections = [];
60395
+ const seenSectionIds = /* @__PURE__ */ new Set();
60396
+ for (const part of parts) {
60397
+ if (part.tabs) {
60398
+ for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
60399
+ }
60400
+ for (const s of part.sections) {
60401
+ if (s.id !== void 0) {
60402
+ if (seenSectionIds.has(s.id)) continue;
60403
+ seenSectionIds.add(s.id);
60404
+ }
60405
+ sections.push(s);
60406
+ }
60407
+ }
60408
+ for (const s of sections) {
60409
+ const tabId = s.tab ?? "general";
60410
+ if (tabDecls.has(tabId)) continue;
60411
+ const known = require_dist10.WELL_KNOWN_TAB_MAP[tabId];
60412
+ if (known) tabDecls.set(tabId, {
60413
+ id: known.id,
60414
+ label: known.label,
60415
+ icon: known.icon,
60416
+ order: known.order
60417
+ });
60418
+ else tabDecls.set(tabId, {
60419
+ id: tabId,
60420
+ label: tabId,
60421
+ icon: "wrench",
60422
+ order: 100
60423
+ });
60424
+ }
60425
+ sections.sort((a, b) => {
60426
+ const tabA = a.tab ?? "general";
60427
+ const tabB = b.tab ?? "general";
60428
+ if (tabA !== tabB) {
60429
+ const orderA = tabDecls.get(tabA)?.order ?? 100;
60430
+ const orderB = tabDecls.get(tabB)?.order ?? 100;
60431
+ if (orderA !== orderB) return orderA - orderB;
60432
+ return tabA.localeCompare(tabB);
60433
+ }
60434
+ return (a.order ?? 0) - (b.order ?? 0);
60435
+ });
60436
+ const sortedTabs = [...tabDecls.values()].toSorted((a, b) => (a.order ?? 100) - (b.order ?? 100));
60437
+ const out = { sections };
60438
+ if (sortedTabs.length > 0) out.tabs = sortedTabs;
60439
+ return out;
60440
+ }
60441
+ function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
60442
+ const localNodeId = ctx.kernel.localNodeId ?? "hub";
60443
+ ctx.eventBus.subscribe({ category: require_dist10.EventCategory.DeviceBindingsChanged }, (event) => {
60444
+ const { deviceId, capName, reason, addonId, nodeId } = event.data;
60445
+ if (nodeId === localNodeId) return;
60446
+ if (reason === "native-registered") {
60447
+ let perDevice = remoteNativeCaps.get(deviceId);
60448
+ if (!perDevice) {
60449
+ perDevice = /* @__PURE__ */ new Map();
60450
+ remoteNativeCaps.set(deviceId, perDevice);
60451
+ }
60452
+ perDevice.set(capName, {
60453
+ addonId,
60454
+ nodeId
60455
+ });
60456
+ } else if (reason === "native-unregistered") {
60457
+ const perDevice = remoteNativeCaps.get(deviceId);
60458
+ if (!perDevice) return;
60459
+ perDevice.delete(capName);
60460
+ if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
60461
+ }
60462
+ });
60463
+ const cluster = ctx.kernel.cluster;
60464
+ if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
60465
+ const gone = payload.node.id;
60466
+ const emptyDevices = [];
60467
+ for (const [deviceId, perDevice] of remoteNativeCaps) {
60468
+ const toDelete = [];
60469
+ for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
60470
+ for (const capName of toDelete) perDevice.delete(capName);
60471
+ if (perDevice.size === 0) emptyDevices.push(deviceId);
60472
+ }
60473
+ for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
60474
+ });
60475
+ }
60476
+ async function readBindingsStore(deps) {
60477
+ return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
60478
+ }
60479
+ async function writeBindingsStore(deps, next) {
60480
+ await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
60481
+ }
60482
+ function resolveWrapperNodeId(_wrapperAddonId) {
60483
+ return "hub";
60484
+ }
60485
+ function toRoutableProviderNodeId(nodeId) {
60486
+ const slash = nodeId.indexOf("/");
60487
+ return slash === -1 ? nodeId : nodeId.slice(0, slash);
60488
+ }
60489
+ function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
60490
+ const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
60491
+ if (!clusterCaps) return null;
60492
+ for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
60493
+ addonId: entry.addonId,
60494
+ nodeId: entry.nodeId
60495
+ };
60496
+ return null;
60497
+ }
60498
+ function resolveDeviceType(deps, row, deviceId) {
60499
+ const persisted = row?.meta.type;
60500
+ if (typeof persisted === "string" && persisted.length > 0) return persisted;
60501
+ const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
60502
+ return typeof live === "string" && live.length > 0 ? live : void 0;
60503
+ }
60504
+ async function resolveDevicePresence(deps, row, deviceId) {
60505
+ if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
60506
+ if (row !== null) return "present";
60507
+ return await deps.rows.count() > 0 ? "absent" : "unknown";
60508
+ }
60509
+ function capAppliesToDeviceType(def, deviceType) {
60510
+ if (deviceType === void 0) return true;
60511
+ const declared = def?.deviceTypes;
60512
+ if (!declared || declared.length === 0) return true;
60513
+ return declared.some((t) => t === deviceType);
60514
+ }
60515
+ async function getBindings(deps, input) {
60516
+ const storeKey = String(input.deviceId);
60517
+ const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
60518
+ const row = await deps.rows.get(input.deviceId);
60519
+ const deviceType = resolveDeviceType(deps, row, input.deviceId);
60520
+ const presence = await resolveDevicePresence(deps, row, input.deviceId);
60521
+ const entries = [];
60522
+ const seenCaps = /* @__PURE__ */ new Set();
60523
+ const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
60524
+ for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
60525
+ const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
60526
+ const remoteNative = resolveRemote(capName);
60527
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
60528
+ const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
60529
+ if (wrapperAddonId === null && !nativeAddonId) {
60530
+ seenCaps.add(capName);
60531
+ continue;
60532
+ }
60533
+ entries.push({
60534
+ capName,
60535
+ kind: wrapperAddonId ? "wrapped" : "native",
60536
+ providerAddonId: wrapperAddonId ?? nativeAddonId,
60537
+ providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
60538
+ nativeAddonId
60539
+ });
60540
+ seenCaps.add(capName);
60541
+ }
60542
+ if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device \u2014 returning none", { tags: { deviceId: input.deviceId } });
60543
+ else if (deps.capabilityRegistry) {
60544
+ const skippedForType = [];
60545
+ for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
60546
+ if (seenCaps.has(capName)) continue;
60547
+ if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
60548
+ skippedForType.push(capName);
60549
+ continue;
60550
+ }
60551
+ const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
60552
+ if (!defaultWrapperAddonId) continue;
60553
+ const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
60554
+ const remoteNative = resolveRemote(capName);
60555
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
60556
+ entries.push({
60557
+ capName,
60558
+ kind: "wrapped",
60559
+ providerAddonId: defaultWrapperAddonId,
60560
+ providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
60561
+ nativeAddonId
60562
+ });
60563
+ seenCaps.add(capName);
60564
+ }
60565
+ if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped \u2014 deviceTypes mismatch", {
60566
+ tags: { deviceId: input.deviceId },
60567
+ meta: {
60568
+ deviceType,
60569
+ skipped: skippedForType
60570
+ }
60571
+ });
60572
+ }
60573
+ if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
60574
+ if (seenCaps.has(capName)) continue;
60575
+ const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
60576
+ entries.push({
60577
+ capName,
60578
+ kind: "native",
60579
+ providerAddonId: nativeAddonId,
60580
+ providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
60581
+ nativeAddonId
60582
+ });
60583
+ seenCaps.add(capName);
60584
+ }
60585
+ const pushFed = deps.remoteNativeCaps.get(input.deviceId);
60586
+ if (pushFed) for (const [capName, info] of pushFed) {
60587
+ if (seenCaps.has(capName)) continue;
60588
+ entries.push({
60589
+ capName,
60590
+ kind: "native",
60591
+ providerAddonId: info.addonId,
60592
+ providerNodeId: toRoutableProviderNodeId(info.nodeId),
60593
+ nativeAddonId: info.addonId
60594
+ });
60595
+ seenCaps.add(capName);
60596
+ }
60597
+ const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
60598
+ if (clusterCaps) for (const entry of clusterCaps) {
60599
+ if (entry.deviceId !== input.deviceId) continue;
60600
+ if (seenCaps.has(entry.capName)) continue;
60601
+ if (!entry.addonId) continue;
60602
+ const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
60603
+ if (entry.nodeId === localNodeId) continue;
60604
+ entries.push({
60605
+ capName: entry.capName,
60606
+ kind: "native",
60607
+ providerAddonId: entry.addonId,
60608
+ providerNodeId: toRoutableProviderNodeId(entry.nodeId),
60609
+ nativeAddonId: entry.addonId
60610
+ });
60611
+ seenCaps.add(entry.capName);
60612
+ }
60613
+ return {
60614
+ deviceId: input.deviceId,
60615
+ entries
60616
+ };
60617
+ }
60618
+ async function getAllBindings(deps) {
60619
+ const ids = /* @__PURE__ */ new Set();
60620
+ for (const row of await deps.rows.listAll()) ids.add(row.meta.id);
60621
+ const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
60622
+ for (const device of registered) ids.add(device.id);
60623
+ if (ids.size === 0) {
60624
+ deps.ctx.logger.warn("getAllBindings found no devices \u2014 warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
60625
+ return [];
60626
+ }
60627
+ const out = [];
60628
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
60629
+ return out;
60630
+ }
60631
+ async function lookupPersistedStableId(deps, deviceId) {
60632
+ return (await deps.rows.get(deviceId))?.meta.stableId;
60633
+ }
60026
60634
  var MODE_LABELS = {
60027
60635
  auto: "Auto",
60028
60636
  day: "Day",
@@ -60345,118 +60953,6 @@ var require_device_manager_addon = __commonJS({
60345
60953
  if (reducer.kind !== "settings") throw new Error(`device-config: builderId "${builderId}" is not a settings-based derived-form`);
60346
60954
  return reducer.parsePatch(patch);
60347
60955
  }
60348
- function tagContribution(contribution, capName, addonId, kind) {
60349
- const source = kind === "settings" ? "settings" : "live";
60350
- return {
60351
- ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
60352
- sections: contribution.sections.map((section) => ({
60353
- ...section,
60354
- fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
60355
- }))
60356
- };
60357
- }
60358
- function isFieldRecord(value) {
60359
- return value !== null && typeof value === "object" && !Array.isArray(value);
60360
- }
60361
- function toWireShape(input) {
60362
- const out = { sections: input.sections.map((s) => ({
60363
- id: s.id,
60364
- title: s.title,
60365
- ...s.description !== void 0 ? { description: s.description } : {},
60366
- ...s.style !== void 0 ? { style: s.style } : {},
60367
- ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
60368
- ...s.columns !== void 0 ? { columns: s.columns } : {},
60369
- ...s.tab !== void 0 ? { tab: s.tab } : {},
60370
- ...s.location !== void 0 ? { location: s.location } : {},
60371
- ...s.order !== void 0 ? { order: s.order } : {},
60372
- fields: [...s.fields]
60373
- })) };
60374
- if (input.tabs) out.tabs = [...input.tabs];
60375
- return out;
60376
- }
60377
- function tagField(field, capName, addonId, source, kind) {
60378
- if (!isFieldRecord(field)) return field;
60379
- const f = field;
60380
- const structuralTypes = /* @__PURE__ */ new Set([
60381
- "separator",
60382
- "info",
60383
- "button"
60384
- ]);
60385
- if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
60386
- const tagged = {
60387
- ...f,
60388
- source
60389
- };
60390
- if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
60391
- else {
60392
- tagged.writerCapName = capName;
60393
- tagged.writerAddonId = addonId;
60394
- }
60395
- if (f.type === "group") {
60396
- const children = Array.isArray(f.fields) ? f.fields : [];
60397
- if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
60398
- } else if (f.type === "sub-tabs") {
60399
- const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
60400
- if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
60401
- if (!isFieldRecord(tab)) return tab;
60402
- const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
60403
- return {
60404
- ...tab,
60405
- fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
60406
- };
60407
- });
60408
- }
60409
- return tagged;
60410
- }
60411
- function mergeAggregates(parts) {
60412
- const tabDecls = /* @__PURE__ */ new Map();
60413
- const sections = [];
60414
- const seenSectionIds = /* @__PURE__ */ new Set();
60415
- for (const part of parts) {
60416
- if (part.tabs) {
60417
- for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
60418
- }
60419
- for (const s of part.sections) {
60420
- if (s.id !== void 0) {
60421
- if (seenSectionIds.has(s.id)) continue;
60422
- seenSectionIds.add(s.id);
60423
- }
60424
- sections.push(s);
60425
- }
60426
- }
60427
- for (const s of sections) {
60428
- const tabId = s.tab ?? "general";
60429
- if (tabDecls.has(tabId)) continue;
60430
- const known = require_dist10.WELL_KNOWN_TAB_MAP[tabId];
60431
- if (known) tabDecls.set(tabId, {
60432
- id: known.id,
60433
- label: known.label,
60434
- icon: known.icon,
60435
- order: known.order
60436
- });
60437
- else tabDecls.set(tabId, {
60438
- id: tabId,
60439
- label: tabId,
60440
- icon: "wrench",
60441
- order: 100
60442
- });
60443
- }
60444
- sections.sort((a, b) => {
60445
- const tabA = a.tab ?? "general";
60446
- const tabB = b.tab ?? "general";
60447
- if (tabA !== tabB) {
60448
- const orderA = tabDecls.get(tabA)?.order ?? 100;
60449
- const orderB = tabDecls.get(tabB)?.order ?? 100;
60450
- if (orderA !== orderB) return orderA - orderB;
60451
- return tabA.localeCompare(tabB);
60452
- }
60453
- return (a.order ?? 0) - (b.order ?? 0);
60454
- });
60455
- const sortedTabs = [...tabDecls.values()].toSorted((a, b) => (a.order ?? 100) - (b.order ?? 100));
60456
- const out = { sections };
60457
- if (sortedTabs.length > 0) out.tabs = sortedTabs;
60458
- return out;
60459
- }
60460
60956
  function isTransientMoleculerError(err) {
60461
60957
  if (err !== null && typeof err === "object") {
60462
60958
  const e = err;
@@ -60548,22 +61044,15 @@ var require_device_manager_addon = __commonJS({
60548
61044
  }
60549
61045
  async function listPersistedByAddon(pctx, input) {
60550
61046
  const { addonId } = input;
60551
- const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
60552
- const stableIds = index[addonId] ?? [];
60553
- const byStableId = /* @__PURE__ */ new Map();
60554
- for (const m of Object.values(meta)) if (m.addonId === addonId) byStableId.set(m.stableId, m);
60555
- return stableIds.map((stableId) => {
60556
- const m = byStableId.get(stableId);
60557
- return {
60558
- id: m.id,
60559
- stableId,
60560
- type: m.type,
60561
- name: m.name,
60562
- location: m.location ?? null,
60563
- disabled: m.disabled ?? false,
60564
- parentDeviceId: m.parentDeviceId
60565
- };
60566
- });
61047
+ return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
61048
+ id: m.id,
61049
+ stableId: m.stableId,
61050
+ type: m.type,
61051
+ name: m.name,
61052
+ location: m.location ?? null,
61053
+ disabled: m.disabled ?? false,
61054
+ parentDeviceId: m.parentDeviceId
61055
+ }));
60567
61056
  }
60568
61057
  async function listAll(pctx, input) {
60569
61058
  const ownerFilter = Reflect.get(input, "addonId");
@@ -60573,8 +61062,9 @@ var require_device_manager_addon = __commonJS({
60573
61062
  const camerasOnly = input.isCamera === true;
60574
61063
  const results = [];
60575
61064
  const seen = /* @__PURE__ */ new Set();
60576
- const meta = await pctx.metaStore.readMeta();
60577
- const metadataMap = await pctx.metaStore.readMetadataMap();
61065
+ const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listAll();
61066
+ const rowById = /* @__PURE__ */ new Map();
61067
+ for (const row of fleet) rowById.set(row.meta.id, row);
60578
61068
  if (pctx.registry) {
60579
61069
  const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
60580
61070
  addonId,
@@ -60582,7 +61072,8 @@ var require_device_manager_addon = __commonJS({
60582
61072
  })) : pctx.registry.getAllWithAddonId();
60583
61073
  for (const { addonId: aid, device } of liveEntries) {
60584
61074
  const key = String(device.id);
60585
- const info = toDeviceInfo(aid, device, metadataMap[key] ?? null, meta[key] ?? null);
61075
+ const row = rowById.get(device.id);
61076
+ const info = toDeviceInfo(aid, device, row?.metadata ?? null, row?.meta ?? null);
60586
61077
  seen.add(key);
60587
61078
  if (camerasOnly && !info.isCamera) continue;
60588
61079
  results.push(slim ? {
@@ -60592,39 +61083,36 @@ var require_device_manager_addon = __commonJS({
60592
61083
  } : info);
60593
61084
  }
60594
61085
  }
60595
- const index = await pctx.metaStore.readIndex();
60596
- const metaByAddonStable = /* @__PURE__ */ new Map();
60597
- for (const m of Object.values(meta)) metaByAddonStable.set(`${m.addonId}\0${m.stableId}`, m);
60598
- const targetAddons = addonId ? [addonId] : Object.keys(index);
60599
- for (const aid of targetAddons) for (const stableId of index[aid] ?? []) {
60600
- const m = metaByAddonStable.get(`${aid}\0${stableId}`);
60601
- const key = String(m.id);
60602
- if (seen.has(key)) continue;
61086
+ for (const row of fleet) {
61087
+ const m = row.meta;
61088
+ const aid = m.addonId;
61089
+ const stableId = m.stableId;
61090
+ if (seen.has(String(m.id))) continue;
60603
61091
  const persistedType = m.type;
60604
61092
  if (camerasOnly && persistedType !== require_dist10.DeviceType.Camera) continue;
60605
61093
  const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
60606
- const metadata = slim ? null : metadataMap[key] ?? null;
61094
+ const metadata = slim ? null : row.metadata;
60607
61095
  results.push({
60608
61096
  id: m.id,
60609
61097
  stableId,
60610
61098
  addonId: aid,
60611
61099
  type: persistedType,
60612
- name: m?.name ?? stableId,
60613
- location: m?.location ?? null,
60614
- disabled: m?.disabled ?? false,
60615
- parentDeviceId: m?.parentDeviceId ?? null,
60616
- role: toDeviceRole(m?.role),
61100
+ name: m.name,
61101
+ location: m.location ?? null,
61102
+ disabled: m.disabled ?? false,
61103
+ parentDeviceId: m.parentDeviceId ?? null,
61104
+ role: toDeviceRole(m.role),
60617
61105
  online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
60618
61106
  probed: pctx.host.resolveDeviceProbed(m.id),
60619
- features: persistedFeatures(m?.features),
61107
+ features: persistedFeatures(m.features),
60620
61108
  isCamera: persistedType === require_dist10.DeviceType.Camera,
60621
61109
  config: persistedConfig ?? {},
60622
61110
  metadata,
60623
- ...m?.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
60624
- ...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
60625
- ...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
60626
- ...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
60627
- ...m?.display !== void 0 ? { display: m.display } : {},
61111
+ ...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
61112
+ ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
61113
+ ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
61114
+ ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
61115
+ ...m.display !== void 0 ? { display: m.display } : {},
60628
61116
  ...(() => {
60629
61117
  if (slim) return {};
60630
61118
  const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
@@ -60639,19 +61127,15 @@ var require_device_manager_addon = __commonJS({
60639
61127
  if (pctx.registry) {
60640
61128
  const found = resolveDeviceById(pctx.registry, deviceId);
60641
61129
  if (found) {
60642
- const key2 = String(found.device.id);
60643
- const [map, metaMap] = await Promise.all([pctx.metaStore.readMetadataMap(), pctx.metaStore.readMeta()]);
60644
- const metadata2 = map[key2] ?? null;
60645
- const metaRow = metaMap[key2] ?? null;
60646
- return toDeviceInfo(found.addonId, found.device, metadata2, metaRow);
61130
+ const row2 = await pctx.metaStore.getRow(found.device.id);
61131
+ return toDeviceInfo(found.addonId, found.device, row2?.metadata ?? null, row2?.meta ?? null);
60647
61132
  }
60648
61133
  }
60649
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
60650
- if (!persisted) return null;
60651
- const { addonId: aid, stableId, meta: m } = persisted;
61134
+ const row = await pctx.metaStore.getRow(deviceId);
61135
+ if (row === null) return null;
61136
+ const { meta: m, metadata } = row;
61137
+ const { addonId: aid, stableId } = m;
60652
61138
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60653
- const key = String(deviceId);
60654
- const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
60655
61139
  const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
60656
61140
  return {
60657
61141
  id: deviceId,
@@ -60690,30 +61174,25 @@ var require_device_manager_addon = __commonJS({
60690
61174
  }
60691
61175
  const results = [];
60692
61176
  const seen = /* @__PURE__ */ new Set();
60693
- const [index, meta, metadataMap] = await Promise.all([
60694
- pctx.metaStore.readIndex(),
60695
- pctx.metaStore.readMeta(),
60696
- pctx.metaStore.readMetadataMap()
60697
- ]);
61177
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
61178
+ const rowById = /* @__PURE__ */ new Map();
61179
+ for (const row of childRows) rowById.set(row.meta.id, row);
60698
61180
  if (pctx.registry) {
60699
61181
  const liveChildren = pctx.registry.getChildren(parentDeviceId);
60700
61182
  for (const device of liveChildren) {
60701
61183
  const key = String(device.id);
60702
- const metadata = metadataMap[key] ?? null;
60703
- const metaRow = meta[key] ?? null;
60704
- results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
61184
+ const row = rowById.get(device.id);
61185
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
60705
61186
  seen.add(key);
60706
61187
  }
60707
61188
  }
60708
- const ownerMetaByStableId = /* @__PURE__ */ new Map();
60709
- for (const m of Object.values(meta)) if (m.addonId === ownerAddonId) ownerMetaByStableId.set(m.stableId, m);
60710
- const persistedChildren = (index[ownerAddonId] ?? []).filter((sid) => ownerMetaByStableId.get(sid)?.parentDeviceId === parentDeviceId);
60711
- for (const childStableId of persistedChildren) {
60712
- const m = ownerMetaByStableId.get(childStableId);
61189
+ for (const row of childRows) {
61190
+ const m = row.meta;
61191
+ const childStableId = m.stableId;
60713
61192
  const key = String(m.id);
60714
61193
  if (seen.has(key)) continue;
60715
61194
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60716
- const metadata = metadataMap[key] ?? null;
61195
+ const metadata = row.metadata;
60717
61196
  const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
60718
61197
  results.push({
60719
61198
  id: m.id,
@@ -60857,16 +61336,10 @@ var require_device_manager_addon = __commonJS({
60857
61336
  }
60858
61337
  async function removeByIntegration(pctx, input) {
60859
61338
  const { integrationId } = input;
60860
- const meta = await pctx.metaStore.readMeta();
60861
- const parentKeys = Object.keys(meta).filter((key) => {
60862
- const m = meta[key];
60863
- return m !== void 0 && m.integrationId === integrationId && m.parentDeviceId === null;
60864
- });
61339
+ const parents = (await pctx.metaStore.rows.listByIntegration(integrationId)).filter((row) => row.meta.parentDeviceId === null);
60865
61340
  let removed = 0;
60866
- for (const _key of parentKeys) {
60867
- const m = meta[_key];
60868
- if (!m) continue;
60869
- await pctx.provider.remove({ deviceId: m.id });
61341
+ for (const parent of parents) {
61342
+ await pctx.provider.remove({ deviceId: parent.meta.id });
60870
61343
  removed++;
60871
61344
  }
60872
61345
  return { removed };
@@ -61255,204 +61728,6 @@ var require_device_manager_addon = __commonJS({
61255
61728
  driverPatch
61256
61729
  };
61257
61730
  }
61258
- function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
61259
- const localNodeId = ctx.kernel.localNodeId ?? "hub";
61260
- ctx.eventBus.subscribe({ category: require_dist10.EventCategory.DeviceBindingsChanged }, (event) => {
61261
- const { deviceId, capName, reason, addonId, nodeId } = event.data;
61262
- if (nodeId === localNodeId) return;
61263
- if (reason === "native-registered") {
61264
- let perDevice = remoteNativeCaps.get(deviceId);
61265
- if (!perDevice) {
61266
- perDevice = /* @__PURE__ */ new Map();
61267
- remoteNativeCaps.set(deviceId, perDevice);
61268
- }
61269
- perDevice.set(capName, {
61270
- addonId,
61271
- nodeId
61272
- });
61273
- } else if (reason === "native-unregistered") {
61274
- const perDevice = remoteNativeCaps.get(deviceId);
61275
- if (!perDevice) return;
61276
- perDevice.delete(capName);
61277
- if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
61278
- }
61279
- });
61280
- const cluster = ctx.kernel.cluster;
61281
- if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
61282
- const gone = payload.node.id;
61283
- const emptyDevices = [];
61284
- for (const [deviceId, perDevice] of remoteNativeCaps) {
61285
- const toDelete = [];
61286
- for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
61287
- for (const capName of toDelete) perDevice.delete(capName);
61288
- if (perDevice.size === 0) emptyDevices.push(deviceId);
61289
- }
61290
- for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
61291
- });
61292
- }
61293
- async function readBindingsStore(deps) {
61294
- return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
61295
- }
61296
- async function writeBindingsStore(deps, next) {
61297
- await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
61298
- }
61299
- function resolveWrapperNodeId(_wrapperAddonId) {
61300
- return "hub";
61301
- }
61302
- function toRoutableProviderNodeId(nodeId) {
61303
- const slash = nodeId.indexOf("/");
61304
- return slash === -1 ? nodeId : nodeId.slice(0, slash);
61305
- }
61306
- function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
61307
- const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
61308
- if (!clusterCaps) return null;
61309
- for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
61310
- addonId: entry.addonId,
61311
- nodeId: entry.nodeId
61312
- };
61313
- return null;
61314
- }
61315
- function resolveDeviceType(deps, rawStore, deviceId) {
61316
- const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
61317
- if (typeof persisted === "string" && persisted.length > 0) return persisted;
61318
- const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
61319
- return typeof live === "string" && live.length > 0 ? live : void 0;
61320
- }
61321
- function resolveDevicePresence(deps, rawStore, deviceId) {
61322
- if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
61323
- const meta = rawStore.deviceMeta;
61324
- if (meta && Object.keys(meta).length > 0) return meta[String(deviceId)] === void 0 ? "absent" : "present";
61325
- return "unknown";
61326
- }
61327
- function capAppliesToDeviceType(def, deviceType) {
61328
- if (deviceType === void 0) return true;
61329
- const declared = def?.deviceTypes;
61330
- if (!declared || declared.length === 0) return true;
61331
- return declared.some((t) => t === deviceType);
61332
- }
61333
- async function getBindings(deps, input) {
61334
- const storeKey = String(input.deviceId);
61335
- const rawStore = await deps.ctx.settings.readAddonStore();
61336
- const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
61337
- const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
61338
- const presence = resolveDevicePresence(deps, rawStore, input.deviceId);
61339
- const entries = [];
61340
- const seenCaps = /* @__PURE__ */ new Set();
61341
- const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
61342
- for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
61343
- const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
61344
- const remoteNative = resolveRemote(capName);
61345
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
61346
- const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
61347
- if (wrapperAddonId === null && !nativeAddonId) {
61348
- seenCaps.add(capName);
61349
- continue;
61350
- }
61351
- entries.push({
61352
- capName,
61353
- kind: wrapperAddonId ? "wrapped" : "native",
61354
- providerAddonId: wrapperAddonId ?? nativeAddonId,
61355
- providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
61356
- nativeAddonId
61357
- });
61358
- seenCaps.add(capName);
61359
- }
61360
- if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device \u2014 returning none", { tags: { deviceId: input.deviceId } });
61361
- else if (deps.capabilityRegistry) {
61362
- const skippedForType = [];
61363
- for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
61364
- if (seenCaps.has(capName)) continue;
61365
- if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
61366
- skippedForType.push(capName);
61367
- continue;
61368
- }
61369
- const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
61370
- if (!defaultWrapperAddonId) continue;
61371
- const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
61372
- const remoteNative = resolveRemote(capName);
61373
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
61374
- entries.push({
61375
- capName,
61376
- kind: "wrapped",
61377
- providerAddonId: defaultWrapperAddonId,
61378
- providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
61379
- nativeAddonId
61380
- });
61381
- seenCaps.add(capName);
61382
- }
61383
- if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped \u2014 deviceTypes mismatch", {
61384
- tags: { deviceId: input.deviceId },
61385
- meta: {
61386
- deviceType,
61387
- skipped: skippedForType
61388
- }
61389
- });
61390
- }
61391
- if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
61392
- if (seenCaps.has(capName)) continue;
61393
- const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
61394
- entries.push({
61395
- capName,
61396
- kind: "native",
61397
- providerAddonId: nativeAddonId,
61398
- providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
61399
- nativeAddonId
61400
- });
61401
- seenCaps.add(capName);
61402
- }
61403
- const pushFed = deps.remoteNativeCaps.get(input.deviceId);
61404
- if (pushFed) for (const [capName, info] of pushFed) {
61405
- if (seenCaps.has(capName)) continue;
61406
- entries.push({
61407
- capName,
61408
- kind: "native",
61409
- providerAddonId: info.addonId,
61410
- providerNodeId: toRoutableProviderNodeId(info.nodeId),
61411
- nativeAddonId: info.addonId
61412
- });
61413
- seenCaps.add(capName);
61414
- }
61415
- const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
61416
- if (clusterCaps) for (const entry of clusterCaps) {
61417
- if (entry.deviceId !== input.deviceId) continue;
61418
- if (seenCaps.has(entry.capName)) continue;
61419
- if (!entry.addonId) continue;
61420
- const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
61421
- if (entry.nodeId === localNodeId) continue;
61422
- entries.push({
61423
- capName: entry.capName,
61424
- kind: "native",
61425
- providerAddonId: entry.addonId,
61426
- providerNodeId: toRoutableProviderNodeId(entry.nodeId),
61427
- nativeAddonId: entry.addonId
61428
- });
61429
- seenCaps.add(entry.capName);
61430
- }
61431
- return {
61432
- deviceId: input.deviceId,
61433
- entries
61434
- };
61435
- }
61436
- async function getAllBindings(deps) {
61437
- const store = await deps.ctx.settings.readAddonStore();
61438
- const ids = /* @__PURE__ */ new Set();
61439
- for (const key of Object.keys(store.deviceMeta ?? {})) {
61440
- const id = Number(key);
61441
- if (Number.isInteger(id)) ids.add(id);
61442
- }
61443
- const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
61444
- for (const device of registered) ids.add(device.id);
61445
- if (ids.size === 0) {
61446
- deps.ctx.logger.warn("getAllBindings found no devices \u2014 warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
61447
- return [];
61448
- }
61449
- const out = [];
61450
- for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
61451
- return out;
61452
- }
61453
- async function lookupPersistedStableId(deps, deviceId) {
61454
- return ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.stableId;
61455
- }
61456
61731
  async function getDeviceAggregate(deps, deviceId, kind) {
61457
61732
  const registry = deps.capabilityRegistry;
61458
61733
  if (!registry) {
@@ -62009,7 +62284,7 @@ var require_device_manager_addon = __commonJS({
62009
62284
  if (wireable) caps.push(wireable);
62010
62285
  }
62011
62286
  if (input.includeSynthesizable === true) {
62012
- const deviceType = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.type;
62287
+ const deviceType = (await deps.bindingsDeps.rows.get(deviceId))?.meta.type;
62013
62288
  if (deviceType !== void 0) for (const def of require_dist10.ALL_CAPABILITY_DEFINITIONS) {
62014
62289
  if (seen.has(def.name)) continue;
62015
62290
  if (def.scope !== "device" || def.kind === "wrapper") continue;
@@ -62097,20 +62372,10 @@ var require_device_manager_addon = __commonJS({
62097
62372
  return chain;
62098
62373
  }
62099
62374
  };
62100
- async function stampIntegrationId(metaStore, settings, ctx, deviceId, integrationId) {
62375
+ async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
62101
62376
  await metaStore.withMetaWriteLock(async () => {
62102
- const persisted = await metaStore.resolvePersistedById(deviceId);
62103
- if (!persisted) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
62104
- const { meta: m } = persisted;
62105
- const key = String(deviceId);
62106
- const allMeta = await metaStore.readMeta();
62107
- await settings.writeAddonStore({ deviceMeta: {
62108
- ...allMeta,
62109
- [key]: {
62110
- ...m,
62111
- integrationId
62112
- }
62113
- } });
62377
+ if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
62378
+ await metaStore.rows.patch(deviceId, { integrationId });
62114
62379
  });
62115
62380
  ctx.eventBus.emit({
62116
62381
  id: (0, node_crypto.randomUUID)(),
@@ -62130,70 +62395,54 @@ var require_device_manager_addon = __commonJS({
62130
62395
  async function allocateDeviceId(pctx, input) {
62131
62396
  const { addonId, stableId } = input;
62132
62397
  return await pctx.metaStore.withMetaWriteLock(async () => {
62133
- const meta = await pctx.metaStore.readMeta();
62134
- const existing = Object.values(meta).find((m) => m.addonId === addonId && m.stableId === stableId);
62135
- if (existing) return { id: existing.id };
62398
+ const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
62399
+ if (existing) return { id: existing.meta.id };
62136
62400
  const id = await pctx.metaStore.allocateNextDeviceId();
62137
- await pctx.settings.writeAddonStore({ deviceMeta: {
62138
- ...meta,
62139
- [String(id)]: {
62140
- addonId,
62141
- stableId,
62142
- type: "generic",
62143
- name: stableId,
62144
- location: null,
62145
- disabled: false,
62146
- parentDeviceId: null,
62147
- id
62148
- }
62149
- } });
62401
+ await pctx.metaStore.rows.upsert({
62402
+ addonId,
62403
+ stableId,
62404
+ type: "generic",
62405
+ name: stableId,
62406
+ location: null,
62407
+ disabled: false,
62408
+ parentDeviceId: null,
62409
+ id
62410
+ }, {
62411
+ registered: false,
62412
+ metadata: null
62413
+ });
62150
62414
  return { id };
62151
62415
  });
62152
62416
  }
62153
62417
  async function registerDevice(pctx, input) {
62154
62418
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
62155
- const key = String(id);
62156
62419
  const featuresArr = Array.isArray(features) ? [...features] : [];
62157
62420
  const { isFirstRegistration, exportFingerprint, fingerprintChanged } = await pctx.metaStore.withMetaWriteLock(async () => {
62158
- const index = await pctx.metaStore.readIndex();
62159
- const existing = index[addonId] ?? [];
62160
- const wasInIndex = existing.includes(stableId);
62161
- if (!wasInIndex) await pctx.settings.writeAddonStore({ deviceIndex: {
62162
- ...index,
62163
- [addonId]: [...existing, stableId]
62164
- } });
62165
- const meta = await pctx.metaStore.readMeta();
62166
- const existingMeta = meta[key];
62421
+ const existingRow = await pctx.metaStore.rows.get(id);
62422
+ const existingMeta = existingRow?.meta;
62423
+ const wasRegistered = existingRow?.registered ?? false;
62167
62424
  const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
62168
62425
  const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
62169
- const isFirst = !existingMeta || !wasInIndex;
62426
+ const isFirst = !existingMeta || !wasRegistered;
62170
62427
  const fingerprint = (0, _camstack_types_node.canonicalDeviceFingerprint)({
62171
62428
  deviceId: id,
62172
62429
  deviceType: type,
62173
62430
  features: featuresArr
62174
62431
  });
62175
- await pctx.settings.writeAddonStore({ deviceMeta: {
62176
- ...meta,
62177
- [key]: {
62178
- addonId,
62179
- stableId,
62180
- type,
62181
- name: resolvedName,
62182
- userNamed: wasUserNamed,
62183
- location: existingMeta?.location ?? null,
62184
- disabled: existingMeta?.disabled ?? false,
62185
- ...existingMeta?.integrationId !== void 0 ? { integrationId: existingMeta.integrationId } : {},
62186
- ...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
62187
- ...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
62188
- ...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
62189
- ...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
62190
- ...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
62191
- parentDeviceId,
62192
- id,
62193
- features: featuresArr,
62194
- exportFingerprint: fingerprint
62195
- }
62196
- } });
62432
+ await pctx.metaStore.rows.upsertRegistration({
62433
+ deviceId: id,
62434
+ addonId,
62435
+ stableId,
62436
+ type,
62437
+ name: resolvedName,
62438
+ userNamed: wasUserNamed,
62439
+ location: existingMeta?.location ?? null,
62440
+ disabled: existingMeta?.disabled ?? false,
62441
+ parentDeviceId: parentDeviceId ?? null,
62442
+ registered: true,
62443
+ features: featuresArr,
62444
+ exportFingerprint: fingerprint
62445
+ });
62197
62446
  return {
62198
62447
  isFirstRegistration: isFirst,
62199
62448
  exportFingerprint: fingerprint,
@@ -62253,26 +62502,9 @@ var require_device_manager_addon = __commonJS({
62253
62502
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62254
62503
  if (!persisted) return;
62255
62504
  const { addonId, stableId, meta: persistedMeta } = persisted;
62256
- const key = String(deviceId);
62257
62505
  const deviceName = persistedMeta.name;
62258
62506
  await pctx.metaStore.withMetaWriteLock(async () => {
62259
- const index = await pctx.metaStore.readIndex();
62260
- const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
62261
- const updatedIndex = remaining.length > 0 ? {
62262
- ...index,
62263
- [addonId]: remaining
62264
- } : (() => {
62265
- const { [addonId]: _removed, ...rest } = index;
62266
- return rest;
62267
- })();
62268
- await pctx.settings.writeAddonStore({ deviceIndex: updatedIndex });
62269
- const { [key]: _removedMeta, ...restMeta } = await pctx.metaStore.readMeta();
62270
- await pctx.settings.writeAddonStore({ deviceMeta: restMeta });
62271
- const map = await pctx.metaStore.readMetadataMap();
62272
- if (key in map) {
62273
- const { [key]: _removedMetadata, ...restMap } = map;
62274
- await pctx.settings.writeAddonStore({ deviceMetadata: restMap });
62275
- }
62507
+ await pctx.metaStore.rows.remove(deviceId);
62276
62508
  });
62277
62509
  await pctx.settings.clearDeviceStore(deviceId);
62278
62510
  await pctx.settings.clearDeviceRuntimeState(deviceId);
@@ -62316,11 +62548,10 @@ var require_device_manager_addon = __commonJS({
62316
62548
  }
62317
62549
  async function loadMeta(pctx, input) {
62318
62550
  const { deviceId } = input;
62319
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62320
- if (!persisted) return null;
62321
- const { addonId, stableId, meta: m } = persisted;
62322
- const key = String(deviceId);
62323
- const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
62551
+ const row = await pctx.metaStore.getRow(deviceId);
62552
+ if (row === null) return null;
62553
+ const { meta: m, metadata } = row;
62554
+ const { addonId, stableId } = m;
62324
62555
  return {
62325
62556
  id: m.id,
62326
62557
  stableId,
@@ -62343,32 +62574,26 @@ var require_device_manager_addon = __commonJS({
62343
62574
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62344
62575
  if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
62345
62576
  const { meta: m } = persisted;
62346
- const key = String(deviceId);
62347
62577
  const oldName = m.name;
62348
- const allMeta = await pctx.metaStore.readMeta();
62349
- const nextMeta = {
62350
- ...allMeta,
62351
- [key]: {
62352
- ...m,
62353
- name,
62354
- userNamed: true
62578
+ await pctx.metaStore.rows.patch(deviceId, {
62579
+ name,
62580
+ userNamed: true
62581
+ });
62582
+ if (oldName.length > 0 && oldName !== name) {
62583
+ const candidates = /* @__PURE__ */ new Map();
62584
+ for (const row of await pctx.metaStore.rows.listByParent(deviceId)) candidates.set(row.meta.id, row.meta);
62585
+ for (const row of await pctx.metaStore.rows.listAll()) if (row.meta.linkDeviceId === deviceId) candidates.set(row.meta.id, row.meta);
62586
+ candidates.delete(deviceId);
62587
+ for (const childMeta of candidates.values()) {
62588
+ if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
62589
+ const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
62590
+ await pctx.metaStore.rows.patch(childMeta.id, { name: childName });
62591
+ cascaded.push({
62592
+ id: childMeta.id,
62593
+ name: childName
62594
+ });
62355
62595
  }
62356
- };
62357
- if (oldName.length > 0 && oldName !== name) for (const [childKey, childMeta] of Object.entries(allMeta)) {
62358
- if (childKey === key) continue;
62359
- if (!(childMeta.parentDeviceId === deviceId || childMeta.linkDeviceId === deviceId)) continue;
62360
- if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
62361
- const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
62362
- nextMeta[childKey] = {
62363
- ...childMeta,
62364
- name: childName
62365
- };
62366
- cascaded.push({
62367
- id: Number(childKey),
62368
- name: childName
62369
- });
62370
62596
  }
62371
- await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
62372
62597
  });
62373
62598
  pctx.host.ctx.eventBus.emit({
62374
62599
  id: (0, node_crypto.randomUUID)(),
@@ -62402,18 +62627,8 @@ var require_device_manager_addon = __commonJS({
62402
62627
  async function setLocation(pctx, input) {
62403
62628
  const { deviceId, location } = input;
62404
62629
  await pctx.metaStore.withMetaWriteLock(async () => {
62405
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62406
- if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
62407
- const { meta: m } = persisted;
62408
- const key = String(deviceId);
62409
- const allMeta = await pctx.metaStore.readMeta();
62410
- await pctx.settings.writeAddonStore({ deviceMeta: {
62411
- ...allMeta,
62412
- [key]: {
62413
- ...m,
62414
- location
62415
- }
62416
- } });
62630
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
62631
+ await pctx.metaStore.rows.patch(deviceId, { location });
62417
62632
  });
62418
62633
  pctx.host.ctx.eventBus.emit({
62419
62634
  id: (0, node_crypto.randomUUID)(),
@@ -62433,18 +62648,8 @@ var require_device_manager_addon = __commonJS({
62433
62648
  async function setType(pctx, input) {
62434
62649
  const { deviceId, type } = input;
62435
62650
  await pctx.metaStore.withMetaWriteLock(async () => {
62436
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62437
- if (!persisted) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
62438
- const { meta: m } = persisted;
62439
- const key = String(deviceId);
62440
- const allMeta = await pctx.metaStore.readMeta();
62441
- await pctx.settings.writeAddonStore({ deviceMeta: {
62442
- ...allMeta,
62443
- [key]: {
62444
- ...m,
62445
- type
62446
- }
62447
- } });
62651
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
62652
+ await pctx.metaStore.rows.patch(deviceId, { type });
62448
62653
  });
62449
62654
  pctx.host.ctx.eventBus.emit({
62450
62655
  id: (0, node_crypto.randomUUID)(),
@@ -62467,18 +62672,8 @@ var require_device_manager_addon = __commonJS({
62467
62672
  async function setLinkDeviceId(pctx, input) {
62468
62673
  const { deviceId, linkDeviceId } = input;
62469
62674
  await pctx.metaStore.withMetaWriteLock(async () => {
62470
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62471
- if (!persisted) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
62472
- const { meta: m } = persisted;
62473
- const key = String(deviceId);
62474
- const allMeta = await pctx.metaStore.readMeta();
62475
- await pctx.settings.writeAddonStore({ deviceMeta: {
62476
- ...allMeta,
62477
- [key]: {
62478
- ...m,
62479
- linkDeviceId
62480
- }
62481
- } });
62675
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
62676
+ await pctx.metaStore.rows.patch(deviceId, { linkDeviceId });
62482
62677
  });
62483
62678
  pctx.host.ctx.eventBus.emit({
62484
62679
  id: (0, node_crypto.randomUUID)(),
@@ -62498,18 +62693,8 @@ var require_device_manager_addon = __commonJS({
62498
62693
  async function setPrimaryChildEntityId(pctx, input) {
62499
62694
  const { deviceId, primaryChildEntityId } = input;
62500
62695
  await pctx.metaStore.withMetaWriteLock(async () => {
62501
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62502
- if (!persisted) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
62503
- const { meta: m } = persisted;
62504
- const key = String(deviceId);
62505
- const allMeta = await pctx.metaStore.readMeta();
62506
- await pctx.settings.writeAddonStore({ deviceMeta: {
62507
- ...allMeta,
62508
- [key]: {
62509
- ...m,
62510
- primaryChildEntityId
62511
- }
62512
- } });
62696
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
62697
+ await pctx.metaStore.rows.patch(deviceId, { primaryChildEntityId });
62513
62698
  });
62514
62699
  pctx.host.ctx.eventBus.emit({
62515
62700
  id: (0, node_crypto.randomUUID)(),
@@ -62529,18 +62714,8 @@ var require_device_manager_addon = __commonJS({
62529
62714
  async function setChildLayout(pctx, input) {
62530
62715
  const { deviceId, childLayout } = input;
62531
62716
  await pctx.metaStore.withMetaWriteLock(async () => {
62532
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62533
- if (!persisted) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
62534
- const { meta: m } = persisted;
62535
- const key = String(deviceId);
62536
- const allMeta = await pctx.metaStore.readMeta();
62537
- await pctx.settings.writeAddonStore({ deviceMeta: {
62538
- ...allMeta,
62539
- [key]: {
62540
- ...m,
62541
- childLayout
62542
- }
62543
- } });
62717
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
62718
+ await pctx.metaStore.rows.patch(deviceId, { childLayout });
62544
62719
  });
62545
62720
  pctx.host.ctx.eventBus.emit({
62546
62721
  id: (0, node_crypto.randomUUID)(),
@@ -62560,18 +62735,8 @@ var require_device_manager_addon = __commonJS({
62560
62735
  async function setRole(pctx, input) {
62561
62736
  const { deviceId, role } = input;
62562
62737
  await pctx.metaStore.withMetaWriteLock(async () => {
62563
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62564
- if (!persisted) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
62565
- const { meta: m } = persisted;
62566
- const key = String(deviceId);
62567
- const allMeta = await pctx.metaStore.readMeta();
62568
- await pctx.settings.writeAddonStore({ deviceMeta: {
62569
- ...allMeta,
62570
- [key]: {
62571
- ...m,
62572
- role
62573
- }
62574
- } });
62738
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
62739
+ await pctx.metaStore.rows.patch(deviceId, { role });
62575
62740
  });
62576
62741
  pctx.host.ctx.eventBus.emit({
62577
62742
  id: (0, node_crypto.randomUUID)(),
@@ -62602,19 +62767,8 @@ var require_device_manager_addon = __commonJS({
62602
62767
  const { deviceId, display } = input;
62603
62768
  const normalized = display === null ? null : normalizeDisplayOverride(display);
62604
62769
  await pctx.metaStore.withMetaWriteLock(async () => {
62605
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62606
- if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
62607
- const { meta: m } = persisted;
62608
- const key = String(deviceId);
62609
- const allMeta = await pctx.metaStore.readMeta();
62610
- const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
62611
- ...m,
62612
- display: normalized
62613
- };
62614
- await pctx.settings.writeAddonStore({ deviceMeta: {
62615
- ...allMeta,
62616
- [key]: nextRow
62617
- } });
62770
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
62771
+ await pctx.metaStore.rows.patch(deviceId, { display: normalized });
62618
62772
  });
62619
62773
  pctx.host.ctx.eventBus.emit({
62620
62774
  id: (0, node_crypto.randomUUID)(),
@@ -62644,24 +62798,15 @@ var require_device_manager_addon = __commonJS({
62644
62798
  async function applyInitialMeta(pctx, input) {
62645
62799
  const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
62646
62800
  await pctx.metaStore.withMetaWriteLock(async () => {
62647
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62648
- if (!persisted) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
62649
- const { meta: m } = persisted;
62650
- const key = String(deviceId);
62651
- const allMeta = await pctx.metaStore.readMeta();
62652
- const merged = {
62653
- ...m,
62801
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
62802
+ await pctx.metaStore.rows.patch(deviceId, {
62654
62803
  ...name !== void 0 ? { name } : {},
62655
62804
  ...location !== void 0 ? { location } : {},
62656
62805
  ...type !== void 0 ? { type } : {},
62657
62806
  ...integrationId !== void 0 ? { integrationId } : {},
62658
62807
  ...linkDeviceId !== void 0 ? { linkDeviceId } : {},
62659
62808
  ...role !== void 0 ? { role } : {}
62660
- };
62661
- await pctx.settings.writeAddonStore({ deviceMeta: {
62662
- ...allMeta,
62663
- [key]: merged
62664
- } });
62809
+ });
62665
62810
  });
62666
62811
  const emitMetaChanged = (field, value) => {
62667
62812
  pctx.host.ctx.eventBus.emit({
@@ -62689,10 +62834,9 @@ var require_device_manager_addon = __commonJS({
62689
62834
  async function setMetadata(pctx, input) {
62690
62835
  const { deviceId, patch } = input;
62691
62836
  const result = await pctx.metaStore.withMetaWriteLock(async () => {
62692
- if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
62693
- const key = String(deviceId);
62694
- const map = await pctx.metaStore.readMetadataMap();
62695
- const next = { ...map[key] ?? {} };
62837
+ const row = await pctx.metaStore.rows.get(deviceId);
62838
+ if (row === null) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
62839
+ const next = { ...row.metadata ?? {} };
62696
62840
  let changed = false;
62697
62841
  for (const [k, v] of Object.entries(patch)) if (v === null) {
62698
62842
  if (k in next) {
@@ -62705,10 +62849,7 @@ var require_device_manager_addon = __commonJS({
62705
62849
  }
62706
62850
  if (!changed) return { changed: false };
62707
62851
  const hasFields = Object.keys(next).length > 0;
62708
- const updatedMap = { ...map };
62709
- if (hasFields) updatedMap[key] = next;
62710
- else delete updatedMap[key];
62711
- await pctx.settings.writeAddonStore({ deviceMetadata: updatedMap });
62852
+ await pctx.metaStore.rows.patch(deviceId, { metadata: hasFields ? next : null });
62712
62853
  return {
62713
62854
  changed: true,
62714
62855
  finalMeta: hasFields ? next : null
@@ -62736,15 +62877,7 @@ var require_device_manager_addon = __commonJS({
62736
62877
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62737
62878
  if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
62738
62879
  const { meta: m } = persisted;
62739
- const key = String(deviceId);
62740
- const allMeta = await pctx.metaStore.readMeta();
62741
- await pctx.settings.writeAddonStore({ deviceMeta: {
62742
- ...allMeta,
62743
- [key]: {
62744
- ...m,
62745
- disabled
62746
- }
62747
- } });
62880
+ await pctx.metaStore.rows.patch(deviceId, { disabled });
62748
62881
  return {
62749
62882
  changed: (m.disabled ?? false) !== disabled,
62750
62883
  integrationId: m.integrationId ?? ""
@@ -62787,9 +62920,7 @@ var require_device_manager_addon = __commonJS({
62787
62920
  return data;
62788
62921
  }
62789
62922
  async function listLocations(pctx) {
62790
- const store = await pctx.settings.readAddonStore();
62791
- const meta = store.deviceMeta ?? {};
62792
- const locations = store.locations ?? [];
62923
+ const locations = (await pctx.metaStore.readStore()).locations ?? [];
62793
62924
  const seen = /* @__PURE__ */ new Map();
62794
62925
  const consider = (raw) => {
62795
62926
  if (typeof raw !== "string") return;
@@ -62799,38 +62930,34 @@ var require_device_manager_addon = __commonJS({
62799
62930
  if (!seen.has(key)) seen.set(key, trimmed);
62800
62931
  };
62801
62932
  for (const label of locations) consider(label);
62802
- for (const m of Object.values(meta)) consider(m.location);
62933
+ for (const row of await pctx.metaStore.rows.listAll()) consider(row.meta.location);
62803
62934
  return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
62804
62935
  }
62805
62936
  async function addLocation(pctx, input) {
62806
62937
  const trimmed = input.name.trim();
62807
62938
  if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
62808
- const current = (await pctx.settings.readAddonStore()).locations ?? [];
62939
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62809
62940
  if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
62810
62941
  await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
62811
62942
  }
62812
62943
  async function removeLocation(pctx, input) {
62813
62944
  const trimmed = input.name.trim();
62814
62945
  if (trimmed.length === 0) return;
62815
- const store = await pctx.settings.readAddonStore();
62816
- const current = store.locations ?? [];
62946
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62817
62947
  const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
62818
62948
  if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
62819
62949
  if (input.cascade !== true) return;
62820
- const meta = store.deviceMeta ?? {};
62821
- const updates = { ...meta };
62822
- const cleared = [];
62823
- for (const [key, m] of Object.entries(meta)) {
62824
- if (typeof m.location !== "string") continue;
62825
- if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
62826
- updates[key] = {
62827
- ...m,
62828
- location: null
62829
- };
62830
- cleared.push(m.id);
62831
- }
62832
- if (cleared.length === 0) return;
62833
- await pctx.settings.writeAddonStore({ deviceMeta: updates });
62950
+ const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
62951
+ const out = [];
62952
+ for (const row of await pctx.metaStore.rows.listAll()) {
62953
+ const location = row.meta.location;
62954
+ if (typeof location !== "string") continue;
62955
+ if (location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
62956
+ await pctx.metaStore.rows.patch(row.meta.id, { location: null });
62957
+ out.push(row.meta.id);
62958
+ }
62959
+ return out;
62960
+ });
62834
62961
  for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
62835
62962
  id: (0, node_crypto.randomUUID)(),
62836
62963
  timestamp: /* @__PURE__ */ new Date(),
@@ -62846,40 +62973,65 @@ var require_device_manager_addon = __commonJS({
62846
62973
  }
62847
62974
  });
62848
62975
  }
62976
+ function decodeAddonStore(raw) {
62977
+ const nextDeviceId = raw["nextDeviceId"];
62978
+ const locations = raw["locations"];
62979
+ const roleDisplayDefaults = raw["roleDisplayDefaults"];
62980
+ return {
62981
+ ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
62982
+ ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
62983
+ ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
62984
+ };
62985
+ }
62986
+ function isRoleDisplayDefaults(value) {
62987
+ return value !== null && typeof value === "object" && !Array.isArray(value);
62988
+ }
62849
62989
  var DeviceMetaStore = class {
62850
62990
  settings;
62851
62991
  registry;
62992
+ rows;
62852
62993
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
62853
- * The persisted meta store is authoritative but reads are async; hub-side
62994
+ * The persisted row store is authoritative but reads are async; hub-side
62854
62995
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
62855
62996
  * ownership without awaiting. Kept in sync with every register/remove and
62856
62997
  * warmed from persistence on boot. */
62857
62998
  idToAddonId = /* @__PURE__ */ new Map();
62858
- /** Serialises every read-modify-write of the deviceMeta / deviceIndex blob
62859
- * through one promise chain (see `withMetaWriteLock`). Per-instance state
62860
- * identical to the former `onInitialize` closure variable. */
62999
+ /** Serialises every read-modify-write of a device row through one promise
63000
+ * chain (see `withMetaWriteLock`). Per-instance state. */
62861
63001
  metaWriteChain = Promise.resolve();
62862
- constructor(settings, registry) {
63002
+ constructor(settings, registry, rows) {
62863
63003
  this.settings = settings;
62864
63004
  this.registry = registry;
63005
+ this.rows = rows;
62865
63006
  }
63007
+ /** The read currently in flight, or null. Never a settled value — see
63008
+ * {@link readStore}. */
63009
+ inFlightRead = null;
63010
+ /**
63011
+ * The addon's own settings row set — `nextDeviceId`, `roleDisplayDefaults`,
63012
+ * `locations`. NOT the fleet: no device has lived here since the flatten.
63013
+ *
63014
+ * **Concurrent callers join the read already in flight.** This is not a
63015
+ * cache and nothing survives settlement: a caller that awaited the running
63016
+ * promise could not have observed anything older than its result, so the
63017
+ * only thing that changes is cost.
63018
+ *
63019
+ * A rejection is NOT latched: the slot is cleared before the promise
63020
+ * settles either way, so a failed read costs the joiners that one failure
63021
+ * and the next caller reaches the store again.
63022
+ */
62866
63023
  readStore = async () => {
62867
- return await this.settings.readAddonStore();
62868
- };
62869
- readIndex = async () => {
62870
- return (await this.readStore()).deviceIndex ?? {};
62871
- };
62872
- readMeta = async () => {
62873
- return (await this.readStore()).deviceMeta ?? {};
62874
- };
62875
- /** Hardware-identity metadata map. Lives in a sibling key on the
62876
- * device-manager addon store so its writers (`setMetadata`) never
62877
- * collide with the lifecycle writers on `deviceMeta`
62878
- * (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
62879
- * Single-writer per row eliminates the "writer X clobbers writer
62880
- * Y's field" bug class — `setMetadata` is the only producer. */
62881
- readMetadataMap = async () => {
62882
- return (await this.readStore()).deviceMetadata ?? {};
63024
+ const existing = this.inFlightRead;
63025
+ if (existing !== null) return existing;
63026
+ const read = (async () => {
63027
+ try {
63028
+ return decodeAddonStore(await this.settings.readAddonStore());
63029
+ } finally {
63030
+ this.inFlightRead = null;
63031
+ }
63032
+ })();
63033
+ this.inFlightRead = read;
63034
+ return read;
62883
63035
  };
62884
63036
  withMetaWriteLock = async (fn) => {
62885
63037
  const previous = this.metaWriteChain;
@@ -62897,31 +63049,36 @@ var require_device_manager_addon = __commonJS({
62897
63049
  release();
62898
63050
  }
62899
63051
  };
63052
+ /** The whole persisted row for one device, or `null`. */
63053
+ getRow = async (deviceId) => this.rows.get(deviceId);
62900
63054
  /**
62901
63055
  * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
62902
- * Scans persisted meta — live IDevice lookup (hub registry) is handled
63056
+ * Reads the device's own row — live IDevice lookup (hub registry) is handled
62903
63057
  * separately per call site so callers can decide whether to route to
62904
63058
  * an in-process driver or to the cross-process `device-ops` bridge.
62905
63059
  * Returns null when no device with that id is known to the hub.
62906
63060
  */
62907
63061
  resolvePersistedById = async (deviceId) => {
62908
- const m = (await this.readMeta())[String(deviceId)];
62909
- if (!m) return null;
63062
+ const row = await this.rows.get(deviceId);
63063
+ if (row === null) return null;
62910
63064
  return {
62911
- addonId: m.addonId,
62912
- stableId: m.stableId,
62913
- meta: m
63065
+ addonId: row.meta.addonId,
63066
+ stableId: row.meta.stableId,
63067
+ meta: row.meta
62914
63068
  };
62915
63069
  };
63070
+ /** The device's hardware-identity metadata blob, or `null`. */
63071
+ readMetadata = async (deviceId) => {
63072
+ return (await this.rows.get(deviceId))?.metadata ?? null;
63073
+ };
62916
63074
  /** Direct children of a device: the union of the live registry's children
62917
- * and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
63075
+ * and the persisted rows whose `parentDeviceId` is `parentId`, deduplicated
62918
63076
  * and excluding self. Shared by the `remove` cascade and the `resetToSource`
62919
63077
  * resync purge (#19). */
62920
63078
  directChildIds = async (parentId) => {
62921
63079
  const ids = /* @__PURE__ */ new Set();
62922
63080
  if (this.registry) for (const c of this.registry.getChildren(parentId)) ids.add(c.id);
62923
- const meta = await this.readMeta();
62924
- for (const m of Object.values(meta)) if (m.parentDeviceId === parentId) ids.add(m.id);
63081
+ for (const row of await this.rows.listByParent(parentId)) ids.add(row.meta.id);
62925
63082
  ids.delete(parentId);
62926
63083
  return [...ids];
62927
63084
  };
@@ -62931,6 +63088,476 @@ var require_device_manager_addon = __commonJS({
62931
63088
  return current;
62932
63089
  };
62933
63090
  };
63091
+ var DEVICE_ROWS_COLLECTION = "device-manager:devices";
63092
+ var DEVICE_ROWS_FLEET_LIMIT = 2e4;
63093
+ var DEVICE_ROWS_COLUMNS = [
63094
+ {
63095
+ name: "id",
63096
+ type: "TEXT",
63097
+ primaryKey: true,
63098
+ notNull: true
63099
+ },
63100
+ {
63101
+ name: "deviceId",
63102
+ type: "INTEGER",
63103
+ notNull: true
63104
+ },
63105
+ {
63106
+ name: "addonId",
63107
+ type: "TEXT",
63108
+ notNull: true
63109
+ },
63110
+ {
63111
+ name: "stableId",
63112
+ type: "TEXT",
63113
+ notNull: true
63114
+ },
63115
+ {
63116
+ name: "type",
63117
+ type: "TEXT",
63118
+ notNull: true
63119
+ },
63120
+ {
63121
+ name: "name",
63122
+ type: "TEXT",
63123
+ notNull: true
63124
+ },
63125
+ {
63126
+ name: "userNamed",
63127
+ type: "BOOLEAN"
63128
+ },
63129
+ {
63130
+ name: "location",
63131
+ type: "TEXT"
63132
+ },
63133
+ {
63134
+ name: "disabled",
63135
+ type: "BOOLEAN",
63136
+ notNull: true,
63137
+ defaultValue: false
63138
+ },
63139
+ {
63140
+ name: "parentDeviceId",
63141
+ type: "INTEGER"
63142
+ },
63143
+ {
63144
+ name: "registered",
63145
+ type: "BOOLEAN",
63146
+ notNull: true,
63147
+ defaultValue: false
63148
+ },
63149
+ {
63150
+ name: "features",
63151
+ type: "JSON"
63152
+ },
63153
+ {
63154
+ name: "exportFingerprint",
63155
+ type: "TEXT"
63156
+ },
63157
+ {
63158
+ name: "integrationId",
63159
+ type: "TEXT"
63160
+ },
63161
+ {
63162
+ name: "linkDeviceId",
63163
+ type: "INTEGER"
63164
+ },
63165
+ {
63166
+ name: "primaryChildEntityId",
63167
+ type: "TEXT"
63168
+ },
63169
+ {
63170
+ name: "childLayout",
63171
+ type: "JSON"
63172
+ },
63173
+ {
63174
+ name: "role",
63175
+ type: "TEXT"
63176
+ },
63177
+ {
63178
+ name: "display",
63179
+ type: "JSON"
63180
+ },
63181
+ {
63182
+ name: "metadata",
63183
+ type: "JSON"
63184
+ }
63185
+ ];
63186
+ var DEVICE_ROWS_INDEXES = [
63187
+ {
63188
+ name: "idx_dm_devices_addon_stable",
63189
+ columns: ["addonId", "stableId"]
63190
+ },
63191
+ {
63192
+ name: "idx_dm_devices_parent",
63193
+ columns: ["parentDeviceId"]
63194
+ },
63195
+ {
63196
+ name: "idx_dm_devices_integration",
63197
+ columns: ["integrationId"]
63198
+ }
63199
+ ];
63200
+ function deviceRowBackendOf(client) {
63201
+ return {
63202
+ declareCollection: async (input) => {
63203
+ await client.declareCollection.mutate(input);
63204
+ },
63205
+ get: (input) => client.get.query(input),
63206
+ set: async (input) => {
63207
+ await client.set.mutate(input);
63208
+ },
63209
+ query: (input) => client.query.query(input),
63210
+ updateWhere: (input) => client.updateWhere.mutate(input),
63211
+ delete: async (input) => {
63212
+ await client.delete.mutate(input);
63213
+ },
63214
+ count: (input) => client.count.query(input)
63215
+ };
63216
+ }
63217
+ function isPlainObject3(value) {
63218
+ return value !== null && typeof value === "object" && !Array.isArray(value);
63219
+ }
63220
+ function readString(value) {
63221
+ return typeof value === "string" ? value : void 0;
63222
+ }
63223
+ function readNumber(value) {
63224
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
63225
+ }
63226
+ function readBoolean(value) {
63227
+ if (typeof value === "boolean") return value;
63228
+ if (value === 1) return true;
63229
+ if (value === 0) return false;
63230
+ }
63231
+ function readStringArray(value) {
63232
+ if (!Array.isArray(value)) return void 0;
63233
+ const out = [];
63234
+ for (const entry of value) if (typeof entry === "string") out.push(entry);
63235
+ return out;
63236
+ }
63237
+ function decodeDeviceRow(data) {
63238
+ const deviceId = readNumber(data["deviceId"]);
63239
+ const addonId = readString(data["addonId"]);
63240
+ const stableId = readString(data["stableId"]);
63241
+ const type = readString(data["type"]);
63242
+ const name = readString(data["name"]);
63243
+ if (deviceId === void 0 || addonId === void 0 || stableId === void 0 || type === void 0 || name === void 0) return null;
63244
+ const location = readString(data["location"]);
63245
+ const parentDeviceId = readNumber(data["parentDeviceId"]);
63246
+ const userNamed = readBoolean(data["userNamed"]);
63247
+ const features = readStringArray(data["features"]);
63248
+ const exportFingerprint = readString(data["exportFingerprint"]);
63249
+ const integrationId = readString(data["integrationId"]);
63250
+ const linkDeviceId = readNumber(data["linkDeviceId"]);
63251
+ const primaryChildEntityId = readString(data["primaryChildEntityId"]);
63252
+ const role = readString(data["role"]);
63253
+ const rawChildLayout = data["childLayout"];
63254
+ const rawDisplay = data["display"];
63255
+ const rawMetadata = data["metadata"];
63256
+ return {
63257
+ meta: {
63258
+ id: deviceId,
63259
+ addonId,
63260
+ stableId,
63261
+ type,
63262
+ name,
63263
+ location: location ?? null,
63264
+ disabled: readBoolean(data["disabled"]) ?? false,
63265
+ parentDeviceId: parentDeviceId ?? null,
63266
+ ...userNamed !== void 0 ? { userNamed } : {},
63267
+ ...features !== void 0 ? { features } : {},
63268
+ ...exportFingerprint !== void 0 ? { exportFingerprint } : {},
63269
+ ...integrationId !== void 0 ? { integrationId } : {},
63270
+ ...linkDeviceId !== void 0 ? { linkDeviceId } : {},
63271
+ ...primaryChildEntityId !== void 0 ? { primaryChildEntityId } : {},
63272
+ ...isChildLayout(rawChildLayout) ? { childLayout: rawChildLayout } : {},
63273
+ ...role !== void 0 ? { role } : {},
63274
+ ...isPlainObject3(rawDisplay) ? { display: toDisplayOverride(rawDisplay) } : {}
63275
+ },
63276
+ metadata: isPlainObject3(rawMetadata) ? rawMetadata : null,
63277
+ registered: readBoolean(data["registered"]) ?? false
63278
+ };
63279
+ }
63280
+ function isChildLayout(value) {
63281
+ return Array.isArray(value);
63282
+ }
63283
+ function toDisplayOverride(value) {
63284
+ return value;
63285
+ }
63286
+ function encodeDeviceRow(meta, extra = {}) {
63287
+ const row = {
63288
+ id: String(meta.id),
63289
+ deviceId: meta.id,
63290
+ addonId: meta.addonId,
63291
+ stableId: meta.stableId,
63292
+ type: meta.type,
63293
+ name: meta.name,
63294
+ userNamed: meta.userNamed ?? null,
63295
+ location: meta.location,
63296
+ disabled: meta.disabled,
63297
+ parentDeviceId: meta.parentDeviceId,
63298
+ registered: extra.registered ?? false,
63299
+ features: meta.features === void 0 ? null : [...meta.features],
63300
+ exportFingerprint: meta.exportFingerprint ?? null,
63301
+ integrationId: meta.integrationId ?? null,
63302
+ linkDeviceId: meta.linkDeviceId ?? null,
63303
+ primaryChildEntityId: meta.primaryChildEntityId ?? null,
63304
+ childLayout: meta.childLayout ?? null,
63305
+ role: meta.role ?? null,
63306
+ display: meta.display ?? null
63307
+ };
63308
+ if (extra.metadata !== void 0) row["metadata"] = extra.metadata;
63309
+ return row;
63310
+ }
63311
+ function encodeDeviceRowPatch(patch) {
63312
+ const row = {};
63313
+ for (const [key, value] of Object.entries(patch)) {
63314
+ if (value === void 0) continue;
63315
+ row[key] = value;
63316
+ }
63317
+ return row;
63318
+ }
63319
+ var DeviceRowStore = class {
63320
+ backend;
63321
+ logger;
63322
+ declared = null;
63323
+ constructor(backend, logger) {
63324
+ this.backend = backend;
63325
+ this.logger = logger;
63326
+ }
63327
+ /**
63328
+ * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
63329
+ * concurrent first-callers issue one declaration rather than N. A rejection
63330
+ * is not latched — the slot is cleared so the next caller retries instead of
63331
+ * inheriting a dead collection forever.
63332
+ */
63333
+ async declare() {
63334
+ const existing = this.declared;
63335
+ if (existing !== null) return existing;
63336
+ const run = (async () => {
63337
+ try {
63338
+ await this.backend.declareCollection({
63339
+ collection: DEVICE_ROWS_COLLECTION,
63340
+ columns: DEVICE_ROWS_COLUMNS,
63341
+ indexes: DEVICE_ROWS_INDEXES
63342
+ });
63343
+ } catch (err) {
63344
+ this.declared = null;
63345
+ throw err;
63346
+ }
63347
+ })();
63348
+ this.declared = run;
63349
+ return run;
63350
+ }
63351
+ /** One device, by numeric id. `null` when the fleet does not know it. */
63352
+ async get(deviceId) {
63353
+ await this.declare();
63354
+ const raw = await this.backend.get({
63355
+ collection: DEVICE_ROWS_COLLECTION,
63356
+ key: String(deviceId)
63357
+ });
63358
+ if (!isPlainObject3(raw)) return null;
63359
+ const decoded = decodeDeviceRow(raw);
63360
+ if (decoded === null) {
63361
+ this.logger.warn("device row skipped \u2014 identity fields missing", { tags: { deviceId } });
63362
+ return null;
63363
+ }
63364
+ return decoded;
63365
+ }
63366
+ /** Every device, ordered by numeric id. */
63367
+ async listAll() {
63368
+ return this.list({
63369
+ orderBy: {
63370
+ field: "deviceId",
63371
+ direction: "asc"
63372
+ },
63373
+ limit: DEVICE_ROWS_FLEET_LIMIT
63374
+ });
63375
+ }
63376
+ /**
63377
+ * The device an addon knows as `stableId`, or `null`.
63378
+ *
63379
+ * `(addonId, stableId)` is the addon-facing identity — unique by
63380
+ * construction, since `allocateDeviceId` is the only thing that mints a row
63381
+ * and it returns the existing id for a pair it already knows. A second row
63382
+ * for the pair would be a corruption, so this takes the LOWEST id and says
63383
+ * so rather than picking arbitrarily.
63384
+ */
63385
+ async findByStableId(addonId, stableId) {
63386
+ const rows = await this.list({
63387
+ where: {
63388
+ addonId,
63389
+ stableId
63390
+ },
63391
+ orderBy: {
63392
+ field: "deviceId",
63393
+ direction: "asc"
63394
+ },
63395
+ limit: 2
63396
+ });
63397
+ const first = rows[0];
63398
+ if (first === void 0) return null;
63399
+ if (rows.length > 1) this.logger.warn("duplicate device rows for one (addonId, stableId) \u2014 using the lowest id", {
63400
+ tags: { deviceId: first.meta.id },
63401
+ meta: {
63402
+ addonId,
63403
+ stableId,
63404
+ ids: rows.map((r) => r.meta.id)
63405
+ }
63406
+ });
63407
+ return first;
63408
+ }
63409
+ /** Every device owned by one addon, ordered by numeric id. */
63410
+ async listByAddon(addonId) {
63411
+ return this.list({
63412
+ where: { addonId },
63413
+ orderBy: {
63414
+ field: "deviceId",
63415
+ direction: "asc"
63416
+ },
63417
+ limit: DEVICE_ROWS_FLEET_LIMIT
63418
+ });
63419
+ }
63420
+ /** Every device an integration owns, ordered by numeric id. */
63421
+ async listByIntegration(integrationId) {
63422
+ return this.list({
63423
+ where: { integrationId },
63424
+ orderBy: {
63425
+ field: "deviceId",
63426
+ direction: "asc"
63427
+ },
63428
+ limit: DEVICE_ROWS_FLEET_LIMIT
63429
+ });
63430
+ }
63431
+ /** Direct children of one device, ordered by numeric id. */
63432
+ async listByParent(parentDeviceId) {
63433
+ return this.list({
63434
+ where: { parentDeviceId },
63435
+ orderBy: {
63436
+ field: "deviceId",
63437
+ direction: "asc"
63438
+ },
63439
+ limit: DEVICE_ROWS_FLEET_LIMIT
63440
+ });
63441
+ }
63442
+ /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
63443
+ async count() {
63444
+ await this.declare();
63445
+ return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
63446
+ }
63447
+ async list(filter) {
63448
+ await this.declare();
63449
+ const records = await this.backend.query({
63450
+ collection: DEVICE_ROWS_COLLECTION,
63451
+ filter
63452
+ });
63453
+ const out = [];
63454
+ for (const record of records) {
63455
+ const decoded = decodeDeviceRow(record.data);
63456
+ if (decoded === null) {
63457
+ this.logger.warn("device row skipped \u2014 identity fields missing", { meta: { rowId: record.id } });
63458
+ continue;
63459
+ }
63460
+ out.push(decoded);
63461
+ }
63462
+ return out;
63463
+ }
63464
+ /** Insert or replace the whole identity row. */
63465
+ async upsert(meta, extra = {}) {
63466
+ await this.declare();
63467
+ await this.backend.set({
63468
+ collection: DEVICE_ROWS_COLLECTION,
63469
+ key: String(meta.id),
63470
+ value: encodeDeviceRow(meta, extra)
63471
+ });
63472
+ }
63473
+ /**
63474
+ * Write the named columns of an EXISTING row and nothing else.
63475
+ *
63476
+ * An UPDATE, deliberately not an upsert: a partial upsert would try to INSERT
63477
+ * a row carrying only the patched columns, and SQLite rejects that on the
63478
+ * `NOT NULL` identity columns before the primary-key conflict can turn it
63479
+ * into an update. Every caller resolves the row under the write lock and
63480
+ * throws when it is gone, so matching zero rows means a device was removed
63481
+ * between the resolve and the write — the patch is lost, and a lost write
63482
+ * that says nothing reads as a write that happened.
63483
+ */
63484
+ async patch(deviceId, patch) {
63485
+ const columns = encodeDeviceRowPatch(patch);
63486
+ if (Object.keys(columns).length === 0) return;
63487
+ await this.declare();
63488
+ const { updated } = await this.backend.updateWhere({
63489
+ collection: DEVICE_ROWS_COLLECTION,
63490
+ filter: { where: { id: String(deviceId) } },
63491
+ data: columns
63492
+ });
63493
+ if (updated === 0) this.logger.warn("device row patch matched no row \u2014 the device was removed under it", {
63494
+ tags: { deviceId },
63495
+ meta: { columns: Object.keys(columns) }
63496
+ });
63497
+ }
63498
+ /**
63499
+ * Insert-or-update the registration columns. See {@link DeviceRegistrationRow}
63500
+ * for why this is a separate, fully-specified statement rather than a patch.
63501
+ */
63502
+ async upsertRegistration(row) {
63503
+ await this.declare();
63504
+ await this.backend.set({
63505
+ collection: DEVICE_ROWS_COLLECTION,
63506
+ key: String(row.deviceId),
63507
+ value: {
63508
+ deviceId: row.deviceId,
63509
+ addonId: row.addonId,
63510
+ stableId: row.stableId,
63511
+ type: row.type,
63512
+ name: row.name,
63513
+ userNamed: row.userNamed,
63514
+ location: row.location,
63515
+ disabled: row.disabled,
63516
+ parentDeviceId: row.parentDeviceId,
63517
+ registered: row.registered,
63518
+ features: [...row.features],
63519
+ exportFingerprint: row.exportFingerprint
63520
+ }
63521
+ });
63522
+ }
63523
+ /** Drop the device's row. Idempotent. */
63524
+ async remove(deviceId) {
63525
+ await this.declare();
63526
+ await this.backend.delete({
63527
+ collection: DEVICE_ROWS_COLLECTION,
63528
+ key: String(deviceId)
63529
+ });
63530
+ }
63531
+ /**
63532
+ * The retirement door for the three blobs this collection replaced.
63533
+ *
63534
+ * The purge is gated on THIS collection being non-empty, and the count has to
63535
+ * be taken after `declare()` — which is why the owning addon runs it and the
63536
+ * settings engine cannot: at the engine's own boot no addon has declared
63537
+ * anything, so every successor would read as unreadable, forever.
63538
+ */
63539
+ retiredRowStore() {
63540
+ return {
63541
+ hasRow: async (spec) => {
63542
+ const raw = await this.backend.get({
63543
+ collection: spec.collection,
63544
+ key: spec.row
63545
+ });
63546
+ return raw !== void 0 && raw !== null;
63547
+ },
63548
+ deleteRow: async (spec) => {
63549
+ await this.backend.delete({
63550
+ collection: spec.collection,
63551
+ key: spec.row
63552
+ });
63553
+ },
63554
+ countRows: async (collection) => {
63555
+ await this.declare();
63556
+ return this.backend.count({ collection });
63557
+ }
63558
+ };
63559
+ }
63560
+ };
62934
63561
  function persistableBlob(blob, policyFor) {
62935
63562
  const out = {};
62936
63563
  for (const [capName, slice] of Object.entries(blob)) {
@@ -63409,12 +64036,21 @@ var require_device_manager_addon = __commonJS({
63409
64036
  }
63410
64037
  }))).filter((id) => id !== null);
63411
64038
  }
64039
+ /**
64040
+ * Row access for `device-manager:devices`. Built once in `onInitialize`
64041
+ * (which is also where the collection is declared) — every consumer reaches
64042
+ * it through {@link bindingsDeps} or the `ProviderContext`.
64043
+ */
64044
+ deviceRows = null;
63412
64045
  /** Build the dependency context the extracted binding resolvers consume. */
63413
64046
  get bindingsDeps() {
64047
+ const rows = this.deviceRows;
64048
+ if (rows === null) throw new Error("[device-manager] device row store not initialized");
63414
64049
  return {
63415
64050
  ctx: this.ctx,
63416
64051
  capabilityRegistry: this.capabilityRegistry,
63417
- remoteNativeCaps: this.remoteNativeCaps
64052
+ remoteNativeCaps: this.remoteNativeCaps,
64053
+ rows
63418
64054
  };
63419
64055
  }
63420
64056
  async getBindings(input) {
@@ -63470,17 +64106,23 @@ var require_device_manager_addon = __commonJS({
63470
64106
  if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
63471
64107
  return ops;
63472
64108
  };
63473
- const metaStore = new DeviceMetaStore(settings, registry);
64109
+ const settingsStoreApi = this.ctx.api?.settingsStore;
64110
+ if (!settingsStoreApi) throw new Error("[device-manager] settings-store API not available \u2014 refusing to serve a fleet it cannot persist");
64111
+ const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
64112
+ await deviceRows.declare();
64113
+ this.deviceRows = deviceRows;
64114
+ try {
64115
+ await require_retired_settings_keys.purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
64116
+ } catch (err) {
64117
+ this.ctx.logger.warn("retired-row purge failed", { meta: { error: require_dist10.errMsg(err) } });
64118
+ }
64119
+ const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
63474
64120
  this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
63475
64121
  const stateMirror = this.stateMirrorImpl;
63476
- const readMeta = metaStore.readMeta;
63477
64122
  const resolvePersistedById = metaStore.resolvePersistedById;
63478
64123
  const idToAddonId = metaStore.idToAddonId;
63479
- {
63480
- const meta = await readMeta();
63481
- for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
63482
- }
63483
- const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, settings, this.ctx, deviceId, integrationId);
64124
+ for (const row of await deviceRows.listAll()) idToAddonId.set(row.meta.id, row.meta.addonId);
64125
+ const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, this.ctx, deviceId, integrationId);
63484
64126
  const pctx = {
63485
64127
  host: this.providerHost,
63486
64128
  metaStore,
@@ -63723,7 +64365,7 @@ var require_hub_forwarder = __commonJS({
63723
64365
  [Symbol.toStringTag]: { value: "Module" }
63724
64366
  });
63725
64367
  require_chunk_Cek0wNdY();
63726
- var require_dist10 = require_dist_Ctfhlrtl();
64368
+ var require_dist10 = require_dist_B2CtwVW2();
63727
64369
  var require_formatter = require_formatter_DqAKDlvN();
63728
64370
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
63729
64371
  var HubForwarderDestination = class {
@@ -63860,7 +64502,7 @@ var require_liveness_monitor_addon = __commonJS({
63860
64502
  "use strict";
63861
64503
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
63862
64504
  require_chunk_Cek0wNdY();
63863
- var require_dist10 = require_dist_Ctfhlrtl();
64505
+ var require_dist10 = require_dist_B2CtwVW2();
63864
64506
  var NO_DEVICES = "liveness:no-devices";
63865
64507
  var ALL_OFFLINE = "liveness:all-devices-offline";
63866
64508
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -64050,7 +64692,7 @@ var require_local_auth_addon = __commonJS({
64050
64692
  [Symbol.toStringTag]: { value: "Module" }
64051
64693
  });
64052
64694
  var require_chunk = require_chunk_Cek0wNdY();
64053
- var require_dist10 = require_dist_Ctfhlrtl();
64695
+ var require_dist10 = require_dist_B2CtwVW2();
64054
64696
  var node_crypto = __require("crypto");
64055
64697
  node_crypto = require_chunk.__toESM(node_crypto);
64056
64698
  var crypto$1 = __require("crypto");
@@ -71734,7 +72376,7 @@ var require_loki_logging = __commonJS({
71734
72376
  [Symbol.toStringTag]: { value: "Module" }
71735
72377
  });
71736
72378
  require_chunk_Cek0wNdY();
71737
- var require_dist10 = require_dist_Ctfhlrtl();
72379
+ var require_dist10 = require_dist_B2CtwVW2();
71738
72380
  function sanitizeLabelName(raw) {
71739
72381
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
71740
72382
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -72299,7 +72941,7 @@ var require_native_metrics_addon = __commonJS({
72299
72941
  [Symbol.toStringTag]: { value: "Module" }
72300
72942
  });
72301
72943
  var require_chunk = require_chunk_Cek0wNdY();
72302
- var require_dist10 = require_dist_Ctfhlrtl();
72944
+ var require_dist10 = require_dist_B2CtwVW2();
72303
72945
  var node_child_process = __require("child_process");
72304
72946
  var node_util = __require("util");
72305
72947
  var node_os = __require("os");
@@ -73241,7 +73883,7 @@ var require_filesystem_storage_addon = __commonJS({
73241
73883
  [Symbol.toStringTag]: { value: "Module" }
73242
73884
  });
73243
73885
  var require_chunk = require_chunk_Cek0wNdY();
73244
- var require_dist10 = require_dist_Ctfhlrtl();
73886
+ var require_dist10 = require_dist_B2CtwVW2();
73245
73887
  var node_crypto = __require("crypto");
73246
73888
  var node_fs_promises = __require("fs/promises");
73247
73889
  var node_path = __require("path");
@@ -74357,7 +74999,8 @@ var require_sqlite_settings_addon = __commonJS({
74357
74999
  [Symbol.toStringTag]: { value: "Module" }
74358
75000
  });
74359
75001
  var require_chunk = require_chunk_Cek0wNdY();
74360
- var require_dist10 = require_dist_Ctfhlrtl();
75002
+ var require_dist10 = require_dist_B2CtwVW2();
75003
+ var require_retired_settings_keys = require_retired_settings_keys_CYDGGkyM();
74361
75004
  var node_crypto = __require("crypto");
74362
75005
  var node_fs = __require("fs");
74363
75006
  var node_module = __require("module");
@@ -74366,123 +75009,47 @@ var require_sqlite_settings_addon = __commonJS({
74366
75009
  var SQLITE_PAGE_CACHE_KIB = -65536;
74367
75010
  var SQLITE_MMAP_SIZE_BYTES = 268435456;
74368
75011
  var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
74369
- function settingsStoreIsAuthoritativeHere(env) {
74370
- const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
74371
- return raw === "" || raw === "hub";
74372
- }
74373
- var RETIRED_SETTINGS_KEYS = [{
74374
- namespace: "detection-pipeline",
74375
- collection: "addon-settings",
74376
- row: "root",
74377
- keys: ["pipelineSteps", "pipelineEngine"],
74378
- perNodeKeys: [
74379
- "engineRuntime",
74380
- "engineBackend",
74381
- "engineDevice",
74382
- "probedBestEngine"
74383
- ],
74384
- reason: "global step/engine seed and the per-node engine cascade \u2014 both removed; the live per-camera dispatch path never read either"
75012
+ var RETIRED_COLLECTIONS = [{
75013
+ collection: "devices",
75014
+ owner: "integration-registry",
75015
+ reason: "the device half of the integration-registry is deleted; the fleet is `device-manager:devices` and always was (0 rows here against 974 live devices)"
74385
75016
  }, {
74386
- namespace: "device-manager",
74387
- collection: "addon-settings",
74388
- row: "deviceMeta",
74389
- keys: [],
74390
- perNodeKeys: [],
74391
- mapValueKeys: ["deviceLinks"],
74392
- reason: "cross-device field wiring, deleted 2026-08-08 \u2014 0 links across 302 devices a month after the UI shipped. There is no reader left: the resolver, the overlay, the cycle guard, the fifth binding kind and the tab are gone"
75017
+ collection: "device_settings_kv",
75018
+ owner: "integration-registry",
75019
+ reason: "per-device settings of a device table that never had a row; the live per-camera store is the device-manager row plus `addon-device-settings`"
74393
75020
  }];
74394
- function retiredKeyStoreOf(backend) {
74395
- return {
74396
- async read(spec) {
74397
- return require_dist10.asJsonObject(await backend.get({
74398
- namespace: spec.namespace,
74399
- collection: spec.collection,
74400
- key: spec.row
74401
- }));
74402
- },
74403
- async write(spec, value) {
74404
- await backend.set({
74405
- namespace: spec.namespace,
74406
- collection: spec.collection,
74407
- key: spec.row,
74408
- value
74409
- });
74410
- }
74411
- };
74412
- }
74413
- function planNestedRetiredKeyPurge(blob, spec) {
74414
- const nestedKeys = spec.mapValueKeys ?? [];
74415
- if (nestedKeys.length === 0) return [];
74416
- const found = [];
74417
- for (const [mapKey, value] of Object.entries(blob)) {
74418
- const inner = require_dist10.asJsonObject(value);
74419
- if (inner === null) continue;
74420
- for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
74421
- }
74422
- return found;
74423
- }
74424
- function planRetiredKeyPurge(blob, spec) {
74425
- const exact = new Set(spec.keys);
74426
- const found = [];
74427
- for (const key of Object.keys(blob)) {
74428
- if (exact.has(key)) {
74429
- found.push(key);
74430
- continue;
74431
- }
74432
- for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
74433
- found.push(key);
74434
- break;
74435
- }
74436
- }
74437
- return found;
75021
+ function retiredCollectionStoreOf(backend) {
75022
+ return { async drop(collection) {
75023
+ return backend.dropRetiredTable(collection);
75024
+ } };
74438
75025
  }
74439
- async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
74440
- if (!settingsStoreIsAuthoritativeHere(env)) return [];
75026
+ async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
74441
75027
  const results = [];
74442
75028
  for (const spec of specs) {
74443
- let blob;
75029
+ let dropped;
74444
75030
  try {
74445
- blob = await store.read(spec);
75031
+ dropped = await store.drop(spec.collection);
74446
75032
  } catch (err) {
74447
- logger.warn("retired settings keys \u2014 read failed, row left untouched", { meta: {
74448
- addon: spec.namespace,
75033
+ logger.warn("retired collection \u2014 drop failed, table left in place", { meta: {
75034
+ addon: spec.owner,
74449
75035
  collection: spec.collection,
74450
- row: spec.row,
74451
75036
  error: err instanceof Error ? err.message : String(err)
74452
75037
  } });
74453
75038
  continue;
74454
75039
  }
74455
- if (blob === null) continue;
74456
- const doomed = planRetiredKeyPurge(blob, spec);
74457
- const doomedNested = planNestedRetiredKeyPurge(blob, spec);
74458
- if (doomed.length === 0 && doomedNested.length === 0) continue;
74459
- const doomedSet = new Set(doomed);
74460
- const nestedByMapKey = /* @__PURE__ */ new Map();
74461
- for (const path of doomedNested) {
74462
- const cut = path.indexOf(".");
74463
- const mapKey = path.slice(0, cut);
74464
- const inner = path.slice(cut + 1);
74465
- const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
74466
- bucket.add(inner);
74467
- nestedByMapKey.set(mapKey, bucket);
74468
- }
74469
- const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
74470
- const retired = nestedByMapKey.get(key);
74471
- if (retired === void 0) return [key, value];
74472
- const inner = require_dist10.asJsonObject(value);
74473
- if (inner === null) return [key, value];
74474
- return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
74475
- }));
74476
- await store.write(spec, next);
75040
+ if (dropped === null) continue;
74477
75041
  const result = {
74478
- addon: spec.namespace,
74479
75042
  collection: spec.collection,
74480
- row: spec.row,
74481
- keys: doomed,
74482
- nested: doomedNested
75043
+ owner: spec.owner,
75044
+ rows: dropped.rows
74483
75045
  };
74484
75046
  results.push(result);
74485
- logger.info(`purged ${doomed.length + doomedNested.length} retired settings keys`, { meta: {
75047
+ const line = "dropped a retired collection";
75048
+ if (dropped.rows > 0) logger.warn(`${line} \u2014 it still held rows`, { meta: {
75049
+ ...result,
75050
+ reason: spec.reason
75051
+ } });
75052
+ else logger.info(line, { meta: {
74486
75053
  ...result,
74487
75054
  reason: spec.reason
74488
75055
  } });
@@ -74542,6 +75109,20 @@ var require_sqlite_settings_addon = __commonJS({
74542
75109
  params
74543
75110
  };
74544
75111
  }
75112
+ var MAX_ASCII = 127;
75113
+ function prefixRange(prefix) {
75114
+ if (prefix.length === 0) return null;
75115
+ for (let i = 0; i < prefix.length; i++) {
75116
+ const code = prefix.charCodeAt(i);
75117
+ if (code === 0 || code > MAX_ASCII) return null;
75118
+ }
75119
+ const last = prefix.charCodeAt(prefix.length - 1);
75120
+ if (last >= MAX_ASCII) return null;
75121
+ return {
75122
+ lo: prefix,
75123
+ hi: prefix.slice(0, -1) + String.fromCharCode(last + 1)
75124
+ };
75125
+ }
74545
75126
  var SETTINGS_QUERY_DEFAULT_ROW_CAP = 2e3;
74546
75127
  var SETTINGS_QUERY_HARD_ROW_CAP = 2e4;
74547
75128
  function resolveRowBound(requested) {
@@ -74562,19 +75143,8 @@ var require_sqlite_settings_addon = __commonJS({
74562
75143
  function isImposedBound(source) {
74563
75144
  return source !== "caller";
74564
75145
  }
74565
- var MAX_ASCII = 127;
74566
- function prefixRange(prefix) {
74567
- if (prefix.length === 0) return null;
74568
- for (let i = 0; i < prefix.length; i++) {
74569
- const code = prefix.charCodeAt(i);
74570
- if (code === 0 || code > MAX_ASCII) return null;
74571
- }
74572
- const last = prefix.charCodeAt(prefix.length - 1);
74573
- if (last >= MAX_ASCII) return null;
74574
- return {
74575
- lo: prefix,
74576
- hi: prefix.slice(0, -1) + String.fromCharCode(last + 1)
74577
- };
75146
+ function isCountRow(raw) {
75147
+ return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
74578
75148
  }
74579
75149
  function parseRowData(raw) {
74580
75150
  return require_dist10.asJsonObject(require_dist10.parseJsonUnknown(raw)) ?? {};
@@ -74958,10 +75528,33 @@ var require_sqlite_settings_addon = __commonJS({
74958
75528
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
74959
75529
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
74960
75530
  }
74961
- /** Get all settings for an addon */
75531
+ /**
75532
+ * Get all settings for an addon.
75533
+ *
75534
+ * Selected by the SAME key range {@link setAllAddon} deletes and re-inserts
75535
+ * — `prefixWhere("<addonId>.")` — and for the same reason `getAllScoped`
75536
+ * uses it: `"<addonId>.<key>"` is the PRIMARY KEY, so the range is a
75537
+ * `SEARCH … USING INDEX` while anything else is a full `SCAN`.
75538
+ *
75539
+ * It used to select on `json_extract(data, '$.addonId')`, which is a second
75540
+ * authority for "this addon's rows" and disagreed with the writer in both
75541
+ * directions: a row inside the JSON's idea of the addon but outside the key
75542
+ * range was READ and never DELETED (a config key that could not be cleared),
75543
+ * and `json_extract` was evaluated on EVERY row — so a four-key addon paid
75544
+ * for the whole table and one unparseable neighbour aborted the statement,
75545
+ * taking out every addon's config read at once.
75546
+ *
75547
+ * The cost was not theoretical. On the live hub `addon-settings` is 627 KB
75548
+ * in 24 rows, 625 KB of it device-manager's three fleet blobs; a V8 profile
75549
+ * of a boot (2026-08-19) had hub-main's JS thread 99.9% busy with **64% of
75550
+ * it inside this method**, which is what put a ~45 s queue in front of every
75551
+ * runner's first store read. Measured on that table: 0.947 ms → ~0.02 ms for
75552
+ * a four-key addon, 2.90 ms → 2.13 ms for device-manager's own.
75553
+ */
74962
75554
  getAllAddon(addonId) {
74963
75555
  this.requireDeclared("addon-settings");
74964
- const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
75556
+ const where = this.prefixWhere(`${addonId}.`);
75557
+ const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE ${where.sql}`).all(...where.params);
74965
75558
  if (rows.length === 0) return {};
74966
75559
  const result = {};
74967
75560
  for (const row of rows) {
@@ -75237,6 +75830,31 @@ var require_sqlite_settings_addon = __commonJS({
75237
75830
  }
75238
75831
  this.structuredTables.set(table, fingerprint);
75239
75832
  }
75833
+ /**
75834
+ * Drop a table nothing declares any more, reporting what went with it.
75835
+ *
75836
+ * The DDL lives here because the engine owns the handle AND the two memos
75837
+ * that outlive a table: `structuredTables` (schema fingerprint) and
75838
+ * `declaredCollections` (the column set `compileFilter` resolves against).
75839
+ * Dropping behind their backs would leave a ghost — a later declaration of
75840
+ * the same name would be memoised as "already materialised" against a table
75841
+ * that no longer exists.
75842
+ *
75843
+ * WHICH tables are retired, and the logging, are policy: they live in
75844
+ * `retired-collections.ts`. This method knows only how to drop one.
75845
+ *
75846
+ * Answers `null` when the table is not there — the steady state after the
75847
+ * first boot that ran the purge, and the case on a fresh install.
75848
+ */
75849
+ dropRetiredTable(table) {
75850
+ if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
75851
+ const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
75852
+ const rows = isCountRow(counted) ? counted.n : 0;
75853
+ this.getDb().exec(`DROP TABLE "${table}"`);
75854
+ this.structuredTables.delete(table);
75855
+ this.declaredCollections.delete(table);
75856
+ return { rows };
75857
+ }
75240
75858
  async tableInsert(table, row) {
75241
75859
  const keys = Object.keys(row);
75242
75860
  const cols = keys.map((k) => `"${k}"`).join(", ");
@@ -75747,10 +76365,11 @@ var require_sqlite_settings_addon = __commonJS({
75747
76365
  this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist10.RUNTIME_DEFAULTS }, this.ctx.logger.child("Query"));
75748
76366
  await this.backend.initialize();
75749
76367
  try {
75750
- await purgeRetiredSettingsKeys(retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
76368
+ await require_retired_settings_keys.purgeRetiredSettingsKeys(require_retired_settings_keys.retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
75751
76369
  } catch (err) {
75752
76370
  this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist10.errMsg(err) } });
75753
76371
  }
76372
+ await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
75754
76373
  const vectorIndex = this.createVectorIndex(this.backend);
75755
76374
  await vectorIndex.loadIndexRegistry();
75756
76375
  this.vectorIndex = vectorIndex;
@@ -75925,7 +76544,7 @@ var require_storage_orchestrator_addon = __commonJS({
75925
76544
  [Symbol.toStringTag]: { value: "Module" }
75926
76545
  });
75927
76546
  var require_chunk = require_chunk_Cek0wNdY();
75928
- var require_dist10 = require_dist_Ctfhlrtl();
76547
+ var require_dist10 = require_dist_B2CtwVW2();
75929
76548
  var node_crypto = __require("crypto");
75930
76549
  var node_fs_promises = __require("fs/promises");
75931
76550
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -77804,7 +78423,7 @@ var require_system_config_addon = __commonJS({
77804
78423
  [Symbol.toStringTag]: { value: "Module" }
77805
78424
  });
77806
78425
  require_chunk_Cek0wNdY();
77807
- var require_dist10 = require_dist_Ctfhlrtl();
78426
+ var require_dist10 = require_dist_B2CtwVW2();
77808
78427
  var SECTION_TITLES = {
77809
78428
  server: "Server",
77810
78429
  auth: "Authentication"
@@ -95865,7 +96484,7 @@ var require_winston_logging = __commonJS({
95865
96484
  [Symbol.toStringTag]: { value: "Module" }
95866
96485
  });
95867
96486
  var require_chunk = require_chunk_Cek0wNdY();
95868
- var require_dist10 = require_dist_Ctfhlrtl();
96487
+ var require_dist10 = require_dist_B2CtwVW2();
95869
96488
  var require_formatter = require_formatter_DqAKDlvN();
95870
96489
  var node_path = __require("path");
95871
96490
  node_path = require_chunk.__toESM(node_path);
@@ -107335,9 +107954,9 @@ var require_dist2 = __commonJS({
107335
107954
  }
107336
107955
  });
107337
107956
 
107338
- // ../system/dist/manifest-python-deps-GejnH--L.js
107339
- var require_manifest_python_deps_GejnH_L = __commonJS({
107340
- "../system/dist/manifest-python-deps-GejnH--L.js"(exports) {
107957
+ // ../system/dist/manifest-python-deps-MA_BZGBz.js
107958
+ var require_manifest_python_deps_MA_BZGBz = __commonJS({
107959
+ "../system/dist/manifest-python-deps-MA_BZGBz.js"(exports) {
107341
107960
  "use strict";
107342
107961
  var require_chunk = require_chunk_Cek0wNdY();
107343
107962
  var node_crypto = __require("crypto");
@@ -111314,6 +111933,8 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111314
111933
  };
111315
111934
  goneHandler = () => {
111316
111935
  };
111936
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
111937
+ nextIncarnation = 1;
111317
111938
  eventHandler = null;
111318
111939
  logHandler = null;
111319
111940
  readinessHandler = null;
@@ -111588,14 +112209,20 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111588
112209
  listChildren() {
111589
112210
  return [...this.children.values()].map((e) => ({
111590
112211
  childId: e.childId,
111591
- caps: e.caps
112212
+ caps: e.caps,
112213
+ incarnation: e.incarnation
111592
112214
  }));
111593
112215
  }
111594
112216
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
111595
112217
  onChildRegistered(handler) {
111596
112218
  this.registeredHandler = handler;
111597
112219
  }
111598
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
112220
+ /**
112221
+ * Register the (single) child-gone handler. Only one handler is active at a
112222
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
112223
+ * connection that closed — carry it into whatever teardown it triggers, so a
112224
+ * dead generation cannot tear down its successor's work.
112225
+ */
111599
112226
  onChildGone(handler) {
111600
112227
  this.goneHandler = handler;
111601
112228
  }
@@ -111680,6 +112307,7 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111680
112307
  }
111681
112308
  onConnection(channel) {
111682
112309
  let childId = null;
112310
+ const incarnation = this.nextIncarnation++;
111683
112311
  channel.onEvent((body) => {
111684
112312
  const msg = body;
111685
112313
  if (msg.kind === "event") {
@@ -111708,16 +112336,24 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111708
112336
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
111709
112337
  childId = msg.childId;
111710
112338
  const eventPatterns = msg.eventPatterns ?? null;
112339
+ const superseded = this.children.get(msg.childId);
112340
+ if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
112341
+ childId: msg.childId,
112342
+ supersededIncarnation: superseded.incarnation,
112343
+ incarnation
112344
+ });
111711
112345
  this.children.set(msg.childId, {
111712
112346
  childId: msg.childId,
111713
112347
  channel,
111714
112348
  caps: msg.caps,
111715
- eventPatterns
112349
+ eventPatterns,
112350
+ incarnation
111716
112351
  });
111717
112352
  this.logPatternSet(msg.childId, eventPatterns);
111718
112353
  this.registeredHandler({
111719
112354
  childId: msg.childId,
111720
- caps: msg.caps
112355
+ caps: msg.caps,
112356
+ incarnation
111721
112357
  });
111722
112358
  return { ok: true };
111723
112359
  }
@@ -111752,11 +112388,21 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111752
112388
  throw new Error(`unknown child request kind: ${msg.kind}`);
111753
112389
  });
111754
112390
  channel.onClose(() => {
111755
- if (childId !== null && this.children.delete(childId)) {
111756
- this.childEventStats.delete(childId);
111757
- this.loggedPatternSet.delete(childId);
111758
- this.goneHandler(childId);
112391
+ if (childId === null) return;
112392
+ const entry = this.children.get(childId);
112393
+ if (entry === void 0) return;
112394
+ if (entry.channel !== channel) {
112395
+ this.logger?.info("ignoring close of a superseded local child connection", {
112396
+ childId,
112397
+ incarnation,
112398
+ liveIncarnation: entry.incarnation
112399
+ });
112400
+ return;
111759
112401
  }
112402
+ this.children.delete(childId);
112403
+ this.childEventStats.delete(childId);
112404
+ this.loggedPatternSet.delete(childId);
112405
+ this.goneHandler(childId, incarnation);
111760
112406
  });
111761
112407
  }
111762
112408
  };
@@ -118114,7 +118760,7 @@ var require_dist3 = __commonJS({
118114
118760
  "use strict";
118115
118761
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
118116
118762
  var require_chunk = require_chunk_Cek0wNdY();
118117
- var require_dist10 = require_dist_Ctfhlrtl();
118763
+ var require_dist10 = require_dist_B2CtwVW2();
118118
118764
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
118119
118765
  require_alerts();
118120
118766
  var require_formatter = require_formatter_DqAKDlvN();
@@ -118139,7 +118785,7 @@ var require_dist3 = __commonJS({
118139
118785
  require_system_config();
118140
118786
  var require_builtins_winston_logging_index = require_winston_logging();
118141
118787
  var require_file_data_plane = require_file_data_plane_DUHPHa_Y();
118142
- var require_manifest_python_deps = require_manifest_python_deps_GejnH_L();
118788
+ var require_manifest_python_deps = require_manifest_python_deps_MA_BZGBz();
118143
118789
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
118144
118790
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
118145
118791
  var zod = require_zod();
@@ -120035,13 +120681,9 @@ var require_dist3 = __commonJS({
120035
120681
  return {};
120036
120682
  }
120037
120683
  var integrationCounter = 0;
120038
- var deviceCounter = 0;
120039
120684
  function nextIntegrationId() {
120040
120685
  return `int_${String(++integrationCounter).padStart(4, "0")}`;
120041
120686
  }
120042
- function nextDeviceId() {
120043
- return `dev_${String(++deviceCounter).padStart(4, "0")}`;
120044
- }
120045
120687
  var INTEGRATIONS_COLUMNS = [
120046
120688
  {
120047
120689
  name: "id",
@@ -120108,99 +120750,8 @@ var require_dist3 = __commonJS({
120108
120750
  defaultValue: "string"
120109
120751
  }
120110
120752
  ];
120111
- var DEVICES_COLUMNS = [
120112
- {
120113
- name: "id",
120114
- type: "TEXT",
120115
- primaryKey: true
120116
- },
120117
- {
120118
- name: "integration_id",
120119
- type: "TEXT",
120120
- notNull: true
120121
- },
120122
- {
120123
- name: "stable_id",
120124
- type: "TEXT",
120125
- notNull: true,
120126
- unique: true
120127
- },
120128
- {
120129
- name: "type",
120130
- type: "TEXT",
120131
- notNull: true,
120132
- defaultValue: "camera"
120133
- },
120134
- {
120135
- name: "name",
120136
- type: "TEXT",
120137
- notNull: true
120138
- },
120139
- {
120140
- name: "enabled",
120141
- type: "INTEGER",
120142
- notNull: true,
120143
- defaultValue: 1
120144
- },
120145
- {
120146
- name: "info",
120147
- type: "JSON",
120148
- notNull: true,
120149
- defaultValue: "{}"
120150
- },
120151
- {
120152
- name: "created_at",
120153
- type: "INTEGER",
120154
- notNull: true
120155
- },
120156
- {
120157
- name: "updated_at",
120158
- type: "INTEGER",
120159
- notNull: true
120160
- }
120161
- ];
120162
- var DEVICES_INDEXES = [
120163
- {
120164
- name: "idx_devices_integration",
120165
- columns: ["integration_id"]
120166
- },
120167
- {
120168
- name: "idx_devices_stable",
120169
- columns: ["stable_id"],
120170
- unique: true
120171
- },
120172
- {
120173
- name: "idx_devices_type",
120174
- columns: ["type"]
120175
- }
120176
- ];
120177
- var DEVICE_SETTINGS_COLUMNS = [
120178
- {
120179
- name: "device_id",
120180
- type: "TEXT",
120181
- notNull: true
120182
- },
120183
- {
120184
- name: "key",
120185
- type: "TEXT",
120186
- notNull: true
120187
- },
120188
- {
120189
- name: "value",
120190
- type: "TEXT",
120191
- notNull: true
120192
- },
120193
- {
120194
- name: "value_type",
120195
- type: "TEXT",
120196
- notNull: true,
120197
- defaultValue: "string"
120198
- }
120199
- ];
120200
120753
  var INTEGRATIONS = "integrations";
120201
120754
  var INTEGRATION_SETTINGS = "integration_settings";
120202
- var DEVICES = "devices";
120203
- var DEVICE_SETTINGS = "device_settings_kv";
120204
120755
  var IntegrationRegistry = class IntegrationRegistry2 {
120205
120756
  store;
120206
120757
  constructor(backend) {
@@ -120216,15 +120767,6 @@ var require_dist3 = __commonJS({
120216
120767
  collection: INTEGRATION_SETTINGS,
120217
120768
  columns: INTEGRATION_SETTINGS_COLUMNS
120218
120769
  });
120219
- await this.store.declareCollection({
120220
- collection: DEVICES,
120221
- columns: DEVICES_COLUMNS,
120222
- indexes: DEVICES_INDEXES
120223
- });
120224
- await this.store.declareCollection({
120225
- collection: DEVICE_SETTINGS,
120226
- columns: DEVICE_SETTINGS_COLUMNS
120227
- });
120228
120770
  const ints = await this.store.query({
120229
120771
  collection: INTEGRATIONS,
120230
120772
  filter: {
@@ -120239,20 +120781,6 @@ var require_dist3 = __commonJS({
120239
120781
  const num = parseInt(ints[0].id.replace("int_", ""), 10);
120240
120782
  if (!isNaN(num)) integrationCounter = num;
120241
120783
  }
120242
- const devs = await this.store.query({
120243
- collection: DEVICES,
120244
- filter: {
120245
- orderBy: {
120246
- field: "id",
120247
- direction: "desc"
120248
- },
120249
- limit: 1
120250
- }
120251
- });
120252
- if (devs[0]) {
120253
- const num = parseInt(devs[0].id.replace("dev_", ""), 10);
120254
- if (!isNaN(num)) deviceCounter = num;
120255
- }
120256
120784
  }
120257
120785
  async createIntegration(input) {
120258
120786
  const id = nextIntegrationId();
@@ -120336,15 +120864,6 @@ var require_dist3 = __commonJS({
120336
120864
  async deleteIntegration(id) {
120337
120865
  const integration = await this.getIntegration(id);
120338
120866
  if (integration !== null && IntegrationRegistry2.isFixed(integration)) throw new Error(`integration "${id}" is fixed \u2014 it belongs to the addon that declared it and cannot be deleted`);
120339
- const devices = await this.listDevices(id);
120340
- for (const d of devices) await this.store.deleteWhere({
120341
- collection: DEVICE_SETTINGS,
120342
- filter: { where: { device_id: d.id } }
120343
- });
120344
- await this.store.deleteWhere({
120345
- collection: DEVICES,
120346
- filter: { where: { integration_id: id } }
120347
- });
120348
120867
  await this.store.deleteWhere({
120349
120868
  collection: INTEGRATION_SETTINGS,
120350
120869
  filter: { where: { integration_id: id } }
@@ -120389,139 +120908,6 @@ var require_dist3 = __commonJS({
120389
120908
  async setIntegrationSettings(integrationId, settings) {
120390
120909
  for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
120391
120910
  }
120392
- async createDevice(input) {
120393
- const id = nextDeviceId();
120394
- const now = Math.floor(Date.now() / 1e3);
120395
- await this.store.insert({
120396
- collection: DEVICES,
120397
- record: {
120398
- id,
120399
- data: {
120400
- integration_id: input.integrationId,
120401
- stable_id: input.stableId,
120402
- type: input.type,
120403
- name: input.name,
120404
- enabled: input.enabled !== false ? 1 : 0,
120405
- info: input.info ?? {},
120406
- created_at: now,
120407
- updated_at: now
120408
- }
120409
- }
120410
- });
120411
- if (input.settings) await this.setDeviceSettings(id, input.settings);
120412
- return {
120413
- id,
120414
- integrationId: input.integrationId,
120415
- stableId: input.stableId,
120416
- type: input.type,
120417
- name: input.name,
120418
- enabled: input.enabled !== false,
120419
- info: input.info ?? {},
120420
- createdAt: now,
120421
- updatedAt: now
120422
- };
120423
- }
120424
- async getDevice(id) {
120425
- const rows = await this.store.query({
120426
- collection: DEVICES,
120427
- filter: {
120428
- where: { id },
120429
- limit: 1
120430
- }
120431
- });
120432
- return rows[0] ? this.mapDevice(rows[0].data) : null;
120433
- }
120434
- async getDeviceByStableId(stableId) {
120435
- const rows = await this.store.query({
120436
- collection: DEVICES,
120437
- filter: {
120438
- where: { stable_id: stableId },
120439
- limit: 1
120440
- }
120441
- });
120442
- return rows[0] ? this.mapDevice(rows[0].data) : null;
120443
- }
120444
- async listDevices(integrationId) {
120445
- return (await this.store.query({
120446
- collection: DEVICES,
120447
- filter: {
120448
- ...integrationId ? { where: { integration_id: integrationId } } : {},
120449
- orderBy: {
120450
- field: "created_at",
120451
- direction: "asc"
120452
- }
120453
- }
120454
- })).map((r) => this.mapDevice(r.data));
120455
- }
120456
- async listCameras() {
120457
- return (await this.store.query({
120458
- collection: DEVICES,
120459
- filter: {
120460
- where: { type: "camera" },
120461
- orderBy: {
120462
- field: "created_at",
120463
- direction: "asc"
120464
- }
120465
- }
120466
- })).map((r) => this.mapDevice(r.data));
120467
- }
120468
- async updateDevice(id, updates) {
120469
- const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
120470
- if (updates.name !== void 0) updateRow["name"] = updates.name;
120471
- if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
120472
- if (updates.info !== void 0) updateRow["info"] = updates.info;
120473
- await this.store.updateWhere({
120474
- collection: DEVICES,
120475
- filter: { where: { id } },
120476
- data: updateRow
120477
- });
120478
- return this.getDevice(id);
120479
- }
120480
- async deleteDevice(id) {
120481
- await this.store.deleteWhere({
120482
- collection: DEVICE_SETTINGS,
120483
- filter: { where: { device_id: id } }
120484
- });
120485
- await this.store.deleteWhere({
120486
- collection: DEVICES,
120487
- filter: { where: { id } }
120488
- });
120489
- return true;
120490
- }
120491
- async getDeviceSettings(deviceId) {
120492
- const result = {};
120493
- const rows = await this.store.query({
120494
- collection: DEVICE_SETTINGS,
120495
- filter: { where: { device_id: deviceId } }
120496
- });
120497
- for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
120498
- return result;
120499
- }
120500
- async setDeviceSetting(deviceId, key, value) {
120501
- const s = serializeSetting(value);
120502
- await this.store.deleteWhere({
120503
- collection: DEVICE_SETTINGS,
120504
- filter: { where: {
120505
- device_id: deviceId,
120506
- key
120507
- } }
120508
- });
120509
- await this.store.insert({
120510
- collection: DEVICE_SETTINGS,
120511
- record: {
120512
- id: `${deviceId}:${key}`,
120513
- data: {
120514
- device_id: deviceId,
120515
- key,
120516
- value: s.value,
120517
- value_type: s.valueType
120518
- }
120519
- }
120520
- });
120521
- }
120522
- async setDeviceSettings(deviceId, settings) {
120523
- for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
120524
- }
120525
120911
  mapIntegration(row) {
120526
120912
  return {
120527
120913
  id: String(row["id"]),
@@ -120533,19 +120919,6 @@ var require_dist3 = __commonJS({
120533
120919
  updatedAt: Number(row["updated_at"])
120534
120920
  };
120535
120921
  }
120536
- mapDevice(row) {
120537
- return {
120538
- id: String(row["id"]),
120539
- integrationId: String(row["integration_id"]),
120540
- stableId: String(row["stable_id"]),
120541
- type: String(row["type"]),
120542
- name: String(row["name"]),
120543
- enabled: row["enabled"] === 1,
120544
- info: readInfo(row["info"]),
120545
- createdAt: Number(row["created_at"]),
120546
- updatedAt: Number(row["updated_at"])
120547
- };
120548
- }
120549
120922
  };
120550
120923
  var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
120551
120924
  var NATIVE_SCAN_DEPTH = 4;
@@ -123039,6 +123412,7 @@ var require_dist3 = __commonJS({
123039
123412
  const state = {
123040
123413
  definition,
123041
123414
  providers: /* @__PURE__ */ new Map(),
123415
+ providerOwners: /* @__PURE__ */ new Map(),
123042
123416
  activeAddonId: null,
123043
123417
  router: null,
123044
123418
  disabledProviders: /* @__PURE__ */ new Set()
@@ -123096,8 +123470,15 @@ var require_dist3 = __commonJS({
123096
123470
  * Double-register of the same pair is always a programmer error —
123097
123471
  * two legitimately different addons implementing the same cap keep
123098
123472
  * using different `addonId`s and the user picks one via `configReader`.
123099
- */
123100
- registerProvider(capabilityName, addonId, provider) {
123473
+ *
123474
+ * `owner` (optional) stamps the registration with the incarnation that made
123475
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
123476
+ * presenting the SAME token can delete it. Registration paths whose key is
123477
+ * reused across process generations (the UDS child manifest, the D3
123478
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
123479
+ * historical unconditional behaviour.
123480
+ */
123481
+ registerProvider(capabilityName, addonId, provider, owner) {
123101
123482
  let state = this.capabilities.get(capabilityName);
123102
123483
  if (!state) {
123103
123484
  const colonIdx = capabilityName.indexOf(":");
@@ -123129,6 +123510,7 @@ var require_dist3 = __commonJS({
123129
123510
  if (missing.length > 0) throw new Error(`CapabilityRegistry: provider "${addonId}" for capability "${capabilityName}" declares exposesDeviceSettings: true but is missing DeviceSettingsContribution method(s): ${missing.join(", ")}. Implement them on the provider (or drop the flag from the cap def).`);
123130
123511
  }
123131
123512
  state.providers.set(addonId, provider);
123513
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
123132
123514
  if (state.definition.mode === "singleton") {
123133
123515
  const userChoice = this.configReader?.(capabilityName);
123134
123516
  const preferred = state.definition.preferredProvider;
@@ -123159,7 +123541,8 @@ var require_dist3 = __commonJS({
123159
123541
  tags: { addonId },
123160
123542
  meta: {
123161
123543
  capability: capabilityName,
123162
- mode: state.definition.mode
123544
+ mode: state.definition.mode,
123545
+ ...owner === void 0 ? {} : { owner }
123163
123546
  }
123164
123547
  });
123165
123548
  this.emitEvent("capability:provider-registered", {
@@ -123206,11 +123589,70 @@ var require_dist3 = __commonJS({
123206
123589
  const declarers = this.manifestDeclarers.get(capabilityName);
123207
123590
  return declarers ? [...declarers] : [];
123208
123591
  }
123209
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
123210
- unregisterProvider(capabilityName, addonId) {
123592
+ /**
123593
+ * The owner token a registration declared, or `null` when the pair is not
123594
+ * registered or was registered without one.
123595
+ */
123596
+ getProviderOwner(capabilityName, addonId) {
123597
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
123598
+ }
123599
+ /**
123600
+ * Hand an EXISTING registration to a new owner, without touching the provider
123601
+ * instance, the active-singleton selection or any event.
123602
+ *
123603
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
123604
+ * set, so the manifest diff correctly leaves the registration alone — but the
123605
+ * registration is still stamped with the dead generation's token, and nothing
123606
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
123607
+ * capability cleanable when the successor eventually dies.
123608
+ *
123609
+ * Returns `false` when the pair is not registered (nothing to hand over) —
123610
+ * the caller decides whether that is a register-instead or a diagnosis.
123611
+ */
123612
+ reassignProviderOwner(capabilityName, addonId, owner) {
123613
+ const state = this.capabilities.get(capabilityName);
123614
+ if (!state || !state.providers.has(addonId)) return false;
123615
+ const previous = state.providerOwners.get(addonId);
123616
+ if (previous === owner) return true;
123617
+ state.providerOwners.set(addonId, owner);
123618
+ this.logger.info("Capability registration ownership reassigned", {
123619
+ tags: { addonId },
123620
+ meta: {
123621
+ capability: capabilityName,
123622
+ owner,
123623
+ previousOwner: previous ?? null
123624
+ }
123625
+ });
123626
+ return true;
123627
+ }
123628
+ /**
123629
+ * Unregister a provider. For singleton: clears active. For collection:
123630
+ * removes from list.
123631
+ *
123632
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
123633
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
123634
+ * and the caller presents a different one — or none at all — this is a
123635
+ * logged no-op: the caller belongs to a generation that no longer holds the
123636
+ * key. This is what stops a dead runner's teardown from erasing its
123637
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
123638
+ */
123639
+ unregisterProvider(capabilityName, addonId, owner) {
123211
123640
  const state = this.capabilities.get(capabilityName);
123212
123641
  if (!state) return;
123642
+ const registeredOwner = state.providerOwners.get(addonId);
123643
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
123644
+ this.logger.info("Unregister ignored \u2014 registration is owned by another incarnation", {
123645
+ tags: { addonId },
123646
+ meta: {
123647
+ capability: capabilityName,
123648
+ registeredOwner,
123649
+ presentedOwner: owner ?? null
123650
+ }
123651
+ });
123652
+ return;
123653
+ }
123213
123654
  state.providers.delete(addonId);
123655
+ state.providerOwners.delete(addonId);
123214
123656
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
123215
123657
  const userChoice = this.configReader?.(capabilityName);
123216
123658
  const preferred = state.definition.preferredProvider;
@@ -198184,6 +198626,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198184
198626
  function safeSegment(name2) {
198185
198627
  return name2.replace(/[^a-zA-Z0-9._-]/g, "_");
198186
198628
  }
198629
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
198187
198630
  var StagingArea = class {
198188
198631
  stagingDir;
198189
198632
  deps;
@@ -198201,8 +198644,12 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198201
198644
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
198202
198645
  await this.deps.extract(tgz, destDir);
198203
198646
  const raw = JSON.parse(node_fs.readFileSync(node_path.join(destDir, "package.json"), "utf-8"));
198204
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
198205
- return { stagedPath: destDir };
198647
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
198648
+ if (EXACT_SEMVER.test(input.version) && raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${raw.version}`);
198649
+ return {
198650
+ stagedPath: destDir,
198651
+ resolvedVersion: raw.version
198652
+ };
198206
198653
  }
198207
198654
  cleanup(jobId) {
198208
198655
  node_fs.rmSync(node_path.join(this.stagingDir, jobId), {
@@ -238360,7 +238807,7 @@ var require_dist9 = __commonJS({
238360
238807
  postBufferSec: zod.z.number().min(0).optional()
238361
238808
  });
238362
238809
  var DEFAULT_EVENTS_BAND_BUFFER_SEC = {
238363
- preBufferSec: 10,
238810
+ preBufferSec: 15,
238364
238811
  postBufferSec: 30
238365
238812
  };
238366
238813
  var VISIT_MERGE_GAP_MS = DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec * 1e3;
@@ -238974,12 +239421,17 @@ var require_dist9 = __commonJS({
238974
239421
  "tflite",
238975
239422
  "pt"
238976
239423
  ];
239424
+ var FAMILY_LABEL = {
239425
+ yolo26: "YOLO26",
239426
+ yolov9: "YOLOv9"
239427
+ };
238977
239428
  function familyLabel(family) {
238978
- return family.toUpperCase();
239429
+ return FAMILY_LABEL[family] ?? family.toUpperCase();
238979
239430
  }
239431
+ var RESOLUTION_IN_NAME = /\s*@\s*\d+\s*(?:[x×]\s*\d+)?/gi;
238980
239432
  function tierLabel(family, baseName) {
238981
239433
  const fl = familyLabel(family);
238982
- const stripped = baseName.replace(new RegExp(`^${fl}\\s+`, "i"), "").trim();
239434
+ const stripped = baseName.replace(new RegExp(`^${fl}\\s+`, "i"), "").replace(RESOLUTION_IN_NAME, "").trim();
238983
239435
  return stripped.length > 0 ? stripped : baseName;
238984
239436
  }
238985
239437
  function variantLabel(precision, optimization) {
@@ -238996,9 +239448,11 @@ var require_dist9 = __commonJS({
238996
239448
  return sizes.length > 0 ? Math.min(...sizes) : 0;
238997
239449
  }
238998
239450
  var TIER_ORDER = [
239451
+ "t",
238999
239452
  "n",
239000
239453
  "s",
239001
239454
  "m",
239455
+ "c",
239002
239456
  "l",
239003
239457
  "x"
239004
239458
  ];
@@ -239035,7 +239489,7 @@ var require_dist9 = __commonJS({
239035
239489
  if (a.optimization !== b.optimization) return a.optimization === "fast" ? 1 : -1;
239036
239490
  return PRECISION_ORDER.indexOf(a.precision) - PRECISION_ORDER.indexOf(b.precision);
239037
239491
  });
239038
- const base = options.find((o) => o.precision === "fp32" && o.optimization === "standard" && o.resolution === void 0) ?? options[0];
239492
+ const base = options.find((o) => o.precision === "fp32" && o.optimization === "standard") ?? options[0];
239039
239493
  if (base === void 0) continue;
239040
239494
  const baseEntry = entries2.find((e) => e.id === base.modelId);
239041
239495
  tierList.push({
@@ -239065,15 +239519,9 @@ var require_dist9 = __commonJS({
239065
239519
  function resolveVariantModelId(models, selection) {
239066
239520
  const precision = selection.precision ?? "fp32";
239067
239521
  const optimization = selection.optimization ?? "standard";
239068
- for (const m of models) {
239069
- if (m.group === void 0) continue;
239070
- if (m.group.family !== selection.family || m.group.tier !== selection.tier) continue;
239071
- if ((m.group.precision ?? "fp32") !== precision) continue;
239072
- if ((m.group.optimization ?? "standard") !== optimization) continue;
239073
- if (m.group.resolution !== selection.resolution) continue;
239074
- return m.id;
239075
- }
239076
- return null;
239522
+ const candidates = models.filter((m) => m.group !== void 0 && m.group.family === selection.family && m.group.tier === selection.tier && (m.group.precision ?? "fp32") === precision && (m.group.optimization ?? "standard") === optimization);
239523
+ if (selection.resolution !== void 0) return candidates.find((m) => m.group?.resolution === selection.resolution)?.id ?? null;
239524
+ return candidates.toSorted((a, b) => (b.group?.resolution ?? Infinity) - (a.group?.resolution ?? Infinity))[0]?.id ?? null;
239077
239525
  }
239078
239526
  function describeModelVariant(models, modelId) {
239079
239527
  const entry = models.find((m) => m.id === modelId);
@@ -240480,11 +240928,15 @@ export default function block(ctx: AddonContext) {
240480
240928
  ): Promise<void> {
240481
240929
  const resolved: Record<string, unknown> = {}
240482
240930
  for (const [k, v] of Object.entries(args)) resolved[k] = substitute(v, vars)
240483
- const client = (ctx.api as unknown as Record<string, Record<string, { mutate: (i: unknown) => Promise<unknown> }>>)[capToRouterKey(cap)]
240484
- if (client === undefined || client[method] === undefined) {
240931
+ const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
240932
+ const proc: unknown =
240933
+ typeof client === 'object' && client !== null ? Reflect.get(client, method) : undefined
240934
+ const mutate: unknown =
240935
+ typeof proc === 'object' && proc !== null ? Reflect.get(proc, 'mutate') : undefined
240936
+ if (typeof mutate !== 'function') {
240485
240937
  throw new Error('no such device method: ' + cap + '.' + method)
240486
240938
  }
240487
- await client[method]!.mutate({ ...resolved, deviceId })
240939
+ await mutate({ ...resolved, deviceId })
240488
240940
  }
240489
240941
 
240490
240942
  async function evaluateConditions(): Promise<boolean> {
@@ -246331,128 +246783,103 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
246331
246783
  var AUDIO_MACRO_LABELS = [
246332
246784
  {
246333
246785
  id: "speech",
246334
- name: "Speech",
246335
- icon: "\u{1F5E3}\uFE0F"
246786
+ name: "Speech"
246336
246787
  },
246337
246788
  {
246338
246789
  id: "scream",
246339
- name: "Scream / Shout",
246340
- icon: "\u{1F631}"
246790
+ name: "Scream / Shout"
246341
246791
  },
246342
246792
  {
246343
246793
  id: "crying",
246344
- name: "Crying / Baby",
246345
- icon: "\u{1F622}"
246794
+ name: "Crying / Baby"
246346
246795
  },
246347
246796
  {
246348
246797
  id: "laughter",
246349
- name: "Laughter",
246350
- icon: "\u{1F602}"
246798
+ name: "Laughter"
246351
246799
  },
246352
246800
  {
246353
246801
  id: "music",
246354
- name: "Music",
246355
- icon: "\u{1F3B5}"
246802
+ name: "Music"
246356
246803
  },
246357
246804
  {
246358
246805
  id: "dog",
246359
- name: "Dog",
246360
- icon: "\u{1F415}"
246806
+ name: "Dog"
246361
246807
  },
246362
246808
  {
246363
246809
  id: "cat",
246364
- name: "Cat",
246365
- icon: "\u{1F408}"
246810
+ name: "Cat"
246366
246811
  },
246367
246812
  {
246368
246813
  id: "bird",
246369
- name: "Bird",
246370
- icon: "\u{1F426}"
246814
+ name: "Bird"
246371
246815
  },
246372
246816
  {
246373
246817
  id: "animal",
246374
- name: "Animal (other)",
246375
- icon: "\u{1F43E}"
246818
+ name: "Animal (other)"
246376
246819
  },
246377
246820
  {
246378
246821
  id: "alarm",
246379
- name: "Alarm / Siren",
246380
- icon: "\u{1F6A8}"
246822
+ name: "Alarm / Siren"
246381
246823
  },
246382
246824
  {
246383
246825
  id: "doorbell",
246384
- name: "Doorbell / Knock",
246385
- icon: "\u{1F514}"
246826
+ name: "Doorbell / Knock"
246386
246827
  },
246387
246828
  {
246388
246829
  id: "glass_breaking",
246389
- name: "Glass Breaking",
246390
- icon: "\u{1F4A5}"
246830
+ name: "Glass Breaking"
246391
246831
  },
246392
246832
  {
246393
246833
  id: "gunshot",
246394
- name: "Gunshot / Explosion",
246395
- icon: "\u{1F4A3}"
246834
+ name: "Gunshot / Explosion"
246396
246835
  },
246397
246836
  {
246398
246837
  id: "vehicle",
246399
- name: "Vehicle",
246400
- icon: "\u{1F697}"
246838
+ name: "Vehicle"
246401
246839
  },
246402
246840
  {
246403
246841
  id: "siren",
246404
- name: "Emergency Siren",
246405
- icon: "\u{1F691}"
246842
+ name: "Emergency Siren"
246406
246843
  },
246407
246844
  {
246408
246845
  id: "fire",
246409
- name: "Fire / Smoke",
246410
- icon: "\u{1F525}"
246846
+ name: "Fire / Smoke"
246411
246847
  },
246412
246848
  {
246413
246849
  id: "water",
246414
- name: "Water",
246415
- icon: "\u{1F4A7}"
246850
+ name: "Water"
246416
246851
  },
246417
246852
  {
246418
246853
  id: "wind",
246419
- name: "Wind / Weather",
246420
- icon: "\u{1F32C}\uFE0F"
246854
+ name: "Wind / Weather"
246421
246855
  },
246422
246856
  {
246423
246857
  id: "door",
246424
- name: "Door",
246425
- icon: "\u{1F6AA}"
246858
+ name: "Door"
246426
246859
  },
246427
246860
  {
246428
246861
  id: "footsteps",
246429
- name: "Footsteps",
246430
- icon: "\u{1F463}"
246862
+ name: "Footsteps"
246431
246863
  },
246432
246864
  {
246433
246865
  id: "crowd",
246434
- name: "Crowd / Chatter",
246435
- icon: "\u{1F465}"
246866
+ name: "Crowd / Chatter"
246436
246867
  },
246437
246868
  {
246438
246869
  id: "telephone",
246439
- name: "Telephone",
246440
- icon: "\u{1F4DE}"
246870
+ name: "Telephone"
246441
246871
  },
246442
246872
  {
246443
246873
  id: "engine",
246444
- name: "Engine / Motor",
246445
- icon: "\u2699\uFE0F"
246874
+ name: "Engine / Motor"
246446
246875
  },
246447
246876
  {
246448
246877
  id: "tools",
246449
- name: "Tools / Construction",
246450
- icon: "\u{1F528}"
246878
+ name: "Tools / Construction"
246451
246879
  },
246452
246880
  {
246453
246881
  id: "silence",
246454
- name: "Silence",
246455
- icon: "\u{1F92B}"
246882
+ name: "Silence"
246456
246883
  }
246457
246884
  ];
246458
246885
  var YAMNET_TO_MACRO = {
@@ -249040,6 +249467,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249040
249467
  */
249041
249468
  hasFace: zod.z.boolean().optional(),
249042
249469
  /**
249470
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
249471
+ * face an operator could ASSIGN to an identity.
249472
+ *
249473
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
249474
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
249475
+ * branch, before every gate, and means no more than "a face detector produced
249476
+ * a face detail". This one is stamped at the single moment the gallery row
249477
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
249478
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
249479
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
249480
+ * the crop-store drop. Everything between the detector and that insert can
249481
+ * legitimately refuse the face, so a flag written any earlier promises the
249482
+ * operator something to assign and delivers nothing.
249483
+ *
249484
+ * **Independent of recognition.** A face collected but never auto-matched is
249485
+ * still assignable — it is in fact the face an operator most wants to reach —
249486
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
249487
+ * `subLabel`; this says only that the raw material exists.
249488
+ *
249489
+ * **Set once, never cleared.** A track that produced a gallery row produced
249490
+ * one; deleting the row later is the gallery's business, not this flag's.
249491
+ *
249492
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
249493
+ * before the column omits it, and so does every server that predates the
249494
+ * field. A consumer must test `=== true` and render nothing otherwise —
249495
+ * never infer "no assignable face".
249496
+ */
249497
+ hasEmbeddedFace: zod.z.boolean().optional(),
249498
+ /**
249043
249499
  * This subject CONTAINS a folded rider — a person the rider-pairing step
249044
249500
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
249045
249501
  * so the passage is tracked once and as a VEHICLE.
@@ -250800,6 +251256,32 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
250800
251256
  /** FRAME-space bbox (already mapped back from crop space). */
250801
251257
  bbox: NativeCropBboxSchema.optional(),
250802
251258
  embedding: zod.z.string().optional(),
251259
+ /**
251260
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
251261
+ * so an older runner that does not send it degrades to "unmeasurable", which
251262
+ * the consuming gate treats as accept — never as reject.
251263
+ */
251264
+ embeddingMagnitude: zod.z.number().optional(),
251265
+ /**
251266
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
251267
+ * `label`, and for the same reason: the producing node is the only side that
251268
+ * knows which model actually ran, and re-deriving it downstream is exactly
251269
+ * the inference this model exists to forbid.
251270
+ *
251271
+ * Additive and optional, so a runner that predates it degrades to
251272
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
251273
+ * its previous assumption instead of refusing.
251274
+ *
251275
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
251276
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
251277
+ * with nothing on the wire it took the id from the GALLERY, so the gate
251278
+ * compared the gallery against itself and could never fire. That turned the
251279
+ * one guard protecting the face feature space into a tautology, and it is
251280
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
251281
+ * gallery as the catalog comment predicts — it would have scored the new
251282
+ * model's vectors against the old model's samples in one cosine space.
251283
+ */
251284
+ embeddingModelId: zod.z.string().optional(),
250803
251285
  label: zod.z.string().optional(),
250804
251286
  /**
250805
251287
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -252624,6 +253106,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252624
253106
  isBattery: zod.z.boolean(),
252625
253107
  reason: zod.z.enum([
252626
253108
  "disabled",
253109
+ "offline",
252627
253110
  "sleeping",
252628
253111
  "unreachable",
252629
253112
  "waking"
@@ -252714,9 +253197,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252714
253197
  sleeping: zod.z.boolean(),
252715
253198
  /** Current device state rendered over the cached frame. State images
252716
253199
  * remain authoritative even when their photographic background is
252717
- * old; null means the link must carry a current camera frame. */
253200
+ * old; null means the link must carry a current camera frame.
253201
+ *
253202
+ * **This is the ONLY source a surface may derive an inactive tile
253203
+ * from.** A client that reads a device's `online`/`disabled` flags and
253204
+ * draws its own verdict will disagree with the pixels it is painting
253205
+ * — the frame comes from here, and so must the sentence over it
253206
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
253207
+ * state. */
252718
253208
  stateReason: zod.z.enum([
252719
253209
  "disabled",
253210
+ "offline",
252720
253211
  "sleeping",
252721
253212
  "unreachable",
252722
253213
  "waking"
@@ -273706,6 +274197,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273706
274197
  function normalizeAudioLabel(value) {
273707
274198
  return value.startsWith(AUDIO_KIND_PREFIX) ? value.slice(6) : value;
273708
274199
  }
274200
+ function audioKindId(macroId) {
274201
+ return macroId.startsWith(AUDIO_KIND_PREFIX) ? macroId : `${AUDIO_KIND_PREFIX}${macroId}`;
274202
+ }
273709
274203
  var NC_AUDIO_DB_MIN = -96;
273710
274204
  var NC_AUDIO_DB_MAX = 0;
273711
274205
  var NC_AUDIO_DB_STEP = 3;
@@ -273753,8 +274247,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273753
274247
  samplingSeconds: clampInt(patch.samplingSeconds ?? base.samplingSeconds, 1, 300)
273754
274248
  };
273755
274249
  }
273756
- var UNKNOWN_LABEL_ICON = "\u{1F508}";
273757
- var ICON_BY_ID = new Map(AUDIO_MACRO_LABELS.flatMap((macro2) => macro2.icon === void 0 ? [] : [[macro2.id, macro2.icon]]));
273758
274250
  function audioLabelChoices(taxonomy, selected) {
273759
274251
  const fromHub = taxonomy?.audioKinds ?? [];
273760
274252
  const choices = fromHub.length > 0 ? fromHub.map((entry) => {
@@ -273762,13 +274254,13 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273762
274254
  return {
273763
274255
  value: id,
273764
274256
  label: entry.label,
273765
- icon: ICON_BY_ID.get(id) ?? UNKNOWN_LABEL_ICON,
274257
+ iconId: audioKindId(id),
273766
274258
  unknown: false
273767
274259
  };
273768
274260
  }) : AUDIO_MACRO_LABELS.map((macro2) => ({
273769
274261
  value: macro2.id,
273770
274262
  label: macro2.name,
273771
- icon: macro2.icon ?? UNKNOWN_LABEL_ICON,
274263
+ iconId: audioKindId(macro2.id),
273772
274264
  unknown: false
273773
274265
  }));
273774
274266
  const known = new Set(choices.map((choice) => choice.value));
@@ -273780,7 +274272,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273780
274272
  choices.push({
273781
274273
  value: id,
273782
274274
  label: id,
273783
- icon: UNKNOWN_LABEL_ICON,
274275
+ iconId: audioKindId(id),
273784
274276
  unknown: true
273785
274277
  });
273786
274278
  }
@@ -274504,6 +274996,74 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274504
274996
  if (map !== void 0) return map[String(deviceId)] ?? 0;
274505
274997
  return rule.lastGeneratedAt ?? 0;
274506
274998
  }
274999
+ var CLUSTER_MODEL_SCOPED_STEPS = [{
275000
+ stepId: "face-embedding",
275001
+ label: "Face recognition model",
275002
+ defaultModelId: "arcface-r100",
275003
+ indexName: "the enrolled face gallery",
275004
+ options: [
275005
+ {
275006
+ id: "arcface-r100",
275007
+ label: "ArcFace (ResNet34)"
275008
+ },
275009
+ {
275010
+ id: "auraface-r100",
275011
+ label: "AuraFace R100"
275012
+ },
275013
+ {
275014
+ id: "inception-resnet-v1",
275015
+ label: "Inception ResNet V1"
275016
+ }
275017
+ ]
275018
+ }, {
275019
+ stepId: "clip-embedding",
275020
+ label: "Semantic search model",
275021
+ defaultModelId: "mobileclip-s1",
275022
+ indexName: "the object semantic-search index",
275023
+ options: [{
275024
+ id: "mobileclip-s1",
275025
+ label: "MobileCLIP S1"
275026
+ }, {
275027
+ id: "mobileclip-s2",
275028
+ label: "MobileCLIP S2"
275029
+ }]
275030
+ }];
275031
+ var CLUSTER_MODEL_SECTION_ID = "cluster-step-models";
275032
+ function clusterModelSettingKey(stepId) {
275033
+ return `clusterModel:${stepId}`;
275034
+ }
275035
+ function isClusterScopedStep(stepId) {
275036
+ return CLUSTER_MODEL_SCOPED_STEPS.some((s) => s.stepId === stepId);
275037
+ }
275038
+ var DEFAULT_CLUSTER_STEP_MODELS = Object.freeze(Object.fromEntries(CLUSTER_MODEL_SCOPED_STEPS.map((s) => [s.stepId, s.defaultModelId])));
275039
+ var ChosenModelIdSchema = zod.z.string().min(1);
275040
+ function readClusterStepModels(config) {
275041
+ const out = {};
275042
+ for (const step2 of CLUSTER_MODEL_SCOPED_STEPS) {
275043
+ const parsed = ChosenModelIdSchema.safeParse(config[clusterModelSettingKey(step2.stepId)]);
275044
+ out[step2.stepId] = parsed.success ? parsed.data : step2.defaultModelId;
275045
+ }
275046
+ return out;
275047
+ }
275048
+ function isHydratedField$2(entry) {
275049
+ return typeof entry === "object" && entry !== null && "key" in entry;
275050
+ }
275051
+ function pickClusterStepModels(view) {
275052
+ if (view === null) return DEFAULT_CLUSTER_STEP_MODELS;
275053
+ const flat = {};
275054
+ const wanted = new Set(CLUSTER_MODEL_SCOPED_STEPS.map((s) => clusterModelSettingKey(s.stepId)));
275055
+ for (const section of view.sections) for (const entry of section.fields) {
275056
+ if (!isHydratedField$2(entry) || typeof entry.key !== "string") continue;
275057
+ if (wanted.has(entry.key)) flat[entry.key] = entry.value;
275058
+ }
275059
+ return readClusterStepModels(flat);
275060
+ }
275061
+ function resolveClusterStepModelId(stepId, models) {
275062
+ const step2 = CLUSTER_MODEL_SCOPED_STEPS.find((s) => s.stepId === stepId);
275063
+ if (step2 === void 0) return null;
275064
+ const chosen = models[stepId];
275065
+ return chosen !== void 0 && chosen !== "" ? chosen : step2.defaultModelId;
275066
+ }
274507
275067
  var DETAIL_CROP_SECTION_ID = "detail-crop";
274508
275068
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
274509
275069
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
@@ -274551,6 +275111,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274551
275111
  }
274552
275112
  return readDetailCropConvention(flat);
274553
275113
  }
275114
+ var FULL_IMAGE_BBOX = Object.freeze({
275115
+ x: 0,
275116
+ y: 0,
275117
+ w: 1,
275118
+ h: 1
275119
+ });
274554
275120
  var DETAIL_CROP_PADDING_FIELD = {
274555
275121
  min: 0,
274556
275122
  max: 1,
@@ -276286,6 +276852,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276286
276852
  exports.CAP_NAMES_WITH_STATUS = CAP_NAMES_WITH_STATUS;
276287
276853
  exports.CAP_NODE_PIN_CONTEXT_KEY = require_sleep.CAP_NODE_PIN_CONTEXT_KEY;
276288
276854
  exports.CAP_PROVIDER_KIND_MAP = CAP_PROVIDER_KIND_MAP;
276855
+ exports.CLUSTER_MODEL_SCOPED_STEPS = CLUSTER_MODEL_SCOPED_STEPS;
276856
+ exports.CLUSTER_MODEL_SECTION_ID = CLUSTER_MODEL_SECTION_ID;
276289
276857
  exports.COCO_80_LABELS = COCO_80_LABELS;
276290
276858
  exports.COCO_TO_MACRO = COCO_TO_MACRO;
276291
276859
  exports.CONNECTION_TEST_TIMEOUT_MS = CONNECTION_TEST_TIMEOUT_MS;
@@ -276373,6 +276941,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276373
276941
  exports.DECLARED_INTEGRATION_FIXED_KEY = DECLARED_INTEGRATION_FIXED_KEY;
276374
276942
  exports.DEFAULT_ADDON_PLACEMENT = DEFAULT_ADDON_PLACEMENT;
276375
276943
  exports.DEFAULT_AUDIO_ANALYZER_CONFIG = DEFAULT_AUDIO_ANALYZER_CONFIG;
276944
+ exports.DEFAULT_CLUSTER_STEP_MODELS = DEFAULT_CLUSTER_STEP_MODELS;
276376
276945
  exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
276377
276946
  exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
276378
276947
  exports.DEFAULT_EVENTS_BAND_BUFFER_SEC = DEFAULT_EVENTS_BAND_BUFFER_SEC;
@@ -276491,6 +277060,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276491
277060
  exports.ExpressionLiteralBindingSchema = ExpressionLiteralBindingSchema;
276492
277061
  exports.ExpressionParseError = ExpressionParseError;
276493
277062
  exports.ExpressionSourceSchema = ExpressionSourceSchema;
277063
+ exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
276494
277064
  exports.FanControlStatusSchema = FanControlStatusSchema;
276495
277065
  exports.FanDirectionSchema = FanDirectionSchema;
276496
277066
  exports.FeatureManifestSchema = FeatureManifestSchema;
@@ -277110,6 +277680,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277110
277680
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
277111
277681
  exports.audioCodecCapability = audioCodecCapability;
277112
277682
  exports.audioIsFailClosed = audioIsFailClosed;
277683
+ exports.audioKindId = audioKindId;
277113
277684
  exports.audioLabelChoices = audioLabelChoices;
277114
277685
  exports.audioMetricsCapability = audioMetricsCapability;
277115
277686
  exports.audioModeOf = audioModeOf;
@@ -277148,6 +277719,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277148
277719
  exports.classifyStream = classifyStream;
277149
277720
  exports.classifyStreams = classifyStreams;
277150
277721
  exports.climateControlCapability = climateControlCapability;
277722
+ exports.clusterModelSettingKey = clusterModelSettingKey;
277151
277723
  exports.collectHydratedFieldEntries = require_sleep.collectHydratedFieldEntries;
277152
277724
  exports.collectHydratedFieldValues = require_sleep.collectHydratedFieldValues;
277153
277725
  exports.colorCapability = colorCapability;
@@ -277266,6 +277838,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277266
277838
  exports.isAudioRule = isAudioRule;
277267
277839
  exports.isBaseConditionKey = isBaseConditionKey;
277268
277840
  exports.isBatteryPresenceFault = isBatteryPresenceFault;
277841
+ exports.isClusterScopedStep = isClusterScopedStep;
277269
277842
  exports.isCollectionArrayMethod = isCollectionArrayMethod;
277270
277843
  exports.isDeployableToAgent = isDeployableToAgent;
277271
277844
  exports.isDetectionMacroClass = isDetectionMacroClass;
@@ -277349,6 +277922,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277349
277922
  exports.patchAudio = patchAudio;
277350
277923
  exports.petFeederCapability = petFeederCapability;
277351
277924
  exports.pickAccessoryControl = pickAccessoryControl;
277925
+ exports.pickClusterStepModels = pickClusterStepModels;
277352
277926
  exports.pickDetailCropConvention = pickDetailCropConvention;
277353
277927
  exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
277354
277928
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
@@ -277372,6 +277946,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277372
277946
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
277373
277947
  exports.ptzCapability = ptzCapability;
277374
277948
  exports.pythonScriptForBackend = pythonScriptForBackend;
277949
+ exports.readClusterStepModels = readClusterStepModels;
277375
277950
  exports.readDetailCropConvention = readDetailCropConvention;
277376
277951
  exports.readDeviceStateFrom = readDeviceStateFrom;
277377
277952
  exports.readNativeLeaseOverride = readNativeLeaseOverride;
@@ -277389,6 +277964,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277389
277964
  exports.resolveAddonPlacement = resolveAddonPlacement;
277390
277965
  exports.resolveAddonRuntime = resolveAddonRuntime;
277391
277966
  exports.resolveCapMount = require_sleep.resolveCapMount;
277967
+ exports.resolveClusterStepModelId = resolveClusterStepModelId;
277392
277968
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
277393
277969
  exports.resolveDeviceControlKind = resolveDeviceControlKind;
277394
277970
  exports.resolveDeviceProfile = resolveDeviceProfile;
@@ -277698,22 +278274,20 @@ var require_update_availability_emitter = __commonJS({
277698
278274
  this.persistIfDirty();
277699
278275
  if (!latestChanged)
277700
278276
  return;
277701
- this.emitList(target);
278277
+ this.emitList(target, scope);
277702
278278
  }
277703
278279
  /** Publish a partial candidate set without treating omissions as up-to-date. */
277704
278280
  publishCandidates(candidates) {
277705
- let latestChanged = false;
277706
- const targets = /* @__PURE__ */ new Set();
278281
+ const changed = /* @__PURE__ */ new Map();
277707
278282
  for (const candidate of candidates) {
277708
- targets.add(candidate.target);
277709
- if (this.note(candidate))
277710
- latestChanged = true;
278283
+ if (!this.note(candidate))
278284
+ continue;
278285
+ const scope = scopeOf(candidate);
278286
+ changed.set(`${candidate.target}:${scope}`, { target: candidate.target, scope });
277711
278287
  }
277712
278288
  this.persistIfDirty();
277713
- if (!latestChanged)
277714
- return;
277715
- for (const target of targets)
277716
- this.emitList(target);
278289
+ for (const { target, scope } of changed.values())
278290
+ this.emitList(target, scope);
277717
278291
  }
277718
278292
  note(candidate) {
277719
278293
  const key = this.key(candidate);
@@ -277739,20 +278313,20 @@ var require_update_availability_emitter = __commonJS({
277739
278313
  held: Object.fromEntries(this.held)
277740
278314
  });
277741
278315
  }
277742
- emitList(target) {
277743
- const list = [...this.held.values()].filter((candidate) => candidate.target === target);
278316
+ /** Announce ONE scope's full list. `scope` is the node id, `hub` for the hub. */
278317
+ emitList(target, scope) {
278318
+ const list = [...this.held.values()].filter((candidate) => candidate.target === target && scopeOf(candidate) === scope);
277744
278319
  if (list.length === 0)
277745
278320
  return;
277746
278321
  const packages = list.map((candidate) => ({
277747
278322
  packageName: candidate.packageName,
277748
278323
  currentVersion: candidate.currentVersion,
277749
278324
  latestVersion: candidate.latestVersion,
277750
- ...candidate.nodeId !== void 0 ? { nodeId: candidate.nodeId } : {}
278325
+ nodeId: scope
277751
278326
  }));
277752
- const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
277753
278327
  const head = list[0];
277754
278328
  this.eventBus.emit({
277755
- id: `update.available:${target}:${packages.map((pkg) => `${pkg.nodeId ?? "hub"}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",")}`,
278329
+ id: `update.available:${target}:${scope}:${packages.map((pkg) => `${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",")}`,
277756
278330
  timestamp: /* @__PURE__ */ new Date(),
277757
278331
  source: this.source,
277758
278332
  category: types_1.EventCategory.UpdateAvailable,
@@ -277762,24 +278336,24 @@ var require_update_availability_emitter = __commonJS({
277762
278336
  currentVersion: head.currentVersion,
277763
278337
  latestVersion: head.latestVersion,
277764
278338
  packages,
277765
- ...head.nodeId !== void 0 ? { nodeId: head.nodeId } : {},
277766
- ...nodeIds.length > 0 ? { nodeIds } : {}
278339
+ nodeId: scope,
278340
+ nodeIds: [scope]
277767
278341
  }
277768
278342
  });
277769
278343
  }
277770
278344
  key(candidate) {
277771
- return `${candidate.target}:${candidate.nodeId ?? "hub"}:${candidate.packageName}`;
278345
+ return `${candidate.target}:${scopeOf(candidate)}:${candidate.packageName}`;
277772
278346
  }
277773
278347
  };
277774
278348
  exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
278349
+ function scopeOf(candidate) {
278350
+ return candidate.nodeId ?? "hub";
278351
+ }
277775
278352
  function stampNode(candidate, nodeId) {
277776
278353
  if (nodeId === void 0 || candidate.nodeId !== void 0)
277777
278354
  return candidate;
277778
278355
  return { ...candidate, nodeId };
277779
278356
  }
277780
- function unique(values) {
277781
- return [...new Set(values)];
277782
- }
277783
278357
  }
277784
278358
  });
277785
278359
 
@@ -279134,7 +279708,13 @@ var require_server_root = __commonJS({
279134
279708
  fromVersion: runningVersion
279135
279709
  }
279136
279710
  });
279137
- this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
279711
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`, {
279712
+ packageName: this.spec.packageName,
279713
+ ...runningVersion !== null ? {
279714
+ fromVersion: runningVersion
279715
+ } : {},
279716
+ toVersion: target
279717
+ });
279138
279718
  return {
279139
279719
  accepted: true,
279140
279720
  targetVersion: target,
@@ -279419,6 +279999,47 @@ var require_server_root = __commonJS({
279419
279999
  }
279420
280000
  });
279421
280001
 
280002
+ // ../../server/backend/dist/core/updates/restart-marker.js
280003
+ var require_restart_marker = __commonJS({
280004
+ "../../server/backend/dist/core/updates/restart-marker.js"(exports) {
280005
+ "use strict";
280006
+ Object.defineProperty(exports, "__esModule", { value: true });
280007
+ exports.buildRestartMarker = buildRestartMarker;
280008
+ exports.isServerUpdateReason = isServerUpdateReason;
280009
+ exports.versionFromReason = versionFromReason;
280010
+ var SERVER_UPDATE_PREFIX = "server-update:";
280011
+ var ROOT_PACKAGE_NAME = "@camstack/server";
280012
+ function buildRestartMarker(input) {
280013
+ const { requestedBy, update, requestedAt } = input;
280014
+ const toVersion = update?.toVersion ?? versionFromReason(requestedBy);
280015
+ const isUpdate = update !== void 0 || isServerUpdateReason(requestedBy) && toVersion !== void 0;
280016
+ if (!isUpdate) {
280017
+ return {
280018
+ kind: "manual",
280019
+ requestedAt,
280020
+ ...requestedBy !== void 0 ? { requestedBy } : {}
280021
+ };
280022
+ }
280023
+ return {
280024
+ kind: "framework-update",
280025
+ requestedAt,
280026
+ ...requestedBy !== void 0 ? { requestedBy } : {},
280027
+ packageName: update?.packageName ?? ROOT_PACKAGE_NAME,
280028
+ ...update?.fromVersion !== void 0 ? { fromVersion: update.fromVersion } : {},
280029
+ ...toVersion !== void 0 ? { toVersion } : {}
280030
+ };
280031
+ }
280032
+ function isServerUpdateReason(requestedBy) {
280033
+ return requestedBy !== void 0 && requestedBy.startsWith(SERVER_UPDATE_PREFIX);
280034
+ }
280035
+ function versionFromReason(requestedBy) {
280036
+ if (requestedBy === void 0)
280037
+ return void 0;
280038
+ return /@([^@\s]+)$/.exec(requestedBy)?.[1];
280039
+ }
280040
+ }
280041
+ });
280042
+
279422
280043
  // ../../server/backend/dist/agent/agent-update-service.js
279423
280044
  var require_agent_update_service = __commonJS({
279424
280045
  "../../server/backend/dist/agent/agent-update-service.js"(exports) {
@@ -279476,6 +280097,7 @@ var require_agent_update_service = __commonJS({
279476
280097
  var update_availability_emitter_js_1 = require_update_availability_emitter();
279477
280098
  var update_availability_store_js_1 = require_update_availability_store();
279478
280099
  var index_js_1 = require_server_root();
280100
+ var restart_marker_js_1 = require_restart_marker();
279479
280101
  exports.AGENT_RUNTIME_ADDON_ID = "agent-runtime";
279480
280102
  exports.AGENT_RESTART_GRACE_MS = 2e3;
279481
280103
  function isRunningInContainer(existsSyncFn) {
@@ -279539,8 +280161,8 @@ var require_agent_update_service = __commonJS({
279539
280161
  seedDir: "CAMSTACK_SEED_SERVER_DIR"
279540
280162
  },
279541
280163
  logger: options.logger,
279542
- restartServer: (requestedBy) => {
279543
- writeAgentRestartMarker(options.dataDir, requestedBy);
280164
+ restartServer: (requestedBy, update) => {
280165
+ writeAgentRestartMarker(options.dataDir, requestedBy, update);
279544
280166
  restartAgent(requestedBy);
279545
280167
  },
279546
280168
  dataDir: options.dataDir,
@@ -279618,18 +280240,13 @@ var require_agent_update_service = __commonJS({
279618
280240
  }
279619
280241
  };
279620
280242
  exports.AgentUpdateService = AgentUpdateService;
279621
- function writeAgentRestartMarker(dataDir, requestedBy) {
279622
- const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
280243
+ function writeAgentRestartMarker(dataDir, requestedBy, update) {
279623
280244
  try {
279624
- (0, system_1.writePendingRestart)(dataDir, {
279625
- kind: requestedBy.startsWith("server-update:") ? "framework-update" : "manual",
280245
+ (0, system_1.writePendingRestart)(dataDir, (0, restart_marker_js_1.buildRestartMarker)({
279626
280246
  requestedAt: Date.now(),
279627
280247
  requestedBy,
279628
- ...requestedBy.startsWith("server-update:") ? {
279629
- packageName: "@camstack/server",
279630
- ...toVersion !== void 0 ? { toVersion } : {}
279631
- } : {}
279632
- });
280248
+ ...update !== void 0 ? { update } : {}
280249
+ }));
279633
280250
  } catch {
279634
280251
  }
279635
280252
  }
@@ -388500,6 +389117,7 @@ var require_addon_package_service = __commonJS({
388500
389117
  var types_1 = require_dist9();
388501
389118
  var update_availability_emitter_js_1 = require_update_availability_emitter();
388502
389119
  var update_check_scheduler_js_1 = require_update_check_scheduler();
389120
+ var restart_marker_js_1 = require_restart_marker();
388503
389121
  var package_dir_utils_js_1 = require_package_dir_utils();
388504
389122
  var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
388505
389123
  exports.SYSTEM_PACKAGE = "@camstack/system";
@@ -389427,26 +390045,24 @@ var require_addon_package_service = __commonJS({
389427
390045
  /**
389428
390046
  * Gracefully restart the server process.
389429
390047
  *
389430
- * Writes a `manual` restart marker so the post-boot service can
389431
- * emit `system.restart-completed` on the next boot (powers the
389432
- * admin-UI "Server restarted" toast). Emits `system.restarting`
390048
+ * Writes the restart marker so the post-boot service can emit
390049
+ * `system.restart-completed` on the next boot (powers the admin-UI
390050
+ * "Server restarted" toast AND the operator's "Server aggiornato"
390051
+ * notification). `update` is passed by `RootUpdateService` for an
390052
+ * update-driven restart and is the ONLY source of the version being left
390053
+ * behind — see `updates/restart-marker.ts`. Emits `system.restarting`
389433
390054
  * with the same payload so subscribers can show a reconnect overlay
389434
390055
  * immediately. Then delegates to `scheduleSelfRestart` which picks
389435
390056
  * the right exit path (Electron `app.relaunch` vs supervisor-driven
389436
390057
  * `process.exit`).
389437
390058
  */
389438
- restartServer(requestedBy) {
390059
+ restartServer(requestedBy, update) {
389439
390060
  this.logger.info("Server restart requested -- initiating graceful shutdown");
389440
- const toVersion = toVersionFromRestartReason(requestedBy);
389441
- const payload = {
389442
- kind: isServerUpdateRequest(requestedBy) ? "framework-update" : "manual",
390061
+ const payload = (0, restart_marker_js_1.buildRestartMarker)({
389443
390062
  requestedAt: Date.now(),
389444
390063
  ...requestedBy !== void 0 ? { requestedBy } : {},
389445
- ...isServerUpdateRequest(requestedBy) ? {
389446
- packageName: "@camstack/server",
389447
- ...toVersion !== void 0 ? { toVersion } : {}
389448
- } : {}
389449
- };
390064
+ ...update !== void 0 ? { update } : {}
390065
+ });
389450
390066
  try {
389451
390067
  (0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
389452
390068
  } catch (err) {
@@ -389937,22 +390553,23 @@ var require_addon_package_service = __commonJS({
389937
390553
  * re-announces them on the next good poll.
389938
390554
  */
389939
390555
  publishAddonAvailability(updates, failures, nodeId) {
390556
+ const scope = nodeId ?? this.resolveNodeId();
389940
390557
  const candidates = updates.map((update) => ({
389941
390558
  target: "addon",
389942
390559
  packageName: update.name,
389943
390560
  currentVersion: update.currentVersion,
389944
390561
  latestVersion: update.latestVersion,
389945
- ...nodeId !== void 0 ? { nodeId } : {}
390562
+ nodeId: scope
389946
390563
  }));
389947
390564
  if (failures > 0) {
389948
390565
  this.logger.warn("Addon update check incomplete \u2014 availability published as partial", {
389949
- tags: { nodeId: nodeId ?? "hub" },
390566
+ tags: { nodeId: scope },
389950
390567
  meta: { failures, published: candidates.length }
389951
390568
  });
389952
390569
  this.updateAvailability.publishCandidates(candidates);
389953
390570
  return;
389954
390571
  }
389955
- this.updateAvailability.publishSnapshot("addon", candidates, nodeId);
390572
+ this.updateAvailability.publishSnapshot("addon", candidates, scope);
389956
390573
  }
389957
390574
  /** Fetch npm search results for camstack addon packages (cached 5 min) */
389958
390575
  async fetchSearchFromNpm() {
@@ -390102,15 +390719,6 @@ var require_addon_package_service = __commonJS({
390102
390719
  }
390103
390720
  };
390104
390721
  exports.AddonPackageService = AddonPackageService;
390105
- function isServerUpdateRequest(requestedBy) {
390106
- return requestedBy !== void 0 && requestedBy.startsWith("server-update:");
390107
- }
390108
- function toVersionFromRestartReason(requestedBy) {
390109
- if (requestedBy === void 0)
390110
- return void 0;
390111
- const match = /@([^@\s]+)$/.exec(requestedBy);
390112
- return match?.[1];
390113
- }
390114
390722
  async function extractTgzStripped(tgz, destDir) {
390115
390723
  fs.mkdirSync(destDir, { recursive: true });
390116
390724
  const tmpFile = path.join(os.tmpdir(), `camstack-tgz-${(0, node_crypto_1.randomUUID)()}.tgz`);
@@ -401619,14 +402227,6 @@ var require_integration_visibility = __commonJS({
401619
402227
  function createFilteredIntegrationRegistry(deps) {
401620
402228
  const { raw, logger } = deps;
401621
402229
  const isInstalled = (addonId) => deps.isAddonInstalled((0, types_1.bareAddonId)(addonId));
401622
- let activeIntegrationIds = null;
401623
- const ensureActiveIds = async () => {
401624
- if (activeIntegrationIds)
401625
- return activeIntegrationIds;
401626
- const all = await raw.listIntegrations();
401627
- activeIntegrationIds = new Set(all.filter((i) => isInstalled(i.addonId)).map((i) => i.id));
401628
- return activeIntegrationIds;
401629
- };
401630
402230
  const logHidden = (integration, phase) => {
401631
402231
  logger.warn("integration hidden \u2014 its addon is not installed, so this read answers null even though the row exists", {
401632
402232
  tags: { integrationId: integration.id, addonId: integration.addonId },
@@ -401672,39 +402272,7 @@ var require_integration_visibility = __commonJS({
401672
402272
  // Integration settings: passthrough (already gated by getIntegration)
401673
402273
  getIntegrationSettings: (id) => raw.getIntegrationSettings(id),
401674
402274
  setIntegrationSetting: (id, key, value) => raw.setIntegrationSetting(id, key, value),
401675
- setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings),
401676
- // Devices: filter out devices belonging to hidden integrations
401677
- createDevice: (input) => raw.createDevice(input),
401678
- getDevice: async (id) => {
401679
- const d = await raw.getDevice(id);
401680
- if (!d)
401681
- return null;
401682
- const ids = await ensureActiveIds();
401683
- return ids.has(d.integrationId) ? d : null;
401684
- },
401685
- getDeviceByStableId: async (stableId) => {
401686
- const d = await raw.getDeviceByStableId(stableId);
401687
- if (!d)
401688
- return null;
401689
- const ids = await ensureActiveIds();
401690
- return ids.has(d.integrationId) ? d : null;
401691
- },
401692
- listDevices: async (integrationId) => {
401693
- const devices = await raw.listDevices(integrationId);
401694
- const ids = await ensureActiveIds();
401695
- return devices.filter((d) => ids.has(d.integrationId));
401696
- },
401697
- listCameras: async () => {
401698
- const cameras = await raw.listCameras();
401699
- const ids = await ensureActiveIds();
401700
- return cameras.filter((d) => ids.has(d.integrationId));
401701
- },
401702
- updateDevice: (id, updates) => raw.updateDevice(id, updates),
401703
- deleteDevice: (id) => raw.deleteDevice(id),
401704
- // Device settings: passthrough
401705
- getDeviceSettings: (id) => raw.getDeviceSettings(id),
401706
- setDeviceSetting: (id, key, value) => raw.setDeviceSetting(id, key, value),
401707
- setDeviceSettings: (id, settings) => raw.setDeviceSettings(id, settings)
402275
+ setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings)
401708
402276
  };
401709
402277
  }
401710
402278
  }
@@ -401908,6 +402476,58 @@ var require_runner_spawn_fanout = __commonJS({
401908
402476
  }
401909
402477
  });
401910
402478
 
402479
+ // ../../server/backend/dist/core/addon/single-flight-refresh.js
402480
+ var require_single_flight_refresh = __commonJS({
402481
+ "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
402482
+ "use strict";
402483
+ Object.defineProperty(exports, "__esModule", { value: true });
402484
+ exports.createSingleFlightRefresh = createSingleFlightRefresh;
402485
+ function createSingleFlightRefresh(run) {
402486
+ let inFlight = null;
402487
+ let pending = false;
402488
+ let pendingPromise = null;
402489
+ let releasePending = () => {
402490
+ };
402491
+ const start = () => {
402492
+ const active = (async () => {
402493
+ try {
402494
+ await run();
402495
+ } finally {
402496
+ inFlight = null;
402497
+ if (pending) {
402498
+ pending = false;
402499
+ const release = releasePending;
402500
+ releasePending = () => {
402501
+ };
402502
+ pendingPromise = null;
402503
+ void start().finally(release);
402504
+ }
402505
+ }
402506
+ })();
402507
+ inFlight = active;
402508
+ return active;
402509
+ };
402510
+ return {
402511
+ request: async () => {
402512
+ if (inFlight === null) {
402513
+ await start();
402514
+ return;
402515
+ }
402516
+ if (!pending) {
402517
+ pending = true;
402518
+ pendingPromise = new Promise((resolve) => {
402519
+ releasePending = resolve;
402520
+ });
402521
+ }
402522
+ const wait = pendingPromise;
402523
+ if (wait !== null)
402524
+ await wait;
402525
+ }
402526
+ };
402527
+ }
402528
+ }
402529
+ });
402530
+
401911
402531
  // ../../server/backend/dist/core/addon/addon-registry.service.js
401912
402532
  var require_addon_registry_service = __commonJS({
401913
402533
  "../../server/backend/dist/core/addon/addon-registry.service.js"(exports) {
@@ -401970,6 +402590,7 @@ var require_addon_registry_service = __commonJS({
401970
402590
  var require_cache_js_1 = require_require_cache();
401971
402591
  var runner_convergence_1 = require_runner_convergence();
401972
402592
  var runner_spawn_fanout_js_1 = require_runner_spawn_fanout();
402593
+ var single_flight_refresh_js_1 = require_single_flight_refresh();
401973
402594
  function shouldEvictMissingOnDisk(entry, id, onDiskIds) {
401974
402595
  return entry.source === "installed" && entry.packageName !== "@camstack/system" && !onDiskIds.has(id);
401975
402596
  }
@@ -402765,11 +403386,18 @@ var require_addon_registry_service = __commonJS({
402765
403386
  }
402766
403387
  return out;
402767
403388
  };
403389
+ /** One full-fleet rebuild in flight, at most one queued behind it. Every
403390
+ * event below asks for the SAME total rebuild, so running it once per event
403391
+ * was ~1 000 rebuilds of a 974-row mirror on a boot — each one a 625 KB read
403392
+ * and parse on hub-main's event loop. No timer: the first event still
403393
+ * refreshes immediately, because this mirror backs scope enforcement.
403394
+ * See `single-flight-refresh.ts`. */
403395
+ deviceMirrorGate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)(() => this.refreshDeviceParentMirror());
402768
403396
  /** Subscribe the mirror to every device-meta lifecycle event, and warm it once
402769
403397
  * the addon set (device-manager included) is up. */
402770
403398
  wireDeviceParentMirror() {
402771
403399
  const refresh = () => {
402772
- void this.refreshDeviceParentMirror();
403400
+ void this.deviceMirrorGate.request();
402773
403401
  };
402774
403402
  for (const category of [
402775
403403
  types_1.EventCategory.DeviceMetaChanged,
@@ -406225,6 +406853,7 @@ var require_moleculer_service = __commonJS({
406225
406853
  "use strict";
406226
406854
  Object.defineProperty(exports, "__esModule", { value: true });
406227
406855
  exports.MoleculerService = void 0;
406856
+ exports.childOwnerToken = childOwnerToken;
406228
406857
  exports.buildChildUdsManifest = buildChildUdsManifest;
406229
406858
  var node_crypto_1 = __require("crypto");
406230
406859
  var system_1 = require_dist3();
@@ -406251,6 +406880,21 @@ var require_moleculer_service = __commonJS({
406251
406880
  * `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
406252
406881
  */
406253
406882
  nodeRegistry = new system_1.HubNodeRegistry();
406883
+ /**
406884
+ * nodeId → the owner token of the INCARNATION whose manifest currently backs
406885
+ * that node's capability registrations.
406886
+ *
406887
+ * A node id is not unique over time: `resolveRunnerId` is deterministic, so a
406888
+ * runner replaced by an addon update reconnects as the same `hub/<runner>`
406889
+ * node. Without an incarnation the predecessor's teardown and the successor's
406890
+ * registration are the same `(cap, addonId)` key, and the teardown wins by
406891
+ * arriving last — `recording`, `storage-evictable` and `recording-export`
406892
+ * were unregistered from a healthy process for 7 hours on 2026-08-20.
406893
+ * Populated only for transports that can observe an incarnation (UDS
406894
+ * children); remote nodes stay ownerless and behave exactly as before.
406895
+ * See `docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md`.
406896
+ */
406897
+ nodeOwners = /* @__PURE__ */ new Map();
406254
406898
  /**
406255
406899
  * Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
406256
406900
  * agent-origin readiness deltas lost while the agent stayed connected.
@@ -406572,14 +407216,18 @@ var require_moleculer_service = __commonJS({
406572
407216
  const hubNodeId = this.brokerSafe.nodeID;
406573
407217
  const childNodeId = `${hubNodeId}/${child.childId}`;
406574
407218
  const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
406575
- this.onRegisterNode(params);
406576
- logger.info("UDS child registered \u2014 manifest applied", { meta: { nodeId: childNodeId } });
407219
+ this.onRegisterNode(params, childOwnerToken(childNodeId, child.incarnation));
407220
+ logger.info("UDS child registered \u2014 manifest applied", {
407221
+ meta: { nodeId: childNodeId, incarnation: child.incarnation }
407222
+ });
406577
407223
  });
406578
- registry.onChildGone((childId) => {
407224
+ registry.onChildGone((childId, incarnation) => {
406579
407225
  const hubNodeId = this.brokerSafe.nodeID;
406580
407226
  const childNodeId = `${hubNodeId}/${childId}`;
406581
- logger.info("UDS child gone \u2014 removing from registry", { meta: { childId } });
406582
- this.removeNodeFromRegistry(childNodeId);
407227
+ logger.info("UDS child gone \u2014 removing from registry", {
407228
+ meta: { childId, incarnation }
407229
+ });
407230
+ this.removeNodeFromRegistry(childNodeId, childOwnerToken(childNodeId, incarnation));
406583
407231
  });
406584
407232
  registry.onChildLog((childId, entry) => {
406585
407233
  this.logging.writeFromWorker((0, system_1.udsChildLogToWorkerEntry)(childId, entry));
@@ -406763,10 +407411,15 @@ var require_moleculer_service = __commonJS({
406763
407411
  * CapabilityRegistry update is a diff (atomic replace) rather than an
406764
407412
  * unconditional re-register — see `applyNodeManifest` for the rationale.
406765
407413
  */
406766
- onRegisterNode(params) {
407414
+ onRegisterNode(params, owner) {
406767
407415
  const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId);
407416
+ const previousOwner = this.nodeOwners.get(params.nodeId);
406768
407417
  this.nodeRegistry.registerNode(params);
406769
- this.applyNodeManifest(params, previousManifest);
407418
+ if (owner === void 0)
407419
+ this.nodeOwners.delete(params.nodeId);
407420
+ else
407421
+ this.nodeOwners.set(params.nodeId, owner);
407422
+ this.applyNodeManifest(params, previousManifest, owner, previousOwner);
406770
407423
  const { nodeId } = params;
406771
407424
  if (nodeId !== "hub" && !nodeId.includes("/") && this.onAgentRegisteredCb) {
406772
407425
  this.onAgentRegisteredCb(nodeId);
@@ -406794,7 +407447,7 @@ var require_moleculer_service = __commonJS({
406794
407447
  * already; this method handles only `params.addons` (system caps).
406795
407448
  * Native-cap wiring into device-manager is done in a later task.
406796
407449
  */
406797
- applyNodeManifest(params, previousManifest) {
407450
+ applyNodeManifest(params, previousManifest, owner, previousOwner) {
406798
407451
  const { nodeId, addons } = params;
406799
407452
  const hubNodeId = this.brokerSafe.nodeID;
406800
407453
  const isLocalChild = nodeId.startsWith(hubNodeId + "/");
@@ -406825,9 +407478,16 @@ var require_moleculer_service = __commonJS({
406825
407478
  for (const [key, { addonId, capName }] of previous) {
406826
407479
  if (desired.has(key))
406827
407480
  continue;
406828
- registry.unregisterProvider(capName, registryKeyFor(addonId));
407481
+ registry.unregisterProvider(capName, registryKeyFor(addonId), previousOwner);
406829
407482
  this.nodeCallFns.delete(`${nodeId}::${capName}`);
406830
407483
  }
407484
+ if (owner !== void 0 && previousOwner !== void 0 && owner !== previousOwner) {
407485
+ for (const [key, { addonId, capName }] of desired) {
407486
+ if (!previous.has(key))
407487
+ continue;
407488
+ registry.reassignProviderOwner(capName, registryKeyFor(addonId), owner);
407489
+ }
407490
+ }
406831
407491
  for (const [key, { addonId, capName, capDef }] of desired) {
406832
407492
  if (previous.has(key))
406833
407493
  continue;
@@ -406852,7 +407512,7 @@ var require_moleculer_service = __commonJS({
406852
407512
  for (const methodName of Object.keys((0, system_1.expandCapMethods)(capDef))) {
406853
407513
  proxy[methodName] = (methodParams) => callFn(methodName, methodParams);
406854
407514
  }
406855
- registry.registerProvider(capName, registryKey, proxy);
407515
+ registry.registerProvider(capName, registryKey, proxy, owner);
406856
407516
  if (capDef.mode === "singleton" && isLocalChild) {
406857
407517
  const activeKey = registry.getSingletonAddonId(capName);
406858
407518
  if (activeKey === null || activeKey.includes("@")) {
@@ -406885,7 +407545,14 @@ var require_moleculer_service = __commonJS({
406885
407545
  * Unregisters every cap the node's last manifest declared and emits
406886
407546
  * synthetic readiness-down events for each.
406887
407547
  */
406888
- removeNodeFromRegistry(nodeId) {
407548
+ removeNodeFromRegistry(nodeId, owner) {
407549
+ const currentOwner = this.nodeOwners.get(nodeId);
407550
+ if (currentOwner !== void 0 && currentOwner !== owner) {
407551
+ this.logger.info("Node teardown ignored \u2014 node id reclaimed by a newer incarnation", {
407552
+ meta: { nodeId, currentOwner, presentedOwner: owner ?? null }
407553
+ });
407554
+ return;
407555
+ }
406889
407556
  const manifest = this.nodeRegistry.getNodeManifest(nodeId);
406890
407557
  if (!manifest)
406891
407558
  return;
@@ -406899,7 +407566,7 @@ var require_moleculer_service = __commonJS({
406899
407566
  const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`;
406900
407567
  if (registry) {
406901
407568
  for (const capName of capabilities) {
406902
- registry.unregisterProvider(capName, registryKey);
407569
+ registry.unregisterProvider(capName, registryKey, owner);
406903
407570
  }
406904
407571
  }
406905
407572
  for (const capName of capabilities) {
@@ -406920,6 +407587,7 @@ var require_moleculer_service = __commonJS({
406920
407587
  }
406921
407588
  }
406922
407589
  }
407590
+ this.nodeOwners.delete(nodeId);
406923
407591
  this.nodeRegistry.removeNode(nodeId);
406924
407592
  }
406925
407593
  findCallFn(nodeId, capabilityName) {
@@ -407083,6 +407751,9 @@ var require_moleculer_service = __commonJS({
407083
407751
  }
407084
407752
  };
407085
407753
  exports.MoleculerService = MoleculerService;
407754
+ function childOwnerToken(childNodeId, incarnation) {
407755
+ return `${childNodeId}#${incarnation}`;
407756
+ }
407086
407757
  function buildChildUdsManifest(nodeId, childId, caps) {
407087
407758
  const capsByAddon = /* @__PURE__ */ new Map();
407088
407759
  for (const cap of caps) {
@@ -408443,7 +409114,7 @@ var require_manual_boot = __commonJS({
408443
409114
  const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
408444
409115
  const serverUpdateService = new server_update_service_1.ServerUpdateService({
408445
409116
  logger: loggingService.createLogger("ServerUpdate"),
408446
- restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
409117
+ restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
408447
409118
  eventBus: eventBusService,
408448
409119
  updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, "server-update", loggingService.createLogger("UpdateAvailability"))
408449
409120
  });