@abloatai/humans 0.48.0 → 0.49.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 (106) hide show
  1. package/dist/client.d.ts +6 -0
  2. package/dist/humans.d.ts +1 -1
  3. package/dist/local/RuntimeContext.d.ts +1 -1
  4. package/dist/local/SyncClient.d.ts +1 -1
  5. package/dist/local/SyncClient.js +3 -3
  6. package/dist/local/client/createModelProxy.d.ts +7 -1
  7. package/dist/local/client/createModelProxy.js +146 -39
  8. package/dist/local/client/options.d.ts +1 -1
  9. package/dist/local/client/reactiveEngine.js +64 -7
  10. package/dist/local/client/resourceTypes.d.ts +1 -1
  11. package/dist/local/client/resourceTypes.js +1 -1
  12. package/dist/local/client/storeCluster.d.ts +3 -0
  13. package/dist/local/client/storeCluster.js +4 -2
  14. package/dist/local/client/storeLifecycle.d.ts +2 -0
  15. package/dist/local/client/storeLifecycle.js +1 -0
  16. package/dist/local/client/wsMutationExecutor.d.ts +4 -1
  17. package/dist/local/client/wsMutationExecutor.js +26 -2
  18. package/dist/local/context.js +5 -1
  19. package/dist/local/interfaces/index.d.ts +3 -1
  20. package/dist/local/mutators/UndoManager.d.ts +1 -1
  21. package/dist/local/mutators/UndoManager.js +1 -1
  22. package/dist/local/query/client.js +15 -0
  23. package/dist/local/query/types.d.ts +14 -0
  24. package/dist/local/stores/syncAction.d.ts +1 -1
  25. package/dist/local/stores/syncAction.js +1 -1
  26. package/dist/local/sync/ConnectionManager.d.ts +1 -1
  27. package/dist/local/sync/ConnectionManager.js +1 -1
  28. package/dist/local/sync/OnDemandLoader.d.ts +3 -0
  29. package/dist/local/sync/OnDemandLoader.js +15 -2
  30. package/dist/local/sync/SyncWebSocket.d.ts +1 -1
  31. package/dist/local/sync/SyncWebSocket.js +1 -1
  32. package/dist/local/sync/commitFrames.d.ts +1 -1
  33. package/dist/local/sync/commitFrames.js +1 -1
  34. package/dist/local/sync/contextPorts.d.ts +1 -1
  35. package/dist/local/sync/contextPorts.js +1 -1
  36. package/dist/local/sync/credentialLifecycle.d.ts +1 -1
  37. package/dist/local/sync/credentialLifecycle.js +1 -1
  38. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  39. package/dist/local/sync/wsFrameHandlers.js +1 -1
  40. package/dist/local/transactions/databaseCommitOutbox.d.ts +1 -1
  41. package/dist/local/transactions/localMutation.d.ts +1 -1
  42. package/dist/local/transactions/localMutation.js +1 -1
  43. package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -2
  44. package/dist/local/transactions/mutations/MutationQueue.js +4 -4
  45. package/dist/local/transactions/mutations/batchProcessing.d.ts +1 -1
  46. package/dist/local/transactions/mutations/commitApi.d.ts +1 -1
  47. package/dist/local/transactions/mutations/commitLane.d.ts +1 -1
  48. package/dist/local/transactions/mutations/commitPayload.d.ts +2 -1
  49. package/dist/local/transactions/mutations/commitPayload.js +8 -0
  50. package/dist/local/transactions/mutations/commitTransport.d.ts +1 -1
  51. package/dist/local/transactions/mutations/commitTransport.js +1 -1
  52. package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
  53. package/dist/local/transactions/mutations/durableCommitRestore.js +1 -1
  54. package/dist/local/transactions/mutations/durableWriteStore.d.ts +4 -4
  55. package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
  56. package/dist/local/transactions/mutations/localMutation.d.ts +1 -1
  57. package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
  58. package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
  59. package/dist/local/transactions/mutations/pendingDrain.js +1 -1
  60. package/dist/local/transactions/mutations/replayValidation.d.ts +13 -1
  61. package/dist/local/transactions/mutations/replayValidation.js +3 -2
  62. package/dist/local/transactions/reconnectDrain.d.ts +1 -1
  63. package/dist/react/useSyncStatus.d.ts +1 -1
  64. package/dist/react/useSyncStatus.js +1 -1
  65. package/package.json +2 -2
  66. package/src/Ablo.ts +1 -1
  67. package/src/client.ts +7 -0
  68. package/src/humans.ts +1 -1
  69. package/src/local/RuntimeContext.ts +1 -1
  70. package/src/local/SyncClient.ts +3 -3
  71. package/src/local/client/createModelProxy.ts +236 -55
  72. package/src/local/client/options.ts +1 -1
  73. package/src/local/client/reactiveEngine.ts +75 -4
  74. package/src/local/client/resourceTypes.ts +1 -1
  75. package/src/local/client/storeCluster.ts +12 -2
  76. package/src/local/client/storeLifecycle.ts +3 -0
  77. package/src/local/client/wsMutationExecutor.ts +43 -1
  78. package/src/local/context.ts +5 -1
  79. package/src/local/interfaces/index.ts +7 -5
  80. package/src/local/mutators/UndoManager.ts +1 -1
  81. package/src/local/query/client.ts +17 -0
  82. package/src/local/query/types.ts +16 -1
  83. package/src/local/stores/syncAction.ts +1 -1
  84. package/src/local/sync/ConnectionManager.ts +1 -1
  85. package/src/local/sync/OnDemandLoader.ts +20 -3
  86. package/src/local/sync/SyncWebSocket.ts +1 -1
  87. package/src/local/sync/commitFrames.ts +1 -1
  88. package/src/local/sync/contextPorts.ts +1 -1
  89. package/src/local/sync/credentialLifecycle.ts +1 -1
  90. package/src/local/sync/wsFrameHandlers.ts +1 -1
  91. package/src/local/transactions/databaseCommitOutbox.ts +1 -1
  92. package/src/local/transactions/localMutation.ts +1 -1
  93. package/src/local/transactions/mutations/MutationQueue.ts +4 -4
  94. package/src/local/transactions/mutations/batchProcessing.ts +1 -1
  95. package/src/local/transactions/mutations/commitApi.ts +1 -1
  96. package/src/local/transactions/mutations/commitLane.ts +1 -1
  97. package/src/local/transactions/mutations/commitPayload.ts +10 -1
  98. package/src/local/transactions/mutations/commitTransport.ts +1 -1
  99. package/src/local/transactions/mutations/durableCommitRestore.ts +2 -2
  100. package/src/local/transactions/mutations/durableWriteStore.ts +4 -4
  101. package/src/local/transactions/mutations/localMutation.ts +1 -1
  102. package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
  103. package/src/local/transactions/mutations/pendingDrain.ts +2 -2
  104. package/src/local/transactions/mutations/replayValidation.ts +3 -2
  105. package/src/local/transactions/reconnectDrain.ts +1 -1
  106. package/src/react/useSyncStatus.ts +1 -1
@@ -20,11 +20,12 @@ import {
20
20
  import {
21
21
  reconcileFunctionalUpdate,
22
22
  type ModelUpdater,
23
- type ContentionOptions,
23
+ type FunctionalUpdateOptions,
24
24
  } from '@abloatai/transaction/resources/functionalUpdate';
25
25
  import type { MutationOptions } from '../interfaces/index.js';
26
26
  import {
27
27
  claimDescription,
28
+ type ReadDependency,
28
29
  type TrackDependency,
29
30
  } from '@abloatai/transaction/coordination/schema';
30
31
  import { Model, modelAsRow } from '../Model.js';
@@ -63,7 +64,7 @@ import type {
63
64
  } from '@abloatai/transaction/types/streams';
64
65
 
65
66
  // The request contract — every option and parameter shape a caller passes to a
66
- // read, a write, or a claim — lives in the settlement core (ADR 0016). This
67
+ // read, a write, or a claim — lives in the confirmation core (ADR 0016). This
67
68
  // factory binds it to reactive model instances; the shapes themselves are
68
69
  // transport- and consumer-agnostic. Re-exported so `./createModelProxy` stays a
69
70
  // working import path for the whole surface.
@@ -122,8 +123,19 @@ import {
122
123
  claimQueueView,
123
124
  resolveClaimContentionOptions,
124
125
  } from '@abloatai/transaction/resources/modelOperations';
125
- import type { HttpModelClient } from '@abloatai/transaction/transport/httpClient';
126
+ import type {
127
+ CapturedRow,
128
+ HttpModelClient,
129
+ } from '@abloatai/transaction/transport/httpClient';
126
130
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
131
+ import {
132
+ abortReadSetCommit,
133
+ capturePointRead,
134
+ consumeReadSet,
135
+ prepareReadSet,
136
+ type PreparedReadSet,
137
+ type ReadSetContext,
138
+ } from '@abloatai/transaction/internal/read-set';
127
139
 
128
140
  export interface ModelClientMeta {
129
141
  readonly key: string;
@@ -150,6 +162,8 @@ type EntityHalf = Pick<ModelTarget, 'model' | 'id'>;
150
162
  // `ModelCollaboration<Task>` and `ModelCollaboration<Invoice>` the same type
151
163
  // while reading as though they differed.
152
164
  export interface ModelCollaboration {
165
+ /** Exact point evidence from the HTTP read boundary (stamp captured before data). */
166
+ readPoint(model: string, id: string): Promise<{ data: unknown; stamp: number }>;
153
167
  createClaim(options: {
154
168
  /**
155
169
  * The locator, in the spelling the SDK surface and the HTTP routes use.
@@ -403,9 +417,12 @@ export function createModelProxy<T, C>(
403
417
  * collaborator, a test most of all, is pushed into a cast through `unknown`
404
418
  * to supply the one method that is actually read.
405
419
  */
406
- hydration: Pick<OnDemandLoader, 'fetch'>,
420
+ hydration: Pick<OnDemandLoader, 'fetch'> &
421
+ Partial<Pick<OnDemandLoader, 'getReadEvidence'>>,
407
422
  collaboration?: ModelCollaboration,
423
+ readSetContext?: ReadSetContext,
408
424
  ): ModelOperations<T, C> {
425
+ const readSetClientIdentity = syncClient as object;
409
426
  /**
410
427
  * Resolve a row **this** resource owns.
411
428
  *
@@ -541,29 +558,56 @@ export function createModelProxy<T, C>(
541
558
  typeof (value as { id?: unknown }).id === 'string' &&
542
559
  typeof (value as { release?: unknown }).release === 'function';
543
560
 
544
- const mutationOptions = (
561
+ const preparedMutation = (
545
562
  params:
546
563
  | ModelCreateParams<T, C>
547
564
  | ModelUpdateParams<T, C>
548
565
  | ModelDeleteParams<T, C>,
549
- ): MutationOptions => {
566
+ ): { options: MutationOptions; prepared: PreparedReadSet } => {
567
+ const prepared = prepareReadSet(
568
+ readSetContext,
569
+ readSetClientIdentity,
570
+ params.readAt,
571
+ params.onStale,
572
+ params.idempotencyKey,
573
+ params.reads,
574
+ );
550
575
  const rest: MutationOptions = {
551
- ...(params.idempotencyKey !== undefined
552
- ? { idempotencyKey: params.idempotencyKey }
576
+ ...(prepared.idempotencyKey !== undefined
577
+ ? { idempotencyKey: prepared.idempotencyKey }
578
+ : params.idempotencyKey !== undefined
579
+ ? { idempotencyKey: params.idempotencyKey }
553
580
  : {}),
554
581
  ...(params.label !== undefined ? { label: params.label } : {}),
555
- ...(params.readAt !== undefined ? { readAt: params.readAt } : {}),
556
- ...(params.onStale !== undefined ? { onStale: params.onStale } : {}),
582
+ ...(prepared.readAt !== undefined
583
+ ? { readAt: prepared.readAt }
584
+ : params.readAt !== undefined
585
+ ? { readAt: params.readAt }
586
+ : {}),
587
+ ...(prepared.onStale !== undefined
588
+ ? { onStale: prepared.onStale }
589
+ : params.onStale !== undefined
590
+ ? { onStale: params.onStale }
591
+ : {}),
557
592
  ...(params.fenceToken !== undefined ? { fenceToken: params.fenceToken } : {}),
558
593
  ...(params.claimRef !== undefined ? { claimRef: params.claimRef } : {}),
559
- ...(params.reads !== undefined ? { reads: params.reads } : {}),
594
+ ...(prepared.reads !== undefined
595
+ ? { reads: prepared.reads === null ? null : [...prepared.reads] }
596
+ : params.reads !== undefined
597
+ ? { reads: params.reads }
598
+ : {}),
560
599
  ...(params.track !== undefined ? { track: params.track } : {}),
561
600
  };
562
601
  // The write-options schema — the runtime twin of the compile-time params.
563
602
  // Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
564
603
  // call site with a typed error instead of a silent no-op or a server 400.
565
- assertWriteOptions(rest, `${schemaKey} write`);
566
- return rest;
604
+ try {
605
+ assertWriteOptions(rest, `${schemaKey} write`);
606
+ } catch (error) {
607
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
608
+ throw error;
609
+ }
610
+ return { options: rest, prepared };
567
611
  };
568
612
 
569
613
  const releaseClaim = async (id: string): Promise<void> => {
@@ -1074,7 +1118,7 @@ export function createModelProxy<T, C>(
1074
1118
  };
1075
1119
 
1076
1120
  const get = guard(
1077
- async (params: ModelRetrieveParams): Promise<T | undefined> => {
1121
+ async (params: ModelRetrieveParams): Promise<CapturedRow<T> | undefined> => {
1078
1122
  // Read-interest enrolment: authoritative point reads enter the same
1079
1123
  // entity scope as the claim stream, while remaining settled reads.
1080
1124
  void collaboration?.enterScope?.({ [schemaKey]: params.id });
@@ -1083,10 +1127,56 @@ export function createModelProxy<T, C>(
1083
1127
  where: [['id', params.id]],
1084
1128
  limit: 1,
1085
1129
  });
1086
- return rows[0];
1130
+ if (readSetContext?.getStore() && collaboration) {
1131
+ const read = await collaboration.readPoint(schemaKey, params.id);
1132
+ const data = (read.data ?? undefined) as T | undefined;
1133
+ capturePointRead(
1134
+ readSetContext,
1135
+ readSetClientIdentity,
1136
+ wireModel,
1137
+ params.id,
1138
+ data,
1139
+ read.stamp,
1140
+ );
1141
+ return data as CapturedRow<T> | undefined;
1142
+ }
1143
+ return rows[0] as CapturedRow<T> | undefined;
1087
1144
  },
1088
1145
  );
1089
1146
 
1147
+ const list = guard(async (
1148
+ options?: ServerReadOptions<T>,
1149
+ ): Promise<CapturedRow<T>[]> => {
1150
+ const registry = readSetContext?.getStore();
1151
+ const rows = await load(options);
1152
+ if (!registry) return rows as CapturedRow<T>[];
1153
+ for (const row of rows) {
1154
+ const stamp = hydration.getReadEvidence?.(row as object);
1155
+ if (stamp === undefined) {
1156
+ // Local-first/lazy rows remain valid reads, but cannot later be used as
1157
+ // guarded dependencies. `prepareReadSet` rejects them if supplied in
1158
+ // `reads`; authoritative complete reads carry evidence here.
1159
+ continue;
1160
+ }
1161
+ const id = (row as { id?: unknown }).id;
1162
+ if (typeof id !== 'string' || id.length === 0) {
1163
+ throw new AbloValidationError(
1164
+ `${registeredModelName}.list returned a row without an id.`,
1165
+ { code: 'commit_no_result' },
1166
+ );
1167
+ }
1168
+ capturePointRead(
1169
+ readSetContext,
1170
+ readSetClientIdentity,
1171
+ wireModel,
1172
+ id,
1173
+ row,
1174
+ stamp,
1175
+ );
1176
+ }
1177
+ return rows as CapturedRow<T>[];
1178
+ });
1179
+
1090
1180
  const operations: ModelOperations<T, C> = {
1091
1181
  local,
1092
1182
 
@@ -1095,11 +1185,10 @@ export function createModelProxy<T, C>(
1095
1185
 
1096
1186
  // No automatic scope enrolment on bulk `list`: that would subscribe to an
1097
1187
  // unbounded set of rows' entity groups.
1098
- list: guard(load),
1188
+ list,
1099
1189
 
1100
1190
  create: guardWrite(async (params: ModelCreateParams<T, C>): Promise<T> => {
1101
1191
  const id = params.id ?? Model.generateId();
1102
- const opts = mutationOptions(params);
1103
1192
  const claim = params.claim;
1104
1193
  let autoLease: Claim | undefined;
1105
1194
  if (claim && !isClaimHandle(claim)) {
@@ -1146,15 +1235,41 @@ export function createModelProxy<T, C>(
1146
1235
  createdAt: new Date(),
1147
1236
  updatedAt: new Date(),
1148
1237
  });
1149
- const effective: MutationOptions = {
1150
- ...opts,
1151
- ...(autoLease ? { claim: autoLease } : {}),
1152
- ...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
1153
- };
1238
+ let prepared: PreparedReadSet | undefined;
1154
1239
  try {
1240
+ const resolved = preparedMutation(params);
1241
+ prepared = resolved.prepared;
1242
+ const effective: MutationOptions = {
1243
+ ...resolved.options,
1244
+ ...(autoLease
1245
+ ? {
1246
+ claimRef: { id: autoLease.id },
1247
+ ...(autoLease.fenceToken !== undefined
1248
+ ? { fenceToken: autoLease.fenceToken }
1249
+ : {}),
1250
+ }
1251
+ : {}),
1252
+ ...(isClaimHandle(claim)
1253
+ ? {
1254
+ claimRef: { id: claim.id },
1255
+ ...(claim.fenceToken !== undefined
1256
+ ? { fenceToken: claim.fenceToken }
1257
+ : {}),
1258
+ }
1259
+ : {}),
1260
+ };
1155
1261
  syncClient.add(model, effective);
1156
1262
  await waitForMutation(model);
1263
+ consumeReadSet(
1264
+ readSetContext,
1265
+ readSetClientIdentity,
1266
+ prepared.consumed,
1267
+ prepared.automaticCommit,
1268
+ );
1157
1269
  return modelAsRow<T>(model);
1270
+ } catch (error) {
1271
+ abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
1272
+ throw error;
1158
1273
  } finally {
1159
1274
  await autoLease?.release?.().catch(() => {});
1160
1275
  }
@@ -1169,7 +1284,7 @@ export function createModelProxy<T, C>(
1169
1284
  async (
1170
1285
  arg: ModelUpdateParams<T, C> | string,
1171
1286
  updater?: ModelUpdater<T>,
1172
- contention?: ContentionOptions,
1287
+ contention?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
1173
1288
  ): Promise<T | undefined> => {
1174
1289
  // Functional form: update(id, current => next). Same guarantee as the
1175
1290
  // HTTP client (shared reconcile loop), implemented with this transport's
@@ -1194,18 +1309,29 @@ export function createModelProxy<T, C>(
1194
1309
  { code: 'model_claim_not_configured' },
1195
1310
  );
1196
1311
  }
1197
- return reconcileFunctionalUpdate<T, T>(updater, contention, {
1312
+ return reconcileFunctionalUpdate<
1313
+ T,
1314
+ T,
1315
+ ReadDependency | CapturedRow
1316
+ >(updater, contention, {
1198
1317
  model: registeredModelName,
1199
1318
  id,
1200
1319
  readFresh: async () => {
1201
1320
  // `type: 'complete'` forces the round-trip — the hydration ledger
1202
1321
  // would otherwise serve a possibly-stale local row for a hydrated id.
1203
1322
  await load({ where: [['id', id]], type: 'complete' });
1204
- const fresh = ownRowOrThrow(id);
1205
- const snapshot = collaboration.createSnapshot(schemaKey, id);
1323
+ const exact = await collaboration.readPoint(wireModel, id);
1324
+ capturePointRead(
1325
+ readSetContext,
1326
+ readSetClientIdentity,
1327
+ wireModel,
1328
+ id,
1329
+ exact.data,
1330
+ exact.stamp,
1331
+ );
1206
1332
  return {
1207
- data: fresh ? modelAsRow<T>(fresh) : undefined,
1208
- stamp: snapshot.stamp,
1333
+ data: exact.data as T | undefined,
1334
+ stamp: exact.stamp,
1209
1335
  };
1210
1336
  },
1211
1337
  writeNext: async (patch, readAt) => {
@@ -1216,14 +1342,39 @@ export function createModelProxy<T, C>(
1216
1342
  { code: 'entity_not_found' },
1217
1343
  );
1218
1344
  }
1219
- const effective: MutationOptions = {
1220
- readAt,
1221
- onStale: 'reject',
1222
- };
1223
- model.applyChanges(patch);
1224
- syncClient.update(model, effective);
1225
- await waitForMutation(model);
1226
- return modelAsRow<T>(model);
1345
+ const prepared = prepareReadSet(
1346
+ readSetContext,
1347
+ readSetClientIdentity,
1348
+ undefined,
1349
+ 'reject',
1350
+ undefined,
1351
+ contention?.reads,
1352
+ );
1353
+ try {
1354
+ const effective: MutationOptions = {
1355
+ readAt: prepared.readAt ?? readAt,
1356
+ onStale: prepared.onStale ?? 'reject',
1357
+ ...(prepared.idempotencyKey
1358
+ ? { idempotencyKey: prepared.idempotencyKey }
1359
+ : {}),
1360
+ ...(prepared.reads !== undefined
1361
+ ? { reads: prepared.reads === null ? null : [...prepared.reads] }
1362
+ : {}),
1363
+ };
1364
+ model.applyChanges(patch);
1365
+ syncClient.update(model, effective);
1366
+ await waitForMutation(model);
1367
+ consumeReadSet(
1368
+ readSetContext,
1369
+ readSetClientIdentity,
1370
+ prepared.consumed,
1371
+ prepared.automaticCommit,
1372
+ );
1373
+ return modelAsRow<T>(model);
1374
+ } catch (error) {
1375
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
1376
+ throw error;
1377
+ }
1227
1378
  },
1228
1379
  });
1229
1380
  }
@@ -1256,13 +1407,17 @@ export function createModelProxy<T, C>(
1256
1407
  // If we hold a claim on this row, guard the write with its snapshot
1257
1408
  // watermark + lease so it's stale-rejected and attributed to the claim.
1258
1409
  const claimed = activeClaims.get(id);
1259
- const opts = mutationOptions(params);
1410
+ const resolved = preparedMutation(params);
1411
+ const opts = resolved.options;
1260
1412
  const handle = isClaimHandle(params.claim) ? params.claim : undefined;
1261
1413
  const effective: MutationOptions | undefined = claimed
1262
1414
  ? {
1263
1415
  readAt: claimed.lease.readAt ?? claimed.snapshot.stamp,
1264
1416
  onStale: 'reject',
1265
1417
  claimRef: { id: claimed.lease.id },
1418
+ ...(claimed.lease.fenceToken !== undefined
1419
+ ? { fenceToken: claimed.lease.fenceToken }
1420
+ : {}),
1266
1421
  ...opts,
1267
1422
  }
1268
1423
  : {
@@ -1279,28 +1434,39 @@ export function createModelProxy<T, C>(
1279
1434
  }
1280
1435
  : {}),
1281
1436
  ...opts,
1282
- ...(handle ? { claim: { id: handle.id } } : {}),
1437
+ ...(handle ? { claimRef: { id: handle.id } } : {}),
1283
1438
  };
1284
1439
  // Local user update: `applyChanges` keeps change tracking on so the
1285
1440
  // edited fields land in `modifiedProperties` and are actually sent to
1286
1441
  // the server. (`updateFromData` is the hydration path and would discard
1287
1442
  // the tracking, producing an empty `input: {}` no-op mutation.)
1288
- model.applyChanges(params.data);
1289
- syncClient.update(model, effective);
1290
- await waitForMutation(model);
1291
- return modelAsRow<T>(model);
1443
+ try {
1444
+ model.applyChanges(params.data);
1445
+ syncClient.update(model, effective);
1446
+ await waitForMutation(model);
1447
+ consumeReadSet(
1448
+ readSetContext,
1449
+ readSetClientIdentity,
1450
+ resolved.prepared.consumed,
1451
+ resolved.prepared.automaticCommit,
1452
+ );
1453
+ return modelAsRow<T>(model);
1454
+ } catch (error) {
1455
+ abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
1456
+ throw error;
1457
+ }
1292
1458
  },
1293
1459
  );
1294
1460
  function update(params: ModelUpdateParams<T, C>): Promise<T>;
1295
1461
  function update(
1296
1462
  id: string,
1297
1463
  updater: ModelUpdater<T>,
1298
- options?: ContentionOptions,
1464
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
1299
1465
  ): Promise<T | undefined>;
1300
1466
  function update(
1301
1467
  arg: ModelUpdateParams<T, C> | string,
1302
1468
  updater?: ModelUpdater<T>,
1303
- contention?: ContentionOptions,
1469
+ contention?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
1304
1470
  ): Promise<T | undefined> {
1305
1471
  return updateImpl(arg, updater, contention);
1306
1472
  }
@@ -1338,7 +1504,8 @@ export function createModelProxy<T, C>(
1338
1504
  // the same row).
1339
1505
  if (!model) return;
1340
1506
  const claimed = activeClaims.get(id);
1341
- const opts = mutationOptions(params);
1507
+ const resolved = preparedMutation(params);
1508
+ const opts = resolved.options;
1342
1509
  const handle = isClaimHandle(params.claim) ? params.claim : undefined;
1343
1510
  const effective: MutationOptions | undefined = claimed
1344
1511
  ? {
@@ -1351,17 +1518,31 @@ export function createModelProxy<T, C>(
1351
1518
  ...opts,
1352
1519
  }
1353
1520
  : {
1354
- ...(handle?.readAt !== undefined
1355
- ? {
1356
- readAt: handle.readAt,
1357
- onStale: 'reject' as const,
1358
- }
1359
- : {}),
1360
- ...opts,
1361
- ...(handle ? { claim: { id: handle.id } } : {}),
1521
+ ...(handle?.readAt !== undefined
1522
+ ? {
1523
+ readAt: handle.readAt,
1524
+ onStale: 'reject' as const,
1525
+ ...(handle.fenceToken !== undefined
1526
+ ? { fenceToken: handle.fenceToken }
1527
+ : {}),
1528
+ }
1529
+ : {}),
1530
+ ...opts,
1531
+ ...(handle ? { claimRef: { id: handle.id } } : {}),
1362
1532
  };
1363
- syncClient.delete(model, effective);
1364
- await waitForMutation(model);
1533
+ try {
1534
+ syncClient.delete(model, effective);
1535
+ await waitForMutation(model);
1536
+ consumeReadSet(
1537
+ readSetContext,
1538
+ readSetClientIdentity,
1539
+ resolved.prepared.consumed,
1540
+ resolved.prepared.automaticCommit,
1541
+ );
1542
+ } catch (error) {
1543
+ abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
1544
+ throw error;
1545
+ }
1365
1546
  }),
1366
1547
 
1367
1548
  // `claim` is a callable namespace (take a claim) carrying the coordination
@@ -20,7 +20,7 @@ import type {
20
20
  DurableWriteStore,
21
21
  DurableWritesConfig,
22
22
  } from '@abloatai/transaction/durableWrites';
23
- import type { CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
23
+ import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
24
24
 
25
25
  // ── Options ───────────────────────────────────────────────────────────────
26
26
 
@@ -17,12 +17,13 @@ import { omittedModelError } from '@abloatai/transaction/schema/select';
17
17
  import {
18
18
  durableCommitOperationSchema,
19
19
  type DurableCommitOperation,
20
- } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
20
+ } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
21
21
  import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
22
22
  import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
23
23
  import type { BatchFence } from '@abloatai/transaction/coordination';
24
24
  import {
25
25
  batchFence,
26
+ claimIdFor,
26
27
  fenceTokenFor,
27
28
  modelTarget,
28
29
  streamTarget,
@@ -69,6 +70,13 @@ import {
69
70
  import { createModelProxy, type ModelOperations } from './createModelProxy.js';
70
71
  import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
71
72
  import type { AbloClient as Ablo } from '../../client.js';
73
+ import {
74
+ modelReadResponseSchema,
75
+ commitRecordSchema,
76
+ commitRecordListSchema,
77
+ commitRecordWhereSchema,
78
+ } from '@abloatai/transaction/wire';
79
+ import { translateHttpError } from '@abloatai/transaction/errors';
72
80
 
73
81
  /**
74
82
  * What the reactive build is fed: the factory's pass over the options bag
@@ -123,12 +131,39 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
123
131
  createSibling,
124
132
  } = inputs;
125
133
  const schema = options.schema;
134
+ const pointReadBaseUrl = resolveBootstrapBaseUrl({
135
+ url,
136
+ bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
137
+ });
138
+
139
+ async function readPoint(model: string, id: string): Promise<{ data: unknown; stamp: number }> {
140
+ const fetchImpl = internalOptions.fetch ?? globalThis.fetch;
141
+ const response = await fetchImpl(
142
+ `${pointReadBaseUrl}/v1/models/${encodeURIComponent(model)}/${encodeURIComponent(id)}`,
143
+ { headers: authCredentials.withAuthHeaders() },
144
+ );
145
+ let body: unknown;
146
+ try {
147
+ body = await response.json();
148
+ } catch {
149
+ body = null;
150
+ }
151
+ if (!response.ok) throw translateHttpError(response.status, body);
152
+ const parsed = modelReadResponseSchema.safeParse(body);
153
+ if (!parsed.success) {
154
+ throw new AbloConnectionError('Model point-read response failed validation.', {
155
+ code: 'commit_no_result',
156
+ cause: parsed.error,
157
+ });
158
+ }
159
+ return { data: parsed.data.data, stamp: parsed.data.stamp };
160
+ }
126
161
 
127
162
  // The store cluster — this client's runtime, the component graph, the
128
163
  // registered models, and the store — was constructed by `humans().init`
129
164
  // from the widened plugin context (see `./storeCluster.ts`). The engine
130
165
  // assembles around it.
131
- const { components, store } = cluster;
166
+ const { components, store, readSetContext } = cluster;
132
167
  const {
133
168
  modelRegistry,
134
169
  objectPool,
@@ -198,6 +233,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
198
233
  /** Resolved account scope — seeded once identity resolution completes;
199
234
  * exposed as the readonly `ablo.organizationId` accessor. */
200
235
  let _resolvedOrganizationId: string | null = null;
236
+ let _resolvedIdentity: import('@abloatai/transaction/auth').EffectiveAuthority | null = null;
201
237
  const lifecycle = startStoreLifecycle({
202
238
  cluster,
203
239
  schema,
@@ -210,10 +246,11 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
210
246
  kind,
211
247
  logger,
212
248
  validationError: _validationError,
213
- onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups }) => {
249
+ onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups, authority }) => {
214
250
  selfParticipantId = userId;
215
251
  selfParticipantKind = participantKind;
216
252
  _resolvedOrganizationId = accountScope;
253
+ _resolvedIdentity = authority;
217
254
  presenceStream.setParticipant({
218
255
  id: userId,
219
256
  kind: participantKind,
@@ -260,6 +297,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
260
297
  op: CommitOperationInput,
261
298
  defaults: Pick<CommitCreateOptions, 'readAt' | 'onStale'>,
262
299
  fence: BatchFence | null,
300
+ claim: Claim | null,
263
301
  ): DurableCommitOperation {
264
302
  const type = op.action.toUpperCase();
265
303
  const id = op.id ?? '';
@@ -269,6 +307,8 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
269
307
  id,
270
308
  input: op.data ?? undefined,
271
309
  transactionId: op.transactionId ?? undefined,
310
+ claimId:
311
+ op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null) ?? undefined,
272
312
  readAt: op.readAt ?? defaults.readAt ?? undefined,
273
313
  onStale: op.onStale ?? defaults.onStale ?? undefined,
274
314
  fenceToken:
@@ -287,7 +327,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
287
327
  );
288
328
  }
289
329
  return commitOptions.operations.map((op) =>
290
- normalizeCommitOperation(op, commitOptions, fence),
330
+ normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null),
291
331
  );
292
332
  }
293
333
 
@@ -548,6 +588,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
548
588
  hydration,
549
589
  {
550
590
  createClaim: (claimOptions) => publicClaims.create(claimOptions),
591
+ readPoint,
551
592
  createSnapshot: (modelKey, id) =>
552
593
  createSnapshot({
553
594
  pool: objectPool,
@@ -607,6 +648,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
607
648
  ...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
608
649
  }),
609
650
  },
651
+ readSetContext,
610
652
  );
611
653
  }
612
654
 
@@ -668,6 +710,31 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
668
710
  ...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
669
711
  };
670
712
  },
713
+ async get({ id }) {
714
+ await ready();
715
+ const response = await (internalOptions.fetch ?? globalThis.fetch)(
716
+ `${pointReadBaseUrl}/v1/commits/${encodeURIComponent(id)}`,
717
+ { headers: authCredentials.withAuthHeaders() },
718
+ );
719
+ const body: unknown = await response.json().catch(() => null);
720
+ if (!response.ok) throw translateHttpError(response.status, body);
721
+ return commitRecordSchema.nullable().parse(body);
722
+ },
723
+ async list(listOptions = {}) {
724
+ await ready();
725
+ const where = commitRecordWhereSchema.parse(listOptions.where ?? {});
726
+ const params = new URLSearchParams();
727
+ if (where.actorId) params.set('actorId', where.actorId);
728
+ if (where.status) params.set('status', where.status);
729
+ const query = params.size > 0 ? `?${params.toString()}` : '';
730
+ const response = await (internalOptions.fetch ?? globalThis.fetch)(
731
+ `${pointReadBaseUrl}/v1/commits${query}`,
732
+ { headers: authCredentials.withAuthHeaders() },
733
+ );
734
+ const body: unknown = await response.json().catch(() => null);
735
+ if (!response.ok) throw translateHttpError(response.status, body);
736
+ return commitRecordListSchema.parse(body);
737
+ },
671
738
  };
672
739
 
673
740
  /**
@@ -767,6 +834,10 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
767
834
  return _resolvedOrganizationId;
768
835
  },
769
836
 
837
+ get identity() {
838
+ return _resolvedIdentity;
839
+ },
840
+
770
841
  nudgeReconnect() {
771
842
  store.nudgeReconnect();
772
843
  },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The interactive resource-type surface.
3
3
  *
4
- * The transport-facing half of these types moved down into the settlement core
4
+ * The transport-facing half of these types moved down into the confirmation core
5
5
  * (ADR 0016). This module keeps the
6
6
  * consumer-facing import path intact and rejoins it with the model-proxy types
7
7
  * that stay here — `ModelOperations` chief among them, because it returns the