@camstack/system 1.2.110 → 1.2.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +72 -18
  36. package/dist/builtins/snapshot/index.mjs +72 -18
  37. package/dist/builtins/snapshot/snapshot.addon.d.ts +16 -0
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/system-config/system-config.addon.js +1 -1
  45. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  46. package/dist/builtins/winston-logging/index.js +1 -1
  47. package/dist/builtins/winston-logging/index.mjs +1 -1
  48. package/dist/{dist-CQkr5Kw0.js → dist-IPLFQ_-A.js} +10 -1
  49. package/dist/{dist-CW4Cims4.mjs → dist-bkg2-2g3.mjs} +10 -1
  50. package/dist/index.js +87 -9
  51. package/dist/index.mjs +87 -9
  52. package/dist/kernel/capability-registry.d.ts +51 -3
  53. package/dist/kernel/index.d.ts +1 -1
  54. package/dist/kernel/lifecycle/staging-area.d.ts +1 -0
  55. package/dist/kernel/transport/child-cap-protocol.d.ts +12 -0
  56. package/dist/kernel/transport/local-child-registry.d.ts +9 -2
  57. package/dist/{manifest-python-deps-5ei2xVUh.mjs → manifest-python-deps-Cb1ANt3i.mjs} +35 -8
  58. package/dist/{manifest-python-deps-GejnH--L.js → manifest-python-deps-MA_BZGBz.js} +35 -8
  59. package/dist/{retired-settings-keys-DCESas5O.js → retired-settings-keys-B-I7w1Ta.js} +1 -1
  60. package/dist/{retired-settings-keys-DGgOxOir.mjs → retired-settings-keys-C7rRSzbP.mjs} +1 -1
  61. package/package.json +1 -1
@@ -464,6 +464,22 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
464
464
  private courtesyFrame;
465
465
  /** A courtesy frame shaped as the cap's response, or null when unrenderable. */
466
466
  private courtesyImage;
467
+ /**
468
+ * The answer for a camera the hub cannot reach: its last good frame, with
469
+ * `Offline` painted over it, and `offline` stamped on the response identity.
470
+ *
471
+ * Called only AFTER the ladder has run (offline is not in
472
+ * {@link CAPTURE_BLOCKING_REASONS}), so this dresses a frame that could not be
473
+ * refreshed — never one that just landed.
474
+ *
475
+ * The `stateReason` is set FIRST and unconditionally, because it is what a
476
+ * surface labels the tile from and what puts `offline` in the ETag: a client
477
+ * holding the live frame has to see a new identity the moment the camera
478
+ * drops, or it paints the unlabelled photograph until the camera comes back.
479
+ * A failed render therefore degrades to the raw frame rather than to nothing —
480
+ * the metadata still says offline, so the tile is still honest.
481
+ */
482
+ private offlineImage;
467
483
  /**
468
484
  * Render a state-labelled answer. A cached real frame is the background, not
469
485
  * the answer by itself; if it cannot be decoded, fall back to the generated
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CQkr5Kw0.js");
6
+ const require_dist = require("../../dist-IPLFQ_-A.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-CW4Cims4.mjs";
1
+ import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-bkg2-2g3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CQkr5Kw0.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-DCESas5O.js");
6
+ const require_dist = require("../../dist-IPLFQ_-A.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-B-I7w1Ta.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_fs = require("node:fs");
10
10
  let node_module = require("node:module");
@@ -1,5 +1,5 @@
1
- import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-CW4Cims4.mjs";
2
- import { r as retiredKeyStoreOf, t as purgeRetiredSettingsKeys } from "../../retired-settings-keys-DGgOxOir.mjs";
1
+ import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-bkg2-2g3.mjs";
2
+ import { r as retiredKeyStoreOf, t as purgeRetiredSettingsKeys } from "../../retired-settings-keys-C7rRSzbP.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { statSync } from "node:fs";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CQkr5Kw0.js");
6
+ const require_dist = require("../../dist-IPLFQ_-A.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-CW4Cims4.mjs";
1
+ import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-bkg2-2g3.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CQkr5Kw0.js");
6
+ const require_dist = require("../../dist-IPLFQ_-A.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-CW4Cims4.mjs";
1
+ import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-bkg2-2g3.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CQkr5Kw0.js");
6
+ const require_dist = require("../../dist-IPLFQ_-A.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-CW4Cims4.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-bkg2-2g3.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -18629,6 +18629,7 @@ var snapshotCapability = {
18629
18629
  isBattery: zod.z.boolean(),
18630
18630
  reason: zod.z.enum([
18631
18631
  "disabled",
18632
+ "offline",
18632
18633
  "sleeping",
18633
18634
  "unreachable",
18634
18635
  "waking"
@@ -18718,9 +18719,17 @@ targets: zod.z.array(zod.z.object({
18718
18719
  sleeping: zod.z.boolean(),
18719
18720
  /** Current device state rendered over the cached frame. State images
18720
18721
  * remain authoritative even when their photographic background is
18721
- * old; null means the link must carry a current camera frame. */
18722
+ * old; null means the link must carry a current camera frame.
18723
+ *
18724
+ * **This is the ONLY source a surface may derive an inactive tile
18725
+ * from.** A client that reads a device's `online`/`disabled` flags and
18726
+ * draws its own verdict will disagree with the pixels it is painting
18727
+ * — the frame comes from here, and so must the sentence over it
18728
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
18729
+ * state. */
18722
18730
  stateReason: zod.z.enum([
18723
18731
  "disabled",
18732
+ "offline",
18724
18733
  "sleeping",
18725
18734
  "unreachable",
18726
18735
  "waking"
@@ -18629,6 +18629,7 @@ var snapshotCapability = {
18629
18629
  isBattery: z.boolean(),
18630
18630
  reason: z.enum([
18631
18631
  "disabled",
18632
+ "offline",
18632
18633
  "sleeping",
18633
18634
  "unreachable",
18634
18635
  "waking"
@@ -18718,9 +18719,17 @@ targets: z.array(z.object({
18718
18719
  sleeping: z.boolean(),
18719
18720
  /** Current device state rendered over the cached frame. State images
18720
18721
  * remain authoritative even when their photographic background is
18721
- * old; null means the link must carry a current camera frame. */
18722
+ * old; null means the link must carry a current camera frame.
18723
+ *
18724
+ * **This is the ONLY source a surface may derive an inactive tile
18725
+ * from.** A client that reads a device's `online`/`disabled` flags and
18726
+ * draws its own verdict will disagree with the pixels it is painting
18727
+ * — the frame comes from here, and so must the sentence over it
18728
+ * (D190). Pair with `ageMs` for the age of the photograph behind the
18729
+ * state. */
18722
18730
  stateReason: z.enum([
18723
18731
  "disabled",
18732
+ "offline",
18724
18733
  "sleeping",
18725
18734
  "unreachable",
18726
18735
  "waking"
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-CQkr5Kw0.js");
3
+ const require_dist = require("./dist-IPLFQ_-A.js");
4
4
  const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
5
  require("./builtins/alerts/index.js");
6
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
@@ -25,7 +25,7 @@ const require_builtins_system_config_system_config_addon = require("./builtins/s
25
25
  require("./builtins/system-config/index.js");
26
26
  const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
27
27
  const require_file_data_plane = require("./file-data-plane-DUHPHa-Y.js");
28
- const require_manifest_python_deps = require("./manifest-python-deps-GejnH--L.js");
28
+ const require_manifest_python_deps = require("./manifest-python-deps-MA_BZGBz.js");
29
29
  const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
30
30
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
31
31
  let zod = require("zod");
@@ -5367,6 +5367,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5367
5367
  const state = {
5368
5368
  definition,
5369
5369
  providers: /* @__PURE__ */ new Map(),
5370
+ providerOwners: /* @__PURE__ */ new Map(),
5370
5371
  activeAddonId: null,
5371
5372
  router: null,
5372
5373
  disabledProviders: /* @__PURE__ */ new Set()
@@ -5424,8 +5425,15 @@ var CapabilityRegistry = class CapabilityRegistry {
5424
5425
  * Double-register of the same pair is always a programmer error —
5425
5426
  * two legitimately different addons implementing the same cap keep
5426
5427
  * using different `addonId`s and the user picks one via `configReader`.
5427
- */
5428
- registerProvider(capabilityName, addonId, provider) {
5428
+ *
5429
+ * `owner` (optional) stamps the registration with the incarnation that made
5430
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
5431
+ * presenting the SAME token can delete it. Registration paths whose key is
5432
+ * reused across process generations (the UDS child manifest, the D3
5433
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
5434
+ * historical unconditional behaviour.
5435
+ */
5436
+ registerProvider(capabilityName, addonId, provider, owner) {
5429
5437
  let state = this.capabilities.get(capabilityName);
5430
5438
  if (!state) {
5431
5439
  const colonIdx = capabilityName.indexOf(":");
@@ -5457,6 +5465,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5457
5465
  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).`);
5458
5466
  }
5459
5467
  state.providers.set(addonId, provider);
5468
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
5460
5469
  if (state.definition.mode === "singleton") {
5461
5470
  const userChoice = this.configReader?.(capabilityName);
5462
5471
  const preferred = state.definition.preferredProvider;
@@ -5487,7 +5496,8 @@ var CapabilityRegistry = class CapabilityRegistry {
5487
5496
  tags: { addonId },
5488
5497
  meta: {
5489
5498
  capability: capabilityName,
5490
- mode: state.definition.mode
5499
+ mode: state.definition.mode,
5500
+ ...owner === void 0 ? {} : { owner }
5491
5501
  }
5492
5502
  });
5493
5503
  this.emitEvent("capability:provider-registered", {
@@ -5534,11 +5544,70 @@ var CapabilityRegistry = class CapabilityRegistry {
5534
5544
  const declarers = this.manifestDeclarers.get(capabilityName);
5535
5545
  return declarers ? [...declarers] : [];
5536
5546
  }
5537
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
5538
- unregisterProvider(capabilityName, addonId) {
5547
+ /**
5548
+ * The owner token a registration declared, or `null` when the pair is not
5549
+ * registered or was registered without one.
5550
+ */
5551
+ getProviderOwner(capabilityName, addonId) {
5552
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
5553
+ }
5554
+ /**
5555
+ * Hand an EXISTING registration to a new owner, without touching the provider
5556
+ * instance, the active-singleton selection or any event.
5557
+ *
5558
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
5559
+ * set, so the manifest diff correctly leaves the registration alone — but the
5560
+ * registration is still stamped with the dead generation's token, and nothing
5561
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
5562
+ * capability cleanable when the successor eventually dies.
5563
+ *
5564
+ * Returns `false` when the pair is not registered (nothing to hand over) —
5565
+ * the caller decides whether that is a register-instead or a diagnosis.
5566
+ */
5567
+ reassignProviderOwner(capabilityName, addonId, owner) {
5568
+ const state = this.capabilities.get(capabilityName);
5569
+ if (!state || !state.providers.has(addonId)) return false;
5570
+ const previous = state.providerOwners.get(addonId);
5571
+ if (previous === owner) return true;
5572
+ state.providerOwners.set(addonId, owner);
5573
+ this.logger.info("Capability registration ownership reassigned", {
5574
+ tags: { addonId },
5575
+ meta: {
5576
+ capability: capabilityName,
5577
+ owner,
5578
+ previousOwner: previous ?? null
5579
+ }
5580
+ });
5581
+ return true;
5582
+ }
5583
+ /**
5584
+ * Unregister a provider. For singleton: clears active. For collection:
5585
+ * removes from list.
5586
+ *
5587
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
5588
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
5589
+ * and the caller presents a different one — or none at all — this is a
5590
+ * logged no-op: the caller belongs to a generation that no longer holds the
5591
+ * key. This is what stops a dead runner's teardown from erasing its
5592
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
5593
+ */
5594
+ unregisterProvider(capabilityName, addonId, owner) {
5539
5595
  const state = this.capabilities.get(capabilityName);
5540
5596
  if (!state) return;
5597
+ const registeredOwner = state.providerOwners.get(addonId);
5598
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
5599
+ this.logger.info("Unregister ignored — registration is owned by another incarnation", {
5600
+ tags: { addonId },
5601
+ meta: {
5602
+ capability: capabilityName,
5603
+ registeredOwner,
5604
+ presentedOwner: owner ?? null
5605
+ }
5606
+ });
5607
+ return;
5608
+ }
5541
5609
  state.providers.delete(addonId);
5610
+ state.providerOwners.delete(addonId);
5542
5611
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
5543
5612
  const userChoice = this.configReader?.(capabilityName);
5544
5613
  const preferred = state.definition.preferredProvider;
@@ -94555,6 +94624,11 @@ var JobJournal = class {
94555
94624
  function safeSegment(name) {
94556
94625
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
94557
94626
  }
94627
+ /** An exact semver — the only shape the staged package.json can be compared
94628
+ * against literally. Anything else ('latest', 'beta', a range) is a dist-tag
94629
+ * the FETCHER resolves; comparing the resolved version to the literal tag
94630
+ * failed every tag-based update (30/30 on 2026-08-20). */
94631
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
94558
94632
  var StagingArea = class {
94559
94633
  stagingDir;
94560
94634
  deps;
@@ -94572,8 +94646,12 @@ var StagingArea = class {
94572
94646
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
94573
94647
  await this.deps.extract(tgz, destDir);
94574
94648
  const raw = JSON.parse(node_fs.readFileSync(node_path.join(destDir, "package.json"), "utf-8"));
94575
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
94576
- return { stagedPath: destDir };
94649
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
94650
+ 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}`);
94651
+ return {
94652
+ stagedPath: destDir,
94653
+ resolvedVersion: raw.version
94654
+ };
94577
94655
  }
94578
94656
  cleanup(jobId) {
94579
94657
  node_fs.rmSync(node_path.join(this.stagingDir, jobId), {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { At as asNumber, B as extractNestedAddonId, Bt as parseJsonUnknown$1, Dt as ReadinessTimeoutError, Et as ReadinessRegistry, Ft as expandCapMethods, Gt as EventCategory$1, H as isArrayOutputSchema, Ht as resolveCapMount, J as lifecycleJobSchema, K as isVoidInput, Mt as createEvent, Nt as emitDownForOwnedCaps, Q as looseSchema, St as DEVICE_STATUS_METHOD, U as isCollectionArrayMethod, Ut as scopeKey, Vt as readinessKey, W as isObjectInput, Z as logLevelAtMost, bt as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, it as procedureAuthKey, jt as asString$1, kt as asJsonObject$1, ot as scopesAllowAddon, q as kebabToCamel, st as scopesAllowDeviceCap, t as ALL_CAPABILITY_DEFINITIONS, tt as objectInputDeclaresAddonId, vt as errMsg$1, xt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, y as addonSettingsCapability, zt as parseJsonObject } from "./dist-CW4Cims4.mjs";
2
+ import { At as asNumber, B as extractNestedAddonId, Bt as parseJsonUnknown$1, Dt as ReadinessTimeoutError, Et as ReadinessRegistry, Ft as expandCapMethods, Gt as EventCategory$1, H as isArrayOutputSchema, Ht as resolveCapMount, J as lifecycleJobSchema, K as isVoidInput, Mt as createEvent, Nt as emitDownForOwnedCaps, Q as looseSchema, St as DEVICE_STATUS_METHOD, U as isCollectionArrayMethod, Ut as scopeKey, Vt as readinessKey, W as isObjectInput, Z as logLevelAtMost, bt as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, it as procedureAuthKey, jt as asString$1, kt as asJsonObject$1, ot as scopesAllowAddon, q as kebabToCamel, st as scopesAllowDeviceCap, t as ALL_CAPABILITY_DEFINITIONS, tt as objectInputDeclaresAddonId, vt as errMsg$1, xt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, y as addonSettingsCapability, zt as parseJsonObject } from "./dist-bkg2-2g3.mjs";
3
3
  import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
4
4
  import "./builtins/alerts/index.mjs";
5
5
  import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
@@ -24,7 +24,7 @@ import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.
24
24
  import "./builtins/system-config/index.mjs";
25
25
  import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
26
26
  import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-CuE_hBli.mjs";
27
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-5ei2xVUh.mjs";
27
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-Cb1ANt3i.mjs";
28
28
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
29
29
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
30
30
  import { z } from "zod";
@@ -5360,6 +5360,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5360
5360
  const state = {
5361
5361
  definition,
5362
5362
  providers: /* @__PURE__ */ new Map(),
5363
+ providerOwners: /* @__PURE__ */ new Map(),
5363
5364
  activeAddonId: null,
5364
5365
  router: null,
5365
5366
  disabledProviders: /* @__PURE__ */ new Set()
@@ -5417,8 +5418,15 @@ var CapabilityRegistry = class CapabilityRegistry {
5417
5418
  * Double-register of the same pair is always a programmer error —
5418
5419
  * two legitimately different addons implementing the same cap keep
5419
5420
  * using different `addonId`s and the user picks one via `configReader`.
5420
- */
5421
- registerProvider(capabilityName, addonId, provider) {
5421
+ *
5422
+ * `owner` (optional) stamps the registration with the incarnation that made
5423
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
5424
+ * presenting the SAME token can delete it. Registration paths whose key is
5425
+ * reused across process generations (the UDS child manifest, the D3
5426
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
5427
+ * historical unconditional behaviour.
5428
+ */
5429
+ registerProvider(capabilityName, addonId, provider, owner) {
5422
5430
  let state = this.capabilities.get(capabilityName);
5423
5431
  if (!state) {
5424
5432
  const colonIdx = capabilityName.indexOf(":");
@@ -5450,6 +5458,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5450
5458
  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).`);
5451
5459
  }
5452
5460
  state.providers.set(addonId, provider);
5461
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
5453
5462
  if (state.definition.mode === "singleton") {
5454
5463
  const userChoice = this.configReader?.(capabilityName);
5455
5464
  const preferred = state.definition.preferredProvider;
@@ -5480,7 +5489,8 @@ var CapabilityRegistry = class CapabilityRegistry {
5480
5489
  tags: { addonId },
5481
5490
  meta: {
5482
5491
  capability: capabilityName,
5483
- mode: state.definition.mode
5492
+ mode: state.definition.mode,
5493
+ ...owner === void 0 ? {} : { owner }
5484
5494
  }
5485
5495
  });
5486
5496
  this.emitEvent("capability:provider-registered", {
@@ -5527,11 +5537,70 @@ var CapabilityRegistry = class CapabilityRegistry {
5527
5537
  const declarers = this.manifestDeclarers.get(capabilityName);
5528
5538
  return declarers ? [...declarers] : [];
5529
5539
  }
5530
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
5531
- unregisterProvider(capabilityName, addonId) {
5540
+ /**
5541
+ * The owner token a registration declared, or `null` when the pair is not
5542
+ * registered or was registered without one.
5543
+ */
5544
+ getProviderOwner(capabilityName, addonId) {
5545
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
5546
+ }
5547
+ /**
5548
+ * Hand an EXISTING registration to a new owner, without touching the provider
5549
+ * instance, the active-singleton selection or any event.
5550
+ *
5551
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
5552
+ * set, so the manifest diff correctly leaves the registration alone — but the
5553
+ * registration is still stamped with the dead generation's token, and nothing
5554
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
5555
+ * capability cleanable when the successor eventually dies.
5556
+ *
5557
+ * Returns `false` when the pair is not registered (nothing to hand over) —
5558
+ * the caller decides whether that is a register-instead or a diagnosis.
5559
+ */
5560
+ reassignProviderOwner(capabilityName, addonId, owner) {
5561
+ const state = this.capabilities.get(capabilityName);
5562
+ if (!state || !state.providers.has(addonId)) return false;
5563
+ const previous = state.providerOwners.get(addonId);
5564
+ if (previous === owner) return true;
5565
+ state.providerOwners.set(addonId, owner);
5566
+ this.logger.info("Capability registration ownership reassigned", {
5567
+ tags: { addonId },
5568
+ meta: {
5569
+ capability: capabilityName,
5570
+ owner,
5571
+ previousOwner: previous ?? null
5572
+ }
5573
+ });
5574
+ return true;
5575
+ }
5576
+ /**
5577
+ * Unregister a provider. For singleton: clears active. For collection:
5578
+ * removes from list.
5579
+ *
5580
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
5581
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
5582
+ * and the caller presents a different one — or none at all — this is a
5583
+ * logged no-op: the caller belongs to a generation that no longer holds the
5584
+ * key. This is what stops a dead runner's teardown from erasing its
5585
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
5586
+ */
5587
+ unregisterProvider(capabilityName, addonId, owner) {
5532
5588
  const state = this.capabilities.get(capabilityName);
5533
5589
  if (!state) return;
5590
+ const registeredOwner = state.providerOwners.get(addonId);
5591
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
5592
+ this.logger.info("Unregister ignored — registration is owned by another incarnation", {
5593
+ tags: { addonId },
5594
+ meta: {
5595
+ capability: capabilityName,
5596
+ registeredOwner,
5597
+ presentedOwner: owner ?? null
5598
+ }
5599
+ });
5600
+ return;
5601
+ }
5534
5602
  state.providers.delete(addonId);
5603
+ state.providerOwners.delete(addonId);
5535
5604
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
5536
5605
  const userChoice = this.configReader?.(capabilityName);
5537
5606
  const preferred = state.definition.preferredProvider;
@@ -94548,6 +94617,11 @@ var JobJournal = class {
94548
94617
  function safeSegment(name) {
94549
94618
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
94550
94619
  }
94620
+ /** An exact semver — the only shape the staged package.json can be compared
94621
+ * against literally. Anything else ('latest', 'beta', a range) is a dist-tag
94622
+ * the FETCHER resolves; comparing the resolved version to the literal tag
94623
+ * failed every tag-based update (30/30 on 2026-08-20). */
94624
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
94551
94625
  var StagingArea = class {
94552
94626
  stagingDir;
94553
94627
  deps;
@@ -94565,8 +94639,12 @@ var StagingArea = class {
94565
94639
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
94566
94640
  await this.deps.extract(tgz, destDir);
94567
94641
  const raw = JSON.parse(fs$17.readFileSync(path$39.join(destDir, "package.json"), "utf-8"));
94568
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
94569
- return { stagedPath: destDir };
94642
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
94643
+ 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}`);
94644
+ return {
94645
+ stagedPath: destDir,
94646
+ resolvedVersion: raw.version
94647
+ };
94570
94648
  }
94571
94649
  cleanup(jobId) {
94572
94650
  fs$17.rmSync(path$39.join(this.stagingDir, jobId), {
@@ -19,6 +19,18 @@ export type ConfigReader = (capability: string) => string | undefined;
19
19
  * {@link ConfigReader} does for singletons.
20
20
  */
21
21
  export type CollectionConfigReader = (capability: string) => readonly string[] | undefined;
22
+ /**
23
+ * Proof that an unregister owns the registration it is about to delete.
24
+ *
25
+ * A `(capability, addonId)` key is NOT unique over time: a runner replaced by
26
+ * an update re-registers under the exact same key (`resolveRunnerId` is
27
+ * deterministic), so the dead generation's teardown and the live generation's
28
+ * registration are indistinguishable by key alone. The owner token names the
29
+ * INCARNATION that made a registration — e.g. `hub/recorder#7` — and
30
+ * {@link CapabilityRegistry.unregisterProvider} refuses any unregister that
31
+ * cannot present it. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
32
+ */
33
+ export type ProviderOwnerToken = string;
22
34
  export declare class CapabilityRegistry {
23
35
  private readonly capabilities;
24
36
  private readonly logger;
@@ -148,8 +160,15 @@ export declare class CapabilityRegistry {
148
160
  * Double-register of the same pair is always a programmer error —
149
161
  * two legitimately different addons implementing the same cap keep
150
162
  * using different `addonId`s and the user picks one via `configReader`.
163
+ *
164
+ * `owner` (optional) stamps the registration with the incarnation that made
165
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
166
+ * presenting the SAME token can delete it. Registration paths whose key is
167
+ * reused across process generations (the UDS child manifest, the D3
168
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
169
+ * historical unconditional behaviour.
151
170
  */
152
- registerProvider(capabilityName: string, addonId: string, provider: unknown): void;
171
+ registerProvider(capabilityName: string, addonId: string, provider: unknown, owner?: ProviderOwnerToken): void;
153
172
  /**
154
173
  * Whether a provider for the exact `(capabilityName, addonId)` pair is
155
174
  * currently registered. Lets a re-registration path (e.g. the agent's
@@ -173,8 +192,37 @@ export declare class CapabilityRegistry {
173
192
  * it is the diagnosis).
174
193
  */
175
194
  getManifestDeclarers(capabilityName: string): readonly string[];
176
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
177
- unregisterProvider(capabilityName: string, addonId: string): void;
195
+ /**
196
+ * The owner token a registration declared, or `null` when the pair is not
197
+ * registered or was registered without one.
198
+ */
199
+ getProviderOwner(capabilityName: string, addonId: string): ProviderOwnerToken | null;
200
+ /**
201
+ * Hand an EXISTING registration to a new owner, without touching the provider
202
+ * instance, the active-singleton selection or any event.
203
+ *
204
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
205
+ * set, so the manifest diff correctly leaves the registration alone — but the
206
+ * registration is still stamped with the dead generation's token, and nothing
207
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
208
+ * capability cleanable when the successor eventually dies.
209
+ *
210
+ * Returns `false` when the pair is not registered (nothing to hand over) —
211
+ * the caller decides whether that is a register-instead or a diagnosis.
212
+ */
213
+ reassignProviderOwner(capabilityName: string, addonId: string, owner: ProviderOwnerToken): boolean;
214
+ /**
215
+ * Unregister a provider. For singleton: clears active. For collection:
216
+ * removes from list.
217
+ *
218
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
219
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
220
+ * and the caller presents a different one — or none at all — this is a
221
+ * logged no-op: the caller belongs to a generation that no longer holds the
222
+ * key. This is what stops a dead runner's teardown from erasing its
223
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
224
+ */
225
+ unregisterProvider(capabilityName: string, addonId: string, owner?: ProviderOwnerToken): void;
178
226
  /** Enable a previously disabled collection provider. */
179
227
  enableCollectionProvider(capability: string, addonId: string): void;
180
228
  /** Disable a collection provider (keeps it registered but excluded from active list). */
@@ -17,7 +17,7 @@ export { buildCapRouters, builderMountedCapNames } from './cap-router-builder.js
17
17
  export type { CapRouterPrimitives, CapRouterServices, CapProviderGetter, RemoteProxyFactory, ProcedureBuilder, ProcedureResolverArgs, } from './cap-router-builder.js';
18
18
  export { describeProviderKindDrift } from './provider-kind-drift.js';
19
19
  export type { ProviderKindHint } from './provider-kind-drift.js';
20
- export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, } from './capability-registry.js';
20
+ export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, ProviderOwnerToken, } from './capability-registry.js';
21
21
  export { CustomActionRegistry, type CustomActionEntry } from './custom-action-registry.js';
22
22
  export { INFRA_CAPABILITIES, isInfraCapability } from './infra-capabilities.js';
23
23
  export type { InfraCapability } from './infra-capabilities.js';
@@ -16,6 +16,7 @@ export declare class StagingArea {
16
16
  constructor(stagingDir: string, deps: StagingAreaDeps);
17
17
  fetchAndStage(input: FetchAndStageInput): Promise<{
18
18
  stagedPath: string;
19
+ resolvedVersion: string;
19
20
  }>;
20
21
  cleanup(jobId: string): void;
21
22
  /**
@@ -222,6 +222,18 @@ export type ParentToChildRequest = CapCallMessage | ParentEventMessage | Readine
222
222
  export interface RegisteredChild {
223
223
  readonly childId: string;
224
224
  readonly caps: readonly ChildCapDescriptor[];
225
+ /**
226
+ * Monotonic id of the CONNECTION this registration arrived on, unique within
227
+ * the parent process.
228
+ *
229
+ * `childId` is deterministic (`resolveRunnerId`), so a runner replaced by an
230
+ * update reconnects under the exact same id — the two generations are
231
+ * indistinguishable without this. Consumers stamp it onto whatever they
232
+ * derive from the child (capability registrations, subtree manifests) so a
233
+ * teardown arriving from the DEAD generation can be told apart from the live
234
+ * one. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
235
+ */
236
+ readonly incarnation: number;
225
237
  }
226
238
  /** Arguments to route a cap method call (the `cap-call` message minus its discriminant). */
227
239
  export type CapCallInput = Omit<CapCallMessage, 'kind'>;
@@ -139,6 +139,8 @@ export declare class LocalChildRegistry {
139
139
  private readonly children;
140
140
  private registeredHandler;
141
141
  private goneHandler;
142
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
143
+ private nextIncarnation;
142
144
  private eventHandler;
143
145
  private logHandler;
144
146
  private readinessHandler;
@@ -291,8 +293,13 @@ export declare class LocalChildRegistry {
291
293
  listChildren(): readonly RegisteredChild[];
292
294
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
293
295
  onChildRegistered(handler: (child: RegisteredChild) => void): void;
294
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
295
- onChildGone(handler: (childId: string) => void): void;
296
+ /**
297
+ * Register the (single) child-gone handler. Only one handler is active at a
298
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
299
+ * connection that closed — carry it into whatever teardown it triggers, so a
300
+ * dead generation cannot tear down its successor's work.
301
+ */
302
+ onChildGone(handler: (childId: string, incarnation: number) => void): void;
296
303
  /**
297
304
  * Register the (single) child-event handler. Invoked when a child sends an
298
305
  * event via `LocalChildClient.emitEvent`. Only one handler is active at a