@serfab/cadre-core 0.13.0 → 1.0.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 (131) hide show
  1. package/README.md +20 -6
  2. package/dist/bootstrap-peer-store-file.js +2 -2
  3. package/dist/bootstrap-peer-store.d.ts +32 -12
  4. package/dist/bootstrap-peer-store.js +41 -15
  5. package/dist/bootstrap-peer-store.js.map +1 -1
  6. package/dist/cadre-node.d.ts +558 -80
  7. package/dist/cadre-node.js +1060 -170
  8. package/dist/cadre-node.js.map +1 -1
  9. package/dist/control-authorization.d.ts +2 -2
  10. package/dist/control-authorization.js +1 -0
  11. package/dist/control-authorization.js.map +1 -1
  12. package/dist/control-cohort.d.ts +13 -26
  13. package/dist/control-cohort.js +0 -26
  14. package/dist/control-cohort.js.map +1 -1
  15. package/dist/control-database.d.ts +299 -44
  16. package/dist/control-database.js +469 -69
  17. package/dist/control-database.js.map +1 -1
  18. package/dist/control-read-retry.d.ts +40 -11
  19. package/dist/control-read-retry.js +48 -15
  20. package/dist/control-read-retry.js.map +1 -1
  21. package/dist/control-retry.d.ts +58 -3
  22. package/dist/control-retry.js +58 -3
  23. package/dist/control-retry.js.map +1 -1
  24. package/dist/control-schema.d.ts +1 -1
  25. package/dist/control-schema.js +103 -19
  26. package/dist/control-schema.js.map +1 -1
  27. package/dist/control-write-retry.d.ts +80 -11
  28. package/dist/control-write-retry.js +168 -29
  29. package/dist/control-write-retry.js.map +1 -1
  30. package/dist/enrolled-machine-store.js +2 -2
  31. package/dist/index.d.ts +14 -7
  32. package/dist/index.js +33 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/node-local-snapshot.d.ts +15 -7
  35. package/dist/node-local-snapshot.js +23 -8
  36. package/dist/node-local-snapshot.js.map +1 -1
  37. package/dist/peer-dial.d.ts +143 -0
  38. package/dist/peer-dial.js +176 -0
  39. package/dist/peer-dial.js.map +1 -0
  40. package/dist/relay-addrs.d.ts +90 -52
  41. package/dist/relay-addrs.js +91 -59
  42. package/dist/relay-addrs.js.map +1 -1
  43. package/dist/relay-reservation.d.ts +71 -3
  44. package/dist/relay-reservation.js +116 -8
  45. package/dist/relay-reservation.js.map +1 -1
  46. package/dist/seed-bootstrap.d.ts +30 -1
  47. package/dist/seed-bootstrap.js +62 -17
  48. package/dist/seed-bootstrap.js.map +1 -1
  49. package/dist/storage-scope.d.ts +62 -0
  50. package/dist/storage-scope.js +75 -0
  51. package/dist/storage-scope.js.map +1 -0
  52. package/dist/strand-database.d.ts +34 -16
  53. package/dist/strand-database.js +38 -24
  54. package/dist/strand-database.js.map +1 -1
  55. package/dist/strand-first-sync-gate.d.ts +152 -0
  56. package/dist/strand-first-sync-gate.js +208 -0
  57. package/dist/strand-first-sync-gate.js.map +1 -0
  58. package/dist/strand-formation-manager.d.ts +87 -5
  59. package/dist/strand-formation-manager.js +109 -5
  60. package/dist/strand-formation-manager.js.map +1 -1
  61. package/dist/strand-formation-protocol.d.ts +21 -1
  62. package/dist/strand-formation-protocol.js +30 -0
  63. package/dist/strand-formation-protocol.js.map +1 -1
  64. package/dist/strand-instance-manager.d.ts +243 -6
  65. package/dist/strand-instance-manager.js +470 -37
  66. package/dist/strand-instance-manager.js.map +1 -1
  67. package/dist/strand-member-key.d.ts +20 -12
  68. package/dist/strand-member-key.js +20 -12
  69. package/dist/strand-member-key.js.map +1 -1
  70. package/dist/strand-member-registry.js +3 -0
  71. package/dist/strand-member-registry.js.map +1 -1
  72. package/dist/strand-membership-reconciler.d.ts +361 -0
  73. package/dist/strand-membership-reconciler.js +548 -0
  74. package/dist/strand-membership-reconciler.js.map +1 -0
  75. package/dist/strand-membership-writer.d.ts +131 -11
  76. package/dist/strand-membership-writer.js +396 -134
  77. package/dist/strand-membership-writer.js.map +1 -1
  78. package/dist/strand-network-config.d.ts +46 -14
  79. package/dist/strand-network-config.js +79 -23
  80. package/dist/strand-network-config.js.map +1 -1
  81. package/dist/strand-solicitation.d.ts +10 -1
  82. package/dist/strand-solicitation.js +5 -0
  83. package/dist/strand-solicitation.js.map +1 -1
  84. package/dist/strand-wake-protocol.d.ts +11 -7
  85. package/dist/strand-wake-protocol.js +20 -52
  86. package/dist/strand-wake-protocol.js.map +1 -1
  87. package/dist/strand-watcher.js +6 -0
  88. package/dist/strand-watcher.js.map +1 -1
  89. package/dist/timed-step.d.ts +12 -0
  90. package/dist/timed-step.js +28 -0
  91. package/dist/timed-step.js.map +1 -0
  92. package/dist/timeout-scheduler.d.ts +19 -0
  93. package/dist/timeout-scheduler.js +22 -0
  94. package/dist/timeout-scheduler.js.map +1 -0
  95. package/dist/types.d.ts +262 -30
  96. package/dist/types.js.map +1 -1
  97. package/package.json +8 -8
  98. package/src/bootstrap-peer-store-file.ts +2 -2
  99. package/src/bootstrap-peer-store.ts +69 -27
  100. package/src/cadre-node.ts +7301 -6353
  101. package/src/control-authorization.ts +2 -1
  102. package/src/control-cohort.ts +122 -130
  103. package/src/control-database.ts +537 -69
  104. package/src/control-read-retry.ts +202 -167
  105. package/src/control-retry.ts +297 -190
  106. package/src/control-schema.ts +103 -19
  107. package/src/control-write-retry.ts +525 -383
  108. package/src/enrolled-machine-store.ts +2 -2
  109. package/src/index.ts +83 -6
  110. package/src/node-local-snapshot.ts +25 -8
  111. package/src/peer-dial.ts +215 -0
  112. package/src/relay-addrs.ts +458 -436
  113. package/src/relay-reservation.ts +951 -828
  114. package/src/seed-bootstrap.ts +76 -18
  115. package/src/storage-scope.ts +78 -0
  116. package/src/strand-database.ts +50 -27
  117. package/src/strand-first-sync-gate.ts +258 -0
  118. package/src/strand-formation-manager.ts +140 -5
  119. package/src/strand-formation-protocol.ts +43 -1
  120. package/src/strand-instance-manager.ts +577 -37
  121. package/src/strand-member-key.ts +20 -12
  122. package/src/strand-member-registry.ts +3 -0
  123. package/src/strand-membership-reconciler.ts +680 -0
  124. package/src/strand-membership-writer.ts +527 -189
  125. package/src/strand-network-config.ts +102 -24
  126. package/src/strand-solicitation.ts +16 -1
  127. package/src/strand-wake-protocol.ts +21 -66
  128. package/src/strand-watcher.ts +6 -0
  129. package/src/timed-step.ts +28 -0
  130. package/src/timeout-scheduler.ts +28 -0
  131. package/src/types.ts +1789 -1554
@@ -14,8 +14,19 @@ import type { ControlRetryOptions } from './control-retry.js';
14
14
  * a moment later, which the degraded-cohort-member scenario measured as safe (a failed
15
15
  * write rolls back, nothing half-commits) and effective (the next write commits in ~1 s).
16
16
  *
17
- * A write somebody actually REJECTED is never retried — re-presenting it would re-present
18
- * a spent signature against a cohort that already said no.
17
+ * A write somebody actually REJECTED is never retried on the strength of the rejection itself —
18
+ * no matcher here claims a rejection message, at any phase, because re-presenting one would
19
+ * re-present a spent signature against a cohort that already said no. Rejections are re-presented
20
+ * anyway when they arrive inside the transactor's promise-phase `[block:` aggregate, which
21
+ * {@link isUncommittedTransactorAggregate} claims on the WRAPPER alone, whatever the cause inside
22
+ * says. Safe because nothing has committed at that phase, and deliberately kept — one of the
23
+ * remaining promise-phase rejections is exactly the kind a re-presentation fixes. See the
24
+ * accepted-tradeoff `NOTE:` there for which, and why no blanket rejection veto is wanted.
25
+ *
26
+ * A failure that says some of the write may be STORED is a different matter, and never
27
+ * re-presented whatever text it carries: {@link reportsPossiblyStoredWrite} vetoes it by type
28
+ * before any matcher runs. The one half-landed write that IS re-presented is a torn write the
29
+ * library marks final — not saved and unable to land ({@link isFinalTornWrite}).
19
30
  *
20
31
  * TWO policies ship from here, and the split is deliberate: the default
21
32
  * ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
@@ -63,6 +74,17 @@ export declare const SCHEMA_INIT_ATTEMPTS = 5;
63
74
  * attempts, measured in `control-write-degraded-cohort-member.integration.ts`), which
64
75
  * already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
65
76
  * and retry adds ZERO latency to the case where it cannot help.
77
+ *
78
+ * NOTE: a failed commit attempt now also pays a cancel discharge before its error returns
79
+ * (optimystic `TransactorSource.transact`'s catch, added by upstream
80
+ * `1-a-failed-attempt-must-discharge-its-own-pend`) — bounded by six rounds and
81
+ * `abortOrCancelTimeoutMs`, which every collection this repo opens sets to 5 s
82
+ * (`../optimystic/packages/quereus-plugin-optimystic/src/optimystic-adapter/collection-factory.ts`).
83
+ * Two failed attempts whose cancels each run their full budget would consume this whole 10 s
84
+ * ceiling and cut the three-attempt policy to two. Not observed — every measured round of the
85
+ * transient-reset case committed on attempt 3 of 3 — so this is a condition to watch, not work
86
+ * to do: if that case ever starts failing with `failed after 2/3 attempt(s)`, the cancel
87
+ * discharge is where the time went.
66
88
  */
67
89
  export declare const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
68
90
  /**
@@ -101,6 +123,42 @@ export declare const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
101
123
  * token on the live aggregate; if it reddens there, an upstream reformat is the first thing to
102
124
  * check.
103
125
  *
126
+ * NOTE: the discriminator is the prefix AND the token within ONE message — never relax it to
127
+ * "contains `[block:`". Beyond the `get` and `pend` sites above, Optimystic renders `[block:` in
128
+ * one further place — a third formatter, outside the aggregate's three sites — `dischargeCancel`
129
+ * (`Cancel of action <id> did not discharge <n> block(s): …`), which does not raise the
130
+ * `Some peers did not complete:` prefix; only the conjunction keeps a cancel fault from
131
+ * classifying as a retriable get/pend (checked against optimystic `c56c2bd4`, 2026-09-16).
132
+ *
133
+ * NOTE: accepted tradeoff — a REJECTION raised while promises are still being collected reaches
134
+ * this repo wrapped in this same aggregate, and this matcher claims it on the wrapper alone,
135
+ * whatever the cause inside says. That re-presents such a rejection up to two more times inside
136
+ * the 10 s budget. Kept deliberately, and re-decided on 2026-09-17 against the current upstream
137
+ * behaviour rather than inherited:
138
+ *
139
+ * - Contention is no longer a rejection at all. "Another write holds this block right now"
140
+ * (`pending conflict`) is now a `held` verdict that counts toward neither approvals nor
141
+ * rejections (`validatePendOperations`,
142
+ * `../optimystic/packages/db-p2p/src/cluster/cluster-repo.ts`), so the rejection this tradeoff
143
+ * was originally written about does not arrive any more. It reaches this funnel as
144
+ * `SyncRetryExhaustedError` instead, which {@link reportsPossiblyStoredWrite} declines by type
145
+ * — upstream documents resubmitting after it as unsafe (the attempt that spent the budget may
146
+ * have left its log entry standing), and its collection sync has already spent ten retries on
147
+ * the race. The torn-write rule beside it: a `TornActionError` is re-presented only when
148
+ * `final` is true; any other torn write is declined.
149
+ * - What remains rejectable at the promise phase is stale revision, block-unavailable,
150
+ * membership-not-admitted, and a configured validator's refusal. Only the first is worth a
151
+ * retry — and it is worth one: this loop re-runs the WHOLE write body, reads included
152
+ * (`ControlDatabase.withWriteLock`'s contract), so attempt 2 presents a pend against the
153
+ * revision that made attempt 1 stale. A blanket "decline any chain reporting a validator
154
+ * rejection" rule would remove that retry to save two wasted attempts on the other three.
155
+ *
156
+ * Revisit if a re-presentation ever becomes expensive relative to what it buys — a rejection class
157
+ * that costs a full cohort round-trip to re-present, or a write body whose reads are no longer
158
+ * cheap — or when upstream offers a typed refusal surface to classify on instead of this text
159
+ * (`../optimystic/tickets/backlog/debt-a-downstream-repo-classifies-retries-by-parsing-our-error-text`).
160
+ * Contention is no longer the reason to revisit.
161
+ *
104
162
  * An aggregate whose details came out EMPTY (possible when `formatBatchStatuses` has no
105
163
  * batches to format) carries neither token, matches nothing here, and is not retried — an
106
164
  * unattributable failure is not a proven non-commit.
@@ -114,16 +172,18 @@ export declare function isUncommittedTransactorAggregate(message: string): boole
114
172
  * Did this control write fail because the cluster cohort did not ANSWER — i.e. is
115
173
  * re-presenting the SAME signed write a moment later the right response?
116
174
  *
117
- * Classifies by MESSAGE, walking the `cause` chain with the shared {@link chainMessages}
118
- * (Quereus' own `unwrapError` underneath) —
119
- * the failure surface is not recognisable by type. The transactor and the coordinator both throw bare `Error`s, and by
120
- * the time one reaches `ControlDatabase` it is wrapped in a `QuereusError` (the real chain
121
- * is `QuereusError` → `Error` → `Error`), so the match must work at any depth. Anything
122
- * that is not an `Error` is never retried.
175
+ * Classifies the transient cases by MESSAGE, walking the `cause` chain with the shared
176
+ * {@link chainMessages} (Quereus' own `unwrapError` underneath) — the transactor and the
177
+ * coordinator both throw bare `Error`s, not recognisable by type. By the time one reaches
178
+ * `ControlDatabase` it is wrapped in a `QuereusError` (the real chain is `QuereusError` → `Error`
179
+ * → `Error`), so the match must work at any depth. Anything that is not an `Error` is never
180
+ * retried.
123
181
  *
124
- * {@link reportsIndeterminateCommit} vetoes the whole chain before any matcher runs, so a
125
- * failure that reports a commit-phase batch anywhere is never retried on the strength of some
126
- * other level looking transient.
182
+ * Two vetoes run over the whole chain before any matcher: {@link reportsPossiblyStoredWrite}
183
+ * (by type — a spent sync budget, a non-final torn write, a partial commit) and
184
+ * {@link reportsIndeterminateCommit} (by text — a commit-phase batch). A chain either veto flags,
185
+ * at any level, is never retried on the strength of some other level looking transient. The one
186
+ * typed failure claimed as retriable is a final torn write ({@link isFinalTornWrite}).
127
187
  *
128
188
  * NOTE: this depends on engine/transactor error TEXT. The messages producible without a
129
189
  * network are driven from the REAL engine in `control-formation-seat-budget.spec.ts`, so
@@ -181,5 +241,14 @@ export declare const SCHEMA_INIT_RETRY_POLICY: Readonly<ControlWriteRetryOptions
181
241
  * On exhaustion (attempts or budget) the LAST error is rethrown unchanged — never wrapped,
182
242
  * so the exact messages downstream code and the degraded-cohort-member scenario assert on
183
243
  * survive. A non-retriable failure propagates from the attempt that raised it.
244
+ *
245
+ * Either way — declined or exhausted — an `options.onAbandon` observer is notified exactly
246
+ * once before the rethrow (a `ControlRetryAbandonment`). Only WRITES pass one: an abandoned
247
+ * read throws to a caller that is awaiting it, so nothing is lost silently there
248
+ * (`control-read-retry.ts` says so at its own seam), while an abandoned write may have no
249
+ * caller at all — a background self-record republish is `void`-ed with a `debug` catch, so
250
+ * its failure reaches nobody unless something is listening here.
251
+ * `ControlDatabase.setControlWriteAbandonedListener` is the only thing that passes one in
252
+ * production; `retryControlWrite` itself neither supplies nor requires an observer.
184
253
  */
185
254
  export declare function retryControlWrite<T>(attempt: () => Promise<T>, options?: ControlWriteRetryOptions): Promise<T>;
@@ -1,4 +1,6 @@
1
- import { chainMessages, retryControlOperation } from './control-retry.js';
1
+ import { CoordinatorPartialCommitError, SyncRetryExhaustedError, TornActionError } from '@optimystic/db-core';
2
+ import { PartialCommitError } from '@optimystic/quereus-plugin-optimystic';
3
+ import { causeChain, chainMessages, retryControlOperation } from './control-retry.js';
2
4
  /**
3
5
  * Bounded retry for TRANSIENT control-write failures — the classifier and loop behind
4
6
  * `ControlDatabase.lockedWithRetry`, the single funnel every local control write passes
@@ -14,8 +16,19 @@ import { chainMessages, retryControlOperation } from './control-retry.js';
14
16
  * a moment later, which the degraded-cohort-member scenario measured as safe (a failed
15
17
  * write rolls back, nothing half-commits) and effective (the next write commits in ~1 s).
16
18
  *
17
- * A write somebody actually REJECTED is never retried — re-presenting it would re-present
18
- * a spent signature against a cohort that already said no.
19
+ * A write somebody actually REJECTED is never retried on the strength of the rejection itself —
20
+ * no matcher here claims a rejection message, at any phase, because re-presenting one would
21
+ * re-present a spent signature against a cohort that already said no. Rejections are re-presented
22
+ * anyway when they arrive inside the transactor's promise-phase `[block:` aggregate, which
23
+ * {@link isUncommittedTransactorAggregate} claims on the WRAPPER alone, whatever the cause inside
24
+ * says. Safe because nothing has committed at that phase, and deliberately kept — one of the
25
+ * remaining promise-phase rejections is exactly the kind a re-presentation fixes. See the
26
+ * accepted-tradeoff `NOTE:` there for which, and why no blanket rejection veto is wanted.
27
+ *
28
+ * A failure that says some of the write may be STORED is a different matter, and never
29
+ * re-presented whatever text it carries: {@link reportsPossiblyStoredWrite} vetoes it by type
30
+ * before any matcher runs. The one half-landed write that IS re-presented is a torn write the
31
+ * library marks final — not saved and unable to land ({@link isFinalTornWrite}).
19
32
  *
20
33
  * TWO policies ship from here, and the split is deliberate: the default
21
34
  * ({@link isRetriableControlWriteFailure}, {@link CONTROL_WRITE_ATTEMPTS}) sits under every
@@ -63,6 +76,17 @@ export const SCHEMA_INIT_ATTEMPTS = 5;
63
76
  * attempts, measured in `control-write-degraded-cohort-member.integration.ts`), which
64
77
  * already exceeds this budget when attempt 1 returns — so that case is surfaced immediately
65
78
  * and retry adds ZERO latency to the case where it cannot help.
79
+ *
80
+ * NOTE: a failed commit attempt now also pays a cancel discharge before its error returns
81
+ * (optimystic `TransactorSource.transact`'s catch, added by upstream
82
+ * `1-a-failed-attempt-must-discharge-its-own-pend`) — bounded by six rounds and
83
+ * `abortOrCancelTimeoutMs`, which every collection this repo opens sets to 5 s
84
+ * (`../optimystic/packages/quereus-plugin-optimystic/src/optimystic-adapter/collection-factory.ts`).
85
+ * Two failed attempts whose cancels each run their full budget would consume this whole 10 s
86
+ * ceiling and cut the three-attempt policy to two. Not observed — every measured round of the
87
+ * transient-reset case committed on attempt 3 of 3 — so this is a condition to watch, not work
88
+ * to do: if that case ever starts failing with `failed after 2/3 attempt(s)`, the cancel
89
+ * discharge is where the time went.
66
90
  */
67
91
  export const CONTROL_WRITE_RETRY_BUDGET_MS = 10000;
68
92
  /**
@@ -110,9 +134,14 @@ const COMMIT_BATCH_TOKEN = '[blocks:';
110
134
  * matched ONLY with a ZERO rejection count. The same message with a non-zero count means a
111
135
  * member actually voted no (that branch carries `membership-not-admitted` rejections), and
112
136
  * retrying it would re-present a spent signature to a cohort that already refused it. This one
113
- * is raised while collecting PROMISES, before any commit, so re-presenting it is safe. A
114
- * decisive rejection is a different error entirely (`ValidatorRejectionError`,
115
- * `Transaction rejected by validators`), which this classifier also never matches.
137
+ * is raised while collecting PROMISES, before any commit, so re-presenting it is safe.
138
+ *
139
+ * This regex itself never matches a decisive rejection (`ValidatorRejectionError`, `Transaction
140
+ * rejected by validators`) — that text carries no `Failed to get super-majority:` prefix. But the
141
+ * composite classifier is not this matcher alone: a rejection raised while promises are still
142
+ * being collected reaches this repo wrapped in the transactor's `Some peers did not complete:
143
+ * …[block:…]` aggregate, and {@link isUncommittedTransactorAggregate} matches that wrapper on its
144
+ * own, whatever the cause inside it says — see the accepted-tradeoff `NOTE:` there.
116
145
  */
117
146
  const SUPER_MAJORITY_SHORTFALL_UNANSWERED = /Failed to get super-majority: \d+\/\d+ approvals \(needed \d+, 0 rejections\)/;
118
147
  /**
@@ -151,6 +180,42 @@ const SUPER_MAJORITY_SHORTFALL_UNANSWERED = /Failed to get super-majority: \d+\/
151
180
  * token on the live aggregate; if it reddens there, an upstream reformat is the first thing to
152
181
  * check.
153
182
  *
183
+ * NOTE: the discriminator is the prefix AND the token within ONE message — never relax it to
184
+ * "contains `[block:`". Beyond the `get` and `pend` sites above, Optimystic renders `[block:` in
185
+ * one further place — a third formatter, outside the aggregate's three sites — `dischargeCancel`
186
+ * (`Cancel of action <id> did not discharge <n> block(s): …`), which does not raise the
187
+ * `Some peers did not complete:` prefix; only the conjunction keeps a cancel fault from
188
+ * classifying as a retriable get/pend (checked against optimystic `c56c2bd4`, 2026-09-16).
189
+ *
190
+ * NOTE: accepted tradeoff — a REJECTION raised while promises are still being collected reaches
191
+ * this repo wrapped in this same aggregate, and this matcher claims it on the wrapper alone,
192
+ * whatever the cause inside says. That re-presents such a rejection up to two more times inside
193
+ * the 10 s budget. Kept deliberately, and re-decided on 2026-09-17 against the current upstream
194
+ * behaviour rather than inherited:
195
+ *
196
+ * - Contention is no longer a rejection at all. "Another write holds this block right now"
197
+ * (`pending conflict`) is now a `held` verdict that counts toward neither approvals nor
198
+ * rejections (`validatePendOperations`,
199
+ * `../optimystic/packages/db-p2p/src/cluster/cluster-repo.ts`), so the rejection this tradeoff
200
+ * was originally written about does not arrive any more. It reaches this funnel as
201
+ * `SyncRetryExhaustedError` instead, which {@link reportsPossiblyStoredWrite} declines by type
202
+ * — upstream documents resubmitting after it as unsafe (the attempt that spent the budget may
203
+ * have left its log entry standing), and its collection sync has already spent ten retries on
204
+ * the race. The torn-write rule beside it: a `TornActionError` is re-presented only when
205
+ * `final` is true; any other torn write is declined.
206
+ * - What remains rejectable at the promise phase is stale revision, block-unavailable,
207
+ * membership-not-admitted, and a configured validator's refusal. Only the first is worth a
208
+ * retry — and it is worth one: this loop re-runs the WHOLE write body, reads included
209
+ * (`ControlDatabase.withWriteLock`'s contract), so attempt 2 presents a pend against the
210
+ * revision that made attempt 1 stale. A blanket "decline any chain reporting a validator
211
+ * rejection" rule would remove that retry to save two wasted attempts on the other three.
212
+ *
213
+ * Revisit if a re-presentation ever becomes expensive relative to what it buys — a rejection class
214
+ * that costs a full cohort round-trip to re-present, or a write body whose reads are no longer
215
+ * cheap — or when upstream offers a typed refusal surface to classify on instead of this text
216
+ * (`../optimystic/tickets/backlog/debt-a-downstream-repo-classifies-retries-by-parsing-our-error-text`).
217
+ * Contention is no longer the reason to revisit.
218
+ *
154
219
  * An aggregate whose details came out EMPTY (possible when `formatBatchStatuses` has no
155
220
  * batches to format) carries neither token, matches nothing here, and is not retried — an
156
221
  * unattributable failure is not a proven non-commit.
@@ -177,6 +242,59 @@ export function isUncommittedTransactorAggregate(message) {
177
242
  function reportsIndeterminateCommit(messages) {
178
243
  return messages.some(message => message.includes(COMMIT_BATCH_TOKEN));
179
244
  }
245
+ /**
246
+ * Does any link of the `cause` chain say some of this write may be stored, or may still be?
247
+ * Such a failure is never re-presented — a re-run could store the write twice.
248
+ *
249
+ * - `SyncRetryExhaustedError` (and its `SyncRevisionStalledError` subclass): upstream documents
250
+ * resubmitting as unsafe, because the attempt that spent the budget may have left its log
251
+ * entry standing.
252
+ * - `TornActionError` without `final: true`: the write's log entry is stored and nobody could
253
+ * establish that the rest can never land — it may be saved already, or land later.
254
+ * - `CoordinatorPartialCommitError` / `PartialCommitError`: another collection or tree of the
255
+ * same transaction is already durable, and re-running the whole body would apply it twice.
256
+ *
257
+ * Every one of these embeds text from elsewhere in its message (a responder's refusal, the
258
+ * underlying failure), which can carry the pend-phase aggregate's prefix and `[block:` token —
259
+ * so this veto runs before any text matcher.
260
+ *
261
+ * Matched by TYPE: each survives the bridge's and Quereus' rewraps on `cause`. An error built by a
262
+ * second loaded copy of `@optimystic/db-core` (or of the plugin, for `PartialCommitError`) fails
263
+ * `instanceof`, and then this veto does not fire — falling back to the text classifiers, which is
264
+ * how these failures were classified before the veto existed. The asymmetry with
265
+ * {@link isFinalTornWrite} is deliberate: there, a missed `instanceof` means no retry, which is the
266
+ * safe side. No text fallback parses `TornActionError`'s closing sentence, since upstream says its
267
+ * wording is for log lines only.
268
+ *
269
+ * NOTE: the plugin is registered through its `/plugin` entry (`control-database.ts`) but
270
+ * `PartialCommitError` is imported from its root entry; the two are one class only because the
271
+ * plugin's build emits both entries over one shared chunk. If upstream ever bundles the entries
272
+ * separately, this `instanceof` stops matching with every spec still green — import the class from
273
+ * the entry the plugin is registered through.
274
+ */
275
+ function reportsPossiblyStoredWrite(links) {
276
+ return links.some(link => link instanceof SyncRetryExhaustedError
277
+ || (link instanceof TornActionError && link.final !== true)
278
+ || link instanceof CoordinatorPartialCommitError
279
+ || link instanceof PartialCommitError);
280
+ }
281
+ /**
282
+ * A torn write the library marks FINAL: not saved, unable to land, its pending records confirmed
283
+ * cancelled — so submitting it again stores it once.
284
+ *
285
+ * Worth re-presenting, not merely safe to: upstream raises it mostly when a rival holds the
286
+ * revision this write's log entry claimed, which is contention, and the loop re-runs the write
287
+ * body's reads, so the next attempt builds on the rival's revision — the same argument that keeps
288
+ * stale-revision rejections retried (the accepted-tradeoff `NOTE:` on
289
+ * {@link isUncommittedTransactorAggregate}).
290
+ *
291
+ * NOTE: `CoordinatorStaleLossError` (db-core; "nothing durably committed, safe to re-drive") is not
292
+ * claimed, so it falls to the text matchers; it escapes only after the coordinator's own retry budget
293
+ * ran out. If control writes are seen abandoned on it, claim it here by type beside this one.
294
+ */
295
+ function isFinalTornWrite(link) {
296
+ return link instanceof TornActionError && link.final === true;
297
+ }
180
298
  /** The coordinator's shortfall, raised pre-commit, with nobody having voted no. */
181
299
  function isUnansweredSuperMajorityShortfall(message) {
182
300
  return SUPER_MAJORITY_SHORTFALL_UNANSWERED.test(message);
@@ -193,13 +311,14 @@ function isUnansweredSuperMajorityShortfall(message) {
193
311
  * one connection again, which a cold-starting node dialling its bootstrap peers is actively
194
312
  * working on.
195
313
  *
196
- * NOTE: matched by TEXT, not by type, and that is forced rather than chosen.
197
- * `FindCoordinatorError` (`code: 'SELF_COORDINATION_BLOCKED'`) is exported from `@optimystic/db-p2p`
198
- * and this package depends on it, but the error OBJECT does not survive the trip:
199
- * `OptimysticVirtualTable.initialize` catches and rethrows as `new Error(message)` with no `cause`
200
- * (`quereus-plugin-optimystic/src/optimystic-module.ts`), so only the text reaches this repo. Like
201
- * every matcher here this fails CLOSED — a rewording upstream stops the retry engaging, it never
202
- * makes it unsafe.
314
+ * NOTE: matched by TEXT, not by type. `FindCoordinatorError` (`code: 'SELF_COORDINATION_BLOCKED'`)
315
+ * is exported from `@optimystic/db-p2p`, and when this matcher was written the error OBJECT did not
316
+ * survive the trip: `OptimysticVirtualTable.initialize` rethrew as `new Error(message)` with no
317
+ * `cause`. It now rethrows through `rewrapAsQueryError`, which keeps `cause`
318
+ * (`quereus-plugin-optimystic/src/optimystic-module.ts`); whether every Quereus wrapper above it on
319
+ * the schema-init path keeps it too has not been checked, and the text still arrives either way.
320
+ * Like every matcher here this fails CLOSED — a rewording upstream stops the retry engaging, it
321
+ * never makes it unsafe.
203
322
  */
204
323
  const SELF_COORDINATION_GRACE_REFUSAL = /Self-coordination blocked: grace-period-not-elapsed\. No coordinator available for key\./;
205
324
  /** Self-coordination refused because the node's last connection dropped moments ago. */
@@ -246,16 +365,18 @@ const RETRIABLE_SCHEMA_INIT_MATCHERS = [
246
365
  * Did this control write fail because the cluster cohort did not ANSWER — i.e. is
247
366
  * re-presenting the SAME signed write a moment later the right response?
248
367
  *
249
- * Classifies by MESSAGE, walking the `cause` chain with the shared {@link chainMessages}
250
- * (Quereus' own `unwrapError` underneath) —
251
- * the failure surface is not recognisable by type. The transactor and the coordinator both throw bare `Error`s, and by
252
- * the time one reaches `ControlDatabase` it is wrapped in a `QuereusError` (the real chain
253
- * is `QuereusError` → `Error` → `Error`), so the match must work at any depth. Anything
254
- * that is not an `Error` is never retried.
368
+ * Classifies the transient cases by MESSAGE, walking the `cause` chain with the shared
369
+ * {@link chainMessages} (Quereus' own `unwrapError` underneath) — the transactor and the
370
+ * coordinator both throw bare `Error`s, not recognisable by type. By the time one reaches
371
+ * `ControlDatabase` it is wrapped in a `QuereusError` (the real chain is `QuereusError` → `Error`
372
+ * → `Error`), so the match must work at any depth. Anything that is not an `Error` is never
373
+ * retried.
255
374
  *
256
- * {@link reportsIndeterminateCommit} vetoes the whole chain before any matcher runs, so a
257
- * failure that reports a commit-phase batch anywhere is never retried on the strength of some
258
- * other level looking transient.
375
+ * Two vetoes run over the whole chain before any matcher: {@link reportsPossiblyStoredWrite}
376
+ * (by type — a spent sync budget, a non-final torn write, a partial commit) and
377
+ * {@link reportsIndeterminateCommit} (by text — a commit-phase batch). A chain either veto flags,
378
+ * at any level, is never retried on the strength of some other level looking transient. The one
379
+ * typed failure claimed as retriable is a final torn write ({@link isFinalTornWrite}).
259
380
  *
260
381
  * NOTE: this depends on engine/transactor error TEXT. The messages producible without a
261
382
  * network are driven from the REAL engine in `control-formation-seat-budget.spec.ts`, so
@@ -266,7 +387,7 @@ const RETRIABLE_SCHEMA_INIT_MATCHERS = [
266
387
  * aggregate from a real stream reset, which the retry there absorbs end to end.
267
388
  */
268
389
  export function isRetriableControlWriteFailure(error) {
269
- return matchesRetriableMessage(error, RETRIABLE_CONTROL_WRITE_MATCHERS);
390
+ return matchesRetriableFailure(error, RETRIABLE_CONTROL_WRITE_MATCHERS);
270
391
  }
271
392
  /**
272
393
  * {@link isRetriableControlWriteFailure} widened by exactly one class — the self-coordination
@@ -276,22 +397,31 @@ export function isRetriableControlWriteFailure(error) {
276
397
  * default classifier untouched.
277
398
  */
278
399
  export function isRetriableSchemaInitFailure(error) {
279
- return matchesRetriableMessage(error, RETRIABLE_SCHEMA_INIT_MATCHERS);
400
+ return matchesRetriableFailure(error, RETRIABLE_SCHEMA_INIT_MATCHERS);
280
401
  }
281
402
  /**
282
- * Shared body of both classifiers: walk the `cause` chain, let
283
- * {@link reportsIndeterminateCommit} veto it, then ask `matchers`. The veto runs BEFORE any
284
- * matcher for every policy — a chain reporting a commit-phase batch is never retried, however
285
- * transient some other level looks.
403
+ * Shared body of both classifiers: walk the `cause` chain, let {@link reportsPossiblyStoredWrite}
404
+ * and then {@link reportsIndeterminateCommit} veto it, then claim a final torn write
405
+ * ({@link isFinalTornWrite}), then ask `matchers`. The vetoes run BEFORE any matcher for every
406
+ * policy — a chain reporting a possibly-stored write or a commit-phase batch is never retried,
407
+ * however transient some other level looks, and a final torn write carried by a partial commit is
408
+ * still declined because a sibling collection landed.
286
409
  */
287
- function matchesRetriableMessage(error, matchers) {
410
+ function matchesRetriableFailure(error, matchers) {
288
411
  if (!(error instanceof Error)) {
289
412
  return false;
290
413
  }
414
+ const links = causeChain(error);
415
+ if (reportsPossiblyStoredWrite(links)) {
416
+ return false;
417
+ }
291
418
  const messages = chainMessages(error);
292
419
  if (reportsIndeterminateCommit(messages)) {
293
420
  return false;
294
421
  }
422
+ if (links.some(isFinalTornWrite)) {
423
+ return true;
424
+ }
295
425
  return messages.some(message => matchers.some(matches => matches(message)));
296
426
  }
297
427
  /**
@@ -326,6 +456,15 @@ export const SCHEMA_INIT_RETRY_POLICY = {
326
456
  * On exhaustion (attempts or budget) the LAST error is rethrown unchanged — never wrapped,
327
457
  * so the exact messages downstream code and the degraded-cohort-member scenario assert on
328
458
  * survive. A non-retriable failure propagates from the attempt that raised it.
459
+ *
460
+ * Either way — declined or exhausted — an `options.onAbandon` observer is notified exactly
461
+ * once before the rethrow (a `ControlRetryAbandonment`). Only WRITES pass one: an abandoned
462
+ * read throws to a caller that is awaiting it, so nothing is lost silently there
463
+ * (`control-read-retry.ts` says so at its own seam), while an abandoned write may have no
464
+ * caller at all — a background self-record republish is `void`-ed with a `debug` catch, so
465
+ * its failure reaches nobody unless something is listening here.
466
+ * `ControlDatabase.setControlWriteAbandonedListener` is the only thing that passes one in
467
+ * production; `retryControlWrite` itself neither supplies nor requires an observer.
329
468
  */
330
469
  export function retryControlWrite(attempt, options = {}) {
331
470
  return retryControlOperation(attempt, {
@@ -1 +1 @@
1
- {"version":3,"file":"control-write-retry.js","sourceRoot":"","sources":["../src/control-write-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAM,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,6BAA6B,GAAsB,CAAC,GAAG,EAAE,IAAK,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,2BAA2B,GAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,IAAK,EAAE,IAAK,CAAC,CAAC;AAEhF,+FAA+F;AAC/F,MAAM,oBAAoB,GAAG,8BAA8B,CAAC;AAE5D;;;GAGG;AACH,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAE3C;;;GAGG;AACH,MAAM,kBAAkB,GAAG,UAAU,CAAC;AAEtC;;;;;;;;GAQG;AACH,MAAM,mCAAmC,GACxC,+EAA+E,CAAC;AAEjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,UAAU,gCAAgC,CAAC,OAAe;IAC/D,OAAO,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;AACzF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,0BAA0B,CAAC,QAA2B;IAC9D,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,mFAAmF;AACnF,SAAS,kCAAkC,CAAC,OAAe;IAC1D,OAAO,mCAAmC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,+BAA+B,GACpC,0FAA0F,CAAC;AAE5F,wFAAwF;AACxF,SAAS,8BAA8B,CAAC,OAAe;IACtD,OAAO,+BAA+B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,gCAAgC,GAA8C;IACnF,gCAAgC;IAChC,kCAAkC;CAClC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,8BAA8B,GAA8C;IACjF,GAAG,gCAAgC;IACnC,8BAA8B;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,8BAA8B,CAAC,KAAc;IAC5D,OAAO,uBAAuB,CAAC,KAAK,EAAE,gCAAgC,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAc;IAC1D,OAAO,uBAAuB,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAAC;AACvE,CAAC;AAED;;;;;GAKG;AACH,SAAS,uBAAuB,CAC/B,KAAc,EACd,QAAmD;IAEnD,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC;IACd,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,0BAA0B,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7E,CAAC;AAcD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAuC;IAC3E,QAAQ,EAAE,oBAAoB;IAC9B,QAAQ,EAAE,2BAA2B;IACrC,WAAW,EAAE,4BAA4B;CACzC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAChC,OAAyB,EACzB,UAAoC,EAAE;IAEtC,OAAO,qBAAqB,CAAC,OAAO,EAAE;QACrC,QAAQ,EAAE,sBAAsB;QAChC,QAAQ,EAAE,6BAA6B;QACvC,QAAQ,EAAE,6BAA6B;QACvC,WAAW,EAAE,8BAA8B;QAC3C,4EAA4E;QAC5E,mDAAmD;QACnD,SAAS,EAAE,eAAe;KAC1B,EAAE,OAAO,CAAC,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"control-write-retry.js","sourceRoot":"","sources":["../src/control-write-retry.ts"],"names":[],"mappings":"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"}
@@ -41,8 +41,8 @@
41
41
  * `Memory*` / `Persistent*` / `File*` split, same party scoping. It does NOT share
42
42
  * their `NodeLocalSnapshot` machinery, for two reasons:
43
43
  *
44
- * - **Shape.** `NodeLocalSnapshot` is a `key -> entry` map with no delete. This
45
- * record is one integer that must be able to go DOWN after a machine is removed.
44
+ * - **Shape.** `NodeLocalSnapshot` is a `key -> entry` map. This record is one
45
+ * integer that must be able to go DOWN after a machine is removed.
46
46
  * - **Load policy — the important one.** `NodeLocalSnapshot.open` THROWS when the
47
47
  * slot is present but unreadable. That is right for the anchor (a record that
48
48
  * cannot be read in full is not a trust anchor) and right for the peer store (a
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export * from './types.js';
2
2
  export { canonicalJson } from './canonical-json.js';
3
3
  export { CadreNode } from './cadre-node.js';
4
- export { ControlDatabase, MissingHostStrandError, buildAuthorizationMessage, formationVouchMessage, formationConsentMessage, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type MembershipChangeListener, type GuardedDeleteListener, type RevokedRowRef } from './control-database.js';
4
+ export { ControlDatabase, MissingHostStrandError, buildAuthorizationMessage, formationVouchMessage, formationConsentMessage, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type MembershipChangeListener, type GuardedDeleteListener, type ControlWriteAbandonedListener, type RevokedRowRef } from './control-database.js';
5
5
  export { CONTROL_WRITE_ATTEMPTS, CONTROL_WRITE_RETRY_BUDGET_MS, SCHEMA_INIT_ATTEMPTS, SCHEMA_INIT_RETRY_POLICY, isRetriableControlWriteFailure, isRetriableSchemaInitFailure, retryControlWrite, type ControlWriteRetryOptions } from './control-write-retry.js';
6
6
  export { CONTROL_READ_ATTEMPTS, CONTROL_READ_RETRY_DELAYS_MS, CONTROL_READ_RETRY_BUDGET_MS, isRetriableControlReadFailure, retryControlRead, type ControlReadRetryOptions } from './control-read-retry.js';
7
+ export type { ControlRetryAbandonment, ControlRetryAbandonReason, ControlRetryAbandonListener } from './control-retry.js';
7
8
  export { controlAuthorizationFields, type ControlDomain, type ControlAction } from './control-authorization.js';
8
9
  export { ed25519KeyPairFromLibp2p, ed25519PublicKeyFromPrivate, requireEd25519PublicKeyB64, type Ed25519KeyPair } from './ed25519-key.js';
9
10
  export { InMemoryKeyStore, KeyStoreAccessError, DEFAULT_IDENTITY_KEY_ID, type KeyStore, type KeyId } from './key-store.js';
@@ -12,11 +13,15 @@ export { MemoryTrustedOwnerStore, PersistentTrustedOwnerStore, type TrustedOwner
12
13
  export type { DurableSlot } from './node-local-snapshot.js';
13
14
  export { MemoryBootstrapPeerStore, PersistentBootstrapPeerStore, type BootstrapPeerStore, type BootstrapPeerEntry } from './bootstrap-peer-store.js';
14
15
  export { MemoryEnrolledMachineStore, PersistentEnrolledMachineStore, type EnrolledMachineStore } from './enrolled-machine-store.js';
16
+ export { controlStorageScope, isControlStorageScope } from './storage-scope.js';
15
17
  export { StrandDatabase, type StrandDatabaseConfig } from './strand-database.js';
16
18
  export { StrandWatcher, type StrandWatcherCallbacks, type StrandQueryable, type SAppIdLookup, type StrandRowWithApp } from './strand-watcher.js';
17
- export { StrandInstanceManager, type StartStrandConfig, type ResumeStrandOverrides, getStrandStoragePath } from './strand-instance-manager.js';
19
+ export { StrandInstanceManager, type StartStrandConfig, type ResumeStrandOverrides, getStrandStoragePath, isAwaitingFirstSync, liveStrandStatus } from './strand-instance-manager.js';
20
+ export { StrandAwaitingFirstSyncError, DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS, DEFAULT_STRAND_FIRST_SYNC_POLL_MS, strandHeaderHeld, appTablesReadable, strandFirstSyncComplete, type StrandFirstSyncConfig } from './strand-first-sync-gate.js';
18
21
  export { PeerJoinBackfill, DEFAULT_PEER_JOIN_BACKFILL, MAX_BLOCK_MESSAGE_BYTES, type PeerJoinBackfillConfig, type PeerJoinBackfillDeps, type PeerJoinBackfillResult, type PeerJoinBackfillPushClient } from './peer-join-backfill.js';
19
22
  export { StrandRevocationEnforcer, createRevocationConnectionGater, readStrandRevocationRows, DEFAULT_REVOCATION_POLL_INTERVAL_MS, type StrandRevocationEnforcementConfig, type StrandRevocationEnforcerDeps, type StrandRevocationRows, type StrandMemberPeerBinding, type StrandRevocationJudge, type StrandRevocationNetwork, type RevocationRefreshScheduler } from './strand-revocation-enforcer.js';
23
+ export { StrandMembershipReconciler, IDLE_PASSES_BEFORE_ESCALATION, INITIAL_JOIN_RETRY_INTERVAL_MS, type StrandMembershipReconcilerDeps, type StrandMembershipReconciliationConfig, type PendingMembershipInviteSource } from './strand-membership-reconciler.js';
24
+ export { defaultTimeoutScheduler, type TimeoutScheduler } from './timeout-scheduler.js';
20
25
  export { HibernationManager, type HibernationCallbacks } from './hibernation-manager.js';
21
26
  export { ArachnodeStub, createArachnodeStub, type RingConfig } from './arachnode-stub.js';
22
27
  export { EnrollmentService, type MemberVerifier, type MemberRegistry } from './enrollment.js';
@@ -24,7 +29,7 @@ export { StrandSolicitationService, createDefaultFormationResponseValidator, typ
24
29
  export { createHttpFormationApprover, signFormationApproval, verifyFormationApproval, FormationApprovalError, type FormationApprovalRequest, type FormationVouchFields, type FormationApproval, type FormationApprover, type FormationApprovalFailure } from './formation-approval.js';
25
30
  export { ControlFormationUsageRecorder } from './control-formation-recorder.js';
26
31
  export { generateStrandMemberKey, strandMemberKeyPair } from './strand-member-key.js';
27
- export { signStrandPayload, verifyStrandPayload, signStrandApproval, generateStrandStampId, bootstrapFounderMembership, issueInvite, consumeInvite, cancelInvite, listOutstandingInvites, addMemberByManager, revokeMember, leaveStrand, registerMemberPeer, listMemberPeers, removeMemberPeer, addManager, admitManager, removeManager, sealStrand, isStrandSealed, STRAND_ENGINE, STRAND_ENGINE_VERSION, type FounderBootstrapParams, type IssueInviteParams, type IssuedInvite, type ConsumeInviteParams, type CancelInviteParams, type OutstandingInvite, type AddMemberByManagerParams, type RevokeMemberParams, type LeaveStrandParams, type RegisterMemberPeerParams, type RemoveOwnPeerParams, type RemoveMemberPeerByManagerParams, type RemoveMemberPeerParams, type AddManagerParams, type AdmitManagerParams, type RemoveManagerParams, type SealStrandParams } from './strand-membership-writer.js';
32
+ export { signStrandPayload, verifyStrandPayload, signStrandApproval, generateStrandStampId, bootstrapFounderMembership, PreSplitStrandIdentityError, issueInvite, consumeInvite, burnInvite, isStrandMember, cancelInvite, listOutstandingInvites, addMemberByManager, revokeMember, leaveStrand, registerMemberPeer, listMemberPeers, removeMemberPeer, addManager, admitManager, removeManager, sealStrand, isStrandSealed, StrandTransactionBusyError, STRAND_ENGINE, STRAND_ENGINE_VERSION, type FounderBootstrapParams, type IssueInviteParams, type IssuedInvite, type ConsumeInviteParams, type CancelInviteParams, type OutstandingInvite, type AddMemberByManagerParams, type RevokeMemberParams, type LeaveStrandParams, type RegisterMemberPeerParams, type RemoveOwnPeerParams, type RemoveMemberPeerByManagerParams, type RemoveMemberPeerParams, type AddManagerParams, type AdmitManagerParams, type RemoveManagerParams, type SealStrandParams, type StrandWriteOptions } from './strand-membership-writer.js';
28
33
  export { canonicalDatetime } from './canonical-datetime.js';
29
34
  export { StrandMemberRegistry, StrandMemberVerifier, memberRegistrationPayload, type StrandAdmission } from './strand-member-registry.js';
30
35
  export { SeedBootstrapService, SEED_PROTOCOL, ed25519PublicKeyB64FromPeerId, type SeedBootstrapConfig, type SeedEventCallbacks } from './seed-bootstrap.js';
@@ -37,12 +42,14 @@ export { STRAND_WAKE_TYPE, type StrandWakePayload } from './strand-wake-payload.
37
42
  export type { PushNotifier, PushMessage, PushSendResult } from './push-notifier.js';
38
43
  export { validatePushCredentials, redactPushCredentials, REDACTED } from './push-credentials.js';
39
44
  export { PushFanoutService, DEFAULT_PUSH_COOLDOWN_MS, DEFAULT_PUSH_DEBOUNCE_MS, type PushFanoutOptions, type FanoutMember } from './push-fanout.js';
40
- export { selectControlCohortDials, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_RECONCILE_MS, DEFAULT_CONTROL_COHORT_TARGET_DEGREE, type ControlCohortSelection } from './control-cohort.js';
45
+ export { selectControlCohortDials, DEFAULT_CONTROL_COHORT_RECONCILE_MS, DEFAULT_CONTROL_COHORT_TARGET_DEGREE, type ControlCohortSelection, type ControlCohortReconcileResult } from './control-cohort.js';
46
+ export { dialPeerAddrs, tryAddrsInTurn, directBeforeRelayed, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS, DEFAULT_PEER_DIAL_BUDGET, type PeerDialBudget, type AddrDialer } from './peer-dial.js';
41
47
  export { anchoredTrustPolicy, pinnedKeyTrustPolicy, tofuTrustPolicy, type SeedTrustPolicy, type SeedTrustContext, type SeedTrustDecision } from './seed-trust-policy.js';
42
48
  export { signSchema, verifySchema, assertSchemaSignature, SchemaVerificationError } from './schema-verification.js';
43
- export { FormationListener, dialFormation, isValidResponderCreatesResult, sanitizeStrandAddrs, FORMATION_PROTOCOL, type FormationParty, type FormationContactMessage, type FormationResultMessage, type FormationProvisionResult, type FormationStrandInfo, type FormationDbConnectionInfo, type FormationListenerOptions, type FormationDialOptions, type FormationDialResult, type ResponderProvisionOutcome } from './strand-formation-protocol.js';
44
- export { StrandFormationManager, createStrandFormationManager, type StrandFormationManagerConfig, type StrandFormationManagerOptions } from './strand-formation-manager.js';
49
+ export { FormationListener, dialFormation, isValidResponderCreatesResult, isWellFormedMembershipInvite, sanitizeStrandAddrs, FORMATION_PROTOCOL, type FormationParty, type FormationContactMessage, type FormationResultMessage, type FormationProvisionResult, type FormationStrandInfo, type FormationDbConnectionInfo, type FormationListenerOptions, type FormationDialOptions, type FormationDialResult, type ResponderProvisionOutcome } from './strand-formation-protocol.js';
50
+ export { StrandFormationManager, createStrandFormationManager, MEMBERSHIP_INVITE_TTL_MS, MEMBERSHIP_INVITE_UNAVAILABLE_REASON, HOST_STRAND_MUST_BE_RECREATED_REASON, type StrandFormationManagerConfig, type StrandFormationManagerOptions } from './strand-formation-manager.js';
45
51
  export { createMembershipConnectionGater, STRANGER_OPEN_PROTOCOLS, DEFAULT_ENROLLMENT_WINDOW_MS, ADMISSION_DECISION_TIMEOUT_MS, RELAY_ADMISSION_RESERVE_DEADLINE_MS, MAX_UNAUTHORIZED_RELAY_RESERVATIONS, UNAUTHORIZED_RESERVATION_TTL_MS, UnauthorizedReservationBudget, type InboundAdmissionPolicy, type InboundConnectionVerdict } from './membership-connection-gater.js';
46
- export { RelayReservationFailedError, UnbindableListenAddressError, RELAY_SEARCH_LISTEN_ADDR } from './relay-addrs.js';
52
+ export { RelayReservationFailedError, UnbindableListenAddressError, RELAY_SEARCH_LISTEN_ADDR, resolveListenAddrs } from './relay-addrs.js';
53
+ export { strandNodeAddrs, type StrandNodeAddrs } from './strand-network-config.js';
47
54
  export { circuitMultiaddrs, superviseRelayReservation, DEFAULT_RELAY_RESERVE_TIMEOUT_MS, DEFAULT_RELAY_RESERVE_POLL_MS, DEFAULT_RELAY_CHECK_MS, DEFAULT_RELAY_MIN_BACKOFF_MS, DEFAULT_RELAY_MAX_BACKOFF_MS, type RelayReservationStatus, type RelayReservationState, type RelayReserveOptions, type RelayReservationSupervisor, type RelayReservationSupervisorOptions } from './relay-reservation.js';
48
55
  export { classifyTransport, classifyConnectionPath, summarizeConnectionPaths, emptyConnectionPathSummary, DEFAULT_SETTLE_WINDOW_MS, type ConnectionPathKind, type ConnectionTransport, type ConnectionPath, type ConnectionPathSummary, type ConnectionLike } from './diagnostics/connection-path.js';
package/dist/index.js CHANGED
@@ -49,11 +49,19 @@ export { MemoryBootstrapPeerStore, PersistentBootstrapPeerStore } from './bootst
49
49
  // DIVERGES from theirs (an unreadable slot cold-starts rather than throwing) —
50
50
  // see the module comment before unifying the three.
51
51
  export { MemoryEnrolledMachineStore, PersistentEnrolledMachineStore } from './enrolled-machine-store.js';
52
+ // Storage scope keys — what `CadreNodeConfig.storage.provider` is called with. A
53
+ // strand's key is its strand id; the control database's key carries the party id,
54
+ // so two parties on one device never share a control store.
55
+ export { controlStorageScope, isControlStorageScope } from './storage-scope.js';
52
56
  // Strand database
53
57
  export { StrandDatabase } from './strand-database.js';
54
58
  // Strand management
55
59
  export { StrandWatcher } from './strand-watcher.js';
56
- export { StrandInstanceManager, getStrandStoragePath } from './strand-instance-manager.js';
60
+ export { StrandInstanceManager, getStrandStoragePath, isAwaitingFirstSync, liveStrandStatus } from './strand-instance-manager.js';
61
+ // The joining machine's first-sync write gate: a non-founder launch withholds its
62
+ // database (status 'syncing') until the strand's Header has arrived from a peer, so a
63
+ // write before the first sync can never fork the strand's tables.
64
+ export { StrandAwaitingFirstSyncError, DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS, DEFAULT_STRAND_FIRST_SYNC_POLL_MS, strandHeaderHeld, appTablesReadable, strandFirstSyncComplete } from './strand-first-sync-gate.js';
57
65
  // Peer-join block catch-up (push this network's own blocks to each newly
58
66
  // connected peer, so a late joiner physically holds pre-join blocks). Shared
59
67
  // by the strand networks (ungated) and the control network (membership-gated).
@@ -63,6 +71,14 @@ export { PeerJoinBackfill, DEFAULT_PEER_JOIN_BACKFILL, MAX_BLOCK_MESSAGE_BYTES }
63
71
  // already hold — see the module doc for the settled deny-list design, the
64
72
  // teardown seam, and the fail directions).
65
73
  export { StrandRevocationEnforcer, createRevocationConnectionGater, readStrandRevocationRows, DEFAULT_REVOCATION_POLL_INTERVAL_MS } from './strand-revocation-enforcer.js';
74
+ // Closed-strand membership reconciliation: the bring-up loop that finishes a
75
+ // party's join on every machine — redeem the staged formation invitation, then
76
+ // write the durable machine→party MemberPeer binding — without ever blocking
77
+ // bring-up (see the module doc for the pass ladder and terminal states).
78
+ export { StrandMembershipReconciler, IDLE_PASSES_BEFORE_ESCALATION, INITIAL_JOIN_RETRY_INTERVAL_MS } from './strand-membership-reconciler.js';
79
+ // The timer seam the self-rescheduling strand bring-up loops share (first-sync gate
80
+ // probe loop, membership retry ladder) — injected by tests as a hand-cranked clock.
81
+ export { defaultTimeoutScheduler } from './timeout-scheduler.js';
66
82
  // Hibernation
67
83
  export { HibernationManager } from './hibernation-manager.js';
68
84
  // Arachnode (stub)
@@ -83,7 +99,7 @@ export { generateStrandMemberKey, strandMemberKeyPair } from './strand-member-ke
83
99
  // manager-admit, member-peer registration, manager rotation, sealing — the sole
84
100
  // manager permanently freezing admission — + shared signing primitives reused
85
101
  // across the flows).
86
- export { signStrandPayload, verifyStrandPayload, signStrandApproval, generateStrandStampId, bootstrapFounderMembership, issueInvite, consumeInvite, cancelInvite, listOutstandingInvites, addMemberByManager, revokeMember, leaveStrand, registerMemberPeer, listMemberPeers, removeMemberPeer, addManager, admitManager, removeManager, sealStrand, isStrandSealed, STRAND_ENGINE, STRAND_ENGINE_VERSION } from './strand-membership-writer.js';
102
+ export { signStrandPayload, verifyStrandPayload, signStrandApproval, generateStrandStampId, bootstrapFounderMembership, PreSplitStrandIdentityError, issueInvite, consumeInvite, burnInvite, isStrandMember, cancelInvite, listOutstandingInvites, addMemberByManager, revokeMember, leaveStrand, registerMemberPeer, listMemberPeers, removeMemberPeer, addManager, admitManager, removeManager, sealStrand, isStrandSealed, StrandTransactionBusyError, STRAND_ENGINE, STRAND_ENGINE_VERSION } from './strand-membership-writer.js';
87
103
  // Engine-canonical datetime helper (shared by control + strand signed-write flows
88
104
  // so a signed timestamp byte-matches the datetime-coerced column the CHECK sees).
89
105
  export { canonicalDatetime } from './canonical-datetime.js';
@@ -115,15 +131,18 @@ export { validatePushCredentials, redactPushCredentials, REDACTED } from './push
115
131
  export { PushFanoutService, DEFAULT_PUSH_COOLDOWN_MS, DEFAULT_PUSH_DEBOUNCE_MS } from './push-fanout.js';
116
132
  // Control-cohort dial selection (backbone-preferential, bounded out-degree) +
117
133
  // the cadence/degree defaults the proactive reconcile routine reads.
118
- export { selectControlCohortDials, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_RECONCILE_MS, DEFAULT_CONTROL_COHORT_TARGET_DEGREE } from './control-cohort.js';
134
+ export { selectControlCohortDials, DEFAULT_CONTROL_COHORT_RECONCILE_MS, DEFAULT_CONTROL_COHORT_TARGET_DEGREE } from './control-cohort.js';
135
+ // Dialing one peer from several candidate addresses, each under its own time
136
+ // limit, so an address that never answers cannot starve the rest.
137
+ export { dialPeerAddrs, tryAddrsInTurn, directBeforeRelayed, DEFAULT_CONTROL_COHORT_DIAL_TIMEOUT_MS, DEFAULT_CONTROL_COHORT_PER_ADDRESS_DIAL_TIMEOUT_MS, DEFAULT_PEER_DIAL_BUDGET } from './peer-dial.js';
119
138
  // Seed trust policy (trust anchor for incoming seeds)
120
139
  export { anchoredTrustPolicy, pinnedKeyTrustPolicy, tofuTrustPolicy } from './seed-trust-policy.js';
121
140
  // Schema Verification
122
141
  export { signSchema, verifySchema, assertSchemaSignature, SchemaVerificationError } from './schema-verification.js';
123
142
  // Strand Formation transport (native cadre-core protocol)
124
- export { FormationListener, dialFormation, isValidResponderCreatesResult, sanitizeStrandAddrs, FORMATION_PROTOCOL } from './strand-formation-protocol.js';
143
+ export { FormationListener, dialFormation, isValidResponderCreatesResult, isWellFormedMembershipInvite, sanitizeStrandAddrs, FORMATION_PROTOCOL } from './strand-formation-protocol.js';
125
144
  // Strand Formation manager (drives the native transport)
126
- export { StrandFormationManager, createStrandFormationManager } from './strand-formation-manager.js';
145
+ export { StrandFormationManager, createStrandFormationManager, MEMBERSHIP_INVITE_TTL_MS, MEMBERSHIP_INVITE_UNAVAILABLE_REASON, HOST_STRAND_MUST_BE_RECREATED_REASON } from './strand-formation-manager.js';
127
146
  // Control-network inbound connection gate (membership defense-in-depth)
128
147
  export { createMembershipConnectionGater, STRANGER_OPEN_PROTOCOLS, DEFAULT_ENROLLMENT_WINDOW_MS, ADMISSION_DECISION_TIMEOUT_MS, RELAY_ADMISSION_RESERVE_DEADLINE_MS, MAX_UNAUTHORIZED_RELAY_RESERVATIONS, UNAUTHORIZED_RESERVATION_TTL_MS, UnauthorizedReservationBudget } from './membership-connection-gater.js';
129
148
  // Thrown out of `CadreNode.start()` when a `network.relayAddrs` reservation does
@@ -132,7 +151,15 @@ export { createMembershipConnectionGater, STRANGER_OPEN_PROTOCOLS, DEFAULT_ENROL
132
151
  // `UnbindableListenAddressError` is the same posture for the OTHER half of the listen
133
152
  // config: a `network.listenAddrs` entry whose transport the node will not have refuses
134
153
  // start instead of being silently dropped by libp2p's transport manager.
135
- export { RelayReservationFailedError, UnbindableListenAddressError, RELAY_SEARCH_LISTEN_ADDR } from './relay-addrs.js';
154
+ //
155
+ // `resolveListenAddrs` and `strandNodeAddrs` are the derivations themselves — what a
156
+ // `NetworkConfig` actually turns into for the control node and for each strand node.
157
+ // Exported so an embedder that hand-writes a `network` block (the React Native and web
158
+ // reference apps) can assert on the resulting SHAPE rather than on its own field names,
159
+ // which is the difference between a test that proves reachability and one that proves a
160
+ // spelling.
161
+ export { RelayReservationFailedError, UnbindableListenAddressError, RELAY_SEARCH_LISTEN_ADDR, resolveListenAddrs } from './relay-addrs.js';
162
+ export { strandNodeAddrs } from './strand-network-config.js';
136
163
  // Relay reservation via the bare `/p2p-circuit` search listener — the one route
137
164
  // every control node takes; `network.relayAddrs` is its fail-fast posture
138
165
  // (see relay-reservation.ts)