@serfab/cadre-core 1.17.0 → 1.18.0

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 (144) hide show
  1. package/README.md +3 -2
  2. package/dist/cadre-invite-protocol.d.ts +35 -13
  3. package/dist/cadre-invite-protocol.js +90 -25
  4. package/dist/cadre-invite-protocol.js.map +1 -1
  5. package/dist/cadre-node.d.ts +586 -255
  6. package/dist/cadre-node.js +1176 -564
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/control-cohort.d.ts +66 -9
  9. package/dist/control-cohort.js +67 -9
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +451 -193
  12. package/dist/control-database.js +701 -359
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +46 -43
  15. package/dist/control-formation-recorder.js +77 -78
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-protocol-guard.d.ts +6 -0
  18. package/dist/control-protocol-guard.js +14 -2
  19. package/dist/control-protocol-guard.js.map +1 -1
  20. package/dist/control-schema-version.d.ts +48 -31
  21. package/dist/control-schema-version.js +67 -45
  22. package/dist/control-schema-version.js.map +1 -1
  23. package/dist/control-schema.d.ts +8 -8
  24. package/dist/control-schema.js +16 -16
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +42 -7
  27. package/dist/control-write-retry.js +87 -19
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/enrolled-machine-store.d.ts +3 -3
  30. package/dist/enrolled-machine-store.js +3 -3
  31. package/dist/formation-token.d.ts +48 -0
  32. package/dist/formation-token.js +26 -0
  33. package/dist/formation-token.js.map +1 -0
  34. package/dist/index.d.ts +14 -13
  35. package/dist/index.js +16 -13
  36. package/dist/index.js.map +1 -1
  37. package/dist/joined-strand-store.d.ts +26 -9
  38. package/dist/joined-strand-store.js +63 -24
  39. package/dist/joined-strand-store.js.map +1 -1
  40. package/dist/link-budget.d.ts +9 -7
  41. package/dist/link-budget.js +9 -7
  42. package/dist/link-budget.js.map +1 -1
  43. package/dist/member-redemption.d.ts +15 -9
  44. package/dist/member-redemption.js +13 -8
  45. package/dist/member-redemption.js.map +1 -1
  46. package/dist/membership-view.d.ts +91 -0
  47. package/dist/membership-view.js +110 -0
  48. package/dist/membership-view.js.map +1 -0
  49. package/dist/owner-grant-protocol.d.ts +11 -7
  50. package/dist/owner-grant-protocol.js +31 -17
  51. package/dist/owner-grant-protocol.js.map +1 -1
  52. package/dist/peer-addr-book.d.ts +10 -1
  53. package/dist/peer-addr-book.js +4 -3
  54. package/dist/peer-addr-book.js.map +1 -1
  55. package/dist/peer-join-backfill.d.ts +89 -20
  56. package/dist/peer-join-backfill.js +265 -108
  57. package/dist/peer-join-backfill.js.map +1 -1
  58. package/dist/pending-join-runner.d.ts +48 -18
  59. package/dist/pending-join-runner.js +74 -56
  60. package/dist/pending-join-runner.js.map +1 -1
  61. package/dist/seed-bootstrap.d.ts +47 -40
  62. package/dist/seed-bootstrap.js +66 -61
  63. package/dist/seed-bootstrap.js.map +1 -1
  64. package/dist/strand-database.d.ts +23 -2
  65. package/dist/strand-database.js +26 -6
  66. package/dist/strand-database.js.map +1 -1
  67. package/dist/strand-first-sync-gate.d.ts +176 -89
  68. package/dist/strand-first-sync-gate.js +288 -122
  69. package/dist/strand-first-sync-gate.js.map +1 -1
  70. package/dist/strand-formation-deadlines.d.ts +9 -9
  71. package/dist/strand-formation-deadlines.js +9 -9
  72. package/dist/strand-formation-manager.d.ts +23 -17
  73. package/dist/strand-formation-manager.js +46 -44
  74. package/dist/strand-formation-manager.js.map +1 -1
  75. package/dist/strand-formation-protocol.d.ts +5 -8
  76. package/dist/strand-formation-protocol.js +101 -16
  77. package/dist/strand-formation-protocol.js.map +1 -1
  78. package/dist/strand-instance-manager.d.ts +26 -17
  79. package/dist/strand-instance-manager.js +119 -50
  80. package/dist/strand-instance-manager.js.map +1 -1
  81. package/dist/strand-membership-reconciler.d.ts +16 -13
  82. package/dist/strand-membership-reconciler.js +30 -22
  83. package/dist/strand-membership-reconciler.js.map +1 -1
  84. package/dist/strand-membership-writer.d.ts +40 -28
  85. package/dist/strand-membership-writer.js +212 -153
  86. package/dist/strand-membership-writer.js.map +1 -1
  87. package/dist/strand-move.js +5 -2
  88. package/dist/strand-move.js.map +1 -1
  89. package/dist/strand-revocation-enforcer.d.ts +134 -60
  90. package/dist/strand-revocation-enforcer.js +144 -71
  91. package/dist/strand-revocation-enforcer.js.map +1 -1
  92. package/dist/strand-solicitation.d.ts +50 -39
  93. package/dist/strand-solicitation.js +10 -3
  94. package/dist/strand-solicitation.js.map +1 -1
  95. package/dist/strand-watcher.d.ts +80 -13
  96. package/dist/strand-watcher.js +152 -48
  97. package/dist/strand-watcher.js.map +1 -1
  98. package/dist/table-change-watch.d.ts +63 -0
  99. package/dist/table-change-watch.js +209 -0
  100. package/dist/table-change-watch.js.map +1 -0
  101. package/dist/timeout-scheduler.d.ts +4 -3
  102. package/dist/timeout-scheduler.js +4 -3
  103. package/dist/timeout-scheduler.js.map +1 -1
  104. package/dist/types.d.ts +90 -49
  105. package/dist/types.js +4 -4
  106. package/dist/types.js.map +1 -1
  107. package/package.json +8 -8
  108. package/src/cadre-invite-protocol.ts +119 -31
  109. package/src/cadre-node.ts +1282 -584
  110. package/src/control-cohort.ts +86 -9
  111. package/src/control-database.ts +848 -403
  112. package/src/control-formation-recorder.ts +359 -353
  113. package/src/control-protocol-guard.ts +16 -2
  114. package/src/control-schema-version.ts +101 -51
  115. package/src/control-schema.ts +16 -16
  116. package/src/control-write-retry.ts +91 -19
  117. package/src/delegate-admission.ts +351 -351
  118. package/src/enrolled-machine-store.ts +3 -3
  119. package/src/formation-token.ts +63 -0
  120. package/src/index.ts +30 -17
  121. package/src/joined-strand-store.ts +69 -24
  122. package/src/link-budget.ts +9 -7
  123. package/src/member-redemption.ts +20 -13
  124. package/src/membership-view.ts +144 -0
  125. package/src/owner-grant-protocol.ts +33 -18
  126. package/src/peer-addr-book.ts +4 -3
  127. package/src/peer-join-backfill.ts +971 -761
  128. package/src/pending-join-runner.ts +76 -53
  129. package/src/seed-bootstrap.ts +72 -67
  130. package/src/strand-database.ts +352 -322
  131. package/src/strand-first-sync-gate.ts +334 -134
  132. package/src/strand-formation-deadlines.ts +227 -227
  133. package/src/strand-formation-manager.ts +731 -725
  134. package/src/strand-formation-protocol.ts +122 -24
  135. package/src/strand-instance-manager.ts +2092 -2026
  136. package/src/strand-membership-reconciler.ts +33 -25
  137. package/src/strand-membership-writer.ts +2093 -2019
  138. package/src/strand-move.ts +5 -2
  139. package/src/strand-revocation-enforcer.ts +740 -654
  140. package/src/strand-solicitation.ts +61 -34
  141. package/src/strand-watcher.ts +175 -56
  142. package/src/table-change-watch.ts +285 -0
  143. package/src/timeout-scheduler.ts +29 -28
  144. package/src/types.ts +89 -51
@@ -28,8 +28,8 @@ import { type ConnectionPathSummary } from './diagnostics/connection-path.js';
28
28
  *
29
29
  * Ten minutes sits comfortably inside the peerStore's one-hour address expiry
30
30
  * (`MAX_ADDRESS_AGE`, see `peer-addr-book.ts`) with headroom for several missed
31
- * passes, and far above the 15 s reconcile cadence the refresh rides on, so the
32
- * strand-addr RPC fan-out stays cheap.
31
+ * passes, and far above the reconcile pass's 2-minute backstop the refresh rides
32
+ * on, so the strand-addr RPC fan-out stays cheap.
33
33
  */
34
34
  export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
35
35
  /**
@@ -38,8 +38,9 @@ export declare const STRAND_PEER_ADDR_REFRESH_MS: number;
38
38
  * phone asking before its `CadrePeer` row has replicated to the sibling is refused
39
39
  * and its delegate grant goes unrecorded, and waiting the full
40
40
  * {@link STRAND_PEER_ADDR_REFRESH_MS} for the next try is the delay this bounds.
41
- * Four reconcile ticks, so a sibling that keeps failing costs one timed-out RPC a
42
- * minute rather than one a tick.
41
+ * A pass arms its own wake for the earliest due time it leaves behind
42
+ * (`nextReconcileWakeMs`), so a sibling that keeps failing costs one timed-out
43
+ * RPC a minute, with no pass in between.
43
44
  */
44
45
  export declare const STRAND_PEER_ADDR_RETRY_MS: number;
45
46
  /**
@@ -240,8 +241,8 @@ export declare class CadreNode implements SAppIdLookup {
240
241
  *
241
242
  * Cleared by {@link cleanup} only, deliberately NOT by {@link detachStrand}: the watcher's
242
243
  * filter rejection stops a replica through that very method, and forgetting the id there
243
- * would have the next poll find it unknown again, re-admit the strand provisionally and
244
- * relaunch the replica — once every other poll, forever. A strand id's sApp never changes,
244
+ * would have the next watcher run find it unknown again, re-admit the strand provisionally
245
+ * and relaunch the replica — on every other run, forever. A strand id's sApp never changes,
245
246
  * and the map is bounded by the strands this party has published, like
246
247
  * {@link discoveredStrands}.
247
248
  */
@@ -269,19 +270,24 @@ export declare class CadreNode implements SAppIdLookup {
269
270
  * wait that ends in mutual denial (the upstream gate is fail-closed on
270
271
  * timeout, unlike the fail-open connection gater). Optimystic's predicate is
271
272
  * synchronous by contract. So the set is refreshed OUT OF BAND instead — after
272
- * start, on every {@link reconcileControlCohort} pass (15s cadence), and
273
- * immediately after every LOCAL membership mutation so a just-vouched peer
274
- * is admitted without waiting for the timer. That last refresh is AUTOMATIC:
275
- * the control database notifies {@link refreshMembershipGate} after every
273
+ * start, at the head of every {@link reconcileControlCohort} pass, whenever a
274
+ * membership table changes (the gate watch {@link start} takes through
275
+ * `ControlDatabase.watchMembershipTables`, which hears a local commit and a change
276
+ * replicated into this node's storage alike, within a 50 ms window), and immediately
277
+ * after every LOCAL membership mutation so a just-vouched peer is admitted
278
+ * before the writer's call returns. That last refresh is AUTOMATIC: the
279
+ * control database notifies {@link refreshMembershipGate} after every
276
280
  * committed `CadrePeer` write (see `ControlDatabase.mutateCadrePeer`), so no
277
- * writer has to remember. A change that arrives by REPLICATION is picked up
278
- * on the next timed refresh — bounded staleness, acceptable because this gate
279
- * is defense in depth: rows an unadmitted peer manages to write are still
280
- * disbelieved at read time by the voucher-anchored predicate. For cohort
281
- * choice the same window means a member whose row arrived by replication is
282
- * left out of this node's control-block cohorts until that refresh.
283
- * Optimystic reads the predicate live on every lookup, so replacing this set
284
- * is the whole re-evaluation; there is no per-peer reclassify call to make.
281
+ * writer has to remember. A change this node's storage never hears of — a
282
+ * machine outside the block's cohort on a party larger than the replication
283
+ * breadth — is picked up at the pass's 2-minute backstop: bounded staleness,
284
+ * acceptable because this gate is defense in depth: rows an unadmitted peer
285
+ * manages to write are still disbelieved at read time by the voucher-anchored
286
+ * predicate. For cohort choice the same window means a member whose row
287
+ * arrived by replication is left out of this node's control-block cohorts
288
+ * until that refresh. Optimystic reads the predicate live on every lookup, so
289
+ * replacing this set is the whole re-evaluation; there is no per-peer
290
+ * reclassify call to make.
285
291
  */
286
292
  private authorizedControlPeers;
287
293
  /**
@@ -339,9 +345,9 @@ export declare class CadreNode implements SAppIdLookup {
339
345
  * When this node last ANNOUNCED a delegate, keyed `<targetPeerId>\n<strandId>`
340
346
  * (the peer announced TO, not the delegate). Throttles
341
347
  * {@link refreshDelegateGrants} to once per `DELEGATE_GRANT_TTL_MS / 2` per
342
- * (relay, strand) so the 15 s reconcile tick never becomes per-tick RPC
343
- * chatter; the launch/resume announce passes record here too. Keys whose
344
- * strand is no longer running are pruned on each reconcile pass.
348
+ * (relay, strand) so a run of change-driven reconcile passes never becomes
349
+ * per-pass RPC chatter; the launch/resume announce passes record here too.
350
+ * Keys whose strand is no longer running are pruned on each reconcile pass.
345
351
  */
346
352
  private readonly delegateAnnounceAt;
347
353
  /**
@@ -368,12 +374,13 @@ export declare class CadreNode implements SAppIdLookup {
368
374
  * strand, keyed `peerStrandKey(siblingControlPeerId, strandId)` → epoch ms; a
369
375
  * missing key is due. Bounds {@link refreshStrandPeerAddrs}'s fan-out per
370
376
  * (sibling, strand) rather than per strand, so a sibling that connects after a
371
- * pass is asked on the next 15 s tick instead of waiting out a stamp another
372
- * sibling's answer set. An answer (even an empty one) makes the sibling due again
373
- * in {@link STRAND_PEER_ADDR_REFRESH_MS}; no answer, in
374
- * {@link STRAND_PEER_ADDR_RETRY_MS}. Keys whose strand stopped running or whose
375
- * sibling is no longer connected are pruned every pass, so a resumed strand and a
376
- * reconnected sibling are both asked at once.
377
+ * pass is asked on the pass its connection starts instead of waiting out a stamp
378
+ * another sibling's answer set. An answer (even an empty one) makes the sibling
379
+ * due again in {@link STRAND_PEER_ADDR_REFRESH_MS}; no answer, in
380
+ * {@link STRAND_PEER_ADDR_RETRY_MS}, and each pass arms its own wake for the
381
+ * earliest due time it leaves behind (`nextReconcileWakeMs`). Keys whose strand
382
+ * stopped running or whose sibling is no longer connected are pruned every pass,
383
+ * so a resumed strand and a reconnected sibling are both asked at once.
377
384
  */
378
385
  private readonly strandAddrAskDueAt;
379
386
  /**
@@ -455,6 +462,18 @@ export declare class CadreNode implements SAppIdLookup {
455
462
  * `unpublishStrand` / `forgetJoinedStrand` as {@link formationStrandAddrs} is.
456
463
  */
457
464
  private readonly pendingMembershipInvites;
465
+ /**
466
+ * The party key {@link adoptFormationMembershipInvite} just seated or read, by strand id,
467
+ * so the launch that follows a join does not read the `StrandPartyKey` row again. Taken
468
+ * (deleted) by the {@link resolveStrandPartyKey} that uses it, so any later launch reads
469
+ * the row; dropped by {@link detachStrand}. The joiner's counterpart of the founder's
470
+ * `StrandConfig.partyMemberPrivateKey`, without the app carrying the secret.
471
+ *
472
+ * NOTE: an entry is not dropped when the row is removed, because no `CadreNode` path removes
473
+ * it; an embedder calling `getControlDatabase().deleteStrandPartyKey` between a join and its
474
+ * launch would get the deleted key. If a node-level rotation is added, drop the entry there.
475
+ */
476
+ private readonly formedPartyKeys;
458
477
  /**
459
478
  * Invite keys of the membership invitations this process has staged, by
460
479
  * {@link adoptFormationMembershipInvite} or from a `JoinSuccess` row
@@ -548,7 +567,7 @@ export declare class CadreNode implements SAppIdLookup {
548
567
  private joinedStrandStore;
549
568
  /**
550
569
  * This session's joined strands: {@link joinedStrandStore} and the party-wide
551
- * `JoinedStrand` table, unioned with the control rows each watcher poll, published and
570
+ * `JoinedStrand` table, unioned with the control rows each watcher run, published and
552
571
  * removed by the reconcile pass. Per SESSION, unlike the store — rebuilt at every
553
572
  * {@link start}, which is what ends a revoked join's "keep offering until this session
554
573
  * ends" (see `JoinedStrandSession.forgetAfterThisSession`).
@@ -579,17 +598,41 @@ export declare class CadreNode implements SAppIdLookup {
579
598
  /** Listener that re-publishes the self record when reachable addresses change. */
580
599
  private selfPeerUpdateHandler;
581
600
  /**
582
- * Recurring proactive control-cohort dial cadence (see
583
- * {@link reconcileControlCohort}). Wired alongside {@link recordRefreshTimer}
584
- * in {@link startRecordRefresh} and torn down symmetrically in
585
- * {@link stopRecordRefresh}; `.unref()`'d so it never keeps the loop alive.
586
- */
587
- private controlCohortReconcileTimer;
588
- /**
589
- * Single-flight guard for {@link reconcileControlCohort}. The eager start pass,
590
- * the recurring interval, and the `self:peer:update` trigger can fire close
591
- * together; collapsing concurrent passes into one in-flight run prevents two
592
- * passes from double-dialing the same siblings (mirrors {@link registerSelfInFlight}).
601
+ * The reconcile pass's schedule (see {@link reconcileControlCohort}): single-flight
602
+ * runs on `table-change-watch.ts`'s rule, with no table subscription of its own — the
603
+ * gate watch ({@link membershipGateWatch}), the control connection listeners,
604
+ * `self:peer:update`, and the pass's own wake ({@link armReconcileWake}) `trigger` it,
605
+ * and its backstop (`network.controlCohort.reconcileMs`, default
606
+ * `DEFAULT_CONTROL_COHORT_RECONCILE_MS`) runs it when nothing has. Built alongside
607
+ * {@link recordRefreshTimer} in {@link startRecordRefresh}, about 1 s after start, and
608
+ * disposed in {@link stopRecordRefresh}; its timers never keep the loop alive.
609
+ */
610
+ private controlReconcileRuns;
611
+ /**
612
+ * The subscription on the membership tables (`ControlDatabase.watchMembershipTables`) that
613
+ * re-materializes the membership gate and triggers {@link controlReconcileRuns} on every
614
+ * change ({@link onMembershipGateTablesChanged}).
615
+ * Taken in {@link start} right after the membership listener is wired, disposed in
616
+ * {@link cleanup} before the control database closes.
617
+ */
618
+ private membershipGateWatch;
619
+ /** The second membership-view drop after a sibling connects ({@link armMembershipViewReload}); cleared in {@link stopRecordRefresh}. */
620
+ private membershipViewReloadTimer;
621
+ /**
622
+ * Consecutive passes whose dial of a selected sibling or a cold-start bootstrap peer
623
+ * failed (`ControlCohortPassOutcome.unconnected`), for the redial ladder
624
+ * ({@link armReconcileWake}); 0 after a pass none of whose dials did. Per session:
625
+ * reset in {@link stopRecordRefresh}.
626
+ */
627
+ private reconcileRedialStreak;
628
+ /** Peers a {@link redialClaimedNode} loop is running for, so a repeated claim starts no second one. */
629
+ private readonly claimRedialPeers;
630
+ /**
631
+ * Single-flight guard for {@link reconcileControlCohort}. An explicit call, the
632
+ * `self:peer:update` trigger and a scheduled run can fire close together; collapsing
633
+ * concurrent passes into one in-flight run prevents two passes from double-dialing the
634
+ * same siblings (mirrors {@link registerSelfInFlight}). A scheduled run waits for this
635
+ * pass and then starts its own rather than joining it.
593
636
  */
594
637
  private reconcileControlCohortInFlight;
595
638
  /**
@@ -612,18 +655,26 @@ export declare class CadreNode implements SAppIdLookup {
612
655
  * machine in the party is already discarding this node's address as stale.
613
656
  */
614
657
  private lastSelfRecordPublishAt;
658
+ /**
659
+ * The newest record this process signed for its own `CadrePeer` row: written by either path
660
+ * of {@link publishSelfRecord}, or handed to a member to write by {@link redeemCadreInvitation}.
661
+ * What {@link publishSelfRecordUnlessCurrent} compares the row against. Per session, cleared
662
+ * with {@link lastSelfRecordPublishAt}: a row carried over from an earlier process is never
663
+ * taken as current.
664
+ */
665
+ private lastSignedSelfRecord;
615
666
  /** Say-once latch for {@link escalateIfSelfRecordStale}; re-armed by the next publish. */
616
667
  private selfRecordStaleWarned;
617
668
  /**
618
669
  * Re-replication queue for owner `authorizePeer` writes that committed while
619
- * this node was alone — `controlNode.getConnections().length === 0` at write
620
- * time means the Optimystic commit was local-only (the block's cluster was ≤1)
621
- * and never broadcast. Maps the affected subject peerId → 'authorize', to
670
+ * this node was alone ({@link committedAlone}) — the Optimystic commit was
671
+ * local-only (the block's cohort was this node) and never broadcast. Maps the
672
+ * affected subject peerId → 'authorize', to
622
673
  * re-issue once the cohort grows. Removals are NOT tracked here — a delete
623
674
  * leaves a `Revocation` tombstone, tracked by {@link pendingRevocations}; a
624
675
  * removePeer also clears any queued authorize for the same subject (the row is
625
- * gone, so re-issuing the insert would be wrong). Drained on the 0→≥1
626
- * control-connection transition by {@link drainPendingControlReplication}.
676
+ * gone, so re-issuing the insert would be wrong). Drained when a sibling becomes
677
+ * reachable ({@link updateControlSiblingReachability}) by {@link drainPendingControlReplication}.
627
678
  */
628
679
  private pendingPeerWrites;
629
680
  /**
@@ -647,14 +698,6 @@ export declare class CadreNode implements SAppIdLookup {
647
698
  * exactly a "removals from before this lifetime" case).
648
699
  */
649
700
  private reissuedHeldRevocations;
650
- /**
651
- * Whether this process has seen the singleton `Revocation` ledger marker filed — its own
652
- * `'opened'` or an `'already-open'` answer, in {@link openRevocationLedgerIfDue}. A
653
- * record of work done, not a cached authorization answer: the marker can never be
654
- * deleted (`NoDelete`), so this can only go stale if the database itself is replaced,
655
- * which is why stop() clears it with the other per-process replication state.
656
- */
657
- private revocationLedgerOpened;
658
701
  /** This node's own `CadrePeer` self-write committed local-only (re-touched on growth). */
659
702
  private pendingSelfPeerWrite;
660
703
  /** This node's own `DeviceToken` self-write committed local-only (re-touched on growth). */
@@ -669,14 +712,24 @@ export declare class CadreNode implements SAppIdLookup {
669
712
  /** Single-flight guard for the re-replication drain (mirrors {@link registerSelfInFlight}). */
670
713
  private drainControlReplicationInFlight;
671
714
  /**
672
- * Tracks the control-connection presence edge so the drain fires only on the
673
- * 0→≥1 transition (the earliest point a re-issue can broadcast), not on every
674
- * subsequent `connection:open`. Re-armed to false once connections return to 0.
715
+ * Connected control peers whose identify says they serve the control database
716
+ * ({@link onControlPeerIdentified}): the peers Optimystic can count into a control block's
717
+ * cohort, before its membership check. A peer joins on its first `peer:identify` of a
718
+ * connected period, not on `connection:open`: until it identifies, every control block's
719
+ * cohort is this node alone. It leaves when its last connection closes. Read through
720
+ * {@link hasReachableControlSibling}, which applies the membership check.
721
+ */
722
+ private readonly identifiedControlPeers;
723
+ /**
724
+ * {@link hasReachableControlSibling} at its last evaluation, so the drain fires on the
725
+ * false→true edge only (the earliest point a re-issue reaches the cohort), not on every later
726
+ * identify. Re-armed to false when no reachable sibling remains
727
+ * ({@link updateControlSiblingReachability}).
675
728
  */
676
- private hasControlConnection;
677
- /** `connection:open` listener driving the growth-triggered drain (teardown in {@link stopRecordRefresh}). */
678
- private controlConnectionOpenHandler;
679
- /** `connection:close` listener re-arming the growth edge (teardown in {@link stopRecordRefresh}). */
729
+ private controlSiblingReachable;
730
+ /** `peer:identify` listener: {@link onControlPeerIdentified} (teardown in {@link stopRecordRefresh}). */
731
+ private controlPeerIdentifyHandler;
732
+ /** `connection:close` listener: {@link onControlConnectionClosed} (teardown in {@link stopRecordRefresh}). */
680
733
  private controlConnectionCloseHandler;
681
734
  constructor(config: CadreNodeConfig);
682
735
  /**
@@ -1002,8 +1055,8 @@ export declare class CadreNode implements SAppIdLookup {
1002
1055
  private controlNetworkName;
1003
1056
  /**
1004
1057
  * Arm the CONTROL network's peer-join block catch-up: push every block in the
1005
- * control database's own raw store to each newly connected AUTHORIZED member
1006
- * this runtime has not yet caught up. This is what physically replicates
1058
+ * control database's own raw store to each AUTHORIZED member on each
1059
+ * (re)connection, skipping what it is known to hold. This is what physically replicates
1007
1060
  * control blocks committed while the writer was alone — the named
1008
1061
  * collection-header blocks written once at genesis above all, whose revision
1009
1062
  * never moves again, so no later commit ever carries them to a member that
@@ -1012,7 +1065,9 @@ export declare class CadreNode implements SAppIdLookup {
1012
1065
  * for peers it knew about before the restart).
1013
1066
  *
1014
1067
  * Unlike the per-strand instances (see `strand-instance-manager.ts`), pushes
1015
- * are gated on {@link isAuthorizedMember}, judged at push time: the control
1068
+ * are gated on the authorized-member snapshot ({@link authorizedControlPeers},
1069
+ * which {@link refreshAuthorizedControlPeers} materializes from the same rows and
1070
+ * rule {@link isAuthorizedMember} queries), judged at push time: the control
1016
1071
  * network's inbound connection gate deliberately admits non-members in
1017
1072
  * several states (seed delivery to an un-enrolled node, an open enrollment
1018
1073
  * window, an outstanding invitation, configured bootstrap/relay peers), and
@@ -1028,9 +1083,9 @@ export declare class CadreNode implements SAppIdLookup {
1028
1083
  * The one non-member the gate admits is a device whose invitation
1029
1084
  * redemption this member is admitting ({@link admitRedeemingDevice}).
1030
1085
  *
1031
- * No-ops (logged) when the embedder configured no control storage or the
1032
- * node exposes no key network — the backfill would have nothing to read or
1033
- * no way to dial.
1086
+ * No-ops (logged) when {@link controlBackfillStorage} answers nothing (disabled,
1087
+ * or no control storage configured) or the node exposes no key network — the
1088
+ * backfill would have nothing to read or no way to dial.
1034
1089
  */
1035
1090
  private startControlBackfill;
1036
1091
  /**
@@ -1070,6 +1125,17 @@ export declare class CadreNode implements SAppIdLookup {
1070
1125
  * operation count.
1071
1126
  */
1072
1127
  private resolveControlStorage;
1128
+ /**
1129
+ * The store the control network's peer-join block catch-up reads, or `undefined` when this
1130
+ * runtime does not arm it: `controlBackfill.enabled: false`, or no control storage. Both of
1131
+ * its readers ask here so they cannot disagree: {@link startControlBackfill} arms on it, and
1132
+ * {@link buildControlNodeOptions} turns Optimystic's rebalance reaction off on it.
1133
+ *
1134
+ * Through the resolver, not the {@link controlStorage} memo field: the resolver is
1135
+ * idempotent, so the catch-up cannot be silently disarmed by a future reordering that moves
1136
+ * a read ahead of the one that populated the field.
1137
+ */
1138
+ private controlBackfillStorage;
1073
1139
  /**
1074
1140
  * Warn the operator when `network.announceAddrs` will silently cost this node the
1075
1141
  * relay reachability it also configured.
@@ -1169,8 +1235,8 @@ export declare class CadreNode implements SAppIdLookup {
1169
1235
  * (it redeems here by seating the row from its bundle, after which check 7
1170
1236
  * admits) or from a sibling whose membership row is still replicating (check 5
1171
1237
  * admits once it lands; if it has not landed by the deadline, the connection
1172
- * is closed and either side's next reconcile dial re-forms it, since outbound
1173
- * dials are never gated). The protocol guard refuses all of them every
1238
+ * is closed and the reconcile pass the close starts on either side re-forms it,
1239
+ * since outbound dials are never gated). The protocol guard refuses all of them every
1174
1240
  * members-only protocol meanwhile. A circuit-relay reservation admitted by
1175
1241
  * {@link admitControlRelayReservation} disarms the deadline (see
1176
1242
  * `membership-connection-gater.ts` → "The relay-reservation seam").
@@ -1318,6 +1384,15 @@ export declare class CadreNode implements SAppIdLookup {
1318
1384
  * needs a key-based admission.
1319
1385
  */
1320
1386
  private isControlNetworkMember;
1387
+ /**
1388
+ * Whether this node sends control writes to `remotePeerId`: Optimystic's `isNetworkMember`
1389
+ * for the control node (cohort and coordinator choice), and the membership half of
1390
+ * {@link hasReachableControlSibling}. The stream gate's rule ({@link isControlNetworkMember}),
1391
+ * unlogged, because Optimystic asks several times per peer per cohort assembly and counts
1392
+ * refusals on its own debug line; plus a device whose verified redemption is being written
1393
+ * ({@link admittingDevices}), which must hold the rows that admit it.
1394
+ */
1395
+ private isControlCohortMember;
1321
1396
  /**
1322
1397
  * Per-stream authorization for the protocol guard's `members-snapshot` class
1323
1398
  * (`control-protocol-guard.ts`): the four Optimystic control-DB protocols
@@ -1345,7 +1420,8 @@ export declare class CadreNode implements SAppIdLookup {
1345
1420
  * this listener with its write lock HELD (`notifyMembershipChanged`), so a retrying
1346
1421
  * read here would sleep its backoff holding the lock and stall every other local
1347
1422
  * writer. Nothing is lost — this refresh already keeps the previous snapshot on
1348
- * failure and is re-driven by the next membership write and by the timed reconcile.
1423
+ * failure and is re-driven by the next membership change (local or replicated, through
1424
+ * the gate watch) and by the reconcile pass's backstop.
1349
1425
  *
1350
1426
  * The trusted-owner anchor sync ({@link syncOwnerAnchor}) runs FIRST, so the snapshot
1351
1427
  * built here judges `CadrePeer` rows against the anchor as the `OwnerKey` table now
@@ -1407,9 +1483,10 @@ export declare class CadreNode implements SAppIdLookup {
1407
1483
  */
1408
1484
  private getBootstrapPeerIds;
1409
1485
  /**
1410
- * What the strand watcher polls: this party's control rows plus a row per cross-party
1486
+ * What the strand watcher reads: this party's control rows plus a row per cross-party
1411
1487
  * join (see {@link joinedStrands}), so a joined strand is offered and relaunched on
1412
- * the same path as the party's own.
1488
+ * the same path as the party's own. It runs on a change to either table; the
1489
+ * machine-local joins wake it through {@link JoinedStrandSession}'s change callback.
1413
1490
  */
1414
1491
  private createStrandQueryable;
1415
1492
  /**
@@ -1448,6 +1525,10 @@ export declare class CadreNode implements SAppIdLookup {
1448
1525
  * after start (non-blocking). {@link registerSelf} is idempotent and safely
1449
1526
  * no-ops when it cannot yet sign/insert (e.g. owner key not installed),
1450
1527
  * so the timer is harmless even when registration only becomes possible later.
1528
+ * A record this process already put in place that is still current (a founding owner's
1529
+ * explicit publish, a redeemed invitation's admission, a restart's first-growth re-touch) is
1530
+ * not written again ({@link publishSelfRecordUnlessCurrent}). Whether the node is connected
1531
+ * yet does not matter: a device that redeems within a second of starting is.
1451
1532
  */
1452
1533
  private scheduleSelfRegistration;
1453
1534
  /**
@@ -1479,11 +1560,42 @@ export declare class CadreNode implements SAppIdLookup {
1479
1560
  /** The body of {@link registerSelf}; serialised by its single-flight guard. */
1480
1561
  private publishSelfRecord;
1481
1562
  /**
1482
- * Record that this node's own address record just landed, and re-arm the stale-record
1483
- * warning. Called from BOTH publishing paths of {@link publishSelfRecord} and nowhere
1484
- * else — a `skipped` publish wrote nothing, so it must not refresh the stamp.
1563
+ * Bookkeeping for a committed write of this node's own `CadrePeer` row by {@link publishSelfRecord}:
1564
+ * `record` is the one the row holds (for the update path, the one its body signed on the
1565
+ * attempt that committed, not the first it signed). Queued for the drain's re-touch
1566
+ * ({@link pendingSelfPeerWrite}) when no sibling was reachable when the write started or when
1567
+ * it ended: a sibling that identifies while the write is out may do so after the write's cohort
1568
+ * was chosen, and the first-growth drain skips a row holding {@link lastSignedSelfRecord} unless
1569
+ * this flag is set. Both fields are set here together so no write can leave the record without
1570
+ * the flag.
1571
+ */
1572
+ private noteSelfPeerWritten;
1573
+ /**
1574
+ * Record that this node's own address record is in place as of `at`, and re-arm the
1575
+ * stale-record warning. Called for BOTH publishing paths of {@link publishSelfRecord}
1576
+ * ({@link noteSelfPeerWritten}), and by {@link publishSelfRecordUnlessCurrent} with the stamp
1577
+ * of a record already in place — a `skipped` publish wrote nothing and found nothing, so it
1578
+ * must not refresh the stamp.
1485
1579
  */
1486
1580
  private noteSelfRecordPublished;
1581
+ /**
1582
+ * {@link registerSelf}, unless the own row already holds the record this process last signed
1583
+ * ({@link lastSignedSelfRecord}) and that record is current ({@link isCurrentSelfRecord});
1584
+ * then nothing is written and the record counts as published at its own stamp. For a publish
1585
+ * that puts a record in place (the boot timer, the reconcile pass's first publish, the
1586
+ * first-growth drain), never the heartbeat, which exists to re-stamp freshness. `'current'`
1587
+ * is this method's own answer; {@link registerSelf} always writes.
1588
+ */
1589
+ private publishSelfRecordUnlessCurrent;
1590
+ /**
1591
+ * Is `record` still the one to publish: signed less than half a heartbeat ago, with this
1592
+ * node's profile, over the addresses it would publish now, and the signature the own row
1593
+ * holds? The age bound keeps a record that is not re-written fresh for resolvers: at most half
1594
+ * a heartbeat old here and re-stamped by the heartbeat within one more, so never older than
1595
+ * 1.5 heartbeats (11.25 min) against {@link DEFAULT_PEER_RECORD_MAX_AGE_MS}. Cheap checks
1596
+ * first: the row comes from the membership view, the addresses from the app's resolver.
1597
+ */
1598
+ private isCurrentSelfRecord;
1487
1599
  /**
1488
1600
  * Report an abandoned control write to the embedding app.
1489
1601
  *
@@ -1570,12 +1682,17 @@ export declare class CadreNode implements SAppIdLookup {
1570
1682
  * process does not learn it was removed ({@link noteOwnRemoval}), and pays that read on every
1571
1683
  * pass.
1572
1684
  *
1573
- * Connected-only: a row written elsewhere arrives over a connection, and an owner's publish
1574
- * committed alone is local-only and forks its own history — a restarted owner whose embedder
1575
- * runs a pass before it reconnects would write its row on the stale side of the fork, and
1576
- * then never see rows written while it was down (`cadre-invite-any-member` runs that pass and
1577
- * fails every time without this condition). The boot-time publish and the first-growth drain already try at the node's lone and
1578
- * first-connection moments; this retries on the passes after.
1685
+ * Only while a sibling is reachable ({@link hasReachableControlSibling}): a row written
1686
+ * elsewhere arrives from a sibling, and an owner's publish committed alone is local-only and
1687
+ * forks its own history — a restarted owner whose embedder runs a pass before it reconnects
1688
+ * would write its row on the stale side of the fork, and then never see rows written while it
1689
+ * was down (`cadre-invite-any-member` runs that pass and fails every time without this
1690
+ * condition). A connection is not enough: until the sibling identifies, the write's cohort is
1691
+ * this node alone. The boot-time publish and the first-growth drain already try at the
1692
+ * node's lone and first-sibling moments; this retries on the passes after.
1693
+ *
1694
+ * A machine admitted by invitation finds its row already holding the record its redemption
1695
+ * carried, and writes nothing ({@link publishSelfRecordUnlessCurrent}).
1579
1696
  */
1580
1697
  private publishFirstSelfRecord;
1581
1698
  /**
@@ -1592,7 +1709,21 @@ export declare class CadreNode implements SAppIdLookup {
1592
1709
  * stack listeners/timers.
1593
1710
  */
1594
1711
  private startRecordRefresh;
1595
- /** Tear down the self-record refresh timers + listener (see {@link cleanup}). */
1712
+ /** The reconcile schedule's backstop: `network.controlCohort.reconcileMs`, default `DEFAULT_CONTROL_COHORT_RECONCILE_MS`. */
1713
+ private controlReconcileBackstopMs;
1714
+ /**
1715
+ * The gate watch's run ({@link membershipGateWatch}): re-materialize the gate, so a
1716
+ * replicated revocation hangs up the removed machine now (`hangUpRevokedControlPeers`
1717
+ * ends every refresh), then start a reconcile pass for whatever else the change means
1718
+ * (a new sibling to dial, a tombstoned row to reap). A local membership write refreshes
1719
+ * the gate twice — once from the awaited membership listener, so the writer sees its
1720
+ * change before returning, and once here, 50 ms later. The listener's refresh runs under
1721
+ * the write lock, so its reads take the committed-read path and the view keeps nothing
1722
+ * from them; this refresh is the read that refills the view after the write, which some
1723
+ * later caller would otherwise pay. Accepted: one reload per change, not two.
1724
+ */
1725
+ private onMembershipGateTablesChanged;
1726
+ /** Tear down the self-record refresh timers, the reconcile schedule and the listeners (see {@link cleanup}). */
1596
1727
  private stopRecordRefresh;
1597
1728
  /**
1598
1729
  * Resolve a peer's current, signed, trust-checkable multiaddrs from only its
@@ -1666,37 +1797,49 @@ export declare class CadreNode implements SAppIdLookup {
1666
1797
  * Run one proactive control-cohort dial pass to keep this node connected to its
1667
1798
  * cadre siblings (so the `CadreControl` collections form a replicating cohort).
1668
1799
  * Public so the cohort-growth-driven re-replication path
1669
- * (`control-write-ensure-replicated`) and tests can trigger a pass on demand;
1670
- * normally driven by the eager start pass, the recurring interval, and
1671
- * `self:peer:update` (all wired in {@link startRecordRefresh}).
1800
+ * (`control-write-ensure-replicated`), `addDrone`'s callers, `claimNode`,
1801
+ * `redeemCadreInvitation` and tests can run a pass on demand. On its own a pass
1802
+ * runs when something it acts on changes, and otherwise at a backstop
1803
+ * ({@link controlReconcileRuns}): once after start; on `self:peer:update`; when a
1804
+ * membership table changes, local commit or replicated ({@link membershipGateWatch});
1805
+ * when a control connection to an authorized sibling opens or closes
1806
+ * ({@link onControlSiblingConnectionChange}); on the redial and strand-address wakes
1807
+ * a pass arms for itself ({@link armReconcileWake}); and at the backstop,
1808
+ * `network.controlCohort.reconcileMs` (default `DEFAULT_CONTROL_COHORT_RECONCILE_MS`,
1809
+ * 2 minutes) after the last pass settled. A connection to a relay, a bootstrap node or
1810
+ * another party's machine starts no pass: the pass acts on none of them, beyond
1811
+ * counting them as "connected" for its write steps, which therefore run at the next
1812
+ * membership change or backstop on a one-machine party whose only connection is a
1813
+ * relay.
1672
1814
  *
1673
1815
  * Concurrent triggers collapse into a single in-flight pass
1674
1816
  * (see {@link reconcileControlCohortInFlight}) so two passes never double-dial;
1675
- * a call that joins an in-flight pass resolves to THAT pass's result.
1817
+ * a call that joins an in-flight pass resolves to THAT pass's result (a scheduled
1818
+ * run waits for it instead, so the change that woke the run is seen).
1676
1819
  * Best-effort throughout: a failure to resolve/dial any one sibling is logged
1677
1820
  * and the pass continues; the whole pass is a no-op when the node is alone.
1678
1821
  *
1679
1822
  * Resolves to the peers the pass dialled ({@link ControlCohortReconcileResult}),
1680
1823
  * so a caller can tell a link this pass opened from one something else opened —
1681
- * the pass skips a peer that is already connected. The timer and event triggers
1682
- * discard it.
1824
+ * the pass skips a peer that is already connected. The scheduled runs discard it.
1683
1825
  */
1684
1826
  reconcileControlCohort(): Promise<ControlCohortReconcileResult>;
1827
+ /** One pass, then the wake it earns ({@link armReconcileWake}); the public result is the pass's `dialed`. */
1828
+ private runReconcileControlCohortAndArmWake;
1829
+ /**
1830
+ * After every pass, explicit or scheduled: advance or reset the redial streak and
1831
+ * `trigger` the schedule for the earliest of the pass's own reasons to run again
1832
+ * (`nextReconcileWakeMs`): a redial for a sibling or bootstrap peer it left
1833
+ * unconnected, or a strand-address ask falling due before the backstop. Nothing else
1834
+ * the pass does needs a wake sooner than the backstop: delegate grants re-announce
1835
+ * every 15 minutes inside a 30-minute TTL, the FRET and formation address re-merges
1836
+ * guard a one-hour expiry, and the self record has its own heartbeat. A pass that
1837
+ * runs before the schedule exists (an explicit call during the first second after
1838
+ * start) still counts for the streak.
1839
+ */
1840
+ private armReconcileWake;
1685
1841
  /** Body of {@link reconcileControlCohort}; serialised by its single-flight guard. */
1686
1842
  private runReconcileControlCohort;
1687
- /**
1688
- * The owner-only half of the reconcile pass's ledger-marker step: file the singleton
1689
- * `Revocation` marker ({@link SeedBootstrapService.openRevocationLedger}) unless this
1690
- * process has already seen it filed. The connectivity gate is the caller's
1691
- * ({@link runReconcileControlCohort}); why the marker exists is on
1692
- * {@link ControlDatabase.openRevocationLedger}.
1693
- *
1694
- * Sets {@link revocationLedgerOpened} on `'opened'` or `'already-open'`. A node that
1695
- * cannot sign as an owner does nothing: it picks the owner's marker up the first time it
1696
- * reads the table, like any other replicated row. Best-effort like every step of the
1697
- * pass — a failure is logged and leaves the flag clear, so the next connected pass retries.
1698
- */
1699
- private openRevocationLedgerIfDue;
1700
1843
  /**
1701
1844
  * Resolve every sibling's signed address record once and merge what resolves
1702
1845
  * into the control node's libp2p **address book** (peerStore). Returns the
@@ -1720,8 +1863,9 @@ export declare class CadreNode implements SAppIdLookup {
1720
1863
  * current ({@link refreshDialHint}).
1721
1864
  *
1722
1865
  * NOTE: this resolves EVERY sibling serially before the dial loop below runs,
1723
- * so it costs one record query per sibling per reconcile pass (~15s) and each
1724
- * one delays the pass's first dial. A cadre is a handful of devices, so today
1866
+ * so it costs one record query per sibling per reconcile pass (served from the
1867
+ * membership rows view while it is current) and each one delays the pass's first
1868
+ * dial. A cadre is a handful of devices, so today
1725
1869
  * that is a few extra local reads; if cadres ever grow large, batch the records
1726
1870
  * into one query, merge only on change, or move the warm pass after the dials.
1727
1871
  */
@@ -1783,16 +1927,18 @@ export declare class CadreNode implements SAppIdLookup {
1783
1927
  */
1784
1928
  private collectSiblingStrandAddrs;
1785
1929
  /**
1786
- * Dial one sibling from its already-resolved addresses, best-effort. Returns
1787
- * whether the dial resolved (false when no address resolves or the dial fails).
1930
+ * Dial one sibling from its already-resolved addresses, best-effort.
1788
1931
  *
1789
1932
  * A per-peer failure (NAT, offline, relay down, connection-gater denial, or
1790
1933
  * the {@link controlDialBudget} expiring) is logged and swallowed so one
1791
1934
  * unreachable sibling never aborts the pass — exactly like
1792
1935
  * {@link SeedBootstrapService.applySeed}'s owner-dial loop. A failed dial is
1793
- * simply retried on the next pass. A sibling whose every address relays
1794
- * through this node ({@link SelfRelayOnlyError}) is not dialed at all and gets
1795
- * a one-line log instead: only the sibling can reconnect.
1936
+ * retried on the pass's redial ladder ({@link armReconcileWake}: 15 s, 30 s, 60 s,
1937
+ * then every backstop) and on any pass a change starts sooner. A sibling with no
1938
+ * dialable address, or whose every address relays through this node
1939
+ * ({@link SelfRelayOnlyError}), is not dialed at all and is `undialable`: it gets a
1940
+ * one-line log and no redial, since only a change — its record replicating in, or
1941
+ * the sibling connecting — can make it dialable, and each of those starts a pass.
1796
1942
  */
1797
1943
  private dialControlSibling;
1798
1944
  /**
@@ -1836,7 +1982,7 @@ export declare class CadreNode implements SAppIdLookup {
1836
1982
  * These addresses are never merged into the libp2p address book: they are
1837
1983
  * unverified, and the address book takes only verified ones (see
1838
1984
  * `mergePeerAddrs`). Layers that dial by bare peer id use the connection this
1839
- * dial opens, and after it drops the next reconcile pass dials again.
1985
+ * dial opens, and after it drops the reconcile pass the close starts dials again.
1840
1986
  *
1841
1987
  * NOTE: an entry is consulted here only for a current sibling, so a peer revoked
1842
1988
  * by another owner is not dialed from it, even though the entry stays (only this
@@ -1870,8 +2016,7 @@ export declare class CadreNode implements SAppIdLookup {
1870
2016
  * Called for every seed this node accepts, on BOTH intake paths — the
1871
2017
  * {@link applySeed} wrapper (which may run on a throwaway service, so the
1872
2018
  * service itself is the wrong place to keep this) and the inbound
1873
- * `/sereus/seed/1.0.0` handler via `onSeedApplied`. Peers with no address are
1874
- * skipped: there is nothing to dial.
2019
+ * `/sereus/seed/1.0.0` handler via `onSeedApplied`.
1875
2020
  *
1876
2021
  * `preferred` is the seed's own claim about its peers, exactly as
1877
2022
  * `SeedBootstrapService.applySeed` uses it to choose its dial targets. That is
@@ -1879,7 +2024,8 @@ export declare class CadreNode implements SAppIdLookup {
1879
2024
  * signature-checked against a trust-anchored signer before this runs, and the
1880
2025
  * flag only *selects a dial target*; a dial grants no authority.
1881
2026
  *
1882
- * Addressless peers and self are skipped by {@link retainDialTarget}.
2027
+ * The peers retained are `seedDialTargets` (addressless peers and self dropped), the same
2028
+ * ones the service dials; they are returned for the `seed:applied` payload.
1883
2029
  */
1884
2030
  private recordSeedBootstrapPeers;
1885
2031
  /**
@@ -1894,9 +2040,8 @@ export declare class CadreNode implements SAppIdLookup {
1894
2040
  * failing a seed or an add that has already been accepted.
1895
2041
  *
1896
2042
  * Two peers are never retained. One with no address: there is nothing to dial.
1897
- * And self: `createSeed` projects EVERY `CadrePeer` row, so an owner that applies
1898
- * a seed minted after it joined finds itself in the owner list, and `addDrone`
1899
- * could be handed this node's own id. Retaining self would make the cold-start
2043
+ * And self: `addDrone` could be handed this node's own id (seed intake arrives
2044
+ * already filtered by `seedDialTargets`). Retaining self would make the cold-start
1900
2045
  * pass dial this node forever (the steady-state pass filters self out of its
1901
2046
  * sibling list for the same reason).
1902
2047
  */
@@ -1914,15 +2059,16 @@ export declare class CadreNode implements SAppIdLookup {
1914
2059
  * help: it dials only siblings enumerated from that very table. Retrying here
1915
2060
  * is the only way back in.
1916
2061
  *
1917
- * Unbounded, at the reconcile cadence, with no backoff: the branch is already
1918
- * gated by "the control database has no siblings", so it stops the moment the
1919
- * node is actually in the party, and a node that is stranded MUST keep trying —
1920
- * a give-up rule would turn a transient outage into a permanent one. The steady
1921
- * cost is one dial per bootstrap peer per pass (a seed prefers its minter and the
1922
- * party's always-on machines, a few at most), each failing fast against an
1923
- * unreachable address.
1924
- * NOTE: if seeds ever carry many preferred peers, or the reconcile cadence tightens
1925
- * well below its 15 s default, add per-peer backoff here.
2062
+ * Unbounded: the branch is already gated by "the control database has no
2063
+ * siblings", so it stops the moment the node is actually in the party, and a node
2064
+ * that is stranded MUST keep trying — a give-up rule would turn a transient outage
2065
+ * into a permanent one. A failed dial earns the pass a redial wake on the ladder
2066
+ * {@link armReconcileWake} runs (15 s, 30 s, 60 s, then every backstop, 2 minutes
2067
+ * by default), so the steady cost is one dial per bootstrap peer per backstop (a
2068
+ * seed prefers its minter and the party's always-on machines, a few at most), each
2069
+ * failing fast against an unreachable address.
2070
+ * NOTE: the ladder is per pass, not per peer; if seeds ever carry many preferred
2071
+ * peers, add per-peer backoff here.
1926
2072
  *
1927
2073
  * Best-effort per peer, exactly like {@link dialControlSibling}: one dead
1928
2074
  * address never aborts the pass.
@@ -1941,20 +2087,26 @@ export declare class CadreNode implements SAppIdLookup {
1941
2087
  * encapsulated; an unencapsulatable one is dropped rather than dialed bare.
1942
2088
  */
1943
2089
  private bootstrapDialAddrs;
1944
- /** Dial one cold-start bootstrap peer, best-effort. Returns whether it connected. */
2090
+ /**
2091
+ * Dial one peer from its retained dial target's addresses, best-effort: a cold-start bootstrap
2092
+ * peer, or a just-claimed node ({@link redialClaimedNode}), which `context` names in the logs.
2093
+ * Returns whether it connected.
2094
+ */
1945
2095
  private dialBootstrapPeer;
1946
2096
  /**
1947
- * Whether a control write happening now would commit local-only. A sound lower
1948
- * bound is "no connected control peers": 0 connections ⇒ the block's cluster is
1949
- * ≤1 ⇒ Optimystic commits without broadcasting. This is the pragmatic proxy for
1950
- * the precise signal (the block's `getClusterSize`); it over-approximates safely
1951
- * — a connected-but-not-in-this-block's-cluster write may still be local-only and
1952
- * is caught by the cohort's periodic pull-on-read instead. Re-issuing an already-
1953
- * replicated row is harmless (an idempotent monotonic bump), so the coarse proxy
1954
- * is acceptable as the agreed first cut.
2097
+ * Whether a control write happening now would commit local-only: no sibling is reachable
2098
+ * ({@link hasReachableControlSibling}, the public form, so the two cannot drift). A relay, a
2099
+ * bootstrap node or a peer that has not identified yet holds a connection without being in the
2100
+ * write's cohort, so a write made with only those connected is queued too. One made alone at
2101
+ * the revision the cohort reached meanwhile is a fork no later consult sees, because a consult
2102
+ * compares revision numbers only; the re-issue on the growth edge is what conflicts with the
2103
+ * cohort's copy and pulls it in.
1955
2104
  *
1956
- * Reads {@link getControlConnectionCount} — the public, embedder-facing form of
1957
- * the same sample — so the two cannot drift.
2105
+ * Still a proxy for the precise signal (the block's own cohort): a reachable sibling outside
2106
+ * this block's cohort leaves the write local-only, caught by the cohort's pull-on-read. On the
2107
+ * control network every block's cohort is the whole party, so that gap is a party larger than
2108
+ * `CONTROL_REPLICATION_BREADTH`. Re-issuing a replicated row is harmless (an idempotent
2109
+ * monotonic bump).
1958
2110
  */
1959
2111
  private committedAlone;
1960
2112
  /**
@@ -1984,7 +2136,7 @@ export declare class CadreNode implements SAppIdLookup {
1984
2136
  /**
1985
2137
  * Drain the write-while-alone re-replication queue: re-issue the writes that
1986
2138
  * committed local-only now that the cohort can broadcast them. Public so the
1987
- * 0→≥1 growth trigger ({@link handleControlConnectionChange}) and tests can drive
2139
+ * growth trigger ({@link updateControlSiblingReachability}) and tests can drive
1988
2140
  * it; concurrent calls collapse into one in-flight drain
1989
2141
  * ({@link drainControlReplicationInFlight}) so two growth signals never
1990
2142
  * double-issue. Best-effort throughout — a per-row failure leaves that entry
@@ -2053,8 +2205,11 @@ export declare class CadreNode implements SAppIdLookup {
2053
2205
  private reconstructAuthoredMembership;
2054
2206
  /**
2055
2207
  * Body of {@link reconstructAuthoredMembership}'s sweep: re-issue each candidate
2056
- * owner-authored row, returning how many were re-touched. A per-row failure is
2057
- * logged and skipped; a teardown mid-sweep stops it.
2208
+ * owner-authored row, returning how many were re-touched. A row the sweep already holds as
2209
+ * self-published is skipped without a call; the re-touch itself decides again over the row
2210
+ * it reads under the lock, so a row that vanished or was self-published since the sweep's
2211
+ * read is reported, not written. A per-row failure is logged and skipped; a teardown
2212
+ * mid-sweep stops it.
2058
2213
  */
2059
2214
  private reissueAuthoredMembershipRows;
2060
2215
  /**
@@ -2070,9 +2225,10 @@ export declare class CadreNode implements SAppIdLookup {
2070
2225
  */
2071
2226
  private drainPendingPeerWrites;
2072
2227
  /**
2073
- * Body of {@link drainPendingPeerWrites}' loop. An entry is cleared only on
2074
- * success (or when there is nothing left to re-issue); a failure is logged and
2075
- * the entry stays queued for the next cohort growth.
2228
+ * Body of {@link drainPendingPeerWrites}' loop. An entry is cleared on every outcome the
2229
+ * re-touch reports — re-touched, or nothing left to re-issue because the row was removed
2230
+ * since or the peer self-published; the re-touch decides which over the row it reads under
2231
+ * the lock. A failure is logged and the entry stays queued for the next cohort growth.
2076
2232
  */
2077
2233
  private reissuePendingPeerWrites;
2078
2234
  /**
@@ -2080,23 +2236,84 @@ export declare class CadreNode implements SAppIdLookup {
2080
2236
  * `UpdatedAt` strictly above the stored value (and the wall clock) and re-sign via
2081
2237
  * the owner branch of `CadrePeer.AuthorizedUpdate`. The row already exists
2082
2238
  * locally (it committed there, just local-only), so this is an UPDATE, not the
2083
- * original INSERT.
2239
+ * original INSERT. The bump, and whether the row is still the owner's to re-issue, are
2240
+ * decided over the row as read inside the write ({@link SeedBootstrapService.reauthorizePeer}'s
2241
+ * re-broadcast mode).
2084
2242
  */
2085
2243
  private reissuePeerAuthorize;
2086
2244
  /**
2087
- * On a control connection opening, detect the 0→≥1 transition and drain the
2088
- * write-while-alone re-replication queue (single-flight; fires only on the edge,
2089
- * not on every subsequent `connection:open`). Best-effort — a drain failure is
2090
- * logged, not thrown.
2091
- */
2092
- private handleControlConnectionChange;
2093
- /** Re-arm the growth edge once all control connections drop, so a later reconnect re-drains. */
2094
- private handleControlConnectionClose;
2095
- /**
2096
- * Wire the control-connection growth listeners that drive write-while-alone
2097
- * re-replication. Wired in {@link start} (not the delayed refresh path) so no
2098
- * early `connection:open` is missed; torn down in {@link stopRecordRefresh}.
2099
- * Idempotent.
2245
+ * Re-evaluate {@link hasReachableControlSibling} and act on its edges: on false→true, drain
2246
+ * the write-while-alone re-replication queue (single-flight; best-effort, a drain failure is
2247
+ * logged, not thrown); on true→false, re-arm, so the next reachable sibling drains again.
2248
+ * Called when a peer identifies, when a connection closes, and after every membership refresh,
2249
+ * where a sibling that connected before this node authorized it (the production join order)
2250
+ * becomes reachable. A device counted in for its admission ({@link admittingDevices}) is not
2251
+ * a trigger: the admission's own writes are in flight, and its row landing refreshes
2252
+ * membership.
2253
+ */
2254
+ private updateControlSiblingReachability;
2255
+ /**
2256
+ * A connected peer identified as serving the control database: count it
2257
+ * ({@link identifiedControlPeers}), re-evaluate the growth edge, and run the sibling-open
2258
+ * reactions, once per connected period. libp2p re-emits `peer:identify` on every
2259
+ * identify-push from a connected peer, and those repeats start nothing.
2260
+ */
2261
+ private onControlPeerIdentified;
2262
+ /**
2263
+ * A control connection closed: `remotePeer` stops counting once it has none left, and the
2264
+ * growth edge re-arms if no reachable sibling remains. The sibling-close reaction runs on every
2265
+ * close. libp2p's connection manager drops the closed connection from `getConnections` before
2266
+ * this listener runs: libp2p dispatches each event on its internal bus, where the connection
2267
+ * manager listens, before re-dispatching it to the node's own listeners.
2268
+ */
2269
+ private onControlConnectionClosed;
2270
+ /**
2271
+ * Count the peers that identified before {@link wireControlConnectionListeners} ran, whose
2272
+ * `peer:identify` has already fired, from their peer-store records. Updates the growth edge
2273
+ * but runs no sibling-open reaction, as a connection that predates the listeners never did. A
2274
+ * peer not yet in the peer store is still identifying, and its own event counts it.
2275
+ */
2276
+ private noteIdentifiedControlPeers;
2277
+ /**
2278
+ * An AUTHORIZED sibling became reachable (its identify, once per connected period:
2279
+ * {@link onControlPeerIdentified}) or one of its connections closed: start a reconcile
2280
+ * pass. On open the pass asks the new sibling for strand addresses and re-arms the
2281
+ * delegate grants; on close it re-dials. First, on open, drop the membership rows
2282
+ * views: a machine that was offline while a sibling committed learns of the commit
2283
+ * only when it next reads the block from storage, and dropping the views makes the
2284
+ * pass's reads do that now rather than after the views' maximum age. Those reads can
2285
+ * fall inside the read-repair window, so the open also arms a second drop past it
2286
+ * ({@link armMembershipViewReload}). Any other
2287
+ * peer — a relay, a bootstrap node, another party's machine — starts nothing.
2288
+ *
2289
+ * On identify rather than `connection:open` because, until the sibling identifies,
2290
+ * Optimystic's cohort for every control block is this node alone: a pass started at the
2291
+ * open writes (the first self-record publish) and reads on a cohort of one.
2292
+ */
2293
+ private onControlSiblingConnectionChange;
2294
+ /**
2295
+ * Drop the membership views again {@link MEMBERSHIP_VIEW_RECONNECT_RELOAD_MS} after a
2296
+ * sibling connects, and start a pass whose reads reload them. This node's reads before
2297
+ * the connection stamped Optimystic's read-repair window on the control blocks, so the
2298
+ * reload the open itself starts is served from the local copy with no cohort consult,
2299
+ * and the view would keep rows that predate the sibling's commits until its maximum age.
2300
+ * The reload past the window starts the consults; one that brings a newer revision
2301
+ * raises the table change that drops the view and refreshes the gate. Re-armed by every
2302
+ * open, so it runs one window after the latest.
2303
+ *
2304
+ * NOTE: a consult Optimystic defers (16 already in flight) starts nothing and leaves the block
2305
+ * unstamped, so the view keeps its rows until the first read past its maximum age, which on an
2306
+ * idle node is the reconcile backstop. If `cluster-tx:read-repair-deferred` lines show at a
2307
+ * reconnect, re-arm this reload once more instead of waiting for the backstop.
2308
+ */
2309
+ private armMembershipViewReload;
2310
+ /**
2311
+ * Wire the control-connection listeners, which keep {@link identifiedControlPeers}: the
2312
+ * growth edge that drives write-while-alone re-replication, and the sibling reconcile
2313
+ * trigger ({@link onControlSiblingConnectionChange}). Wired in {@link start} (not the
2314
+ * delayed refresh path) so no early identify is missed, and reads back the peers that
2315
+ * identified before it ran ({@link noteIdentifiedControlPeers}); torn down in
2316
+ * {@link stopRecordRefresh}. Idempotent.
2100
2317
  */
2101
2318
  private wireControlConnectionListeners;
2102
2319
  /**
@@ -2353,8 +2570,9 @@ export declare class CadreNode implements SAppIdLookup {
2353
2570
  * registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
2354
2571
  * automatic relaunch need it. A failed launch here hands the strand back to that
2355
2572
  * relaunch — `StrandWatcher.forgetStrand` drops the id from the watcher's
2356
- * `knownStrands` and records the backoff, so a later poll re-attempts it on the same
2357
- * `pollInterval * 2^(failures-1)` ladder a watcher-driven failure gets. Because the
2573
+ * `knownStrands`, records the backoff and wakes the watcher when it expires, so the
2574
+ * strand is re-attempted on the same ladder a watcher-driven failure gets
2575
+ * (`min(strandWatchInterval, 5 s) * 2^(failures-1)`, capped at 5 minutes). Because the
2358
2576
  * config stays registered, the retry takes `handleStrandAdded`'s auto-launch branch:
2359
2577
  * what the app sees is `strand:error` per failed retry and `strand:started` when one
2360
2578
  * succeeds, never a second `strand:discovered`. {@link detachStrand} (reached via
@@ -2412,7 +2630,8 @@ export declare class CadreNode implements SAppIdLookup {
2412
2630
  * (the key exists nowhere else). No app-data wipe is needed for either.
2413
2631
  *
2414
2632
  * The insert is signed through this node's owner signer ({@link getOwnerSigner}), in one
2415
- * approval with a closed strand's party-key mint. The signer's key is persisted on the row
2633
+ * approval and one control transaction with a closed strand's party key
2634
+ * ({@link ControlDatabase.insertFoundingRows}). The signer's key is persisted on the row
2416
2635
  * as `FounderOwnerKey` (the schema pins the column to the verified signer), and this node's
2417
2636
  * peer id as `FounderPeerId` (bound into the signature), which is what later lets any
2418
2637
  * launch derive "this machine is the founder" from the row alone.
@@ -2423,7 +2642,8 @@ export declare class CadreNode implements SAppIdLookup {
2423
2642
  *
2424
2643
  * A CLOSED strand's publish also seats this party's own membership identity key —
2425
2644
  * the `StrandPartyKey` row {@link ensureStrandPartyKey} documents — which is what the
2426
- * founder bootstrap derives `Member.Key`/`Manager.MemberKey` from. The
2645
+ * founder bootstrap derives `Member.Key`/`Manager.MemberKey` from; a key the party already
2646
+ * holds for the id is kept. The
2427
2647
  * `memberPrivateKey` ARGUMENT stays the strand-wide read secret formation hands to
2428
2648
  * joiners; it derives nobody's identity.
2429
2649
  *
@@ -2443,16 +2663,55 @@ export declare class CadreNode implements SAppIdLookup {
2443
2663
  */
2444
2664
  publishStrand(strandId: string, type?: 'o' | 'c', memberPrivateKey?: string, sign?: OwnerSign): Promise<StrandRow>;
2445
2665
  /**
2446
- * The write half of {@link publishStrand}: insert `desired`, adopting a matching row a
2447
- * concurrent founder landed first, then mint a closed strand's party key, all under one
2448
- * owner approval's `sign`.
2666
+ * The `Strand` row this machine publishes for `strandId`, as {@link publishStrand} and
2667
+ * {@link foundStrand} write it: the id trimmed and checked, this node's owner key and peer id
2668
+ * as its founder.
2669
+ */
2670
+ private founderStrandRow;
2671
+ /**
2672
+ * The write half of {@link publishStrand} and {@link foundStrand}: under one owner approval,
2673
+ * insert `desired`, a closed strand's party key and the requested invitation in one control
2674
+ * transaction ({@link ControlDatabase.insertFoundingRows}), adopting a matching row a
2675
+ * concurrent founder landed first. Reads nothing first: the caller has just read the
2676
+ * `Strand` row, and a stored key or row is resolved from the insert's own rejection.
2677
+ */
2678
+ private writeFoundingRows;
2679
+ /** {@link writeFoundingRows} inside its approval. */
2680
+ private insertOrAdoptFounding;
2681
+ /**
2682
+ * Insert `desired`'s founding rows, minting a closed strand's party key — this party's own
2683
+ * membership identity, which the founder bootstrap derives `Member`/`Manager` from, distinct
2684
+ * from the row's shared `MemberPrivateKey` read secret. A key already stored for the id with
2685
+ * no strand row (one {@link ensureStrandPartyKey} seated, which it would adopt too) refuses
2686
+ * that transaction whole; the rows are then written again without a key.
2687
+ *
2688
+ * @returns The party key this seated; undefined when it seated none, so the launch reads the stored one.
2689
+ */
2690
+ private seatFoundingRows;
2691
+ /**
2692
+ * Resolve a founding refused on `Strand.Id`: re-read the row that landed and adopt it when it
2693
+ * matches `desired` — a concurrent founder, such as two machines of one party on one id —
2694
+ * or throw naming the columns that differ.
2449
2695
  */
2450
- private insertStrandRow;
2696
+ private adoptConcurrentFounding;
2697
+ /**
2698
+ * Publish a founding's invitation on its own, bound to `strandId`, for a founding that adopted
2699
+ * a stored row rather than writing its own — unless the invitation is already stored, bound to
2700
+ * this strand, as a repeat of a founding whose transaction committed leaves it.
2701
+ *
2702
+ * @param sign - The open approval's `sign`; omitted, the publish asks for its own.
2703
+ */
2704
+ private publishFoundingInviteIfAbsent;
2451
2705
  /**
2452
2706
  * Found a strand — publish its row cadre-wide AND start the local instance as its
2453
2707
  * founder — in one resumable call. The single entry point for creating a strand; a
2454
2708
  * caller joining one someone else founded uses {@link addStrand} alone.
2455
2709
  *
2710
+ * The publish is one control transaction under one owner approval: the `Strand` row, a
2711
+ * closed strand's `StrandPartyKey` row, and the invitation in
2712
+ * {@link FoundStrandConfig.formationInvite} when one is given. The start then uses the party
2713
+ * key the publish seated rather than reading it back.
2714
+ *
2456
2715
  * Safe to re-run from any point of interruption, which hand-rolling
2457
2716
  * {@link publishStrand} + {@link addStrand} is not. Either half can already have
2458
2717
  * happened:
@@ -2511,10 +2770,20 @@ export declare class CadreNode implements SAppIdLookup {
2511
2770
  * so both forms of `foundStrand` reject on it before returning.
2512
2771
  */
2513
2772
  private publishFoundingRow;
2773
+ /** {@link publishFoundingRow} when no row is stored: write the founding rows under this node's owner. */
2774
+ private publishFoundingStrand;
2775
+ /**
2776
+ * {@link publishFoundingRow} over a stored row: adopt it ({@link adoptPublishedStrand}), then
2777
+ * publish the requested invitation on its own, under its own approval.
2778
+ */
2779
+ private resumeFounding;
2514
2780
  /**
2515
2781
  * The start half of {@link foundStrand}. Its timing lines (`addStrand: start`, then the
2516
2782
  * duration or `failed after`) are logged when the start settles, also in the background
2517
2783
  * form, so a start that hangs still shows as a start line with no end line.
2784
+ *
2785
+ * A party key the publish half just seated is handed to the launch, which would otherwise
2786
+ * read back the row the same founding wrote.
2518
2787
  */
2519
2788
  private attachFoundingRow;
2520
2789
  /**
@@ -2530,9 +2799,10 @@ export declare class CadreNode implements SAppIdLookup {
2530
2799
  * inverse of {@link publishStrand}.
2531
2800
  *
2532
2801
  * Party-wide, unlike {@link stopStrand}, which only stops the strand on THIS node: every
2533
- * cadre node watching the table sees the row vanish on its next poll (default 5 s) and
2534
- * stops its own instance. This node converges immediately — the method forces a watcher
2535
- * poll and stops any still-running local instance before resolving. Removing the row
2802
+ * cadre node watching the table hears the row's removal and stops its own instance (a
2803
+ * machine outside the `Strand` block's cohort, on a cadre larger than 16, only at its
2804
+ * watcher's backstop read, default 5 minutes). This node converges immediately — the
2805
+ * method forces a watcher run and stops any still-running local instance before resolving. Removing the row
2536
2806
  * removes OUR party's participation only: other parties in the strand keep their own
2537
2807
  * rows and the strand network carries on, so no cross-party sign-off is involved — this
2538
2808
  * is an owner-signed control-plane write like every other, not "destroy the network".
@@ -2597,9 +2867,11 @@ export declare class CadreNode implements SAppIdLookup {
2597
2867
  * as `MemberPrivateKey`; docs/strands.md → "Closed-Strand Member Key Handling") and is
2598
2868
  * never put on the formation wire.
2599
2869
  *
2600
- * Called internally by {@link publishStrand} (closed strands mint at publish) and by a
2601
- * founder launch that finds no row (a publish interrupted before its mint); public so a
2602
- * test harness — or a joiner flow that persists a formation-issued identity — can seat
2870
+ * A founding seats the key in the same transaction as the strand row, not through here.
2871
+ * Called internally by {@link publishStrand}'s repeat of a published closed strand and by a
2872
+ * founder launch that finds no row, which heal a closed strand row stored without its key
2873
+ * (one seated by `ControlDatabase.insertStrand` directly, or whose key was removed); public
2874
+ * so a test harness — or a joiner flow that persists a formation-issued identity — can seat
2603
2875
  * a specific key deliberately.
2604
2876
  *
2605
2877
  * @param strandId - The strand the key is this party's identity for.
@@ -2616,8 +2888,8 @@ export declare class CadreNode implements SAppIdLookup {
2616
2888
  * Resolve the party membership key a closed strand's launch threads into the founder
2617
2889
  * bootstrap: the explicit attach-time key when given, else the party's persisted
2618
2890
  * `StrandPartyKey` row, else — on the one machine the row names as founder (`FounderPeerId`)
2619
- * — a freshly minted-and-persisted key, which seats the identity for a publish that was
2620
- * interrupted before its mint. Everyone else resolves undefined: non-founding
2891
+ * — a freshly minted-and-persisted key, which seats the identity for a closed row stored
2892
+ * without one (see {@link ensureStrandPartyKey}). Everyone else resolves undefined: non-founding
2621
2893
  * machines never mint (no mint race between a party's machines), and only a founder
2622
2894
  * bootstrap needs the key at all.
2623
2895
  *
@@ -2642,8 +2914,8 @@ export declare class CadreNode implements SAppIdLookup {
2642
2914
  * Counterpart to {@link createOpenInvitation}, which only mints the
2643
2915
  * out-of-band {@link OpenInvitation} envelope: persisting the matching
2644
2916
  * `FormationInvite` row is what makes the token *redeemable* — the host's
2645
- * {@link ControlFormationUsageRecorder} answers `isTokenValid`/`isTokenUsed`
2646
- * from this row. A host minting a closed-strand invite does both (mint +
2917
+ * {@link ControlFormationUsageRecorder} reads it (`readToken`) to check a
2918
+ * redemption. A host minting a closed-strand invite does both (mint +
2647
2919
  * publish), exactly as the integration harness's `createInvitation` does.
2648
2920
  *
2649
2921
  * Signs through the same owner signer as {@link publishStrand}, whose key must be an
@@ -2663,6 +2935,14 @@ export declare class CadreNode implements SAppIdLookup {
2663
2935
  validationUrl?: string;
2664
2936
  strandId?: string;
2665
2937
  }, sign?: OwnerSign): Promise<void>;
2938
+ /**
2939
+ * Register an invitation this node just published with the strand solicitation service. A
2940
+ * host may publish one whose token was minted elsewhere; registering it locally opens the
2941
+ * connection gate's formation exemption immediately rather than waiting for the durable row
2942
+ * to become readable. A token {@link createOpenInvitation} minted here is already registered,
2943
+ * and this records the same expiry again.
2944
+ */
2945
+ private registerPublishedInvitation;
2666
2946
  /**
2667
2947
  * Enroll an approver public key allowed to sign off on `ValidationUrl` redemptions.
2668
2948
  *
@@ -2744,7 +3024,7 @@ export declare class CadreNode implements SAppIdLookup {
2744
3024
  * fan-out, so a rediscovery costs nothing beyond the map lookup plus the (pure)
2745
3025
  * founder derivation. NOT a silent no-op any more when the launch resolves as a
2746
3026
  * FOUNDER: the tracked instance may have been launched first as a joiner (an app
2747
- * attach, or this node's own watcher poll winning the `resolveCohortSeed` window
3027
+ * attach, or this node's own watcher run winning the `resolveCohortSeed` window
2748
3028
  * below), which used to drop the founder request and leave the strand headerless —
2749
3029
  * now the tracked instance is founded in place
2750
3030
  * ({@link StrandInstanceManager.foundExistingStrand}), waking it first if quiesced
@@ -2756,9 +3036,9 @@ export declare class CadreNode implements SAppIdLookup {
2756
3036
  *
2757
3037
  * An absent `sAppConfig` launches a storage replica ({@link CadreNodeConfig.hostUnclaimedStrands}),
2758
3038
  * which is ALWAYS a joiner whatever the row says: the founder bootstrap writes the sApp
2759
- * into `Strand.Header`, and a replica has none. So a self-founded row whose watcher poll
3039
+ * into `Strand.Header`, and a replica has none. So a self-founded row whose watcher run
2760
3040
  * wins the race against its app's `addStrand` after a restart comes up as a replica — and
2761
- * so does a `foundStrand` whose publish a watcher poll saw before its attach. A claim
3041
+ * so does a `foundStrand` whose publish a watcher run saw before its attach. A claim
2762
3042
  * that finds a tracked replica upgrades it in place, over the same node and store
2763
3043
  * ({@link StrandInstanceManager.attachSApp}), BEFORE any founding: the founder bootstrap
2764
3044
  * needs the sApp the attach supplies.
@@ -2887,7 +3167,7 @@ export declare class CadreNode implements SAppIdLookup {
2887
3167
  *
2888
3168
  * Timestamps are recorded OPTIMISTICALLY, whatever `collectStrandAddrs` reports
2889
3169
  * per peer: recording only on success would re-announce to an unreachable relay
2890
- * on every reconcile tick, one dial timeout each. A failed INITIAL announce costs
3170
+ * on every reconcile pass, one dial timeout each. A failed INITIAL announce costs
2891
3171
  * the strand supervisor its first attempt only (the relay denies the
2892
3172
  * reservation; every re-drive re-announces first through
2893
3173
  * {@link announceDelegateToRelay}); a failed REFRESH retries within
@@ -2961,9 +3241,11 @@ export declare class CadreNode implements SAppIdLookup {
2961
3241
  *
2962
3242
  * Due times are per (sibling, strand) and set from each sibling's own outcome, so
2963
3243
  * a sibling that connects late — a phone joining after the party's always-on
2964
- * machines — is asked on the next tick, and one that could not answer or refused
2965
- * (its view of the membership may not include us yet) is retried within
2966
- * {@link STRAND_PEER_ADDR_RETRY_MS} rather than {@link STRAND_PEER_ADDR_REFRESH_MS}.
3244
+ * machines — is asked on the pass its connection starts, and one that could not
3245
+ * answer or refused (its view of the membership may not include us yet) is retried
3246
+ * within {@link STRAND_PEER_ADDR_RETRY_MS} rather than
3247
+ * {@link STRAND_PEER_ADDR_REFRESH_MS}; the pass that stamped the retry arms a wake
3248
+ * for it ({@link armReconcileWake}).
2967
3249
  *
2968
3250
  * Distinct from {@link refreshDelegateGrants}, deliberately: that pass covers
2969
3251
  * RELAYS on a `DELEGATE_GRANT_TTL_MS / 2` throttle to keep circuit-relay
@@ -2983,15 +3265,17 @@ export declare class CadreNode implements SAppIdLookup {
2983
3265
  *
2984
3266
  * `connectedSiblingTargets`' membership read is unbounded, and with zero
2985
3267
  * connections its answer is empty whatever the table holds, so decide it from the
2986
- * connection list instead of paying for the read once a tick.
3268
+ * connection list instead of paying for the read once a pass.
2987
3269
  *
2988
- * NOTE: otherwise the read runs on EVERY tick with a running strand, not only when
3270
+ * NOTE: otherwise the read runs on EVERY pass with a running strand, not only when
2989
3271
  * some (sibling, strand) is due: pruning a departed sibling needs the current
2990
3272
  * target set, and whether anyone is due cannot be told without it (a connected
2991
- * non-member never gets a due time to compare). One more `CadrePeer` read per 15 s
2992
- * tick, on top of the two `runReconcileControlCohort` already makes; if those reads
2993
- * get costly, share one row-set across the pass (see the NOTE there) rather than
2994
- * skipping ticks here.
3273
+ * non-member never gets a due time to compare). One more `CadrePeer` read per
3274
+ * pass, on top of the two `runReconcileControlCohort` already makes, all served
3275
+ * from the membership rows view: an in-memory scan while the view is current,
3276
+ * storage only after the table changed or the view's maximum age passed. Sharing
3277
+ * one row set across the pass would save no storage read and was declined (see
3278
+ * the NOTE there).
2995
3279
  *
2996
3280
  * NOTE: one strand-addr RPC per (running strand × due sibling) — each a tiny
2997
3281
  * request/response on an already-open control connection. If a node ever runs
@@ -3036,8 +3320,8 @@ export declare class CadreNode implements SAppIdLookup {
3036
3320
  *
3037
3321
  * NOTE: every pass serialises the whole FRET table (`exportTable()`), verifies one
3038
3322
  * signature per record and makes one `peerStore.merge` per peer, per running strand
3039
- * per 15 s tick. Fine at strand table sizes, and an unchanged address set writes
3040
- * nothing in libp2p's persistent peer store. If many strands or large tables make it
3323
+ * per pass — every change-driven pass and the 2-minute backstop. Fine at strand table
3324
+ * sizes, and an unchanged address set writes nothing in libp2p's persistent peer store. If many strands or large tables make it
3041
3325
  * show up, read only the entries whose address stamp is near the one-hour mark.
3042
3326
  *
3043
3327
  * @returns the ids of the peers the table holds a usable record for.
@@ -3194,8 +3478,8 @@ export declare class CadreNode implements SAppIdLookup {
3194
3478
  * The stop + emit are skipped when the strand manager holds no instance for `strandId` —
3195
3479
  * e.g. a party owner that published a strand's row but never ran it locally, or an
3196
3480
  * explicit {@link stopStrand} for an id this node never started. `hibernationManager`
3197
- * untrack and the `sAppConfigs` / {@link strandLaunchRefusals} / {@link discoveredStrands}
3198
- * deletes stay unconditional (all no-ops when there is nothing to remove), so a launch
3481
+ * untrack and the `sAppConfigs` / {@link strandLaunchRefusals} / {@link discoveredStrands} /
3482
+ * {@link formedPartyKeys} deletes stay unconditional (all no-ops when there is nothing to remove), so a launch
3199
3483
  * that failed before an instance was ever tracked still gets its stray entries cleared.
3200
3484
  *
3201
3485
  * Dropping the {@link discoveredStrands} entry here covers both callers, and both
@@ -3386,18 +3670,26 @@ export declare class CadreNode implements SAppIdLookup {
3386
3670
  */
3387
3671
  getControlDatabase(): ControlDatabase | null;
3388
3672
  /**
3389
- * Open control-network connections right now. A lower-bound proxy for replication
3390
- * reach: 0 connections ⇒ a control write commits local-only. The private
3391
- * `committedAlone` write-while-alone seam is defined in terms of this.
3392
- *
3393
- * It approximates the precise signal (the block's cluster size), and a caller that
3394
- * samples it AFTER a write samples a slightly wider window than `committedAlone`
3395
- * does inside the write itself. It exists so an embedder can warn an owner before a
3396
- * control write that this machine currently sees none of its siblings, and report
3397
- * after one that the write may not have travelled — a warning shown unconditionally
3398
- * is a warning people learn to ignore.
3673
+ * Open control-network connections right now, of every kind: siblings, relays, bootstrap
3674
+ * nodes and peers still identifying. A report of connectivity, not of replication reach: a
3675
+ * control write can commit local-only with connections open, and whether one would is
3676
+ * {@link hasReachableControlSibling}.
3399
3677
  */
3400
3678
  getControlConnectionCount(): number;
3679
+ /**
3680
+ * Whether a control write made now reaches another machine: a connected peer has identified
3681
+ * as serving the control database and is one this node sends control writes to (an
3682
+ * authorized member, or a device whose admission it is writing). Optimystic counts a peer
3683
+ * into a control block's cohort on those same terms; with none, a write commits on this node
3684
+ * alone and is queued for re-issue once one is reachable (docs/architecture.md → "Writes made
3685
+ * while alone").
3686
+ *
3687
+ * It exists so an embedder can warn an owner before a control write that this machine
3688
+ * currently reaches none of its siblings, and report after one that the write may not have
3689
+ * travelled; a warning shown unconditionally is a warning people learn to ignore. A caller
3690
+ * that samples it AFTER a write samples a slightly wider window than the write did.
3691
+ */
3692
+ hasReachableControlSibling(): boolean;
3401
3693
  /**
3402
3694
  * Force a poll of the strand watcher (for testing)
3403
3695
  */
@@ -3593,7 +3885,7 @@ export declare class CadreNode implements SAppIdLookup {
3593
3885
  * builds its snapshot against that anchor, which is also what its previous snapshot was
3594
3886
  * judged against. Caught here rather than failing the refresh, because the membership read
3595
3887
  * tolerates an unreachable `Revocation` block on an isolated node
3596
- * (`ControlDatabase.readRevokedStampRows`) and `queryRevocations` does not: failing the
3888
+ * (`ControlDatabase.readRevocationRowsOrNone`) and `queryRevocations` does not: failing the
3597
3889
  * whole refresh on that would freeze such a node's gate snapshot.
3598
3890
  * NOTE: on such a node the sync is skipped at every refresh until the block is held; if an
3599
3891
  * isolated node ever has to derive owner additions while cut off, give `queryRevocations`
@@ -3602,9 +3894,12 @@ export declare class CadreNode implements SAppIdLookup {
3602
3894
  * NOTE: cost per membership refresh is one `OwnerKey` read and one `Revocation` read (the
3603
3895
  * tombstone list, whose `OwnerKey` stamps are handed to `queryOwnerKeyRows` so it does not
3604
3896
  * read the retired set again, and which the hang-up of removed peers shares through
3605
- * `loadTombstones`), plus the owner-grant chain when some owner row is grant-seated (the
3606
- * NOTE in the body). Before the Revocation ledger marker is filed that one read is a cohort
3607
- * consult per refresh, which `control-founding-consult-budget.spec.ts` pins.
3897
+ * `loadTombstones`), plus the owner-grant chain's two reads when some owner row is
3898
+ * grant-seated. All are served from the control database's membership views, so they reach
3899
+ * storage only after their table changed or the view's maximum age passed
3900
+ * (`control-founding-consult-budget.spec.ts` pins the counts). On a party founded before
3901
+ * genesis created every control collection, whose `Revocation` table has never been
3902
+ * written, each `Revocation` load is a cohort consult until the first tombstone.
3608
3903
  *
3609
3904
  * @param retry - forwarded to every read; `false` on the refresh path, for the reason on
3610
3905
  * {@link refreshAuthorizedControlPeers}.
@@ -3636,15 +3931,8 @@ export declare class CadreNode implements SAppIdLookup {
3636
3931
  /**
3637
3932
  * The usage and invitation rows {@link hasAnchoredProof} resolves an invitation-admitted
3638
3933
  * `CadrePeer` row through, keyed for lookup. Both reads honour `retry` for
3639
- * {@link listAuthorizedMembers}' reason.
3640
- *
3641
- * NOTE: once invitations are the only admission route (ticket
3642
- * `cadre-invitations-redeemable-by-any-member`), every cadre with a member not yet
3643
- * re-vouched pays these two whole-table reads and three ed25519 verifies per row on every
3644
- * membership refresh. Both tables are append-only and a verified chain never changes (expiry,
3645
- * use count and withdrawal are not re-checked), so if the gate's admission deadline ever
3646
- * shows this, memoise verified (row stamp → usage stamp) pairs per node and skip the reads
3647
- * when every admitted row is already memoised.
3934
+ * {@link listAuthorizedMembers}' reason. The rows come from the membership views, so they
3935
+ * reach storage only after the table changed or the view's maximum age passed.
3648
3936
  */
3649
3937
  private loadInvitationChain;
3650
3938
  /**
@@ -3725,7 +4013,7 @@ export declare class CadreNode implements SAppIdLookup {
3725
4013
  * write-driven refresh never fires for it — yet applying it can ANCHOR a new
3726
4014
  * owner key, which flips rows already present from unauthorized to authorized.
3727
4015
  * Hence the explicit refresh here; without it a peer the freshly anchored owner
3728
- * vouched for stays denied until the next timed cohort reconcile.
4016
+ * vouched for stays denied until the next membership change or the backstop pass.
3729
4017
  */
3730
4018
  private seedEventCallbacks;
3731
4019
  /**
@@ -3744,13 +4032,14 @@ export declare class CadreNode implements SAppIdLookup {
3744
4032
  * NO `CadrePeer` writer needs to call this: the control database notifies it
3745
4033
  * after every committed member-row write (`ControlDatabase.mutateCadrePeer`,
3746
4034
  * wired in {@link start}), which is exactly what makes the refresh automatic
3747
- * rather than a caller obligation. It stays public for the changes that write
3748
- * NO row locally and so raise no notification:
3749
- *
3750
- * - a membership row that arrived by REPLICATION (otherwise picked up on the
3751
- * next timed cohort reconcile — bounded staleness by design), and
3752
- * - a newly anchored trusted owner key ({@link applySeed}), which flips rows
3753
- * ALREADY present from unauthorized to authorized without touching them.
4035
+ * rather than a caller obligation. A membership row that arrived by REPLICATION
4036
+ * refreshes it too, through the gate watch {@link start} takes on the membership
4037
+ * tables (within its 50 ms window; a change this node's storage never hears of
4038
+ * waits for the reconcile pass's backstop). It stays public for the one change
4039
+ * that writes no row and raises no table event: a newly anchored trusted owner
4040
+ * key ({@link applySeed}), which flips rows ALREADY present from unauthorized to
4041
+ * authorized without touching them. A caller may also await it to observe a
4042
+ * refresh that began after its own change.
3754
4043
  *
3755
4044
  * Coalescing: marks the snapshot stale and resolves once a refresh that began
3756
4045
  * after this call has completed, so a caller that awaits it always observes
@@ -3763,13 +4052,10 @@ export declare class CadreNode implements SAppIdLookup {
3763
4052
  * connected peer it newly covers — the strand-side counterpart to
3764
4053
  * {@link refreshMembershipGate}.
3765
4054
  *
3766
- * Unlike that one, NOTHING calls this automatically: strand membership is
3767
- * written through the strand's own `Database` handle
3768
- * (`strand-membership-writer.ts`), which raises no notification this runtime
3769
- * can hook, and a revocation that arrives by REPLICATION raises none either.
3770
- * The gate therefore polls (default 30 s). An app that has just called
3771
- * `revokeMember` or `leaveStrand` should follow the write with this call so
3772
- * the cut is immediate rather than up to one poll interval late.
4055
+ * The gate already refreshes on its own when the strand's `Member` or
4056
+ * `MemberPeer` table changes: at once for a `revokeMember` or `leaveStrand`
4057
+ * written on this machine, and when a replicated removal reaches this node's
4058
+ * storage. Call this to await the cut — it resolves after the sweep.
3773
4059
  *
3774
4060
  * Quiet no-op for a strand that is not running, is quiesced, is open, or has
3775
4061
  * the gate disabled. Never rejects; resolves once the sweep has finished.
@@ -3799,8 +4085,8 @@ export declare class CadreNode implements SAppIdLookup {
3799
4085
  * CONCURRENT external write's refresh for the life of the scope: that writer's
3800
4086
  * promise resolves before its peer is admitted, and admission lands at scope
3801
4087
  * exit instead. Acceptable — scope exit is milliseconds away (bounded by the
3802
- * drain), versus the ~15 s reconcile interval that was the alternative before
3803
- * any of this existed — but it is the reason these scopes stay short and rare.
4088
+ * drain), versus the reconcile pass that was the alternative before any of this
4089
+ * existed — but it is the reason these scopes stay short and rare.
3804
4090
  */
3805
4091
  private deferMembershipGateRefresh;
3806
4092
  /**
@@ -3892,7 +4178,7 @@ export declare class CadreNode implements SAppIdLookup {
3892
4178
  * `'self'` or `'alone'`, before any write.
3893
4179
  */
3894
4180
  removeOwner(ownerPublicKeyB64: string, options?: RemoveOwnerOptions): Promise<OwnerKeyRemovalResult>;
3895
- /** {@link planOwnerKeyRemoval} with the rows it was judged from, which {@link removeOwner} re-vouches dependents by. */
4181
+ /** {@link planOwnerKeyRemoval} over `controlDatabase`, for a caller that already holds it. */
3896
4182
  private planOwnerKeyRemovalWith;
3897
4183
  /**
3898
4184
  * The re-vouch step of {@link removeOwner}: every chosen dependent row is rebound to this
@@ -3901,12 +4187,14 @@ export declare class CadreNode implements SAppIdLookup {
3901
4187
  * the only rebinding `OwnerKey.NoUpdate` allows); peers through the owner re-touch
3902
4188
  * (`reauthorizePeer`), which rewrites `VouchOwner`/`VouchSig` and clears `VouchUsage`.
3903
4189
  *
3904
- * The peer re-touch keeps the row's `UpdatedAt` where it is. The owner branch of
3905
- * `CadrePeer.AuthorizedUpdate` does not require a bump, and bumping a self-published row
3906
- * would leave its `Sig` unverifiable, so unresolvable and unlisted as self-published, until
3907
- * the machine's next heartbeat (the NOTE on that constraint); the write-while-alone drain
3908
- * bumps because it re-issues an otherwise unchanged row, whereas here the voucher itself
3909
- * changes. A row never self-published carries no stamp to keep and gets the clock.
4190
+ * The peer re-touch is the re-vouch mode of `reauthorizePeer`, which leaves the row's
4191
+ * `UpdatedAt` untouched and writes whether or not the machine has self-published. The owner
4192
+ * branch of `CadrePeer.AuthorizedUpdate` does not require a bump, and bumping a self-published
4193
+ * row would leave its `Sig` unverifiable, so unresolvable and unlisted as self-published, until
4194
+ * the machine's next heartbeat (the NOTE on that constraint); the write-while-alone drain's
4195
+ * re-broadcast mode bumps because it re-issues an otherwise unchanged row, whereas here the
4196
+ * voucher itself changes. A dependent whose row vanished since the plan is reported absent and
4197
+ * skipped.
3910
4198
  *
3911
4199
  * A failure rethrows and stops the call before the removal; re-vouches already made stay
3912
4200
  * (the row is only re-signed by a current owner) and the caller retries. An owner re-vouch
@@ -4022,12 +4310,13 @@ export declare class CadreNode implements SAppIdLookup {
4022
4310
  * that connection from the addresses it was handed, on this launch or a later
4023
4311
  * one.
4024
4312
  *
4025
- * Nothing is dialed here: the drone has not received the seed yet. After
4026
- * delivering it, call {@link reconcileControlCohort} to dial straight away;
4027
- * otherwise the next timed reconcile pass does. A pass already in flight is
4028
- * joined rather than restarted, and one that listed siblings before this add
4029
- * does not dial the drone, so a caller waiting for the connection should allow
4030
- * for one more timed pass.
4313
+ * Nothing is dialed here: the drone has not received the seed yet. Its `CadrePeer`
4314
+ * insert is a membership change, so a reconcile pass dials the retained addresses
4315
+ * within about 50 ms of this call returning, whether or not the caller runs one, and
4316
+ * a failed dial is retried on the redial ladder (15 s, 30 s, 60 s, then every
4317
+ * backstop). A caller that wants to await the connection calls
4318
+ * {@link reconcileControlCohort} itself, twice in a row: a call made while a pass is
4319
+ * in flight joins it, and that pass may have listed siblings before this add.
4031
4320
  */
4032
4321
  addDrone(options: AddDroneOptions): Promise<DroneInitResult>;
4033
4322
  /**
@@ -4060,12 +4349,19 @@ export declare class CadreNode implements SAppIdLookup {
4060
4349
  * by the node (its anchored branch), the row insert is a no-op on an existing row
4061
4350
  * (`ControlDatabase.insertCadrePeer`), and the dial target is replaced, not duplicated.
4062
4351
  *
4063
- * The first dial is kicked (not awaited) so it happens now rather than on the next
4064
- * timed reconcile pass; a caller that wants to wait for the connection awaits
4065
- * {@link reconcileControlCohort} itself, with `addDrone`'s caveat that a pass already
4066
- * in flight is joined rather than restarted. The claimed node dials this one back too,
4067
- * as the seed's minter, when this node has a listen or relay address; a phone
4068
- * with neither is reached by its own reconcile pass alone.
4352
+ * Once it has acknowledged, the node restarts into this party (cadre-cli's
4353
+ * `restartIntoClaimedParty`), closing the connection the claim used. The dial target puts
4354
+ * the address that connection was dialed on first, so every later dial tries the address
4355
+ * that just worked before the others (a QR code lists public addresses before LAN ones, and
4356
+ * a home router often drops a connection to its own public address from inside). A
4357
+ * reconcile pass is kicked (not awaited) now, and the vouch's `CadrePeer` insert starts
4358
+ * another about 50 ms later; for the first {@link CONTROL_COHORT_REDIAL_MS} after the
4359
+ * acknowledgement this node also retries the node every second while it restarts
4360
+ * ({@link redialClaimedNode}). A caller that wants to wait for the connection awaits
4361
+ * {@link reconcileControlCohort} itself, with `addDrone`'s caveat that a pass already in
4362
+ * flight is joined rather than restarted. The claimed node dials this one back too, as the
4363
+ * seed's minter, when this node has a listen or relay address; a phone with neither is
4364
+ * reached by its own dials alone.
4069
4365
  *
4070
4366
  * @throws {ClaimRefusedError} when the node was reached and refused the claim.
4071
4367
  * @throws {PeerUnreachableError} when the node was never reached.
@@ -4075,6 +4371,33 @@ export declare class CadreNode implements SAppIdLookup {
4075
4371
  multiaddrs: string[];
4076
4372
  secret: string;
4077
4373
  }): Promise<void>;
4374
+ /**
4375
+ * `addrs` with the address this node's open connection to `peerId` was dialed on moved to
4376
+ * the front, when it is one of them; `addrs` unchanged otherwise. Compared without a
4377
+ * trailing `/p2p/<peerId>`, which a QR code's address may or may not carry ({@link addrKey}).
4378
+ */
4379
+ private connectedAddrFirst;
4380
+ /**
4381
+ * Retry a node this node has just claimed ({@link claimNode}) once every
4382
+ * {@link CLAIM_RESTART_REDIAL_STEP_MS} for {@link CONTROL_COHORT_REDIAL_MS} after it accepted.
4383
+ * The node restarts into this party right after it acknowledges, and while it does, a
4384
+ * reconcile pass's dial either fails or opens a connection the restarting node closes at
4385
+ * once. The first failure puts the pass's next redial 15 s out ({@link armReconcileWake}),
4386
+ * and an owner that cannot be dialed has no other way back to the node.
4387
+ *
4388
+ * Each step dials once from the retained dial target, unless a connection to the node is
4389
+ * open: the one the claim used until the restart closes it, or one to the restarted node
4390
+ * that has not identified yet. A dial that connects does not end the loop, because the
4391
+ * restarting node may still close it. The loop ends once the node has identified as serving
4392
+ * this party's control database ({@link identifiedControlPeers}, which the placeholder node
4393
+ * it ran before the claim never joins), at the window's end, when this node stops, or when
4394
+ * the target is forgotten ({@link removePeer}). Steps run one after another, so a dial
4395
+ * finishes on its own budget first. The redial streak is left alone: the ladder carries on
4396
+ * after the window. One loop per peer: a repeated claim while one runs starts none.
4397
+ */
4398
+ private redialClaimedNode;
4399
+ /** One step of {@link redialClaimedNode}; false ends the loop. */
4400
+ private redialClaimedNodeStep;
4078
4401
  /**
4079
4402
  * Add a phone to the cadre with relay support.
4080
4403
  * Use when both nodes are NAT'd (phone-to-phone).
@@ -4162,11 +4485,13 @@ export declare class CadreNode implements SAppIdLookup {
4162
4485
  * Steps: refuse an invitation for another party; pin its `ownerKeys` into the node-local
4163
4486
  * anchor (`'invite'` provenance — the pin sticks even if the redemption then fails, as it
4164
4487
  * does for a seed invite, so a later attempt or seed from the same owner is anchored);
4165
- * sign the request; dial the listed members in order (`redeemAtMembers`); on
4166
- * acceptance verify the reply against the pinned keys, merge its dial hints into the peer
4167
- * store, retain the answering member as a cold-start dial target, refresh the membership
4168
- * gate and start a cohort reconcile pass. This node then syncs the control database over
4169
- * the connection it already holds, the same unified behaviour as after a seed.
4488
+ * sign the request, which carries this node's own signed address record; dial the listed
4489
+ * members in order (`redeemAtMembers`); on acceptance verify the reply against the pinned
4490
+ * keys, merge its dial hints into the peer store, retain the answering member as a
4491
+ * cold-start dial target and start a cohort reconcile pass. This node then syncs the control
4492
+ * database over the connection it already holds, the same unified behaviour as after a seed.
4493
+ * The member writes the record on this node's `CadrePeer` row when its signature verifies, so
4494
+ * the row resolves to this node's addresses from its admission.
4170
4495
  *
4171
4496
  * A device that is already a member (a restart with the invitation still configured) is
4172
4497
  * answered as accepted by the member's idempotent redemption, with no second usage row.
@@ -4304,9 +4629,11 @@ export declare class CadreNode implements SAppIdLookup {
4304
4629
  */
4305
4630
  redeemOwnerGrant(bundle: OwnerGrantBundle, signer: OwnerSigner, options?: RedeemOwnerGrantOptions): Promise<RedeemOwnerGrantResult>;
4306
4631
  /**
4307
- * {@link redeemOwnerGrant}'s local path: the alone refusal, the seat, the liveness refusal,
4308
- * then one approval around the consent signature and the write. Every refusal comes before
4309
- * the approval, so a prompting signer never asks for a redemption this node would refuse.
4632
+ * {@link redeemOwnerGrant}'s local path: the alone refusal, the seat (only when this node holds
4633
+ * no grant row), the liveness refusal, then one approval around the consent signature and the
4634
+ * write. Every refusal comes before the approval, so a prompting signer never asks for a
4635
+ * redemption this node would refuse. The held row is read once and carried into the liveness
4636
+ * check and the write, as the member handler carries it (`MemberRedemptionHandler`).
4310
4637
  */
4311
4638
  private redeemOwnerGrantLocally;
4312
4639
  /**
@@ -4553,6 +4880,10 @@ export declare class CadreNode implements SAppIdLookup {
4553
4880
  * implements): mint a single-use `Strand.Invite` against the LIVE host strand so a
4554
4881
  * validated joiner can seat its own `Strand.Member` row.
4555
4882
  *
4883
+ * `host` is the strand as the formation session resolved it from its `Strand` row, read
4884
+ * once there; this method reads no `Strand` row of its own. A row removed in between is
4885
+ * refused when the consent row is written (the `StrandExists` CHECK).
4886
+ *
4556
4887
  * - Open host strand → `null` (no members, nothing to invite into).
4557
4888
  * - Closed host strand whose founder launch was refused as pre-split
4558
4889
  * ({@link strandLaunchRefusals}) → rethrow the recorded `PreSplitStrandIdentityError`;