@serfab/cadre-core 1.5.0 → 1.7.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 (68) hide show
  1. package/dist/cadre-node.d.ts +206 -67
  2. package/dist/cadre-node.js +399 -98
  3. package/dist/cadre-node.js.map +1 -1
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.js +17 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/joined-strand-store.d.ts +112 -0
  8. package/dist/joined-strand-store.js +263 -0
  9. package/dist/joined-strand-store.js.map +1 -0
  10. package/dist/key-store.d.ts +6 -0
  11. package/dist/key-store.js.map +1 -1
  12. package/dist/link-budget.d.ts +95 -53
  13. package/dist/link-budget.js +97 -53
  14. package/dist/link-budget.js.map +1 -1
  15. package/dist/node-local-snapshot.d.ts +5 -4
  16. package/dist/node-local-snapshot.js +6 -3
  17. package/dist/node-local-snapshot.js.map +1 -1
  18. package/dist/peer-dial.d.ts +3 -3
  19. package/dist/peer-dial.js +3 -3
  20. package/dist/peer-join-backfill.d.ts +35 -16
  21. package/dist/peer-join-backfill.js +117 -57
  22. package/dist/peer-join-backfill.js.map +1 -1
  23. package/dist/relay-reservation.d.ts +2 -2
  24. package/dist/relay-reservation.js +2 -2
  25. package/dist/seed-bootstrap.js +1 -1
  26. package/dist/strand-first-sync-gate.d.ts +88 -43
  27. package/dist/strand-first-sync-gate.js +92 -46
  28. package/dist/strand-first-sync-gate.js.map +1 -1
  29. package/dist/strand-instance-manager.d.ts +39 -0
  30. package/dist/strand-instance-manager.js +80 -3
  31. package/dist/strand-instance-manager.js.map +1 -1
  32. package/dist/strand-peer-book-file.d.ts +12 -0
  33. package/dist/strand-peer-book-file.js +37 -0
  34. package/dist/strand-peer-book-file.js.map +1 -0
  35. package/dist/strand-peer-book-protocol.d.ts +159 -0
  36. package/dist/strand-peer-book-protocol.js +360 -0
  37. package/dist/strand-peer-book-protocol.js.map +1 -0
  38. package/dist/strand-peer-book-swap.d.ts +104 -0
  39. package/dist/strand-peer-book-swap.js +284 -0
  40. package/dist/strand-peer-book-swap.js.map +1 -0
  41. package/dist/strand-peer-book.d.ts +159 -0
  42. package/dist/strand-peer-book.js +354 -0
  43. package/dist/strand-peer-book.js.map +1 -0
  44. package/dist/strand-peer-observer.d.ts +88 -0
  45. package/dist/strand-peer-observer.js +180 -0
  46. package/dist/strand-peer-observer.js.map +1 -0
  47. package/dist/types.d.ts +84 -17
  48. package/dist/types.js +6 -0
  49. package/dist/types.js.map +1 -1
  50. package/package.json +11 -7
  51. package/src/cadre-node.ts +7776 -7445
  52. package/src/index.ts +68 -0
  53. package/src/joined-strand-store.ts +308 -0
  54. package/src/key-store.ts +6 -0
  55. package/src/link-budget.ts +100 -53
  56. package/src/node-local-snapshot.ts +293 -289
  57. package/src/peer-dial.ts +3 -3
  58. package/src/peer-join-backfill.ts +709 -647
  59. package/src/relay-reservation.ts +2 -2
  60. package/src/seed-bootstrap.ts +1 -1
  61. package/src/strand-first-sync-gate.ts +352 -306
  62. package/src/strand-instance-manager.ts +106 -3
  63. package/src/strand-peer-book-file.ts +39 -0
  64. package/src/strand-peer-book-protocol.ts +463 -0
  65. package/src/strand-peer-book-swap.ts +338 -0
  66. package/src/strand-peer-book.ts +468 -0
  67. package/src/strand-peer-observer.ts +235 -0
  68. package/src/types.ts +86 -17
@@ -4,6 +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
8
  import { type EnrolledMachineStore } from './enrolled-machine-store.js';
8
9
  import { type SAppIdLookup } from './strand-watcher.js';
9
10
  import { type ControlCohortReconcileResult } from './control-cohort.js';
@@ -257,37 +258,36 @@ export declare class CadreNode implements SAppIdLookup {
257
258
  */
258
259
  private readonly strandPeerAddrRefreshAt;
259
260
  /**
260
- * CROSS-PARTY strand-network addresses learned at formation, keyed by strandId — the
261
- * responder's live strand addrs, carried back on the formation result
262
- * (`FormationResultMessage.strandAddrs`) and recorded by {@link formStrand}.
261
+ * Node-local strand peer book (see `strand-peer-book.ts`): per strand, the strand
262
+ * peers this node has met and their last-known strand-network addresses. Three
263
+ * writers: {@link recordFormationStrandPeers} files the responder's live strand
264
+ * addrs a formation result carried back (`FormationResultMessage.strandAddrs`),
265
+ * {@link observeStrandPeer} files every strand peer a running strand node
266
+ * identifies, and the per-strand book swap (`strand-peer-book-swap.ts`, handed the
267
+ * store as `StartStrandConfig.strandPeerBook`) files the node's own signed entry
268
+ * plus every signed entry a strand peer sends it — the only writer whose entries
269
+ * carry a signature, and the only one that can refresh a peer's addresses without
270
+ * meeting that peer. Read by {@link resolveCohortSeed} (launch + hibernation resume) and
271
+ * re-merged by every {@link refreshStrandPeerAddrs} pass, which is what keeps the
272
+ * entries alive past the peerStore's one-hour address expiry.
263
273
  *
264
274
  * This is the ONLY cross-party discovery input there is. The strand-addr RPC that
265
275
  * resolves a strand's addresses is membership-gated and answers own-party siblings
266
- * only, so without this map a joiner's cohort seed for a two-party strand is empty and
267
- * the mesh never forms.
268
- *
269
- * Read by {@link resolveCohortSeed} (launch + hibernation resume) and re-merged by
270
- * every {@link refreshStrandPeerAddrs} pass — the re-merge is what keeps the entries
271
- * alive past the peerStore's one-hour address expiry, since nothing can re-resolve
272
- * them.
273
- *
274
- * IN-MEMORY and ONE-SHOT, deliberately (see `docs/strands.md`): the addresses die with
275
- * the process, and a restarted initiator with no sibling running the strand is back to
276
- * an empty seed until the durability work lands
277
- * (`backlog/feat-cross-party-strand-addr-durability`). They are also never refreshed,
278
- * so a responder whose relay reservation rotates before the initiator dials leaves a
279
- * dead entry behind; recovery today is a fresh invitation.
280
- *
281
- * NOTE: entries are kept for the node's lifetime, including across a
282
- * {@link stopStrand} — a stopped strand may be claimed again with {@link addStrand}
283
- * (the watcher itself will not re-offer it: the stop suppresses the id for the rest of
284
- * the session) and must still get its seed. Each strand's list is capped at
285
- * `MAX_STRAND_ADDRS`, but the
286
- * number of KEYS is bounded only by the strands this node has ever formed as an
287
- * initiator (not by time); if a node ever forms strands at scale, evict on
288
- * `unpublishStrand` or cap the map.
289
- */
290
- private readonly crossPartyStrandAddrs;
276
+ * only, so without the book a joiner's cohort seed for a two-party strand is empty
277
+ * and the mesh never forms — and, with an in-memory book, never RE-forms after a
278
+ * restart (gotchoices/sereus#18). A STORE rather than a map so an embedder can make
279
+ * it outlive the process; constructed (or adopted from `config.strandPeers.store`)
280
+ * by {@link initializeStrandPeerBookStore} during {@link start}, deliberately NOT
281
+ * cleared by {@link cleanup}, so it survives a stop()→start() cycle of the same node
282
+ * instance — same lifecycle as {@link bootstrapPeerStore}.
283
+ *
284
+ * Entries survive a {@link stopStrand} (a stopped strand may be claimed again with
285
+ * {@link addStrand}, and is rediscovered on restart) and are forgotten by
286
+ * {@link unpublishStrand}, {@link forgetJoinedStrand} and self-revocation; the store
287
+ * ages the rest out after 14 days and caps each strand at 16 peers, so the book is
288
+ * bounded by time and by the strands this node runs rather than by its history.
289
+ */
290
+ private strandPeerBookStore;
291
291
  /**
292
292
  * PENDING strand membership invitations learned at formation, keyed by strandId — the
293
293
  * single-use `Strand.Invite` credential a closed-strand formation result carried back
@@ -295,10 +295,9 @@ export declare class CadreNode implements SAppIdLookup {
295
295
  * to redeem (`consumeInvite` seats the `Strand.Member` row under this party's own key
296
296
  * — the `strand-node-binds-member-peer` half of the party-identity chain).
297
297
  *
298
- * IN-MEMORY, deliberately, like {@link crossPartyStrandAddrs}: a restarted joiner
299
- * re-forms from scratch anyway (durability is
300
- * `backlog/feat-cross-party-strand-addr-durability`'s problem), and re-forming issues
301
- * a fresh invitation. A re-formation against the same strand REPLACES the entry — the
298
+ * IN-MEMORY, deliberately: an invitation is single-use and short-lived, a restarted
299
+ * joiner that never redeemed it re-forms from scratch, and re-forming issues a fresh
300
+ * one. A re-formation against the same strand REPLACES the entry — the
302
301
  * fresh invitation supersedes one that may have expired. The party key the invitation
303
302
  * admits, by contrast, IS persisted (`StrandPartyKey`, seated by
304
303
  * {@link adoptFormationMembershipInvite} before the entry lands here), so a lost
@@ -314,8 +313,9 @@ export declare class CadreNode implements SAppIdLookup {
314
313
  * just waits; once launched, staging notifies the manager, which re-arms a finished loop.
315
314
  *
316
315
  * NOTE: entries live for the node's lifetime (one small pair per formed closed
317
- * strand), the same unbounded-keys tripwire {@link crossPartyStrandAddrs} documents —
318
- * if a node ever forms strands at scale, evict alongside that map.
316
+ * strand); the number of keys is bounded only by the strands this node has ever
317
+ * formed, not by time — if a node ever forms strands at scale, evict on
318
+ * `unpublishStrand` / `forgetJoinedStrand` as the strand peer book does.
319
319
  */
320
320
  private readonly pendingMembershipInvites;
321
321
  /**
@@ -386,6 +386,23 @@ export declare class CadreNode implements SAppIdLookup {
386
386
  * so it survives a stop()→start() cycle of the same node instance.
387
387
  */
388
388
  private enrolledMachineStore;
389
+ /**
390
+ * Node-local record of the strands this node joined from ANOTHER party (see
391
+ * `joined-strand-store.ts`) — the only thing that names them after a restart, since
392
+ * this party's control database holds no row for them. Written by {@link formStrand}
393
+ * and by an {@link addStrand} of a foreign row; forgotten by {@link forgetJoinedStrand}
394
+ * and on self-revocation. Adopted from `config.joinedStrands.store`, else built over
395
+ * `config.keyStore`, else in-memory, by {@link initializeJoinedStrandStore}. Like
396
+ * {@link bootstrapPeerStore} it is deliberately NOT cleared by {@link cleanup}.
397
+ */
398
+ private joinedStrandStore;
399
+ /**
400
+ * The strand watcher's view of {@link joinedStrandStore}: its records unioned with the
401
+ * control rows each poll. Per SESSION, unlike the store — rebuilt at every
402
+ * {@link start}, which is what ends a revoked join's "keep offering until this session
403
+ * ends" (see `JoinedStrandRows.forgetAfterThisSession`).
404
+ */
405
+ private joinedStrandRows;
389
406
  /**
390
407
  * The enrolled-machine count this node's CONTROL libp2p node was (or will be)
391
408
  * built with — read out of {@link enrolledMachineStore} during {@link start},
@@ -693,6 +710,25 @@ export declare class CadreNode implements SAppIdLookup {
693
710
  * number nobody chose, and the mismatch always means a miswired embedder.
694
711
  */
695
712
  private initializeEnrolledMachineStore;
713
+ /**
714
+ * Construct (or adopt) the node-local joined-strand store (see
715
+ * {@link joinedStrandStore}) and this session's watcher view over it.
716
+ *
717
+ * The store instance is kept across stop()→start(); the view is rebuilt. A store
718
+ * scoped to a different party is a configuration error, fail closed: its joins
719
+ * would be offered to this party's app as if this party had made them.
720
+ */
721
+ private initializeJoinedStrandStore;
722
+ /**
723
+ * Construct (or adopt) the node-local strand peer book (see
724
+ * {@link strandPeerBookStore}). Synchronous for the reason
725
+ * {@link initializeBootstrapPeerStore} is: an injected store has already loaded its
726
+ * persisted entries by the time it is handed in, and the in-memory fallback has
727
+ * nothing to load. The instance is kept across stop()→start(). A store scoped to a
728
+ * different party is a configuration error, fail closed: another party's strand
729
+ * peers must never enter this node's dial loop.
730
+ */
731
+ private initializeStrandPeerBookStore;
696
732
  /**
697
733
  * The node-local enrolled-machine store (null before {@link start}) — what this
698
734
  * node last knew about its party's size. Exposed for diagnostics and for a host
@@ -706,6 +742,13 @@ export declare class CadreNode implements SAppIdLookup {
706
742
  * node dial if it is stranded?".
707
743
  */
708
744
  getBootstrapPeerStore(): BootstrapPeerStore | null;
745
+ /**
746
+ * The node-local strand peer book (null before {@link start}) — per strand, the
747
+ * strand peers this node dials first on launch, resume and refresh. Exposed for
748
+ * diagnostics and for a host that wants to show "whom would this strand re-find
749
+ * after a restart?".
750
+ */
751
+ getStrandPeerBookStore(): StrandPeerBookStore | null;
709
752
  /**
710
753
  * The node-local trusted-owner anchor (null before {@link start}). This is
711
754
  * the set the authorized-membership predicate and seed-trust anchor consult —
@@ -765,10 +808,10 @@ export declare class CadreNode implements SAppIdLookup {
765
808
  * needs no work of its own: `createLibp2pNode` registers the block-transfer
766
809
  * handler on every node it builds, control node included, and this node's
767
810
  * per-stream gate (`authorizeInboundControlStream`) covers the inbound
768
- * direction. A denied peer is retried on its next `connection:open`, and —
769
- * because the production join order is connect-then-authorize — on every
770
- * committed membership change via {@link refreshAuthorizedControlPeers}'s
771
- * `scheduleConnectedPeers()` call.
811
+ * direction. A denied peer is retried on its next `peer:identify` (a
812
+ * reconnect re-runs identify), and — because the production join order is
813
+ * connect-then-authorize — on every committed membership change via
814
+ * {@link refreshAuthorizedControlPeers}'s `scheduleConnectedPeers()` call.
772
815
  *
773
816
  * No-ops (logged) when the embedder configured no control storage or the
774
817
  * node exposes no key network — the backfill would have nothing to read or
@@ -1045,6 +1088,11 @@ export declare class CadreNode implements SAppIdLookup {
1045
1088
  * address without a `/p2p/<id>` component contributes nothing.
1046
1089
  */
1047
1090
  private getBootstrapPeerIds;
1091
+ /**
1092
+ * What the strand watcher polls: this party's control rows plus a row per remembered
1093
+ * cross-party join (see {@link joinedStrandRows}), so a joined strand is re-offered
1094
+ * and relaunched on the same path as the party's own.
1095
+ */
1048
1096
  private createStrandQueryable;
1049
1097
  /**
1050
1098
  * Schedule the node's initial self-record publish + ongoing refresh shortly
@@ -1875,6 +1923,10 @@ export declare class CadreNode implements SAppIdLookup {
1875
1923
  * published (e.g. re-attaching its own orphan after a restart) founds it, and attaching
1876
1924
  * anyone else's row joins, without the caller needing to know which it is.
1877
1925
  *
1926
+ * A joined row (no `founder: true`) this party's control database does not hold — a
1927
+ * strand joined from another party — is remembered in the node's joined-strand store, so it is re-offered as
1928
+ * `strand:discovered` after a restart (see {@link forgetJoinedStrand}).
1929
+ *
1878
1930
  * A rejected call leaves nothing running but DOES leave the sApp config
1879
1931
  * registered, deliberately: both an explicit retry and the {@link StrandWatcher}'s
1880
1932
  * automatic relaunch need it. A failed launch here hands the strand back to that
@@ -2273,6 +2325,13 @@ export declare class CadreNode implements SAppIdLookup {
2273
2325
  * for a relay we are not yet connected to.
2274
2326
  */
2275
2327
  private resolveCohortSeed;
2328
+ /**
2329
+ * The strand peer book's contribution to a strand's seed: every live entry's
2330
+ * addresses, freshest peer first, minus this node's own entry when `selfPeerId` is
2331
+ * known (the book swap files one under the strand transport id; a node must never
2332
+ * dial itself). Empty before {@link start} builds the store.
2333
+ */
2334
+ private strandPeerBookAddrs;
2276
2335
  /**
2277
2336
  * The own-party half of {@link resolveCohortSeed}: strand-network addresses resolved
2278
2337
  * from CONNECTED cohort siblings over the control-mesh strand-addr RPC, doubling as
@@ -2390,7 +2449,7 @@ export declare class CadreNode implements SAppIdLookup {
2390
2449
  * NOTE: against a relay that is DOWN this hook costs up to two strand-addr
2391
2450
  * timeouts (10 s each: dial by peer id, then by addr) before the reservation drive even
2392
2451
  * starts, so one failed re-drive holds the supervisor `driving` for those 20 s plus the
2393
- * drive's own deadline — 28 s at the default declared link round trip, and longer on a host
2452
+ * drive's own deadline — 34 s at the default declared link round trip, and longer on a host
2394
2453
  * that declared a slower one (`link-budget.ts`).
2395
2454
  * Bounded and harmless while the relay is unreachable anyway; if recovery
2396
2455
  * latency after a relay comes back ever matters, skip the announce when the
@@ -2427,17 +2486,17 @@ export declare class CadreNode implements SAppIdLookup {
2427
2486
  private refreshStrandPeerAddrs;
2428
2487
  /**
2429
2488
  * One strand's share of {@link refreshStrandPeerAddrs}: RPC the siblings, union their
2430
- * answers with this strand's cross-party contact addresses, and merge the lot into the
2431
- * strand's address book. Errors are logged and swallowed so one strand's failure never
2432
- * costs the others their refresh.
2489
+ * answers with this strand's peer-book addresses, and merge the lot into the strand's
2490
+ * address book. Errors are logged and swallowed so one strand's failure never costs
2491
+ * the others their refresh.
2433
2492
  *
2434
2493
  * `targets` may be EMPTY — a solo node, or one whose only control peers are strangers.
2435
- * The cross-party addresses still have to be re-merged in that case: they were learned
2436
- * at formation and can never be re-resolved, so this periodic re-merge is the ONLY
2437
- * thing standing between them and the peerStore's one-hour expiry. A strand with
2438
- * neither a sibling to ask nor a contact entry does nothing and leaves its throttle
2439
- * unstamped, so the next reconcile tick retries rather than sitting out the whole
2440
- * refresh interval having done nothing.
2494
+ * The peer-book addresses still have to be re-merged in that case: nothing re-resolves
2495
+ * another party's addresses, so this periodic re-merge is the ONLY thing standing
2496
+ * between them and the peerStore's one-hour expiry. A strand with neither a sibling to
2497
+ * ask nor a book entry does nothing and leaves its throttle unstamped, so the next
2498
+ * reconcile tick retries rather than sitting out the whole refresh interval having
2499
+ * done nothing.
2441
2500
  */
2442
2501
  private refreshOneStrandPeerAddrs;
2443
2502
  /**
@@ -2459,14 +2518,14 @@ export declare class CadreNode implements SAppIdLookup {
2459
2518
  * peerStore's one-hour expiry. No new gating here — cross-party strand trust is
2460
2519
  * `backlog/strand-network-nat-relay-reachability`.
2461
2520
  *
2462
- * NOTE: one input does NOT age out — the cross-party addresses a formation
2463
- * carried back ({@link crossPartyStrandAddrs}). Nothing can re-resolve them, so
2464
- * {@link refreshStrandPeerAddrs} re-merges the same stored list every pass, which
2465
- * also means a junk entry from a responder survives for the node's lifetime rather
2466
- * than an hour. Bounded to {@link MAX_STRAND_ADDRS} per strand, from a party this
2467
- * node deliberately formed a strand with, and still authority-free — so the
2468
- * exposure is a handful of failed dials, not a trust hole. Evict on
2469
- * `unpublishStrand` if that ever stops being an acceptable price.
2521
+ * NOTE: one input ages out on a slower clock — the strand peer book's entries
2522
+ * ({@link strandPeerBookStore}). Nothing can re-resolve another party's addresses,
2523
+ * so {@link refreshStrandPeerAddrs} re-merges the book every pass, which means a
2524
+ * junk entry from a responder or a peer's identify survives until the book drops it
2525
+ * (14 days unrefreshed, or the peer's next connection replacing the entry) rather
2526
+ * than an hour. Bounded to 16 peers × 16 addrs per strand, forgotten on
2527
+ * `unpublishStrand` / `forgetJoinedStrand`, and still authority-free — so the
2528
+ * exposure is a handful of failed dials, not a trust hole.
2470
2529
  */
2471
2530
  private mergeStrandPeerAddrs;
2472
2531
  /**
@@ -2479,6 +2538,69 @@ export declare class CadreNode implements SAppIdLookup {
2479
2538
  * live node, so it answers and a later sibling can dial in.
2480
2539
  */
2481
2540
  private getStrandMultiaddrs;
2541
+ /**
2542
+ * Leave a strand joined from another party: forget its remembered join, so no later
2543
+ * start re-offers it, then {@link stopStrand} it here. The "leave" a joiner has, since it
2544
+ * cannot {@link unpublishStrand} a row its party's control database never held. For one
2545
+ * of this party's own strands the forget is a no-op and this is just {@link stopStrand}.
2546
+ *
2547
+ * Touches nothing beyond this node — the other members are not told, and this party's
2548
+ * membership row in the strand stays.
2549
+ */
2550
+ forgetJoinedStrand(strandId: string): Promise<void>;
2551
+ /**
2552
+ * Record `row` as a join from another party when this party's control database holds no
2553
+ * row for it — the one case nothing else would re-offer after a restart. Skips the write
2554
+ * when an identical record is already there (an app re-claiming a remembered join on
2555
+ * every start), and keeps the first `joinedAt`.
2556
+ *
2557
+ * Best-effort, for {@link addStrand}: a failure costs the strand its re-offer after the
2558
+ * next restart, not this attach, so it is reported and the attach goes on.
2559
+ * {@link formStrand}, whose join nothing else would name, fails loudly instead.
2560
+ *
2561
+ * NOTE: "no control row" also describes a row the party has just unpublished. A claim
2562
+ * of such a row the app kept from an earlier offer, made after the watcher already
2563
+ * withdrew it, is remembered as a join and survives the removal on this machine until
2564
+ * {@link forgetJoinedStrand}. Contrived today (a claim of a row the node itself is still
2565
+ * offering skips this method); if it shows up, check the strand's `Revocation`
2566
+ * tombstone here before recording.
2567
+ */
2568
+ private rememberForeignStrand;
2569
+ /**
2570
+ * Self-revocation arm of the joined-strand record: a party removed from a strand must
2571
+ * not re-attach it on every launch. The strand keeps running this session — the
2572
+ * `strand:revoked` contract is that nothing is torn down for the app — and is gone
2573
+ * after the next start. The join record is a no-op for this party's own strands, which
2574
+ * have none; the strand peer book is forgotten either way, since a removed party must
2575
+ * not keep dialing the strand's peers.
2576
+ *
2577
+ * NOTE: a manager can re-admit a removed party directly (`addMemberByManager`), and the
2578
+ * membership loop then finishes the join on its own — but the record is gone by then, so
2579
+ * the next start does not re-attach the strand. Re-forming records it again. If direct
2580
+ * re-admission becomes a routine flow, re-record the join when the revoked-peer gate
2581
+ * clears for this node.
2582
+ */
2583
+ private forgetRevokedJoin;
2584
+ /**
2585
+ * Forget a strand's entries in the peer book (see {@link strandPeerBookStore}):
2586
+ * fire-and-log, like every book write — the removal is visible synchronously by the
2587
+ * store's contract, and a failed persist costs restart survival only.
2588
+ */
2589
+ private forgetStrandPeers;
2590
+ /**
2591
+ * The strand peer book's observation writer (`StartStrandConfig.onStrandPeerIdentified`):
2592
+ * a strand node identified a strand peer at `observation.addrs`, so remember it as an
2593
+ * unsigned entry seen just now. The observer never reports self, and the store binds
2594
+ * every address to the peer id before filing it.
2595
+ */
2596
+ private observeStrandPeer;
2597
+ /**
2598
+ * Merge one entry into the peer book. Fire-and-log: the entry is visible
2599
+ * synchronously by the store's contract, and the promise tracks durability only. A
2600
+ * persist failure costs restart survival, never this session's seed — the same trade
2601
+ * {@link retainDialTarget} makes.
2602
+ */
2603
+ private rememberStrandPeer;
2482
2604
  /**
2483
2605
  * Stop a strand on THIS node only: untrack it from hibernation, drop its sApp config,
2484
2606
  * stop the local instance, and emit `strand:stopped`. The shared `Strand` row is left
@@ -2487,7 +2609,9 @@ export declare class CadreNode implements SAppIdLookup {
2487
2609
  * id in the watcher (`StrandWatcher.suppressStrand`) and drops it from
2488
2610
  * {@link getDiscoveredStrands}, so neither a later poll nor a drain can undo a
2489
2611
  * deliberate stop. Only an explicit {@link addStrand} does, which is the caller
2490
- * reversing its own decision. Party-wide removal is {@link unpublishStrand}.
2612
+ * reversing its own decision. Party-wide removal is {@link unpublishStrand}. A strand
2613
+ * joined from another party comes back the same way, from its remembered join;
2614
+ * {@link forgetJoinedStrand} is how to leave one for good.
2491
2615
  */
2492
2616
  stopStrand(strandId: string): Promise<void>;
2493
2617
  /**
@@ -2613,7 +2737,7 @@ export declare class CadreNode implements SAppIdLookup {
2613
2737
  *
2614
2738
  * BUDGET: the supervisor's first attempt is what `start()` waits on — deliberately the drive's
2615
2739
  * ordinary deadline rather than a boot-specific one. That deadline is now COUNTED, four link
2616
- * round trips at the declared `network.linkRoundTripMs` (8 s at its default, where it was a
2740
+ * round trips at the declared `network.linkRoundTripMs` (14 s at its default, where it was a
2617
2741
  * fixed 10 s): a healthy dial-plus-reserve is sub-second even over a WAN, so this is slack for
2618
2742
  * a slow link, while going much longer would make a dead relay indistinguishable from a hung
2619
2743
  * start and much shorter would fail nodes on links that were merely slow. A host that declares
@@ -3059,6 +3183,16 @@ export declare class CadreNode implements SAppIdLookup {
3059
3183
  * @returns The member key and strand info if successful
3060
3184
  */
3061
3185
  formStrand(invitation: OpenInvitation, disclosure?: StrandFormationDisclosure): Promise<FormStrandResult>;
3186
+ /**
3187
+ * Remember the strand a formation just joined (see {@link joinedStrandStore}), so it
3188
+ * is re-offered after a restart even when the app is killed before its `addStrand`.
3189
+ * Last in {@link formStrand}, after the membership adoption, so a failure here leaves
3190
+ * the party key and staged invitation in place for the re-formation it asks for.
3191
+ *
3192
+ * Throws on failure, like {@link adoptFormationMembershipInvite}: the formation's
3193
+ * token is spent, and a join no store names would vanish at the next restart.
3194
+ */
3195
+ private rememberFormedStrand;
3062
3196
  /**
3063
3197
  * Adopt an approved closed-strand formation's membership half on the JOINER:
3064
3198
  *
@@ -3145,15 +3279,20 @@ export declare class CadreNode implements SAppIdLookup {
3145
3279
  private issueStrandMembershipInvite;
3146
3280
  /**
3147
3281
  * Remember the responder's strand-network addresses for a strand this node just
3148
- * formed, so the strand's discovery seed has something to work with when the app
3149
- * launches it (see {@link crossPartyStrandAddrs}).
3150
- *
3151
- * Scoped strictly to `strandId`: these addresses reach ONE strand node of ONE other
3152
- * party and must never seed another strand's mesh or the control peerStore. An empty
3153
- * list is recorded as no entry at all rather than an empty one, so a later formation
3154
- * against the same strand that DOES disclose addresses is not shadowed.
3155
- */
3156
- private recordCrossPartyStrandAddrs;
3282
+ * formed in the strand peer book (see {@link strandPeerBookStore}), so the strand's
3283
+ * discovery seed has something to work with when the app launches it — and again
3284
+ * after every restart.
3285
+ *
3286
+ * The carried list is peer-agnostic (`sanitizeStrandAddrs` bounds and parses it,
3287
+ * nothing more), so it is attributed per peer here (`groupAddrsByPeerId`, the same
3288
+ * rule the address-book merge applies) and each peer becomes one unsigned entry seen
3289
+ * just now: the responder disclosed the addresses live a moment ago. An entry naming
3290
+ * no destination peer is dropped there. Scoped strictly to `strandId`: these
3291
+ * addresses reach ONE strand node of ONE other party and must never seed another
3292
+ * strand's mesh or the control peerStore. An empty list records nothing, so a later
3293
+ * formation against the same strand that DOES disclose addresses is not shadowed.
3294
+ */
3295
+ private recordFormationStrandPeers;
3157
3296
  /**
3158
3297
  * Encode an open invitation for out-of-band delivery (QR, link, etc.).
3159
3298
  */