@serfab/cadre-core 1.7.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 (188) hide show
  1. package/README.md +8 -6
  2. package/dist/cadre-node.d.ts +527 -206
  3. package/dist/cadre-node.js +1084 -528
  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 +10 -10
  45. package/dist/index.js +25 -20
  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/node-local-snapshot.d.ts +6 -4
  59. package/dist/node-local-snapshot.js +7 -3
  60. package/dist/node-local-snapshot.js.map +1 -1
  61. package/dist/peer-dial.d.ts +24 -4
  62. package/dist/peer-dial.js +74 -5
  63. package/dist/peer-dial.js.map +1 -1
  64. package/dist/peer-join-backfill.d.ts +2 -10
  65. package/dist/peer-join-backfill.js +8 -8
  66. package/dist/peer-join-backfill.js.map +1 -1
  67. package/dist/push-fanout.d.ts +6 -0
  68. package/dist/push-fanout.js +6 -0
  69. package/dist/push-fanout.js.map +1 -1
  70. package/dist/relay-reservation.d.ts +3 -1
  71. package/dist/relay-reservation.js +3 -1
  72. package/dist/relay-reservation.js.map +1 -1
  73. package/dist/relay-server.d.ts +79 -0
  74. package/dist/relay-server.js +74 -0
  75. package/dist/relay-server.js.map +1 -0
  76. package/dist/seed-bootstrap.d.ts +57 -4
  77. package/dist/seed-bootstrap.js +107 -83
  78. package/dist/seed-bootstrap.js.map +1 -1
  79. package/dist/storage-scope.d.ts +33 -17
  80. package/dist/storage-scope.js +44 -23
  81. package/dist/storage-scope.js.map +1 -1
  82. package/dist/strand-addr-protocol.d.ts +58 -21
  83. package/dist/strand-addr-protocol.js +133 -85
  84. package/dist/strand-addr-protocol.js.map +1 -1
  85. package/dist/strand-database.d.ts +20 -2
  86. package/dist/strand-database.js +42 -5
  87. package/dist/strand-database.js.map +1 -1
  88. package/dist/strand-first-sync-gate.d.ts +59 -21
  89. package/dist/strand-first-sync-gate.js +58 -19
  90. package/dist/strand-first-sync-gate.js.map +1 -1
  91. package/dist/strand-formation-deadlines.d.ts +151 -0
  92. package/dist/strand-formation-deadlines.js +171 -0
  93. package/dist/strand-formation-deadlines.js.map +1 -0
  94. package/dist/strand-formation-manager.d.ts +63 -30
  95. package/dist/strand-formation-manager.js +67 -41
  96. package/dist/strand-formation-manager.js.map +1 -1
  97. package/dist/strand-formation-protocol.d.ts +45 -46
  98. package/dist/strand-formation-protocol.js +62 -126
  99. package/dist/strand-formation-protocol.js.map +1 -1
  100. package/dist/strand-fret-addrs.d.ts +54 -0
  101. package/dist/strand-fret-addrs.js +108 -0
  102. package/dist/strand-fret-addrs.js.map +1 -0
  103. package/dist/strand-instance-manager.d.ts +122 -44
  104. package/dist/strand-instance-manager.js +270 -114
  105. package/dist/strand-instance-manager.js.map +1 -1
  106. package/dist/strand-membership-writer.d.ts +6 -0
  107. package/dist/strand-membership-writer.js +11 -0
  108. package/dist/strand-membership-writer.js.map +1 -1
  109. package/dist/strand-network-state-file.d.ts +12 -0
  110. package/dist/strand-network-state-file.js +38 -0
  111. package/dist/strand-network-state-file.js.map +1 -0
  112. package/dist/strand-network-state.d.ts +111 -0
  113. package/dist/strand-network-state.js +191 -0
  114. package/dist/strand-network-state.js.map +1 -0
  115. package/dist/strand-revocation-enforcer.js +2 -1
  116. package/dist/strand-revocation-enforcer.js.map +1 -1
  117. package/dist/strand-solicitation.d.ts +36 -20
  118. package/dist/strand-solicitation.js +4 -4
  119. package/dist/strand-solicitation.js.map +1 -1
  120. package/dist/strand-wake-protocol.d.ts +80 -23
  121. package/dist/strand-wake-protocol.js +89 -64
  122. package/dist/strand-wake-protocol.js.map +1 -1
  123. package/dist/types.d.ts +148 -61
  124. package/dist/types.js +4 -3
  125. package/dist/types.js.map +1 -1
  126. package/package.json +11 -10
  127. package/src/cadre-node.ts +8400 -7776
  128. package/src/control-authorization.ts +2 -1
  129. package/src/control-cohort.ts +6 -0
  130. package/src/control-database.ts +122 -18
  131. package/src/control-formation-recorder.ts +354 -305
  132. package/src/control-read-retry.ts +14 -12
  133. package/src/control-schema.ts +67 -5
  134. package/src/control-stream.ts +25 -1
  135. package/src/control-write-retry.ts +13 -2
  136. package/src/delegate-admission.ts +23 -13
  137. package/src/diagnostics/connection-path.ts +0 -7
  138. package/src/formation-approval.ts +610 -604
  139. package/src/fs-atomic.ts +14 -4
  140. package/src/hibernation-manager.ts +638 -437
  141. package/src/index.ts +43 -65
  142. package/src/joined-strand-store.ts +247 -57
  143. package/src/key-store-file.ts +14 -5
  144. package/src/link-budget.ts +393 -74
  145. package/src/membership-connection-gater.ts +31 -45
  146. package/src/node-local-snapshot.ts +12 -7
  147. package/src/peer-dial.ts +81 -7
  148. package/src/peer-join-backfill.ts +10 -9
  149. package/src/push-fanout.ts +6 -0
  150. package/src/relay-reservation.ts +3 -1
  151. package/src/relay-server.ts +98 -0
  152. package/src/seed-bootstrap.ts +133 -91
  153. package/src/storage-scope.ts +44 -23
  154. package/src/strand-addr-protocol.ts +547 -458
  155. package/src/strand-database.ts +287 -242
  156. package/src/strand-first-sync-gate.ts +58 -19
  157. package/src/strand-formation-deadlines.ts +227 -0
  158. package/src/strand-formation-manager.ts +700 -647
  159. package/src/strand-formation-protocol.ts +106 -154
  160. package/src/strand-fret-addrs.ts +145 -0
  161. package/src/strand-instance-manager.ts +307 -140
  162. package/src/strand-membership-writer.ts +12 -0
  163. package/src/strand-network-state-file.ts +40 -0
  164. package/src/strand-network-state.ts +243 -0
  165. package/src/strand-revocation-enforcer.ts +2 -1
  166. package/src/strand-solicitation.ts +519 -500
  167. package/src/strand-wake-protocol.ts +401 -355
  168. package/src/types.ts +150 -61
  169. package/dist/strand-peer-book-file.d.ts +0 -12
  170. package/dist/strand-peer-book-file.js +0 -37
  171. package/dist/strand-peer-book-file.js.map +0 -1
  172. package/dist/strand-peer-book-protocol.d.ts +0 -159
  173. package/dist/strand-peer-book-protocol.js +0 -360
  174. package/dist/strand-peer-book-protocol.js.map +0 -1
  175. package/dist/strand-peer-book-swap.d.ts +0 -104
  176. package/dist/strand-peer-book-swap.js +0 -284
  177. package/dist/strand-peer-book-swap.js.map +0 -1
  178. package/dist/strand-peer-book.d.ts +0 -159
  179. package/dist/strand-peer-book.js +0 -354
  180. package/dist/strand-peer-book.js.map +0 -1
  181. package/dist/strand-peer-observer.d.ts +0 -88
  182. package/dist/strand-peer-observer.js +0 -180
  183. package/dist/strand-peer-observer.js.map +0 -1
  184. package/src/strand-peer-book-file.ts +0 -39
  185. package/src/strand-peer-book-protocol.ts +0 -463
  186. package/src/strand-peer-book-swap.ts +0 -338
  187. package/src/strand-peer-book.ts +0 -468
  188. package/src/strand-peer-observer.ts +0 -235
@@ -10,13 +10,38 @@
10
10
  * budget changes ONE declaration, and the next person to add a dial writes down its round-trip
11
11
  * count beside the arithmetic.
12
12
  *
13
+ * This module is the only place a new deadline over the link should be written, and in
14
+ * cadre-core's source `yarn lint` enforces it (`LINK_DEADLINE_GUARD` in `eslint.config.mjs`): a
15
+ * value named `…TIMEOUT_MS`, `…BUDGET_MS` or `…DEADLINE_MS` (or `…TimeoutMs` and the like) set
16
+ * to a number rather than a derivation fails, unless the line above it gives one of four
17
+ * reasons. `// eslint-disable-next-line no-restricted-syntax -- link-independent: <why>` keeps a
18
+ * deadline that never waits on the link; `-- link-bound, not yet derived: <ticket slug>` marks
19
+ * one that does and names the ticket that owns converting it. Grep for the second to list the
20
+ * deadlines this module does not yet cover. `-- cuts off by design: <why>; see
21
+ * docs/cadre-consistency.md → "Deadlines Over Optimystic's Reads and Commits"` keeps a deadline
22
+ * that does wait on the link, through an Optimystic read or commit, but whose value is what its
23
+ * caller can tolerate: it gives up on purpose and must not grow with the link. That doc section
24
+ * lists every such site. `-- measured, not derived: <where the measurement lives>` keeps a
25
+ * deadline that waits on the link but has no round-trip count to derive from, because it bounds
26
+ * a whole phase of many exchanges whose number depends on the data (a joining machine's first
27
+ * sync): its value is sized from a recorded measurement, which the directive names, and it is
28
+ * re-measured rather than recomputed when a number under it moves.
29
+ *
30
+ * Optimystic's per-peer cohort read deadline is derived here too ({@link cohortReadDeadlineMs}),
31
+ * but declared in another package: `COHORT_READ_DEADLINE_MS` in
32
+ * `quereus-plugin-sereus/src/cluster-size.ts` is the same arithmetic at the default declaration,
33
+ * spelled as a number because that package cannot import this one. `link-budget.spec.ts` pins
34
+ * the two equal.
35
+ *
13
36
  * ── The instrument ──
14
37
  *
15
- * Every count and every number below comes from
38
+ * Every count and every number below, except the commit count, comes from
16
39
  * `packages/integration-tests/src/scenarios/relayed-dial-cost-by-latency.integration.ts`, which
17
40
  * is committed and opt-in (`RELAY_DIAL_COST=1`). **Its doc comment is the single home of the
18
41
  * measurement**; this module holds only what the derivation needs. Re-run it before changing
19
- * anything here.
42
+ * anything here. The commit count ({@link COMMIT_ROUND_TRIPS}) is a whole-operation
43
+ * measurement from `relay-round-trip-measure.integration.ts` instead, and its figures live on
44
+ * the constant.
20
45
  *
21
46
  * ── The unit ──
22
47
  *
@@ -39,6 +64,15 @@
39
64
  * | dial the relay itself | 1 | 3 500 ms |
40
65
  * | request a reservation on an open relay connection | 1 | 3 500 ms |
41
66
  * | negotiate a protocol over an established circuit | 1 | 3 500 ms |
67
+ * | one request and its answer over an open circuit | 2 | 7 000 ms |
68
+ * | open a relayed connection, then negotiate one protocol| 5 | 17 500 ms |
69
+ * | open a relayed connection, then one request on it | 6 | 21 000 ms |
70
+ * | one Optimystic commit over connections already open | 20 | 70 000 ms |
71
+ *
72
+ * The reservation-request row is {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS}, which also sizes how long
73
+ * a party-run relay waits for a stranger's reservation ({@link relayAdmissionReserveDeadlineMs}).
74
+ * The commit row is a measurement of a whole write, not a count of exchanges; its doc comment
75
+ * ({@link COMMIT_ROUND_TRIPS}) holds the figures and says when to re-measure it.
42
76
  *
43
77
  * **Measured** 2026-09-26, one Windows machine, loopback dedicated relay: a relayed dial took
44
78
  * 20-25 ms at no delay, **7 255-7 279 ms at 900 ms one-way**, and **12 066-12 094 ms at 1 500 ms
@@ -48,40 +82,97 @@
48
82
  * supports rather than matching it exactly: a declaration equal to the supported round trip
49
83
  * would derive a budget marginally BELOW the dial it has to contain.
50
84
  *
51
- * ── libp2p's own two limits ──
52
- *
53
- * Two limits inside libp2p bound the same relayed dial as cadre's budgets do, and both were
54
- * 10 000 ms until `@optimystic/db-p2p` 1.7.0 let an embedder set them: the DIALER's
55
- * `connectionManager.dialTimeout`, which bounds every dial that carries no abort signal of its
56
- * own, and the LISTENER's `connectionManager.inboundUpgradeTimeout`, which is how long the
57
- * machine being called lets a half-built connection finish its handshakes. At 10 000 ms each,
58
- * no relayed connection could be opened above about 1 250 ms one-way (a 2.5-second link round
59
- * trip) whatever cadre declared. The listener's limit is the one that makes that failure
60
- * silent: at 1 500 ms one-way the dialer's own 12 094 ms dial RESOLVED while the listener had
61
- * already thrown the connection away at 10 s, so the first stream over it died with
62
- * `Unexpected EOF - stream closed while reading 0/1 bytes` and the listener reported no peer at
63
- * all. cadre now declares both from this module ({@link connectionManagerTimeouts}) on the
64
- * control node and every strand node.
85
+ * ── The listener's admission decision ──
86
+ *
87
+ * The table is link time only. Every budget that OPENS a connection also adds
88
+ * {@link ADMISSION_DECISION_TIMEOUT_MS} (2 000 ms) per admission decision the machine being
89
+ * called may make on the way, because that machine's gate runs on the dialer's clock (the
90
+ * constant's doc says why). So a relayed dial is budgeted at 14 000 + 2 000 = 16 000 ms at the
91
+ * default declaration, a dial plus one request at 23 000 ms, and a reservation drive, which a
92
+ * party-run relay decides twice, at 18 000 ms. The allowance is flat: it is local decision
93
+ * time, not link time, so it does not scale with the declaration.
94
+ *
95
+ * The measurement above crossed no gate. The instrument is bare libp2p on both sides, with no
96
+ * cadre connection gater, so the 12 094 ms contains no admission decision at all. The
97
+ * allowance is therefore on top of the measured dial rather than already inside it.
98
+ *
99
+ * NOTE: the measured dial also reused a connection to the relay that the dialer already held
100
+ * (the instrument dials the relay first). A relayed dial that has to open that connection itself
101
+ * spends one more link round trip, plus the relay's own admission decision when the relay is a
102
+ * party-run control node, and the count counts neither; at the supported link such a dial fits
103
+ * only while the called machine decides in under about 0.9 s.
104
+ * `bug-relayed-dial-budget-omits-opening-the-relay-connection`.
105
+ *
106
+ * ── Optimystic's deadlines, from the same declaration ──
107
+ *
108
+ * cadre states its declared link to Optimystic on the control node and every strand node
109
+ * (`@optimystic/db-p2p`'s `NodeOptions.linkRoundTripMs`, from 1.8.0), and Optimystic derives
110
+ * its network deadlines from it (`resolveLinkDeadlines` in its `rpc-deadline.ts`), each never
111
+ * below the LAN value it replaced. At the default declaration:
112
+ *
113
+ * | Optimystic deadline | link round trips | at the default | undeclared |
114
+ * | ---------------------------------------------------- | ---------------- | -------------- | ---------- |
115
+ * | libp2p `dialTimeout` and `inboundUpgradeTimeout` | 5 | 17 500 ms | 10 000 ms |
116
+ * | one request's dial, stream negotiation included | 6 | 21 000 ms | 3 000 ms |
117
+ * | one request's response | 3 | 10 500 ms | 10 000 ms |
118
+ * | a rebalance transfer's dial, and again its reply | 6, floor 30 000 | 30 000 ms | 30 000 ms |
119
+ *
120
+ * Its churn and under-replication pushes take the request rows' values. The undeclared column
121
+ * is why cadre always states the value, default included. A fixed 3 000 ms request dial cannot
122
+ * open a relayed connection above 375 ms one-way, and it also bounded the protocol negotiation
123
+ * on a connection that was already open, which is one link round trip: at the supported link
124
+ * every cohort consult was cut off at 3.0 s, before
125
+ * {@link cohortReadDeadlineMs} was reached (measured 2026-09-29, on 1.7.0). From 1.8.0 a cohort
126
+ * consult runs under the per-peer read deadline alone. Optimystic would derive that deadline
127
+ * from the declaration too, at three round trips; cadre states its own, at two
128
+ * ({@link cohortReadDeadlineMs} says why).
129
+ *
130
+ * NOTE: the longer deadlines are paid against a machine that is gone. An Optimystic request
131
+ * to a peer that never completes a connection now waits up to the 21 000 ms dial deadline where
132
+ * it waited 3 000, and one that connects and never answers waits 10 500 where it waited 10 000.
133
+ * Measured 2026-09-30 (`control-write-degraded-cohort-member.integration.ts`): a control write
134
+ * against a connected, silent member failed at 42.2 s, against 40.2 s with no link declared.
135
+ * The dial case was not measured. cadre's cut-off budgets (`CONTROL_WRITE_RETRY_BUDGET_MS`,
136
+ * `CONTROL_READ_RETRY_BUDGET_MS`) are checked between attempts, so one attempt can outlast
137
+ * them by that much. If writes on a fast link are seen waiting out the dial deadline against an
138
+ * unreachable machine, declare that deployment's real round trip instead of the default.
139
+ *
140
+ * libp2p's two limits bound the same relayed dial as cadre's budgets do: the DIALER's
141
+ * `dialTimeout`, which bounds every dial that carries no abort signal of its own, and the
142
+ * LISTENER's `inboundUpgradeTimeout`, which is how long the machine being called lets a
143
+ * half-built connection finish its handshakes. At libp2p's 10 000 ms, no relayed connection
144
+ * could be opened above about 1 250 ms one-way (a 2.5-second link round trip) whatever cadre
145
+ * declared, and the listener's limit is the one that makes that failure silent: at 1 500 ms
146
+ * one-way the dialer's own 12 094 ms dial RESOLVED while the listener had already thrown the
147
+ * connection away at 10 s, so the first stream over it died with `Unexpected EOF - stream
148
+ * closed while reading 0/1 bytes` and the listener reported no peer at all. Optimystic's
149
+ * derivation contains {@link relayedDialBudgetMs} at every declaration: five round trips are at
150
+ * least four plus the {@link ADMISSION_DECISION_TIMEOUT_MS} allowance once the round trip
151
+ * reaches 2 000 ms, and the 10 000 ms floor covers every shorter one. `link-budget.spec.ts` pins
152
+ * that against Optimystic's own derivation, so a change on either side that breaks it fails
153
+ * there rather than silently on a slow link.
154
+ *
155
+ * NOTE: derived from the declaration alone, so a per-field override that raises a cadre dial
156
+ * budget above it (`controlCohort.perAddressDialTimeoutMs`, `strandBackfill.dialTimeoutMs`)
157
+ * does not raise the listener's limit; a dial that outlasts it gets the silent failure again.
158
+ * If such overrides start being used for slow links, raise the declaration instead.
65
159
  *
66
160
  * ── What still fails at the supported link ──
67
161
  *
68
- * - **Optimystic's own request dials.** `@optimystic/db-p2p`'s RPC clients dial with fixed
69
- * 3 000 ms deadlines of their own (`DEFAULT_DIAL_TIMEOUT_MS`, the `pushDialTimeoutMs`
70
- * defaults), which neither limit above reaches because a caller's signal replaces
71
- * `dialTimeout`. So an Optimystic request that has to OPEN a relayed connection fails above
72
- * 375 ms one-way. A request over a connection that is already open does not dial, and the
73
- * connections cadre opens itself are budgeted here. Upstream:
74
- * `debt-rpc-dial-deadlines-cannot-open-a-slow-relayed-connection` in optimystic.
75
- * - **Cadre deadlines still typed as milliseconds.** The strand wake, strand-address and seed
76
- * delivery deadlines, and the cohort read deadline, bound exchanges over the same link but do
77
- * not derive from it: `tickets/backlog/debt-three-more-dial-deadlines-ignore-the-declared-link`.
78
162
  * - **A machine that declares a faster link than its peers.** Every machine is the listener
79
- * for the others, so its `inboundUpgradeTimeout` — derived from ITS declaration — bounds
80
- * connections other machines open to it. A peer declaring 3 500 ms dialing a machine that
81
- * declared 500 ms gets exactly the silent failure above. Declare the same link on every
82
- * machine of a party.
163
+ * for the others, so its `inboundUpgradeTimeout` — which Optimystic derives from ITS
164
+ * declaration — bounds connections other machines open to it. A peer declaring 3 500 ms
165
+ * dialing a machine that declared 500 ms gets exactly the silent failure above, once its
166
+ * dial outlasts that machine's 10 000 ms floor. Strand formation has the same
167
+ * shape: the joiner derives how long it waits for the host's reply from ITS declaration and
168
+ * the host derives its provisioning budget from its own, so a joiner declaring a faster
169
+ * link than the host gives up on a reply the host is still entitled to send. Declare the
170
+ * same link on every machine of a party.
171
+ * Relay admission too: a party-run relay derives how long a stranger has to ask for a
172
+ * reservation from ITS declaration ({@link relayAdmissionReserveDeadlineMs}), so a relay
173
+ * declaring a faster link than a client closes that client's connection before its request
174
+ * arrives.
83
175
  */
84
- import type { Libp2pConnectionTimeouts } from '@optimystic/db-p2p';
85
176
  /**
86
177
  * The link round trip cadre assumes when a host declares none, in milliseconds. A deployment
87
178
  * that knows its own link states it with `NetworkConfig.linkRoundTripMs` instead.
@@ -91,18 +182,22 @@ import type { Libp2pConnectionTimeouts } from '@optimystic/db-p2p';
91
182
  * `docs/architecture.md` → "Relay Integration"). The worst relayed dial measured at that link
92
183
  * was 12 094 ms, above the 12 000 ms a declaration of exactly 3 000 would derive, so the
93
184
  * declaration rounds the supported link up to the next half second. 3 500 derives 14 000 ms
94
- * and leaves about 1.9 s over that dial: room for a phone's Noise handshake, which the
95
- * loopback measurement on a desktop does not contain. (The previous 2 000 followed the same
96
- * convention for the 1.8 s band sereus supported before, with about 720 ms to spare.)
185
+ * of link time and leaves about 1.9 s over that dial: room for a phone's Noise handshake,
186
+ * which the loopback measurement on a desktop does not contain. The listener's admission
187
+ * decision is not in that 1.9 s; it has its own allowance on top (the module doc's "The
188
+ * listener's admission decision"). (The previous 2 000 followed the same convention for the
189
+ * 1.8 s band sereus supported before, with about 720 ms to spare.)
97
190
  *
98
- * Two budgets outside this module were sized against the 1.8 s band and do NOT move with it:
99
- * `COHORT_READ_DEADLINE_MS` (`quereus-plugin-sereus/src/cluster-size.ts`, 5 000 ms) and
100
- * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` (`strand-first-sync-gate.ts`). The module doc's
101
- * "What still fails" list carries the first.
191
+ * One budget outside this module does NOT move with it: `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS`
192
+ * (`strand-first-sync-gate.ts`) bounds a whole first sync, which has no round-trip count, so it
193
+ * is sized from a measurement instead and re-measured when this declaration or the cohort read
194
+ * deadline moves. The per-peer cohort read deadline itself does move with it
195
+ * ({@link cohortReadDeadlineMs}).
102
196
  *
103
197
  * What 3 500 costs, against 2 000: a peer that is genuinely gone holds each operation longer
104
- * before it is abandoned and retried — a relayed dial or a reservation drive 14 s instead of
105
- * 8 s, and a control-cohort dial of one peer across all its addresses 56 s instead of 32 s.
198
+ * before it is abandoned and retried — the link part of a relayed dial or a reservation drive
199
+ * 14 s instead of 8 s, and of a control-cohort dial of one peer across all its addresses 56 s
200
+ * instead of 32 s. The admission allowance adds to both (see {@link relayedDialBudgetMs}).
106
201
  * Raise it further for a link slower still; lower it only on a deployment where EVERY machine
107
202
  * of the party is that close, for the reason the "What still fails" list gives.
108
203
  */
@@ -125,14 +220,71 @@ export declare const RELAYED_DIAL_ROUND_TRIPS = 4;
125
220
  * (`DEFAULT_RELAY_RESERVE_POLL_MS`) rounds up on top of it.
126
221
  */
127
222
  export declare const RELAY_RESERVATION_ROUND_TRIPS = 4;
223
+ /**
224
+ * Link round trips from the moment a party-run relay admits a stranger's connection for relay
225
+ * use to the moment that stranger's RESERVE request reaches it. libp2p's reservation store
226
+ * (`@libp2p/circuit-relay-v2`'s `addRelay`) opens the hop stream the instant its dial resolves,
227
+ * without waiting for identify, so:
228
+ *
229
+ * 1. The client's last Noise handshake message and its hop-stream open arrive at the relay
230
+ * together. The relay's gate runs on the handshake message (and arms the reserve deadline),
231
+ * then the relay answers the protocol negotiation.
232
+ * 2. The answer reaches the client and the client's RESERVE request reaches the relay: one round
233
+ * trip on the client-to-relay hop, which is the whole link round trip when all of the link's
234
+ * delay is on the client's own hop (a phone on a congested mobile link).
235
+ *
236
+ * The relay's decision on that request is local time, not link time;
237
+ * {@link relayAdmissionReserveDeadlineMs} adds it separately.
238
+ */
239
+ export declare const RELAY_RESERVE_REQUEST_ROUND_TRIPS = 1;
240
+ /**
241
+ * Link round trips negotiating one protocol over an ALREADY-OPEN connection costs: the
242
+ * multistream-select exchange that `dialProtocol` / `newStream` runs before the stream is
243
+ * handed back. Measured at 2 one-way delays.
244
+ */
245
+ export declare const PROTOCOL_NEGOTIATION_ROUND_TRIPS = 1;
128
246
  /**
129
247
  * Link round trips one request-and-answer over an ALREADY-OPEN circuit costs: the protocol
130
- * negotiation (1, measured at 2 one-way delays) plus the request and its response (1). It does
131
- * not include a dial — a caller that may have to open the connection budgets
248
+ * negotiation ({@link PROTOCOL_NEGOTIATION_ROUND_TRIPS}) plus the request and its response (1).
249
+ * It does not include a dial — a caller that may have to open the connection budgets
132
250
  * {@link RELAYED_DIAL_ROUND_TRIPS} separately, which is exactly what the two-field shape of
133
251
  * Optimystic's `dialTimeoutMs` / `responseTimeoutMs` pair is for.
134
252
  */
135
- export declare const CIRCUIT_REQUEST_ROUND_TRIPS = 2;
253
+ export declare const CIRCUIT_REQUEST_ROUND_TRIPS: number;
254
+ /**
255
+ * Link round trips one Optimystic commit is budgeted for, over connections that are ALREADY
256
+ * open. **Measured, not counted**: a commit is many sequential exchanges whose exact number
257
+ * the transactor decides, so this is a whole-operation figure and it depends on the Optimystic
258
+ * version.
259
+ *
260
+ * Measured 2026-09-23 at optimystic `9e5c1e85` with
261
+ * `packages/integration-tests/src/scenarios/relay-round-trip-measure.integration.ts`,
262
+ * `delayed` configuration (`RELAY_RRT_MEASURE=1 RELAY_RRT_CONFIG=delayed`: a 150 ms one-way
263
+ * delay on one party's link, so a 300 ms link round trip): one strand insert took 3.8–6.1 s
264
+ * (the founder 4.46, 4.43, 3.84 s; the joiner 5.11, 5.09, 6.09 s), which is up to 20.3 link
265
+ * round trips, including about 0.1–0.2 s of local work (the same inserts on an undelayed
266
+ * loopback took 57–165 ms). A static count of the write path agrees on the order: three repo
267
+ * messages through three consensus rounds each, nine sequential requests of a negotiation plus
268
+ * a request, so 18.
269
+ *
270
+ * Re-measured 2026-09-29 on `@optimystic/*` 1.8.0, same scenario and configuration: the founder
271
+ * 3.26-3.94 s and the joiner 4.45-5.17 s over three inserts each, so at most 17.2 link round
272
+ * trips. The count stands.
273
+ *
274
+ * It measures a strand insert; a control-database insert runs the same network transactor
275
+ * against the responder's own party's machines, which the module doc already says must
276
+ * declare the same link. Re-measure with that scenario, rather than recompute, whenever
277
+ * `@optimystic/*` is bumped.
278
+ */
279
+ export declare const COMMIT_ROUND_TRIPS = 20;
280
+ /**
281
+ * Link round trips one request-and-answer costs when the connection may first have to be OPENED,
282
+ * possibly through a relay: {@link RELAYED_DIAL_ROUND_TRIPS} + {@link CIRCUIT_REQUEST_ROUND_TRIPS}.
283
+ * The shape of cadre's own one-frame control protocols (strand wake, strand address, seed
284
+ * delivery), each of which bounds the dial and the exchange with ONE deadline. Link time only:
285
+ * {@link relayedRequestBudgetMs} adds the listener's admission allowance on top.
286
+ */
287
+ export declare const RELAYED_REQUEST_ROUND_TRIPS: number;
136
288
  /**
137
289
  * What a block-transfer push message's BYTES get to cross, on top of the latency
138
290
  * {@link CIRCUIT_REQUEST_ROUND_TRIPS} covers, in milliseconds.
@@ -149,6 +301,58 @@ export declare const CIRCUIT_REQUEST_ROUND_TRIPS = 2;
149
301
  * not the declared round trip.
150
302
  */
151
303
  export declare const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
304
+ /**
305
+ * Deadline for one admission decision, after which the fail-open outcome is
306
+ * used (connection admitted / reservation admitted). Both inbound gaters use
307
+ * it: the control node's membership gate (`membership-connection-gater.ts`),
308
+ * and the closed-strand revoked-peer gate (`strand-revocation-enforcer.ts`).
309
+ * It lives here, not beside the gaters, because the dial budgets below add it
310
+ * and this module imports nothing of cadre-core's own: importing the gater
311
+ * would close the cycle link-budget → membership-connection-gater →
312
+ * seed-bootstrap → link-budget.
313
+ *
314
+ * libp2p awaits `denyInboundEncryptedConnection` inside the inbound upgrade
315
+ * WITHOUT racing its inbound-upgrade timeout signal (unlike the pre-encryption
316
+ * `denyInboundConnection` hook), so a decision that never settles wedges that
317
+ * upgrade forever — the connection-manager's inbound-upgrade slot is taken by
318
+ * `acceptIncomingConnection` and released in the `finally` that never runs. The
319
+ * real policy reads the control DB (`listAuthorizedMembers`), which can pull
320
+ * over the network, so "never settles" is reachable. Bounding it keeps the
321
+ * fail-open contract honest: a slow decision admits rather than silently
322
+ * failing closed (or not at all).
323
+ *
324
+ * The control gate's decision waits on the link, through two live control reads
325
+ * (`Revocation`, then `CadrePeer`) that can consult the cohort, yet this deadline
326
+ * cuts it off on purpose and must not grow with the link:
327
+ *
328
+ * - **Fail-open is the designed outcome.** An expired decision admits a
329
+ * connection and nothing more: the per-protocol stream gates still refuse
330
+ * every members-only protocol from the materialized snapshot, and unplaced
331
+ * relay reservations stay capped (`MAX_UNAUTHORIZED_RELAY_RESERVATIONS`).
332
+ * - **The slow case is bring-up, not steady state.** A membership read consults
333
+ * the cohort only for a block this node does not hold, or before the
334
+ * `Revocation` ledger marker exists; once the marker exists every block the
335
+ * decision reads is held (`control-founding-consult-budget.spec.ts` pins both
336
+ * states). So the gate always fails open only on a node whose membership reads
337
+ * still consult: one consult costs about two link round trips, and asking a
338
+ * silent peer costs the per-peer read deadline ({@link cohortReadDeadlineMs}, 7 s
339
+ * at the default declaration).
340
+ * - **The decision is spent on the dialing machine's clock.** libp2p's listener
341
+ * runs this gate before it answers the multiplexer negotiation the dialer is
342
+ * waiting on (`libp2p/dist/src/upgrader.js`), and the listener's own
343
+ * `inboundUpgradeTimeout` is already running while it does. So
344
+ * {@link relayedDialBudgetMs}, and every budget built on it, adds this
345
+ * deadline once per decision the called machine may make, and the two libp2p
346
+ * limits Optimystic derives from the same declaration contain it (the module
347
+ * doc's "Optimystic's deadlines, from the same declaration").
348
+ * Raising it therefore lengthens every dial budget with it: a slow decision
349
+ * still fits, and a dial to a peer that is gone takes that much longer to
350
+ * give up.
351
+ *
352
+ * `CONTROL_READ_RETRY_BUDGET_MS` must stay below this, which
353
+ * `control-read-retry.spec.ts` pins.
354
+ */
355
+ export declare const ADMISSION_DECISION_TIMEOUT_MS = 2000;
152
356
  /**
153
357
  * Resolve a host's declared link round trip, falling back to
154
358
  * {@link DECLARED_LINK_ROUND_TRIP_MS}.
@@ -168,46 +372,140 @@ export declare const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
168
372
  export declare function resolveLinkRoundTripMs(linkRoundTripMs?: number): number;
169
373
  /**
170
374
  * Deadline for OPENING a connection to another machine that may only be reachable through a
171
- * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip.
375
+ * relay: {@link RELAYED_DIAL_ROUND_TRIPS} at the declared link round trip, plus one
376
+ * {@link ADMISSION_DECISION_TIMEOUT_MS} for the called machine's inbound gate, which runs on
377
+ * this dial's clock. 4 x 3 500 + 2 000 = 16 000 ms at the default declaration.
378
+ *
379
+ * What the allowance costs: a peer that is truly gone holds each dial 2 s longer before it is
380
+ * abandoned — a per-address dial 16 s instead of 14 s, a control-cohort dial of one peer across
381
+ * its 4 addresses 64 s instead of 56 s, and a wake call's two attempts 46 s instead of 42 s. A
382
+ * machine that runs no gate (the dedicated relay container, an open-strand node) gets the same
383
+ * budget; the extra 2 s only lengthens a failure there, never a success.
384
+ *
385
+ * A per-field override of a deadline derived from this one
386
+ * (`network.controlCohort.perAddressDialTimeoutMs`, `strandBackfill.dialTimeoutMs`,
387
+ * `DialWakeOptions.timeoutMs`, …) replaces the whole derived value, allowance included.
172
388
  */
173
389
  export declare function relayedDialBudgetMs(linkRoundTripMs?: number): number;
174
390
  /**
175
- * libp2p's two connection-manager limits for a node at the declared link, handed to
176
- * `@optimystic/db-p2p`'s `NodeOptions.connectionManager` on the control node and every strand
177
- * node.
178
- *
179
- * Both bound the same thing — opening a relayed connection, measured at 8 one-way link delays,
180
- * which is {@link RELAYED_DIAL_ROUND_TRIPS} (4) link round trips — so both are
181
- * {@link relayedDialBudgetMs}: 4 x 3 500 = 14 000 ms at the default declaration, covering the
182
- * 12 094 ms measured at the supported 3-second link.
183
- *
184
- * The listener's limit is deliberately NOT smaller than the dialer's. The listener's clock
185
- * starts only when the relay hands it the circuit, a few one-way delays after the dialer's
186
- * started, and both sides finish the handshakes at about the same moment; so with equal limits
187
- * the listener never discards a connection that the dialer's own dial would still accept. A
188
- * listener limit below the dialer's is what produced the silent failure in the module doc.
189
- *
190
- * The cost of a longer `inboundUpgradeTimeout` is that a peer which opens a connection and then
191
- * stalls its handshake holds that half-built connection 14 s instead of 10 s.
192
- *
193
- * NOTE: derived from the declaration alone, so a per-field override that raises a cadre dial
194
- * budget above it (`controlCohort.perAddressDialTimeoutMs`, `strandBackfill.dialTimeoutMs`)
195
- * does not raise these; a dial that outlasts the listener's limit gets the silent failure
196
- * again. If such overrides start being used for slow links, raise the declaration instead or
197
- * take the largest configured dial budget here.
391
+ * Deadline for what `dialProtocol` does end to end: OPEN a possibly relayed connection
392
+ * ({@link relayedDialBudgetMs}, admission allowance included) and then NEGOTIATE one protocol
393
+ * on it ({@link PROTOCOL_NEGOTIATION_ROUND_TRIPS} at the declared link round trip). 16 000 +
394
+ * 3 500 = 19 500 ms at the default declaration. The strand formation dial is bounded by this;
395
+ * the one-frame control exchanges bound the negotiation together with their request instead
396
+ * ({@link relayedRequestBudgetMs}).
397
+ */
398
+ export declare function relayedStreamOpenBudgetMs(linkRoundTripMs?: number): number;
399
+ /**
400
+ * Deadline for one Optimystic commit over connections that are already open:
401
+ * {@link COMMIT_ROUND_TRIPS} at the declared link round trip, 70 000 ms at the default
402
+ * declaration. No admission allowance: a commit dials nothing, so no called machine decides
403
+ * on its clock.
198
404
  */
199
- export declare function connectionManagerTimeouts(linkRoundTripMs?: number): Libp2pConnectionTimeouts;
405
+ export declare function commitBudgetMs(linkRoundTripMs?: number): number;
200
406
  /**
201
407
  * Deadline for one whole relay reservation drive: {@link RELAY_RESERVATION_ROUND_TRIPS} at the
202
- * declared link round trip.
408
+ * declared link round trip, plus two {@link ADMISSION_DECISION_TIMEOUT_MS}.
409
+ * 4 x 3 500 + 2 x 2 000 = 18 000 ms at the default declaration.
410
+ *
411
+ * Two, because a party-run relay is a control node and decides twice on the drive's clock:
412
+ * once for the connection (`denyInboundEncryptedConnection`), then again for the reservation
413
+ * (`denyInboundRelayReservation`), each under the same deadline. The count's slack of 2 round
414
+ * trips over the measured protocol work happens to cover both at the default declaration, but
415
+ * not at a declared round trip under 2 000 ms; counting them makes the budget hold at every
416
+ * declaration.
203
417
  */
204
418
  export declare function relayReservationBudgetMs(linkRoundTripMs?: number): number;
419
+ /**
420
+ * How long a party-run relay keeps a connection it admitted only for relay use
421
+ * (`membership-connection-gater.ts`'s `'admit-for-relay'`) before closing it, unless a
422
+ * reservation is admitted on it first: {@link RELAY_RESERVE_REQUEST_ROUND_TRIPS} at the declared
423
+ * link round trip, plus one {@link ADMISSION_DECISION_TIMEOUT_MS} for the relay's own decision on
424
+ * the reservation (`denyInboundRelayReservation`), which disarms the timer only once it settles.
425
+ * 3 500 + 2 000 = 5 500 ms at the default declaration.
426
+ *
427
+ * The declaration is the RELAY's own, because the relay is the machine that decides.
428
+ *
429
+ * What it costs, against the fixed 5 000 ms it replaced: a stranger that connects and never
430
+ * reserves holds a mute connection 0.5 s longer at the default declaration.
431
+ */
432
+ export declare function relayAdmissionReserveDeadlineMs(linkRoundTripMs?: number): number;
205
433
  /**
206
434
  * Deadline for one request and its answer over a circuit that is already open —
207
435
  * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip, plus `transferAllowanceMs`
208
436
  * for the payload's own bytes.
209
437
  */
210
438
  export declare function circuitRequestBudgetMs(linkRoundTripMs?: number, transferAllowanceMs?: number): number;
439
+ /**
440
+ * Deadline for opening a (possibly relayed) connection and completing one small request on it:
441
+ * {@link relayedDialBudgetMs}, admission allowance included, plus
442
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip — so
443
+ * {@link RELAYED_REQUEST_ROUND_TRIPS} of link time in all. 16 000 + 2 x 3 500 = 23 000 ms at the
444
+ * default declaration.
445
+ *
446
+ * No transfer allowance, unlike {@link circuitRequestBudgetMs}: the requests this bounds are a few
447
+ * hundred bytes to a few KB, so their bytes cost nothing a round trip does not already cover.
448
+ */
449
+ export declare function relayedRequestBudgetMs(linkRoundTripMs?: number): number;
450
+ /**
451
+ * How long ONE cohort peer gets to answer ONE read-path request — Optimystic's
452
+ * `clusterPolicy.cohortQueryTimeoutMs`, which bounds the latest-revision query a read runs when
453
+ * its local copy may be stale and the archive fetch that follows it.
454
+ *
455
+ * {@link CIRCUIT_REQUEST_ROUND_TRIPS} at the declared link round trip: 2 x 3 500 = 7 000 ms at
456
+ * the default declaration. A read-path request opens a fresh protocol stream over a connection
457
+ * that is already open (`@optimystic/db-p2p`'s `openProtocolStream`), so it costs the protocol
458
+ * negotiation plus the request and its answer, and no dial. No admission allowance either: the
459
+ * called machine decided that when the connection was opened. From `@optimystic/db-p2p` 1.8.0
460
+ * this is the only limit on a consult; before it, a fixed 3 000 ms request dial deadline also
461
+ * bounded the negotiation and cut every consult off first at the supported link.
462
+ *
463
+ * Measured at the supported link on 1.8.0 (2026-09-29, 1 500 ms one-way,
464
+ * `strand-reattach-first-sync-measure.integration.ts`): every consult on a joining machine
465
+ * completed, 6.03-6.05 s after the one before it, which is the two round trips and about 40 ms of
466
+ * work, inside this deadline with about 0.95 s to spare.
467
+ *
468
+ * Stated to Optimystic explicitly rather than left to its own derivation from the same
469
+ * declaration, which counts three round trips (10 500 ms at the default declaration). The
470
+ * headroom over the supported link is already in the declaration (3 500 over 3 000), and the
471
+ * third round trip would lengthen every consult against a peer that is gone, which a joining
472
+ * machine's first sync pays several times over. The plugin's frozen policies have to state it
473
+ * anyway: a host that embeds the plugin without cadre-core declares no link to Optimystic.
474
+ *
475
+ * Optimystic derives its whole-reconcile-pass bound from this value, `max(5 000, 5 x per-peer)`
476
+ * (`db-p2p/src/cluster/cluster-policy.ts`): 35 000 ms at the default declaration.
477
+ *
478
+ * What it costs, against the 5 000 ms that preceded it: a peer that is truly gone holds a read
479
+ * of a block missing locally 7 s instead of 5 s before the read is declined and retried, and a
480
+ * joining machine's first sync runs several such consults. That is why
481
+ * `DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS` is re-measured whenever this moves; the bands are on
482
+ * that constant.
483
+ *
484
+ * The plugin declares the same number as `COHORT_READ_DEADLINE_MS` (it cannot import this
485
+ * module), and `link-budget.spec.ts` pins the two equal.
486
+ */
487
+ export declare function cohortReadDeadlineMs(linkRoundTripMs?: number): number;
488
+ /** The two `NetworkConfig` fields the per-peer cohort read deadline is settled from. */
489
+ export interface DeclaredReadDeadline {
490
+ /** The host's own per-peer read deadline, which wins over the link derivation. */
491
+ cohortQueryTimeoutMs?: number;
492
+ /** The host's declared link round trip, from which the deadline is derived when no explicit one is set. */
493
+ linkRoundTripMs?: number;
494
+ }
495
+ /**
496
+ * The per-peer cohort read deadline to declare onto a network's cluster policy, or `undefined`
497
+ * for "declare nothing and take the plugin's frozen policy whole". One helper for both policy
498
+ * construction sites (`CadreNode`'s control policy and `StrandInstanceManager`'s strand policy),
499
+ * because the two networks ride one link and must get the same value.
500
+ *
501
+ * An explicit `cohortQueryTimeoutMs` wins over the derivation, so a host that set the deadline
502
+ * by hand keeps it whatever link it declared. With a declared link and no explicit deadline,
503
+ * the deadline is {@link cohortReadDeadlineMs} at that link. With neither, `undefined`: the
504
+ * plugin's frozen policy already carries `COHORT_READ_DEADLINE_MS`, which equals the derivation
505
+ * at the default declaration, and returning `undefined` lets the builders return that frozen
506
+ * object by identity, which existing specs pin.
507
+ */
508
+ export declare function declaredCohortReadDeadlineMs(network?: DeclaredReadDeadline): number | undefined;
211
509
  /** The two deadlines one peer-join catch-up push needs, both derived from the declared link. */
212
510
  export interface PeerJoinPushBudget {
213
511
  /** Opening the connection to the peer being caught up — a relayed dial, in the worst case. */