@optimystic/db-p2p 0.29.0 → 1.0.0-beta.1

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.
@@ -163,6 +163,7 @@ export declare class ClusterMember implements ICluster {
163
163
  private executedTransactions;
164
164
  private executedPendResults;
165
165
  private executedCommitResults;
166
+ private refusedPendActions;
166
167
  private appliedInvalidations;
167
168
  private cleanupQueue;
168
169
  private pendingUpdates;
@@ -245,6 +246,15 @@ export declare class ClusterMember implements ICluster {
245
246
  */
246
247
  update(record: ClusterRecord): Promise<ClusterRecord>;
247
248
  private processUpdate;
249
+ /**
250
+ * Add this member's own conflict-shaped pend verdict to a record's {@link ClusterRecord.applyOutcomes},
251
+ * when storage produced one for this transaction. A success, a bare-reason fault, or no retained
252
+ * verdict at all leaves the record untouched — the coordinator's rule is "an entry means retry",
253
+ * so an entry that does not mean retry must not exist.
254
+ *
255
+ * The member never signs this and never writes another peer's entry.
256
+ */
257
+ private withOwnApplyOutcome;
248
258
  /**
249
259
  * Merges two records, validating that non-signature fields match.
250
260
  * Detects equivocation (same peer changing vote type) and applies penalties.
@@ -405,7 +415,10 @@ export declare class ClusterMember implements ICluster {
405
415
  * "I checked this". The four-way rule, per committed block:
406
416
  * - no local `latest`, or `latest.rev < commit.rev` → abstain (approve). Preserves the
407
417
  * lagging-member tolerance (`coordinator-repo-commit-divergence.spec.ts`): a member behind
408
- * the commit cannot judge it.
418
+ * the commit cannot judge it. Abstaining is safe rather than merely tolerable: apply-time
419
+ * catches the case the vote cannot, since `StorageRepo.internalCommit` refuses an update-only
420
+ * transform whose declared `baseRev` is not this member's `latest.rev` and reconciles instead
421
+ * of forking the block's content.
409
422
  * - `latest.rev === commit.rev` with the SAME action → abstain (approve). Idempotent
410
423
  * redelivery of an already-durable commit; rejecting would make the writer rebase and
411
424
  * re-append an action that already landed — a duplicate entry. (Storage's `alreadyDone`
@@ -420,20 +433,64 @@ export declare class ClusterMember implements ICluster {
420
433
  * abstains. Reason stays plain prose — it is fed to computeSigningPayload and carried as
421
434
  * Signature.rejectReason, exactly like the stale-revision pend reject.
422
435
  *
423
- * Residual (see the commit-tier handoff): a member that signed the winner's commit but has not
424
- * yet APPLIED it sits in a window where it holds neither the winner's record (reservation
425
- * dropped at commit-sign, `shouldPersist = false`) nor the winner's revision (storage still
426
- * behind) — it abstains here. A capability-less member, or one with history truncated below
427
- * `latest`, abstains at `latest.rev > commit.rev` too. A rival's commit can therefore still
428
- * pass the promise round if EVERY member is simultaneously in one of those states — on a fast
429
- * cohort that window is the COMMON case, not the corner. The backstop is downstream of
430
- * consensus: every member's apply then refuses the rival as stale, the coordinating node's own
431
- * member retains that refusal (`getExecutedCommitResult`), and `CoordinatorRepo.commit`
432
- * confirms the rival against local storage and answers the writer with a retryable conflict
433
- * instead of a fabricated success. `ConflictRaceLostError` conversion and classified
434
- * rejections close the re-drive route the same way.
436
+ * Residual (narrowed, see below): a member that signed the winner's commit but has not yet
437
+ * APPLIED it sits in a window where it holds neither the winner's record (reservation dropped at
438
+ * commit-sign, `shouldPersist = false`) nor the winner's revision (storage still behind) — it
439
+ * abstains here. A capability-less member, or one with history truncated below `latest`, abstains
440
+ * at `latest.rev > commit.rev` too. A rival's commit can therefore still pass THIS check if every
441
+ * member is simultaneously in one of those states — on a fast cohort that window is the COMMON
442
+ * case, not the corner.
443
+ *
444
+ * What now covers it: {@link validateCommitAgainstRefusedPend} rejects the same commit from the
445
+ * other direction not "did a rival take the revision" but "did I already refuse this action's
446
+ * pend" which is precisely the state a member is left in when it lost the race in the
447
+ * promise→apply window. Downstream of consensus the backstop still stands: every member's apply
448
+ * refuses the rival as stale, the coordinating node's own member retains that refusal
449
+ * (`getExecutedCommitResult`), and `CoordinatorRepo.commit` confirms the rival against local
450
+ * storage and answers the writer with a retryable conflict instead of a fabricated success.
451
+ * `ConflictRaceLostError` conversion and classified rejections close the re-drive route the same
452
+ * way.
435
453
  */
436
454
  private validateCommitRevisions;
455
+ /**
456
+ * Promise-round check that this member is not about to co-sign the commit of an action whose PEND
457
+ * its own storage refused.
458
+ *
459
+ * The hole this closes: the commit-round vote is deliberately blind ({@link getTransactionPhase}
460
+ * signs whenever promise approvals reach super-majority), and both other commit-promise checks
461
+ * abstain for a member that holds no pend of the committing action — which is exactly the state a
462
+ * member is in *because* it refused that pend. So a member could refuse action X's pend as
463
+ * conflicting with a rival, and milliseconds later sign X's commit, letting X reach commit
464
+ * consensus at a revision this member will never durably hold.
465
+ *
466
+ * Two conditions, both required:
467
+ * (a) this member retains a conflict-shaped pend refusal for `commit.actionId`
468
+ * ({@link refusedPendActions}), and
469
+ * (b) local storage STILL corroborates it — a rival pending action holds one of the commit's
470
+ * blocks, or a different action already took `commit.rev`.
471
+ *
472
+ * (b) is what keeps this from regressing the lagging-member and cohort-drift tolerances that
473
+ * {@link validateCommitRevisions} documents. A member that merely *missed* the pend retains no
474
+ * refusal and abstains exactly as before. A member whose refusal has since been settled — the
475
+ * rival cancelled, the reservation released — abstains too, rather than vetoing a commit the rest
476
+ * of the cohort is holding perfectly well. The retention is in-memory and TTL'd, so a restart also
477
+ * degrades to the previous abstain behaviour rather than to a stuck veto.
478
+ *
479
+ * Effect on a small cohort: one reject makes super-majority unreachable, so the commit never
480
+ * assembles consensus and the write fails loudly instead of forking the block. That is the
481
+ * intended outcome here — a clean retryable conflict answer is {@link ClusterRecord.applyOutcomes}'
482
+ * job, and this arm is the backstop for the paths that channel cannot reach (a member that applies
483
+ * late, via the scheduled commit-retry timer, after the coordinator has already answered).
484
+ *
485
+ * Never throws out of the vote path: a read fault is an abstain, same rule as its two siblings.
486
+ */
487
+ private validateCommitAgainstRefusedPend;
488
+ /**
489
+ * Condition (b) of {@link validateCommitAgainstRefusedPend}: does local storage still back up a
490
+ * retained pend refusal for this commit? Returns a short prose fragment naming what corroborates
491
+ * it, or `undefined` for "no longer corroborated — abstain" (which a read fault also yields).
492
+ */
493
+ private refusedPendConflictsWithLocalState;
437
494
  /**
438
495
  * Promise-round check of a commit record's declared content digests
439
496
  * (`CommitRequest.blockDigests`) against what this member's OWN pended copy of each transform
@@ -450,9 +507,11 @@ export declare class ClusterMember implements ICluster {
450
507
  * authored, delivered at pend — a hostile declarer cannot force or dodge a check by mis-declaring
451
508
  * `baseRev`):
452
509
  * - transform carries an `insert` → base-independent, ALWAYS check (declared `baseRev` ignored);
453
- * - `updates` only → check iff this member's local base rev equals the declared `baseRev`
454
- * (StorageRepo.commit accepts any `latest.rev < request.rev`, so a lagging member applying an
455
- * update-only transform to an older base legitimately materializes different bytes);
510
+ * - `updates` only → check iff this member's local base rev equals the declared `baseRev`. A
511
+ * member whose base does not match cannot JUDGE the content it would preview the transform
512
+ * against different bytes than the declarer used so it abstains here. It never goes on to
513
+ * materialize those bytes: `StorageRepo.internalCommit` refuses that same mismatch at apply
514
+ * and reconciles from a cohort peer;
456
515
  * - `delete` only / no base / unmaterializable base → materializes nothing to compare, abstain;
457
516
  * - no pending transform for the action (this member never saw the pend) → abstain.
458
517
  * "Abstain" = contribute no content attestation: approve exactly as before this check existed.
@@ -1 +1 @@
1
- {"version":3,"file":"cluster-repo.d.ts","sourceRoot":"","sources":["../../../src/cluster/cluster-repo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cluster-repo.d.ts","sourceRoot":"","sources":["../../../src/cluster/cluster-repo.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { blockIdsForTransforms, isOwnRevision, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
1
+ import { blockIdsForTransforms, isOwnRevision, isConflictFailure, DEFAULT_SUPER_MAJORITY_THRESHOLD } from "@optimystic/db-core";
2
2
  import { computeClusterCommitHash, computeClusterMessageHash, computeClusterPromiseHash, membershipDigest, recordMembershipDigest, clusterVoteSigningPayload, clusterVoteVerificationPayload } from "@optimystic/db-core";
3
3
  import { verifyInvalidationCertificate } from "../dispute/invalidation.js";
4
4
  import { buildCommitCert, invalidationActionId } from "./commit-cert.js";
@@ -107,6 +107,14 @@ export class ClusterMember {
107
107
  // at apply, and without this verdict the coordinator fabricates a success no member durably
108
108
  // stored. Pruned alongside executedTransactions (same TTL).
109
109
  executedCommitResults = new Map();
110
+ // Conflict-shaped pend refusals this member's storage produced at consensus-apply, keyed by the
111
+ // refused action's id rather than by messageHash. The messageHash-keyed map above cannot serve the
112
+ // commit-promise guard: a commit is a DIFFERENT message with a different hash, so a member holding
113
+ // "I refused action X's pend" has no way to find that verdict when X's commit arrives to be voted
114
+ // on. Keyed by actionId, it does. Same TTL, pruning and rollback discipline as its sibling —
115
+ // see {@link refusedPendConflictsWithLocalState} for how a stale entry is prevented from vetoing a
116
+ // commit the rest of the cohort holds fine. (-> refusedAt timestamp)
117
+ refusedPendActions = new Map();
110
118
  // Fast in-memory dedup for applied invalidations, keyed `${invalidatedActionId}:${disputeId}`.
111
119
  // The durable source of truth is the invalidation log entry (Log.findInvalidation, re-checked
112
120
  // inside the sink); this map only spares redundant work when the same invalidation reaches
@@ -212,6 +220,7 @@ export class ClusterMember {
212
220
  this.cleanupQueue.length = 0;
213
221
  this.executedPendResults.clear();
214
222
  this.executedCommitResults.clear();
223
+ this.refusedPendActions.clear();
215
224
  }
216
225
  /**
217
226
  * Checks if a transaction's operations were already executed during consensus.
@@ -447,6 +456,14 @@ export class ClusterMember {
447
456
  }
448
457
  // Skip propagation - the coordinator manages distribution
449
458
  // await this.propagateIfNeeded(currentRecord);
459
+ // Stamp our own apply verdict on the response, AFTER the phase loop has run (and therefore
460
+ // after any consensus apply this delivery triggered). This is the return channel that closes
461
+ // the non-coordinating-member hole: a member that refused the pend at apply is the only node
462
+ // that knows it, and until now that verdict never left the member. Attached here rather than
463
+ // inside the Consensus branch so a redelivery of an already-applied record answers with the
464
+ // same verdict instead of an empty one. Deliberately not folded into the persisted state: it
465
+ // is a property of this response, not of the transaction the member is holding.
466
+ currentRecord = this.withOwnApplyOutcome(currentRecord);
450
467
  log('cluster-member:update-complete', {
451
468
  messageHash: record.messageHash,
452
469
  promiseCount: Object.keys(currentRecord.promises).length,
@@ -454,6 +471,24 @@ export class ClusterMember {
454
471
  });
455
472
  return currentRecord;
456
473
  }
474
+ /**
475
+ * Add this member's own conflict-shaped pend verdict to a record's {@link ClusterRecord.applyOutcomes},
476
+ * when storage produced one for this transaction. A success, a bare-reason fault, or no retained
477
+ * verdict at all leaves the record untouched — the coordinator's rule is "an entry means retry",
478
+ * so an entry that does not mean retry must not exist.
479
+ *
480
+ * The member never signs this and never writes another peer's entry.
481
+ */
482
+ withOwnApplyOutcome(record) {
483
+ const verdict = this.executedPendResults.get(record.messageHash);
484
+ if (verdict === undefined || verdict.success || !isConflictFailure(verdict)) {
485
+ return record;
486
+ }
487
+ return {
488
+ ...record,
489
+ applyOutcomes: { ...record.applyOutcomes, [this.peerId.toString()]: { pend: verdict } }
490
+ };
491
+ }
457
492
  /**
458
493
  * Merges two records, validating that non-signature fields match.
459
494
  * Detects equivocation (same peer changing vote type) and applies penalties.
@@ -502,10 +537,17 @@ export class ClusterMember {
502
537
  // Merge signatures with equivocation detection
503
538
  const mergedPromises = this.detectEquivocation(existing.promises, incoming.promises, 'promise', existing.messageHash);
504
539
  const mergedCommits = this.detectEquivocation(existing.commits, incoming.commits, 'commit', existing.messageHash);
540
+ // Union the advisory apply outcomes per peer, first-seen wins. A peer only ever writes its own
541
+ // entry, so "first-seen wins" is per-peer idempotence, not a conflict rule. Omitted entirely
542
+ // when neither side carries any, so an ordinary record gains no empty object.
543
+ const mergedOutcomes = (existing.applyOutcomes || incoming.applyOutcomes)
544
+ ? { ...incoming.applyOutcomes, ...existing.applyOutcomes }
545
+ : undefined;
505
546
  return {
506
547
  ...existing,
507
548
  promises: mergedPromises,
508
- commits: mergedCommits
549
+ commits: mergedCommits,
550
+ ...(mergedOutcomes === undefined ? {} : { applyOutcomes: mergedOutcomes })
509
551
  };
510
552
  }
511
553
  /**
@@ -834,6 +876,14 @@ export class ClusterMember {
834
876
  if (!commitRevValidation.valid) {
835
877
  return commitRevValidation;
836
878
  }
879
+ // Then our own refusal history: a commit whose pend THIS member refused, where local state
880
+ // still corroborates the refusal. Runs after the revision check because that one is sharper
881
+ // (it names the rival revision) and catches the same commit whenever the rival has already
882
+ // applied here; this arm covers the window where it has not.
883
+ const refusedPendValidation = await this.validateCommitAgainstRefusedPend(record);
884
+ if (!refusedPendValidation.valid) {
885
+ return refusedPendValidation;
886
+ }
837
887
  return await this.validateCommitOperations(record);
838
888
  }
839
889
  /**
@@ -1188,8 +1238,10 @@ export class ClusterMember {
1188
1238
  // moves that verdict into the phase where the cohort aggregates it, so the loser is
1189
1239
  // refused with a real answer instead of burning a consensus round it cannot win. A
1190
1240
  // member that has not yet applied the rival's pend has no record and simply abstains
1191
- // from this reason; the coordinator returning the retained apply verdict
1192
- // (getExecutedPendResult) catches that residual. Self is excluded so a redelivered pend
1241
+ // from this reason; the apply-time verdict catches that residual — retained locally
1242
+ // (getExecutedPendResult) for the coordinating node's own member, and returned to the
1243
+ // coordinator on the response record (ClusterRecord.applyOutcomes) by every other
1244
+ // member. Self is excluded so a redelivered pend
1193
1245
  // for this same action stays approvable. An unavailable block carries no `pendings` and
1194
1246
  // abstains (the rev branch above already fail-closes when a revision claim is at stake).
1195
1247
  // Reason stays plain prose: it is fed to computeSigningPayload and carried as
@@ -1246,7 +1298,10 @@ export class ClusterMember {
1246
1298
  * "I checked this". The four-way rule, per committed block:
1247
1299
  * - no local `latest`, or `latest.rev < commit.rev` → abstain (approve). Preserves the
1248
1300
  * lagging-member tolerance (`coordinator-repo-commit-divergence.spec.ts`): a member behind
1249
- * the commit cannot judge it.
1301
+ * the commit cannot judge it. Abstaining is safe rather than merely tolerable: apply-time
1302
+ * catches the case the vote cannot, since `StorageRepo.internalCommit` refuses an update-only
1303
+ * transform whose declared `baseRev` is not this member's `latest.rev` and reconciles instead
1304
+ * of forking the block's content.
1250
1305
  * - `latest.rev === commit.rev` with the SAME action → abstain (approve). Idempotent
1251
1306
  * redelivery of an already-durable commit; rejecting would make the writer rebase and
1252
1307
  * re-append an action that already landed — a duplicate entry. (Storage's `alreadyDone`
@@ -1261,18 +1316,23 @@ export class ClusterMember {
1261
1316
  * abstains. Reason stays plain prose — it is fed to computeSigningPayload and carried as
1262
1317
  * Signature.rejectReason, exactly like the stale-revision pend reject.
1263
1318
  *
1264
- * Residual (see the commit-tier handoff): a member that signed the winner's commit but has not
1265
- * yet APPLIED it sits in a window where it holds neither the winner's record (reservation
1266
- * dropped at commit-sign, `shouldPersist = false`) nor the winner's revision (storage still
1267
- * behind) — it abstains here. A capability-less member, or one with history truncated below
1268
- * `latest`, abstains at `latest.rev > commit.rev` too. A rival's commit can therefore still
1269
- * pass the promise round if EVERY member is simultaneously in one of those states — on a fast
1270
- * cohort that window is the COMMON case, not the corner. The backstop is downstream of
1271
- * consensus: every member's apply then refuses the rival as stale, the coordinating node's own
1272
- * member retains that refusal (`getExecutedCommitResult`), and `CoordinatorRepo.commit`
1273
- * confirms the rival against local storage and answers the writer with a retryable conflict
1274
- * instead of a fabricated success. `ConflictRaceLostError` conversion and classified
1275
- * rejections close the re-drive route the same way.
1319
+ * Residual (narrowed, see below): a member that signed the winner's commit but has not yet
1320
+ * APPLIED it sits in a window where it holds neither the winner's record (reservation dropped at
1321
+ * commit-sign, `shouldPersist = false`) nor the winner's revision (storage still behind) — it
1322
+ * abstains here. A capability-less member, or one with history truncated below `latest`, abstains
1323
+ * at `latest.rev > commit.rev` too. A rival's commit can therefore still pass THIS check if every
1324
+ * member is simultaneously in one of those states — on a fast cohort that window is the COMMON
1325
+ * case, not the corner.
1326
+ *
1327
+ * What now covers it: {@link validateCommitAgainstRefusedPend} rejects the same commit from the
1328
+ * other direction not "did a rival take the revision" but "did I already refuse this action's
1329
+ * pend" which is precisely the state a member is left in when it lost the race in the
1330
+ * promise→apply window. Downstream of consensus the backstop still stands: every member's apply
1331
+ * refuses the rival as stale, the coordinating node's own member retains that refusal
1332
+ * (`getExecutedCommitResult`), and `CoordinatorRepo.commit` confirms the rival against local
1333
+ * storage and answers the writer with a retryable conflict instead of a fabricated success.
1334
+ * `ConflictRaceLostError` conversion and classified rejections close the re-drive route the same
1335
+ * way.
1276
1336
  */
1277
1337
  async validateCommitRevisions(record) {
1278
1338
  for (const operation of record.message.operations) {
@@ -1349,6 +1409,93 @@ export class ClusterMember {
1349
1409
  }
1350
1410
  return { valid: true };
1351
1411
  }
1412
+ /**
1413
+ * Promise-round check that this member is not about to co-sign the commit of an action whose PEND
1414
+ * its own storage refused.
1415
+ *
1416
+ * The hole this closes: the commit-round vote is deliberately blind ({@link getTransactionPhase}
1417
+ * signs whenever promise approvals reach super-majority), and both other commit-promise checks
1418
+ * abstain for a member that holds no pend of the committing action — which is exactly the state a
1419
+ * member is in *because* it refused that pend. So a member could refuse action X's pend as
1420
+ * conflicting with a rival, and milliseconds later sign X's commit, letting X reach commit
1421
+ * consensus at a revision this member will never durably hold.
1422
+ *
1423
+ * Two conditions, both required:
1424
+ * (a) this member retains a conflict-shaped pend refusal for `commit.actionId`
1425
+ * ({@link refusedPendActions}), and
1426
+ * (b) local storage STILL corroborates it — a rival pending action holds one of the commit's
1427
+ * blocks, or a different action already took `commit.rev`.
1428
+ *
1429
+ * (b) is what keeps this from regressing the lagging-member and cohort-drift tolerances that
1430
+ * {@link validateCommitRevisions} documents. A member that merely *missed* the pend retains no
1431
+ * refusal and abstains exactly as before. A member whose refusal has since been settled — the
1432
+ * rival cancelled, the reservation released — abstains too, rather than vetoing a commit the rest
1433
+ * of the cohort is holding perfectly well. The retention is in-memory and TTL'd, so a restart also
1434
+ * degrades to the previous abstain behaviour rather than to a stuck veto.
1435
+ *
1436
+ * Effect on a small cohort: one reject makes super-majority unreachable, so the commit never
1437
+ * assembles consensus and the write fails loudly instead of forking the block. That is the
1438
+ * intended outcome here — a clean retryable conflict answer is {@link ClusterRecord.applyOutcomes}'
1439
+ * job, and this arm is the backstop for the paths that channel cannot reach (a member that applies
1440
+ * late, via the scheduled commit-retry timer, after the coordinator has already answered).
1441
+ *
1442
+ * Never throws out of the vote path: a read fault is an abstain, same rule as its two siblings.
1443
+ */
1444
+ async validateCommitAgainstRefusedPend(record) {
1445
+ for (const operation of record.message.operations) {
1446
+ if (!('commit' in operation)) {
1447
+ continue;
1448
+ }
1449
+ const commit = operation.commit;
1450
+ if (!this.refusedPendActions.has(commit.actionId)) {
1451
+ continue;
1452
+ }
1453
+ const corroboration = await this.refusedPendConflictsWithLocalState(commit);
1454
+ if (corroboration === undefined) {
1455
+ continue;
1456
+ }
1457
+ log('cluster-member:validation-refused-pend-commit', {
1458
+ messageHash: record.messageHash,
1459
+ actionId: commit.actionId,
1460
+ rev: commit.rev,
1461
+ corroboration
1462
+ });
1463
+ // Prose only, for the same reason every other vote reason here is: it is fed to
1464
+ // computeSigningPayload and carried as Signature.rejectReason.
1465
+ return { valid: false, reason: `refused pend: this member refused action ${commit.actionId}'s pend and ${corroboration}` };
1466
+ }
1467
+ return { valid: true };
1468
+ }
1469
+ /**
1470
+ * Condition (b) of {@link validateCommitAgainstRefusedPend}: does local storage still back up a
1471
+ * retained pend refusal for this commit? Returns a short prose fragment naming what corroborates
1472
+ * it, or `undefined` for "no longer corroborated — abstain" (which a read fault also yields).
1473
+ */
1474
+ async refusedPendConflictsWithLocalState(commit) {
1475
+ let blockResults;
1476
+ try {
1477
+ blockResults = await this.storageRepo.get({ blockIds: commit.blockIds });
1478
+ }
1479
+ catch (err) {
1480
+ log('cluster-member:refused-pend-read-error', {
1481
+ actionId: commit.actionId,
1482
+ error: err instanceof Error ? err.message : String(err)
1483
+ });
1484
+ return undefined; // a local read fault is an abstain, never an escape out of the vote path
1485
+ }
1486
+ for (const blockId of commit.blockIds) {
1487
+ const state = blockResults[blockId]?.state;
1488
+ const rivals = (state?.pendings ?? []).filter(actionId => actionId !== commit.actionId);
1489
+ if (rivals.length > 0) {
1490
+ return `block ${blockId} is still held by unresolved action(s) ${rivals.join(', ')}`;
1491
+ }
1492
+ const latest = state?.latest;
1493
+ if (latest?.rev === commit.rev && !isOwnRevision(latest, commit.rev, commit.actionId)) {
1494
+ return `block ${blockId} rev ${commit.rev} is already committed by a different action`;
1495
+ }
1496
+ }
1497
+ return undefined;
1498
+ }
1352
1499
  /**
1353
1500
  * Promise-round check of a commit record's declared content digests
1354
1501
  * (`CommitRequest.blockDigests`) against what this member's OWN pended copy of each transform
@@ -1365,9 +1512,11 @@ export class ClusterMember {
1365
1512
  * authored, delivered at pend — a hostile declarer cannot force or dodge a check by mis-declaring
1366
1513
  * `baseRev`):
1367
1514
  * - transform carries an `insert` → base-independent, ALWAYS check (declared `baseRev` ignored);
1368
- * - `updates` only → check iff this member's local base rev equals the declared `baseRev`
1369
- * (StorageRepo.commit accepts any `latest.rev < request.rev`, so a lagging member applying an
1370
- * update-only transform to an older base legitimately materializes different bytes);
1515
+ * - `updates` only → check iff this member's local base rev equals the declared `baseRev`. A
1516
+ * member whose base does not match cannot JUDGE the content it would preview the transform
1517
+ * against different bytes than the declarer used so it abstains here. It never goes on to
1518
+ * materialize those bytes: `StorageRepo.internalCommit` refuses that same mismatch at apply
1519
+ * and reconciles from a cohort peer;
1371
1520
  * - `delete` only / no base / unmaterializable base → materializes nothing to compare, abstain;
1372
1521
  * - no pending transform for the action (this member never saw the pend) → abstain.
1373
1522
  * "Abstain" = contribute no content attestation: approve exactly as before this check existed.
@@ -1536,9 +1685,15 @@ export class ClusterMember {
1536
1685
  // divergence is absorbed inside applyConsensusOperation and never reaches here.
1537
1686
  // A retained pend verdict rolls back with the marker: it belongs to an apply
1538
1687
  // that is now considered not-executed, and a re-run will retain a fresh one.
1688
+ // The actionId-keyed refusals for this record's pend operations go with it — leaving
1689
+ // one behind would let a rolled-back apply veto that action's commit forever.
1539
1690
  this.executedTransactions.delete(record.messageHash);
1540
1691
  this.executedPendResults.delete(record.messageHash);
1541
1692
  this.executedCommitResults.delete(record.messageHash);
1693
+ for (const operation of record.message.operations) {
1694
+ if ('pend' in operation)
1695
+ this.refusedPendActions.delete(operation.pend.actionId);
1696
+ }
1542
1697
  throw err;
1543
1698
  }
1544
1699
  // Persist the durable marker only now that apply has actually succeeded. Writing it
@@ -1615,6 +1770,14 @@ export class ClusterMember {
1615
1770
  // already committed) is the optimistic-concurrency verdict, and swallowing it acknowledged
1616
1771
  // writes that no member stored.
1617
1772
  this.executedPendResults.set(messageHash, result);
1773
+ // Second, actionId-keyed retention for the commit tier: this same refusal must still be
1774
+ // findable when THIS action's commit record (a different messageHash) arrives for a
1775
+ // promise vote, so a member cannot co-sign the commit of a pend it just refused. Only
1776
+ // conflict-shaped refusals are kept — a bare-reason fault is local trouble, not evidence
1777
+ // that a rival holds the blocks, and must not veto the cohort's commit.
1778
+ if (!result.success && isConflictFailure(result)) {
1779
+ this.refusedPendActions.set(operation.pend.actionId, this.now());
1780
+ }
1618
1781
  if (!result.success) {
1619
1782
  log('cluster-member:consensus-pend-diverged', {
1620
1783
  messageHash,
@@ -1706,10 +1869,12 @@ export class ClusterMember {
1706
1869
  // lets the coordinating node convert its OWN member's rival-confirmed refusal into a
1707
1870
  // conflict answer — that last guard is what closes the signed-but-not-yet-applied
1708
1871
  // window, where two commits for one revision both assemble consensus because signing
1709
- // drops each member's reservation before applying advances its storage. If
1710
- // consensus-without-durability is ever observed again, look at those guards' abstain
1711
- // residuals (non-coordinating members' verdicts are not threaded back; capability-less
1712
- // or history-truncated storage abstains), not at this branch.
1872
+ // drops each member's reservation before applying advances its storage
1873
+ // `validateCommitAgainstRefusedPend` now refuses to sign that commit in the first
1874
+ // place on any member that refused its pend. If consensus-without-durability is ever
1875
+ // observed again, look at those guards' remaining abstain residuals (capability-less
1876
+ // or history-truncated storage; a member whose refusal has aged out of retention),
1877
+ // not at this branch.
1713
1878
  if (result.missing?.length) {
1714
1879
  log('cluster-member:consensus-commit-diverged', {
1715
1880
  messageHash,
@@ -2042,6 +2207,12 @@ export class ClusterMember {
2042
2207
  this.executedCommitResults.delete(messageHash);
2043
2208
  }
2044
2209
  }
2210
+ // Prune actionId-keyed pend refusals on the same TTL as the verdicts they were derived from.
2211
+ for (const [actionId, refusedAt] of Array.from(this.refusedPendActions.entries())) {
2212
+ if (refusedAt < expirationThreshold) {
2213
+ this.refusedPendActions.delete(actionId);
2214
+ }
2215
+ }
2045
2216
  // Prune old applied-invalidation dedup markers on the same TTL.
2046
2217
  for (const [dedupKey, appliedAt] of Array.from(this.appliedInvalidations.entries())) {
2047
2218
  if (appliedAt < expirationThreshold) {