camstack 1.2.35 → 1.2.37

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-C1Aea3c6.js
23635
+ var require_dist_C1Aea3c6 = __commonJS({
23636
+ "../system/dist/dist-C1Aea3c6.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
@@ -37270,6 +37300,12 @@ var require_dist_Ctfhlrtl = __commonJS({
37270
37300
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
37271
37301
  fetchedAt: zod.z.number()
37272
37302
  });
37303
+ var InferenceDeviceExclusionReasonSchema = zod.z.enum([
37304
+ "disabled",
37305
+ "unavailable",
37306
+ "cannot-host-camera-root",
37307
+ "accelerator-preferred"
37308
+ ]);
37273
37309
  var NodeInferenceDeviceSchema = zod.z.object({
37274
37310
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
37275
37311
  key: zod.z.string(),
@@ -37300,7 +37336,17 @@ var require_dist_Ctfhlrtl = __commonJS({
37300
37336
  * available per format; this is the stored selection that becomes the
37301
37337
  * default for EVERY camera landing on this accelerator.
37302
37338
  */
37303
- steps: zod.z.record(zod.z.string(), DeviceStepConfigSchema).optional()
37339
+ steps: zod.z.record(zod.z.string(), DeviceStepConfigSchema).optional(),
37340
+ /**
37341
+ * `null` when the device IS a camera-root candidate on this node; otherwise
37342
+ * the reason the dispatcher drops it. Computed by the SAME
37343
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
37344
+ * never disagree with the election — deriving it in the UI from
37345
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
37346
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
37347
+ * "an accelerator is serving" predicate).
37348
+ */
37349
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
37304
37350
  });
37305
37351
  var NodeInferenceDevicesSchema = zod.z.object({
37306
37352
  nodeId: zod.z.string(),
@@ -38098,6 +38144,7 @@ var require_dist_Ctfhlrtl = __commonJS({
38098
38144
  isBattery: zod.z.boolean(),
38099
38145
  reason: zod.z.enum([
38100
38146
  "disabled",
38147
+ "offline",
38101
38148
  "sleeping",
38102
38149
  "unreachable",
38103
38150
  "waking"
@@ -38188,9 +38235,17 @@ var require_dist_Ctfhlrtl = __commonJS({
38188
38235
  sleeping: zod.z.boolean(),
38189
38236
  /** Current device state rendered over the cached frame. State images
38190
38237
  * remain authoritative even when their photographic background is
38191
- * old; null means the link must carry a current camera frame. */
38238
+ * old; null means the link must carry a current camera frame.
38239
+ *
38240
+ * **This is the ONLY source a surface may derive an inactive tile
38241
+ * from.** A client that reads a device's `online`/`disabled` flags and
38242
+ * draws its own verdict will disagree with the pixels it is painting
38243
+ * — the frame comes from here, and so must the sentence over it
38244
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
38245
+ * state. */
38192
38246
  stateReason: zod.z.enum([
38193
38247
  "disabled",
38248
+ "offline",
38194
38249
  "sleeping",
38195
38250
  "unreachable",
38196
38251
  "waking"
@@ -42300,7 +42355,35 @@ var require_dist_Ctfhlrtl = __commonJS({
42300
42355
  */
42301
42356
  priority: zod.z.number()
42302
42357
  });
42303
- var GetConnectionEndpointsResultSchema = zod.z.object({ endpoints: zod.z.array(ConnectionEndpointSchema).readonly() });
42358
+ var LocalPortSourceEnum = zod.z.enum([
42359
+ "server-config",
42360
+ "server-env",
42361
+ "caller-hint",
42362
+ "default"
42363
+ ]);
42364
+ var AdvertisedLocalPortSchema = zod.z.object({
42365
+ port: zod.z.number().int().min(1).max(65535),
42366
+ source: LocalPortSourceEnum
42367
+ });
42368
+ var GetConnectionEndpointsResultSchema = zod.z.object({
42369
+ endpoints: zod.z.array(ConnectionEndpointSchema).readonly(),
42370
+ /**
42371
+ * The port the hub built the LAN/loopback URLs with, and where that number
42372
+ * came from.
42373
+ *
42374
+ * Returned rather than merely applied, because "the URL is right" and "the
42375
+ * client can KNOW the URL is right" are different properties. A client that
42376
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
42377
+ * a hub that echoed the port the client sent, so it cannot decide whether to
42378
+ * race the candidate or discard it. With `source` it can: anything but
42379
+ * `caller-hint` is the hub's own socket.
42380
+ *
42381
+ * Absent on hubs predating this field — a client that finds it missing is
42382
+ * talking to an echoing hub and must degrade exactly as it does for
42383
+ * `caller-hint`.
42384
+ */
42385
+ localPort: AdvertisedLocalPortSchema
42386
+ });
42304
42387
  var NotificationEndpointSchema = zod.z.object({
42305
42388
  /** The operator's explicit choice, or null for AUTO. */
42306
42389
  baseUrl: zod.z.string().nullable(),
@@ -42342,10 +42425,35 @@ var require_dist_Ctfhlrtl = __commonJS({
42342
42425
  * Honours `getAllowedAddresses()` when set — addresses outside
42343
42426
  * the allowlist are dropped (the public tunnel + loopback are
42344
42427
  * always included as escape hatches).
42428
+ *
42429
+ * **The port is the hub's, not the caller's** (D62 — a function's fact
42430
+ * belongs to whoever already owns it). This method used to take a `port`
42431
+ * and echo it onto every LAN and loopback URL it advertised, which made
42432
+ * every client a second authority on a socket only the hub binds. A viewer
42433
+ * configured with a port-less public URL (`https://camstack.example.top`)
42434
+ * infers 443 from the scheme, asks with 443, and was handed
42435
+ * `https://192.168.1.9:443` for a hub listening on 4443. That candidate
42436
+ * cannot ever answer, and losing its race reads in the log exactly like
42437
+ * "the LAN was tried and it was slower" — so an operator sitting on his own
42438
+ * WiFi stayed on the public tunnel for weeks, with the LAN candidate born
42439
+ * dead on every single connect.
42440
+ *
42441
+ * The hub now resolves its own listen port and reports it as `localPort`,
42442
+ * with the SOURCE of the number, so a client can tell a fact from an echo.
42443
+ *
42444
+ * Out of scope on purpose: a LAN URL that is not the hub's own socket (a
42445
+ * reverse proxy in front of it on another port) is not this method's to
42446
+ * invent either — pin it with `setNotificationEndpoint` / a configured
42447
+ * origin, which is an operator statement rather than a guess.
42345
42448
  */
42346
42449
  getConnectionEndpoints: method(zod.z.object({
42347
- /** Local hub HTTP port to use in base URLs. */
42348
- port: zod.z.number().int().min(1).max(65535),
42450
+ /**
42451
+ * LEGACY HINT — do not send from new code. Kept optional so clients
42452
+ * written against the echoing contract keep working; the hub uses it
42453
+ * only when it cannot read its own port, and says so via
42454
+ * `localPort.source === 'caller-hint'`.
42455
+ */
42456
+ port: zod.z.number().int().min(1).max(65535).optional(),
42349
42457
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
42350
42458
  * candidate. Default `true`. */
42351
42459
  includeLoopback: zod.z.boolean().optional(),
@@ -55173,7 +55281,6 @@ var require_dist_Ctfhlrtl = __commonJS({
55173
55281
  hitPercent: 60,
55174
55282
  samplingSeconds: 10
55175
55283
  };
55176
- new Map(AUDIO_MACRO_LABELS.flatMap((macro2) => macro2.icon === void 0 ? [] : [[macro2.id, macro2.icon]]));
55177
55284
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
55178
55285
  var TimelapseTemplateSchema = zod.z.object({
55179
55286
  title: zod.z.string().max(500).optional(),
@@ -55284,6 +55391,39 @@ var require_dist_Ctfhlrtl = __commonJS({
55284
55391
  createdAt: zod.z.number(),
55285
55392
  updatedAt: zod.z.number()
55286
55393
  });
55394
+ Object.freeze(Object.fromEntries([{
55395
+ stepId: "face-embedding",
55396
+ label: "Face recognition model",
55397
+ defaultModelId: "arcface-r100",
55398
+ indexName: "the enrolled face gallery",
55399
+ options: [
55400
+ {
55401
+ id: "arcface-r100",
55402
+ label: "ArcFace (ResNet34)"
55403
+ },
55404
+ {
55405
+ id: "auraface-r100",
55406
+ label: "AuraFace R100"
55407
+ },
55408
+ {
55409
+ id: "inception-resnet-v1",
55410
+ label: "Inception ResNet V1"
55411
+ }
55412
+ ]
55413
+ }, {
55414
+ stepId: "clip-embedding",
55415
+ label: "Semantic search model",
55416
+ defaultModelId: "mobileclip-s1",
55417
+ indexName: "the object semantic-search index",
55418
+ options: [{
55419
+ id: "mobileclip-s1",
55420
+ label: "MobileCLIP S1"
55421
+ }, {
55422
+ id: "mobileclip-s2",
55423
+ label: "MobileCLIP S2"
55424
+ }]
55425
+ }].map((s) => [s.stepId, s.defaultModelId])));
55426
+ zod.z.string().min(1);
55287
55427
  zod.z.object({
55288
55428
  /**
55289
55429
  * Fraction of the box's own size added on EACH side before cutting.
@@ -55304,10 +55444,17 @@ var require_dist_Ctfhlrtl = __commonJS({
55304
55444
  */
55305
55445
  square: zod.z.boolean()
55306
55446
  });
55307
- ({
55447
+ var DEFAULT_DETAIL_CROP_CONVENTION = {
55308
55448
  paddingRatio: 0.15,
55309
55449
  square: false
55310
- }).paddingRatio;
55450
+ };
55451
+ Object.freeze({
55452
+ x: 0,
55453
+ y: 0,
55454
+ w: 1,
55455
+ h: 1
55456
+ });
55457
+ DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
55311
55458
  var NativeLeaseAdmissionSchema = zod.z.enum(["all", "inferred"]);
55312
55459
  zod.z.object({
55313
55460
  /**
@@ -56108,7 +56255,7 @@ var require_alerts_addon = __commonJS({
56108
56255
  [Symbol.toStringTag]: { value: "Module" }
56109
56256
  });
56110
56257
  require_chunk_Cek0wNdY();
56111
- var require_dist10 = require_dist_Ctfhlrtl();
56258
+ var require_dist10 = require_dist_C1Aea3c6();
56112
56259
  function selectExpired(alerts, cutoffMs) {
56113
56260
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
56114
56261
  }
@@ -56921,7 +57068,7 @@ var require_console_logging = __commonJS({
56921
57068
  [Symbol.toStringTag]: { value: "Module" }
56922
57069
  });
56923
57070
  require_chunk_Cek0wNdY();
56924
- var require_dist10 = require_dist_Ctfhlrtl();
57071
+ var require_dist10 = require_dist_C1Aea3c6();
56925
57072
  var require_formatter = require_formatter_DqAKDlvN();
56926
57073
  var LEVEL_RANK = {
56927
57074
  debug: 0,
@@ -57015,7 +57162,7 @@ var require_core_blocks_addon = __commonJS({
57015
57162
  "use strict";
57016
57163
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
57017
57164
  var require_chunk = require_chunk_Cek0wNdY();
57018
- var require_dist10 = require_dist_Ctfhlrtl();
57165
+ var require_dist10 = require_dist_C1Aea3c6();
57019
57166
  var node_crypto = __require("crypto");
57020
57167
  var node_fs_promises = __require("fs/promises");
57021
57168
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -57912,6 +58059,230 @@ var require_core_blocks = __commonJS({
57912
58059
  }
57913
58060
  });
57914
58061
 
58062
+ // ../system/dist/retired-settings-keys-B52KsNz8.js
58063
+ var require_retired_settings_keys_B52KsNz8 = __commonJS({
58064
+ "../system/dist/retired-settings-keys-B52KsNz8.js"(exports) {
58065
+ "use strict";
58066
+ var require_dist10 = require_dist_C1Aea3c6();
58067
+ function settingsStoreIsAuthoritativeHere(env) {
58068
+ const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
58069
+ return raw === "" || raw === "hub";
58070
+ }
58071
+ var RETIRED_SETTINGS_KEYS = [{
58072
+ namespace: "detection-pipeline",
58073
+ collection: "addon-settings",
58074
+ row: "root",
58075
+ keys: ["pipelineSteps", "pipelineEngine"],
58076
+ perNodeKeys: [
58077
+ "engineRuntime",
58078
+ "engineBackend",
58079
+ "engineDevice",
58080
+ "probedBestEngine"
58081
+ ],
58082
+ reason: "global step/engine seed and the per-node engine cascade \u2014 both removed; the live per-camera dispatch path never read either"
58083
+ }];
58084
+ var RETIRED_SETTINGS_ROWS = [
58085
+ {
58086
+ collection: "addon-settings",
58087
+ row: "device-manager.deviceMeta",
58088
+ owner: "device-manager",
58089
+ evidence: { collection: "device-manager:devices" },
58090
+ 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"
58091
+ },
58092
+ {
58093
+ collection: "addon-settings",
58094
+ row: "device-manager.deviceMetadata",
58095
+ owner: "device-manager",
58096
+ evidence: { collection: "device-manager:devices" },
58097
+ reason: "hardware-identity metadata is the `metadata` column of the device row; the separate blob existed only because a blob write is whole-value"
58098
+ },
58099
+ {
58100
+ collection: "addon-settings",
58101
+ row: "device-manager.deviceIndex",
58102
+ owner: "device-manager",
58103
+ evidence: { collection: "device-manager:devices" },
58104
+ reason: "derivable: `addonId`/`stableId` are indexed columns, and index membership is the `registered` column"
58105
+ }
58106
+ ];
58107
+ function retiredKeyStoreOf(backend) {
58108
+ return {
58109
+ async read(spec) {
58110
+ return require_dist10.asJsonObject(await backend.get({
58111
+ namespace: spec.namespace,
58112
+ collection: spec.collection,
58113
+ key: spec.row
58114
+ }));
58115
+ },
58116
+ async write(spec, value) {
58117
+ await backend.set({
58118
+ namespace: spec.namespace,
58119
+ collection: spec.collection,
58120
+ key: spec.row,
58121
+ value
58122
+ });
58123
+ }
58124
+ };
58125
+ }
58126
+ function planNestedRetiredKeyPurge(blob, spec) {
58127
+ const nestedKeys = spec.mapValueKeys ?? [];
58128
+ if (nestedKeys.length === 0) return [];
58129
+ const found = [];
58130
+ for (const [mapKey, value] of Object.entries(blob)) {
58131
+ const inner = require_dist10.asJsonObject(value);
58132
+ if (inner === null) continue;
58133
+ for (const retired of nestedKeys) if (retired in inner) found.push(`${mapKey}.${retired}`);
58134
+ }
58135
+ return found;
58136
+ }
58137
+ function planRetiredKeyPurge(blob, spec) {
58138
+ const exact = new Set(spec.keys);
58139
+ const found = [];
58140
+ for (const key of Object.keys(blob)) {
58141
+ if (exact.has(key)) {
58142
+ found.push(key);
58143
+ continue;
58144
+ }
58145
+ for (const perNode of spec.perNodeKeys) if (key === perNode || key.startsWith(`${perNode}@`)) {
58146
+ found.push(key);
58147
+ break;
58148
+ }
58149
+ }
58150
+ return found;
58151
+ }
58152
+ async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
58153
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
58154
+ const results = [];
58155
+ for (const spec of specs) {
58156
+ let blob;
58157
+ try {
58158
+ blob = await store.read(spec);
58159
+ } catch (err) {
58160
+ logger.warn("retired settings keys \u2014 read failed, row left untouched", { meta: {
58161
+ addon: spec.namespace,
58162
+ collection: spec.collection,
58163
+ row: spec.row,
58164
+ error: err instanceof Error ? err.message : String(err)
58165
+ } });
58166
+ continue;
58167
+ }
58168
+ if (blob === null) continue;
58169
+ const doomed = planRetiredKeyPurge(blob, spec);
58170
+ const doomedNested = planNestedRetiredKeyPurge(blob, spec);
58171
+ if (doomed.length === 0 && doomedNested.length === 0) continue;
58172
+ const doomedSet = new Set(doomed);
58173
+ const nestedByMapKey = /* @__PURE__ */ new Map();
58174
+ for (const path of doomedNested) {
58175
+ const cut = path.indexOf(".");
58176
+ const mapKey = path.slice(0, cut);
58177
+ const inner = path.slice(cut + 1);
58178
+ const bucket = nestedByMapKey.get(mapKey) ?? /* @__PURE__ */ new Set();
58179
+ bucket.add(inner);
58180
+ nestedByMapKey.set(mapKey, bucket);
58181
+ }
58182
+ const next = Object.fromEntries(Object.entries(blob).filter(([key]) => !doomedSet.has(key)).map(([key, value]) => {
58183
+ const retired = nestedByMapKey.get(key);
58184
+ if (retired === void 0) return [key, value];
58185
+ const inner = require_dist10.asJsonObject(value);
58186
+ if (inner === null) return [key, value];
58187
+ return [key, Object.fromEntries(Object.entries(inner).filter(([k]) => !retired.has(k)))];
58188
+ }));
58189
+ await store.write(spec, next);
58190
+ const result = {
58191
+ addon: spec.namespace,
58192
+ collection: spec.collection,
58193
+ row: spec.row,
58194
+ keys: doomed,
58195
+ nested: doomedNested
58196
+ };
58197
+ results.push(result);
58198
+ logger.info(`purged ${doomed.length + doomedNested.length} retired settings keys`, { meta: {
58199
+ ...result,
58200
+ reason: spec.reason
58201
+ } });
58202
+ }
58203
+ return results;
58204
+ }
58205
+ async function purgeRetiredSettingsRows(store, logger, specs = RETIRED_SETTINGS_ROWS, env = process.env) {
58206
+ if (!settingsStoreIsAuthoritativeHere(env)) return [];
58207
+ const results = [];
58208
+ const evidence = /* @__PURE__ */ new Map();
58209
+ for (const spec of specs) {
58210
+ let present;
58211
+ try {
58212
+ present = await store.hasRow(spec);
58213
+ } catch (err) {
58214
+ logger.warn("retired settings row \u2014 read failed, row left untouched", { meta: {
58215
+ addon: spec.owner,
58216
+ collection: spec.collection,
58217
+ row: spec.row,
58218
+ error: errText(err)
58219
+ } });
58220
+ continue;
58221
+ }
58222
+ if (!present) continue;
58223
+ const successor = spec.evidence.collection;
58224
+ let count = evidence.get(successor);
58225
+ if (count === void 0) {
58226
+ try {
58227
+ count = await store.countRows(successor);
58228
+ } catch (err) {
58229
+ logger.warn("retired settings row \u2014 successor unreadable, row left untouched", { meta: {
58230
+ addon: spec.owner,
58231
+ row: spec.row,
58232
+ successor,
58233
+ error: errText(err)
58234
+ } });
58235
+ count = 0;
58236
+ }
58237
+ evidence.set(successor, count);
58238
+ }
58239
+ if (count === 0) {
58240
+ logger.warn("retired settings row kept \u2014 its successor is empty, migration has not run", { meta: {
58241
+ addon: spec.owner,
58242
+ collection: spec.collection,
58243
+ row: spec.row,
58244
+ successor
58245
+ } });
58246
+ continue;
58247
+ }
58248
+ await store.deleteRow(spec);
58249
+ const result = {
58250
+ owner: spec.owner,
58251
+ collection: spec.collection,
58252
+ row: spec.row
58253
+ };
58254
+ results.push(result);
58255
+ logger.info("purged a retired settings row", { meta: {
58256
+ ...result,
58257
+ reason: spec.reason
58258
+ } });
58259
+ }
58260
+ return results;
58261
+ }
58262
+ function errText(err) {
58263
+ return err instanceof Error ? err.message : String(err);
58264
+ }
58265
+ Object.defineProperty(exports, "purgeRetiredSettingsKeys", {
58266
+ enumerable: true,
58267
+ get: function() {
58268
+ return purgeRetiredSettingsKeys;
58269
+ }
58270
+ });
58271
+ Object.defineProperty(exports, "purgeRetiredSettingsRows", {
58272
+ enumerable: true,
58273
+ get: function() {
58274
+ return purgeRetiredSettingsRows;
58275
+ }
58276
+ });
58277
+ Object.defineProperty(exports, "retiredKeyStoreOf", {
58278
+ enumerable: true,
58279
+ get: function() {
58280
+ return retiredKeyStoreOf;
58281
+ }
58282
+ });
58283
+ }
58284
+ });
58285
+
57915
58286
  // ../types/dist/canonical-hash-DNV8S5ET.js
57916
58287
  var require_canonical_hash_DNV8S5ET = __commonJS({
57917
58288
  "../types/dist/canonical-hash-DNV8S5ET.js"(exports) {
@@ -59735,7 +60106,8 @@ var require_device_manager_addon = __commonJS({
59735
60106
  [Symbol.toStringTag]: { value: "Module" }
59736
60107
  });
59737
60108
  require_chunk_Cek0wNdY();
59738
- var require_dist10 = require_dist_Ctfhlrtl();
60109
+ var require_dist10 = require_dist_C1Aea3c6();
60110
+ var require_retired_settings_keys = require_retired_settings_keys_B52KsNz8();
59739
60111
  var node_crypto = __require("crypto");
59740
60112
  var _camstack_types_node = require_node();
59741
60113
  var JOB_HISTORY = 20;
@@ -60023,6 +60395,311 @@ var require_device_manager_addon = __commonJS({
60023
60395
  }
60024
60396
  }
60025
60397
  };
60398
+ function tagContribution(contribution, capName, addonId, kind) {
60399
+ const source = kind === "settings" ? "settings" : "live";
60400
+ return {
60401
+ ...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
60402
+ sections: contribution.sections.map((section) => ({
60403
+ ...section,
60404
+ fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
60405
+ }))
60406
+ };
60407
+ }
60408
+ function isFieldRecord(value) {
60409
+ return value !== null && typeof value === "object" && !Array.isArray(value);
60410
+ }
60411
+ function toWireShape(input) {
60412
+ const out = { sections: input.sections.map((s) => ({
60413
+ id: s.id,
60414
+ title: s.title,
60415
+ ...s.description !== void 0 ? { description: s.description } : {},
60416
+ ...s.style !== void 0 ? { style: s.style } : {},
60417
+ ...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
60418
+ ...s.columns !== void 0 ? { columns: s.columns } : {},
60419
+ ...s.tab !== void 0 ? { tab: s.tab } : {},
60420
+ ...s.location !== void 0 ? { location: s.location } : {},
60421
+ ...s.order !== void 0 ? { order: s.order } : {},
60422
+ fields: [...s.fields]
60423
+ })) };
60424
+ if (input.tabs) out.tabs = [...input.tabs];
60425
+ return out;
60426
+ }
60427
+ function tagField(field, capName, addonId, source, kind) {
60428
+ if (!isFieldRecord(field)) return field;
60429
+ const f = field;
60430
+ const structuralTypes = /* @__PURE__ */ new Set([
60431
+ "separator",
60432
+ "info",
60433
+ "button"
60434
+ ]);
60435
+ if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
60436
+ const tagged = {
60437
+ ...f,
60438
+ source
60439
+ };
60440
+ if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
60441
+ else {
60442
+ tagged.writerCapName = capName;
60443
+ tagged.writerAddonId = addonId;
60444
+ }
60445
+ if (f.type === "group") {
60446
+ const children = Array.isArray(f.fields) ? f.fields : [];
60447
+ if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
60448
+ } else if (f.type === "sub-tabs") {
60449
+ const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
60450
+ if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
60451
+ if (!isFieldRecord(tab)) return tab;
60452
+ const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
60453
+ return {
60454
+ ...tab,
60455
+ fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
60456
+ };
60457
+ });
60458
+ }
60459
+ return tagged;
60460
+ }
60461
+ function mergeAggregates(parts) {
60462
+ const tabDecls = /* @__PURE__ */ new Map();
60463
+ const sections = [];
60464
+ const seenSectionIds = /* @__PURE__ */ new Set();
60465
+ for (const part of parts) {
60466
+ if (part.tabs) {
60467
+ for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
60468
+ }
60469
+ for (const s of part.sections) {
60470
+ if (s.id !== void 0) {
60471
+ if (seenSectionIds.has(s.id)) continue;
60472
+ seenSectionIds.add(s.id);
60473
+ }
60474
+ sections.push(s);
60475
+ }
60476
+ }
60477
+ for (const s of sections) {
60478
+ const tabId = s.tab ?? "general";
60479
+ if (tabDecls.has(tabId)) continue;
60480
+ const known = require_dist10.WELL_KNOWN_TAB_MAP[tabId];
60481
+ if (known) tabDecls.set(tabId, {
60482
+ id: known.id,
60483
+ label: known.label,
60484
+ icon: known.icon,
60485
+ order: known.order
60486
+ });
60487
+ else tabDecls.set(tabId, {
60488
+ id: tabId,
60489
+ label: tabId,
60490
+ icon: "wrench",
60491
+ order: 100
60492
+ });
60493
+ }
60494
+ sections.sort((a, b) => {
60495
+ const tabA = a.tab ?? "general";
60496
+ const tabB = b.tab ?? "general";
60497
+ if (tabA !== tabB) {
60498
+ const orderA = tabDecls.get(tabA)?.order ?? 100;
60499
+ const orderB = tabDecls.get(tabB)?.order ?? 100;
60500
+ if (orderA !== orderB) return orderA - orderB;
60501
+ return tabA.localeCompare(tabB);
60502
+ }
60503
+ return (a.order ?? 0) - (b.order ?? 0);
60504
+ });
60505
+ const sortedTabs = [...tabDecls.values()].toSorted((a, b) => (a.order ?? 100) - (b.order ?? 100));
60506
+ const out = { sections };
60507
+ if (sortedTabs.length > 0) out.tabs = sortedTabs;
60508
+ return out;
60509
+ }
60510
+ function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
60511
+ const localNodeId = ctx.kernel.localNodeId ?? "hub";
60512
+ ctx.eventBus.subscribe({ category: require_dist10.EventCategory.DeviceBindingsChanged }, (event) => {
60513
+ const { deviceId, capName, reason, addonId, nodeId } = event.data;
60514
+ if (nodeId === localNodeId) return;
60515
+ if (reason === "native-registered") {
60516
+ let perDevice = remoteNativeCaps.get(deviceId);
60517
+ if (!perDevice) {
60518
+ perDevice = /* @__PURE__ */ new Map();
60519
+ remoteNativeCaps.set(deviceId, perDevice);
60520
+ }
60521
+ perDevice.set(capName, {
60522
+ addonId,
60523
+ nodeId
60524
+ });
60525
+ } else if (reason === "native-unregistered") {
60526
+ const perDevice = remoteNativeCaps.get(deviceId);
60527
+ if (!perDevice) return;
60528
+ perDevice.delete(capName);
60529
+ if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
60530
+ }
60531
+ });
60532
+ const cluster = ctx.kernel.cluster;
60533
+ if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
60534
+ const gone = payload.node.id;
60535
+ const emptyDevices = [];
60536
+ for (const [deviceId, perDevice] of remoteNativeCaps) {
60537
+ const toDelete = [];
60538
+ for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
60539
+ for (const capName of toDelete) perDevice.delete(capName);
60540
+ if (perDevice.size === 0) emptyDevices.push(deviceId);
60541
+ }
60542
+ for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
60543
+ });
60544
+ }
60545
+ async function readBindingsStore(deps) {
60546
+ return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
60547
+ }
60548
+ async function writeBindingsStore(deps, next) {
60549
+ await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
60550
+ }
60551
+ function resolveWrapperNodeId(_wrapperAddonId) {
60552
+ return "hub";
60553
+ }
60554
+ function toRoutableProviderNodeId(nodeId) {
60555
+ const slash = nodeId.indexOf("/");
60556
+ return slash === -1 ? nodeId : nodeId.slice(0, slash);
60557
+ }
60558
+ function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
60559
+ const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
60560
+ if (!clusterCaps) return null;
60561
+ for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
60562
+ addonId: entry.addonId,
60563
+ nodeId: entry.nodeId
60564
+ };
60565
+ return null;
60566
+ }
60567
+ function resolveDeviceType(deps, row, deviceId) {
60568
+ const persisted = row?.meta.type;
60569
+ if (typeof persisted === "string" && persisted.length > 0) return persisted;
60570
+ const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
60571
+ return typeof live === "string" && live.length > 0 ? live : void 0;
60572
+ }
60573
+ async function resolveDevicePresence(deps, row, deviceId) {
60574
+ if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
60575
+ if (row !== null) return "present";
60576
+ return await deps.rows.count() > 0 ? "absent" : "unknown";
60577
+ }
60578
+ function capAppliesToDeviceType(def, deviceType) {
60579
+ if (deviceType === void 0) return true;
60580
+ const declared = def?.deviceTypes;
60581
+ if (!declared || declared.length === 0) return true;
60582
+ return declared.some((t) => t === deviceType);
60583
+ }
60584
+ async function getBindings(deps, input) {
60585
+ const storeKey = String(input.deviceId);
60586
+ const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
60587
+ const row = await deps.rows.get(input.deviceId);
60588
+ const deviceType = resolveDeviceType(deps, row, input.deviceId);
60589
+ const presence = await resolveDevicePresence(deps, row, input.deviceId);
60590
+ const entries = [];
60591
+ const seenCaps = /* @__PURE__ */ new Set();
60592
+ const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
60593
+ for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
60594
+ const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
60595
+ const remoteNative = resolveRemote(capName);
60596
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
60597
+ const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
60598
+ if (wrapperAddonId === null && !nativeAddonId) {
60599
+ seenCaps.add(capName);
60600
+ continue;
60601
+ }
60602
+ entries.push({
60603
+ capName,
60604
+ kind: wrapperAddonId ? "wrapped" : "native",
60605
+ providerAddonId: wrapperAddonId ?? nativeAddonId,
60606
+ providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
60607
+ nativeAddonId
60608
+ });
60609
+ seenCaps.add(capName);
60610
+ }
60611
+ if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device \u2014 returning none", { tags: { deviceId: input.deviceId } });
60612
+ else if (deps.capabilityRegistry) {
60613
+ const skippedForType = [];
60614
+ for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
60615
+ if (seenCaps.has(capName)) continue;
60616
+ if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
60617
+ skippedForType.push(capName);
60618
+ continue;
60619
+ }
60620
+ const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
60621
+ if (!defaultWrapperAddonId) continue;
60622
+ const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
60623
+ const remoteNative = resolveRemote(capName);
60624
+ const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
60625
+ entries.push({
60626
+ capName,
60627
+ kind: "wrapped",
60628
+ providerAddonId: defaultWrapperAddonId,
60629
+ providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
60630
+ nativeAddonId
60631
+ });
60632
+ seenCaps.add(capName);
60633
+ }
60634
+ if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped \u2014 deviceTypes mismatch", {
60635
+ tags: { deviceId: input.deviceId },
60636
+ meta: {
60637
+ deviceType,
60638
+ skipped: skippedForType
60639
+ }
60640
+ });
60641
+ }
60642
+ if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
60643
+ if (seenCaps.has(capName)) continue;
60644
+ const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
60645
+ entries.push({
60646
+ capName,
60647
+ kind: "native",
60648
+ providerAddonId: nativeAddonId,
60649
+ providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
60650
+ nativeAddonId
60651
+ });
60652
+ seenCaps.add(capName);
60653
+ }
60654
+ const pushFed = deps.remoteNativeCaps.get(input.deviceId);
60655
+ if (pushFed) for (const [capName, info] of pushFed) {
60656
+ if (seenCaps.has(capName)) continue;
60657
+ entries.push({
60658
+ capName,
60659
+ kind: "native",
60660
+ providerAddonId: info.addonId,
60661
+ providerNodeId: toRoutableProviderNodeId(info.nodeId),
60662
+ nativeAddonId: info.addonId
60663
+ });
60664
+ seenCaps.add(capName);
60665
+ }
60666
+ const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
60667
+ if (clusterCaps) for (const entry of clusterCaps) {
60668
+ if (entry.deviceId !== input.deviceId) continue;
60669
+ if (seenCaps.has(entry.capName)) continue;
60670
+ if (!entry.addonId) continue;
60671
+ const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
60672
+ if (entry.nodeId === localNodeId) continue;
60673
+ entries.push({
60674
+ capName: entry.capName,
60675
+ kind: "native",
60676
+ providerAddonId: entry.addonId,
60677
+ providerNodeId: toRoutableProviderNodeId(entry.nodeId),
60678
+ nativeAddonId: entry.addonId
60679
+ });
60680
+ seenCaps.add(entry.capName);
60681
+ }
60682
+ return {
60683
+ deviceId: input.deviceId,
60684
+ entries
60685
+ };
60686
+ }
60687
+ async function getAllBindings(deps) {
60688
+ const ids = /* @__PURE__ */ new Set();
60689
+ for (const row of await deps.rows.listAll()) ids.add(row.meta.id);
60690
+ const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
60691
+ for (const device of registered) ids.add(device.id);
60692
+ if (ids.size === 0) {
60693
+ deps.ctx.logger.warn("getAllBindings found no devices \u2014 warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
60694
+ return [];
60695
+ }
60696
+ const out = [];
60697
+ for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
60698
+ return out;
60699
+ }
60700
+ async function lookupPersistedStableId(deps, deviceId) {
60701
+ return (await deps.rows.get(deviceId))?.meta.stableId;
60702
+ }
60026
60703
  var MODE_LABELS = {
60027
60704
  auto: "Auto",
60028
60705
  day: "Day",
@@ -60345,118 +61022,6 @@ var require_device_manager_addon = __commonJS({
60345
61022
  if (reducer.kind !== "settings") throw new Error(`device-config: builderId "${builderId}" is not a settings-based derived-form`);
60346
61023
  return reducer.parsePatch(patch);
60347
61024
  }
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
61025
  function isTransientMoleculerError(err) {
60461
61026
  if (err !== null && typeof err === "object") {
60462
61027
  const e = err;
@@ -60548,22 +61113,15 @@ var require_device_manager_addon = __commonJS({
60548
61113
  }
60549
61114
  async function listPersistedByAddon(pctx, input) {
60550
61115
  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
- });
61116
+ return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
61117
+ id: m.id,
61118
+ stableId: m.stableId,
61119
+ type: m.type,
61120
+ name: m.name,
61121
+ location: m.location ?? null,
61122
+ disabled: m.disabled ?? false,
61123
+ parentDeviceId: m.parentDeviceId
61124
+ }));
60567
61125
  }
60568
61126
  async function listAll(pctx, input) {
60569
61127
  const ownerFilter = Reflect.get(input, "addonId");
@@ -60573,7 +61131,9 @@ var require_device_manager_addon = __commonJS({
60573
61131
  const camerasOnly = input.isCamera === true;
60574
61132
  const results = [];
60575
61133
  const seen = /* @__PURE__ */ new Set();
60576
- const { meta, metadata: metadataMap, index } = await pctx.metaStore.readAll();
61134
+ const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listAll();
61135
+ const rowById = /* @__PURE__ */ new Map();
61136
+ for (const row of fleet) rowById.set(row.meta.id, row);
60577
61137
  if (pctx.registry) {
60578
61138
  const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
60579
61139
  addonId,
@@ -60581,7 +61141,8 @@ var require_device_manager_addon = __commonJS({
60581
61141
  })) : pctx.registry.getAllWithAddonId();
60582
61142
  for (const { addonId: aid, device } of liveEntries) {
60583
61143
  const key = String(device.id);
60584
- const info = toDeviceInfo(aid, device, metadataMap[key] ?? null, meta[key] ?? null);
61144
+ const row = rowById.get(device.id);
61145
+ const info = toDeviceInfo(aid, device, row?.metadata ?? null, row?.meta ?? null);
60585
61146
  seen.add(key);
60586
61147
  if (camerasOnly && !info.isCamera) continue;
60587
61148
  results.push(slim ? {
@@ -60591,38 +61152,36 @@ var require_device_manager_addon = __commonJS({
60591
61152
  } : info);
60592
61153
  }
60593
61154
  }
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;
61155
+ for (const row of fleet) {
61156
+ const m = row.meta;
61157
+ const aid = m.addonId;
61158
+ const stableId = m.stableId;
61159
+ if (seen.has(String(m.id))) continue;
60601
61160
  const persistedType = m.type;
60602
61161
  if (camerasOnly && persistedType !== require_dist10.DeviceType.Camera) continue;
60603
61162
  const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
60604
- const metadata = slim ? null : metadataMap[key] ?? null;
61163
+ const metadata = slim ? null : row.metadata;
60605
61164
  results.push({
60606
61165
  id: m.id,
60607
61166
  stableId,
60608
61167
  addonId: aid,
60609
61168
  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),
61169
+ name: m.name,
61170
+ location: m.location ?? null,
61171
+ disabled: m.disabled ?? false,
61172
+ parentDeviceId: m.parentDeviceId ?? null,
61173
+ role: toDeviceRole(m.role),
60615
61174
  online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
60616
61175
  probed: pctx.host.resolveDeviceProbed(m.id),
60617
- features: persistedFeatures(m?.features),
61176
+ features: persistedFeatures(m.features),
60618
61177
  isCamera: persistedType === require_dist10.DeviceType.Camera,
60619
61178
  config: persistedConfig ?? {},
60620
61179
  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 } : {},
61180
+ ...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
61181
+ ...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
61182
+ ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
61183
+ ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
61184
+ ...m.display !== void 0 ? { display: m.display } : {},
60626
61185
  ...(() => {
60627
61186
  if (slim) return {};
60628
61187
  const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
@@ -60637,19 +61196,15 @@ var require_device_manager_addon = __commonJS({
60637
61196
  if (pctx.registry) {
60638
61197
  const found = resolveDeviceById(pctx.registry, deviceId);
60639
61198
  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);
61199
+ const row2 = await pctx.metaStore.getRow(found.device.id);
61200
+ return toDeviceInfo(found.addonId, found.device, row2?.metadata ?? null, row2?.meta ?? null);
60645
61201
  }
60646
61202
  }
60647
- const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
60648
- if (!persisted) return null;
60649
- const { addonId: aid, stableId, meta: m } = persisted;
61203
+ const row = await pctx.metaStore.getRow(deviceId);
61204
+ if (row === null) return null;
61205
+ const { meta: m, metadata } = row;
61206
+ const { addonId: aid, stableId } = m;
60650
61207
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60651
- const key = String(deviceId);
60652
- const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
60653
61208
  const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
60654
61209
  return {
60655
61210
  id: deviceId,
@@ -60688,26 +61243,25 @@ var require_device_manager_addon = __commonJS({
60688
61243
  }
60689
61244
  const results = [];
60690
61245
  const seen = /* @__PURE__ */ new Set();
60691
- const { index, meta, metadata: metadataMap } = await pctx.metaStore.readAll();
61246
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
61247
+ const rowById = /* @__PURE__ */ new Map();
61248
+ for (const row of childRows) rowById.set(row.meta.id, row);
60692
61249
  if (pctx.registry) {
60693
61250
  const liveChildren = pctx.registry.getChildren(parentDeviceId);
60694
61251
  for (const device of liveChildren) {
60695
61252
  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));
61253
+ const row = rowById.get(device.id);
61254
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
60699
61255
  seen.add(key);
60700
61256
  }
60701
61257
  }
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);
61258
+ for (const row of childRows) {
61259
+ const m = row.meta;
61260
+ const childStableId = m.stableId;
60707
61261
  const key = String(m.id);
60708
61262
  if (seen.has(key)) continue;
60709
61263
  const persistedConfig = await pctx.settings.readDeviceStore(m.id);
60710
- const metadata = metadataMap[key] ?? null;
61264
+ const metadata = row.metadata;
60711
61265
  const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
60712
61266
  results.push({
60713
61267
  id: m.id,
@@ -60851,16 +61405,10 @@ var require_device_manager_addon = __commonJS({
60851
61405
  }
60852
61406
  async function removeByIntegration(pctx, input) {
60853
61407
  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
- });
61408
+ const parents = (await pctx.metaStore.rows.listByIntegration(integrationId)).filter((row) => row.meta.parentDeviceId === null);
60859
61409
  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 });
61410
+ for (const parent of parents) {
61411
+ await pctx.provider.remove({ deviceId: parent.meta.id });
60864
61412
  removed++;
60865
61413
  }
60866
61414
  return { removed };
@@ -61249,204 +61797,6 @@ var require_device_manager_addon = __commonJS({
61249
61797
  driverPatch
61250
61798
  };
61251
61799
  }
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
61800
  async function getDeviceAggregate(deps, deviceId, kind) {
61451
61801
  const registry = deps.capabilityRegistry;
61452
61802
  if (!registry) {
@@ -62003,7 +62353,7 @@ var require_device_manager_addon = __commonJS({
62003
62353
  if (wireable) caps.push(wireable);
62004
62354
  }
62005
62355
  if (input.includeSynthesizable === true) {
62006
- const deviceType = ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.type;
62356
+ const deviceType = (await deps.bindingsDeps.rows.get(deviceId))?.meta.type;
62007
62357
  if (deviceType !== void 0) for (const def of require_dist10.ALL_CAPABILITY_DEFINITIONS) {
62008
62358
  if (seen.has(def.name)) continue;
62009
62359
  if (def.scope !== "device" || def.kind === "wrapper") continue;
@@ -62091,20 +62441,10 @@ var require_device_manager_addon = __commonJS({
62091
62441
  return chain;
62092
62442
  }
62093
62443
  };
62094
- async function stampIntegrationId(metaStore, settings, ctx, deviceId, integrationId) {
62444
+ async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
62095
62445
  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
- } });
62446
+ if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
62447
+ await metaStore.rows.patch(deviceId, { integrationId });
62108
62448
  });
62109
62449
  ctx.eventBus.emit({
62110
62450
  id: (0, node_crypto.randomUUID)(),
@@ -62124,70 +62464,54 @@ var require_device_manager_addon = __commonJS({
62124
62464
  async function allocateDeviceId(pctx, input) {
62125
62465
  const { addonId, stableId } = input;
62126
62466
  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 };
62467
+ const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
62468
+ if (existing) return { id: existing.meta.id };
62130
62469
  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
- } });
62470
+ await pctx.metaStore.rows.upsert({
62471
+ addonId,
62472
+ stableId,
62473
+ type: "generic",
62474
+ name: stableId,
62475
+ location: null,
62476
+ disabled: false,
62477
+ parentDeviceId: null,
62478
+ id
62479
+ }, {
62480
+ registered: false,
62481
+ metadata: null
62482
+ });
62144
62483
  return { id };
62145
62484
  });
62146
62485
  }
62147
62486
  async function registerDevice(pctx, input) {
62148
62487
  const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
62149
- const key = String(id);
62150
62488
  const featuresArr = Array.isArray(features) ? [...features] : [];
62151
62489
  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];
62490
+ const existingRow = await pctx.metaStore.rows.get(id);
62491
+ const existingMeta = existingRow?.meta;
62492
+ const wasRegistered = existingRow?.registered ?? false;
62161
62493
  const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
62162
62494
  const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
62163
- const isFirst = !existingMeta || !wasInIndex;
62495
+ const isFirst = !existingMeta || !wasRegistered;
62164
62496
  const fingerprint = (0, _camstack_types_node.canonicalDeviceFingerprint)({
62165
62497
  deviceId: id,
62166
62498
  deviceType: type,
62167
62499
  features: featuresArr
62168
62500
  });
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
- } });
62501
+ await pctx.metaStore.rows.upsertRegistration({
62502
+ deviceId: id,
62503
+ addonId,
62504
+ stableId,
62505
+ type,
62506
+ name: resolvedName,
62507
+ userNamed: wasUserNamed,
62508
+ location: existingMeta?.location ?? null,
62509
+ disabled: existingMeta?.disabled ?? false,
62510
+ parentDeviceId: parentDeviceId ?? null,
62511
+ registered: true,
62512
+ features: featuresArr,
62513
+ exportFingerprint: fingerprint
62514
+ });
62191
62515
  return {
62192
62516
  isFirstRegistration: isFirst,
62193
62517
  exportFingerprint: fingerprint,
@@ -62247,26 +62571,9 @@ var require_device_manager_addon = __commonJS({
62247
62571
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62248
62572
  if (!persisted) return;
62249
62573
  const { addonId, stableId, meta: persistedMeta } = persisted;
62250
- const key = String(deviceId);
62251
62574
  const deviceName = persistedMeta.name;
62252
62575
  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
- }
62576
+ await pctx.metaStore.rows.remove(deviceId);
62270
62577
  });
62271
62578
  await pctx.settings.clearDeviceStore(deviceId);
62272
62579
  await pctx.settings.clearDeviceRuntimeState(deviceId);
@@ -62310,11 +62617,10 @@ var require_device_manager_addon = __commonJS({
62310
62617
  }
62311
62618
  async function loadMeta(pctx, input) {
62312
62619
  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;
62620
+ const row = await pctx.metaStore.getRow(deviceId);
62621
+ if (row === null) return null;
62622
+ const { meta: m, metadata } = row;
62623
+ const { addonId, stableId } = m;
62318
62624
  return {
62319
62625
  id: m.id,
62320
62626
  stableId,
@@ -62337,32 +62643,26 @@ var require_device_manager_addon = __commonJS({
62337
62643
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62338
62644
  if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
62339
62645
  const { meta: m } = persisted;
62340
- const key = String(deviceId);
62341
62646
  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
62647
+ await pctx.metaStore.rows.patch(deviceId, {
62648
+ name,
62649
+ userNamed: true
62650
+ });
62651
+ if (oldName.length > 0 && oldName !== name) {
62652
+ const candidates = /* @__PURE__ */ new Map();
62653
+ for (const row of await pctx.metaStore.rows.listByParent(deviceId)) candidates.set(row.meta.id, row.meta);
62654
+ for (const row of await pctx.metaStore.rows.listAll()) if (row.meta.linkDeviceId === deviceId) candidates.set(row.meta.id, row.meta);
62655
+ candidates.delete(deviceId);
62656
+ for (const childMeta of candidates.values()) {
62657
+ if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
62658
+ const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
62659
+ await pctx.metaStore.rows.patch(childMeta.id, { name: childName });
62660
+ cascaded.push({
62661
+ id: childMeta.id,
62662
+ name: childName
62663
+ });
62349
62664
  }
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
62665
  }
62365
- await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
62366
62666
  });
62367
62667
  pctx.host.ctx.eventBus.emit({
62368
62668
  id: (0, node_crypto.randomUUID)(),
@@ -62396,18 +62696,8 @@ var require_device_manager_addon = __commonJS({
62396
62696
  async function setLocation(pctx, input) {
62397
62697
  const { deviceId, location } = input;
62398
62698
  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
- } });
62699
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
62700
+ await pctx.metaStore.rows.patch(deviceId, { location });
62411
62701
  });
62412
62702
  pctx.host.ctx.eventBus.emit({
62413
62703
  id: (0, node_crypto.randomUUID)(),
@@ -62427,18 +62717,8 @@ var require_device_manager_addon = __commonJS({
62427
62717
  async function setType(pctx, input) {
62428
62718
  const { deviceId, type } = input;
62429
62719
  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
- } });
62720
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
62721
+ await pctx.metaStore.rows.patch(deviceId, { type });
62442
62722
  });
62443
62723
  pctx.host.ctx.eventBus.emit({
62444
62724
  id: (0, node_crypto.randomUUID)(),
@@ -62461,18 +62741,8 @@ var require_device_manager_addon = __commonJS({
62461
62741
  async function setLinkDeviceId(pctx, input) {
62462
62742
  const { deviceId, linkDeviceId } = input;
62463
62743
  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
- } });
62744
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
62745
+ await pctx.metaStore.rows.patch(deviceId, { linkDeviceId });
62476
62746
  });
62477
62747
  pctx.host.ctx.eventBus.emit({
62478
62748
  id: (0, node_crypto.randomUUID)(),
@@ -62492,18 +62762,8 @@ var require_device_manager_addon = __commonJS({
62492
62762
  async function setPrimaryChildEntityId(pctx, input) {
62493
62763
  const { deviceId, primaryChildEntityId } = input;
62494
62764
  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
- } });
62765
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
62766
+ await pctx.metaStore.rows.patch(deviceId, { primaryChildEntityId });
62507
62767
  });
62508
62768
  pctx.host.ctx.eventBus.emit({
62509
62769
  id: (0, node_crypto.randomUUID)(),
@@ -62523,18 +62783,8 @@ var require_device_manager_addon = __commonJS({
62523
62783
  async function setChildLayout(pctx, input) {
62524
62784
  const { deviceId, childLayout } = input;
62525
62785
  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
- } });
62786
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
62787
+ await pctx.metaStore.rows.patch(deviceId, { childLayout });
62538
62788
  });
62539
62789
  pctx.host.ctx.eventBus.emit({
62540
62790
  id: (0, node_crypto.randomUUID)(),
@@ -62554,18 +62804,8 @@ var require_device_manager_addon = __commonJS({
62554
62804
  async function setRole(pctx, input) {
62555
62805
  const { deviceId, role } = input;
62556
62806
  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
- } });
62807
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
62808
+ await pctx.metaStore.rows.patch(deviceId, { role });
62569
62809
  });
62570
62810
  pctx.host.ctx.eventBus.emit({
62571
62811
  id: (0, node_crypto.randomUUID)(),
@@ -62596,19 +62836,8 @@ var require_device_manager_addon = __commonJS({
62596
62836
  const { deviceId, display } = input;
62597
62837
  const normalized = display === null ? null : normalizeDisplayOverride(display);
62598
62838
  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
- } });
62839
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
62840
+ await pctx.metaStore.rows.patch(deviceId, { display: normalized });
62612
62841
  });
62613
62842
  pctx.host.ctx.eventBus.emit({
62614
62843
  id: (0, node_crypto.randomUUID)(),
@@ -62638,24 +62867,15 @@ var require_device_manager_addon = __commonJS({
62638
62867
  async function applyInitialMeta(pctx, input) {
62639
62868
  const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
62640
62869
  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,
62870
+ if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
62871
+ await pctx.metaStore.rows.patch(deviceId, {
62648
62872
  ...name !== void 0 ? { name } : {},
62649
62873
  ...location !== void 0 ? { location } : {},
62650
62874
  ...type !== void 0 ? { type } : {},
62651
62875
  ...integrationId !== void 0 ? { integrationId } : {},
62652
62876
  ...linkDeviceId !== void 0 ? { linkDeviceId } : {},
62653
62877
  ...role !== void 0 ? { role } : {}
62654
- };
62655
- await pctx.settings.writeAddonStore({ deviceMeta: {
62656
- ...allMeta,
62657
- [key]: merged
62658
- } });
62878
+ });
62659
62879
  });
62660
62880
  const emitMetaChanged = (field, value) => {
62661
62881
  pctx.host.ctx.eventBus.emit({
@@ -62683,10 +62903,9 @@ var require_device_manager_addon = __commonJS({
62683
62903
  async function setMetadata(pctx, input) {
62684
62904
  const { deviceId, patch } = input;
62685
62905
  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] ?? {} };
62906
+ const row = await pctx.metaStore.rows.get(deviceId);
62907
+ if (row === null) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
62908
+ const next = { ...row.metadata ?? {} };
62690
62909
  let changed = false;
62691
62910
  for (const [k, v] of Object.entries(patch)) if (v === null) {
62692
62911
  if (k in next) {
@@ -62699,10 +62918,7 @@ var require_device_manager_addon = __commonJS({
62699
62918
  }
62700
62919
  if (!changed) return { changed: false };
62701
62920
  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 });
62921
+ await pctx.metaStore.rows.patch(deviceId, { metadata: hasFields ? next : null });
62706
62922
  return {
62707
62923
  changed: true,
62708
62924
  finalMeta: hasFields ? next : null
@@ -62730,15 +62946,7 @@ var require_device_manager_addon = __commonJS({
62730
62946
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
62731
62947
  if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
62732
62948
  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
- } });
62949
+ await pctx.metaStore.rows.patch(deviceId, { disabled });
62742
62950
  return {
62743
62951
  changed: (m.disabled ?? false) !== disabled,
62744
62952
  integrationId: m.integrationId ?? ""
@@ -62781,9 +62989,7 @@ var require_device_manager_addon = __commonJS({
62781
62989
  return data;
62782
62990
  }
62783
62991
  async function listLocations(pctx) {
62784
- const store = await pctx.settings.readAddonStore();
62785
- const meta = store.deviceMeta ?? {};
62786
- const locations = store.locations ?? [];
62992
+ const locations = (await pctx.metaStore.readStore()).locations ?? [];
62787
62993
  const seen = /* @__PURE__ */ new Map();
62788
62994
  const consider = (raw) => {
62789
62995
  if (typeof raw !== "string") return;
@@ -62793,38 +62999,34 @@ var require_device_manager_addon = __commonJS({
62793
62999
  if (!seen.has(key)) seen.set(key, trimmed);
62794
63000
  };
62795
63001
  for (const label of locations) consider(label);
62796
- for (const m of Object.values(meta)) consider(m.location);
63002
+ for (const row of await pctx.metaStore.rows.listAll()) consider(row.meta.location);
62797
63003
  return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
62798
63004
  }
62799
63005
  async function addLocation(pctx, input) {
62800
63006
  const trimmed = input.name.trim();
62801
63007
  if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
62802
- const current = (await pctx.settings.readAddonStore()).locations ?? [];
63008
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62803
63009
  if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
62804
63010
  await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
62805
63011
  }
62806
63012
  async function removeLocation(pctx, input) {
62807
63013
  const trimmed = input.name.trim();
62808
63014
  if (trimmed.length === 0) return;
62809
- const store = await pctx.settings.readAddonStore();
62810
- const current = store.locations ?? [];
63015
+ const current = (await pctx.metaStore.readStore()).locations ?? [];
62811
63016
  const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
62812
63017
  if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
62813
63018
  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 });
63019
+ const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
63020
+ const out = [];
63021
+ for (const row of await pctx.metaStore.rows.listAll()) {
63022
+ const location = row.meta.location;
63023
+ if (typeof location !== "string") continue;
63024
+ if (location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
63025
+ await pctx.metaStore.rows.patch(row.meta.id, { location: null });
63026
+ out.push(row.meta.id);
63027
+ }
63028
+ return out;
63029
+ });
62828
63030
  for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
62829
63031
  id: (0, node_crypto.randomUUID)(),
62830
63032
  timestamp: /* @__PURE__ */ new Date(),
@@ -62840,41 +63042,48 @@ var require_device_manager_addon = __commonJS({
62840
63042
  }
62841
63043
  });
62842
63044
  }
63045
+ function decodeAddonStore(raw) {
63046
+ const nextDeviceId = raw["nextDeviceId"];
63047
+ const locations = raw["locations"];
63048
+ const roleDisplayDefaults = raw["roleDisplayDefaults"];
63049
+ return {
63050
+ ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
63051
+ ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
63052
+ ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
63053
+ };
63054
+ }
63055
+ function isRoleDisplayDefaults(value) {
63056
+ return value !== null && typeof value === "object" && !Array.isArray(value);
63057
+ }
62843
63058
  var DeviceMetaStore = class {
62844
63059
  settings;
62845
63060
  registry;
63061
+ rows;
62846
63062
  /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
62847
- * The persisted meta store is authoritative but reads are async; hub-side
63063
+ * The persisted row store is authoritative but reads are async; hub-side
62848
63064
  * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
62849
63065
  * ownership without awaiting. Kept in sync with every register/remove and
62850
63066
  * warmed from persistence on boot. */
62851
63067
  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. */
63068
+ /** Serialises every read-modify-write of a device row through one promise
63069
+ * chain (see `withMetaWriteLock`). Per-instance state. */
62855
63070
  metaWriteChain = Promise.resolve();
62856
- constructor(settings, registry) {
63071
+ constructor(settings, registry, rows) {
62857
63072
  this.settings = settings;
62858
63073
  this.registry = registry;
63074
+ this.rows = rows;
62859
63075
  }
62860
63076
  /** The read currently in flight, or null. Never a settled value — see
62861
63077
  * {@link readStore}. */
62862
63078
  inFlightRead = null;
62863
63079
  /**
62864
- * The whole persisted addon store.
63080
+ * The addon's own settings row set — `nextDeviceId`, `roleDisplayDefaults`,
63081
+ * `locations`. NOT the fleet: no device has lived here since the flatten.
62865
63082
  *
62866
63083
  * **Concurrent callers join the read already in flight.** This is not a
62867
63084
  * cache and nothing survives settlement: a caller that awaited the running
62868
63085
  * 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).
63086
+ * only thing that changes is cost.
62878
63087
  *
62879
63088
  * A rejection is NOT latched: the slot is cleared before the promise
62880
63089
  * settles either way, so a failed read costs the joiners that one failure
@@ -62885,7 +63094,7 @@ var require_device_manager_addon = __commonJS({
62885
63094
  if (existing !== null) return existing;
62886
63095
  const read = (async () => {
62887
63096
  try {
62888
- return await this.settings.readAddonStore();
63097
+ return decodeAddonStore(await this.settings.readAddonStore());
62889
63098
  } finally {
62890
63099
  this.inFlightRead = null;
62891
63100
  }
@@ -62893,41 +63102,6 @@ var require_device_manager_addon = __commonJS({
62893
63102
  this.inFlightRead = read;
62894
63103
  return read;
62895
63104
  };
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
63105
  withMetaWriteLock = async (fn) => {
62932
63106
  const previous = this.metaWriteChain;
62933
63107
  let release = () => {
@@ -62944,31 +63118,36 @@ var require_device_manager_addon = __commonJS({
62944
63118
  release();
62945
63119
  }
62946
63120
  };
63121
+ /** The whole persisted row for one device, or `null`. */
63122
+ getRow = async (deviceId) => this.rows.get(deviceId);
62947
63123
  /**
62948
63124
  * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
62949
- * Scans persisted meta — live IDevice lookup (hub registry) is handled
63125
+ * Reads the device's own row — live IDevice lookup (hub registry) is handled
62950
63126
  * separately per call site so callers can decide whether to route to
62951
63127
  * an in-process driver or to the cross-process `device-ops` bridge.
62952
63128
  * Returns null when no device with that id is known to the hub.
62953
63129
  */
62954
63130
  resolvePersistedById = async (deviceId) => {
62955
- const m = (await this.readMeta())[String(deviceId)];
62956
- if (!m) return null;
63131
+ const row = await this.rows.get(deviceId);
63132
+ if (row === null) return null;
62957
63133
  return {
62958
- addonId: m.addonId,
62959
- stableId: m.stableId,
62960
- meta: m
63134
+ addonId: row.meta.addonId,
63135
+ stableId: row.meta.stableId,
63136
+ meta: row.meta
62961
63137
  };
62962
63138
  };
63139
+ /** The device's hardware-identity metadata blob, or `null`. */
63140
+ readMetadata = async (deviceId) => {
63141
+ return (await this.rows.get(deviceId))?.metadata ?? null;
63142
+ };
62963
63143
  /** Direct children of a device: the union of the live registry's children
62964
- * and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
63144
+ * and the persisted rows whose `parentDeviceId` is `parentId`, deduplicated
62965
63145
  * and excluding self. Shared by the `remove` cascade and the `resetToSource`
62966
63146
  * resync purge (#19). */
62967
63147
  directChildIds = async (parentId) => {
62968
63148
  const ids = /* @__PURE__ */ new Set();
62969
63149
  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);
63150
+ for (const row of await this.rows.listByParent(parentId)) ids.add(row.meta.id);
62972
63151
  ids.delete(parentId);
62973
63152
  return [...ids];
62974
63153
  };
@@ -62978,6 +63157,476 @@ var require_device_manager_addon = __commonJS({
62978
63157
  return current;
62979
63158
  };
62980
63159
  };
63160
+ var DEVICE_ROWS_COLLECTION = "device-manager:devices";
63161
+ var DEVICE_ROWS_FLEET_LIMIT = 2e4;
63162
+ var DEVICE_ROWS_COLUMNS = [
63163
+ {
63164
+ name: "id",
63165
+ type: "TEXT",
63166
+ primaryKey: true,
63167
+ notNull: true
63168
+ },
63169
+ {
63170
+ name: "deviceId",
63171
+ type: "INTEGER",
63172
+ notNull: true
63173
+ },
63174
+ {
63175
+ name: "addonId",
63176
+ type: "TEXT",
63177
+ notNull: true
63178
+ },
63179
+ {
63180
+ name: "stableId",
63181
+ type: "TEXT",
63182
+ notNull: true
63183
+ },
63184
+ {
63185
+ name: "type",
63186
+ type: "TEXT",
63187
+ notNull: true
63188
+ },
63189
+ {
63190
+ name: "name",
63191
+ type: "TEXT",
63192
+ notNull: true
63193
+ },
63194
+ {
63195
+ name: "userNamed",
63196
+ type: "BOOLEAN"
63197
+ },
63198
+ {
63199
+ name: "location",
63200
+ type: "TEXT"
63201
+ },
63202
+ {
63203
+ name: "disabled",
63204
+ type: "BOOLEAN",
63205
+ notNull: true,
63206
+ defaultValue: false
63207
+ },
63208
+ {
63209
+ name: "parentDeviceId",
63210
+ type: "INTEGER"
63211
+ },
63212
+ {
63213
+ name: "registered",
63214
+ type: "BOOLEAN",
63215
+ notNull: true,
63216
+ defaultValue: false
63217
+ },
63218
+ {
63219
+ name: "features",
63220
+ type: "JSON"
63221
+ },
63222
+ {
63223
+ name: "exportFingerprint",
63224
+ type: "TEXT"
63225
+ },
63226
+ {
63227
+ name: "integrationId",
63228
+ type: "TEXT"
63229
+ },
63230
+ {
63231
+ name: "linkDeviceId",
63232
+ type: "INTEGER"
63233
+ },
63234
+ {
63235
+ name: "primaryChildEntityId",
63236
+ type: "TEXT"
63237
+ },
63238
+ {
63239
+ name: "childLayout",
63240
+ type: "JSON"
63241
+ },
63242
+ {
63243
+ name: "role",
63244
+ type: "TEXT"
63245
+ },
63246
+ {
63247
+ name: "display",
63248
+ type: "JSON"
63249
+ },
63250
+ {
63251
+ name: "metadata",
63252
+ type: "JSON"
63253
+ }
63254
+ ];
63255
+ var DEVICE_ROWS_INDEXES = [
63256
+ {
63257
+ name: "idx_dm_devices_addon_stable",
63258
+ columns: ["addonId", "stableId"]
63259
+ },
63260
+ {
63261
+ name: "idx_dm_devices_parent",
63262
+ columns: ["parentDeviceId"]
63263
+ },
63264
+ {
63265
+ name: "idx_dm_devices_integration",
63266
+ columns: ["integrationId"]
63267
+ }
63268
+ ];
63269
+ function deviceRowBackendOf(client) {
63270
+ return {
63271
+ declareCollection: async (input) => {
63272
+ await client.declareCollection.mutate(input);
63273
+ },
63274
+ get: (input) => client.get.query(input),
63275
+ set: async (input) => {
63276
+ await client.set.mutate(input);
63277
+ },
63278
+ query: (input) => client.query.query(input),
63279
+ updateWhere: (input) => client.updateWhere.mutate(input),
63280
+ delete: async (input) => {
63281
+ await client.delete.mutate(input);
63282
+ },
63283
+ count: (input) => client.count.query(input)
63284
+ };
63285
+ }
63286
+ function isPlainObject3(value) {
63287
+ return value !== null && typeof value === "object" && !Array.isArray(value);
63288
+ }
63289
+ function readString(value) {
63290
+ return typeof value === "string" ? value : void 0;
63291
+ }
63292
+ function readNumber(value) {
63293
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
63294
+ }
63295
+ function readBoolean(value) {
63296
+ if (typeof value === "boolean") return value;
63297
+ if (value === 1) return true;
63298
+ if (value === 0) return false;
63299
+ }
63300
+ function readStringArray(value) {
63301
+ if (!Array.isArray(value)) return void 0;
63302
+ const out = [];
63303
+ for (const entry of value) if (typeof entry === "string") out.push(entry);
63304
+ return out;
63305
+ }
63306
+ function decodeDeviceRow(data) {
63307
+ const deviceId = readNumber(data["deviceId"]);
63308
+ const addonId = readString(data["addonId"]);
63309
+ const stableId = readString(data["stableId"]);
63310
+ const type = readString(data["type"]);
63311
+ const name = readString(data["name"]);
63312
+ if (deviceId === void 0 || addonId === void 0 || stableId === void 0 || type === void 0 || name === void 0) return null;
63313
+ const location = readString(data["location"]);
63314
+ const parentDeviceId = readNumber(data["parentDeviceId"]);
63315
+ const userNamed = readBoolean(data["userNamed"]);
63316
+ const features = readStringArray(data["features"]);
63317
+ const exportFingerprint = readString(data["exportFingerprint"]);
63318
+ const integrationId = readString(data["integrationId"]);
63319
+ const linkDeviceId = readNumber(data["linkDeviceId"]);
63320
+ const primaryChildEntityId = readString(data["primaryChildEntityId"]);
63321
+ const role = readString(data["role"]);
63322
+ const rawChildLayout = data["childLayout"];
63323
+ const rawDisplay = data["display"];
63324
+ const rawMetadata = data["metadata"];
63325
+ return {
63326
+ meta: {
63327
+ id: deviceId,
63328
+ addonId,
63329
+ stableId,
63330
+ type,
63331
+ name,
63332
+ location: location ?? null,
63333
+ disabled: readBoolean(data["disabled"]) ?? false,
63334
+ parentDeviceId: parentDeviceId ?? null,
63335
+ ...userNamed !== void 0 ? { userNamed } : {},
63336
+ ...features !== void 0 ? { features } : {},
63337
+ ...exportFingerprint !== void 0 ? { exportFingerprint } : {},
63338
+ ...integrationId !== void 0 ? { integrationId } : {},
63339
+ ...linkDeviceId !== void 0 ? { linkDeviceId } : {},
63340
+ ...primaryChildEntityId !== void 0 ? { primaryChildEntityId } : {},
63341
+ ...isChildLayout(rawChildLayout) ? { childLayout: rawChildLayout } : {},
63342
+ ...role !== void 0 ? { role } : {},
63343
+ ...isPlainObject3(rawDisplay) ? { display: toDisplayOverride(rawDisplay) } : {}
63344
+ },
63345
+ metadata: isPlainObject3(rawMetadata) ? rawMetadata : null,
63346
+ registered: readBoolean(data["registered"]) ?? false
63347
+ };
63348
+ }
63349
+ function isChildLayout(value) {
63350
+ return Array.isArray(value);
63351
+ }
63352
+ function toDisplayOverride(value) {
63353
+ return value;
63354
+ }
63355
+ function encodeDeviceRow(meta, extra = {}) {
63356
+ const row = {
63357
+ id: String(meta.id),
63358
+ deviceId: meta.id,
63359
+ addonId: meta.addonId,
63360
+ stableId: meta.stableId,
63361
+ type: meta.type,
63362
+ name: meta.name,
63363
+ userNamed: meta.userNamed ?? null,
63364
+ location: meta.location,
63365
+ disabled: meta.disabled,
63366
+ parentDeviceId: meta.parentDeviceId,
63367
+ registered: extra.registered ?? false,
63368
+ features: meta.features === void 0 ? null : [...meta.features],
63369
+ exportFingerprint: meta.exportFingerprint ?? null,
63370
+ integrationId: meta.integrationId ?? null,
63371
+ linkDeviceId: meta.linkDeviceId ?? null,
63372
+ primaryChildEntityId: meta.primaryChildEntityId ?? null,
63373
+ childLayout: meta.childLayout ?? null,
63374
+ role: meta.role ?? null,
63375
+ display: meta.display ?? null
63376
+ };
63377
+ if (extra.metadata !== void 0) row["metadata"] = extra.metadata;
63378
+ return row;
63379
+ }
63380
+ function encodeDeviceRowPatch(patch) {
63381
+ const row = {};
63382
+ for (const [key, value] of Object.entries(patch)) {
63383
+ if (value === void 0) continue;
63384
+ row[key] = value;
63385
+ }
63386
+ return row;
63387
+ }
63388
+ var DeviceRowStore = class {
63389
+ backend;
63390
+ logger;
63391
+ declared = null;
63392
+ constructor(backend, logger) {
63393
+ this.backend = backend;
63394
+ this.logger = logger;
63395
+ }
63396
+ /**
63397
+ * Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
63398
+ * concurrent first-callers issue one declaration rather than N. A rejection
63399
+ * is not latched — the slot is cleared so the next caller retries instead of
63400
+ * inheriting a dead collection forever.
63401
+ */
63402
+ async declare() {
63403
+ const existing = this.declared;
63404
+ if (existing !== null) return existing;
63405
+ const run = (async () => {
63406
+ try {
63407
+ await this.backend.declareCollection({
63408
+ collection: DEVICE_ROWS_COLLECTION,
63409
+ columns: DEVICE_ROWS_COLUMNS,
63410
+ indexes: DEVICE_ROWS_INDEXES
63411
+ });
63412
+ } catch (err) {
63413
+ this.declared = null;
63414
+ throw err;
63415
+ }
63416
+ })();
63417
+ this.declared = run;
63418
+ return run;
63419
+ }
63420
+ /** One device, by numeric id. `null` when the fleet does not know it. */
63421
+ async get(deviceId) {
63422
+ await this.declare();
63423
+ const raw = await this.backend.get({
63424
+ collection: DEVICE_ROWS_COLLECTION,
63425
+ key: String(deviceId)
63426
+ });
63427
+ if (!isPlainObject3(raw)) return null;
63428
+ const decoded = decodeDeviceRow(raw);
63429
+ if (decoded === null) {
63430
+ this.logger.warn("device row skipped \u2014 identity fields missing", { tags: { deviceId } });
63431
+ return null;
63432
+ }
63433
+ return decoded;
63434
+ }
63435
+ /** Every device, ordered by numeric id. */
63436
+ async listAll() {
63437
+ return this.list({
63438
+ orderBy: {
63439
+ field: "deviceId",
63440
+ direction: "asc"
63441
+ },
63442
+ limit: DEVICE_ROWS_FLEET_LIMIT
63443
+ });
63444
+ }
63445
+ /**
63446
+ * The device an addon knows as `stableId`, or `null`.
63447
+ *
63448
+ * `(addonId, stableId)` is the addon-facing identity — unique by
63449
+ * construction, since `allocateDeviceId` is the only thing that mints a row
63450
+ * and it returns the existing id for a pair it already knows. A second row
63451
+ * for the pair would be a corruption, so this takes the LOWEST id and says
63452
+ * so rather than picking arbitrarily.
63453
+ */
63454
+ async findByStableId(addonId, stableId) {
63455
+ const rows = await this.list({
63456
+ where: {
63457
+ addonId,
63458
+ stableId
63459
+ },
63460
+ orderBy: {
63461
+ field: "deviceId",
63462
+ direction: "asc"
63463
+ },
63464
+ limit: 2
63465
+ });
63466
+ const first = rows[0];
63467
+ if (first === void 0) return null;
63468
+ if (rows.length > 1) this.logger.warn("duplicate device rows for one (addonId, stableId) \u2014 using the lowest id", {
63469
+ tags: { deviceId: first.meta.id },
63470
+ meta: {
63471
+ addonId,
63472
+ stableId,
63473
+ ids: rows.map((r) => r.meta.id)
63474
+ }
63475
+ });
63476
+ return first;
63477
+ }
63478
+ /** Every device owned by one addon, ordered by numeric id. */
63479
+ async listByAddon(addonId) {
63480
+ return this.list({
63481
+ where: { addonId },
63482
+ orderBy: {
63483
+ field: "deviceId",
63484
+ direction: "asc"
63485
+ },
63486
+ limit: DEVICE_ROWS_FLEET_LIMIT
63487
+ });
63488
+ }
63489
+ /** Every device an integration owns, ordered by numeric id. */
63490
+ async listByIntegration(integrationId) {
63491
+ return this.list({
63492
+ where: { integrationId },
63493
+ orderBy: {
63494
+ field: "deviceId",
63495
+ direction: "asc"
63496
+ },
63497
+ limit: DEVICE_ROWS_FLEET_LIMIT
63498
+ });
63499
+ }
63500
+ /** Direct children of one device, ordered by numeric id. */
63501
+ async listByParent(parentDeviceId) {
63502
+ return this.list({
63503
+ where: { parentDeviceId },
63504
+ orderBy: {
63505
+ field: "deviceId",
63506
+ direction: "asc"
63507
+ },
63508
+ limit: DEVICE_ROWS_FLEET_LIMIT
63509
+ });
63510
+ }
63511
+ /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
63512
+ async count() {
63513
+ await this.declare();
63514
+ return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
63515
+ }
63516
+ async list(filter) {
63517
+ await this.declare();
63518
+ const records = await this.backend.query({
63519
+ collection: DEVICE_ROWS_COLLECTION,
63520
+ filter
63521
+ });
63522
+ const out = [];
63523
+ for (const record of records) {
63524
+ const decoded = decodeDeviceRow(record.data);
63525
+ if (decoded === null) {
63526
+ this.logger.warn("device row skipped \u2014 identity fields missing", { meta: { rowId: record.id } });
63527
+ continue;
63528
+ }
63529
+ out.push(decoded);
63530
+ }
63531
+ return out;
63532
+ }
63533
+ /** Insert or replace the whole identity row. */
63534
+ async upsert(meta, extra = {}) {
63535
+ await this.declare();
63536
+ await this.backend.set({
63537
+ collection: DEVICE_ROWS_COLLECTION,
63538
+ key: String(meta.id),
63539
+ value: encodeDeviceRow(meta, extra)
63540
+ });
63541
+ }
63542
+ /**
63543
+ * Write the named columns of an EXISTING row and nothing else.
63544
+ *
63545
+ * An UPDATE, deliberately not an upsert: a partial upsert would try to INSERT
63546
+ * a row carrying only the patched columns, and SQLite rejects that on the
63547
+ * `NOT NULL` identity columns before the primary-key conflict can turn it
63548
+ * into an update. Every caller resolves the row under the write lock and
63549
+ * throws when it is gone, so matching zero rows means a device was removed
63550
+ * between the resolve and the write — the patch is lost, and a lost write
63551
+ * that says nothing reads as a write that happened.
63552
+ */
63553
+ async patch(deviceId, patch) {
63554
+ const columns = encodeDeviceRowPatch(patch);
63555
+ if (Object.keys(columns).length === 0) return;
63556
+ await this.declare();
63557
+ const { updated } = await this.backend.updateWhere({
63558
+ collection: DEVICE_ROWS_COLLECTION,
63559
+ filter: { where: { id: String(deviceId) } },
63560
+ data: columns
63561
+ });
63562
+ if (updated === 0) this.logger.warn("device row patch matched no row \u2014 the device was removed under it", {
63563
+ tags: { deviceId },
63564
+ meta: { columns: Object.keys(columns) }
63565
+ });
63566
+ }
63567
+ /**
63568
+ * Insert-or-update the registration columns. See {@link DeviceRegistrationRow}
63569
+ * for why this is a separate, fully-specified statement rather than a patch.
63570
+ */
63571
+ async upsertRegistration(row) {
63572
+ await this.declare();
63573
+ await this.backend.set({
63574
+ collection: DEVICE_ROWS_COLLECTION,
63575
+ key: String(row.deviceId),
63576
+ value: {
63577
+ deviceId: row.deviceId,
63578
+ addonId: row.addonId,
63579
+ stableId: row.stableId,
63580
+ type: row.type,
63581
+ name: row.name,
63582
+ userNamed: row.userNamed,
63583
+ location: row.location,
63584
+ disabled: row.disabled,
63585
+ parentDeviceId: row.parentDeviceId,
63586
+ registered: row.registered,
63587
+ features: [...row.features],
63588
+ exportFingerprint: row.exportFingerprint
63589
+ }
63590
+ });
63591
+ }
63592
+ /** Drop the device's row. Idempotent. */
63593
+ async remove(deviceId) {
63594
+ await this.declare();
63595
+ await this.backend.delete({
63596
+ collection: DEVICE_ROWS_COLLECTION,
63597
+ key: String(deviceId)
63598
+ });
63599
+ }
63600
+ /**
63601
+ * The retirement door for the three blobs this collection replaced.
63602
+ *
63603
+ * The purge is gated on THIS collection being non-empty, and the count has to
63604
+ * be taken after `declare()` — which is why the owning addon runs it and the
63605
+ * settings engine cannot: at the engine's own boot no addon has declared
63606
+ * anything, so every successor would read as unreadable, forever.
63607
+ */
63608
+ retiredRowStore() {
63609
+ return {
63610
+ hasRow: async (spec) => {
63611
+ const raw = await this.backend.get({
63612
+ collection: spec.collection,
63613
+ key: spec.row
63614
+ });
63615
+ return raw !== void 0 && raw !== null;
63616
+ },
63617
+ deleteRow: async (spec) => {
63618
+ await this.backend.delete({
63619
+ collection: spec.collection,
63620
+ key: spec.row
63621
+ });
63622
+ },
63623
+ countRows: async (collection) => {
63624
+ await this.declare();
63625
+ return this.backend.count({ collection });
63626
+ }
63627
+ };
63628
+ }
63629
+ };
62981
63630
  function persistableBlob(blob, policyFor) {
62982
63631
  const out = {};
62983
63632
  for (const [capName, slice] of Object.entries(blob)) {
@@ -63456,12 +64105,21 @@ var require_device_manager_addon = __commonJS({
63456
64105
  }
63457
64106
  }))).filter((id) => id !== null);
63458
64107
  }
64108
+ /**
64109
+ * Row access for `device-manager:devices`. Built once in `onInitialize`
64110
+ * (which is also where the collection is declared) — every consumer reaches
64111
+ * it through {@link bindingsDeps} or the `ProviderContext`.
64112
+ */
64113
+ deviceRows = null;
63459
64114
  /** Build the dependency context the extracted binding resolvers consume. */
63460
64115
  get bindingsDeps() {
64116
+ const rows = this.deviceRows;
64117
+ if (rows === null) throw new Error("[device-manager] device row store not initialized");
63461
64118
  return {
63462
64119
  ctx: this.ctx,
63463
64120
  capabilityRegistry: this.capabilityRegistry,
63464
- remoteNativeCaps: this.remoteNativeCaps
64121
+ remoteNativeCaps: this.remoteNativeCaps,
64122
+ rows
63465
64123
  };
63466
64124
  }
63467
64125
  async getBindings(input) {
@@ -63517,17 +64175,23 @@ var require_device_manager_addon = __commonJS({
63517
64175
  if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
63518
64176
  return ops;
63519
64177
  };
63520
- const metaStore = new DeviceMetaStore(settings, registry);
64178
+ const settingsStoreApi = this.ctx.api?.settingsStore;
64179
+ if (!settingsStoreApi) throw new Error("[device-manager] settings-store API not available \u2014 refusing to serve a fleet it cannot persist");
64180
+ const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
64181
+ await deviceRows.declare();
64182
+ this.deviceRows = deviceRows;
64183
+ try {
64184
+ await require_retired_settings_keys.purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
64185
+ } catch (err) {
64186
+ this.ctx.logger.warn("retired-row purge failed", { meta: { error: require_dist10.errMsg(err) } });
64187
+ }
64188
+ const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
63521
64189
  this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
63522
64190
  const stateMirror = this.stateMirrorImpl;
63523
- const readMeta = metaStore.readMeta;
63524
64191
  const resolvePersistedById = metaStore.resolvePersistedById;
63525
64192
  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);
64193
+ for (const row of await deviceRows.listAll()) idToAddonId.set(row.meta.id, row.meta.addonId);
64194
+ const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, this.ctx, deviceId, integrationId);
63531
64195
  const pctx = {
63532
64196
  host: this.providerHost,
63533
64197
  metaStore,
@@ -63770,7 +64434,7 @@ var require_hub_forwarder = __commonJS({
63770
64434
  [Symbol.toStringTag]: { value: "Module" }
63771
64435
  });
63772
64436
  require_chunk_Cek0wNdY();
63773
- var require_dist10 = require_dist_Ctfhlrtl();
64437
+ var require_dist10 = require_dist_C1Aea3c6();
63774
64438
  var require_formatter = require_formatter_DqAKDlvN();
63775
64439
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
63776
64440
  var HubForwarderDestination = class {
@@ -63907,7 +64571,7 @@ var require_liveness_monitor_addon = __commonJS({
63907
64571
  "use strict";
63908
64572
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
63909
64573
  require_chunk_Cek0wNdY();
63910
- var require_dist10 = require_dist_Ctfhlrtl();
64574
+ var require_dist10 = require_dist_C1Aea3c6();
63911
64575
  var NO_DEVICES = "liveness:no-devices";
63912
64576
  var ALL_OFFLINE = "liveness:all-devices-offline";
63913
64577
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -64097,7 +64761,7 @@ var require_local_auth_addon = __commonJS({
64097
64761
  [Symbol.toStringTag]: { value: "Module" }
64098
64762
  });
64099
64763
  var require_chunk = require_chunk_Cek0wNdY();
64100
- var require_dist10 = require_dist_Ctfhlrtl();
64764
+ var require_dist10 = require_dist_C1Aea3c6();
64101
64765
  var node_crypto = __require("crypto");
64102
64766
  node_crypto = require_chunk.__toESM(node_crypto);
64103
64767
  var crypto$1 = __require("crypto");
@@ -71781,7 +72445,7 @@ var require_loki_logging = __commonJS({
71781
72445
  [Symbol.toStringTag]: { value: "Module" }
71782
72446
  });
71783
72447
  require_chunk_Cek0wNdY();
71784
- var require_dist10 = require_dist_Ctfhlrtl();
72448
+ var require_dist10 = require_dist_C1Aea3c6();
71785
72449
  function sanitizeLabelName(raw) {
71786
72450
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
71787
72451
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -72346,7 +73010,7 @@ var require_native_metrics_addon = __commonJS({
72346
73010
  [Symbol.toStringTag]: { value: "Module" }
72347
73011
  });
72348
73012
  var require_chunk = require_chunk_Cek0wNdY();
72349
- var require_dist10 = require_dist_Ctfhlrtl();
73013
+ var require_dist10 = require_dist_C1Aea3c6();
72350
73014
  var node_child_process = __require("child_process");
72351
73015
  var node_util = __require("util");
72352
73016
  var node_os = __require("os");
@@ -73288,7 +73952,7 @@ var require_filesystem_storage_addon = __commonJS({
73288
73952
  [Symbol.toStringTag]: { value: "Module" }
73289
73953
  });
73290
73954
  var require_chunk = require_chunk_Cek0wNdY();
73291
- var require_dist10 = require_dist_Ctfhlrtl();
73955
+ var require_dist10 = require_dist_C1Aea3c6();
73292
73956
  var node_crypto = __require("crypto");
73293
73957
  var node_fs_promises = __require("fs/promises");
73294
73958
  var node_path = __require("path");
@@ -74404,7 +75068,8 @@ var require_sqlite_settings_addon = __commonJS({
74404
75068
  [Symbol.toStringTag]: { value: "Module" }
74405
75069
  });
74406
75070
  var require_chunk = require_chunk_Cek0wNdY();
74407
- var require_dist10 = require_dist_Ctfhlrtl();
75071
+ var require_dist10 = require_dist_C1Aea3c6();
75072
+ var require_retired_settings_keys = require_retired_settings_keys_B52KsNz8();
74408
75073
  var node_crypto = __require("crypto");
74409
75074
  var node_fs = __require("fs");
74410
75075
  var node_module = __require("module");
@@ -74413,123 +75078,47 @@ var require_sqlite_settings_addon = __commonJS({
74413
75078
  var SQLITE_PAGE_CACHE_KIB = -65536;
74414
75079
  var SQLITE_MMAP_SIZE_BYTES = 268435456;
74415
75080
  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"
75081
+ var RETIRED_COLLECTIONS = [{
75082
+ collection: "devices",
75083
+ owner: "integration-registry",
75084
+ 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
75085
  }, {
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"
75086
+ collection: "device_settings_kv",
75087
+ owner: "integration-registry",
75088
+ 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
75089
  }];
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;
75090
+ function retiredCollectionStoreOf(backend) {
75091
+ return { async drop(collection) {
75092
+ return backend.dropRetiredTable(collection);
75093
+ } };
74485
75094
  }
74486
- async function purgeRetiredSettingsKeys(store, logger, specs = RETIRED_SETTINGS_KEYS, env = process.env) {
74487
- if (!settingsStoreIsAuthoritativeHere(env)) return [];
75095
+ async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
74488
75096
  const results = [];
74489
75097
  for (const spec of specs) {
74490
- let blob;
75098
+ let dropped;
74491
75099
  try {
74492
- blob = await store.read(spec);
75100
+ dropped = await store.drop(spec.collection);
74493
75101
  } catch (err) {
74494
- logger.warn("retired settings keys \u2014 read failed, row left untouched", { meta: {
74495
- addon: spec.namespace,
75102
+ logger.warn("retired collection \u2014 drop failed, table left in place", { meta: {
75103
+ addon: spec.owner,
74496
75104
  collection: spec.collection,
74497
- row: spec.row,
74498
75105
  error: err instanceof Error ? err.message : String(err)
74499
75106
  } });
74500
75107
  continue;
74501
75108
  }
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);
75109
+ if (dropped === null) continue;
74524
75110
  const result = {
74525
- addon: spec.namespace,
74526
75111
  collection: spec.collection,
74527
- row: spec.row,
74528
- keys: doomed,
74529
- nested: doomedNested
75112
+ owner: spec.owner,
75113
+ rows: dropped.rows
74530
75114
  };
74531
75115
  results.push(result);
74532
- logger.info(`purged ${doomed.length + doomedNested.length} retired settings keys`, { meta: {
75116
+ const line = "dropped a retired collection";
75117
+ if (dropped.rows > 0) logger.warn(`${line} \u2014 it still held rows`, { meta: {
75118
+ ...result,
75119
+ reason: spec.reason
75120
+ } });
75121
+ else logger.info(line, { meta: {
74533
75122
  ...result,
74534
75123
  reason: spec.reason
74535
75124
  } });
@@ -74623,6 +75212,9 @@ var require_sqlite_settings_addon = __commonJS({
74623
75212
  function isImposedBound(source) {
74624
75213
  return source !== "caller";
74625
75214
  }
75215
+ function isCountRow(raw) {
75216
+ return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
75217
+ }
74626
75218
  function parseRowData(raw) {
74627
75219
  return require_dist10.asJsonObject(require_dist10.parseJsonUnknown(raw)) ?? {};
74628
75220
  }
@@ -75307,6 +75899,31 @@ var require_sqlite_settings_addon = __commonJS({
75307
75899
  }
75308
75900
  this.structuredTables.set(table, fingerprint);
75309
75901
  }
75902
+ /**
75903
+ * Drop a table nothing declares any more, reporting what went with it.
75904
+ *
75905
+ * The DDL lives here because the engine owns the handle AND the two memos
75906
+ * that outlive a table: `structuredTables` (schema fingerprint) and
75907
+ * `declaredCollections` (the column set `compileFilter` resolves against).
75908
+ * Dropping behind their backs would leave a ghost — a later declaration of
75909
+ * the same name would be memoised as "already materialised" against a table
75910
+ * that no longer exists.
75911
+ *
75912
+ * WHICH tables are retired, and the logging, are policy: they live in
75913
+ * `retired-collections.ts`. This method knows only how to drop one.
75914
+ *
75915
+ * Answers `null` when the table is not there — the steady state after the
75916
+ * first boot that ran the purge, and the case on a fresh install.
75917
+ */
75918
+ dropRetiredTable(table) {
75919
+ if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
75920
+ const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
75921
+ const rows = isCountRow(counted) ? counted.n : 0;
75922
+ this.getDb().exec(`DROP TABLE "${table}"`);
75923
+ this.structuredTables.delete(table);
75924
+ this.declaredCollections.delete(table);
75925
+ return { rows };
75926
+ }
75310
75927
  async tableInsert(table, row) {
75311
75928
  const keys = Object.keys(row);
75312
75929
  const cols = keys.map((k) => `"${k}"`).join(", ");
@@ -75817,10 +76434,11 @@ var require_sqlite_settings_addon = __commonJS({
75817
76434
  this.backend = new SqliteSettingsBackend(dbPath, { ...require_dist10.RUNTIME_DEFAULTS }, this.ctx.logger.child("Query"));
75818
76435
  await this.backend.initialize();
75819
76436
  try {
75820
- await purgeRetiredSettingsKeys(retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
76437
+ await require_retired_settings_keys.purgeRetiredSettingsKeys(require_retired_settings_keys.retiredKeyStoreOf(this.backend), this.ctx.logger.child("RetiredKeys"));
75821
76438
  } catch (err) {
75822
76439
  this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist10.errMsg(err) } });
75823
76440
  }
76441
+ await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
75824
76442
  const vectorIndex = this.createVectorIndex(this.backend);
75825
76443
  await vectorIndex.loadIndexRegistry();
75826
76444
  this.vectorIndex = vectorIndex;
@@ -75995,7 +76613,7 @@ var require_storage_orchestrator_addon = __commonJS({
75995
76613
  [Symbol.toStringTag]: { value: "Module" }
75996
76614
  });
75997
76615
  var require_chunk = require_chunk_Cek0wNdY();
75998
- var require_dist10 = require_dist_Ctfhlrtl();
76616
+ var require_dist10 = require_dist_C1Aea3c6();
75999
76617
  var node_crypto = __require("crypto");
76000
76618
  var node_fs_promises = __require("fs/promises");
76001
76619
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -77874,7 +78492,7 @@ var require_system_config_addon = __commonJS({
77874
78492
  [Symbol.toStringTag]: { value: "Module" }
77875
78493
  });
77876
78494
  require_chunk_Cek0wNdY();
77877
- var require_dist10 = require_dist_Ctfhlrtl();
78495
+ var require_dist10 = require_dist_C1Aea3c6();
77878
78496
  var SECTION_TITLES = {
77879
78497
  server: "Server",
77880
78498
  auth: "Authentication"
@@ -95935,7 +96553,7 @@ var require_winston_logging = __commonJS({
95935
96553
  [Symbol.toStringTag]: { value: "Module" }
95936
96554
  });
95937
96555
  var require_chunk = require_chunk_Cek0wNdY();
95938
- var require_dist10 = require_dist_Ctfhlrtl();
96556
+ var require_dist10 = require_dist_C1Aea3c6();
95939
96557
  var require_formatter = require_formatter_DqAKDlvN();
95940
96558
  var node_path = __require("path");
95941
96559
  node_path = require_chunk.__toESM(node_path);
@@ -107405,9 +108023,9 @@ var require_dist2 = __commonJS({
107405
108023
  }
107406
108024
  });
107407
108025
 
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) {
108026
+ // ../system/dist/manifest-python-deps-MA_BZGBz.js
108027
+ var require_manifest_python_deps_MA_BZGBz = __commonJS({
108028
+ "../system/dist/manifest-python-deps-MA_BZGBz.js"(exports) {
107411
108029
  "use strict";
107412
108030
  var require_chunk = require_chunk_Cek0wNdY();
107413
108031
  var node_crypto = __require("crypto");
@@ -111384,6 +112002,8 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111384
112002
  };
111385
112003
  goneHandler = () => {
111386
112004
  };
112005
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
112006
+ nextIncarnation = 1;
111387
112007
  eventHandler = null;
111388
112008
  logHandler = null;
111389
112009
  readinessHandler = null;
@@ -111658,14 +112278,20 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111658
112278
  listChildren() {
111659
112279
  return [...this.children.values()].map((e) => ({
111660
112280
  childId: e.childId,
111661
- caps: e.caps
112281
+ caps: e.caps,
112282
+ incarnation: e.incarnation
111662
112283
  }));
111663
112284
  }
111664
112285
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
111665
112286
  onChildRegistered(handler) {
111666
112287
  this.registeredHandler = handler;
111667
112288
  }
111668
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
112289
+ /**
112290
+ * Register the (single) child-gone handler. Only one handler is active at a
112291
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
112292
+ * connection that closed — carry it into whatever teardown it triggers, so a
112293
+ * dead generation cannot tear down its successor's work.
112294
+ */
111669
112295
  onChildGone(handler) {
111670
112296
  this.goneHandler = handler;
111671
112297
  }
@@ -111750,6 +112376,7 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111750
112376
  }
111751
112377
  onConnection(channel) {
111752
112378
  let childId = null;
112379
+ const incarnation = this.nextIncarnation++;
111753
112380
  channel.onEvent((body) => {
111754
112381
  const msg = body;
111755
112382
  if (msg.kind === "event") {
@@ -111778,16 +112405,24 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111778
112405
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
111779
112406
  childId = msg.childId;
111780
112407
  const eventPatterns = msg.eventPatterns ?? null;
112408
+ const superseded = this.children.get(msg.childId);
112409
+ if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
112410
+ childId: msg.childId,
112411
+ supersededIncarnation: superseded.incarnation,
112412
+ incarnation
112413
+ });
111781
112414
  this.children.set(msg.childId, {
111782
112415
  childId: msg.childId,
111783
112416
  channel,
111784
112417
  caps: msg.caps,
111785
- eventPatterns
112418
+ eventPatterns,
112419
+ incarnation
111786
112420
  });
111787
112421
  this.logPatternSet(msg.childId, eventPatterns);
111788
112422
  this.registeredHandler({
111789
112423
  childId: msg.childId,
111790
- caps: msg.caps
112424
+ caps: msg.caps,
112425
+ incarnation
111791
112426
  });
111792
112427
  return { ok: true };
111793
112428
  }
@@ -111822,11 +112457,21 @@ var require_manifest_python_deps_GejnH_L = __commonJS({
111822
112457
  throw new Error(`unknown child request kind: ${msg.kind}`);
111823
112458
  });
111824
112459
  channel.onClose(() => {
111825
- if (childId !== null && this.children.delete(childId)) {
111826
- this.childEventStats.delete(childId);
111827
- this.loggedPatternSet.delete(childId);
111828
- this.goneHandler(childId);
112460
+ if (childId === null) return;
112461
+ const entry = this.children.get(childId);
112462
+ if (entry === void 0) return;
112463
+ if (entry.channel !== channel) {
112464
+ this.logger?.info("ignoring close of a superseded local child connection", {
112465
+ childId,
112466
+ incarnation,
112467
+ liveIncarnation: entry.incarnation
112468
+ });
112469
+ return;
111829
112470
  }
112471
+ this.children.delete(childId);
112472
+ this.childEventStats.delete(childId);
112473
+ this.loggedPatternSet.delete(childId);
112474
+ this.goneHandler(childId, incarnation);
111830
112475
  });
111831
112476
  }
111832
112477
  };
@@ -118184,7 +118829,7 @@ var require_dist3 = __commonJS({
118184
118829
  "use strict";
118185
118830
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
118186
118831
  var require_chunk = require_chunk_Cek0wNdY();
118187
- var require_dist10 = require_dist_Ctfhlrtl();
118832
+ var require_dist10 = require_dist_C1Aea3c6();
118188
118833
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
118189
118834
  require_alerts();
118190
118835
  var require_formatter = require_formatter_DqAKDlvN();
@@ -118209,7 +118854,7 @@ var require_dist3 = __commonJS({
118209
118854
  require_system_config();
118210
118855
  var require_builtins_winston_logging_index = require_winston_logging();
118211
118856
  var require_file_data_plane = require_file_data_plane_DUHPHa_Y();
118212
- var require_manifest_python_deps = require_manifest_python_deps_GejnH_L();
118857
+ var require_manifest_python_deps = require_manifest_python_deps_MA_BZGBz();
118213
118858
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
118214
118859
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
118215
118860
  var zod = require_zod();
@@ -120105,13 +120750,9 @@ var require_dist3 = __commonJS({
120105
120750
  return {};
120106
120751
  }
120107
120752
  var integrationCounter = 0;
120108
- var deviceCounter = 0;
120109
120753
  function nextIntegrationId() {
120110
120754
  return `int_${String(++integrationCounter).padStart(4, "0")}`;
120111
120755
  }
120112
- function nextDeviceId() {
120113
- return `dev_${String(++deviceCounter).padStart(4, "0")}`;
120114
- }
120115
120756
  var INTEGRATIONS_COLUMNS = [
120116
120757
  {
120117
120758
  name: "id",
@@ -120178,99 +120819,8 @@ var require_dist3 = __commonJS({
120178
120819
  defaultValue: "string"
120179
120820
  }
120180
120821
  ];
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 = [
120248
- {
120249
- name: "device_id",
120250
- type: "TEXT",
120251
- notNull: true
120252
- },
120253
- {
120254
- name: "key",
120255
- type: "TEXT",
120256
- notNull: true
120257
- },
120258
- {
120259
- name: "value",
120260
- type: "TEXT",
120261
- notNull: true
120262
- },
120263
- {
120264
- name: "value_type",
120265
- type: "TEXT",
120266
- notNull: true,
120267
- defaultValue: "string"
120268
- }
120269
- ];
120270
120822
  var INTEGRATIONS = "integrations";
120271
120823
  var INTEGRATION_SETTINGS = "integration_settings";
120272
- var DEVICES = "devices";
120273
- var DEVICE_SETTINGS = "device_settings_kv";
120274
120824
  var IntegrationRegistry = class IntegrationRegistry2 {
120275
120825
  store;
120276
120826
  constructor(backend) {
@@ -120286,15 +120836,6 @@ var require_dist3 = __commonJS({
120286
120836
  collection: INTEGRATION_SETTINGS,
120287
120837
  columns: INTEGRATION_SETTINGS_COLUMNS
120288
120838
  });
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
120839
  const ints = await this.store.query({
120299
120840
  collection: INTEGRATIONS,
120300
120841
  filter: {
@@ -120309,20 +120850,6 @@ var require_dist3 = __commonJS({
120309
120850
  const num = parseInt(ints[0].id.replace("int_", ""), 10);
120310
120851
  if (!isNaN(num)) integrationCounter = num;
120311
120852
  }
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
120853
  }
120327
120854
  async createIntegration(input) {
120328
120855
  const id = nextIntegrationId();
@@ -120406,15 +120933,6 @@ var require_dist3 = __commonJS({
120406
120933
  async deleteIntegration(id) {
120407
120934
  const integration = await this.getIntegration(id);
120408
120935
  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
120936
  await this.store.deleteWhere({
120419
120937
  collection: INTEGRATION_SETTINGS,
120420
120938
  filter: { where: { integration_id: id } }
@@ -120459,139 +120977,6 @@ var require_dist3 = __commonJS({
120459
120977
  async setIntegrationSettings(integrationId, settings) {
120460
120978
  for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
120461
120979
  }
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
120980
  mapIntegration(row) {
120596
120981
  return {
120597
120982
  id: String(row["id"]),
@@ -120603,19 +120988,6 @@ var require_dist3 = __commonJS({
120603
120988
  updatedAt: Number(row["updated_at"])
120604
120989
  };
120605
120990
  }
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
120991
  };
120620
120992
  var execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
120621
120993
  var NATIVE_SCAN_DEPTH = 4;
@@ -123109,6 +123481,7 @@ var require_dist3 = __commonJS({
123109
123481
  const state = {
123110
123482
  definition,
123111
123483
  providers: /* @__PURE__ */ new Map(),
123484
+ providerOwners: /* @__PURE__ */ new Map(),
123112
123485
  activeAddonId: null,
123113
123486
  router: null,
123114
123487
  disabledProviders: /* @__PURE__ */ new Set()
@@ -123166,8 +123539,15 @@ var require_dist3 = __commonJS({
123166
123539
  * Double-register of the same pair is always a programmer error —
123167
123540
  * two legitimately different addons implementing the same cap keep
123168
123541
  * using different `addonId`s and the user picks one via `configReader`.
123169
- */
123170
- registerProvider(capabilityName, addonId, provider) {
123542
+ *
123543
+ * `owner` (optional) stamps the registration with the incarnation that made
123544
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
123545
+ * presenting the SAME token can delete it. Registration paths whose key is
123546
+ * reused across process generations (the UDS child manifest, the D3
123547
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
123548
+ * historical unconditional behaviour.
123549
+ */
123550
+ registerProvider(capabilityName, addonId, provider, owner) {
123171
123551
  let state = this.capabilities.get(capabilityName);
123172
123552
  if (!state) {
123173
123553
  const colonIdx = capabilityName.indexOf(":");
@@ -123199,6 +123579,7 @@ var require_dist3 = __commonJS({
123199
123579
  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
123580
  }
123201
123581
  state.providers.set(addonId, provider);
123582
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
123202
123583
  if (state.definition.mode === "singleton") {
123203
123584
  const userChoice = this.configReader?.(capabilityName);
123204
123585
  const preferred = state.definition.preferredProvider;
@@ -123229,7 +123610,8 @@ var require_dist3 = __commonJS({
123229
123610
  tags: { addonId },
123230
123611
  meta: {
123231
123612
  capability: capabilityName,
123232
- mode: state.definition.mode
123613
+ mode: state.definition.mode,
123614
+ ...owner === void 0 ? {} : { owner }
123233
123615
  }
123234
123616
  });
123235
123617
  this.emitEvent("capability:provider-registered", {
@@ -123276,11 +123658,70 @@ var require_dist3 = __commonJS({
123276
123658
  const declarers = this.manifestDeclarers.get(capabilityName);
123277
123659
  return declarers ? [...declarers] : [];
123278
123660
  }
123279
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
123280
- unregisterProvider(capabilityName, addonId) {
123661
+ /**
123662
+ * The owner token a registration declared, or `null` when the pair is not
123663
+ * registered or was registered without one.
123664
+ */
123665
+ getProviderOwner(capabilityName, addonId) {
123666
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
123667
+ }
123668
+ /**
123669
+ * Hand an EXISTING registration to a new owner, without touching the provider
123670
+ * instance, the active-singleton selection or any event.
123671
+ *
123672
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
123673
+ * set, so the manifest diff correctly leaves the registration alone — but the
123674
+ * registration is still stamped with the dead generation's token, and nothing
123675
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
123676
+ * capability cleanable when the successor eventually dies.
123677
+ *
123678
+ * Returns `false` when the pair is not registered (nothing to hand over) —
123679
+ * the caller decides whether that is a register-instead or a diagnosis.
123680
+ */
123681
+ reassignProviderOwner(capabilityName, addonId, owner) {
123682
+ const state = this.capabilities.get(capabilityName);
123683
+ if (!state || !state.providers.has(addonId)) return false;
123684
+ const previous = state.providerOwners.get(addonId);
123685
+ if (previous === owner) return true;
123686
+ state.providerOwners.set(addonId, owner);
123687
+ this.logger.info("Capability registration ownership reassigned", {
123688
+ tags: { addonId },
123689
+ meta: {
123690
+ capability: capabilityName,
123691
+ owner,
123692
+ previousOwner: previous ?? null
123693
+ }
123694
+ });
123695
+ return true;
123696
+ }
123697
+ /**
123698
+ * Unregister a provider. For singleton: clears active. For collection:
123699
+ * removes from list.
123700
+ *
123701
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
123702
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
123703
+ * and the caller presents a different one — or none at all — this is a
123704
+ * logged no-op: the caller belongs to a generation that no longer holds the
123705
+ * key. This is what stops a dead runner's teardown from erasing its
123706
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
123707
+ */
123708
+ unregisterProvider(capabilityName, addonId, owner) {
123281
123709
  const state = this.capabilities.get(capabilityName);
123282
123710
  if (!state) return;
123711
+ const registeredOwner = state.providerOwners.get(addonId);
123712
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
123713
+ this.logger.info("Unregister ignored \u2014 registration is owned by another incarnation", {
123714
+ tags: { addonId },
123715
+ meta: {
123716
+ capability: capabilityName,
123717
+ registeredOwner,
123718
+ presentedOwner: owner ?? null
123719
+ }
123720
+ });
123721
+ return;
123722
+ }
123283
123723
  state.providers.delete(addonId);
123724
+ state.providerOwners.delete(addonId);
123284
123725
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
123285
123726
  const userChoice = this.configReader?.(capabilityName);
123286
123727
  const preferred = state.definition.preferredProvider;
@@ -198254,6 +198695,7 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198254
198695
  function safeSegment(name2) {
198255
198696
  return name2.replace(/[^a-zA-Z0-9._-]/g, "_");
198256
198697
  }
198698
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
198257
198699
  var StagingArea = class {
198258
198700
  stagingDir;
198259
198701
  deps;
@@ -198271,8 +198713,12 @@ globstar while`, t, d, e, f, m), this.matchOne(t.slice(d), e.slice(f), s)) retur
198271
198713
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
198272
198714
  await this.deps.extract(tgz, destDir);
198273
198715
  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 };
198716
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
198717
+ 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}`);
198718
+ return {
198719
+ stagedPath: destDir,
198720
+ resolvedVersion: raw.version
198721
+ };
198276
198722
  }
198277
198723
  cleanup(jobId) {
198278
198724
  node_fs.rmSync(node_path.join(this.stagingDir, jobId), {
@@ -238430,7 +238876,7 @@ var require_dist9 = __commonJS({
238430
238876
  postBufferSec: zod.z.number().min(0).optional()
238431
238877
  });
238432
238878
  var DEFAULT_EVENTS_BAND_BUFFER_SEC = {
238433
- preBufferSec: 10,
238879
+ preBufferSec: 15,
238434
238880
  postBufferSec: 30
238435
238881
  };
238436
238882
  var VISIT_MERGE_GAP_MS = DEFAULT_EVENTS_BAND_BUFFER_SEC.postBufferSec * 1e3;
@@ -239044,12 +239490,17 @@ var require_dist9 = __commonJS({
239044
239490
  "tflite",
239045
239491
  "pt"
239046
239492
  ];
239493
+ var FAMILY_LABEL = {
239494
+ yolo26: "YOLO26",
239495
+ yolov9: "YOLOv9"
239496
+ };
239047
239497
  function familyLabel(family) {
239048
- return family.toUpperCase();
239498
+ return FAMILY_LABEL[family] ?? family.toUpperCase();
239049
239499
  }
239500
+ var RESOLUTION_IN_NAME = /\s*@\s*\d+\s*(?:[x×]\s*\d+)?/gi;
239050
239501
  function tierLabel(family, baseName) {
239051
239502
  const fl = familyLabel(family);
239052
- const stripped = baseName.replace(new RegExp(`^${fl}\\s+`, "i"), "").trim();
239503
+ const stripped = baseName.replace(new RegExp(`^${fl}\\s+`, "i"), "").replace(RESOLUTION_IN_NAME, "").trim();
239053
239504
  return stripped.length > 0 ? stripped : baseName;
239054
239505
  }
239055
239506
  function variantLabel(precision, optimization) {
@@ -239066,9 +239517,11 @@ var require_dist9 = __commonJS({
239066
239517
  return sizes.length > 0 ? Math.min(...sizes) : 0;
239067
239518
  }
239068
239519
  var TIER_ORDER = [
239520
+ "t",
239069
239521
  "n",
239070
239522
  "s",
239071
239523
  "m",
239524
+ "c",
239072
239525
  "l",
239073
239526
  "x"
239074
239527
  ];
@@ -239105,7 +239558,7 @@ var require_dist9 = __commonJS({
239105
239558
  if (a.optimization !== b.optimization) return a.optimization === "fast" ? 1 : -1;
239106
239559
  return PRECISION_ORDER.indexOf(a.precision) - PRECISION_ORDER.indexOf(b.precision);
239107
239560
  });
239108
- const base = options.find((o) => o.precision === "fp32" && o.optimization === "standard" && o.resolution === void 0) ?? options[0];
239561
+ const base = options.find((o) => o.precision === "fp32" && o.optimization === "standard") ?? options[0];
239109
239562
  if (base === void 0) continue;
239110
239563
  const baseEntry = entries2.find((e) => e.id === base.modelId);
239111
239564
  tierList.push({
@@ -239135,15 +239588,9 @@ var require_dist9 = __commonJS({
239135
239588
  function resolveVariantModelId(models, selection) {
239136
239589
  const precision = selection.precision ?? "fp32";
239137
239590
  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;
239591
+ 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);
239592
+ if (selection.resolution !== void 0) return candidates.find((m) => m.group?.resolution === selection.resolution)?.id ?? null;
239593
+ return candidates.toSorted((a, b) => (b.group?.resolution ?? Infinity) - (a.group?.resolution ?? Infinity))[0]?.id ?? null;
239147
239594
  }
239148
239595
  function describeModelVariant(models, modelId) {
239149
239596
  const entry = models.find((m) => m.id === modelId);
@@ -240550,11 +240997,15 @@ export default function block(ctx: AddonContext) {
240550
240997
  ): Promise<void> {
240551
240998
  const resolved: Record<string, unknown> = {}
240552
240999
  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) {
241000
+ const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
241001
+ const proc: unknown =
241002
+ typeof client === 'object' && client !== null ? Reflect.get(client, method) : undefined
241003
+ const mutate: unknown =
241004
+ typeof proc === 'object' && proc !== null ? Reflect.get(proc, 'mutate') : undefined
241005
+ if (typeof mutate !== 'function') {
240555
241006
  throw new Error('no such device method: ' + cap + '.' + method)
240556
241007
  }
240557
- await client[method]!.mutate({ ...resolved, deviceId })
241008
+ await mutate({ ...resolved, deviceId })
240558
241009
  }
240559
241010
 
240560
241011
  async function evaluateConditions(): Promise<boolean> {
@@ -246401,128 +246852,103 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
246401
246852
  var AUDIO_MACRO_LABELS = [
246402
246853
  {
246403
246854
  id: "speech",
246404
- name: "Speech",
246405
- icon: "\u{1F5E3}\uFE0F"
246855
+ name: "Speech"
246406
246856
  },
246407
246857
  {
246408
246858
  id: "scream",
246409
- name: "Scream / Shout",
246410
- icon: "\u{1F631}"
246859
+ name: "Scream / Shout"
246411
246860
  },
246412
246861
  {
246413
246862
  id: "crying",
246414
- name: "Crying / Baby",
246415
- icon: "\u{1F622}"
246863
+ name: "Crying / Baby"
246416
246864
  },
246417
246865
  {
246418
246866
  id: "laughter",
246419
- name: "Laughter",
246420
- icon: "\u{1F602}"
246867
+ name: "Laughter"
246421
246868
  },
246422
246869
  {
246423
246870
  id: "music",
246424
- name: "Music",
246425
- icon: "\u{1F3B5}"
246871
+ name: "Music"
246426
246872
  },
246427
246873
  {
246428
246874
  id: "dog",
246429
- name: "Dog",
246430
- icon: "\u{1F415}"
246875
+ name: "Dog"
246431
246876
  },
246432
246877
  {
246433
246878
  id: "cat",
246434
- name: "Cat",
246435
- icon: "\u{1F408}"
246879
+ name: "Cat"
246436
246880
  },
246437
246881
  {
246438
246882
  id: "bird",
246439
- name: "Bird",
246440
- icon: "\u{1F426}"
246883
+ name: "Bird"
246441
246884
  },
246442
246885
  {
246443
246886
  id: "animal",
246444
- name: "Animal (other)",
246445
- icon: "\u{1F43E}"
246887
+ name: "Animal (other)"
246446
246888
  },
246447
246889
  {
246448
246890
  id: "alarm",
246449
- name: "Alarm / Siren",
246450
- icon: "\u{1F6A8}"
246891
+ name: "Alarm / Siren"
246451
246892
  },
246452
246893
  {
246453
246894
  id: "doorbell",
246454
- name: "Doorbell / Knock",
246455
- icon: "\u{1F514}"
246895
+ name: "Doorbell / Knock"
246456
246896
  },
246457
246897
  {
246458
246898
  id: "glass_breaking",
246459
- name: "Glass Breaking",
246460
- icon: "\u{1F4A5}"
246899
+ name: "Glass Breaking"
246461
246900
  },
246462
246901
  {
246463
246902
  id: "gunshot",
246464
- name: "Gunshot / Explosion",
246465
- icon: "\u{1F4A3}"
246903
+ name: "Gunshot / Explosion"
246466
246904
  },
246467
246905
  {
246468
246906
  id: "vehicle",
246469
- name: "Vehicle",
246470
- icon: "\u{1F697}"
246907
+ name: "Vehicle"
246471
246908
  },
246472
246909
  {
246473
246910
  id: "siren",
246474
- name: "Emergency Siren",
246475
- icon: "\u{1F691}"
246911
+ name: "Emergency Siren"
246476
246912
  },
246477
246913
  {
246478
246914
  id: "fire",
246479
- name: "Fire / Smoke",
246480
- icon: "\u{1F525}"
246915
+ name: "Fire / Smoke"
246481
246916
  },
246482
246917
  {
246483
246918
  id: "water",
246484
- name: "Water",
246485
- icon: "\u{1F4A7}"
246919
+ name: "Water"
246486
246920
  },
246487
246921
  {
246488
246922
  id: "wind",
246489
- name: "Wind / Weather",
246490
- icon: "\u{1F32C}\uFE0F"
246923
+ name: "Wind / Weather"
246491
246924
  },
246492
246925
  {
246493
246926
  id: "door",
246494
- name: "Door",
246495
- icon: "\u{1F6AA}"
246927
+ name: "Door"
246496
246928
  },
246497
246929
  {
246498
246930
  id: "footsteps",
246499
- name: "Footsteps",
246500
- icon: "\u{1F463}"
246931
+ name: "Footsteps"
246501
246932
  },
246502
246933
  {
246503
246934
  id: "crowd",
246504
- name: "Crowd / Chatter",
246505
- icon: "\u{1F465}"
246935
+ name: "Crowd / Chatter"
246506
246936
  },
246507
246937
  {
246508
246938
  id: "telephone",
246509
- name: "Telephone",
246510
- icon: "\u{1F4DE}"
246939
+ name: "Telephone"
246511
246940
  },
246512
246941
  {
246513
246942
  id: "engine",
246514
- name: "Engine / Motor",
246515
- icon: "\u2699\uFE0F"
246943
+ name: "Engine / Motor"
246516
246944
  },
246517
246945
  {
246518
246946
  id: "tools",
246519
- name: "Tools / Construction",
246520
- icon: "\u{1F528}"
246947
+ name: "Tools / Construction"
246521
246948
  },
246522
246949
  {
246523
246950
  id: "silence",
246524
- name: "Silence",
246525
- icon: "\u{1F92B}"
246951
+ name: "Silence"
246526
246952
  }
246527
246953
  ];
246528
246954
  var YAMNET_TO_MACRO = {
@@ -249110,6 +249536,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
249110
249536
  */
249111
249537
  hasFace: zod.z.boolean().optional(),
249112
249538
  /**
249539
+ * This track has a face row IN THE GALLERY: a crop **and** an embedding — a
249540
+ * face an operator could ASSIGN to an identity.
249541
+ *
249542
+ * The STRICT twin of {@link hasFace}, and the pair only earns its keep
249543
+ * because the two disagree. `hasFace` is stamped at the TOP of the face
249544
+ * branch, before every gate, and means no more than "a face detector produced
249545
+ * a face detail". This one is stamped at the single moment the gallery row
249546
+ * LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
249547
+ * past the embedding-magnitude verdict, the `minFacePx` detection gate, the
249548
+ * candidate gate, the imageless-track drop (no crop was ever captured) and
249549
+ * the crop-store drop. Everything between the detector and that insert can
249550
+ * legitimately refuse the face, so a flag written any earlier promises the
249551
+ * operator something to assign and delivers nothing.
249552
+ *
249553
+ * **Independent of recognition.** A face collected but never auto-matched is
249554
+ * still assignable — it is in fact the face an operator most wants to reach —
249555
+ * so this is NOT gated on `recognizedIdentityId`. Recognition lands in
249556
+ * `subLabel`; this says only that the raw material exists.
249557
+ *
249558
+ * **Set once, never cleared.** A track that produced a gallery row produced
249559
+ * one; deleting the row later is the gallery's business, not this flag's.
249560
+ *
249561
+ * **Absent ≠ false**, the same rule as {@link hasFace}: every row written
249562
+ * before the column omits it, and so does every server that predates the
249563
+ * field. A consumer must test `=== true` and render nothing otherwise —
249564
+ * never infer "no assignable face".
249565
+ */
249566
+ hasEmbeddedFace: zod.z.boolean().optional(),
249567
+ /**
249113
249568
  * This subject CONTAINS a folded rider — a person the rider-pairing step
249114
249569
  * ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
249115
249570
  * so the passage is tracked once and as a VEHICLE.
@@ -250870,6 +251325,32 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
250870
251325
  /** FRAME-space bbox (already mapped back from crop space). */
250871
251326
  bbox: NativeCropBboxSchema.optional(),
250872
251327
  embedding: zod.z.string().optional(),
251328
+ /**
251329
+ * L2 magnitude of the RAW pre-normalization embedding. Additive and optional
251330
+ * so an older runner that does not send it degrades to "unmeasurable", which
251331
+ * the consuming gate treats as accept — never as reject.
251332
+ */
251333
+ embeddingMagnitude: zod.z.number().optional(),
251334
+ /**
251335
+ * Model that produced `embedding` — the SAME contract `labelModelId` has for
251336
+ * `label`, and for the same reason: the producing node is the only side that
251337
+ * knows which model actually ran, and re-deriving it downstream is exactly
251338
+ * the inference this model exists to forbid.
251339
+ *
251340
+ * Additive and optional, so a runner that predates it degrades to
251341
+ * "unmeasurable" rather than to a wrong answer — the consumer falls back to
251342
+ * its previous assumption instead of refusing.
251343
+ *
251344
+ * Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
251345
+ * to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
251346
+ * with nothing on the wire it took the id from the GALLERY, so the gate
251347
+ * compared the gallery against itself and could never fire. That turned the
251348
+ * one guard protecting the face feature space into a tautology, and it is
251349
+ * why swapping `face-embedding`'s model would NOT have stranded the enrolled
251350
+ * gallery as the catalog comment predicts — it would have scored the new
251351
+ * model's vectors against the old model's samples in one cosine space.
251352
+ */
251353
+ embeddingModelId: zod.z.string().optional(),
250873
251354
  label: zod.z.string().optional(),
250874
251355
  /**
250875
251356
  * The tier `label` occupies, copied VERBATIM from the producing step's
@@ -251846,6 +252327,13 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
251846
252327
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
251847
252328
  fetchedAt: zod.z.number()
251848
252329
  });
252330
+ var INFERENCE_DEVICE_EXCLUSION_REASONS = [
252331
+ "disabled",
252332
+ "unavailable",
252333
+ "cannot-host-camera-root",
252334
+ "accelerator-preferred"
252335
+ ];
252336
+ var InferenceDeviceExclusionReasonSchema = zod.z.enum(INFERENCE_DEVICE_EXCLUSION_REASONS);
251849
252337
  var NodeInferenceDeviceSchema = zod.z.object({
251850
252338
  /** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
251851
252339
  key: zod.z.string(),
@@ -251876,7 +252364,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
251876
252364
  * available per format; this is the stored selection that becomes the
251877
252365
  * default for EVERY camera landing on this accelerator.
251878
252366
  */
251879
- steps: zod.z.record(zod.z.string(), DeviceStepConfigSchema).optional()
252367
+ steps: zod.z.record(zod.z.string(), DeviceStepConfigSchema).optional(),
252368
+ /**
252369
+ * `null` when the device IS a camera-root candidate on this node; otherwise
252370
+ * the reason the dispatcher drops it. Computed by the SAME
252371
+ * `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
252372
+ * never disagree with the election — deriving it in the UI from
252373
+ * `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
252374
+ * the node's model catalog) and `accelerator-preferred` (needs the node-wide
252375
+ * "an accelerator is serving" predicate).
252376
+ */
252377
+ exclusion: InferenceDeviceExclusionReasonSchema.nullable()
251880
252378
  });
251881
252379
  var NodeInferenceDevicesSchema = zod.z.object({
251882
252380
  nodeId: zod.z.string(),
@@ -252694,6 +253192,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252694
253192
  isBattery: zod.z.boolean(),
252695
253193
  reason: zod.z.enum([
252696
253194
  "disabled",
253195
+ "offline",
252697
253196
  "sleeping",
252698
253197
  "unreachable",
252699
253198
  "waking"
@@ -252784,9 +253283,17 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
252784
253283
  sleeping: zod.z.boolean(),
252785
253284
  /** Current device state rendered over the cached frame. State images
252786
253285
  * remain authoritative even when their photographic background is
252787
- * old; null means the link must carry a current camera frame. */
253286
+ * old; null means the link must carry a current camera frame.
253287
+ *
253288
+ * **This is the ONLY source a surface may derive an inactive tile
253289
+ * from.** A client that reads a device's `online`/`disabled` flags and
253290
+ * draws its own verdict will disagree with the pixels it is painting
253291
+ * — the frame comes from here, and so must the sentence over it
253292
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
253293
+ * state. */
252788
253294
  stateReason: zod.z.enum([
252789
253295
  "disabled",
253296
+ "offline",
252790
253297
  "sleeping",
252791
253298
  "unreachable",
252792
253299
  "waking"
@@ -256899,7 +257406,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
256899
257406
  */
256900
257407
  priority: zod.z.number()
256901
257408
  });
256902
- var GetConnectionEndpointsResultSchema = zod.z.object({ endpoints: zod.z.array(ConnectionEndpointSchema).readonly() });
257409
+ var LocalPortSourceEnum = zod.z.enum([
257410
+ "server-config",
257411
+ "server-env",
257412
+ "caller-hint",
257413
+ "default"
257414
+ ]);
257415
+ var AdvertisedLocalPortSchema = zod.z.object({
257416
+ port: zod.z.number().int().min(1).max(65535),
257417
+ source: LocalPortSourceEnum
257418
+ });
257419
+ var GetConnectionEndpointsResultSchema = zod.z.object({
257420
+ endpoints: zod.z.array(ConnectionEndpointSchema).readonly(),
257421
+ /**
257422
+ * The port the hub built the LAN/loopback URLs with, and where that number
257423
+ * came from.
257424
+ *
257425
+ * Returned rather than merely applied, because "the URL is right" and "the
257426
+ * client can KNOW the URL is right" are different properties. A client that
257427
+ * only sees a corrected URL cannot distinguish a hub that fixed the port from
257428
+ * a hub that echoed the port the client sent, so it cannot decide whether to
257429
+ * race the candidate or discard it. With `source` it can: anything but
257430
+ * `caller-hint` is the hub's own socket.
257431
+ *
257432
+ * Absent on hubs predating this field — a client that finds it missing is
257433
+ * talking to an echoing hub and must degrade exactly as it does for
257434
+ * `caller-hint`.
257435
+ */
257436
+ localPort: AdvertisedLocalPortSchema
257437
+ });
256903
257438
  var NotificationEndpointSchema = zod.z.object({
256904
257439
  /** The operator's explicit choice, or null for AUTO. */
256905
257440
  baseUrl: zod.z.string().nullable(),
@@ -256941,10 +257476,35 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
256941
257476
  * Honours `getAllowedAddresses()` when set — addresses outside
256942
257477
  * the allowlist are dropped (the public tunnel + loopback are
256943
257478
  * always included as escape hatches).
257479
+ *
257480
+ * **The port is the hub's, not the caller's** (D62 — a function's fact
257481
+ * belongs to whoever already owns it). This method used to take a `port`
257482
+ * and echo it onto every LAN and loopback URL it advertised, which made
257483
+ * every client a second authority on a socket only the hub binds. A viewer
257484
+ * configured with a port-less public URL (`https://camstack.example.top`)
257485
+ * infers 443 from the scheme, asks with 443, and was handed
257486
+ * `https://192.168.1.9:443` for a hub listening on 4443. That candidate
257487
+ * cannot ever answer, and losing its race reads in the log exactly like
257488
+ * "the LAN was tried and it was slower" — so an operator sitting on his own
257489
+ * WiFi stayed on the public tunnel for weeks, with the LAN candidate born
257490
+ * dead on every single connect.
257491
+ *
257492
+ * The hub now resolves its own listen port and reports it as `localPort`,
257493
+ * with the SOURCE of the number, so a client can tell a fact from an echo.
257494
+ *
257495
+ * Out of scope on purpose: a LAN URL that is not the hub's own socket (a
257496
+ * reverse proxy in front of it on another port) is not this method's to
257497
+ * invent either — pin it with `setNotificationEndpoint` / a configured
257498
+ * origin, which is an operator statement rather than a guess.
256944
257499
  */
256945
257500
  getConnectionEndpoints: require_sleep.method(zod.z.object({
256946
- /** Local hub HTTP port to use in base URLs. */
256947
- port: zod.z.number().int().min(1).max(65535),
257501
+ /**
257502
+ * LEGACY HINT — do not send from new code. Kept optional so clients
257503
+ * written against the echoing contract keep working; the hub uses it
257504
+ * only when it cannot read its own port, and says so via
257505
+ * `localPort.source === 'caller-hint'`.
257506
+ */
257507
+ port: zod.z.number().int().min(1).max(65535).optional(),
256948
257508
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
256949
257509
  * candidate. Default `true`. */
256950
257510
  includeLoopback: zod.z.boolean().optional(),
@@ -263188,10 +263748,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
263188
263748
  } };
263189
263749
  }
263190
263750
  var LAST_FETCHED_FIELD = "lastFetchedAt";
263751
+ var RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = 6e4;
263752
+ var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
263191
263753
  function createRuntimeStateBridge(params) {
263192
263754
  const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
263193
263755
  const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
263756
+ const staleRead = params.staleRead ?? "await-refresh";
263194
263757
  let missCooldownUntil = 0;
263758
+ let refreshInFlight = null;
263195
263759
  const readFetchedAt = () => {
263196
263760
  const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
263197
263761
  return typeof value === "number" ? value : 0;
@@ -263206,14 +263770,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
263206
263770
  }
263207
263771
  });
263208
263772
  };
263209
- const ensureFresh = async () => {
263210
- const slice = runtimeState.getCapState(cap.name);
263211
- const fetchedAt = readFetchedAt();
263212
- if (slice && Date.now() - fetchedAt <= staleMs) {
263213
- missCooldownUntil = 0;
263214
- return;
263215
- }
263216
- if (Date.now() < missCooldownUntil) return;
263773
+ const runRefresh = async (fetchedAt) => {
263217
263774
  try {
263218
263775
  await refresh();
263219
263776
  } catch (err) {
@@ -263229,6 +263786,34 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
263229
263786
  }
263230
263787
  openMissCooldown(void 0);
263231
263788
  };
263789
+ const startRefresh = (fetchedAt) => {
263790
+ const existing = refreshInFlight;
263791
+ if (existing !== null) return existing;
263792
+ const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
263793
+ refreshInFlight = null;
263794
+ });
263795
+ refreshInFlight = started;
263796
+ return started;
263797
+ };
263798
+ const ensureFresh = async () => {
263799
+ const slice = runtimeState.getCapState(cap.name);
263800
+ const fetchedAt = readFetchedAt();
263801
+ if (slice && Date.now() - fetchedAt <= staleMs) {
263802
+ missCooldownUntil = 0;
263803
+ return;
263804
+ }
263805
+ if (Date.now() < missCooldownUntil) return;
263806
+ if (staleRead === "serve-and-revalidate") {
263807
+ if (refreshInFlight !== null) return;
263808
+ if (slice && fetchedAt > 0) {
263809
+ startRefresh(fetchedAt);
263810
+ return;
263811
+ }
263812
+ await startRefresh(fetchedAt);
263813
+ return;
263814
+ }
263815
+ await runRefresh(fetchedAt);
263816
+ };
263232
263817
  const projectStatus = () => {
263233
263818
  const slice = runtimeState.getCapState(cap.name);
263234
263819
  if (!slice) return empty();
@@ -273776,6 +274361,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273776
274361
  function normalizeAudioLabel(value) {
273777
274362
  return value.startsWith(AUDIO_KIND_PREFIX) ? value.slice(6) : value;
273778
274363
  }
274364
+ function audioKindId(macroId) {
274365
+ return macroId.startsWith(AUDIO_KIND_PREFIX) ? macroId : `${AUDIO_KIND_PREFIX}${macroId}`;
274366
+ }
273779
274367
  var NC_AUDIO_DB_MIN = -96;
273780
274368
  var NC_AUDIO_DB_MAX = 0;
273781
274369
  var NC_AUDIO_DB_STEP = 3;
@@ -273823,8 +274411,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273823
274411
  samplingSeconds: clampInt(patch.samplingSeconds ?? base.samplingSeconds, 1, 300)
273824
274412
  };
273825
274413
  }
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
274414
  function audioLabelChoices(taxonomy, selected) {
273829
274415
  const fromHub = taxonomy?.audioKinds ?? [];
273830
274416
  const choices = fromHub.length > 0 ? fromHub.map((entry) => {
@@ -273832,13 +274418,13 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273832
274418
  return {
273833
274419
  value: id,
273834
274420
  label: entry.label,
273835
- icon: ICON_BY_ID.get(id) ?? UNKNOWN_LABEL_ICON,
274421
+ iconId: audioKindId(id),
273836
274422
  unknown: false
273837
274423
  };
273838
274424
  }) : AUDIO_MACRO_LABELS.map((macro2) => ({
273839
274425
  value: macro2.id,
273840
274426
  label: macro2.name,
273841
- icon: macro2.icon ?? UNKNOWN_LABEL_ICON,
274427
+ iconId: audioKindId(macro2.id),
273842
274428
  unknown: false
273843
274429
  }));
273844
274430
  const known = new Set(choices.map((choice) => choice.value));
@@ -273850,7 +274436,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
273850
274436
  choices.push({
273851
274437
  value: id,
273852
274438
  label: id,
273853
- icon: UNKNOWN_LABEL_ICON,
274439
+ iconId: audioKindId(id),
273854
274440
  unknown: true
273855
274441
  });
273856
274442
  }
@@ -274574,6 +275160,74 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274574
275160
  if (map !== void 0) return map[String(deviceId)] ?? 0;
274575
275161
  return rule.lastGeneratedAt ?? 0;
274576
275162
  }
275163
+ var CLUSTER_MODEL_SCOPED_STEPS = [{
275164
+ stepId: "face-embedding",
275165
+ label: "Face recognition model",
275166
+ defaultModelId: "arcface-r100",
275167
+ indexName: "the enrolled face gallery",
275168
+ options: [
275169
+ {
275170
+ id: "arcface-r100",
275171
+ label: "ArcFace (ResNet34)"
275172
+ },
275173
+ {
275174
+ id: "auraface-r100",
275175
+ label: "AuraFace R100"
275176
+ },
275177
+ {
275178
+ id: "inception-resnet-v1",
275179
+ label: "Inception ResNet V1"
275180
+ }
275181
+ ]
275182
+ }, {
275183
+ stepId: "clip-embedding",
275184
+ label: "Semantic search model",
275185
+ defaultModelId: "mobileclip-s1",
275186
+ indexName: "the object semantic-search index",
275187
+ options: [{
275188
+ id: "mobileclip-s1",
275189
+ label: "MobileCLIP S1"
275190
+ }, {
275191
+ id: "mobileclip-s2",
275192
+ label: "MobileCLIP S2"
275193
+ }]
275194
+ }];
275195
+ var CLUSTER_MODEL_SECTION_ID = "cluster-step-models";
275196
+ function clusterModelSettingKey(stepId) {
275197
+ return `clusterModel:${stepId}`;
275198
+ }
275199
+ function isClusterScopedStep(stepId) {
275200
+ return CLUSTER_MODEL_SCOPED_STEPS.some((s) => s.stepId === stepId);
275201
+ }
275202
+ var DEFAULT_CLUSTER_STEP_MODELS = Object.freeze(Object.fromEntries(CLUSTER_MODEL_SCOPED_STEPS.map((s) => [s.stepId, s.defaultModelId])));
275203
+ var ChosenModelIdSchema = zod.z.string().min(1);
275204
+ function readClusterStepModels(config) {
275205
+ const out = {};
275206
+ for (const step2 of CLUSTER_MODEL_SCOPED_STEPS) {
275207
+ const parsed = ChosenModelIdSchema.safeParse(config[clusterModelSettingKey(step2.stepId)]);
275208
+ out[step2.stepId] = parsed.success ? parsed.data : step2.defaultModelId;
275209
+ }
275210
+ return out;
275211
+ }
275212
+ function isHydratedField$2(entry) {
275213
+ return typeof entry === "object" && entry !== null && "key" in entry;
275214
+ }
275215
+ function pickClusterStepModels(view) {
275216
+ if (view === null) return DEFAULT_CLUSTER_STEP_MODELS;
275217
+ const flat = {};
275218
+ const wanted = new Set(CLUSTER_MODEL_SCOPED_STEPS.map((s) => clusterModelSettingKey(s.stepId)));
275219
+ for (const section of view.sections) for (const entry of section.fields) {
275220
+ if (!isHydratedField$2(entry) || typeof entry.key !== "string") continue;
275221
+ if (wanted.has(entry.key)) flat[entry.key] = entry.value;
275222
+ }
275223
+ return readClusterStepModels(flat);
275224
+ }
275225
+ function resolveClusterStepModelId(stepId, models) {
275226
+ const step2 = CLUSTER_MODEL_SCOPED_STEPS.find((s) => s.stepId === stepId);
275227
+ if (step2 === void 0) return null;
275228
+ const chosen = models[stepId];
275229
+ return chosen !== void 0 && chosen !== "" ? chosen : step2.defaultModelId;
275230
+ }
274577
275231
  var DETAIL_CROP_SECTION_ID = "detail-crop";
274578
275232
  var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
274579
275233
  var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
@@ -274621,6 +275275,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
274621
275275
  }
274622
275276
  return readDetailCropConvention(flat);
274623
275277
  }
275278
+ var FULL_IMAGE_BBOX = Object.freeze({
275279
+ x: 0,
275280
+ y: 0,
275281
+ w: 1,
275282
+ h: 1
275283
+ });
274624
275284
  var DETAIL_CROP_PADDING_FIELD = {
274625
275285
  min: 0,
274626
275286
  max: 1,
@@ -276356,6 +277016,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276356
277016
  exports.CAP_NAMES_WITH_STATUS = CAP_NAMES_WITH_STATUS;
276357
277017
  exports.CAP_NODE_PIN_CONTEXT_KEY = require_sleep.CAP_NODE_PIN_CONTEXT_KEY;
276358
277018
  exports.CAP_PROVIDER_KIND_MAP = CAP_PROVIDER_KIND_MAP;
277019
+ exports.CLUSTER_MODEL_SCOPED_STEPS = CLUSTER_MODEL_SCOPED_STEPS;
277020
+ exports.CLUSTER_MODEL_SECTION_ID = CLUSTER_MODEL_SECTION_ID;
276359
277021
  exports.COCO_80_LABELS = COCO_80_LABELS;
276360
277022
  exports.COCO_TO_MACRO = COCO_TO_MACRO;
276361
277023
  exports.CONNECTION_TEST_TIMEOUT_MS = CONNECTION_TEST_TIMEOUT_MS;
@@ -276443,6 +277105,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276443
277105
  exports.DECLARED_INTEGRATION_FIXED_KEY = DECLARED_INTEGRATION_FIXED_KEY;
276444
277106
  exports.DEFAULT_ADDON_PLACEMENT = DEFAULT_ADDON_PLACEMENT;
276445
277107
  exports.DEFAULT_AUDIO_ANALYZER_CONFIG = DEFAULT_AUDIO_ANALYZER_CONFIG;
277108
+ exports.DEFAULT_CLUSTER_STEP_MODELS = DEFAULT_CLUSTER_STEP_MODELS;
276446
277109
  exports.DEFAULT_DECODER_HWACCEL_CONFIG = DEFAULT_DECODER_HWACCEL_CONFIG;
276447
277110
  exports.DEFAULT_DETAIL_CROP_CONVENTION = DEFAULT_DETAIL_CROP_CONVENTION;
276448
277111
  exports.DEFAULT_EVENTS_BAND_BUFFER_SEC = DEFAULT_EVENTS_BAND_BUFFER_SEC;
@@ -276561,6 +277224,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276561
277224
  exports.ExpressionLiteralBindingSchema = ExpressionLiteralBindingSchema;
276562
277225
  exports.ExpressionParseError = ExpressionParseError;
276563
277226
  exports.ExpressionSourceSchema = ExpressionSourceSchema;
277227
+ exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
276564
277228
  exports.FanControlStatusSchema = FanControlStatusSchema;
276565
277229
  exports.FanDirectionSchema = FanDirectionSchema;
276566
277230
  exports.FeatureManifestSchema = FeatureManifestSchema;
@@ -276587,6 +277251,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276587
277251
  exports.HumidifierStatusSchema = HumidifierStatusSchema;
276588
277252
  exports.HumiditySensorStatusSchema = HumiditySensorStatusSchema;
276589
277253
  exports.HvacModeSchema = HvacModeSchema;
277254
+ exports.INFERENCE_DEVICE_EXCLUSION_REASONS = INFERENCE_DEVICE_EXCLUSION_REASONS;
276590
277255
  exports.ImageContractSchema = ImageContractSchema;
276591
277256
  exports.ImageContractStateSchema = ImageContractStateSchema;
276592
277257
  exports.ImageRotateSchema = ImageRotateSchema;
@@ -276594,6 +277259,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276594
277259
  exports.ImageSettingsPatchSchema = ImageSettingsPatchSchema;
276595
277260
  exports.ImageSettingsStatusSchema = ImageSettingsStatusSchema;
276596
277261
  exports.ImageStatusSchema = ImageStatusSchema;
277262
+ exports.InferenceDeviceExclusionReasonSchema = InferenceDeviceExclusionReasonSchema;
276597
277263
  exports.IngestOwnerSchema = IngestOwnerSchema;
276598
277264
  exports.InstalledPackageSchema = InstalledPackageSchema;
276599
277265
  exports.IntegrationLiteSchema = IntegrationLiteSchema;
@@ -276806,6 +277472,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276806
277472
  exports.NotificationSchema = NotificationSchema;
276807
277473
  exports.NotifierStatusSchema = NotifierStatusSchema;
276808
277474
  exports.NumericSensorStatusSchema = NumericSensorStatusSchema;
277475
+ exports.OPERATOR_WRITTEN_STALE_MS = OPERATOR_WRITTEN_STALE_MS;
276809
277476
  exports.OPS_LOG_DEFAULT_LIMIT = OPS_LOG_DEFAULT_LIMIT;
276810
277477
  exports.OPS_LOG_RING_DEFAULT_MAX = OPS_LOG_RING_DEFAULT_MAX;
276811
277478
  exports.OauthIntegrationDescriptorSchema = OauthIntegrationDescriptorSchema;
@@ -276888,6 +277555,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
276888
277555
  exports.RESTORED_CAP_NAMES = RESTORED_CAP_NAMES;
276889
277556
  exports.RUNTIME_DEFAULTS = RUNTIME_DEFAULTS;
276890
277557
  exports.RUNTIME_STATE_POLICY = RUNTIME_STATE_POLICY;
277558
+ exports.RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS;
276891
277559
  exports.RUNTIME_TO_FORMAT = RUNTIME_TO_FORMAT;
276892
277560
  exports.RawStateResultSchema = require_sleep.RawStateResultSchema;
276893
277561
  exports.ReadGopBytesResultSchema = ReadGopBytesResultSchema;
@@ -277180,6 +277848,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277180
277848
  exports.audioAnalyzerCapability = audioAnalyzerCapability;
277181
277849
  exports.audioCodecCapability = audioCodecCapability;
277182
277850
  exports.audioIsFailClosed = audioIsFailClosed;
277851
+ exports.audioKindId = audioKindId;
277183
277852
  exports.audioLabelChoices = audioLabelChoices;
277184
277853
  exports.audioMetricsCapability = audioMetricsCapability;
277185
277854
  exports.audioModeOf = audioModeOf;
@@ -277218,6 +277887,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277218
277887
  exports.classifyStream = classifyStream;
277219
277888
  exports.classifyStreams = classifyStreams;
277220
277889
  exports.climateControlCapability = climateControlCapability;
277890
+ exports.clusterModelSettingKey = clusterModelSettingKey;
277221
277891
  exports.collectHydratedFieldEntries = require_sleep.collectHydratedFieldEntries;
277222
277892
  exports.collectHydratedFieldValues = require_sleep.collectHydratedFieldValues;
277223
277893
  exports.colorCapability = colorCapability;
@@ -277336,6 +278006,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277336
278006
  exports.isAudioRule = isAudioRule;
277337
278007
  exports.isBaseConditionKey = isBaseConditionKey;
277338
278008
  exports.isBatteryPresenceFault = isBatteryPresenceFault;
278009
+ exports.isClusterScopedStep = isClusterScopedStep;
277339
278010
  exports.isCollectionArrayMethod = isCollectionArrayMethod;
277340
278011
  exports.isDeployableToAgent = isDeployableToAgent;
277341
278012
  exports.isDetectionMacroClass = isDetectionMacroClass;
@@ -277419,6 +278090,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277419
278090
  exports.patchAudio = patchAudio;
277420
278091
  exports.petFeederCapability = petFeederCapability;
277421
278092
  exports.pickAccessoryControl = pickAccessoryControl;
278093
+ exports.pickClusterStepModels = pickClusterStepModels;
277422
278094
  exports.pickDetailCropConvention = pickDetailCropConvention;
277423
278095
  exports.pickNativeLeaseOverride = pickNativeLeaseOverride;
277424
278096
  exports.pickPreferredRtspEntry = pickPreferredRtspEntry;
@@ -277442,6 +278114,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277442
278114
  exports.ptzAutotrackCapability = ptzAutotrackCapability;
277443
278115
  exports.ptzCapability = ptzCapability;
277444
278116
  exports.pythonScriptForBackend = pythonScriptForBackend;
278117
+ exports.readClusterStepModels = readClusterStepModels;
277445
278118
  exports.readDetailCropConvention = readDetailCropConvention;
277446
278119
  exports.readDeviceStateFrom = readDeviceStateFrom;
277447
278120
  exports.readNativeLeaseOverride = readNativeLeaseOverride;
@@ -277459,6 +278132,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
277459
278132
  exports.resolveAddonPlacement = resolveAddonPlacement;
277460
278133
  exports.resolveAddonRuntime = resolveAddonRuntime;
277461
278134
  exports.resolveCapMount = require_sleep.resolveCapMount;
278135
+ exports.resolveClusterStepModelId = resolveClusterStepModelId;
277462
278136
  exports.resolveDetectionRuntime = resolveDetectionRuntime;
277463
278137
  exports.resolveDeviceControlKind = resolveDeviceControlKind;
277464
278138
  exports.resolveDeviceProfile = resolveDeviceProfile;
@@ -277768,22 +278442,20 @@ var require_update_availability_emitter = __commonJS({
277768
278442
  this.persistIfDirty();
277769
278443
  if (!latestChanged)
277770
278444
  return;
277771
- this.emitList(target);
278445
+ this.emitList(target, scope);
277772
278446
  }
277773
278447
  /** Publish a partial candidate set without treating omissions as up-to-date. */
277774
278448
  publishCandidates(candidates) {
277775
- let latestChanged = false;
277776
- const targets = /* @__PURE__ */ new Set();
278449
+ const changed = /* @__PURE__ */ new Map();
277777
278450
  for (const candidate of candidates) {
277778
- targets.add(candidate.target);
277779
- if (this.note(candidate))
277780
- latestChanged = true;
278451
+ if (!this.note(candidate))
278452
+ continue;
278453
+ const scope = scopeOf(candidate);
278454
+ changed.set(`${candidate.target}:${scope}`, { target: candidate.target, scope });
277781
278455
  }
277782
278456
  this.persistIfDirty();
277783
- if (!latestChanged)
277784
- return;
277785
- for (const target of targets)
277786
- this.emitList(target);
278457
+ for (const { target, scope } of changed.values())
278458
+ this.emitList(target, scope);
277787
278459
  }
277788
278460
  note(candidate) {
277789
278461
  const key = this.key(candidate);
@@ -277809,20 +278481,20 @@ var require_update_availability_emitter = __commonJS({
277809
278481
  held: Object.fromEntries(this.held)
277810
278482
  });
277811
278483
  }
277812
- emitList(target) {
277813
- const list = [...this.held.values()].filter((candidate) => candidate.target === target);
278484
+ /** Announce ONE scope's full list. `scope` is the node id, `hub` for the hub. */
278485
+ emitList(target, scope) {
278486
+ const list = [...this.held.values()].filter((candidate) => candidate.target === target && scopeOf(candidate) === scope);
277814
278487
  if (list.length === 0)
277815
278488
  return;
277816
278489
  const packages = list.map((candidate) => ({
277817
278490
  packageName: candidate.packageName,
277818
278491
  currentVersion: candidate.currentVersion,
277819
278492
  latestVersion: candidate.latestVersion,
277820
- ...candidate.nodeId !== void 0 ? { nodeId: candidate.nodeId } : {}
278493
+ nodeId: scope
277821
278494
  }));
277822
- const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
277823
278495
  const head = list[0];
277824
278496
  this.eventBus.emit({
277825
- id: `update.available:${target}:${packages.map((pkg) => `${pkg.nodeId ?? "hub"}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",")}`,
278497
+ id: `update.available:${target}:${scope}:${packages.map((pkg) => `${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",")}`,
277826
278498
  timestamp: /* @__PURE__ */ new Date(),
277827
278499
  source: this.source,
277828
278500
  category: types_1.EventCategory.UpdateAvailable,
@@ -277832,24 +278504,24 @@ var require_update_availability_emitter = __commonJS({
277832
278504
  currentVersion: head.currentVersion,
277833
278505
  latestVersion: head.latestVersion,
277834
278506
  packages,
277835
- ...head.nodeId !== void 0 ? { nodeId: head.nodeId } : {},
277836
- ...nodeIds.length > 0 ? { nodeIds } : {}
278507
+ nodeId: scope,
278508
+ nodeIds: [scope]
277837
278509
  }
277838
278510
  });
277839
278511
  }
277840
278512
  key(candidate) {
277841
- return `${candidate.target}:${candidate.nodeId ?? "hub"}:${candidate.packageName}`;
278513
+ return `${candidate.target}:${scopeOf(candidate)}:${candidate.packageName}`;
277842
278514
  }
277843
278515
  };
277844
278516
  exports.UpdateAvailabilityEmitter = UpdateAvailabilityEmitter;
278517
+ function scopeOf(candidate) {
278518
+ return candidate.nodeId ?? "hub";
278519
+ }
277845
278520
  function stampNode(candidate, nodeId) {
277846
278521
  if (nodeId === void 0 || candidate.nodeId !== void 0)
277847
278522
  return candidate;
277848
278523
  return { ...candidate, nodeId };
277849
278524
  }
277850
- function unique(values) {
277851
- return [...new Set(values)];
277852
- }
277853
278525
  }
277854
278526
  });
277855
278527
 
@@ -279204,7 +279876,13 @@ var require_server_root = __commonJS({
279204
279876
  fromVersion: runningVersion
279205
279877
  }
279206
279878
  });
279207
- this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`);
279879
+ this.restartServerFn(`${RESTART_REASON_PREFIX}: ${this.spec.packageName}@${target}`, {
279880
+ packageName: this.spec.packageName,
279881
+ ...runningVersion !== null ? {
279882
+ fromVersion: runningVersion
279883
+ } : {},
279884
+ toVersion: target
279885
+ });
279208
279886
  return {
279209
279887
  accepted: true,
279210
279888
  targetVersion: target,
@@ -279489,6 +280167,47 @@ var require_server_root = __commonJS({
279489
280167
  }
279490
280168
  });
279491
280169
 
280170
+ // ../../server/backend/dist/core/updates/restart-marker.js
280171
+ var require_restart_marker = __commonJS({
280172
+ "../../server/backend/dist/core/updates/restart-marker.js"(exports) {
280173
+ "use strict";
280174
+ Object.defineProperty(exports, "__esModule", { value: true });
280175
+ exports.buildRestartMarker = buildRestartMarker;
280176
+ exports.isServerUpdateReason = isServerUpdateReason;
280177
+ exports.versionFromReason = versionFromReason;
280178
+ var SERVER_UPDATE_PREFIX = "server-update:";
280179
+ var ROOT_PACKAGE_NAME = "@camstack/server";
280180
+ function buildRestartMarker(input) {
280181
+ const { requestedBy, update, requestedAt } = input;
280182
+ const toVersion = update?.toVersion ?? versionFromReason(requestedBy);
280183
+ const isUpdate = update !== void 0 || isServerUpdateReason(requestedBy) && toVersion !== void 0;
280184
+ if (!isUpdate) {
280185
+ return {
280186
+ kind: "manual",
280187
+ requestedAt,
280188
+ ...requestedBy !== void 0 ? { requestedBy } : {}
280189
+ };
280190
+ }
280191
+ return {
280192
+ kind: "framework-update",
280193
+ requestedAt,
280194
+ ...requestedBy !== void 0 ? { requestedBy } : {},
280195
+ packageName: update?.packageName ?? ROOT_PACKAGE_NAME,
280196
+ ...update?.fromVersion !== void 0 ? { fromVersion: update.fromVersion } : {},
280197
+ ...toVersion !== void 0 ? { toVersion } : {}
280198
+ };
280199
+ }
280200
+ function isServerUpdateReason(requestedBy) {
280201
+ return requestedBy !== void 0 && requestedBy.startsWith(SERVER_UPDATE_PREFIX);
280202
+ }
280203
+ function versionFromReason(requestedBy) {
280204
+ if (requestedBy === void 0)
280205
+ return void 0;
280206
+ return /@([^@\s]+)$/.exec(requestedBy)?.[1];
280207
+ }
280208
+ }
280209
+ });
280210
+
279492
280211
  // ../../server/backend/dist/agent/agent-update-service.js
279493
280212
  var require_agent_update_service = __commonJS({
279494
280213
  "../../server/backend/dist/agent/agent-update-service.js"(exports) {
@@ -279546,6 +280265,7 @@ var require_agent_update_service = __commonJS({
279546
280265
  var update_availability_emitter_js_1 = require_update_availability_emitter();
279547
280266
  var update_availability_store_js_1 = require_update_availability_store();
279548
280267
  var index_js_1 = require_server_root();
280268
+ var restart_marker_js_1 = require_restart_marker();
279549
280269
  exports.AGENT_RUNTIME_ADDON_ID = "agent-runtime";
279550
280270
  exports.AGENT_RESTART_GRACE_MS = 2e3;
279551
280271
  function isRunningInContainer(existsSyncFn) {
@@ -279609,8 +280329,8 @@ var require_agent_update_service = __commonJS({
279609
280329
  seedDir: "CAMSTACK_SEED_SERVER_DIR"
279610
280330
  },
279611
280331
  logger: options.logger,
279612
- restartServer: (requestedBy) => {
279613
- writeAgentRestartMarker(options.dataDir, requestedBy);
280332
+ restartServer: (requestedBy, update) => {
280333
+ writeAgentRestartMarker(options.dataDir, requestedBy, update);
279614
280334
  restartAgent(requestedBy);
279615
280335
  },
279616
280336
  dataDir: options.dataDir,
@@ -279688,18 +280408,13 @@ var require_agent_update_service = __commonJS({
279688
280408
  }
279689
280409
  };
279690
280410
  exports.AgentUpdateService = AgentUpdateService;
279691
- function writeAgentRestartMarker(dataDir, requestedBy) {
279692
- const toVersion = /@([^@\s]+)$/.exec(requestedBy)?.[1];
280411
+ function writeAgentRestartMarker(dataDir, requestedBy, update) {
279693
280412
  try {
279694
- (0, system_1.writePendingRestart)(dataDir, {
279695
- kind: requestedBy.startsWith("server-update:") ? "framework-update" : "manual",
280413
+ (0, system_1.writePendingRestart)(dataDir, (0, restart_marker_js_1.buildRestartMarker)({
279696
280414
  requestedAt: Date.now(),
279697
280415
  requestedBy,
279698
- ...requestedBy.startsWith("server-update:") ? {
279699
- packageName: "@camstack/server",
279700
- ...toVersion !== void 0 ? { toVersion } : {}
279701
- } : {}
279702
- });
280416
+ ...update !== void 0 ? { update } : {}
280417
+ }));
279703
280418
  } catch {
279704
280419
  }
279705
280420
  }
@@ -388570,6 +389285,7 @@ var require_addon_package_service = __commonJS({
388570
389285
  var types_1 = require_dist9();
388571
389286
  var update_availability_emitter_js_1 = require_update_availability_emitter();
388572
389287
  var update_check_scheduler_js_1 = require_update_check_scheduler();
389288
+ var restart_marker_js_1 = require_restart_marker();
388573
389289
  var package_dir_utils_js_1 = require_package_dir_utils();
388574
389290
  var execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
388575
389291
  exports.SYSTEM_PACKAGE = "@camstack/system";
@@ -389497,26 +390213,24 @@ var require_addon_package_service = __commonJS({
389497
390213
  /**
389498
390214
  * Gracefully restart the server process.
389499
390215
  *
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`
390216
+ * Writes the restart marker so the post-boot service can emit
390217
+ * `system.restart-completed` on the next boot (powers the admin-UI
390218
+ * "Server restarted" toast AND the operator's "Server aggiornato"
390219
+ * notification). `update` is passed by `RootUpdateService` for an
390220
+ * update-driven restart and is the ONLY source of the version being left
390221
+ * behind — see `updates/restart-marker.ts`. Emits `system.restarting`
389503
390222
  * with the same payload so subscribers can show a reconnect overlay
389504
390223
  * immediately. Then delegates to `scheduleSelfRestart` which picks
389505
390224
  * the right exit path (Electron `app.relaunch` vs supervisor-driven
389506
390225
  * `process.exit`).
389507
390226
  */
389508
- restartServer(requestedBy) {
390227
+ restartServer(requestedBy, update) {
389509
390228
  this.logger.info("Server restart requested -- initiating graceful shutdown");
389510
- const toVersion = toVersionFromRestartReason(requestedBy);
389511
- const payload = {
389512
- kind: isServerUpdateRequest(requestedBy) ? "framework-update" : "manual",
390229
+ const payload = (0, restart_marker_js_1.buildRestartMarker)({
389513
390230
  requestedAt: Date.now(),
389514
390231
  ...requestedBy !== void 0 ? { requestedBy } : {},
389515
- ...isServerUpdateRequest(requestedBy) ? {
389516
- packageName: "@camstack/server",
389517
- ...toVersion !== void 0 ? { toVersion } : {}
389518
- } : {}
389519
- };
390232
+ ...update !== void 0 ? { update } : {}
390233
+ });
389520
390234
  try {
389521
390235
  (0, system_1.writePendingRestart)(this.resolveDataDir(), payload);
389522
390236
  } catch (err) {
@@ -390007,22 +390721,23 @@ var require_addon_package_service = __commonJS({
390007
390721
  * re-announces them on the next good poll.
390008
390722
  */
390009
390723
  publishAddonAvailability(updates, failures, nodeId) {
390724
+ const scope = nodeId ?? this.resolveNodeId();
390010
390725
  const candidates = updates.map((update) => ({
390011
390726
  target: "addon",
390012
390727
  packageName: update.name,
390013
390728
  currentVersion: update.currentVersion,
390014
390729
  latestVersion: update.latestVersion,
390015
- ...nodeId !== void 0 ? { nodeId } : {}
390730
+ nodeId: scope
390016
390731
  }));
390017
390732
  if (failures > 0) {
390018
390733
  this.logger.warn("Addon update check incomplete \u2014 availability published as partial", {
390019
- tags: { nodeId: nodeId ?? "hub" },
390734
+ tags: { nodeId: scope },
390020
390735
  meta: { failures, published: candidates.length }
390021
390736
  });
390022
390737
  this.updateAvailability.publishCandidates(candidates);
390023
390738
  return;
390024
390739
  }
390025
- this.updateAvailability.publishSnapshot("addon", candidates, nodeId);
390740
+ this.updateAvailability.publishSnapshot("addon", candidates, scope);
390026
390741
  }
390027
390742
  /** Fetch npm search results for camstack addon packages (cached 5 min) */
390028
390743
  async fetchSearchFromNpm() {
@@ -390172,15 +390887,6 @@ var require_addon_package_service = __commonJS({
390172
390887
  }
390173
390888
  };
390174
390889
  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
390890
  async function extractTgzStripped(tgz, destDir) {
390185
390891
  fs.mkdirSync(destDir, { recursive: true });
390186
390892
  const tmpFile = path.join(os.tmpdir(), `camstack-tgz-${(0, node_crypto_1.randomUUID)()}.tgz`);
@@ -401689,14 +402395,6 @@ var require_integration_visibility = __commonJS({
401689
402395
  function createFilteredIntegrationRegistry(deps) {
401690
402396
  const { raw, logger } = deps;
401691
402397
  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
402398
  const logHidden = (integration, phase) => {
401701
402399
  logger.warn("integration hidden \u2014 its addon is not installed, so this read answers null even though the row exists", {
401702
402400
  tags: { integrationId: integration.id, addonId: integration.addonId },
@@ -401742,39 +402440,7 @@ var require_integration_visibility = __commonJS({
401742
402440
  // Integration settings: passthrough (already gated by getIntegration)
401743
402441
  getIntegrationSettings: (id) => raw.getIntegrationSettings(id),
401744
402442
  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)
402443
+ setIntegrationSettings: (id, settings) => raw.setIntegrationSettings(id, settings)
401778
402444
  };
401779
402445
  }
401780
402446
  }
@@ -406355,6 +407021,7 @@ var require_moleculer_service = __commonJS({
406355
407021
  "use strict";
406356
407022
  Object.defineProperty(exports, "__esModule", { value: true });
406357
407023
  exports.MoleculerService = void 0;
407024
+ exports.childOwnerToken = childOwnerToken;
406358
407025
  exports.buildChildUdsManifest = buildChildUdsManifest;
406359
407026
  var node_crypto_1 = __require("crypto");
406360
407027
  var system_1 = require_dist3();
@@ -406381,6 +407048,21 @@ var require_moleculer_service = __commonJS({
406381
407048
  * `$hub.registerNode`. Populated by `onRegisterNode` in `hubDeps`.
406382
407049
  */
406383
407050
  nodeRegistry = new system_1.HubNodeRegistry();
407051
+ /**
407052
+ * nodeId → the owner token of the INCARNATION whose manifest currently backs
407053
+ * that node's capability registrations.
407054
+ *
407055
+ * A node id is not unique over time: `resolveRunnerId` is deterministic, so a
407056
+ * runner replaced by an addon update reconnects as the same `hub/<runner>`
407057
+ * node. Without an incarnation the predecessor's teardown and the successor's
407058
+ * registration are the same `(cap, addonId)` key, and the teardown wins by
407059
+ * arriving last — `recording`, `storage-evictable` and `recording-export`
407060
+ * were unregistered from a healthy process for 7 hours on 2026-08-20.
407061
+ * Populated only for transports that can observe an incarnation (UDS
407062
+ * children); remote nodes stay ownerless and behave exactly as before.
407063
+ * See `docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md`.
407064
+ */
407065
+ nodeOwners = /* @__PURE__ */ new Map();
406384
407066
  /**
406385
407067
  * Fixed-period agent-readiness snapshot sweep (D8 reconcile) — repairs
406386
407068
  * agent-origin readiness deltas lost while the agent stayed connected.
@@ -406702,14 +407384,18 @@ var require_moleculer_service = __commonJS({
406702
407384
  const hubNodeId = this.brokerSafe.nodeID;
406703
407385
  const childNodeId = `${hubNodeId}/${child.childId}`;
406704
407386
  const params = buildChildUdsManifest(childNodeId, child.childId, child.caps);
406705
- this.onRegisterNode(params);
406706
- logger.info("UDS child registered \u2014 manifest applied", { meta: { nodeId: childNodeId } });
407387
+ this.onRegisterNode(params, childOwnerToken(childNodeId, child.incarnation));
407388
+ logger.info("UDS child registered \u2014 manifest applied", {
407389
+ meta: { nodeId: childNodeId, incarnation: child.incarnation }
407390
+ });
406707
407391
  });
406708
- registry.onChildGone((childId) => {
407392
+ registry.onChildGone((childId, incarnation) => {
406709
407393
  const hubNodeId = this.brokerSafe.nodeID;
406710
407394
  const childNodeId = `${hubNodeId}/${childId}`;
406711
- logger.info("UDS child gone \u2014 removing from registry", { meta: { childId } });
406712
- this.removeNodeFromRegistry(childNodeId);
407395
+ logger.info("UDS child gone \u2014 removing from registry", {
407396
+ meta: { childId, incarnation }
407397
+ });
407398
+ this.removeNodeFromRegistry(childNodeId, childOwnerToken(childNodeId, incarnation));
406713
407399
  });
406714
407400
  registry.onChildLog((childId, entry) => {
406715
407401
  this.logging.writeFromWorker((0, system_1.udsChildLogToWorkerEntry)(childId, entry));
@@ -406893,10 +407579,15 @@ var require_moleculer_service = __commonJS({
406893
407579
  * CapabilityRegistry update is a diff (atomic replace) rather than an
406894
407580
  * unconditional re-register — see `applyNodeManifest` for the rationale.
406895
407581
  */
406896
- onRegisterNode(params) {
407582
+ onRegisterNode(params, owner) {
406897
407583
  const previousManifest = this.nodeRegistry.getNodeManifest(params.nodeId);
407584
+ const previousOwner = this.nodeOwners.get(params.nodeId);
406898
407585
  this.nodeRegistry.registerNode(params);
406899
- this.applyNodeManifest(params, previousManifest);
407586
+ if (owner === void 0)
407587
+ this.nodeOwners.delete(params.nodeId);
407588
+ else
407589
+ this.nodeOwners.set(params.nodeId, owner);
407590
+ this.applyNodeManifest(params, previousManifest, owner, previousOwner);
406900
407591
  const { nodeId } = params;
406901
407592
  if (nodeId !== "hub" && !nodeId.includes("/") && this.onAgentRegisteredCb) {
406902
407593
  this.onAgentRegisteredCb(nodeId);
@@ -406924,7 +407615,7 @@ var require_moleculer_service = __commonJS({
406924
407615
  * already; this method handles only `params.addons` (system caps).
406925
407616
  * Native-cap wiring into device-manager is done in a later task.
406926
407617
  */
406927
- applyNodeManifest(params, previousManifest) {
407618
+ applyNodeManifest(params, previousManifest, owner, previousOwner) {
406928
407619
  const { nodeId, addons } = params;
406929
407620
  const hubNodeId = this.brokerSafe.nodeID;
406930
407621
  const isLocalChild = nodeId.startsWith(hubNodeId + "/");
@@ -406955,9 +407646,16 @@ var require_moleculer_service = __commonJS({
406955
407646
  for (const [key, { addonId, capName }] of previous) {
406956
407647
  if (desired.has(key))
406957
407648
  continue;
406958
- registry.unregisterProvider(capName, registryKeyFor(addonId));
407649
+ registry.unregisterProvider(capName, registryKeyFor(addonId), previousOwner);
406959
407650
  this.nodeCallFns.delete(`${nodeId}::${capName}`);
406960
407651
  }
407652
+ if (owner !== void 0 && previousOwner !== void 0 && owner !== previousOwner) {
407653
+ for (const [key, { addonId, capName }] of desired) {
407654
+ if (!previous.has(key))
407655
+ continue;
407656
+ registry.reassignProviderOwner(capName, registryKeyFor(addonId), owner);
407657
+ }
407658
+ }
406961
407659
  for (const [key, { addonId, capName, capDef }] of desired) {
406962
407660
  if (previous.has(key))
406963
407661
  continue;
@@ -406982,7 +407680,7 @@ var require_moleculer_service = __commonJS({
406982
407680
  for (const methodName of Object.keys((0, system_1.expandCapMethods)(capDef))) {
406983
407681
  proxy[methodName] = (methodParams) => callFn(methodName, methodParams);
406984
407682
  }
406985
- registry.registerProvider(capName, registryKey, proxy);
407683
+ registry.registerProvider(capName, registryKey, proxy, owner);
406986
407684
  if (capDef.mode === "singleton" && isLocalChild) {
406987
407685
  const activeKey = registry.getSingletonAddonId(capName);
406988
407686
  if (activeKey === null || activeKey.includes("@")) {
@@ -407015,7 +407713,14 @@ var require_moleculer_service = __commonJS({
407015
407713
  * Unregisters every cap the node's last manifest declared and emits
407016
407714
  * synthetic readiness-down events for each.
407017
407715
  */
407018
- removeNodeFromRegistry(nodeId) {
407716
+ removeNodeFromRegistry(nodeId, owner) {
407717
+ const currentOwner = this.nodeOwners.get(nodeId);
407718
+ if (currentOwner !== void 0 && currentOwner !== owner) {
407719
+ this.logger.info("Node teardown ignored \u2014 node id reclaimed by a newer incarnation", {
407720
+ meta: { nodeId, currentOwner, presentedOwner: owner ?? null }
407721
+ });
407722
+ return;
407723
+ }
407019
407724
  const manifest = this.nodeRegistry.getNodeManifest(nodeId);
407020
407725
  if (!manifest)
407021
407726
  return;
@@ -407029,7 +407734,7 @@ var require_moleculer_service = __commonJS({
407029
407734
  const registryKey = isLocalChild ? addonId : `${addonId}@${nodeId}`;
407030
407735
  if (registry) {
407031
407736
  for (const capName of capabilities) {
407032
- registry.unregisterProvider(capName, registryKey);
407737
+ registry.unregisterProvider(capName, registryKey, owner);
407033
407738
  }
407034
407739
  }
407035
407740
  for (const capName of capabilities) {
@@ -407050,6 +407755,7 @@ var require_moleculer_service = __commonJS({
407050
407755
  }
407051
407756
  }
407052
407757
  }
407758
+ this.nodeOwners.delete(nodeId);
407053
407759
  this.nodeRegistry.removeNode(nodeId);
407054
407760
  }
407055
407761
  findCallFn(nodeId, capabilityName) {
@@ -407213,6 +407919,9 @@ var require_moleculer_service = __commonJS({
407213
407919
  }
407214
407920
  };
407215
407921
  exports.MoleculerService = MoleculerService;
407922
+ function childOwnerToken(childNodeId, incarnation) {
407923
+ return `${childNodeId}#${incarnation}`;
407924
+ }
407216
407925
  function buildChildUdsManifest(nodeId, childId, caps) {
407217
407926
  const capsByAddon = /* @__PURE__ */ new Map();
407218
407927
  for (const cap of caps) {
@@ -408573,7 +409282,7 @@ var require_manual_boot = __commonJS({
408573
409282
  const replEngineService = new repl_engine_service_1.ReplEngineService(addonRegistryService, eventBusService, loggingService);
408574
409283
  const serverUpdateService = new server_update_service_1.ServerUpdateService({
408575
409284
  logger: loggingService.createLogger("ServerUpdate"),
408576
- restartServer: (requestedBy) => addonPackageService.restartServer(requestedBy),
409285
+ restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
408577
409286
  eventBus: eventBusService,
408578
409287
  updateAvailabilityStore: new update_availability_store_js_1.FileUpdateAvailabilityStore(availabilityDataDir, "server-update", loggingService.createLogger("UpdateAvailability"))
408579
409288
  });