@cleocode/core 2026.10.7 → 2026.10.8

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 (105) hide show
  1. package/dist/build-provenance.json +2 -2
  2. package/dist/cloud/nexus-link.d.ts +15 -0
  3. package/dist/cloud/nexus-link.d.ts.map +1 -1
  4. package/dist/cloud/nexus-link.js +25 -0
  5. package/dist/cloud/nexus-link.js.map +1 -1
  6. package/dist/cloud/nexus-vault.d.ts +14 -1
  7. package/dist/cloud/nexus-vault.d.ts.map +1 -1
  8. package/dist/cloud/nexus-vault.js +72 -5
  9. package/dist/cloud/nexus-vault.js.map +1 -1
  10. package/dist/cloud/replica-rebind.d.ts +51 -0
  11. package/dist/cloud/replica-rebind.d.ts.map +1 -0
  12. package/dist/cloud/replica-rebind.js +101 -0
  13. package/dist/cloud/replica-rebind.js.map +1 -0
  14. package/dist/doctor/container-reaper.d.ts +107 -0
  15. package/dist/doctor/container-reaper.d.ts.map +1 -0
  16. package/dist/doctor/container-reaper.js +194 -0
  17. package/dist/doctor/container-reaper.js.map +1 -0
  18. package/dist/store/sync/apply/applier.d.ts +3 -0
  19. package/dist/store/sync/apply/applier.d.ts.map +1 -1
  20. package/dist/store/sync/apply/applier.js +208 -85
  21. package/dist/store/sync/apply/applier.js.map +1 -1
  22. package/dist/store/sync/capture.d.ts +23 -3
  23. package/dist/store/sync/capture.d.ts.map +1 -1
  24. package/dist/store/sync/capture.js +31 -3
  25. package/dist/store/sync/capture.js.map +1 -1
  26. package/dist/store/sync/collision-settle.d.ts +62 -0
  27. package/dist/store/sync/collision-settle.d.ts.map +1 -0
  28. package/dist/store/sync/collision-settle.js +156 -0
  29. package/dist/store/sync/collision-settle.js.map +1 -0
  30. package/dist/store/sync/conflicts.d.ts +11 -0
  31. package/dist/store/sync/conflicts.d.ts.map +1 -1
  32. package/dist/store/sync/conflicts.js +18 -0
  33. package/dist/store/sync/conflicts.js.map +1 -1
  34. package/dist/store/sync/inherit.d.ts +15 -8
  35. package/dist/store/sync/inherit.d.ts.map +1 -1
  36. package/dist/store/sync/inherit.js +28 -11
  37. package/dist/store/sync/inherit.js.map +1 -1
  38. package/dist/store/sync/merge/types.d.ts +6 -0
  39. package/dist/store/sync/merge/types.d.ts.map +1 -1
  40. package/dist/store/sync/pull.d.ts +23 -2
  41. package/dist/store/sync/pull.d.ts.map +1 -1
  42. package/dist/store/sync/pull.js +27 -2
  43. package/dist/store/sync/pull.js.map +1 -1
  44. package/dist/store/sync/rebind.d.ts +112 -0
  45. package/dist/store/sync/rebind.d.ts.map +1 -0
  46. package/dist/store/sync/rebind.js +187 -0
  47. package/dist/store/sync/rebind.js.map +1 -0
  48. package/dist/store/sync/reconcile.d.ts +93 -0
  49. package/dist/store/sync/reconcile.d.ts.map +1 -0
  50. package/dist/store/sync/reconcile.js +330 -0
  51. package/dist/store/sync/reconcile.js.map +1 -0
  52. package/dist/store/sync/repair.d.ts +5 -0
  53. package/dist/store/sync/repair.d.ts.map +1 -1
  54. package/dist/store/sync/repair.js +2 -2
  55. package/dist/store/sync/repair.js.map +1 -1
  56. package/dist/store/sync/replica.d.ts +28 -1
  57. package/dist/store/sync/replica.d.ts.map +1 -1
  58. package/dist/store/sync/replica.js +17 -3
  59. package/dist/store/sync/replica.js.map +1 -1
  60. package/dist/store/sync/retire.d.ts +161 -0
  61. package/dist/store/sync/retire.d.ts.map +1 -0
  62. package/dist/store/sync/retire.js +193 -0
  63. package/dist/store/sync/retire.js.map +1 -0
  64. package/dist/store/sync/schema.d.ts +27 -0
  65. package/dist/store/sync/schema.d.ts.map +1 -1
  66. package/dist/store/sync/schema.js +43 -0
  67. package/dist/store/sync/schema.js.map +1 -1
  68. package/dist/store/sync/sealer.d.ts +10 -0
  69. package/dist/store/sync/sealer.d.ts.map +1 -1
  70. package/dist/store/sync/sealer.js +51 -5
  71. package/dist/store/sync/sealer.js.map +1 -1
  72. package/dist/store/sync/segments.d.ts.map +1 -1
  73. package/dist/store/sync/segments.js +6 -2
  74. package/dist/store/sync/segments.js.map +1 -1
  75. package/dist/store/sync/uid-alias.d.ts +100 -0
  76. package/dist/store/sync/uid-alias.d.ts.map +1 -0
  77. package/dist/store/sync/uid-alias.js +169 -0
  78. package/dist/store/sync/uid-alias.js.map +1 -0
  79. package/dist/store/table-classification.d.ts.map +1 -1
  80. package/dist/store/table-classification.js +48 -0
  81. package/dist/store/table-classification.js.map +1 -1
  82. package/dist/tasks/change-set.d.ts.map +1 -1
  83. package/dist/tasks/change-set.js +16 -5
  84. package/dist/tasks/change-set.js.map +1 -1
  85. package/dist/tasks/complete-ci.d.ts.map +1 -1
  86. package/dist/tasks/complete-ci.js +7 -1
  87. package/dist/tasks/complete-ci.js.map +1 -1
  88. package/dist/tasks/done-plan.d.ts.map +1 -1
  89. package/dist/tasks/done-plan.js +24 -17
  90. package/dist/tasks/done-plan.js.map +1 -1
  91. package/dist/tasks/evidence.d.ts.map +1 -1
  92. package/dist/tasks/evidence.js +3 -2
  93. package/dist/tasks/evidence.js.map +1 -1
  94. package/dist/tasks/gate-runner.d.ts +20 -0
  95. package/dist/tasks/gate-runner.d.ts.map +1 -1
  96. package/dist/tasks/gate-runner.js +141 -19
  97. package/dist/tasks/gate-runner.js.map +1 -1
  98. package/dist/validation/engine-ops.d.ts.map +1 -1
  99. package/dist/validation/engine-ops.js +2 -0
  100. package/dist/validation/engine-ops.js.map +1 -1
  101. package/migrations/sync-journal/20261009130000_t13278-replica-retire/migration.sql +29 -0
  102. package/migrations/sync-journal/20261009140000_t13366-retire-confirm/migration.sql +18 -0
  103. package/migrations/sync-journal/20261010150000_t13397-uid-alias/migration.sql +32 -0
  104. package/migrations/sync-journal/20261010160000_t13399-rekey-follow/migration.sql +57 -0
  105. package/package.json +13 -13
@@ -39,6 +39,9 @@
39
39
  * so the sealer echoes nothing; row meta is set to the engine's state,
40
40
  * conflicts are recorded, and the transaction is marked `applied`,
41
41
  * `conflict` (something recorded) or `void` (every op refused).
42
+ * A late transaction of a replica retired earlier in the stream is
43
+ * inherited history (§3.5 D5, T13278): applied the same way, but its merge
44
+ * conflicts are not recorded against the successor's reconcile.
42
45
  *
43
46
  * Passes repeat while a pass applies something, so a transaction pending on
44
47
  * a row a later transaction inserts applies in the same call.
@@ -58,7 +61,8 @@
58
61
  */
59
62
  import { BIRTH_FP_COLUMN, UID_COLUMN } from '../../row-identity-registry.js';
60
63
  import { captureTableDef } from '../capture.js';
61
- import { recordConflictOnce, recordConflicts } from '../conflicts.js';
64
+ import { announcePlacedRekeys, settleLostUidCollisions } from '../collision-settle.js';
65
+ import { recordConflictOnce, recordConflicts, resolveHeldConflicts } from '../conflicts.js';
62
66
  import { clearFieldLeaves, readFieldFrontiers, readFieldLeaves, recordFieldLeaves, restoreFieldState, setFieldFrontiers, } from '../field-leave.js';
63
67
  import { accountRebaseRows, heldOp, holdOp, txnHasHold, unholdOp } from '../held.js';
64
68
  import { encodeHlc, genesisHlc, isWithinSkew, parseHlc } from '../hlc.js';
@@ -67,16 +71,19 @@ import { applyOp, checkSchemaVersion } from '../merge/engine.js';
67
71
  import { mergeSpecFor } from '../merge/rules.js';
68
72
  import { UNSEEN_ROW, } from '../merge/types.js';
69
73
  import { remapPending } from '../remap.js';
74
+ import { isInheritedHistory } from '../retire.js';
70
75
  import { fieldHlcsOf, readRowMetaFull, restoreRowMeta } from '../row-meta.js';
71
76
  import { hasTable } from '../schema.js';
72
77
  import { canonicalJson } from '../sealer-values.js';
73
78
  import { capturePosition, markSequenced, ownEchoFastPath, readRowUndo, recordForeignTouches, recordUndoBudget, resnapshotRowUndo, unsequencedLocalTxn, unsequencedLocalTxns, } from '../sequencing.js';
74
79
  import { withTriggersSuspended } from '../trigger-classes.js';
80
+ import { followUidAliases, markAliasPlaced, recordUidAlias } from '../uid-alias.js';
75
81
  import { widenFootprint } from './footprints.js';
76
82
  import { withApplyFrame } from './frame.js';
77
83
  import { parentDeletePolicy } from './parent-delete.js';
78
84
  import { checkApplyPreconditions, checkTaskTreeShape } from './post-apply.js';
79
85
  import { resolveRef, uidOfKey } from './refs.js';
86
+ import { wireToSql } from './write-api.js';
80
87
  const rowKey = (t, u) => `${t}\u0000${u}`;
81
88
  const keyText = (k) => `${k.stream}:${k.seq}:${k.txnIdx}`;
82
89
  /**
@@ -161,6 +168,63 @@ function uidCollision(db, def, op, resolution) {
161
168
  opHlc: op.h,
162
169
  };
163
170
  }
171
+ /**
172
+ * The local-key collision an insert would hit, or null (T12341 §6.4 step 7,
173
+ * T13397): a minted row whose local key (a display id, never the uid or a
174
+ * device-local rowid) a live row of another uid holds here. The insert is
175
+ * held, never voided by the UNIQUE constraint: a re-mint of the key settles
176
+ * it (§9.2).
177
+ */
178
+ function keyCollision(db, def, op, goneInTxn) {
179
+ if (op.o !== 'I' || !op.a || !def.identity.includes(BIRTH_FP_COLUMN))
180
+ return null;
181
+ const cols = def.key.filter((k) => k !== UID_COLUMN && k !== def.localRowid);
182
+ if (cols.length === 0 || cols.some((k) => op.a?.[k] === undefined || op.a?.[k] === null)) {
183
+ return null;
184
+ }
185
+ const row = db
186
+ .prepare(`SELECT "${UID_COLUMN}" AS uid FROM main."${def.table.replaceAll('"', '""')}" WHERE ${cols
187
+ .map((k) => `"${k.replaceAll('"', '""')}" = ?`)
188
+ .join(' AND ')}`)
189
+ .get(...cols.map((k) => wireToSql(op.a?.[k])));
190
+ // The holder is this row, or a row the transaction deletes first (a key re-used).
191
+ if (!row || row.uid === op.u || (row.uid !== null && goneInTxn(row.uid)))
192
+ return null;
193
+ return {
194
+ kind: 'key-collision',
195
+ table: op.t,
196
+ uid: op.u,
197
+ columns: cols,
198
+ resolution: 'op-held',
199
+ opHlc: op.h,
200
+ };
201
+ }
202
+ /**
203
+ * A reference of `op` to a uid in an open uid collision here, or null
204
+ * (T13397). Until the collision settles the uid names two rows, so the
205
+ * reference waits rather than bind to the wrong one; after the loser's K its
206
+ * origin's earlier references are read through the alias.
207
+ */
208
+ function collidingRef(db, op, def, inTxn) {
209
+ if (!op.a || !hasTable(db, '_sync_conflict'))
210
+ return null;
211
+ const open = db.prepare(`SELECT 1 FROM _sync_conflict WHERE kind = 'uid-collision' AND tbl = ? AND uid = ? AND resolved_at IS NULL LIMIT 1`);
212
+ for (const [col, v] of Object.entries(op.a)) {
213
+ const target = def.refs.get(col);
214
+ if (!target || typeof v !== 'string' || inTxn.has(rowKey(target.table, v)))
215
+ continue;
216
+ if (open.get(target.table, v) !== undefined)
217
+ return { col, uid: v };
218
+ }
219
+ return null;
220
+ }
221
+ /** Whether a uid collision on (table, uid) was ever recorded here (T13399). */
222
+ function hadUidCollision(db, table, uid) {
223
+ return (hasTable(db, '_sync_conflict') &&
224
+ db
225
+ .prepare(`SELECT 1 FROM _sync_conflict WHERE kind = 'uid-collision' AND tbl = ? AND uid = ? LIMIT 1`)
226
+ .get(table, uid) !== undefined);
227
+ }
164
228
  /**
165
229
  * A missing reference of `op` (a target never seen), or null. A target the
166
230
  * transaction itself inserts or re-keys to counts as present wherever its op
@@ -236,6 +300,25 @@ function planTxn(db, api, st, defs, localReplica) {
236
300
  holds: holds(),
237
301
  };
238
302
  }
303
+ const ambiguous = collidingRef(db, op, def, txnRows);
304
+ if (ambiguous !== null) {
305
+ return {
306
+ kind: 'pending',
307
+ reason: `${op.t}/${op.u}: reference ${ambiguous.col} to ${ambiguous.uid}, a uid in an open collision`,
308
+ holds: holds(),
309
+ };
310
+ }
311
+ const keyClash = states.has(k)
312
+ ? null
313
+ : keyCollision(db, def, op, (uid) => states.get(rowKey(op.t, uid))?.live === false);
314
+ if (keyClash !== null) {
315
+ return {
316
+ kind: 'pending',
317
+ reason: `${op.t}/${op.u}: local key ${keyClash.columns.join(', ')} is held by another row`,
318
+ holds: holds(),
319
+ collision: { opIdx, conflict: keyClash },
320
+ };
321
+ }
239
322
  const before = states.get(k) ?? loadRowState(db, api, def, op.u, localReplica);
240
323
  let spec = specs.get(op.t);
241
324
  if (!spec) {
@@ -460,8 +543,15 @@ function applyRekey(c, opIdx, op) {
460
543
  const nu = op.nu ?? op.u;
461
544
  const oldLive = rowExists(c.api, op.t, op.u);
462
545
  const newLive = nu !== op.u && rowExists(c.api, op.t, nu);
546
+ // The re-keyed row's later ops, and its origin's earlier references, follow
547
+ // it (T12341 §6.4 step 1, T13397).
548
+ const oldFp = op.obfp ?? op.bfp;
549
+ if (oldFp && nu !== op.u) {
550
+ recordUidAlias(c.db, { table: op.t, oldUid: op.u, oldBfp: oldFp, newUid: nu, origin: c.st.replicaId, hlc: op.h }, c.nowIso);
551
+ }
552
+ // Already re-keyed, deleted, or never placed here (a held loser): the alias is all.
463
553
  if (!oldLive)
464
- return { result: 'skipped', conflicts: 0 }; // already re-keyed, or deleted
554
+ return { result: 'skipped', conflicts: 0 };
465
555
  // A K names the row it moves by (uid, old fingerprint): a live row holding
466
556
  // that uid with another fingerprint is the collision's winner, and a re-key
467
557
  // of the other row never touches it (T12341 §6.4, T13394).
@@ -484,6 +574,12 @@ function applyRekey(c, opIdx, op) {
484
574
  // and it would no longer match its `*K` intent.
485
575
  remapPending(c.db, { table: op.t, oldUid: op.u, newUid: nu, newBfp: op.bfp ?? null });
486
576
  c.api.rekeyRow(op.t, op.u, nu, op.bfp ?? null);
577
+ // Another replica settled a uid collision by re-keying a loser this replica
578
+ // had placed: its own earlier references to the old uid meant this row, so
579
+ // it owes the re-key's announcement (T13399).
580
+ if (op.obfp && nu !== op.u && c.st.replicaId !== c.replica && hadUidCollision(c.db, op.t, op.u)) {
581
+ markAliasPlaced(c.db, op.t, op.u, op.obfp);
582
+ }
487
583
  return { result: 'applied', conflicts: 0 };
488
584
  }
489
585
  /**
@@ -599,7 +695,9 @@ function applyOne(c, opIdx, op) {
599
695
  const st = c.replay
600
696
  ? { ...c.st, replicaId: c.replica, txn: { ...c.st.txn, actor: c.replay.actor } }
601
697
  : c.st;
602
- const n = effect(c.db, c.api, st, opIdx, { op, def, before, out, refKeys }, c.nowIso, c.replay !== undefined);
698
+ const history = c.history === true && out.status !== 'void';
699
+ const recorded = effect(c.db, c.api, st, opIdx, { op, def, before, out, refKeys }, c.nowIso, c.replay !== undefined || history);
700
+ const n = history ? 0 : recorded;
603
701
  c.db.exec(`RELEASE ${sp}`);
604
702
  const result = out.status === 'applied' || out.status === 'partial'
605
703
  ? 'applied'
@@ -1085,7 +1183,15 @@ function applyInPage(x, st) {
1085
1183
  const local = x.sequencingOn && st.replicaId === opts.replica ? unsequencedLocalTxn(db, st.txn.txn) : null;
1086
1184
  // An own echo whose transaction the page rewound applies it at its stream position.
1087
1185
  const rewoundEcho = local !== null && x.rewound.has(local.txn);
1088
- const c = { db, api, st, defs, replica: opts.replica, nowIso };
1186
+ const c = {
1187
+ db,
1188
+ api,
1189
+ st,
1190
+ defs,
1191
+ replica: opts.replica,
1192
+ nowIso,
1193
+ history: isInheritedHistory(db, opts.stream, st.replicaId, st.key.seq),
1194
+ };
1089
1195
  let n = 0;
1090
1196
  // Gate C (§3.6): the whole transaction is one savepoint, so a broken
1091
1197
  // multi-row invariant rolls all of it back (the page's rewind stays).
@@ -1165,6 +1271,8 @@ function applyInPage(x, st) {
1165
1271
  reason: n > 0 ? `${n} conflict(s) recorded` : null,
1166
1272
  nowIso,
1167
1273
  });
1274
+ // A collision it was held on has settled: it went through (T13397).
1275
+ resolveHeldConflicts(db, st.key, nowIso);
1168
1276
  return { status, holds: [], n, applied: true, decided: rewoundEcho && local ? local.txn : null };
1169
1277
  }
1170
1278
  /**
@@ -1216,94 +1324,109 @@ export function applyStagedTxns(db, opts) {
1216
1324
  let passes = 0;
1217
1325
  let rebased = 0;
1218
1326
  const heldTxns = new Set();
1219
- for (let progress = true; progress && passes < maxPasses;) {
1220
- progress = false;
1221
- passes += 1;
1222
- const heldReplicas = new Set();
1223
- const held = new Set(); // rows written by a pending transaction
1224
- const staged = stagedTxns(db, opts.stream).map((st) => withoutLocalRowids(st, defs));
1225
- // Each page is one frame and one scoped rebase (§3.5 Rule 3): rewind the
1226
- // page's scope once, apply its transactions in stream order, replay once.
1227
- for (let at = 0; at < staged.length;) {
1228
- const page = takePage(staged, at, opts.pageOps ?? REBASE_PAGE_OPS);
1229
- const pageStart = now();
1230
- const done = withApplyFrame(db, opts.scope, page[0]?.actor ?? null, (api) => {
1231
- const pageIso = new Date(pageStart).toISOString();
1232
- for (const p of page)
1233
- adoptNaturalRows(db, api, defs, p.st.txn.ops);
1234
- const planned = sequencingOn
1235
- ? planPageRebase(db, page.map((p) => p.st), defs, opts.replica)
1236
- : { plan: null, inPlace: new Set() };
1237
- const rebase = planned.plan;
1238
- const c0 = {
1239
- db,
1240
- api,
1241
- st: page[0].st,
1242
- defs,
1243
- replica: opts.replica,
1244
- nowIso: pageIso,
1245
- };
1246
- const rw = rebase ? rewindTxns(c0, rebase) : { kept: new Map(), after: new Map() };
1247
- const rewound = new Set(rebase?.rewind.map((l) => l.txn) ?? []);
1248
- const decided = new Set(); // rewound locals whose echo this page applied
1249
- // The time bound covers applying, not the rewind: a large scope must
1250
- // not shrink every page to one transaction that rewinds it again.
1251
- const applyStart = now();
1252
- let taken = 0;
1253
- for (const { st } of page) {
1254
- // The time bound cuts a page between transactions (never inside one).
1255
- if (taken > 0 && now() - applyStart > (opts.pageMs ?? REBASE_PAGE_MS))
1256
- break;
1257
- taken += 1;
1258
- const nowMs = now();
1259
- const nowIso = new Date(nowMs).toISOString();
1260
- const id = keyText(st.key);
1261
- const result = applyInPage({
1327
+ const runPasses = () => {
1328
+ for (let progress = true; progress && passes < maxPasses;) {
1329
+ progress = false;
1330
+ passes += 1;
1331
+ const heldReplicas = new Set();
1332
+ const held = new Set(); // rows written by a pending transaction
1333
+ const staged = stagedTxns(db, opts.stream).map((st) => followUidAliases(db, withoutLocalRowids(st, defs), defs));
1334
+ // Each page is one frame and one scoped rebase (§3.5 Rule 3): rewind the
1335
+ // page's scope once, apply its transactions in stream order, replay once.
1336
+ for (let at = 0; at < staged.length;) {
1337
+ const page = takePage(staged, at, opts.pageOps ?? REBASE_PAGE_OPS);
1338
+ const pageStart = now();
1339
+ const done = withApplyFrame(db, opts.scope, page[0]?.actor ?? null, (api) => {
1340
+ const pageIso = new Date(pageStart).toISOString();
1341
+ for (const p of page)
1342
+ adoptNaturalRows(db, api, defs, p.st.txn.ops);
1343
+ const planned = sequencingOn
1344
+ ? planPageRebase(db, page.map((p) => p.st), defs, opts.replica)
1345
+ : { plan: null, inPlace: new Set() };
1346
+ const rebase = planned.plan;
1347
+ const c0 = {
1262
1348
  db,
1263
1349
  api,
1350
+ st: page[0].st,
1264
1351
  defs,
1265
- opts,
1266
- sequencingOn,
1267
- rewound,
1268
- inPlace: planned.inPlace,
1269
- rw,
1270
- nowMs,
1271
- nowIso,
1272
- held,
1273
- heldReplicas,
1274
- }, st);
1275
- if (result.decided)
1276
- decided.add(result.decided);
1277
- for (const t of result.heldTxns ?? [])
1278
- heldTxns.add(t);
1279
- if (result.status !== null)
1280
- last.set(id, result.status);
1281
- conflicts += result.n;
1282
- if (rebase && result.applied)
1283
- rebased += 1;
1284
- if (result.status === 'held-skew')
1285
- heldReplicas.add(st.replicaId);
1286
- if (result.status === 'pending')
1287
- for (const k of result.holds)
1288
- held.add(k);
1289
- if (result.status === 'applied' ||
1290
- result.status === 'conflict' ||
1291
- result.status === 'void') {
1292
- progress = true;
1352
+ replica: opts.replica,
1353
+ nowIso: pageIso,
1354
+ };
1355
+ const rw = rebase
1356
+ ? rewindTxns(c0, rebase)
1357
+ : { kept: new Map(), after: new Map() };
1358
+ const rewound = new Set(rebase?.rewind.map((l) => l.txn) ?? []);
1359
+ const decided = new Set(); // rewound locals whose echo this page applied
1360
+ // The time bound covers applying, not the rewind: a large scope must
1361
+ // not shrink every page to one transaction that rewinds it again.
1362
+ const applyStart = now();
1363
+ let taken = 0;
1364
+ for (const { st } of page) {
1365
+ // The time bound cuts a page between transactions (never inside one).
1366
+ if (taken > 0 && now() - applyStart > (opts.pageMs ?? REBASE_PAGE_MS))
1367
+ break;
1368
+ taken += 1;
1369
+ const nowMs = now();
1370
+ const nowIso = new Date(nowMs).toISOString();
1371
+ const id = keyText(st.key);
1372
+ const result = applyInPage({
1373
+ db,
1374
+ api,
1375
+ defs,
1376
+ opts,
1377
+ sequencingOn,
1378
+ rewound,
1379
+ inPlace: planned.inPlace,
1380
+ rw,
1381
+ nowMs,
1382
+ nowIso,
1383
+ held,
1384
+ heldReplicas,
1385
+ }, st);
1386
+ if (result.decided)
1387
+ decided.add(result.decided);
1388
+ for (const t of result.heldTxns ?? [])
1389
+ heldTxns.add(t);
1390
+ if (result.status !== null)
1391
+ last.set(id, result.status);
1392
+ conflicts += result.n;
1393
+ if (rebase && result.applied)
1394
+ rebased += 1;
1395
+ if (result.status === 'held-skew')
1396
+ heldReplicas.add(st.replicaId);
1397
+ if (result.status === 'pending')
1398
+ for (const k of result.holds)
1399
+ held.add(k);
1400
+ if (result.status === 'applied' ||
1401
+ result.status === 'conflict' ||
1402
+ result.status === 'void') {
1403
+ progress = true;
1404
+ }
1293
1405
  }
1294
- }
1295
- // Replay what the page rewound and did not decide by its echo.
1296
- if (rebase) {
1297
- const replay = rebase.rewind.filter((l) => !decided.has(l.txn));
1298
- for (const t of replayTxns(c0, { rewind: rebase.rewind, replay, own: null }, rw)) {
1299
- heldTxns.add(t);
1406
+ // Replay what the page rewound and did not decide by its echo.
1407
+ if (rebase) {
1408
+ const replay = rebase.rewind.filter((l) => !decided.has(l.txn));
1409
+ for (const t of replayTxns(c0, { rewind: rebase.rewind, replay, own: null }, rw)) {
1410
+ heldTxns.add(t);
1411
+ }
1300
1412
  }
1301
- }
1302
- return taken;
1303
- });
1304
- at += done;
1413
+ return taken;
1414
+ });
1415
+ at += done;
1416
+ }
1305
1417
  }
1418
+ };
1419
+ runPasses();
1420
+ // The origin of a lost uid collision re-keys its row, then the held winner
1421
+ // places under the freed uid (T12341 §6.4, T13397).
1422
+ if (settleLostUidCollisions(db, opts).length > 0) {
1423
+ opts.seal?.();
1424
+ runPasses();
1306
1425
  }
1426
+ // Every other replica that had placed a re-keyed loser announces the
1427
+ // re-key, so its earlier references follow the loser too (T13399).
1428
+ if (announcePlacedRekeys(db, opts, new Date(now()).toISOString()) > 0)
1429
+ opts.seal?.();
1307
1430
  const count = (s) => [...last.values()].filter((v) => v === s).length;
1308
1431
  return {
1309
1432
  applied: count('applied'),