@serfab/cadre-core 1.8.0 → 1.9.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 (59) hide show
  1. package/dist/cadre-node.d.ts +153 -104
  2. package/dist/cadre-node.js +227 -199
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/index.d.ts +2 -4
  5. package/dist/index.js +10 -11
  6. package/dist/index.js.map +1 -1
  7. package/dist/node-local-snapshot.d.ts +6 -4
  8. package/dist/node-local-snapshot.js +7 -3
  9. package/dist/node-local-snapshot.js.map +1 -1
  10. package/dist/peer-join-backfill.d.ts +0 -9
  11. package/dist/peer-join-backfill.js +2 -3
  12. package/dist/peer-join-backfill.js.map +1 -1
  13. package/dist/strand-formation-protocol.d.ts +0 -11
  14. package/dist/strand-formation-protocol.js +3 -4
  15. package/dist/strand-formation-protocol.js.map +1 -1
  16. package/dist/strand-fret-addrs.d.ts +54 -0
  17. package/dist/strand-fret-addrs.js +108 -0
  18. package/dist/strand-fret-addrs.js.map +1 -0
  19. package/dist/strand-instance-manager.d.ts +9 -38
  20. package/dist/strand-instance-manager.js +13 -73
  21. package/dist/strand-instance-manager.js.map +1 -1
  22. package/dist/strand-network-state-file.d.ts +12 -0
  23. package/dist/strand-network-state-file.js +38 -0
  24. package/dist/strand-network-state-file.js.map +1 -0
  25. package/dist/strand-network-state.d.ts +111 -0
  26. package/dist/strand-network-state.js +191 -0
  27. package/dist/strand-network-state.js.map +1 -0
  28. package/dist/types.d.ts +20 -23
  29. package/package.json +11 -10
  30. package/src/cadre-node.ts +238 -205
  31. package/src/index.ts +15 -55
  32. package/src/node-local-snapshot.ts +12 -7
  33. package/src/peer-join-backfill.ts +2 -3
  34. package/src/strand-formation-protocol.ts +3 -4
  35. package/src/strand-fret-addrs.ts +145 -0
  36. package/src/strand-instance-manager.ts +20 -96
  37. package/src/strand-network-state-file.ts +40 -0
  38. package/src/strand-network-state.ts +243 -0
  39. package/src/types.ts +20 -23
  40. package/dist/strand-peer-book-file.d.ts +0 -12
  41. package/dist/strand-peer-book-file.js +0 -37
  42. package/dist/strand-peer-book-file.js.map +0 -1
  43. package/dist/strand-peer-book-protocol.d.ts +0 -168
  44. package/dist/strand-peer-book-protocol.js +0 -356
  45. package/dist/strand-peer-book-protocol.js.map +0 -1
  46. package/dist/strand-peer-book-swap.d.ts +0 -104
  47. package/dist/strand-peer-book-swap.js +0 -284
  48. package/dist/strand-peer-book-swap.js.map +0 -1
  49. package/dist/strand-peer-book.d.ts +0 -159
  50. package/dist/strand-peer-book.js +0 -361
  51. package/dist/strand-peer-book.js.map +0 -1
  52. package/dist/strand-peer-observer.d.ts +0 -88
  53. package/dist/strand-peer-observer.js +0 -180
  54. package/dist/strand-peer-observer.js.map +0 -1
  55. package/src/strand-peer-book-file.ts +0 -39
  56. package/src/strand-peer-book-protocol.ts +0 -466
  57. package/src/strand-peer-book-swap.ts +0 -338
  58. package/src/strand-peer-book.ts +0 -475
  59. package/src/strand-peer-observer.ts +0 -235
@@ -4,7 +4,7 @@ import type { CadreNodeConfig, StrandInstance, StrandRow, StrandConfig, FoundStr
4
4
  import { type Ed25519KeyPair } from './ed25519-key.js';
5
5
  import { type TrustedOwnerStore, type TrustSource } from './trusted-owner-store.js';
6
6
  import { type BootstrapPeerStore } from './bootstrap-peer-store.js';
7
- import { type StrandPeerBookStore } from './strand-peer-book.js';
7
+ import { type StrandNetworkStateStore } from './strand-network-state.js';
8
8
  import { type EnrolledMachineStore } from './enrolled-machine-store.js';
9
9
  import { type SAppIdLookup } from './strand-watcher.js';
10
10
  import { type ControlCohortReconcileResult } from './control-cohort.js';
@@ -304,36 +304,54 @@ export declare class CadreNode implements SAppIdLookup {
304
304
  */
305
305
  private readonly strandAddrAskDueAt;
306
306
  /**
307
- * Node-local strand peer book (see `strand-peer-book.ts`): per strand, the strand
308
- * peers this node has met and their last-known strand-network addresses. Three
309
- * writers: {@link recordFormationStrandPeers} files the responder's live strand
310
- * addrs a formation result carried back (`FormationResultMessage.strandAddrs`),
311
- * {@link observeStrandPeer} files every strand peer a running strand node
312
- * identifies, and the per-strand book swap (`strand-peer-book-swap.ts`, handed the
313
- * store as `StartStrandConfig.strandPeerBook`) files the node's own signed entry
314
- * plus every signed entry a strand peer sends it — the only writer whose entries
315
- * carry a signature, and the only one that can refresh a peer's addresses without
316
- * meeting that peer. Read by {@link resolveCohortSeed} (launch + hibernation resume) and
317
- * re-merged by every {@link refreshStrandPeerAddrs} pass, which is what keeps the
318
- * entries alive past the peerStore's one-hour address expiry.
319
- *
320
- * This is the ONLY cross-party discovery input there is. The strand-addr RPC that
307
+ * The strand-network addresses a formation carried back
308
+ * (`FormationResultMessage.strandAddrs`), by strand id: the responder's live strand
309
+ * node addresses, kept so the strand's first attach has something to dial. Written by
310
+ * {@link recordFormationStrandAddrs}, read by {@link resolveCohortSeed} (launch and
311
+ * hibernation resume) and by every refresh pass for the peers the strand node's FRET
312
+ * table holds no record for yet ({@link remergeUnrecordedFormationAddrs}).
313
+ *
314
+ * This is the only cross-party input a FIRST attach has. The strand-addr RPC that
321
315
  * resolves a strand's addresses is membership-gated and answers own-party siblings
322
- * only, so without the book a joiner's cohort seed for a two-party strand is empty
323
- * and the mesh never forms — and, with an in-memory book, never RE-forms after a
324
- * restart (gotchoices/sereus#18). A STORE rather than a map so an embedder can make
325
- * it outlive the process; constructed (or adopted from `config.strandPeers.store`)
326
- * by {@link initializeStrandPeerBookStore} during {@link start}, deliberately NOT
316
+ * only, so without it a joiner's seed for a two-party strand is empty and the mesh
317
+ * never forms. Every later launch has the strand node's saved network state as well
318
+ * ({@link strandNetworkStateStore}), which is what a restart re-finds the other party
319
+ * from (gotchoices/sereus#18).
320
+ *
321
+ * A re-formation replaces the strand's list. An entry survives a {@link stopStrand}
322
+ * and a stop()→start() of this node instance ({@link cleanup} leaves the map alone),
323
+ * and is dropped where the saved network state is: {@link unpublishStrand},
324
+ * {@link forgetJoinedStrand} and self-revocation.
325
+ *
326
+ * NOTE: accepted tradeoff — in memory only, so a process restart between `formStrand`
327
+ * and the strand's first launch loses the carried addresses, and the strand then
328
+ * launches with no cross-party seed until it is re-formed. Persisting the list on the
329
+ * joined-strand record was weighed and declined: it mixes address state into the join
330
+ * record, and needs an "only until the first run" rule to stop dead addresses being
331
+ * dialled on every launch. Every embedder launches straight after forming, and once
332
+ * the strand has run the saved network state covers every later restart. Revisit if an
333
+ * embedder forms and launches in separate sessions.
334
+ */
335
+ private readonly formationStrandAddrs;
336
+ /**
337
+ * Node-local strand network state (see `strand-network-state.ts`): per strand, the
338
+ * state db-p2p saves for the strand's libp2p node — its FRET routing table with each
339
+ * peer's signed address record. Handed to every strand launch as
340
+ * `StartStrandConfig.networkState`; the strand node saves into it on every connection
341
+ * and re-imports it when it is next built, which is what gives a restarted strand
342
+ * node addresses for the peers it was talking to.
343
+ *
344
+ * Constructed (or adopted from `config.strandNetworkState.store`) by
345
+ * {@link initializeStrandNetworkStateStore} during {@link start}, deliberately NOT
327
346
  * cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same node
328
347
  * instance — same lifecycle as {@link bootstrapPeerStore}.
329
348
  *
330
- * Entries survive a {@link stopStrand} (a stopped strand may be claimed again with
331
- * {@link addStrand}, and is rediscovered on restart) and are forgotten by
332
- * {@link unpublishStrand}, {@link forgetJoinedStrand} and self-revocation; the store
333
- * ages the rest out after 14 days and caps each strand at 16 peers, so the book is
334
- * bounded by time and by the strands this node runs rather than by its history.
349
+ * A strand's state survives a {@link stopStrand} and a hibernation quiesce, and is
350
+ * forgotten by {@link unpublishStrand}, {@link forgetJoinedStrand} and
351
+ * self-revocation. A strand detached because the watcher saw its row gone keeps its
352
+ * state (`NOTE:` on `PersistentStrandNetworkStateStore`).
335
353
  */
336
- private strandPeerBookStore;
354
+ private strandNetworkStateStore;
337
355
  /**
338
356
  * PENDING strand membership invitations learned at formation, keyed by strandId — the
339
357
  * single-use `Strand.Invite` credential a closed-strand formation result carried back
@@ -361,7 +379,7 @@ export declare class CadreNode implements SAppIdLookup {
361
379
  * NOTE: entries live for the node's lifetime (one small pair per formed closed
362
380
  * strand); the number of keys is bounded only by the strands this node has ever
363
381
  * formed, not by time — if a node ever forms strands at scale, evict on
364
- * `unpublishStrand` / `forgetJoinedStrand` as the strand peer book does.
382
+ * `unpublishStrand` / `forgetJoinedStrand` as {@link formationStrandAddrs} is.
365
383
  */
366
384
  private readonly pendingMembershipInvites;
367
385
  /**
@@ -767,15 +785,15 @@ export declare class CadreNode implements SAppIdLookup {
767
785
  */
768
786
  private initializeJoinedStrandStore;
769
787
  /**
770
- * Construct (or adopt) the node-local strand peer book (see
771
- * {@link strandPeerBookStore}). Synchronous for the reason
788
+ * Construct (or adopt) the node-local strand network state (see
789
+ * {@link strandNetworkStateStore}). Synchronous for the reason
772
790
  * {@link initializeBootstrapPeerStore} is: an injected store has already loaded its
773
791
  * persisted entries by the time it is handed in, and the in-memory fallback has
774
792
  * nothing to load. The instance is kept across stop()→start(). A store scoped to a
775
- * different party is a configuration error, fail closed: another party's strand
776
- * peers must never enter this node's dial loop.
793
+ * different party is a configuration error, fail closed: another party's routing
794
+ * tables must never be imported into this node's strands.
777
795
  */
778
- private initializeStrandPeerBookStore;
796
+ private initializeStrandNetworkStateStore;
779
797
  /**
780
798
  * The node-local enrolled-machine store (null before {@link start}) — what this
781
799
  * node last knew about its party's size. Exposed for diagnostics and for a host
@@ -790,12 +808,12 @@ export declare class CadreNode implements SAppIdLookup {
790
808
  */
791
809
  getBootstrapPeerStore(): BootstrapPeerStore | null;
792
810
  /**
793
- * The node-local strand peer book (null before {@link start}) — per strand, the
794
- * strand peers this node dials first on launch, resume and refresh. Exposed for
795
- * diagnostics and for a host that wants to show "whom would this strand re-find
796
- * after a restart?".
811
+ * The node-local strand network state (null before {@link start}) — per strand, the
812
+ * state its strand node last saved and will re-import when next built. Exposed for
813
+ * diagnostics and for a host that wants to show "what routing table would this
814
+ * strand restart with?".
797
815
  */
798
- getStrandPeerBookStore(): StrandPeerBookStore | null;
816
+ getStrandNetworkStateStore(): StrandNetworkStateStore | null;
799
817
  /**
800
818
  * The node-local trusted-owner anchor (null before {@link start}). This is
801
819
  * the set the authorized-membership predicate and seed-trust anchor consult —
@@ -2479,13 +2497,6 @@ export declare class CadreNode implements SAppIdLookup {
2479
2497
  * for a relay we are not yet connected to.
2480
2498
  */
2481
2499
  private resolveCohortSeed;
2482
- /**
2483
- * The strand peer book's contribution to a strand's seed: every live entry's
2484
- * addresses, freshest peer first, minus this node's own entry when `selfPeerId` is
2485
- * known (the book swap files one under the strand transport id; a node must never
2486
- * dial itself). Empty before {@link start} builds the store.
2487
- */
2488
- private strandPeerBookAddrs;
2489
2500
  /**
2490
2501
  * The own-party half of {@link resolveCohortSeed}: strand-network addresses resolved
2491
2502
  * from CONNECTED cohort siblings over the control-mesh strand-addr RPC, doubling as
@@ -2615,18 +2626,19 @@ export declare class CadreNode implements SAppIdLookup {
2615
2626
  /** One strand's share of {@link refreshDelegateGrants}: announce to the relays whose grant is due. */
2616
2627
  private announceDelegateToDueRelays;
2617
2628
  /**
2618
- * Keep each running strand's own libp2p address book warm: re-merge its strand
2619
- * peer book on every pass, and re-ask each connected SIBLING for its strand
2620
- * addresses over the control mesh when that (sibling, strand) is due
2629
+ * Keep each running strand's own libp2p address book warm: re-merge the address
2630
+ * records its FRET routing table holds on every pass
2631
+ * ({@link remergeStrandFretRecords}), and re-ask each connected SIBLING for its
2632
+ * strand addresses over the control mesh when that (sibling, strand) is due
2621
2633
  * ({@link strandAddrAskDueAt}).
2622
2634
  *
2623
- * Without this, a strand's address book is written exactly once — the
2635
+ * Without this, cadre-core writes a strand's address book exactly once — the
2624
2636
  * launch/resume seed — and everything below cadre-core that dials a strand peer
2625
2637
  * by bare peer id (Optimystic's cluster and repo clients, FRET ping/announce)
2626
- * loses its address for any sibling it is not currently connected to: a sibling
2638
+ * loses its address for any peer it is not currently connected to: a sibling
2627
2639
  * that restarted its strand node or rotated its relay reservation is never
2628
- * re-resolved, and even the original seed addresses fall off at the peerStore's
2629
- * one-hour expiry (see `peer-addr-book.ts`).
2640
+ * re-resolved, and every address, another party's included, falls off at the
2641
+ * peerStore's one-hour expiry (see `peer-addr-book.ts`).
2630
2642
  *
2631
2643
  * Due times are per (sibling, strand) and set from each sibling's own outcome, so
2632
2644
  * a sibling that connects late — a phone joining after the party's always-on
@@ -2670,17 +2682,59 @@ export declare class CadreNode implements SAppIdLookup {
2670
2682
  private strandAddrRefreshTargets;
2671
2683
  /**
2672
2684
  * One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings that are
2673
- * due, union their answers with this strand's peer-book addresses, and merge the
2674
- * lot into the strand's address book. Errors are logged and swallowed so one
2675
- * strand's failure never costs the others their refresh.
2685
+ * due and merge their answers into the strand's address book, then re-merge the
2686
+ * address records the strand node's FRET table holds. Errors are logged and
2687
+ * swallowed so one strand's failure never costs the others their refresh.
2676
2688
  *
2677
- * The peer book merges on EVERY pass, whether or not any sibling is due or
2678
- * connected: nothing re-resolves another party's addresses, so this re-merge is the
2679
- * ONLY thing standing between them and the peerStore's one-hour expiry — and it is
2680
- * also how a re-formation's freshly carried addresses reach a running strand on the
2681
- * next tick. A strand with nobody due and no book entry does nothing.
2689
+ * The sibling half runs first, so a failure reading the FRET table cannot cost a
2690
+ * sibling answer its merge. The FRET half, and the formation-carried addresses of
2691
+ * the peers it holds no record for, run on EVERY pass, whether or not any sibling is
2692
+ * due or connected — see {@link remergeStrandFretRecords} and
2693
+ * {@link remergeUnrecordedFormationAddrs} for why.
2682
2694
  */
2683
2695
  private refreshOneStrandPeerAddrs;
2696
+ /**
2697
+ * Is `strandNode` still the node `strandId` runs on? Asked again after every await
2698
+ * in a refresh pass: a strand stopped mid-pass, or one already restarted onto a new
2699
+ * node, must never have its store written to.
2700
+ */
2701
+ private isRunningStrandNode;
2702
+ /**
2703
+ * Re-merge, into a running strand node's address book, the addresses of every peer
2704
+ * its FRET routing table holds a signed address record for (`strand-fret-addrs.ts`).
2705
+ *
2706
+ * This is what keeps ANOTHER PARTY's strand nodes dialable from a node that stays
2707
+ * up. Nothing re-resolves their addresses — the strand-addr RPC answers own-party
2708
+ * siblings only — and the peerStore hides an address one hour after it was first
2709
+ * observed. FRET keeps each peer's record but hands it to the peerStore only when it
2710
+ * arrives or is imported, so without this pass a connection to another party that
2711
+ * drops after the first hour could not be redialed by either side.
2712
+ *
2713
+ * A record is held while FRET holds the peer's table entry, and at most 14 days after
2714
+ * it was last confirmed, so that is also how long a wrong address can keep being
2715
+ * re-merged. Each record is signed by the peer it names and verified before use, so
2716
+ * the exposure is failed dials to an address that peer itself published.
2717
+ *
2718
+ * NOTE: every pass serialises the whole FRET table (`exportTable()`), verifies one
2719
+ * signature per record and makes one `peerStore.merge` per peer, per running strand
2720
+ * per 15 s tick. Fine at strand table sizes, and an unchanged address set writes
2721
+ * nothing in libp2p's persistent peer store. If many strands or large tables make it
2722
+ * show up, read only the entries whose address stamp is near the one-hour mark.
2723
+ *
2724
+ * @returns the ids of the peers the table holds a usable record for.
2725
+ */
2726
+ private remergeStrandFretRecords;
2727
+ /**
2728
+ * Re-merge the addresses this strand's formation carried ({@link formationStrandAddrs})
2729
+ * for every peer the strand node's FRET table holds no record for (`recorded`).
2730
+ *
2731
+ * Until the first connection to the responder, the carried addresses are the only
2732
+ * thing naming it, and the launch seed writes them once. FRET's `bootstraps` keep only
2733
+ * the peer id, so if the responder stays unreachable past the peerStore's one-hour
2734
+ * expiry, nothing could dial it when it returns. Once FRET holds the peer's own signed
2735
+ * record, that record supersedes what the formation carried and this stops.
2736
+ */
2737
+ private remergeUnrecordedFormationAddrs;
2684
2738
  /**
2685
2739
  * RPC `due` siblings for `strandId` and set each one's next due time from its own
2686
2740
  * outcome: an answer, even an empty one, waits the full refresh interval; anything
@@ -2695,11 +2749,11 @@ export declare class CadreNode implements SAppIdLookup {
2695
2749
  * attributed per peer. Best-effort throughout: an address-book write must never
2696
2750
  * fail a launch, a resume, or a reconcile pass.
2697
2751
  *
2698
- * `addrs` is the peer-agnostic union the strand-addr RPC returns, so
2699
- * {@link groupAddrsByPeerId} attributes each entry to the **strand transport**
2700
- * peerId in its final `/p2p/` component — never the sibling's control peerId,
2701
- * which names a different libp2p node entirely. Entries that name no peer are
2702
- * dropped there and counted here, once per pass.
2752
+ * `addrs` is a peer-agnostic list — the union the strand-addr RPC returns, or the
2753
+ * addresses a formation carried — so {@link groupAddrsByPeerId} attributes each
2754
+ * entry to the **strand transport** peerId in its final `/p2p/` component — never
2755
+ * the sibling's control peerId, which names a different libp2p node entirely.
2756
+ * Entries that name no peer are dropped there and counted here, once per pass.
2703
2757
  *
2704
2758
  * NOTE: a member could answer with arbitrary multiaddrs bound to arbitrary peer
2705
2759
  * ids and poison this address book. That is the same exposure the launch-time
@@ -2709,14 +2763,13 @@ export declare class CadreNode implements SAppIdLookup {
2709
2763
  * peerStore's one-hour expiry. No new gating here — cross-party strand trust is
2710
2764
  * `backlog/strand-network-nat-relay-reachability`.
2711
2765
  *
2712
- * NOTE: one input ages out on a slower clock — the strand peer book's entries
2713
- * ({@link strandPeerBookStore}). Nothing can re-resolve another party's addresses,
2714
- * so {@link refreshStrandPeerAddrs} re-merges the book every pass, which means a
2715
- * junk entry from a responder or a peer's identify survives until the book drops it
2716
- * (14 days unrefreshed, or the peer's next connection replacing the entry) rather
2717
- * than an hour. Bounded to 16 peers × 16 addrs per strand, forgotten on
2718
- * `unpublishStrand` / `forgetJoinedStrand`, and still authority-free — so the
2719
- * exposure is a handful of failed dials, not a trust hole.
2766
+ * NOTE: one input outlives that hour — a formation's carried addresses
2767
+ * ({@link formationStrandAddrs}) are in every launch and resume seed for as long as
2768
+ * this process remembers the formation, and re-merged on every refresh pass until the
2769
+ * strand node's FRET table holds the peer's own record, so a junk address from a
2770
+ * responder that is never met keeps being re-merged. Bounded by `sanitizeStrandAddrs`,
2771
+ * replaced by a re-formation, dropped on `unpublishStrand` / `forgetJoinedStrand`, and
2772
+ * still authority-free — so the exposure is a handful of failed dials, not a trust hole.
2720
2773
  */
2721
2774
  private mergeStrandPeerAddrs;
2722
2775
  /**
@@ -2732,7 +2785,8 @@ export declare class CadreNode implements SAppIdLookup {
2732
2785
  /**
2733
2786
  * Leave a strand joined from another party, for the whole party: remove its party-wide
2734
2787
  * `JoinedStrand` row (owner-signed, with a `Revocation` tombstone), then this machine's
2735
- * unpublished record, its strand peer book entries and any session-kept entry, then
2788
+ * unpublished record, its formation-carried addresses, its saved network state and any
2789
+ * session-kept entry, then
2736
2790
  * {@link stopStrand} it here. Every other machine's watcher then sees the row gone and
2737
2791
  * detaches the strand, a storage replica included; a machine offline at the time reaps the
2738
2792
  * row once the tombstone reaches it. The joiner's counterpart of {@link unpublishStrand},
@@ -2771,8 +2825,9 @@ export declare class CadreNode implements SAppIdLookup {
2771
2825
  * session here — the `strand:revoked` contract is that nothing is torn down for the app —
2772
2826
  * and its party-wide row is queued for removal by the next connected owner reconcile pass
2773
2827
  * (`JoinedStrandSession.forgetAfterThisSession`). A no-op for this party's own strands,
2774
- * which have neither record; the strand peer book is forgotten either way, since a
2775
- * removed party must not keep dialing the strand's peers.
2828
+ * which have neither record; the formation-carried addresses and the saved network
2829
+ * state are forgotten either way, since a removed party must not keep dialing the
2830
+ * strand's peers.
2776
2831
  *
2777
2832
  * NOTE: accepted tradeoff — a sibling machine that has not raised `strand:revoked` itself
2778
2833
  * detaches the strand (`strand:stopped`) when the party-wide removal reaches it, instead
@@ -2790,25 +2845,13 @@ export declare class CadreNode implements SAppIdLookup {
2790
2845
  */
2791
2846
  private forgetRevokedJoin;
2792
2847
  /**
2793
- * Forget a strand's entries in the peer book (see {@link strandPeerBookStore}):
2794
- * fire-and-log, like every book write — the removal is visible synchronously by the
2795
- * store's contract, and a failed persist costs restart survival only.
2848
+ * Forget what this node holds about a strand's peers: the addresses its formation
2849
+ * carried (see {@link formationStrandAddrs}) and its saved network state (see
2850
+ * {@link strandNetworkStateStore}). The store write is fire-and-log — the removal is
2851
+ * visible synchronously by the store's contract, and a failed persist only leaves the
2852
+ * entry on disk for the next start.
2796
2853
  */
2797
2854
  private forgetStrandPeers;
2798
- /**
2799
- * The strand peer book's observation writer (`StartStrandConfig.onStrandPeerIdentified`):
2800
- * a strand node identified a strand peer at `observation.addrs`, so remember it as an
2801
- * unsigned entry seen just now. The observer never reports self, and the store binds
2802
- * every address to the peer id before filing it.
2803
- */
2804
- private observeStrandPeer;
2805
- /**
2806
- * Merge one entry into the peer book. Fire-and-log: the entry is visible
2807
- * synchronously by the store's contract, and the promise tracks durability only. A
2808
- * persist failure costs restart survival, never this session's seed — the same trade
2809
- * {@link retainDialTarget} makes.
2810
- */
2811
- private rememberStrandPeer;
2812
2855
  /**
2813
2856
  * Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
2814
2857
  * stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
@@ -3550,21 +3593,27 @@ export declare class CadreNode implements SAppIdLookup {
3550
3593
  */
3551
3594
  private wakeHostStrandForFormation;
3552
3595
  /**
3553
- * Remember the responder's strand-network addresses for a strand this node just
3554
- * formed in the strand peer book (see {@link strandPeerBookStore}), so the strand's
3555
- * discovery seed has something to work with when the app launches it — and again
3556
- * after every restart.
3596
+ * Keep the responder's strand-network addresses for a strand this node just formed
3597
+ * (see {@link formationStrandAddrs}), so the strand's discovery seed has something to
3598
+ * dial when the app launches it.
3557
3599
  *
3558
3600
  * The carried list is peer-agnostic (`sanitizeStrandAddrs` bounds and parses it,
3559
3601
  * nothing more), so it is attributed per peer here (`groupAddrsByPeerId`, the same
3560
- * rule the address-book merge applies) and each peer becomes one unsigned entry seen
3561
- * just now: the responder disclosed the addresses live a moment ago. An entry naming
3562
- * no destination peer is dropped there. Scoped strictly to `strandId`: these
3563
- * addresses reach ONE strand node of ONE other party and must never seed another
3564
- * strand's mesh or the control peerStore. An empty list records nothing, so a later
3565
- * formation against the same strand that DOES disclose addresses is not shadowed.
3566
- */
3567
- private recordFormationStrandPeers;
3602
+ * rule the address-book merge applies) and kept grouped by peer; an entry naming no
3603
+ * destination peer is dropped there. Scoped strictly to `strandId`: these addresses
3604
+ * reach ONE strand node of ONE other party and must never seed another strand's mesh
3605
+ * or the control peerStore.
3606
+ *
3607
+ * A list that names a peer REPLACES the strand's earlier one: the responder
3608
+ * disclosed its current addresses, so the older list is stale. An empty list, or one
3609
+ * naming no peer, records nothing, so it cannot wipe an earlier disclosure.
3610
+ *
3611
+ * A strand already running when this lands (a re-formation, which is the recovery
3612
+ * path for a dead address) gets the new addresses merged into its address book now.
3613
+ * Nothing else would deliver them: the seed is read only at launch and resume, and the
3614
+ * refresh pass skips a peer FRET already holds a record for — the stale one, here.
3615
+ */
3616
+ private recordFormationStrandAddrs;
3568
3617
  /**
3569
3618
  * Encode an open invitation for out-of-band delivery (QR, link, etc.).
3570
3619
  */