camstack 1.2.35 → 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 pctx.metaStore.readAll();
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,7 +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, metadata: metadataMap, index } = await pctx.metaStore.readAll();
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);
60577
61068
  if (pctx.registry) {
60578
61069
  const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
60579
61070
  addonId,
@@ -60581,7 +61072,8 @@ var require_device_manager_addon = __commonJS({
60581
61072
  })) : pctx.registry.getAllWithAddonId();
60582
61073
  for (const { addonId: aid, device } of liveEntries) {
60583
61074
  const key = String(device.id);
60584
- 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);
60585
61077
  seen.add(key);
60586
61078
  if (camerasOnly && !info.isCamera) continue;
60587
61079
  results.push(slim ? {
@@ -60591,38 +61083,36 @@ var require_device_manager_addon = __commonJS({
60591
61083
  } : info);
60592
61084
  }
60593
61085
  }
60594
- const metaByAddonStable = /* @__PURE__ */ new Map();
60595
- for (const m of Object.values(meta)) metaByAddonStable.set(`${m.addonId}\0${m.stableId}`, m);
60596
- const targetAddons = addonId ? [addonId] : Object.keys(index);
60597
- for (const aid of targetAddons) for (const stableId of index[aid] ?? []) {
60598
- const m = metaByAddonStable.get(`${aid}\0${stableId}`);
60599
- const key = String(m.id);
60600
- 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;
60601
61091
  const persistedType = m.type;
60602
61092
  if (camerasOnly && persistedType !== require_dist10.DeviceType.Camera) continue;
60603
61093
  const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
60604
- const metadata = slim ? null : metadataMap[key] ?? null;
61094
+ const metadata = slim ? null : row.metadata;
60605
61095
  results.push({
60606
61096
  id: m.id,
60607
61097
  stableId,
60608
61098
  addonId: aid,
60609
61099
  type: persistedType,
60610
- name: m?.name ?? stableId,
60611
- location: m?.location ?? null,
60612
- disabled: m?.disabled ?? false,
60613
- parentDeviceId: m?.parentDeviceId ?? null,
60614
- 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),
60615
61105
  online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
60616
61106
  probed: pctx.host.resolveDeviceProbed(m.id),
60617
- features: persistedFeatures(m?.features),
61107
+ features: persistedFeatures(m.features),
60618
61108
  isCamera: persistedType === require_dist10.DeviceType.Camera,
60619
61109
  config: persistedConfig ?? {},
60620
61110
  metadata,
60621
- ...m?.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
60622
- ...m?.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
60623
- ...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
60624
- ...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
60625
- ...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 } : {},
60626
61116
  ...(() => {
60627
61117
  if (slim) return {};
60628
61118
  const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
@@ -60637,19 +61127,15 @@ var require_device_manager_addon = __commonJS({
60637
61127
  if (pctx.registry) {
60638
61128
  const found = resolveDeviceById(pctx.registry, deviceId);
60639
61129
  if (found) {
60640
- const key2 = String(found.device.id);
60641
- const [map, metaMap] = await Promise.all([pctx.metaStore.readMetadataMap(), pctx.metaStore.readMeta()]);
60642
- const metadata2 = map[key2] ?? null;
60643
- const metaRow = metaMap[key2] ?? null;
60644
- 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);
60645
61132
  }
60646
61133
  }
60647
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
60648
- if (!persisted) return null;
60649
- 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;
60650
61138
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60651
- const key = String(deviceId);
60652
- const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
60653
61139
  const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
60654
61140
  return {
60655
61141
  id: deviceId,
@@ -60688,26 +61174,25 @@ var require_device_manager_addon = __commonJS({
60688
61174
  }
60689
61175
  const results = [];
60690
61176
  const seen = /* @__PURE__ */ new Set();
60691
- const { index, meta, metadata: metadataMap } = await pctx.metaStore.readAll();
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);
60692
61180
  if (pctx.registry) {
60693
61181
  const liveChildren = pctx.registry.getChildren(parentDeviceId);
60694
61182
  for (const device of liveChildren) {
60695
61183
  const key = String(device.id);
60696
- const metadata = metadataMap[key] ?? null;
60697
- const metaRow = meta[key] ?? null;
60698
- 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));
60699
61186
  seen.add(key);
60700
61187
  }
60701
61188
  }
60702
- const ownerMetaByStableId = /* @__PURE__ */ new Map();
60703
- for (const m of Object.values(meta)) if (m.addonId === ownerAddonId) ownerMetaByStableId.set(m.stableId, m);
60704
- const persistedChildren = (index[ownerAddonId] ?? []).filter((sid) => ownerMetaByStableId.get(sid)?.parentDeviceId === parentDeviceId);
60705
- for (const childStableId of persistedChildren) {
60706
- const m = ownerMetaByStableId.get(childStableId);
61189
+ for (const row of childRows) {
61190
+ const m = row.meta;
61191
+ const childStableId = m.stableId;
60707
61192
  const key = String(m.id);
60708
61193
  if (seen.has(key)) continue;
60709
61194
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60710
- const metadata = metadataMap[key] ?? null;
61195
+ const metadata = row.metadata;
60711
61196
  const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
60712
61197
  results.push({
60713
61198
  id: m.id,
@@ -60851,16 +61336,10 @@ var require_device_manager_addon = __commonJS({
60851
61336
  }
60852
61337
  async function removeByIntegration(pctx, input) {
60853
61338
  const { integrationId } = input;
60854
- const meta = await pctx.metaStore.readMeta();
60855
- const parentKeys = Object.keys(meta).filter((key) => {
60856
- const m = meta[key];
60857
- return m !== void 0 && m.integrationId === integrationId && m.parentDeviceId === null;
60858
- });
61339
+ const parents = (await pctx.metaStore.rows.listByIntegration(integrationId)).filter((row) => row.meta.parentDeviceId === null);
60859
61340
  let removed = 0;
60860
- for (const _key of parentKeys) {
60861
- const m = meta[_key];
60862
- if (!m) continue;
60863
- await pctx.provider.remove({ deviceId: m.id });
61341
+ for (const parent of parents) {
61342
+ await pctx.provider.remove({ deviceId: parent.meta.id });
60864
61343
  removed++;
60865
61344
  }
60866
61345
  return { removed };
@@ -61249,204 +61728,6 @@ var require_device_manager_addon = __commonJS({
61249
61728
  driverPatch
61250
61729
  };
61251
61730
  }
61252
- function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
61253
- const localNodeId = ctx.kernel.localNodeId ?? "hub";
61254
- ctx.eventBus.subscribe({ category: require_dist10.EventCategory.DeviceBindingsChanged }, (event) => {
61255
- const { deviceId, capName, reason, addonId, nodeId } = event.data;
61256
- if (nodeId === localNodeId) return;
61257
- if (reason === "native-registered") {
61258
- let perDevice = remoteNativeCaps.get(deviceId);
61259
- if (!perDevice) {
61260
- perDevice = /* @__PURE__ */ new Map();
61261
- remoteNativeCaps.set(deviceId, perDevice);
61262
- }
61263
- perDevice.set(capName, {
61264
- addonId,
61265
- nodeId
61266
- });
61267
- } else if (reason === "native-unregistered") {
61268
- const perDevice = remoteNativeCaps.get(deviceId);
61269
- if (!perDevice) return;
61270
- perDevice.delete(capName);
61271
- if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
61272
- }
61273
- });
61274
- const cluster = ctx.kernel.cluster;
61275
- if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
61276
- const gone = payload.node.id;
61277
- const emptyDevices = [];
61278
- for (const [deviceId, perDevice] of remoteNativeCaps) {
61279
- const toDelete = [];
61280
- for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
61281
- for (const capName of toDelete) perDevice.delete(capName);
61282
- if (perDevice.size === 0) emptyDevices.push(deviceId);
61283
- }
61284
- for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
61285
- });
61286
- }
61287
- async function readBindingsStore(deps) {
61288
- return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
61289
- }
61290
- async function writeBindingsStore(deps, next) {
61291
- await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
61292
- }
61293
- function resolveWrapperNodeId(_wrapperAddonId) {
61294
- return "hub";
61295
- }
61296
- function toRoutableProviderNodeId(nodeId) {
61297
- const slash = nodeId.indexOf("/");
61298
- return slash === -1 ? nodeId : nodeId.slice(0, slash);
61299
- }
61300
- function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
61301
- const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
61302
- if (!clusterCaps) return null;
61303
- for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
61304
- addonId: entry.addonId,
61305
- nodeId: entry.nodeId
61306
- };
61307
- return null;
61308
- }
61309
- function resolveDeviceType(deps, rawStore, deviceId) {
61310
- const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
61311
- if (typeof persisted === "string" && persisted.length > 0) return persisted;
61312
- const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
61313
- return typeof live === "string" && live.length > 0 ? live : void 0;
61314
- }
61315
- function resolveDevicePresence(deps, rawStore, deviceId) {
61316
- if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
61317
- const meta = rawStore.deviceMeta;
61318
- if (meta && Object.keys(meta).length > 0) return meta[String(deviceId)] === void 0 ? "absent" : "present";
61319
- return "unknown";
61320
- }
61321
- function capAppliesToDeviceType(def, deviceType) {
61322
- if (deviceType === void 0) return true;
61323
- const declared = def?.deviceTypes;
61324
- if (!declared || declared.length === 0) return true;
61325
- return declared.some((t) => t === deviceType);
61326
- }
61327
- async function getBindings(deps, input) {
61328
- const storeKey = String(input.deviceId);
61329
- const rawStore = await deps.ctx.settings.readAddonStore();
61330
- const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
61331
- const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
61332
- const presence = resolveDevicePresence(deps, rawStore, input.deviceId);
61333
- const entries = [];
61334
- const seenCaps = /* @__PURE__ */ new Set();
61335
- const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
61336
- for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
61337
- const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
61338
- const remoteNative = resolveRemote(capName);
61339
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
61340
- const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
61341
- if (wrapperAddonId === null && !nativeAddonId) {
61342
- seenCaps.add(capName);
61343
- continue;
61344
- }
61345
- entries.push({
61346
- capName,
61347
- kind: wrapperAddonId ? "wrapped" : "native",
61348
- providerAddonId: wrapperAddonId ?? nativeAddonId,
61349
- providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
61350
- nativeAddonId
61351
- });
61352
- seenCaps.add(capName);
61353
- }
61354
- if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device \u2014 returning none", { tags: { deviceId: input.deviceId } });
61355
- else if (deps.capabilityRegistry) {
61356
- const skippedForType = [];
61357
- for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
61358
- if (seenCaps.has(capName)) continue;
61359
- if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
61360
- skippedForType.push(capName);
61361
- continue;
61362
- }
61363
- const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
61364
- if (!defaultWrapperAddonId) continue;
61365
- const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
61366
- const remoteNative = resolveRemote(capName);
61367
- const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
61368
- entries.push({
61369
- capName,
61370
- kind: "wrapped",
61371
- providerAddonId: defaultWrapperAddonId,
61372
- providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
61373
- nativeAddonId
61374
- });
61375
- seenCaps.add(capName);
61376
- }
61377
- if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped \u2014 deviceTypes mismatch", {
61378
- tags: { deviceId: input.deviceId },
61379
- meta: {
61380
- deviceType,
61381
- skipped: skippedForType
61382
- }
61383
- });
61384
- }
61385
- if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
61386
- if (seenCaps.has(capName)) continue;
61387
- const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
61388
- entries.push({
61389
- capName,
61390
- kind: "native",
61391
- providerAddonId: nativeAddonId,
61392
- providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
61393
- nativeAddonId
61394
- });
61395
- seenCaps.add(capName);
61396
- }
61397
- const pushFed = deps.remoteNativeCaps.get(input.deviceId);
61398
- if (pushFed) for (const [capName, info] of pushFed) {
61399
- if (seenCaps.has(capName)) continue;
61400
- entries.push({
61401
- capName,
61402
- kind: "native",
61403
- providerAddonId: info.addonId,
61404
- providerNodeId: toRoutableProviderNodeId(info.nodeId),
61405
- nativeAddonId: info.addonId
61406
- });
61407
- seenCaps.add(capName);
61408
- }
61409
- const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
61410
- if (clusterCaps) for (const entry of clusterCaps) {
61411
- if (entry.deviceId !== input.deviceId) continue;
61412
- if (seenCaps.has(entry.capName)) continue;
61413
- if (!entry.addonId) continue;
61414
- const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
61415
- if (entry.nodeId === localNodeId) continue;
61416
- entries.push({
61417
- capName: entry.capName,
61418
- kind: "native",
61419
- providerAddonId: entry.addonId,
61420
- providerNodeId: toRoutableProviderNodeId(entry.nodeId),
61421
- nativeAddonId: entry.addonId
61422
- });
61423
- seenCaps.add(entry.capName);
61424
- }
61425
- return {
61426
- deviceId: input.deviceId,
61427
- entries
61428
- };
61429
- }
61430
- async function getAllBindings(deps) {
61431
- const store = await deps.ctx.settings.readAddonStore();
61432
- const ids = /* @__PURE__ */ new Set();
61433
- for (const key of Object.keys(store.deviceMeta ?? {})) {
61434
- const id = Number(key);
61435
- if (Number.isInteger(id)) ids.add(id);
61436
- }
61437
- const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
61438
- for (const device of registered) ids.add(device.id);
61439
- if (ids.size === 0) {
61440
- deps.ctx.logger.warn("getAllBindings found no devices \u2014 warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
61441
- return [];
61442
- }
61443
- const out = [];
61444
- for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
61445
- return out;
61446
- }
61447
- async function lookupPersistedStableId(deps, deviceId) {
61448
- return ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.stableId;
61449
- }
61450
61731
  async function getDeviceAggregate(deps, deviceId, kind) {
61451
61732
  const registry = deps.capabilityRegistry;
61452
61733
  if (!registry) {
@@ -62003,7 +62284,7 @@ var require_device_manager_addon = __commonJS({
62003
62284
  if (wireable) caps.push(wireable);
62004
62285
  }
62005
62286
  if (input.includeSynthesizable === true) {
62006
- const deviceType = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.type;
62287
+ const deviceType = (await deps.bindingsDeps.rows.get(deviceId))?.meta.type;
62007
62288
  if (deviceType !== void 0) for (const def of require_dist10.ALL_CAPABILITY_DEFINITIONS) {
62008
62289
  if (seen.has(def.name)) continue;
62009
62290
  if (def.scope !== "device" || def.kind === "wrapper") continue;
@@ -62091,20 +62372,10 @@ var require_device_manager_addon = __commonJS({
62091
62372
  return chain;
62092
62373
  }
62093
62374
  };
62094
- async function stampIntegrationId(metaStore, settings, ctx, deviceId, integrationId) {
62375
+ async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
62095
62376
  await metaStore.withMetaWriteLock(async () => {
62096
- const persisted = await metaStore.resolvePersistedById(deviceId);
62097
- if (!persisted) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
62098
- const { meta: m } = persisted;
62099
- const key = String(deviceId);
62100
- const allMeta = await metaStore.readMeta();
62101
- await settings.writeAddonStore({ deviceMeta: {
62102
- ...allMeta,
62103
- [key]: {
62104
- ...m,
62105
- integrationId
62106
- }
62107
- } });
62377
+ if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
62378
+ await metaStore.rows.patch(deviceId, { integrationId });
62108
62379
  });
62109
62380
  ctx.eventBus.emit({
62110
62381
  id: (0, node_crypto.randomUUID)(),
@@ -62124,70 +62395,54 @@ var require_device_manager_addon = __commonJS({
62124
62395
  async function allocateDeviceId(pctx, input) {
62125
62396
  const { addonId, stableId } = input;
62126
62397
  return await pctx.metaStore.withMetaWriteLock(async () => {
62127
- const meta = await pctx.metaStore.readMeta();
62128
- const existing = Object.values(meta).find((m) => m.addonId === addonId && m.stableId === stableId);
62129
- if (existing) return { id: existing.id };
62398
+ const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
62399
+ if (existing) return { id: existing.meta.id };
62130
62400
  const id = await pctx.metaStore.allocateNextDeviceId();
62131
- await pctx.settings.writeAddonStore({ deviceMeta: {
62132
- ...meta,
62133
- [String(id)]: {
62134
- addonId,
62135
- stableId,
62136
- type: "generic",
62137
- name: stableId,
62138
- location: null,
62139
- disabled: false,
62140
- parentDeviceId: null,
62141
- id
62142
- }
62143
- } });
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
+ });
62144
62414
  return { id };
62145
62415
  });
62146
62416
  }
62147
62417
  async function registerDevice(pctx, input) {
62148
62418
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
62149
- const key = String(id);
62150
62419
  const featuresArr = Array.isArray(features) ? [...features] : [];
62151
62420
  const { isFirstRegistration, exportFingerprint, fingerprintChanged } = await pctx.metaStore.withMetaWriteLock(async () => {
62152
- const index = await pctx.metaStore.readIndex();
62153
- const existing = index[addonId] ?? [];
62154
- const wasInIndex = existing.includes(stableId);
62155
- if (!wasInIndex) await pctx.settings.writeAddonStore({ deviceIndex: {
62156
- ...index,
62157
- [addonId]: [...existing, stableId]
62158
- } });
62159
- const meta = await pctx.metaStore.readMeta();
62160
- const existingMeta = meta[key];
62421
+ const existingRow = await pctx.metaStore.rows.get(id);
62422
+ const existingMeta = existingRow?.meta;
62423
+ const wasRegistered = existingRow?.registered ?? false;
62161
62424
  const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
62162
62425
  const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
62163
- const isFirst = !existingMeta || !wasInIndex;
62426
+ const isFirst = !existingMeta || !wasRegistered;
62164
62427
  const fingerprint = (0, _camstack_types_node.canonicalDeviceFingerprint)({
62165
62428
  deviceId: id,
62166
62429
  deviceType: type,
62167
62430
  features: featuresArr
62168
62431
  });
62169
- await pctx.settings.writeAddonStore({ deviceMeta: {
62170
- ...meta,
62171
- [key]: {
62172
- addonId,
62173
- stableId,
62174
- type,
62175
- name: resolvedName,
62176
- userNamed: wasUserNamed,
62177
- location: existingMeta?.location ?? null,
62178
- disabled: existingMeta?.disabled ?? false,
62179
- ...existingMeta?.integrationId !== void 0 ? { integrationId: existingMeta.integrationId } : {},
62180
- ...existingMeta?.linkDeviceId !== void 0 ? { linkDeviceId: existingMeta.linkDeviceId } : {},
62181
- ...existingMeta?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: existingMeta.primaryChildEntityId } : {},
62182
- ...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
62183
- ...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
62184
- ...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
62185
- parentDeviceId,
62186
- id,
62187
- features: featuresArr,
62188
- exportFingerprint: fingerprint
62189
- }
62190
- } });
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
+ });
62191
62446
  return {
62192
62447
  isFirstRegistration: isFirst,
62193
62448
  exportFingerprint: fingerprint,
@@ -62247,26 +62502,9 @@ var require_device_manager_addon = __commonJS({
62247
62502
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62248
62503
  if (!persisted) return;
62249
62504
  const { addonId, stableId, meta: persistedMeta } = persisted;
62250
- const key = String(deviceId);
62251
62505
  const deviceName = persistedMeta.name;
62252
62506
  await pctx.metaStore.withMetaWriteLock(async () => {
62253
- const index = await pctx.metaStore.readIndex();
62254
- const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
62255
- const updatedIndex = remaining.length > 0 ? {
62256
- ...index,
62257
- [addonId]: remaining
62258
- } : (() => {
62259
- const { [addonId]: _removed, ...rest } = index;
62260
- return rest;
62261
- })();
62262
- await pctx.settings.writeAddonStore({ deviceIndex: updatedIndex });
62263
- const { [key]: _removedMeta, ...restMeta } = await pctx.metaStore.readMeta();
62264
- await pctx.settings.writeAddonStore({ deviceMeta: restMeta });
62265
- const map = await pctx.metaStore.readMetadataMap();
62266
- if (key in map) {
62267
- const { [key]: _removedMetadata, ...restMap } = map;
62268
- await pctx.settings.writeAddonStore({ deviceMetadata: restMap });
62269
- }
62507
+ await pctx.metaStore.rows.remove(deviceId);
62270
62508
  });
62271
62509
  await pctx.settings.clearDeviceStore(deviceId);
62272
62510
  await pctx.settings.clearDeviceRuntimeState(deviceId);
@@ -62310,11 +62548,10 @@ var require_device_manager_addon = __commonJS({
62310
62548
  }
62311
62549
  async function loadMeta(pctx, input) {
62312
62550
  const { deviceId } = input;
62313
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62314
- if (!persisted) return null;
62315
- const { addonId, stableId, meta: m } = persisted;
62316
- const key = String(deviceId);
62317
- 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;
62318
62555
  return {
62319
62556
  id: m.id,
62320
62557
  stableId,
@@ -62337,32 +62574,26 @@ var require_device_manager_addon = __commonJS({
62337
62574
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62338
62575
  if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
62339
62576
  const { meta: m } = persisted;
62340
- const key = String(deviceId);
62341
62577
  const oldName = m.name;
62342
- const allMeta = await pctx.metaStore.readMeta();
62343
- const nextMeta = {
62344
- ...allMeta,
62345
- [key]: {
62346
- ...m,
62347
- name,
62348
- 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
+ });
62349
62595
  }
62350
- };
62351
- if (oldName.length > 0 && oldName !== name) for (const [childKey, childMeta] of Object.entries(allMeta)) {
62352
- if (childKey === key) continue;
62353
- if (!(childMeta.parentDeviceId === deviceId || childMeta.linkDeviceId === deviceId)) continue;
62354
- if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
62355
- const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
62356
- nextMeta[childKey] = {
62357
- ...childMeta,
62358
- name: childName
62359
- };
62360
- cascaded.push({
62361
- id: Number(childKey),
62362
- name: childName
62363
- });
62364
62596
  }
62365
- await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
62366
62597
  });
62367
62598
  pctx.host.ctx.eventBus.emit({
62368
62599
  id: (0, node_crypto.randomUUID)(),
@@ -62396,18 +62627,8 @@ var require_device_manager_addon = __commonJS({
62396
62627
  async function setLocation(pctx, input) {
62397
62628
  const { deviceId, location } = input;
62398
62629
  await pctx.metaStore.withMetaWriteLock(async () => {
62399
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62400
- if (!persisted) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
62401
- const { meta: m } = persisted;
62402
- const key = String(deviceId);
62403
- const allMeta = await pctx.metaStore.readMeta();
62404
- await pctx.settings.writeAddonStore({ deviceMeta: {
62405
- ...allMeta,
62406
- [key]: {
62407
- ...m,
62408
- location
62409
- }
62410
- } });
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 });
62411
62632
  });
62412
62633
  pctx.host.ctx.eventBus.emit({
62413
62634
  id: (0, node_crypto.randomUUID)(),
@@ -62427,18 +62648,8 @@ var require_device_manager_addon = __commonJS({
62427
62648
  async function setType(pctx, input) {
62428
62649
  const { deviceId, type } = input;
62429
62650
  await pctx.metaStore.withMetaWriteLock(async () => {
62430
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62431
- if (!persisted) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
62432
- const { meta: m } = persisted;
62433
- const key = String(deviceId);
62434
- const allMeta = await pctx.metaStore.readMeta();
62435
- await pctx.settings.writeAddonStore({ deviceMeta: {
62436
- ...allMeta,
62437
- [key]: {
62438
- ...m,
62439
- type
62440
- }
62441
- } });
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 });
62442
62653
  });
62443
62654
  pctx.host.ctx.eventBus.emit({
62444
62655
  id: (0, node_crypto.randomUUID)(),
@@ -62461,18 +62672,8 @@ var require_device_manager_addon = __commonJS({
62461
62672
  async function setLinkDeviceId(pctx, input) {
62462
62673
  const { deviceId, linkDeviceId } = input;
62463
62674
  await pctx.metaStore.withMetaWriteLock(async () => {
62464
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62465
- if (!persisted) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
62466
- const { meta: m } = persisted;
62467
- const key = String(deviceId);
62468
- const allMeta = await pctx.metaStore.readMeta();
62469
- await pctx.settings.writeAddonStore({ deviceMeta: {
62470
- ...allMeta,
62471
- [key]: {
62472
- ...m,
62473
- linkDeviceId
62474
- }
62475
- } });
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 });
62476
62677
  });
62477
62678
  pctx.host.ctx.eventBus.emit({
62478
62679
  id: (0, node_crypto.randomUUID)(),
@@ -62492,18 +62693,8 @@ var require_device_manager_addon = __commonJS({
62492
62693
  async function setPrimaryChildEntityId(pctx, input) {
62493
62694
  const { deviceId, primaryChildEntityId } = input;
62494
62695
  await pctx.metaStore.withMetaWriteLock(async () => {
62495
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62496
- if (!persisted) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
62497
- const { meta: m } = persisted;
62498
- const key = String(deviceId);
62499
- const allMeta = await pctx.metaStore.readMeta();
62500
- await pctx.settings.writeAddonStore({ deviceMeta: {
62501
- ...allMeta,
62502
- [key]: {
62503
- ...m,
62504
- primaryChildEntityId
62505
- }
62506
- } });
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 });
62507
62698
  });
62508
62699
  pctx.host.ctx.eventBus.emit({
62509
62700
  id: (0, node_crypto.randomUUID)(),
@@ -62523,18 +62714,8 @@ var require_device_manager_addon = __commonJS({
62523
62714
  async function setChildLayout(pctx, input) {
62524
62715
  const { deviceId, childLayout } = input;
62525
62716
  await pctx.metaStore.withMetaWriteLock(async () => {
62526
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62527
- if (!persisted) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
62528
- const { meta: m } = persisted;
62529
- const key = String(deviceId);
62530
- const allMeta = await pctx.metaStore.readMeta();
62531
- await pctx.settings.writeAddonStore({ deviceMeta: {
62532
- ...allMeta,
62533
- [key]: {
62534
- ...m,
62535
- childLayout
62536
- }
62537
- } });
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 });
62538
62719
  });
62539
62720
  pctx.host.ctx.eventBus.emit({
62540
62721
  id: (0, node_crypto.randomUUID)(),
@@ -62554,18 +62735,8 @@ var require_device_manager_addon = __commonJS({
62554
62735
  async function setRole(pctx, input) {
62555
62736
  const { deviceId, role } = input;
62556
62737
  await pctx.metaStore.withMetaWriteLock(async () => {
62557
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62558
- if (!persisted) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
62559
- const { meta: m } = persisted;
62560
- const key = String(deviceId);
62561
- const allMeta = await pctx.metaStore.readMeta();
62562
- await pctx.settings.writeAddonStore({ deviceMeta: {
62563
- ...allMeta,
62564
- [key]: {
62565
- ...m,
62566
- role
62567
- }
62568
- } });
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 });
62569
62740
  });
62570
62741
  pctx.host.ctx.eventBus.emit({
62571
62742
  id: (0, node_crypto.randomUUID)(),
@@ -62596,19 +62767,8 @@ var require_device_manager_addon = __commonJS({
62596
62767
  const { deviceId, display } = input;
62597
62768
  const normalized = display === null ? null : normalizeDisplayOverride(display);
62598
62769
  await pctx.metaStore.withMetaWriteLock(async () => {
62599
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62600
- if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
62601
- const { meta: m } = persisted;
62602
- const key = String(deviceId);
62603
- const allMeta = await pctx.metaStore.readMeta();
62604
- const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
62605
- ...m,
62606
- display: normalized
62607
- };
62608
- await pctx.settings.writeAddonStore({ deviceMeta: {
62609
- ...allMeta,
62610
- [key]: nextRow
62611
- } });
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 });
62612
62772
  });
62613
62773
  pctx.host.ctx.eventBus.emit({
62614
62774
  id: (0, node_crypto.randomUUID)(),
@@ -62638,24 +62798,15 @@ var require_device_manager_addon = __commonJS({
62638
62798
  async function applyInitialMeta(pctx, input) {
62639
62799
  const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
62640
62800
  await pctx.metaStore.withMetaWriteLock(async () => {
62641
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62642
- if (!persisted) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
62643
- const { meta: m } = persisted;
62644
- const key = String(deviceId);
62645
- const allMeta = await pctx.metaStore.readMeta();
62646
- const merged = {
62647
- ...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, {
62648
62803
  ...name !== void 0 ? { name } : {},
62649
62804
  ...location !== void 0 ? { location } : {},
62650
62805
  ...type !== void 0 ? { type } : {},
62651
62806
  ...integrationId !== void 0 ? { integrationId } : {},
62652
62807
  ...linkDeviceId !== void 0 ? { linkDeviceId } : {},
62653
62808
  ...role !== void 0 ? { role } : {}
62654
- };
62655
- await pctx.settings.writeAddonStore({ deviceMeta: {
62656
- ...allMeta,
62657
- [key]: merged
62658
- } });
62809
+ });
62659
62810
  });
62660
62811
  const emitMetaChanged = (field, value) => {
62661
62812
  pctx.host.ctx.eventBus.emit({
@@ -62683,10 +62834,9 @@ var require_device_manager_addon = __commonJS({
62683
62834
  async function setMetadata(pctx, input) {
62684
62835
  const { deviceId, patch } = input;
62685
62836
  const result = await pctx.metaStore.withMetaWriteLock(async () => {
62686
- if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
62687
- const key = String(deviceId);
62688
- const map = await pctx.metaStore.readMetadataMap();
62689
- 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 ?? {} };
62690
62840
  let changed = false;
62691
62841
  for (const [k, v] of Object.entries(patch)) if (v === null) {
62692
62842
  if (k in next) {
@@ -62699,10 +62849,7 @@ var require_device_manager_addon = __commonJS({
62699
62849
  }
62700
62850
  if (!changed) return { changed: false };
62701
62851
  const hasFields = Object.keys(next).length > 0;
62702
- const updatedMap = { ...map };
62703
- if (hasFields) updatedMap[key] = next;
62704
- else delete updatedMap[key];
62705
- await pctx.settings.writeAddonStore({ deviceMetadata: updatedMap });
62852
+ await pctx.metaStore.rows.patch(deviceId, { metadata: hasFields ? next : null });
62706
62853
  return {
62707
62854
  changed: true,
62708
62855
  finalMeta: hasFields ? next : null
@@ -62730,15 +62877,7 @@ var require_device_manager_addon = __commonJS({
62730
62877
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62731
62878
  if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
62732
62879
  const { meta: m } = persisted;
62733
- const key = String(deviceId);
62734
- const allMeta = await pctx.metaStore.readMeta();
62735
- await pctx.settings.writeAddonStore({ deviceMeta: {
62736
- ...allMeta,
62737
- [key]: {
62738
- ...m,
62739
- disabled
62740
- }
62741
- } });
62880
+ await pctx.metaStore.rows.patch(deviceId, { disabled });
62742
62881
  return {
62743
62882
  changed: (m.disabled ?? false) !== disabled,
62744
62883
  integrationId: m.integrationId ?? ""
@@ -62781,9 +62920,7 @@ var require_device_manager_addon = __commonJS({
62781
62920
  return data;
62782
62921
  }
62783
62922
  async function listLocations(pctx) {
62784
- const store = await pctx.settings.readAddonStore();
62785
- const meta = store.deviceMeta ?? {};
62786
- const locations = store.locations ?? [];
62923
+ const locations = (await pctx.metaStore.readStore()).locations ?? [];
62787
62924
  const seen = /* @__PURE__ */ new Map();
62788
62925
  const consider = (raw) => {
62789
62926
  if (typeof raw !== "string") return;
@@ -62793,38 +62930,34 @@ var require_device_manager_addon = __commonJS({
62793
62930
  if (!seen.has(key)) seen.set(key, trimmed);
62794
62931
  };
62795
62932
  for (const label of locations) consider(label);
62796
- for (const m of Object.values(meta)) consider(m.location);
62933
+ for (const row of await pctx.metaStore.rows.listAll()) consider(row.meta.location);
62797
62934
  return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
62798
62935
  }
62799
62936
  async function addLocation(pctx, input) {
62800
62937
  const trimmed = input.name.trim();
62801
62938
  if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
62802
- const current = (await pctx.settings.readAddonStore()).locations ?? [];
62939
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62803
62940
  if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
62804
62941
  await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
62805
62942
  }
62806
62943
  async function removeLocation(pctx, input) {
62807
62944
  const trimmed = input.name.trim();
62808
62945
  if (trimmed.length === 0) return;
62809
- const store = await pctx.settings.readAddonStore();
62810
- const current = store.locations ?? [];
62946
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62811
62947
  const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
62812
62948
  if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
62813
62949
  if (input.cascade !== true) return;
62814
- const meta = store.deviceMeta ?? {};
62815
- const updates = { ...meta };
62816
- const cleared = [];
62817
- for (const [key, m] of Object.entries(meta)) {
62818
- if (typeof m.location !== "string") continue;
62819
- if (m.location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
62820
- updates[key] = {
62821
- ...m,
62822
- location: null
62823
- };
62824
- cleared.push(m.id);
62825
- }
62826
- if (cleared.length === 0) return;
62827
- 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
+ });
62828
62961
  for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
62829
62962
  id: (0, node_crypto.randomUUID)(),
62830
62963
  timestamp: /* @__PURE__ */ new Date(),
@@ -62840,41 +62973,48 @@ var require_device_manager_addon = __commonJS({
62840
62973
  }
62841
62974
  });
62842
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
+ }
62843
62989
  var DeviceMetaStore = class {
62844
62990
  settings;
62845
62991
  registry;
62992
+ rows;
62846
62993
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
62847
- * 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
62848
62995
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
62849
62996
  * ownership without awaiting. Kept in sync with every register/remove and
62850
62997
  * warmed from persistence on boot. */
62851
62998
  idToAddonId = /* @__PURE__ */ new Map();
62852
- /** Serialises every read-modify-write of the deviceMeta / deviceIndex blob
62853
- * through one promise chain (see `withMetaWriteLock`). Per-instance state
62854
- * 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. */
62855
63001
  metaWriteChain = Promise.resolve();
62856
- constructor(settings, registry) {
63002
+ constructor(settings, registry, rows) {
62857
63003
  this.settings = settings;
62858
63004
  this.registry = registry;
63005
+ this.rows = rows;
62859
63006
  }
62860
63007
  /** The read currently in flight, or null. Never a settled value — see
62861
63008
  * {@link readStore}. */
62862
63009
  inFlightRead = null;
62863
63010
  /**
62864
- * The whole persisted addon store.
63011
+ * The addon's own settings row set — `nextDeviceId`, `roleDisplayDefaults`,
63012
+ * `locations`. NOT the fleet: no device has lived here since the flatten.
62865
63013
  *
62866
63014
  * **Concurrent callers join the read already in flight.** This is not a
62867
63015
  * cache and nothing survives settlement: a caller that awaited the running
62868
63016
  * promise could not have observed anything older than its result, so the
62869
- * only thing that changes is cost. What that cost was, measured on the
62870
- * 2026-08-19 boot: `readAddonStore` lands in `SqliteSettingsBackend
62871
- * .getAllAddon`, which reads and `JSON.parse`s this addon's rows —
62872
- * 625 KB on the live hub (`deviceMeta` 467 KB + `deviceMetadata` 87 KB +
62873
- * `deviceIndex` 70 KB) — synchronously, on the hub's event loop. A V8
62874
- * profile of that boot had hub-main's JS thread 99.9% busy with 64% of it
62875
- * inside `getAllAddon`, ~89% of that entered here, and every runner's first
62876
- * store read queued behind it (the notification centre's six parallel reads
62877
- * all resolved together at t+44.7 s).
63017
+ * only thing that changes is cost.
62878
63018
  *
62879
63019
  * A rejection is NOT latched: the slot is cleared before the promise
62880
63020
  * settles either way, so a failed read costs the joiners that one failure
@@ -62885,7 +63025,7 @@ var require_device_manager_addon = __commonJS({
62885
63025
  if (existing !== null) return existing;
62886
63026
  const read = (async () => {
62887
63027
  try {
62888
- return await this.settings.readAddonStore();
63028
+ return decodeAddonStore(await this.settings.readAddonStore());
62889
63029
  } finally {
62890
63030
  this.inFlightRead = null;
62891
63031
  }
@@ -62893,41 +63033,6 @@ var require_device_manager_addon = __commonJS({
62893
63033
  this.inFlightRead = read;
62894
63034
  return read;
62895
63035
  };
62896
- /**
62897
- * The three fleet projections from ONE read.
62898
- *
62899
- * `listAll` asked for `deviceMeta`, then `deviceMetadata`, then
62900
- * `deviceIndex` — three SEQUENTIAL awaits, which {@link readStore}'s
62901
- * in-flight join cannot collapse because each starts after the previous one
62902
- * settled. Three full 625 KB parses per call, on a call made once per device
62903
- * lifecycle event during a 974-device boot: 23% of hub-main's CPU.
62904
- *
62905
- * It is also ONE snapshot. Three separate reads could straddle a write and
62906
- * hand back an index that names a device the meta map no longer has.
62907
- */
62908
- readAll = async () => {
62909
- const store = await this.readStore();
62910
- return {
62911
- index: store.deviceIndex ?? {},
62912
- meta: store.deviceMeta ?? {},
62913
- metadata: store.deviceMetadata ?? {}
62914
- };
62915
- };
62916
- readIndex = async () => {
62917
- return (await this.readStore()).deviceIndex ?? {};
62918
- };
62919
- readMeta = async () => {
62920
- return (await this.readStore()).deviceMeta ?? {};
62921
- };
62922
- /** Hardware-identity metadata map. Lives in a sibling key on the
62923
- * device-manager addon store so its writers (`setMetadata`) never
62924
- * collide with the lifecycle writers on `deviceMeta`
62925
- * (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
62926
- * Single-writer per row eliminates the "writer X clobbers writer
62927
- * Y's field" bug class — `setMetadata` is the only producer. */
62928
- readMetadataMap = async () => {
62929
- return (await this.readStore()).deviceMetadata ?? {};
62930
- };
62931
63036
  withMetaWriteLock = async (fn) => {
62932
63037
  const previous = this.metaWriteChain;
62933
63038
  let release = () => {
@@ -62944,31 +63049,36 @@ var require_device_manager_addon = __commonJS({
62944
63049
  release();
62945
63050
  }
62946
63051
  };
63052
+ /** The whole persisted row for one device, or `null`. */
63053
+ getRow = async (deviceId) => this.rows.get(deviceId);
62947
63054
  /**
62948
63055
  * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
62949
- * Scans persisted meta — live IDevice lookup (hub registry) is handled
63056
+ * Reads the device's own row — live IDevice lookup (hub registry) is handled
62950
63057
  * separately per call site so callers can decide whether to route to
62951
63058
  * an in-process driver or to the cross-process `device-ops` bridge.
62952
63059
  * Returns null when no device with that id is known to the hub.
62953
63060
  */
62954
63061
  resolvePersistedById = async (deviceId) => {
62955
- const m = (await this.readMeta())[String(deviceId)];
62956
- if (!m) return null;
63062
+ const row = await this.rows.get(deviceId);
63063
+ if (row === null) return null;
62957
63064
  return {
62958
- addonId: m.addonId,
62959
- stableId: m.stableId,
62960
- meta: m
63065
+ addonId: row.meta.addonId,
63066
+ stableId: row.meta.stableId,
63067
+ meta: row.meta
62961
63068
  };
62962
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
+ };
62963
63074
  /** Direct children of a device: the union of the live registry's children
62964
- * and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
63075
+ * and the persisted rows whose `parentDeviceId` is `parentId`, deduplicated
62965
63076
  * and excluding self. Shared by the `remove` cascade and the `resetToSource`
62966
63077
  * resync purge (#19). */
62967
63078
  directChildIds = async (parentId) => {
62968
63079
  const ids = /* @__PURE__ */ new Set();
62969
63080
  if (this.registry) for (const c of this.registry.getChildren(parentId)) ids.add(c.id);
62970
- const meta = await this.readMeta();
62971
- 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);
62972
63082
  ids.delete(parentId);
62973
63083
  return [...ids];
62974
63084
  };
@@ -62978,6 +63088,476 @@ var require_device_manager_addon = __commonJS({
62978
63088
  return current;
62979
63089
  };
62980
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
+ };
62981
63561
  function persistableBlob(blob, policyFor) {
62982
63562
  const out = {};
62983
63563
  for (const [capName, slice] of Object.entries(blob)) {
@@ -63456,12 +64036,21 @@ var require_device_manager_addon = __commonJS({
63456
64036
  }
63457
64037
  }))).filter((id) => id !== null);
63458
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;
63459
64045
  /** Build the dependency context the extracted binding resolvers consume. */
63460
64046
  get bindingsDeps() {
64047
+ const rows = this.deviceRows;
64048
+ if (rows === null) throw new Error("[device-manager] device row store not initialized");
63461
64049
  return {
63462
64050
  ctx: this.ctx,
63463
64051
  capabilityRegistry: this.capabilityRegistry,
63464
- remoteNativeCaps: this.remoteNativeCaps
64052
+ remoteNativeCaps: this.remoteNativeCaps,
64053
+ rows
63465
64054
  };
63466
64055
  }
63467
64056
  async getBindings(input) {
@@ -63517,17 +64106,23 @@ var require_device_manager_addon = __commonJS({
63517
64106
  if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
63518
64107
  return ops;
63519
64108
  };
63520
- 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);
63521
64120
  this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
63522
64121
  const stateMirror = this.stateMirrorImpl;
63523
- const readMeta = metaStore.readMeta;
63524
64122
  const resolvePersistedById = metaStore.resolvePersistedById;
63525
64123
  const idToAddonId = metaStore.idToAddonId;
63526
- {
63527
- const meta = await readMeta();
63528
- for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
63529
- }
63530
- 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);
63531
64126
  const pctx = {
63532
64127
  host: this.providerHost,
63533
64128
  metaStore,
@@ -63770,7 +64365,7 @@ var require_hub_forwarder = __commonJS({
63770
64365
  [Symbol.toStringTag]: { value: "Module" }
63771
64366
  });
63772
64367
  require_chunk_Cek0wNdY();
63773
- var require_dist10 = require_dist_Ctfhlrtl();
64368
+ var require_dist10 = require_dist_B2CtwVW2();
63774
64369
  var require_formatter = require_formatter_DqAKDlvN();
63775
64370
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
63776
64371
  var HubForwarderDestination = class {
@@ -63907,7 +64502,7 @@ var require_liveness_monitor_addon = __commonJS({
63907
64502
  "use strict";
63908
64503
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
63909
64504
  require_chunk_Cek0wNdY();
63910
- var require_dist10 = require_dist_Ctfhlrtl();
64505
+ var require_dist10 = require_dist_B2CtwVW2();
63911
64506
  var NO_DEVICES = "liveness:no-devices";
63912
64507
  var ALL_OFFLINE = "liveness:all-devices-offline";
63913
64508
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -64097,7 +64692,7 @@ var require_local_auth_addon = __commonJS({
64097
64692
  [Symbol.toStringTag]: { value: "Module" }
64098
64693
  });
64099
64694
  var require_chunk = require_chunk_Cek0wNdY();
64100
- var require_dist10 = require_dist_Ctfhlrtl();
64695
+ var require_dist10 = require_dist_B2CtwVW2();
64101
64696
  var node_crypto = __require("crypto");
64102
64697
  node_crypto = require_chunk.__toESM(node_crypto);
64103
64698
  var crypto$1 = __require("crypto");
@@ -71781,7 +72376,7 @@ var require_loki_logging = __commonJS({
71781
72376
  [Symbol.toStringTag]: { value: "Module" }
71782
72377
  });
71783
72378
  require_chunk_Cek0wNdY();
71784
- var require_dist10 = require_dist_Ctfhlrtl();
72379
+ var require_dist10 = require_dist_B2CtwVW2();
71785
72380
  function sanitizeLabelName(raw) {
71786
72381
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
71787
72382
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -72346,7 +72941,7 @@ var require_native_metrics_addon = __commonJS({
72346
72941
  [Symbol.toStringTag]: { value: "Module" }
72347
72942
  });
72348
72943
  var require_chunk = require_chunk_Cek0wNdY();
72349
- var require_dist10 = require_dist_Ctfhlrtl();
72944
+ var require_dist10 = require_dist_B2CtwVW2();
72350
72945
  var node_child_process = __require("child_process");
72351
72946
  var node_util = __require("util");
72352
72947
  var node_os = __require("os");
@@ -73288,7 +73883,7 @@ var require_filesystem_storage_addon = __commonJS({
73288
73883
  [Symbol.toStringTag]: { value: "Module" }
73289
73884
  });
73290
73885
  var require_chunk = require_chunk_Cek0wNdY();
73291
- var require_dist10 = require_dist_Ctfhlrtl();
73886
+ var require_dist10 = require_dist_B2CtwVW2();
73292
73887
  var node_crypto = __require("crypto");
73293
73888
  var node_fs_promises = __require("fs/promises");
73294
73889
  var node_path = __require("path");
@@ -74404,7 +74999,8 @@ var require_sqlite_settings_addon = __commonJS({
74404
74999
  [Symbol.toStringTag]: { value: "Module" }
74405
75000
  });
74406
75001
  var require_chunk = require_chunk_Cek0wNdY();
74407
- var require_dist10 = require_dist_Ctfhlrtl();
75002
+ var require_dist10 = require_dist_B2CtwVW2();
75003
+ var require_retired_settings_keys = require_retired_settings_keys_CYDGGkyM();
74408
75004
  var node_crypto = __require("crypto");
74409
75005
  var node_fs = __require("fs");
74410
75006
  var node_module = __require("module");
@@ -74413,123 +75009,47 @@ var require_sqlite_settings_addon = __commonJS({
74413
75009
  var SQLITE_PAGE_CACHE_KIB = -65536;
74414
75010
  var SQLITE_MMAP_SIZE_BYTES = 268435456;
74415
75011
  var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
74416
- function settingsStoreIsAuthoritativeHere(env) {
74417
- const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
74418
- return raw === "" || raw === "hub";
74419
- }
74420
- var RETIRED_SETTINGS_KEYS = [{
74421
- namespace: "detection-pipeline",
74422
- collection: "addon-settings",
74423
- row: "root",
74424
- keys: ["pipelineSteps", "pipelineEngine"],
74425
- perNodeKeys: [
74426
- "engineRuntime",
74427
- "engineBackend",
74428
- "engineDevice",
74429
- "probedBestEngine"
74430
- ],
74431
- 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)"
74432
75016
  }, {
74433
- namespace: "device-manager",
74434
- collection: "addon-settings",
74435
- row: "deviceMeta",
74436
- keys: [],
74437
- perNodeKeys: [],
74438
- mapValueKeys: ["deviceLinks"],
74439
- 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`"
74440
75020
  }];
74441
- function retiredKeyStoreOf(backend) {
74442
- return {
74443
- async read(spec) {
74444
- return require_dist10.asJsonObject(await backend.get({
74445
- namespace: spec.namespace,
74446
- collection: spec.collection,
74447
- key: spec.row
74448
- }));
74449
- },
74450
- async write(spec, value) {
74451
- await backend.set({
74452
- namespace: spec.namespace,
74453
- collection: spec.collection,
74454
- key: spec.row,
74455
- value
74456
- });
74457
- }
74458
- };
74459
- }
74460
- function planNestedRetiredKeyPurge(blob, spec) {
74461
- const nestedKeys = spec.mapValueKeys ?? [];
74462
- if (nestedKeys.length === 0) return [];
74463
- const found = [];
74464
- for (const [mapKey, value] of Object.entries(blob)) {
74465
- const inner = require_dist10.asJsonObject(value);
74466
- if (inner === null) continue;
74467
- for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
74468
- }
74469
- return found;
74470
- }
74471
- function planRetiredKeyPurge(blob, spec) {
74472
- const exact = new Set(spec.keys);
74473
- const found = [];
74474
- for (const key of Object.keys(blob)) {
74475
- if (exact.has(key)) {
74476
- found.push(key);
74477
- continue;
74478
- }
74479
- for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
74480
- found.push(key);
74481
- break;
74482
- }
74483
- }
74484
- return found;
75021
+ function retiredCollectionStoreOf(backend) {
75022
+ return { async drop(collection) {
75023
+ return backend.dropRetiredTable(collection);
75024
+ } };
74485
75025
  }
74486
- async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
74487
- if (!settingsStoreIsAuthoritativeHere(env)) return [];
75026
+ async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
74488
75027
  const results = [];
74489
75028
  for (const spec of specs) {
74490
- let blob;
75029
+ let dropped;
74491
75030
  try {
74492
- blob = await store.read(spec);
75031
+ dropped = await store.drop(spec.collection);
74493
75032
  } catch (err) {
74494
- logger.warn("retired settings keys \u2014 read failed, row left untouched", { meta: {
74495
- addon: spec.namespace,
75033
+ logger.warn("retired collection \u2014 drop failed, table left in place", { meta: {
75034
+ addon: spec.owner,
74496
75035
  collection: spec.collection,
74497
- row: spec.row,
74498
75036
  error: err instanceof Error ? err.message : String(err)
74499
75037
  } });
74500
75038
  continue;
74501
75039
  }
74502
- if (blob === null) continue;
74503
- const doomed = planRetiredKeyPurge(blob, spec);
74504
- const doomedNested = planNestedRetiredKeyPurge(blob, spec);
74505
- if (doomed.length === 0 && doomedNested.length === 0) continue;
74506
- const doomedSet = new Set(doomed);
74507
- const nestedByMapKey = /* @__PURE__ */ new Map();
74508
- for (const path of doomedNested) {
74509
- const cut = path.indexOf(".");
74510
- const mapKey = path.slice(0, cut);
74511
- const inner = path.slice(cut + 1);
74512
- const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
74513
- bucket.add(inner);
74514
- nestedByMapKey.set(mapKey, bucket);
74515
- }
74516
- const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
74517
- const retired = nestedByMapKey.get(key);
74518
- if (retired === void 0) return [key, value];
74519
- const inner = require_dist10.asJsonObject(value);
74520
- if (inner === null) return [key, value];
74521
- return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
74522
- }));
74523
- await store.write(spec, next);
75040
+ if (dropped === null) continue;
74524
75041
  const result = {
74525
- addon: spec.namespace,
74526
75042
  collection: spec.collection,
74527
- row: spec.row,
74528
- keys: doomed,
74529
- nested: doomedNested
75043
+ owner: spec.owner,
75044
+ rows: dropped.rows
74530
75045
  };
74531
75046
  results.push(result);
74532
- 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: {
74533
75053
  ...result,
74534
75054
  reason: spec.reason
74535
75055
  } });
@@ -74623,6 +75143,9 @@ var require_sqlite_settings_addon = __commonJS({
74623
75143
  function isImposedBound(source) {
74624
75144
  return source !== "caller";
74625
75145
  }
75146
+ function isCountRow(raw) {
75147
+ return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
75148
+ }
74626
75149
  function parseRowData(raw) {
74627
75150
  return require_dist10.asJsonObject(require_dist10.parseJsonUnknown(raw)) ?? {};
74628
75151
  }
@@ -75307,6 +75830,31 @@ var require_sqlite_settings_addon = __commonJS({
75307
75830
  }
75308
75831
  this.structuredTables.set(table, fingerprint);
75309
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
+ }
75310
75858
  async tableInsert(table, row) {
75311
75859
  const keys = Object.keys(row);
75312
75860
  const cols = keys.map((k) => `"${k}"`).join(", ");
@@ -75817,10 +76365,11 @@ var require_sqlite_settings_addon = __commonJS({
75817
76365
  this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist10.RUNTIME_DEFAULTS }, this.ctx.logger.child("Query"));
75818
76366
  await this.backend.initialize();
75819
76367
  try {
75820
- 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"));
75821
76369
  } catch (err) {
75822
76370
  this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist10.errMsg(err) } });
75823
76371
  }
76372
+ await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
75824
76373
  const vectorIndex = this.createVectorIndex(this.backend);
75825
76374
  await vectorIndex.loadIndexRegistry();
75826
76375
  this.vectorIndex = vectorIndex;
@@ -75995,7 +76544,7 @@ var require_storage_orchestrator_addon = __commonJS({
75995
76544
  [Symbol.toStringTag]: { value: "Module" }
75996
76545
  });
75997
76546
  var require_chunk = require_chunk_Cek0wNdY();
75998
- var require_dist10 = require_dist_Ctfhlrtl();
76547
+ var require_dist10 = require_dist_B2CtwVW2();
75999
76548
  var node_crypto = __require("crypto");
76000
76549
  var node_fs_promises = __require("fs/promises");
76001
76550
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -77874,7 +78423,7 @@ var require_system_config_addon = __commonJS({
77874
78423
  [Symbol.toStringTag]: { value: "Module" }
77875
78424
  });
77876
78425
  require_chunk_Cek0wNdY();
77877
- var require_dist10 = require_dist_Ctfhlrtl();
78426
+ var require_dist10 = require_dist_B2CtwVW2();
77878
78427
  var SECTION_TITLES = {
77879
78428
  server: "Server",
77880
78429
  auth: "Authentication"
@@ -95935,7 +96484,7 @@ var require_winston_logging = __commonJS({
95935
96484
  [Symbol.toStringTag]: { value: "Module" }
95936
96485
  });
95937
96486
  var require_chunk = require_chunk_Cek0wNdY();
95938
- var require_dist10 = require_dist_Ctfhlrtl();
96487
+ var require_dist10 = require_dist_B2CtwVW2();
95939
96488
  var require_formatter = require_formatter_DqAKDlvN();
95940
96489
  var node_path = __require("path");
95941
96490
  node_path = require_chunk.__toESM(node_path);
@@ -107405,9 +107954,9 @@ var require_dist2 = __commonJS({
107405
107954
  }
107406
107955
  });
107407
107956
 
107408
- // ../system/dist/manifest-python-deps-GejnH--L.js
107409
- var require_manifest_python_deps_GejnH_L = __commonJS({
107410
- "../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) {
107411
107960
  "use strict";
107412
107961
  var require_chunk = require_chunk_Cek0wNdY();
107413
107962
  var node_crypto = __require("crypto");
@@ -111384,6 +111933,8 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111384
111933
  };
111385
111934
  goneHandler = () => {
111386
111935
  };
111936
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
111937
+ nextIncarnation = 1;
111387
111938
  eventHandler = null;
111388
111939
  logHandler = null;
111389
111940
  readinessHandler = null;
@@ -111658,14 +112209,20 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111658
112209
  listChildren() {
111659
112210
  return [...this.children.values()].map((e) => ({
111660
112211
  childId: e.childId,
111661
- caps: e.caps
112212
+ caps: e.caps,
112213
+ incarnation: e.incarnation
111662
112214
  }));
111663
112215
  }
111664
112216
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
111665
112217
  onChildRegistered(handler) {
111666
112218
  this.registeredHandler = handler;
111667
112219
  }
111668
- /** 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
+ */
111669
112226
  onChildGone(handler) {
111670
112227
  this.goneHandler = handler;
111671
112228
  }
@@ -111750,6 +112307,7 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111750
112307
  }
111751
112308
  onConnection(channel) {
111752
112309
  let childId = null;
112310
+ const incarnation = this.nextIncarnation++;
111753
112311
  channel.onEvent((body) => {
111754
112312
  const msg = body;
111755
112313
  if (msg.kind === "event") {
@@ -111778,16 +112336,24 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111778
112336
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
111779
112337
  childId = msg.childId;
111780
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
+ });
111781
112345
  this.children.set(msg.childId, {
111782
112346
  childId: msg.childId,
111783
112347
  channel,
111784
112348
  caps: msg.caps,
111785
- eventPatterns
112349
+ eventPatterns,
112350
+ incarnation
111786
112351
  });
111787
112352
  this.logPatternSet(msg.childId, eventPatterns);
111788
112353
  this.registeredHandler({
111789
112354
  childId: msg.childId,
111790
- caps: msg.caps
112355
+ caps: msg.caps,
112356
+ incarnation
111791
112357
  });
111792
112358
  return { ok: true };
111793
112359
  }
@@ -111822,11 +112388,21 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111822
112388
  throw new Error(`unknown child request kind: ${msg.kind}`);
111823
112389
  });
111824
112390
  channel.onClose(() => {
111825
- if (childId !== null && this.children.delete(childId)) {
111826
- this.childEventStats.delete(childId);
111827
- this.loggedPatternSet.delete(childId);
111828
- 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;
111829
112401
  }
112402
+ this.children.delete(childId);
112403
+ this.childEventStats.delete(childId);
112404
+ this.loggedPatternSet.delete(childId);
112405
+ this.goneHandler(childId, incarnation);
111830
112406
  });
111831
112407
  }
111832
112408
  };
@@ -118184,7 +118760,7 @@ var require_dist3 = __commonJS({
118184
118760
  "use strict";
118185
118761
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
118186
118762
  var require_chunk = require_chunk_Cek0wNdY();
118187
- var require_dist10 = require_dist_Ctfhlrtl();
118763
+ var require_dist10 = require_dist_B2CtwVW2();
118188
118764
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
118189
118765
  require_alerts();
118190
118766
  var require_formatter = require_formatter_DqAKDlvN();
@@ -118209,7 +118785,7 @@ var require_dist3 = __commonJS({
118209
118785
  require_system_config();
118210
118786
  var require_builtins_winston_logging_index = require_winston_logging();
118211
118787
  var require_file_data_plane = require_file_data_plane_DUHPHa_Y();
118212
- var require_manifest_python_deps = require_manifest_python_deps_GejnH_L();
118788
+ var require_manifest_python_deps = require_manifest_python_deps_MA_BZGBz();
118213
118789
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
118214
118790
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
118215
118791
  var zod = require_zod();
@@ -120105,13 +120681,9 @@ var require_dist3 = __commonJS({
120105
120681
  return {};
120106
120682
  }
120107
120683
  var integrationCounter = 0;
120108
- var deviceCounter = 0;
120109
120684
  function nextIntegrationId() {
120110
120685
  return `int_${String(++integrationCounter).padStart(4, "0")}`;
120111
120686
  }
120112
- function nextDeviceId() {
120113
- return `dev_${String(++deviceCounter).padStart(4, "0")}`;
120114
- }
120115
120687
  var INTEGRATIONS_COLUMNS = [
120116
120688
  {
120117
120689
  name: "id",
@@ -120151,102 +120723,13 @@ var require_dist3 = __commonJS({
120151
120723
  notNull: true
120152
120724
  }
120153
120725
  ];
120154
- var INTEGRATIONS_INDEXES = [{
120155
- name: "idx_integrations_addon",
120156
- columns: ["addon_id"]
120157
- }];
120158
- var INTEGRATION_SETTINGS_COLUMNS = [
120159
- {
120160
- name: "integration_id",
120161
- type: "TEXT",
120162
- notNull: true
120163
- },
120164
- {
120165
- name: "key",
120166
- type: "TEXT",
120167
- notNull: true
120168
- },
120169
- {
120170
- name: "value",
120171
- type: "TEXT",
120172
- notNull: true
120173
- },
120174
- {
120175
- name: "value_type",
120176
- type: "TEXT",
120177
- notNull: true,
120178
- defaultValue: "string"
120179
- }
120180
- ];
120181
- var DEVICES_COLUMNS = [
120182
- {
120183
- name: "id",
120184
- type: "TEXT",
120185
- primaryKey: true
120186
- },
120187
- {
120188
- name: "integration_id",
120189
- type: "TEXT",
120190
- notNull: true
120191
- },
120192
- {
120193
- name: "stable_id",
120194
- type: "TEXT",
120195
- notNull: true,
120196
- unique: true
120197
- },
120198
- {
120199
- name: "type",
120200
- type: "TEXT",
120201
- notNull: true,
120202
- defaultValue: "camera"
120203
- },
120204
- {
120205
- name: "name",
120206
- type: "TEXT",
120207
- notNull: true
120208
- },
120209
- {
120210
- name: "enabled",
120211
- type: "INTEGER",
120212
- notNull: true,
120213
- defaultValue: 1
120214
- },
120215
- {
120216
- name: "info",
120217
- type: "JSON",
120218
- notNull: true,
120219
- defaultValue: "{}"
120220
- },
120221
- {
120222
- name: "created_at",
120223
- type: "INTEGER",
120224
- notNull: true
120225
- },
120226
- {
120227
- name: "updated_at",
120228
- type: "INTEGER",
120229
- notNull: true
120230
- }
120231
- ];
120232
- var DEVICES_INDEXES = [
120233
- {
120234
- name: "idx_devices_integration",
120235
- columns: ["integration_id"]
120236
- },
120237
- {
120238
- name: "idx_devices_stable",
120239
- columns: ["stable_id"],
120240
- unique: true
120241
- },
120242
- {
120243
- name: "idx_devices_type",
120244
- columns: ["type"]
120245
- }
120246
- ];
120247
- var DEVICE_SETTINGS_COLUMNS = [
120726
+ var INTEGRATIONS_INDEXES = [{
120727
+ name: "idx_integrations_addon",
120728
+ columns: ["addon_id"]
120729
+ }];
120730
+ var INTEGRATION_SETTINGS_COLUMNS = [
120248
120731
  {
120249
- name: "device_id",
120732
+ name: "integration_id",
120250
120733
  type: "TEXT",
120251
120734
  notNull: true
120252
120735
  },
@@ -120269,8 +120752,6 @@ var require_dist3 = __commonJS({
120269
120752
  ];
120270
120753
  var INTEGRATIONS = "integrations";
120271
120754
  var INTEGRATION_SETTINGS = "integration_settings";
120272
- var DEVICES = "devices";
120273
- var DEVICE_SETTINGS = "device_settings_kv";
120274
120755
  var IntegrationRegistry = class IntegrationRegistry2 {
120275
120756
  store;
120276
120757
  constructor(backend) {
@@ -120286,15 +120767,6 @@ var require_dist3 = __commonJS({
120286
120767
  collection: INTEGRATION_SETTINGS,
120287
120768
  columns: INTEGRATION_SETTINGS_COLUMNS
120288
120769
  });
120289
- await this.store.declareCollection({
120290
- collection: DEVICES,
120291
- columns: DEVICES_COLUMNS,
120292
- indexes: DEVICES_INDEXES
120293
- });
120294
- await this.store.declareCollection({
120295
- collection: DEVICE_SETTINGS,
120296
- columns: DEVICE_SETTINGS_COLUMNS
120297
- });
120298
120770
  const ints = await this.store.query({
120299
120771
  collection: INTEGRATIONS,
120300
120772
  filter: {
@@ -120309,20 +120781,6 @@ var require_dist3 = __commonJS({
120309
120781
  const num = parseInt(ints[0].id.replace("int_", ""), 10);
120310
120782
  if (!isNaN(num)) integrationCounter = num;
120311
120783
  }
120312
- const devs = await this.store.query({
120313
- collection: DEVICES,
120314
- filter: {
120315
- orderBy: {
120316
- field: "id",
120317
- direction: "desc"
120318
- },
120319
- limit: 1
120320
- }
120321
- });
120322
- if (devs[0]) {
120323
- const num = parseInt(devs[0].id.replace("dev_", ""), 10);
120324
- if (!isNaN(num)) deviceCounter = num;
120325
- }
120326
120784
  }
120327
120785
  async createIntegration(input) {
120328
120786
  const id = nextIntegrationId();
@@ -120406,15 +120864,6 @@ var require_dist3 = __commonJS({
120406
120864
  async deleteIntegration(id) {
120407
120865
  const integration = await this.getIntegration(id);
120408
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`);
120409
- const devices = await this.listDevices(id);
120410
- for (const d of devices) await this.store.deleteWhere({
120411
- collection: DEVICE_SETTINGS,
120412
- filter: { where: { device_id: d.id } }
120413
- });
120414
- await this.store.deleteWhere({
120415
- collection: DEVICES,
120416
- filter: { where: { integration_id: id } }
120417
- });
120418
120867
  await this.store.deleteWhere({
120419
120868
  collection: INTEGRATION_SETTINGS,
120420
120869
  filter: { where: { integration_id: id } }
@@ -120459,139 +120908,6 @@ var require_dist3 = __commonJS({
120459
120908
  async setIntegrationSettings(integrationId, settings) {
120460
120909
  for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
120461
120910
  }
120462
- async createDevice(input) {
120463
- const id = nextDeviceId();
120464
- const now = Math.floor(Date.now() / 1e3);
120465
- await this.store.insert({
120466
- collection: DEVICES,
120467
- record: {
120468
- id,
120469
- data: {
120470
- integration_id: input.integrationId,
120471
- stable_id: input.stableId,
120472
- type: input.type,
120473
- name: input.name,
120474
- enabled: input.enabled !== false ? 1 : 0,
120475
- info: input.info ?? {},
120476
- created_at: now,
120477
- updated_at: now
120478
- }
120479
- }
120480
- });
120481
- if (input.settings) await this.setDeviceSettings(id, input.settings);
120482
- return {
120483
- id,
120484
- integrationId: input.integrationId,
120485
- stableId: input.stableId,
120486
- type: input.type,
120487
- name: input.name,
120488
- enabled: input.enabled !== false,
120489
- info: input.info ?? {},
120490
- createdAt: now,
120491
- updatedAt: now
120492
- };
120493
- }
120494
- async getDevice(id) {
120495
- const rows = await this.store.query({
120496
- collection: DEVICES,
120497
- filter: {
120498
- where: { id },
120499
- limit: 1
120500
- }
120501
- });
120502
- return rows[0] ? this.mapDevice(rows[0].data) : null;
120503
- }
120504
- async getDeviceByStableId(stableId) {
120505
- const rows = await this.store.query({
120506
- collection: DEVICES,
120507
- filter: {
120508
- where: { stable_id: stableId },
120509
- limit: 1
120510
- }
120511
- });
120512
- return rows[0] ? this.mapDevice(rows[0].data) : null;
120513
- }
120514
- async listDevices(integrationId) {
120515
- return (await this.store.query({
120516
- collection: DEVICES,
120517
- filter: {
120518
- ...integrationId ? { where: { integration_id: integrationId } } : {},
120519
- orderBy: {
120520
- field: "created_at",
120521
- direction: "asc"
120522
- }
120523
- }
120524
- })).map((r) => this.mapDevice(r.data));
120525
- }
120526
- async listCameras() {
120527
- return (await this.store.query({
120528
- collection: DEVICES,
120529
- filter: {
120530
- where: { type: "camera" },
120531
- orderBy: {
120532
- field: "created_at",
120533
- direction: "asc"
120534
- }
120535
- }
120536
- })).map((r) => this.mapDevice(r.data));
120537
- }
120538
- async updateDevice(id, updates) {
120539
- const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
120540
- if (updates.name !== void 0) updateRow["name"] = updates.name;
120541
- if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
120542
- if (updates.info !== void 0) updateRow["info"] = updates.info;
120543
- await this.store.updateWhere({
120544
- collection: DEVICES,
120545
- filter: { where: { id } },
120546
- data: updateRow
120547
- });
120548
- return this.getDevice(id);
120549
- }
120550
- async deleteDevice(id) {
120551
- await this.store.deleteWhere({
120552
- collection: DEVICE_SETTINGS,
120553
- filter: { where: { device_id: id } }
120554
- });
120555
- await this.store.deleteWhere({
120556
- collection: DEVICES,
120557
- filter: { where: { id } }
120558
- });
120559
- return true;
120560
- }
120561
- async getDeviceSettings(deviceId) {
120562
- const result = {};
120563
- const rows = await this.store.query({
120564
- collection: DEVICE_SETTINGS,
120565
- filter: { where: { device_id: deviceId } }
120566
- });
120567
- for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
120568
- return result;
120569
- }
120570
- async setDeviceSetting(deviceId, key, value) {
120571
- const s = serializeSetting(value);
120572
- await this.store.deleteWhere({
120573
- collection: DEVICE_SETTINGS,
120574
- filter: { where: {
120575
- device_id: deviceId,
120576
- key
120577
- } }
120578
- });
120579
- await this.store.insert({
120580
- collection: DEVICE_SETTINGS,
120581
- record: {
120582
- id: `${deviceId}:${key}`,
120583
- data: {
120584
- device_id: deviceId,
120585
- key,
120586
- value: s.value,
120587
- value_type: s.valueType
120588
- }
120589
- }
120590
- });
120591
- }
120592
- async setDeviceSettings(deviceId, settings) {
120593
- for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
120594
- }
120595
120911
  mapIntegration(row) {
120596
120912
  return {
120597
120913
  id: String(row["id"]),
@@ -120603,19 +120919,6 @@ var require_dist3 = __commonJS({
120603
120919
  updatedAt: Number(row["updated_at"])
120604
120920
  };
120605
120921
  }
120606
- mapDevice(row) {
120607
- return {
120608
- id: String(row["id"]),
120609
- integrationId: String(row["integration_id"]),
120610
- stableId: String(row["stable_id"]),
120611
- type: String(row["type"]),
120612
- name: String(row["name"]),
120613
- enabled: row["enabled"] === 1,
120614
- info: readInfo(row["info"]),
120615
- createdAt: Number(row["created_at"]),
120616
- updatedAt: Number(row["updated_at"])
120617
- };
120618
- }
120619
120922
  };
120620
120923
  var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
120621
120924
  var NATIVE_SCAN_DEPTH = 4;
@@ -123109,6 +123412,7 @@ var require_dist3 = __commonJS({
123109
123412
  const state = {
123110
123413
  definition,
123111
123414
  providers: /* @__PURE__ */ new Map(),
123415
+ providerOwners: /* @__PURE__ */ new Map(),
123112
123416
  activeAddonId: null,
123113
123417
  router: null,
123114
123418
  disabledProviders: /* @__PURE__ */ new Set()
@@ -123166,8 +123470,15 @@ var require_dist3 = __commonJS({
123166
123470
  * Double-register of the same pair is always a programmer error —
123167
123471
  * two legitimately different addons implementing the same cap keep
123168
123472
  * using different `addonId`s and the user picks one via `configReader`.
123169
- */
123170
- 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) {
123171
123482
  let state = this.capabilities.get(capabilityName);
123172
123483
  if (!state) {
123173
123484
  const colonIdx = capabilityName.indexOf(":");
@@ -123199,6 +123510,7 @@ var require_dist3 = __commonJS({
123199
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).`);
123200
123511
  }
123201
123512
  state.providers.set(addonId, provider);
123513
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
123202
123514
  if (state.definition.mode === "singleton") {
123203
123515
  const userChoice = this.configReader?.(capabilityName);
123204
123516
  const preferred = state.definition.preferredProvider;
@@ -123229,7 +123541,8 @@ var require_dist3 = __commonJS({
123229
123541
  tags: { addonId },
123230
123542
  meta: {
123231
123543
  capability: capabilityName,
123232
- mode: state.definition.mode
123544
+ mode: state.definition.mode,
123545
+ ...owner === void 0 ? {} : { owner }
123233
123546
  }
123234
123547
  });
123235
123548
  this.emitEvent("capability:provider-registered", {
@@ -123276,11 +123589,70 @@ var require_dist3 = __commonJS({
123276
123589
  const declarers = this.manifestDeclarers.get(capabilityName);
123277
123590
  return declarers ? [...declarers] : [];
123278
123591
  }
123279
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
123280
- 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) {
123281
123640
  const state = this.capabilities.get(capabilityName);
123282
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
+ }
123283
123654
  state.providers.delete(addonId);
123655
+ state.providerOwners.delete(addonId);
123284
123656
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
123285
123657
  const userChoice = this.configReader?.(capabilityName);
123286
123658
  const preferred = state.definition.preferredProvider;
@@ -198254,6 +198626,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198254
198626
  function safeSegment(name2) {
198255
198627
  return name2.replace(/[^a-zA-Z0-9._-]/g, "_");
198256
198628
  }
198629
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
198257
198630
  var StagingArea = class {
198258
198631
  stagingDir;
198259
198632
  deps;
@@ -198271,8 +198644,12 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198271
198644
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
198272
198645
  await this.deps.extract(tgz, destDir);
198273
198646
  const raw = JSON.parse(node_fs.readFileSync(node_path.join(destDir, "package.json"), "utf-8"));
198274
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
198275
- 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
+ };
198276
198653
  }
198277
198654
  cleanup(jobId) {
198278
198655
  node_fs.rmSync(node_path.join(this.stagingDir, jobId), {
@@ -238430,7 +238807,7 @@ var require_dist9 = __commonJS({
238430
238807
  postBufferSec: zod.z.number().min(0).optional()
238431
238808
  });
238432
238809
  var DEFAULT_EVENTS_BAND_BUFFER_SEC = {
238433
- preBufferSec: 10,
238810
+ preBufferSec: 15,
238434
238811
  postBufferSec: 30
238435
238812
  };
238436
238813
  var VISIT_MERGE_GAP_MS = DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec * 1e3;
@@ -239044,12 +239421,17 @@ var require_dist9 = __commonJS({
239044
239421
  "tflite",
239045
239422
  "pt"
239046
239423
  ];
239424
+ var FAMILY_LABEL = {
239425
+ yolo26: "YOLO26",
239426
+ yolov9: "YOLOv9"
239427
+ };
239047
239428
  function familyLabel(family) {
239048
- return family.toUpperCase();
239429
+ return FAMILY_LABEL[family] ?? family.toUpperCase();
239049
239430
  }
239431
+ var RESOLUTION_IN_NAME = /\s*@\s*\d+\s*(?:[x×]\s*\d+)?/gi;
239050
239432
  function tierLabel(family, baseName) {
239051
239433
  const fl = familyLabel(family);
239052
- 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();
239053
239435
  return stripped.length > 0 ? stripped : baseName;
239054
239436
  }
239055
239437
  function variantLabel(precision, optimization) {
@@ -239066,9 +239448,11 @@ var require_dist9 = __commonJS({
239066
239448
  return sizes.length > 0 ? Math.min(...sizes) : 0;
239067
239449
  }
239068
239450
  var TIER_ORDER = [
239451
+ "t",
239069
239452
  "n",
239070
239453
  "s",
239071
239454
  "m",
239455
+ "c",
239072
239456
  "l",
239073
239457
  "x"
239074
239458
  ];
@@ -239105,7 +239489,7 @@ var require_dist9 = __commonJS({
239105
239489
  if (a.optimization !== b.optimization) return a.optimization === "fast" ? 1 : -1;
239106
239490
  return PRECISION_ORDER.indexOf(a.precision) - PRECISION_ORDER.indexOf(b.precision);
239107
239491
  });
239108
- 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];
239109
239493
  if (base === void 0) continue;
239110
239494
  const baseEntry = entries2.find((e) => e.id === base.modelId);
239111
239495
  tierList.push({
@@ -239135,15 +239519,9 @@ var require_dist9 = __commonJS({
239135
239519
  function resolveVariantModelId(models, selection) {
239136
239520
  const precision = selection.precision ?? "fp32";
239137
239521
  const optimization = selection.optimization ?? "standard";
239138
- for (const m of models) {
239139
- if (m.group === void 0) continue;
239140
- if (m.group.family !== selection.family || m.group.tier !== selection.tier) continue;
239141
- if ((m.group.precision ?? "fp32") !== precision) continue;
239142
- if ((m.group.optimization ?? "standard") !== optimization) continue;
239143
- if (m.group.resolution !== selection.resolution) continue;
239144
- return m.id;
239145
- }
239146
- 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;
239147
239525
  }
239148
239526
  function describeModelVariant(models, modelId) {
239149
239527
  const entry = models.find((m) => m.id === modelId);
@@ -240550,11 +240928,15 @@ export default function block(ctx: AddonContext) {
240550
240928
  ): Promise<void> {
240551
240929
  const resolved: Record<string, unknown> = {}
240552
240930
  for (const [k, v] of Object.entries(args)) resolved[k] = substitute(v, vars)
240553
- const client = (ctx.api as unknown as Record<string, Record<string, { mutate: (i: unknown) => Promise<unknown> }>>)[capToRouterKey(cap)]
240554
- 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') {
240555
240937
  throw new Error('no such device method: ' + cap + '.' + method)
240556
240938
  }
240557
- await client[method]!.mutate({ ...resolved, deviceId })
240939
+ await mutate({ ...resolved, deviceId })
240558
240940
  }
240559
240941
 
240560
240942
  async function evaluateConditions(): Promise<boolean> {
@@ -246401,128 +246783,103 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
246401
246783
  var AUDIO_MACRO_LABELS = [
246402
246784
  {
246403
246785
  id: "speech",
246404
- name: "Speech",
246405
- icon: "\u{1F5E3}\uFE0F"
246786
+ name: "Speech"
246406
246787
  },
246407
246788
  {
246408
246789
  id: "scream",
246409
- name: "Scream / Shout",
246410
- icon: "\u{1F631}"
246790
+ name: "Scream / Shout"
246411
246791
  },
246412
246792
  {
246413
246793
  id: "crying",
246414
- name: "Crying / Baby",
246415
- icon: "\u{1F622}"
246794
+ name: "Crying / Baby"
246416
246795
  },
246417
246796
  {
246418
246797
  id: "laughter",
246419
- name: "Laughter",
246420
- icon: "\u{1F602}"
246798
+ name: "Laughter"
246421
246799
  },
246422
246800
  {
246423
246801
  id: "music",
246424
- name: "Music",
246425
- icon: "\u{1F3B5}"
246802
+ name: "Music"
246426
246803
  },
246427
246804
  {
246428
246805
  id: "dog",
246429
- name: "Dog",
246430
- icon: "\u{1F415}"
246806
+ name: "Dog"
246431
246807
  },
246432
246808
  {
246433
246809
  id: "cat",
246434
- name: "Cat",
246435
- icon: "\u{1F408}"
246810
+ name: "Cat"
246436
246811
  },
246437
246812
  {
246438
246813
  id: "bird",
246439
- name: "Bird",
246440
- icon: "\u{1F426}"
246814
+ name: "Bird"
246441
246815
  },
246442
246816
  {
246443
246817
  id: "animal",
246444
- name: "Animal (other)",
246445
- icon: "\u{1F43E}"
246818
+ name: "Animal (other)"
246446
246819
  },
246447
246820
  {
246448
246821
  id: "alarm",
246449
- name: "Alarm / Siren",
246450
- icon: "\u{1F6A8}"
246822
+ name: "Alarm / Siren"
246451
246823
  },
246452
246824
  {
246453
246825
  id: "doorbell",
246454
- name: "Doorbell / Knock",
246455
- icon: "\u{1F514}"
246826
+ name: "Doorbell / Knock"
246456
246827
  },
246457
246828
  {
246458
246829
  id: "glass_breaking",
246459
- name: "Glass Breaking",
246460
- icon: "\u{1F4A5}"
246830
+ name: "Glass Breaking"
246461
246831
  },
246462
246832
  {
246463
246833
  id: "gunshot",
246464
- name: "Gunshot / Explosion",
246465
- icon: "\u{1F4A3}"
246834
+ name: "Gunshot / Explosion"
246466
246835
  },
246467
246836
  {
246468
246837
  id: "vehicle",
246469
- name: "Vehicle",
246470
- icon: "\u{1F697}"
246838
+ name: "Vehicle"
246471
246839
  },
246472
246840
  {
246473
246841
  id: "siren",
246474
- name: "Emergency Siren",
246475
- icon: "\u{1F691}"
246842
+ name: "Emergency Siren"
246476
246843
  },
246477
246844
  {
246478
246845
  id: "fire",
246479
- name: "Fire / Smoke",
246480
- icon: "\u{1F525}"
246846
+ name: "Fire / Smoke"
246481
246847
  },
246482
246848
  {
246483
246849
  id: "water",
246484
- name: "Water",
246485
- icon: "\u{1F4A7}"
246850
+ name: "Water"
246486
246851
  },
246487
246852
  {
246488
246853
  id: "wind",
246489
- name: "Wind / Weather",
246490
- icon: "\u{1F32C}\uFE0F"
246854
+ name: "Wind / Weather"
246491
246855
  },
246492
246856
  {
246493
246857
  id: "door",
246494
- name: "Door",
246495
- icon: "\u{1F6AA}"
246858
+ name: "Door"
246496
246859
  },
246497
246860
  {
246498
246861
  id: "footsteps",
246499
- name: "Footsteps",
246500
- icon: "\u{1F463}"
246862
+ name: "Footsteps"
246501
246863
  },
246502
246864
  {
246503
246865
  id: "crowd",
246504
- name: "Crowd / Chatter",
246505
- icon: "\u{1F465}"
246866
+ name: "Crowd / Chatter"
246506
246867
  },
246507
246868
  {
246508
246869
  id: "telephone",
246509
- name: "Telephone",
246510
- icon: "\u{1F4DE}"
246870
+ name: "Telephone"
246511
246871
  },
246512
246872
  {
246513
246873
  id: "engine",
246514
- name: "Engine / Motor",
246515
- icon: "\u2699\uFE0F"
246874
+ name: "Engine / Motor"
246516
246875
  },
246517
246876
  {
246518
246877
  id: "tools",
246519
- name: "Tools / Construction",
246520
- icon: "\u{1F528}"
246878
+ name: "Tools / Construction"
246521
246879
  },
246522
246880
  {
246523
246881
  id: "silence",
246524
- name: "Silence",
246525
- icon: "\u{1F92B}"
246882
+ name: "Silence"
246526
246883
  }
246527
246884
  ];
246528
246885
  var YAMNET_TO_MACRO = {
@@ -249110,6 +249467,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249110
249467
  */
249111
249468
  hasFace: zod.z.boolean().optional(),
249112
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
+ /**
249113
249499
  * This subject CONTAINS a folded rider — a person the rider-pairing step
249114
249500
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
249115
249501
  * so the passage is tracked once and as a VEHICLE.
@@ -250870,6 +251256,32 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
250870
251256
  /** FRAME-space bbox (already mapped back from crop space). */
250871
251257
  bbox: NativeCropBboxSchema.optional(),
250872
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(),
250873
251285
  label: zod.z.string().optional(),
250874
251286
  /**
250875
251287
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -252694,6 +253106,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252694
253106
  isBattery: zod.z.boolean(),
252695
253107
  reason: zod.z.enum([
252696
253108
  "disabled",
253109
+ "offline",
252697
253110
  "sleeping",
252698
253111
  "unreachable",
252699
253112
  "waking"
@@ -252784,9 +253197,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252784
253197
  sleeping: zod.z.boolean(),
252785
253198
  /** Current device state rendered over the cached frame. State images
252786
253199
  * remain authoritative even when their photographic background is
252787
- * 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. */
252788
253208
  stateReason: zod.z.enum([
252789
253209
  "disabled",
253210
+ "offline",
252790
253211
  "sleeping",
252791
253212
  "unreachable",
252792
253213
  "waking"
@@ -273776,6 +274197,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273776
274197
  function normalizeAudioLabel(value) {
273777
274198
  return value.startsWith(AUDIO_KIND_PREFIX) ? value.slice(6) : value;
273778
274199
  }
274200
+ function audioKindId(macroId) {
274201
+ return macroId.startsWith(AUDIO_KIND_PREFIX) ? macroId : `${AUDIO_KIND_PREFIX}${macroId}`;
274202
+ }
273779
274203
  var NC_AUDIO_DB_MIN = -96;
273780
274204
  var NC_AUDIO_DB_MAX = 0;
273781
274205
  var NC_AUDIO_DB_STEP = 3;
@@ -273823,8 +274247,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273823
274247
  samplingSeconds: clampInt(patch.samplingSeconds ?? base.samplingSeconds, 1, 300)
273824
274248
  };
273825
274249
  }
273826
- var UNKNOWN_LABEL_ICON = "\u{1F508}";
273827
- var ICON_BY_ID = new Map(AUDIO_MACRO_LABELS.flatMap((macro2) => macro2.icon === void 0 ? [] : [[macro2.id, macro2.icon]]));
273828
274250
  function audioLabelChoices(taxonomy, selected) {
273829
274251
  const fromHub = taxonomy?.audioKinds ?? [];
273830
274252
  const choices = fromHub.length > 0 ? fromHub.map((entry) => {
@@ -273832,13 +274254,13 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273832
274254
  return {
273833
274255
  value: id,
273834
274256
  label: entry.label,
273835
- icon: ICON_BY_ID.get(id) ?? UNKNOWN_LABEL_ICON,
274257
+ iconId: audioKindId(id),
273836
274258
  unknown: false
273837
274259
  };
273838
274260
  }) : AUDIO_MACRO_LABELS.map((macro2) => ({
273839
274261
  value: macro2.id,
273840
274262
  label: macro2.name,
273841
- icon: macro2.icon ?? UNKNOWN_LABEL_ICON,
274263
+ iconId: audioKindId(macro2.id),
273842
274264
  unknown: false
273843
274265
  }));
273844
274266
  const known = new Set(choices.map((choice) => choice.value));
@@ -273850,7 +274272,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273850
274272
  choices.push({
273851
274273
  value: id,
273852
274274
  label: id,
273853
- icon: UNKNOWN_LABEL_ICON,
274275
+ iconId: audioKindId(id),
273854
274276
  unknown: true
273855
274277
  });
273856
274278
  }
@@ -274574,6 +274996,74 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274574
274996
  if (map !== void 0) return map[String(deviceId)] ?? 0;
274575
274997
  return rule.lastGeneratedAt ?? 0;
274576
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
+ }
274577
275067
  var DETAIL_CROP_SECTION_ID = "detail-crop";
274578
275068
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
274579
275069
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
@@ -274621,6 +275111,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274621
275111
  }
274622
275112
  return readDetailCropConvention(flat);
274623
275113
  }
275114
+ var FULL_IMAGE_BBOX = Object.freeze({
275115
+ x: 0,
275116
+ y: 0,
275117
+ w: 1,
275118
+ h: 1
275119
+ });
274624
275120
  var DETAIL_CROP_PADDING_FIELD = {
274625
275121
  min: 0,
274626
275122
  max: 1,
@@ -276356,6 +276852,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276356
276852
  exports.CAP_NAMES_WITH_STATUS = CAP_NAMES_WITH_STATUS;
276357
276853
  exports.CAP_NODE_PIN_CONTEXT_KEY = require_sleep.CAP_NODE_PIN_CONTEXT_KEY;
276358
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;
276359
276857
  exports.COCO_80_LABELS = COCO_80_LABELS;
276360
276858
  exports.COCO_TO_MACRO = COCO_TO_MACRO;
276361
276859
  exports.CONNECTION_TEST_TIMEOUT_MS = CONNECTION_TEST_TIMEOUT_MS;
@@ -276443,6 +276941,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276443
276941
  exports.DECLARED_INTEGRATION_FIXED_KEY = DECLARED_INTEGRATION_FIXED_KEY;
276444
276942
  exports.DEFAULT_ADDON_PLACEMENT = DEFAULT_ADDON_PLACEMENT;
276445
276943
  exports.DEFAULT_AUDIO_ANALYZER_CONFIG = DEFAULT_AUDIO_ANALYZER_CONFIG;
276944
+ exports.DEFAULT_CLUSTER_STEP_MODELS = DEFAULT_CLUSTER_STEP_MODELS;
276446
276945
  exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
276447
276946
  exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
276448
276947
  exports.DEFAULT_EVENTS_BAND_BUFFER_SEC = DEFAULT_EVENTS_BAND_BUFFER_SEC;
@@ -276561,6 +277060,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276561
277060
  exports.ExpressionLiteralBindingSchema = ExpressionLiteralBindingSchema;
276562
277061
  exports.ExpressionParseError = ExpressionParseError;
276563
277062
  exports.ExpressionSourceSchema = ExpressionSourceSchema;
277063
+ exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
276564
277064
  exports.FanControlStatusSchema = FanControlStatusSchema;
276565
277065
  exports.FanDirectionSchema = FanDirectionSchema;
276566
277066
  exports.FeatureManifestSchema = FeatureManifestSchema;
@@ -277180,6 +277680,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277180
277680
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
277181
277681
  exports.audioCodecCapability = audioCodecCapability;
277182
277682
  exports.audioIsFailClosed = audioIsFailClosed;
277683
+ exports.audioKindId = audioKindId;
277183
277684
  exports.audioLabelChoices = audioLabelChoices;
277184
277685
  exports.audioMetricsCapability = audioMetricsCapability;
277185
277686
  exports.audioModeOf = audioModeOf;
@@ -277218,6 +277719,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277218
277719
  exports.classifyStream = classifyStream;
277219
277720
  exports.classifyStreams = classifyStreams;
277220
277721
  exports.climateControlCapability = climateControlCapability;
277722
+ exports.clusterModelSettingKey = clusterModelSettingKey;
277221
277723
  exports.collectHydratedFieldEntries = require_sleep.collectHydratedFieldEntries;
277222
277724
  exports.collectHydratedFieldValues = require_sleep.collectHydratedFieldValues;
277223
277725
  exports.colorCapability = colorCapability;
@@ -277336,6 +277838,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277336
277838
  exports.isAudioRule = isAudioRule;
277337
277839
  exports.isBaseConditionKey = isBaseConditionKey;
277338
277840
  exports.isBatteryPresenceFault = isBatteryPresenceFault;
277841
+ exports.isClusterScopedStep = isClusterScopedStep;
277339
277842
  exports.isCollectionArrayMethod = isCollectionArrayMethod;
277340
277843
  exports.isDeployableToAgent = isDeployableToAgent;
277341
277844
  exports.isDetectionMacroClass = isDetectionMacroClass;
@@ -277419,6 +277922,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277419
277922
  exports.patchAudio = patchAudio;
277420
277923
  exports.petFeederCapability = petFeederCapability;
277421
277924
  exports.pickAccessoryControl = pickAccessoryControl;
277925
+ exports.pickClusterStepModels = pickClusterStepModels;
277422
277926
  exports.pickDetailCropConvention = pickDetailCropConvention;
277423
277927
  exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
277424
277928
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
@@ -277442,6 +277946,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277442
277946
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
277443
277947
  exports.ptzCapability = ptzCapability;
277444
277948
  exports.pythonScriptForBackend = pythonScriptForBackend;
277949
+ exports.readClusterStepModels = readClusterStepModels;
277445
277950
  exports.readDetailCropConvention = readDetailCropConvention;
277446
277951
  exports.readDeviceStateFrom = readDeviceStateFrom;
277447
277952
  exports.readNativeLeaseOverride = readNativeLeaseOverride;
@@ -277459,6 +277964,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277459
277964
  exports.resolveAddonPlacement = resolveAddonPlacement;
277460
277965
  exports.resolveAddonRuntime = resolveAddonRuntime;
277461
277966
  exports.resolveCapMount = require_sleep.resolveCapMount;
277967
+ exports.resolveClusterStepModelId = resolveClusterStepModelId;
277462
277968
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
277463
277969
  exports.resolveDeviceControlKind = resolveDeviceControlKind;
277464
277970
  exports.resolveDeviceProfile = resolveDeviceProfile;
@@ -277768,22 +278274,20 @@ var require_update_availability_emitter = __commonJS({
277768
278274
  this.persistIfDirty();
277769
278275
  if (!latestChanged)
277770
278276
  return;
277771
- this.emitList(target);
278277
+ this.emitList(target, scope);
277772
278278
  }
277773
278279
  /** Publish a partial candidate set without treating omissions as up-to-date. */
277774
278280
  publishCandidates(candidates) {
277775
- let latestChanged = false;
277776
- const targets = /* @__PURE__ */ new Set();
278281
+ const changed = /* @__PURE__ */ new Map();
277777
278282
  for (const candidate of candidates) {
277778
- targets.add(candidate.target);
277779
- if (this.note(candidate))
277780
- latestChanged = true;
278283
+ if (!this.note(candidate))
278284
+ continue;
278285
+ const scope = scopeOf(candidate);
278286
+ changed.set(`${candidate.target}:${scope}`, { target: candidate.target, scope });
277781
278287
  }
277782
278288
  this.persistIfDirty();
277783
- if (!latestChanged)
277784
- return;
277785
- for (const target of targets)
277786
- this.emitList(target);
278289
+ for (const { target, scope } of changed.values())
278290
+ this.emitList(target, scope);
277787
278291
  }
277788
278292
  note(candidate) {
277789
278293
  const key = this.key(candidate);
@@ -277809,20 +278313,20 @@ var require_update_availability_emitter = __commonJS({
277809
278313
  held: Object.fromEntries(this.held)
277810
278314
  });
277811
278315
  }
277812
- emitList(target) {
277813
- 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);
277814
278319
  if (list.length === 0)
277815
278320
  return;
277816
278321
  const packages = list.map((candidate) => ({
277817
278322
  packageName: candidate.packageName,
277818
278323
  currentVersion: candidate.currentVersion,
277819
278324
  latestVersion: candidate.latestVersion,
277820
- ...candidate.nodeId !== void 0 ? { nodeId: candidate.nodeId } : {}
278325
+ nodeId: scope
277821
278326
  }));
277822
- const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
277823
278327
  const head = list[0];
277824
278328
  this.eventBus.emit({
277825
- 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(",")}`,
277826
278330
  timestamp: /* @__PURE__ */ new Date(),
277827
278331
  source: this.source,
277828
278332
  category: types_1.EventCategory.UpdateAvailable,
@@ -277832,24 +278336,24 @@ var require_update_availability_emitter = __commonJS({
277832
278336
  currentVersion: head.currentVersion,
277833
278337
  latestVersion: head.latestVersion,
277834
278338
  packages,
277835
- ...head.nodeId !== void 0 ? { nodeId: head.nodeId } : {},
277836
- ...nodeIds.length > 0 ? { nodeIds } : {}
278339
+ nodeId: scope,
278340
+ nodeIds: [scope]
277837
278341
  }
277838
278342
  });
277839
278343
  }
277840
278344
  key(candidate) {
277841
- return `${candidate.target}:${candidate.nodeId ?? "hub"}:${candidate.packageName}`;
278345
+ return `${candidate.target}:${scopeOf(candidate)}:${candidate.packageName}`;
277842
278346
  }
277843
278347
  };
277844
278348
  exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
278349
+ function scopeOf(candidate) {
278350
+ return candidate.nodeId ?? "hub";
278351
+ }
277845
278352
  function stampNode(candidate, nodeId) {
277846
278353
  if (nodeId === void 0 || candidate.nodeId !== void 0)
277847
278354
  return candidate;
277848
278355
  return { ...candidate, nodeId };
277849
278356
  }
277850
- function unique(values) {
277851
- return [...new Set(values)];
277852
- }
277853
278357
  }
277854
278358
  });
277855
278359
 
@@ -279204,7 +279708,13 @@ var require_server_root = __commonJS({
279204
279708
  fromVersion: runningVersion
279205
279709
  }
279206
279710
  });
279207
- 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
+ });
279208
279718
  return {
279209
279719
  accepted: true,
279210
279720
  targetVersion: target,
@@ -279489,6 +279999,47 @@ var require_server_root = __commonJS({
279489
279999
  }
279490
280000
  });
279491
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
+
279492
280043
  // ../../server/backend/dist/agent/agent-update-service.js
279493
280044
  var require_agent_update_service = __commonJS({
279494
280045
  "../../server/backend/dist/agent/agent-update-service.js"(exports) {
@@ -279546,6 +280097,7 @@ var require_agent_update_service = __commonJS({
279546
280097
  var update_availability_emitter_js_1 = require_update_availability_emitter();
279547
280098
  var update_availability_store_js_1 = require_update_availability_store();
279548
280099
  var index_js_1 = require_server_root();
280100
+ var restart_marker_js_1 = require_restart_marker();
279549
280101
  exports.AGENT_RUNTIME_ADDON_ID = "agent-runtime";
279550
280102
  exports.AGENT_RESTART_GRACE_MS = 2e3;
279551
280103
  function isRunningInContainer(existsSyncFn) {
@@ -279609,8 +280161,8 @@ var require_agent_update_service = __commonJS({
279609
280161
  seedDir: "CAMSTACK_SEED_SERVER_DIR"
279610
280162
  },
279611
280163
  logger: options.logger,
279612
- restartServer: (requestedBy) => {
279613
- writeAgentRestartMarker(options.dataDir, requestedBy);
280164
+ restartServer: (requestedBy, update) => {
280165
+ writeAgentRestartMarker(options.dataDir, requestedBy, update);
279614
280166
  restartAgent(requestedBy);
279615
280167
  },
279616
280168
  dataDir: options.dataDir,
@@ -279688,18 +280240,13 @@ var require_agent_update_service = __commonJS({
279688
280240
  }
279689
280241
  };
279690
280242
  exports.AgentUpdateService = AgentUpdateService;
279691
- function writeAgentRestartMarker(dataDir, requestedBy) {
279692
- const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
280243
+ function writeAgentRestartMarker(dataDir, requestedBy, update) {
279693
280244
  try {
279694
- (0, system_1.writePendingRestart)(dataDir, {
279695
- kind: requestedBy.startsWith("server-update:") ? "framework-update" : "manual",
280245
+ (0, system_1.writePendingRestart)(dataDir, (0, restart_marker_js_1.buildRestartMarker)({
279696
280246
  requestedAt: Date.now(),
279697
280247
  requestedBy,
279698
- ...requestedBy.startsWith("server-update:") ? {
279699
- packageName: "@camstack/server",
279700
- ...toVersion !== void 0 ? { toVersion } : {}
279701
- } : {}
279702
- });
280248
+ ...update !== void 0 ? { update } : {}
280249
+ }));
279703
280250
  } catch {
279704
280251
  }
279705
280252
  }
@@ -388570,6 +389117,7 @@ var require_addon_package_service = __commonJS({
388570
389117
  var types_1 = require_dist9();
388571
389118
  var update_availability_emitter_js_1 = require_update_availability_emitter();
388572
389119
  var update_check_scheduler_js_1 = require_update_check_scheduler();
389120
+ var restart_marker_js_1 = require_restart_marker();
388573
389121
  var package_dir_utils_js_1 = require_package_dir_utils();
388574
389122
  var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
388575
389123
  exports.SYSTEM_PACKAGE = "@camstack/system";
@@ -389497,26 +390045,24 @@ var require_addon_package_service = __commonJS({
389497
390045
  /**
389498
390046
  * Gracefully restart the server process.
389499
390047
  *
389500
- * Writes a `manual` restart marker so the post-boot service can
389501
- * emit `system.restart-completed` on the next boot (powers the
389502
- * 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`
389503
390054
  * with the same payload so subscribers can show a reconnect overlay
389504
390055
  * immediately. Then delegates to `scheduleSelfRestart` which picks
389505
390056
  * the right exit path (Electron `app.relaunch` vs supervisor-driven
389506
390057
  * `process.exit`).
389507
390058
  */
389508
- restartServer(requestedBy) {
390059
+ restartServer(requestedBy, update) {
389509
390060
  this.logger.info("Server restart requested -- initiating graceful shutdown");
389510
- const toVersion = toVersionFromRestartReason(requestedBy);
389511
- const payload = {
389512
- kind: isServerUpdateRequest(requestedBy) ? "framework-update" : "manual",
390061
+ const payload = (0, restart_marker_js_1.buildRestartMarker)({
389513
390062
  requestedAt: Date.now(),
389514
390063
  ...requestedBy !== void 0 ? { requestedBy } : {},
389515
- ...isServerUpdateRequest(requestedBy) ? {
389516
- packageName: "@camstack/server",
389517
- ...toVersion !== void 0 ? { toVersion } : {}
389518
- } : {}
389519
- };
390064
+ ...update !== void 0 ? { update } : {}
390065
+ });
389520
390066
  try {
389521
390067
  (0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
389522
390068
  } catch (err) {
@@ -390007,22 +390553,23 @@ var require_addon_package_service = __commonJS({
390007
390553
  * re-announces them on the next good poll.
390008
390554
  */
390009
390555
  publishAddonAvailability(updates, failures, nodeId) {
390556
+ const scope = nodeId ?? this.resolveNodeId();
390010
390557
  const candidates = updates.map((update) => ({
390011
390558
  target: "addon",
390012
390559
  packageName: update.name,
390013
390560
  currentVersion: update.currentVersion,
390014
390561
  latestVersion: update.latestVersion,
390015
- ...nodeId !== void 0 ? { nodeId } : {}
390562
+ nodeId: scope
390016
390563
  }));
390017
390564
  if (failures > 0) {
390018
390565
  this.logger.warn("Addon update check incomplete \u2014 availability published as partial", {
390019
- tags: { nodeId: nodeId ?? "hub" },
390566
+ tags: { nodeId: scope },
390020
390567
  meta: { failures, published: candidates.length }
390021
390568
  });
390022
390569
  this.updateAvailability.publishCandidates(candidates);
390023
390570
  return;
390024
390571
  }
390025
- this.updateAvailability.publishSnapshot("addon", candidates, nodeId);
390572
+ this.updateAvailability.publishSnapshot("addon", candidates, scope);
390026
390573
  }
390027
390574
  /** Fetch npm search results for camstack addon packages (cached 5 min) */
390028
390575
  async fetchSearchFromNpm() {
@@ -390172,15 +390719,6 @@ var require_addon_package_service = __commonJS({
390172
390719
  }
390173
390720
  };
390174
390721
  exports.AddonPackageService = AddonPackageService;
390175
- function isServerUpdateRequest(requestedBy) {
390176
- return requestedBy !== void 0 && requestedBy.startsWith("server-update:");
390177
- }
390178
- function toVersionFromRestartReason(requestedBy) {
390179
- if (requestedBy === void 0)
390180
- return void 0;
390181
- const match = /@([^@\s]+)$/.exec(requestedBy);
390182
- return match?.[1];
390183
- }
390184
390722
  async function extractTgzStripped(tgz, destDir) {
390185
390723
  fs.mkdirSync(destDir, { recursive: true });
390186
390724
  const tmpFile = path.join(os.tmpdir(), `camstack-tgz-${(0, node_crypto_1.randomUUID)()}.tgz`);
@@ -401689,14 +402227,6 @@ var require_integration_visibility = __commonJS({
401689
402227
  function createFilteredIntegrationRegistry(deps) {
401690
402228
  const { raw, logger } = deps;
401691
402229
  const isInstalled = (addonId) => deps.isAddonInstalled((0, types_1.bareAddonId)(addonId));
401692
- let activeIntegrationIds = null;
401693
- const ensureActiveIds = async () => {
401694
- if (activeIntegrationIds)
401695
- return activeIntegrationIds;
401696
- const all = await raw.listIntegrations();
401697
- activeIntegrationIds = new Set(all.filter((i) => isInstalled(i.addonId)).map((i) => i.id));
401698
- return activeIntegrationIds;
401699
- };
401700
402230
  const logHidden = (integration, phase) => {
401701
402231
  logger.warn("integration hidden \u2014 its addon is not installed, so this read answers null even though the row exists", {
401702
402232
  tags: { integrationId: integration.id, addonId: integration.addonId },
@@ -401742,39 +402272,7 @@ var require_integration_visibility = __commonJS({
401742
402272
  // Integration settings: passthrough (already gated by getIntegration)
401743
402273
  getIntegrationSettings: (id) => raw.getIntegrationSettings(id),
401744
402274
  setIntegrationSetting: (id, key, value) => raw.setIntegrationSetting(id, key, value),
401745
- setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings),
401746
- // Devices: filter out devices belonging to hidden integrations
401747
- createDevice: (input) => raw.createDevice(input),
401748
- getDevice: async (id) => {
401749
- const d = await raw.getDevice(id);
401750
- if (!d)
401751
- return null;
401752
- const ids = await ensureActiveIds();
401753
- return ids.has(d.integrationId) ? d : null;
401754
- },
401755
- getDeviceByStableId: async (stableId) => {
401756
- const d = await raw.getDeviceByStableId(stableId);
401757
- if (!d)
401758
- return null;
401759
- const ids = await ensureActiveIds();
401760
- return ids.has(d.integrationId) ? d : null;
401761
- },
401762
- listDevices: async (integrationId) => {
401763
- const devices = await raw.listDevices(integrationId);
401764
- const ids = await ensureActiveIds();
401765
- return devices.filter((d) => ids.has(d.integrationId));
401766
- },
401767
- listCameras: async () => {
401768
- const cameras = await raw.listCameras();
401769
- const ids = await ensureActiveIds();
401770
- return cameras.filter((d) => ids.has(d.integrationId));
401771
- },
401772
- updateDevice: (id, updates) => raw.updateDevice(id, updates),
401773
- deleteDevice: (id) => raw.deleteDevice(id),
401774
- // Device settings: passthrough
401775
- getDeviceSettings: (id) => raw.getDeviceSettings(id),
401776
- setDeviceSetting: (id, key, value) => raw.setDeviceSetting(id, key, value),
401777
- setDeviceSettings: (id, settings) => raw.setDeviceSettings(id, settings)
402275
+ setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings)
401778
402276
  };
401779
402277
  }
401780
402278
  }
@@ -406355,6 +406853,7 @@ var require_moleculer_service = __commonJS({
406355
406853
  "use strict";
406356
406854
  Object.defineProperty(exports, "__esModule", { value: true });
406357
406855
  exports.MoleculerService = void 0;
406856
+ exports.childOwnerToken = childOwnerToken;
406358
406857
  exports.buildChildUdsManifest = buildChildUdsManifest;
406359
406858
  var node_crypto_1 = __require("crypto");
406360
406859
  var system_1 = require_dist3();
@@ -406381,6 +406880,21 @@ var require_moleculer_service = __commonJS({
406381
406880
  * `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
406382
406881
  */
406383
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();
406384
406898
  /**
406385
406899
  * Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
406386
406900
  * agent-origin readiness deltas lost while the agent stayed connected.
@@ -406702,14 +407216,18 @@ var require_moleculer_service = __commonJS({
406702
407216
  const hubNodeId = this.brokerSafe.nodeID;
406703
407217
  const childNodeId = `${hubNodeId}/${child.childId}`;
406704
407218
  const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
406705
- this.onRegisterNode(params);
406706
- 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
+ });
406707
407223
  });
406708
- registry.onChildGone((childId) => {
407224
+ registry.onChildGone((childId, incarnation) => {
406709
407225
  const hubNodeId = this.brokerSafe.nodeID;
406710
407226
  const childNodeId = `${hubNodeId}/${childId}`;
406711
- logger.info("UDS child gone \u2014 removing from registry", { meta: { childId } });
406712
- 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));
406713
407231
  });
406714
407232
  registry.onChildLog((childId, entry) => {
406715
407233
  this.logging.writeFromWorker((0, system_1.udsChildLogToWorkerEntry)(childId, entry));
@@ -406893,10 +407411,15 @@ var require_moleculer_service = __commonJS({
406893
407411
  * CapabilityRegistry update is a diff (atomic replace) rather than an
406894
407412
  * unconditional re-register — see `applyNodeManifest` for the rationale.
406895
407413
  */
406896
- onRegisterNode(params) {
407414
+ onRegisterNode(params, owner) {
406897
407415
  const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId);
407416
+ const previousOwner = this.nodeOwners.get(params.nodeId);
406898
407417
  this.nodeRegistry.registerNode(params);
406899
- 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);
406900
407423
  const { nodeId } = params;
406901
407424
  if (nodeId !== "hub" && !nodeId.includes("/") && this.onAgentRegisteredCb) {
406902
407425
  this.onAgentRegisteredCb(nodeId);
@@ -406924,7 +407447,7 @@ var require_moleculer_service = __commonJS({
406924
407447
  * already; this method handles only `params.addons` (system caps).
406925
407448
  * Native-cap wiring into device-manager is done in a later task.
406926
407449
  */
406927
- applyNodeManifest(params, previousManifest) {
407450
+ applyNodeManifest(params, previousManifest, owner, previousOwner) {
406928
407451
  const { nodeId, addons } = params;
406929
407452
  const hubNodeId = this.brokerSafe.nodeID;
406930
407453
  const isLocalChild = nodeId.startsWith(hubNodeId + "/");
@@ -406955,9 +407478,16 @@ var require_moleculer_service = __commonJS({
406955
407478
  for (const [key, { addonId, capName }] of previous) {
406956
407479
  if (desired.has(key))
406957
407480
  continue;
406958
- registry.unregisterProvider(capName, registryKeyFor(addonId));
407481
+ registry.unregisterProvider(capName, registryKeyFor(addonId), previousOwner);
406959
407482
  this.nodeCallFns.delete(`${nodeId}::${capName}`);
406960
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
+ }
406961
407491
  for (const [key, { addonId, capName, capDef }] of desired) {
406962
407492
  if (previous.has(key))
406963
407493
  continue;
@@ -406982,7 +407512,7 @@ var require_moleculer_service = __commonJS({
406982
407512
  for (const methodName of Object.keys((0, system_1.expandCapMethods)(capDef))) {
406983
407513
  proxy[methodName] = (methodParams) => callFn(methodName, methodParams);
406984
407514
  }
406985
- registry.registerProvider(capName, registryKey, proxy);
407515
+ registry.registerProvider(capName, registryKey, proxy, owner);
406986
407516
  if (capDef.mode === "singleton" && isLocalChild) {
406987
407517
  const activeKey = registry.getSingletonAddonId(capName);
406988
407518
  if (activeKey === null || activeKey.includes("@")) {
@@ -407015,7 +407545,14 @@ var require_moleculer_service = __commonJS({
407015
407545
  * Unregisters every cap the node's last manifest declared and emits
407016
407546
  * synthetic readiness-down events for each.
407017
407547
  */
407018
- 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
+ }
407019
407556
  const manifest = this.nodeRegistry.getNodeManifest(nodeId);
407020
407557
  if (!manifest)
407021
407558
  return;
@@ -407029,7 +407566,7 @@ var require_moleculer_service = __commonJS({
407029
407566
  const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`;
407030
407567
  if (registry) {
407031
407568
  for (const capName of capabilities) {
407032
- registry.unregisterProvider(capName, registryKey);
407569
+ registry.unregisterProvider(capName, registryKey, owner);
407033
407570
  }
407034
407571
  }
407035
407572
  for (const capName of capabilities) {
@@ -407050,6 +407587,7 @@ var require_moleculer_service = __commonJS({
407050
407587
  }
407051
407588
  }
407052
407589
  }
407590
+ this.nodeOwners.delete(nodeId);
407053
407591
  this.nodeRegistry.removeNode(nodeId);
407054
407592
  }
407055
407593
  findCallFn(nodeId, capabilityName) {
@@ -407213,6 +407751,9 @@ var require_moleculer_service = __commonJS({
407213
407751
  }
407214
407752
  };
407215
407753
  exports.MoleculerService = MoleculerService;
407754
+ function childOwnerToken(childNodeId, incarnation) {
407755
+ return `${childNodeId}#${incarnation}`;
407756
+ }
407216
407757
  function buildChildUdsManifest(nodeId, childId, caps) {
407217
407758
  const capsByAddon = /* @__PURE__ */ new Map();
407218
407759
  for (const cap of caps) {
@@ -408573,7 +409114,7 @@ var require_manual_boot = __commonJS({
408573
409114
  const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
408574
409115
  const serverUpdateService = new server_update_service_1.ServerUpdateService({
408575
409116
  logger: loggingService.createLogger("ServerUpdate"),
408576
- restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
409117
+ restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
408577
409118
  eventBus: eventBusService,
408578
409119
  updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, "server-update", loggingService.createLogger("UpdateAvailability"))
408579
409120
  });