@serfab/cadre-core 1.7.0 → 1.8.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 (162) hide show
  1. package/README.md +8 -6
  2. package/dist/cadre-node.d.ts +386 -114
  3. package/dist/cadre-node.js +887 -359
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-authorization.d.ts +2 -2
  6. package/dist/control-authorization.js +1 -0
  7. package/dist/control-authorization.js.map +1 -1
  8. package/dist/control-cohort.d.ts +6 -0
  9. package/dist/control-cohort.js +6 -0
  10. package/dist/control-cohort.js.map +1 -1
  11. package/dist/control-database.d.ts +46 -8
  12. package/dist/control-database.js +97 -18
  13. package/dist/control-database.js.map +1 -1
  14. package/dist/control-formation-recorder.d.ts +37 -14
  15. package/dist/control-formation-recorder.js +60 -10
  16. package/dist/control-formation-recorder.js.map +1 -1
  17. package/dist/control-read-retry.d.ts +13 -12
  18. package/dist/control-read-retry.js +14 -12
  19. package/dist/control-read-retry.js.map +1 -1
  20. package/dist/control-schema.d.ts +1 -1
  21. package/dist/control-schema.js +67 -5
  22. package/dist/control-schema.js.map +1 -1
  23. package/dist/control-stream.d.ts +9 -1
  24. package/dist/control-stream.js +24 -1
  25. package/dist/control-stream.js.map +1 -1
  26. package/dist/control-write-retry.d.ts +12 -2
  27. package/dist/control-write-retry.js +13 -2
  28. package/dist/control-write-retry.js.map +1 -1
  29. package/dist/delegate-admission.d.ts +11 -8
  30. package/dist/delegate-admission.js +19 -11
  31. package/dist/delegate-admission.js.map +1 -1
  32. package/dist/diagnostics/connection-path.d.ts +0 -7
  33. package/dist/diagnostics/connection-path.js +0 -7
  34. package/dist/diagnostics/connection-path.js.map +1 -1
  35. package/dist/formation-approval.d.ts +7 -0
  36. package/dist/formation-approval.js +9 -3
  37. package/dist/formation-approval.js.map +1 -1
  38. package/dist/fs-atomic.d.ts +11 -2
  39. package/dist/fs-atomic.js +13 -3
  40. package/dist/fs-atomic.js.map +1 -1
  41. package/dist/hibernation-manager.d.ts +111 -8
  42. package/dist/hibernation-manager.js +234 -49
  43. package/dist/hibernation-manager.js.map +1 -1
  44. package/dist/index.d.ts +8 -6
  45. package/dist/index.js +15 -9
  46. package/dist/index.js.map +1 -1
  47. package/dist/joined-strand-store.d.ts +90 -25
  48. package/dist/joined-strand-store.js +223 -55
  49. package/dist/joined-strand-store.js.map +1 -1
  50. package/dist/key-store-file.js +14 -5
  51. package/dist/key-store-file.js.map +1 -1
  52. package/dist/link-budget.d.ts +367 -69
  53. package/dist/link-budget.js +375 -72
  54. package/dist/link-budget.js.map +1 -1
  55. package/dist/membership-connection-gater.d.ts +30 -43
  56. package/dist/membership-connection-gater.js +31 -42
  57. package/dist/membership-connection-gater.js.map +1 -1
  58. package/dist/peer-dial.d.ts +24 -4
  59. package/dist/peer-dial.js +74 -5
  60. package/dist/peer-dial.js.map +1 -1
  61. package/dist/peer-join-backfill.d.ts +2 -1
  62. package/dist/peer-join-backfill.js +6 -5
  63. package/dist/peer-join-backfill.js.map +1 -1
  64. package/dist/push-fanout.d.ts +6 -0
  65. package/dist/push-fanout.js +6 -0
  66. package/dist/push-fanout.js.map +1 -1
  67. package/dist/relay-reservation.d.ts +3 -1
  68. package/dist/relay-reservation.js +3 -1
  69. package/dist/relay-reservation.js.map +1 -1
  70. package/dist/relay-server.d.ts +79 -0
  71. package/dist/relay-server.js +74 -0
  72. package/dist/relay-server.js.map +1 -0
  73. package/dist/seed-bootstrap.d.ts +57 -4
  74. package/dist/seed-bootstrap.js +107 -83
  75. package/dist/seed-bootstrap.js.map +1 -1
  76. package/dist/storage-scope.d.ts +33 -17
  77. package/dist/storage-scope.js +44 -23
  78. package/dist/storage-scope.js.map +1 -1
  79. package/dist/strand-addr-protocol.d.ts +58 -21
  80. package/dist/strand-addr-protocol.js +133 -85
  81. package/dist/strand-addr-protocol.js.map +1 -1
  82. package/dist/strand-database.d.ts +20 -2
  83. package/dist/strand-database.js +42 -5
  84. package/dist/strand-database.js.map +1 -1
  85. package/dist/strand-first-sync-gate.d.ts +59 -21
  86. package/dist/strand-first-sync-gate.js +58 -19
  87. package/dist/strand-first-sync-gate.js.map +1 -1
  88. package/dist/strand-formation-deadlines.d.ts +151 -0
  89. package/dist/strand-formation-deadlines.js +171 -0
  90. package/dist/strand-formation-deadlines.js.map +1 -0
  91. package/dist/strand-formation-manager.d.ts +63 -30
  92. package/dist/strand-formation-manager.js +67 -41
  93. package/dist/strand-formation-manager.js.map +1 -1
  94. package/dist/strand-formation-protocol.d.ts +45 -35
  95. package/dist/strand-formation-protocol.js +59 -122
  96. package/dist/strand-formation-protocol.js.map +1 -1
  97. package/dist/strand-instance-manager.d.ts +113 -6
  98. package/dist/strand-instance-manager.js +257 -41
  99. package/dist/strand-instance-manager.js.map +1 -1
  100. package/dist/strand-membership-writer.d.ts +6 -0
  101. package/dist/strand-membership-writer.js +11 -0
  102. package/dist/strand-membership-writer.js.map +1 -1
  103. package/dist/strand-peer-book-protocol.d.ts +13 -4
  104. package/dist/strand-peer-book-protocol.js +3 -7
  105. package/dist/strand-peer-book-protocol.js.map +1 -1
  106. package/dist/strand-peer-book.d.ts +1 -1
  107. package/dist/strand-peer-book.js +8 -1
  108. package/dist/strand-peer-book.js.map +1 -1
  109. package/dist/strand-peer-observer.js +1 -1
  110. package/dist/strand-revocation-enforcer.js +2 -1
  111. package/dist/strand-revocation-enforcer.js.map +1 -1
  112. package/dist/strand-solicitation.d.ts +36 -20
  113. package/dist/strand-solicitation.js +4 -4
  114. package/dist/strand-solicitation.js.map +1 -1
  115. package/dist/strand-wake-protocol.d.ts +80 -23
  116. package/dist/strand-wake-protocol.js +89 -64
  117. package/dist/strand-wake-protocol.js.map +1 -1
  118. package/dist/types.d.ts +128 -38
  119. package/dist/types.js +4 -3
  120. package/dist/types.js.map +1 -1
  121. package/package.json +7 -7
  122. package/src/cadre-node.ts +8367 -7776
  123. package/src/control-authorization.ts +2 -1
  124. package/src/control-cohort.ts +6 -0
  125. package/src/control-database.ts +122 -18
  126. package/src/control-formation-recorder.ts +354 -305
  127. package/src/control-read-retry.ts +14 -12
  128. package/src/control-schema.ts +67 -5
  129. package/src/control-stream.ts +25 -1
  130. package/src/control-write-retry.ts +13 -2
  131. package/src/delegate-admission.ts +23 -13
  132. package/src/diagnostics/connection-path.ts +0 -7
  133. package/src/formation-approval.ts +610 -604
  134. package/src/fs-atomic.ts +14 -4
  135. package/src/hibernation-manager.ts +638 -437
  136. package/src/index.ts +28 -10
  137. package/src/joined-strand-store.ts +247 -57
  138. package/src/key-store-file.ts +14 -5
  139. package/src/link-budget.ts +393 -74
  140. package/src/membership-connection-gater.ts +31 -45
  141. package/src/peer-dial.ts +81 -7
  142. package/src/peer-join-backfill.ts +8 -6
  143. package/src/push-fanout.ts +6 -0
  144. package/src/relay-reservation.ts +3 -1
  145. package/src/relay-server.ts +98 -0
  146. package/src/seed-bootstrap.ts +133 -91
  147. package/src/storage-scope.ts +44 -23
  148. package/src/strand-addr-protocol.ts +547 -458
  149. package/src/strand-database.ts +287 -242
  150. package/src/strand-first-sync-gate.ts +58 -19
  151. package/src/strand-formation-deadlines.ts +227 -0
  152. package/src/strand-formation-manager.ts +700 -647
  153. package/src/strand-formation-protocol.ts +103 -150
  154. package/src/strand-instance-manager.ts +287 -44
  155. package/src/strand-membership-writer.ts +12 -0
  156. package/src/strand-peer-book-protocol.ts +16 -13
  157. package/src/strand-peer-book.ts +8 -1
  158. package/src/strand-peer-observer.ts +1 -1
  159. package/src/strand-revocation-enforcer.ts +2 -1
  160. package/src/strand-solicitation.ts +519 -500
  161. package/src/strand-wake-protocol.ts +401 -355
  162. package/src/types.ts +130 -38
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ConnectionGater, Libp2p, PeerId, PrivateKey } from '@libp2p/interface';
2
+ import type { CircuitRelayServerInit } from '@libp2p/circuit-relay-v2';
2
3
  import type { IRawStorage, Libp2pConnectionMonitorInit, Libp2pTransports, NoiseCryptoInterface } from '@optimystic/db-p2p';
3
4
  import type { IPeerNetwork, IRepo } from '@optimystic/db-core';
4
5
  import type { PeerJoinBackfillConfig } from './peer-join-backfill.js';
@@ -123,10 +124,12 @@ export const HIBERNATION_TIMEOUTS: Record<LatencyHint, HibernationTimeouts> = {
123
124
  * Factory functions are useful for creating per-scope storage instances.
124
125
  *
125
126
  * **The argument is an opaque scope key.** Use it directly as a file name, directory
126
- * name or database name: every key stays within `[A-Za-z0-9._-]`, so no escaping is
127
- * needed and none should be assumed. Do not parse it; `controlStorageScope` /
128
- * `isControlStorageScope` (`storage-scope.ts`) are the supported way to mint and
129
- * recognize the control key. The control key holds the charset by base64url encoding;
127
+ * name or database name: every key stays within `[a-z0-9._-]`, so no escaping is
128
+ * needed and none should be assumed, and two different keys are two different names
129
+ * even on a filesystem that ignores case (Windows, macOS). Do not parse it;
130
+ * `controlStorageScope` / `isControlStorageScope` (`storage-scope.ts`) are the supported
131
+ * way to mint and recognize the control key. The control key holds the charset by
132
+ * lowercase hex encoding;
130
133
  * a strand's key — its strand id, which may have replicated in from another node in
131
134
  * the party — holds it because `StrandInstanceManager.startStrand` runs
132
135
  * `assertStrandScopeKey` on every launch and refuses a strand that fails it.
@@ -171,7 +174,7 @@ export interface StorageConfig {
171
174
  * the single-instance form cannot serve more than one party.
172
175
  *
173
176
  * The `scope` a factory receives is an opaque key already safe as a path or
174
- * database-name segment (always within `[A-Za-z0-9._-]`), so each example below
177
+ * database-name segment (always within `[a-z0-9._-]`), so each example below
175
178
  * interpolates it directly.
176
179
  *
177
180
  * For Node.js environments, use FileRawStorage from @optimystic/db-p2p-storage-fs:
@@ -319,7 +322,8 @@ export interface NetworkConfig {
319
322
  * It buys a node that BOOTS, not a node that boots fast: `start()` still waits
320
323
  * out that first attempt, which costs the drive's whole timeout
321
324
  * (`DEFAULT_RELAY_RESERVE_TIMEOUT_MS`, four link round trips at
322
- * {@link linkRoundTripMs} — 14 s at its default) against a relay that is unreachable
325
+ * {@link linkRoundTripMs} plus two admission decisions — 18 s at its default) against a relay
326
+ * that is unreachable
323
327
  * rather than merely refusing (`relay-reservation.ts` polls to the deadline, in
324
328
  * case libp2p's own discovery lands a reservation independently).
325
329
  *
@@ -342,6 +346,31 @@ export interface NetworkConfig {
342
346
  * better connectivity and uptime), false for transaction profile nodes.
343
347
  */
344
348
  enableRelay?: boolean;
349
+ /**
350
+ * Settings for this machine's circuit-relay SERVER, handed to `@libp2p/circuit-relay-v2`'s
351
+ * `circuitRelayServer(...)` (through db-p2p's `NodeOptions.relayServerInit`) on the control
352
+ * node and on every strand node. Only meaningful while {@link enableRelay} is on.
353
+ *
354
+ * Omitted, a party-run relay FORWARDS WITHOUT LIMIT: `reservations.applyDefaultLimit` is
355
+ * `false`, where libp2p's own default caps every relayed connection at 128 KiB or two
356
+ * minutes and resets it — which cut off any database sync or chat history forwarded
357
+ * through a party's own always-on machine. The store also holds
358
+ * `PARTY_RELAY_MAX_RESERVATIONS` (128) reservations instead of libp2p's 15, and each lives
359
+ * `PARTY_RELAY_RESERVATION_TTL_MS` (2 h) unrefreshed. See `relay-server.ts`.
360
+ *
361
+ * A value is MERGED over those defaults, not substituted for them: `reservations` key by
362
+ * key, every other top-level key as given. Setting only `reservations.maxReservations`
363
+ * keeps the limit off; `reservations: { applyDefaultLimit: true }` is how to turn libp2p's
364
+ * cap back on.
365
+ *
366
+ * The accepted tradeoff of the unlimited default: once a slot is granted, nothing caps what
367
+ * is forwarded through it, and a holder that keeps refreshing keeps it indefinitely — the
368
+ * 2 h lifetime only reclaims a slot whose holder went away. On the control node a peer it
369
+ * cannot place as a party member is bounded by COUNT ({@link unauthorizedRelayReservationCap}).
370
+ * A strand node's relay server has no such budget (its peers are cross-party by design), so
371
+ * any peer that reaches it is bounded only by `maxReservations`.
372
+ */
373
+ relayServerInit?: CircuitRelayServerInit;
345
374
  /**
346
375
  * Cap on concurrent circuit-relay reservations this node's relay server grants to
347
376
  * peers it cannot (yet) recognize as authorized members — the boot-ordering window
@@ -413,16 +442,21 @@ export interface NetworkConfig {
413
442
  * How long ONE cohort peer gets to answer ONE read-path request, in milliseconds, for the
414
443
  * control node and every strand node — as {@link connectionMonitor} is, and for the same
415
444
  * reason: the setting describes the LINK, and a phone's control node and its strand nodes
416
- * ride the same one. Omitted takes {@link COHORT_READ_DEADLINE_MS} (5000 ms), chosen for two
417
- * parties reaching each other only through a relay; Optimystic's own default is 1000 ms.
445
+ * ride the same one. Omitted, the deadline is DERIVED from {@link linkRoundTripMs}: one
446
+ * request and its answer over an open circuit costs two link round trips, so 7 000 ms at the
447
+ * default declaration (`cohortReadDeadlineMs` in `link-budget.ts`; the plugin's
448
+ * {@link COHORT_READ_DEADLINE_MS} is the same number). Optimystic's own default is 1000 ms.
418
449
  *
419
- * Raise it for a link slower still, lower it for a deployment that is all LAN and wants a
420
- * departed peer to stop holding up a read sooner. The cost of a larger value is that a peer
421
- * which is truly gone holds a read of a block missing locally for that long before the read
422
- * is declined and retried, and a joining machine's first sync runs several such consults —
423
- * so a change here should be weighed against
424
- * {@link CadreNodeConfig.strandFirstSync}'s budget. The measurement behind the default, and
425
- * what it costs, are on {@link COHORT_READ_DEADLINE_MS}.
450
+ * Set this only to break the derivation on purpose: an explicit value wins over whatever
451
+ * {@link linkRoundTripMs} would derive, so a deployment that is all LAN can make a departed
452
+ * peer stop holding up a read sooner without declaring a faster link (which would also
453
+ * shorten the dial budgets). A deployment on a slower link should raise
454
+ * {@link linkRoundTripMs} instead, which moves this and every dial budget together. The
455
+ * cost of a larger value is that a peer which is truly gone holds a read of a block missing
456
+ * locally for that long before the read is declined and retried, and a joining machine's
457
+ * first sync runs several such consults — so a change here should be weighed against
458
+ * {@link CadreNodeConfig.strandFirstSync}'s budget. The derivation, its history and what it
459
+ * costs are on {@link COHORT_READ_DEADLINE_MS}.
426
460
  *
427
461
  * Handed to db-p2p's `clusterPolicy.cohortQueryTimeoutMs` unchanged and NOT re-validated
428
462
  * here. Optimystic refuses a value that is not a finite number above zero, or is above its
@@ -441,11 +475,14 @@ export interface NetworkConfig {
441
475
  * This is NOT a timeout. It is the one stated assumption that cadre's own dial and
442
476
  * reservation deadlines are DERIVED from, each by the number of round trips that operation
443
477
  * was measured to cost: a peer-join catch-up's dial to one peer, its push response, one relay
444
- * reservation drive, the control-cohort dial budgets, and libp2p's own `dialTimeout` and
445
- * `inboundUpgradeTimeout` on every node. Reaching another machine through a relay costs a
446
- * fixed number of exchanges, so a deadline written as milliseconds has a link speed above
447
- * which it can never open a connection — which is the defect this declaration exists to make
448
- * impossible to reintroduce one budget at a time. The counts, the measurement behind them,
478
+ * reservation drive, the control-cohort dial budgets, and the per-peer cohort read deadline
479
+ * ({@link cohortQueryTimeoutMs}, unless set explicitly). It is also handed to Optimystic
480
+ * (`NodeOptions.linkRoundTripMs`), default included, which derives its own deadlines from it:
481
+ * its request dials and responses, its block pushes, and libp2p's `dialTimeout` and
482
+ * `inboundUpgradeTimeout` on every node. Reaching another machine through a
483
+ * relay costs a fixed number of exchanges, so a deadline written as milliseconds has a link
484
+ * speed above which it can never open a connection — which is the defect this declaration
485
+ * exists to make impossible to reintroduce one budget at a time. The counts, the measurement behind them,
449
486
  * and what still fails at the supported link are in `link-budget.ts`.
450
487
  *
451
488
  * The default covers the slowest link sereus supports, a 3-second round trip through a relay.
@@ -459,7 +496,8 @@ export interface NetworkConfig {
459
496
  * Refused where the libp2p node is built — inside `CadreNode.start()` for the control
460
497
  * network, inside `CadreNode.addStrand` for a strand — if it is not a finite number above
461
498
  * zero, because every consumer multiplies it into a deadline where a zero means "give up at
462
- * once" and a `NaN` means "never".
499
+ * once" and a `NaN` means "never". Optimystic refuses, in the same place, a value above its
500
+ * `MAX_LINK_ROUND_TRIP_MS` (about 1.66 days).
463
501
  */
464
502
  linkRoundTripMs?: number;
465
503
  /**
@@ -530,14 +568,19 @@ export interface NetworkConfig {
530
568
  * {@link dialTimeoutMs}. Each address is dialed on its own under this limit,
531
569
  * so an address that never answers cannot use up the time the peer's other
532
570
  * addresses needed. Defaults to
533
- * {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}.
571
+ * {@link DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS}. A value given
572
+ * here replaces that whole derived budget, including its allowance for the
573
+ * called machine's admission decision (`relayedDialBudgetMs`).
534
574
  */
535
575
  perAddressDialTimeoutMs?: number;
536
576
  /**
537
- * How often each running strand re-resolves its siblings' strand-network
538
- * addresses into its own libp2p address book — a step of the reconcile pass,
539
- * on its own much longer throttle. Defaults to `STRAND_PEER_ADDR_REFRESH_MS`
540
- * (10 min); must stay well under the peerStore's one-hour address expiry.
577
+ * How long a running strand waits, after a sibling answers its strand-addr
578
+ * RPC, before asking that sibling again for its strand-network addresses — a
579
+ * step of the reconcile pass, scheduled per (sibling, strand). A sibling that
580
+ * did not answer is retried sooner, on `STRAND_PEER_ADDR_RETRY_MS` (or this
581
+ * interval, when it is set shorter). Defaults to
582
+ * `STRAND_PEER_ADDR_REFRESH_MS` (10 min); must stay well under the peerStore's
583
+ * one-hour address expiry.
541
584
  */
542
585
  strandAddrRefreshMs?: number;
543
586
  };
@@ -656,9 +699,10 @@ export interface ControlNetworkConfig {
656
699
  *
657
700
  * The two `*ClusterPolicy` BUILDERS are the same objects with the block-repair
658
701
  * corroboration yardstick declared from the machines enrolled in this party
659
- * ({@link resolveRepairYardstick}) and, if the host set one, its own per-peer read
660
- * deadline in place of {@link COHORT_READ_DEADLINE_MS}. Both arrive in one named
661
- * declarations object, because both are plain numbers meaning unrelated things;
702
+ * ({@link resolveRepairYardstick}) and, if the host set one or declared its link, a
703
+ * per-peer read deadline in place of {@link COHORT_READ_DEADLINE_MS}
704
+ * (`declaredCohortReadDeadlineMs` in `link-budget.ts` settles which). Both arrive in one
705
+ * named declarations object, because both are plain numbers meaning unrelated things;
662
706
  * declaring neither returns the frozen base constant unchanged. A network picks the
663
707
  * derived numbers up when its libp2p node is built, which for a strand is every wake
664
708
  * from hibernation.
@@ -722,6 +766,22 @@ export interface CadreNodeConfig {
722
766
  /** Which strands to participate in */
723
767
  strandFilter?: StrandFilter;
724
768
 
769
+ /**
770
+ * Run every strand {@link strandFilter} admits, including ones no app on this machine
771
+ * has claimed with `addStrand`, as a **storage replica**: the strand's own libp2p node,
772
+ * storage and `Strand` membership schema, storing and serving its blocks, without the
773
+ * app's schema (no sApp config needed, none of the app's code or schema runs). This is
774
+ * how an always-on machine keeps a copy of every shared workspace its party runs, so a
775
+ * lost phone loses nothing that had reached it.
776
+ *
777
+ * The strand is still announced as `strand:discovered` (once) and stays in
778
+ * `getDiscoveredStrands()` until an app claims it. A replica is always a joiner.
779
+ *
780
+ * Default: `profile === 'storage'` — always-on machines host replicas, phones
781
+ * (`'transaction'`) keep today's announce-only behaviour.
782
+ */
783
+ hostUnclaimedStrands?: boolean;
784
+
725
785
  /** Storage configuration (only for storage profile) */
726
786
  storage?: StorageConfig;
727
787
 
@@ -950,11 +1010,12 @@ export interface CadreNodeConfig {
950
1010
  };
951
1011
 
952
1012
  /**
953
- * Node-local record of the strands this node joined from ANOTHER party (see
954
- * `joined-strand-store.ts`): id, type and the closed strand's read secret, recorded
955
- * by `formStrand` and by a joining (not `founder: true`) `addStrand` whose row is
956
- * not in this party's control database, and re-offered on every start as `strand:discovered` — so an embedding
957
- * app keeps no list of its own.
1013
+ * Node-local record of the strands this node joined from ANOTHER party and has not yet
1014
+ * published to the party-wide `JoinedStrand` table (see `joined-strand-store.ts`): id,
1015
+ * type and the closed strand's read secret, recorded by `formStrand` and by a joining
1016
+ * (not `founder: true`) `addStrand` whose row this party's control database does not
1017
+ * name, offered on every start as `strand:discovered` until an owner machine's
1018
+ * reconcile pass publishes it — so an embedding app keeps no list of its own.
958
1019
  *
959
1020
  * Absent ⇒ a `KeyStoreJoinedStrandStore` over {@link keyStore} when one is
960
1021
  * configured (the record carries a secret, and the KeyStore is where every platform
@@ -1082,6 +1143,12 @@ export interface StrandInstance {
1082
1143
 
1083
1144
  /** Activity tracking */
1084
1145
  connectedPeers: number;
1146
+ /**
1147
+ * When the strand last saw activity: the app's (`recordStrandActivity`), a requested wake
1148
+ * (`wakeStrand`), or a peer delivering a gated joiner's `Strand.Header`. Always a fresh
1149
+ * `Date`. Bringing the runtime up is NOT activity — a check-in window decides whether to
1150
+ * re-hibernate by whether this changed since before its resume.
1151
+ */
1085
1152
  lastActivity: Date;
1086
1153
  nextCheckIn?: Date;
1087
1154
 
@@ -1508,6 +1575,12 @@ export interface CadreNodeEvents {
1508
1575
  * be idempotent (guard on an in-flight set, not only on
1509
1576
  * `getStrands().has(id)` — the strand manager tracks an instance only once
1510
1577
  * `addStrand` has resolved).
1578
+ *
1579
+ * On a node that hosts storage replicas ({@link CadreNodeConfig.hostUnclaimedStrands},
1580
+ * the default for `profile: 'storage'`) the event still fires once, and the node launches
1581
+ * the strand as a replica right after it — so there `getStrands().has(id)` soon turns
1582
+ * true for a strand no app has claimed; an unclaimed instance has no `sAppInfo`. A
1583
+ * launch that fails is retried by the watcher without a second `strand:discovered`.
1511
1584
  */
1512
1585
  'strand:discovered': { strandId: string; strand: StrandRow };
1513
1586
  'control:connected': void;
@@ -1886,7 +1959,11 @@ export interface WakeRequest {
1886
1959
  export interface WakeAck {
1887
1960
  /** Whether the receiver honored the wake (member + participated strand). */
1888
1961
  accepted: boolean;
1889
- /** The strand's status after the wake (present when `accepted`). */
1962
+ /**
1963
+ * The strand's status when the receiver accepted the wake (present when `accepted`). The
1964
+ * receiver replies before the wake runs, so `hibernating` or `idle` here means "a wake was
1965
+ * started", not "the strand is now up".
1966
+ */
1890
1967
  status?: StrandStatus;
1891
1968
  /** Reason for rejection (non-member, unknown strand, malformed frame). */
1892
1969
  reason?: string;
@@ -1915,17 +1992,32 @@ export interface StrandAddrRequest {
1915
1992
  delegatePeerId?: string;
1916
1993
  }
1917
1994
 
1995
+ /**
1996
+ * How a strand-addr responder handled a {@link StrandAddrRequest}. Carried so the
1997
+ * asker can tell "I have nothing" from "I could not answer" — the two need
1998
+ * different retry timing, and an empty address list alone cannot say which.
1999
+ *
2000
+ * - `ok` — looked up; `multiaddrs` is the truth, possibly empty (strand not running here).
2001
+ * - `unavailable` — the responder could not answer: over its concurrency cap, the
2002
+ * request was unreadable, or its own lookup threw (e.g. a control-database read failed).
2003
+ * - `refused` — the requester is not an authorized member in the responder's
2004
+ * current view (possibly because its `CadrePeer` row has not replicated there yet);
2005
+ * any `delegatePeerId` it carried was NOT recorded.
2006
+ */
2007
+ export type StrandAddrStatus = 'ok' | 'unavailable' | 'refused';
2008
+
1918
2009
  /**
1919
2010
  * Response to a {@link StrandAddrRequest}, carrying the responder's strand-node
1920
2011
  * multiaddrs, returned on the same stream.
1921
2012
  */
1922
2013
  export interface StrandAddrResponse {
1923
- /** Echoes the requested strand id (empty on a reject/error reply). */
2014
+ /** How the responder handled the request; a reply without one is malformed. */
2015
+ status: StrandAddrStatus;
2016
+ /** Echoes the requested strand id; always empty on an `unavailable` reply. */
1924
2017
  strandId: string;
1925
2018
  /**
1926
2019
  * Dialable strand-network multiaddr strings (signaling/`p2p-circuit` first);
1927
- * empty when the responder is a non-member, or does not currently run that
1928
- * strand, or the exchange failed.
2020
+ * empty unless `status` is `ok` and the responder runs that strand.
1929
2021
  */
1930
2022
  multiaddrs: string[];
1931
2023
  }