@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
@@ -38,6 +38,10 @@ import { registerModelsFromSchema } from './modelRegistration.js';
38
38
  import { deriveConfigFromSchema } from './schemaConfig.js';
39
39
  import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
40
40
  import type { InternalAbloOptions } from './options.js';
41
+ import {
42
+ createReadSetContext,
43
+ type ReadSetContext,
44
+ } from '@abloatai/transaction/internal/read-set';
41
45
  export type { InternalAbloOptions } from './options.js';
42
46
 
43
47
  /**
@@ -55,6 +59,8 @@ export interface StoreCluster {
55
59
  readonly components: InternalComponents;
56
60
  /** The store orchestrating the graph, holding the host-built connection. */
57
61
  readonly store: BaseSyncedStore;
62
+ /** Client-local opaque evidence registry; never an ambient execution scope. */
63
+ readonly readSetContext: ReadSetContext;
58
64
  }
59
65
 
60
66
  /**
@@ -99,6 +105,7 @@ export function buildStoreCluster(
99
105
  return null;
100
106
  }
101
107
  const schema = options.schema;
108
+ const readSetContext = createReadSetContext();
102
109
 
103
110
  // Config derives from the schema; caller-supplied overrides layer on top,
104
111
  // caller winning per key.
@@ -112,7 +119,10 @@ export function buildStoreCluster(
112
119
  // error and the MutationQueue owns the retry. A caller-supplied executor
113
120
  // still wins (test mocks, alternative transports).
114
121
  const executor: MutationExecutor =
115
- options.mutationExecutor ?? createDefaultMutationExecutor(() => transport);
122
+ options.mutationExecutor ?? createDefaultMutationExecutor(
123
+ () => transport,
124
+ readSetContext,
125
+ );
116
126
 
117
127
  // This client's runtime — the instance the whole graph is constructed
118
128
  // with, so two clients in one process never read each other's logger,
@@ -168,5 +178,5 @@ export function buildStoreCluster(
168
178
  { collaborationEvents: options.collaborationEvents ?? [] },
169
179
  );
170
180
 
171
- return { runtime, components, store };
181
+ return { runtime, components, store, readSetContext };
172
182
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { SchemaRecord, Schema } from '@abloatai/transaction/schema/schema';
13
13
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
14
+ import type { EffectiveAuthority } from '@abloatai/transaction/auth';
14
15
  import type { Logger } from '@abloatai/transaction/logger';
15
16
  import type { AuthCredentialSource } from '@abloatai/transaction/auth/credentialSource';
16
17
  import type { RefreshScheduler } from '@abloatai/transaction/auth';
@@ -32,6 +33,7 @@ export interface IdentitySeed {
32
33
  /** The resolved account scope; null until known. */
33
34
  readonly accountScope: string | null;
34
35
  readonly syncGroups: readonly string[];
36
+ readonly authority: EffectiveAuthority;
35
37
  }
36
38
 
37
39
  export interface StoreLifecycle {
@@ -282,6 +284,7 @@ export function startStoreLifecycle<S extends SchemaRecord>(
282
284
  participantKind,
283
285
  accountScope,
284
286
  syncGroups: resolvedSyncGroups,
287
+ authority: resolved.authority,
285
288
  });
286
289
 
287
290
  if (resolved.refreshScheduler) {
@@ -12,6 +12,12 @@ import type {
12
12
  MutationOptions,
13
13
  } from '../interfaces/index.js';
14
14
  import type { CommitAck } from '../sync/commitFrames.js';
15
+ import type { CommitReceiptWire } from '@abloatai/transaction/wire/commit';
16
+ import {
17
+ recordWebSocketCommitReceipt,
18
+ type ReadSetContext,
19
+ } from '@abloatai/transaction/internal/read-set';
20
+ import { commitAckSchema } from '@abloatai/transaction/wire/commit';
15
21
  import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
16
22
 
17
23
  // ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
@@ -33,6 +39,13 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
33
39
  */
34
40
  export function createDefaultMutationExecutor(
35
41
  getWs: () => {
42
+ sendCommitReceipt?: (
43
+ operations: readonly MutationOperation[],
44
+ clientTxId: string,
45
+ timeoutMs?: number,
46
+ reads?: readonly ReadDependency[] | null,
47
+ track?: readonly TrackDependency[] | null,
48
+ ) => Promise<CommitReceiptWire>;
36
49
  sendCommit?: (
37
50
  operations: readonly MutationOperation[],
38
51
  clientTxId: string,
@@ -41,13 +54,14 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
41
54
  track?: readonly TrackDependency[] | null,
42
55
  ) => Promise<CommitAck>;
43
56
  } | null,
57
+ readSetContext?: ReadSetContext,
44
58
  ): MutationExecutor {
45
59
  async function commit(
46
60
  operations: MutationOperation[],
47
61
  options?: MutationOptions,
48
62
  ) {
49
63
  const ws = getWs();
50
- if (!ws?.sendCommit) {
64
+ if (!ws || (!ws.sendCommit && !ws.sendCommitReceipt)) {
51
65
  throw new AbloConnectionError(
52
66
  'SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
53
67
  'before mutations can be sent.',
@@ -60,6 +74,34 @@ import { AbloError, AbloConnectionError } from '@abloatai/transaction/errors';
60
74
  ? crypto.randomUUID()
61
75
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
62
76
  try {
77
+ if (ws.sendCommitReceipt) {
78
+ const receipt = await ws.sendCommitReceipt(
79
+ operations,
80
+ clientTxId,
81
+ undefined,
82
+ options?.reads,
83
+ options?.track,
84
+ );
85
+ recordWebSocketCommitReceipt(readSetContext, {
86
+ receipt,
87
+ operations,
88
+ reads: options?.reads,
89
+ track: options?.track,
90
+ });
91
+ return commitAckSchema.parse({
92
+ status: receipt.status,
93
+ statusAt: receipt.statusAt,
94
+ lastSyncId: receipt.lastSyncId,
95
+ ...(receipt.correlationId ? { correlationId: receipt.correlationId } : {}),
96
+ ...(receipt.notifications ? { notifications: receipt.notifications } : {}),
97
+ ...(receipt.missingIds ? { missingIds: receipt.missingIds } : {}),
98
+ });
99
+ }
100
+ if (!ws.sendCommit) {
101
+ throw new AbloConnectionError('SyncWebSocket commit transport is unavailable.', {
102
+ code: 'ws_not_ready',
103
+ });
104
+ }
63
105
  return await ws.sendCommit(
64
106
  operations,
65
107
  clientTxId,
@@ -86,7 +86,11 @@ const _fallback: RuntimeContext = {
86
86
  config: emptyConfig,
87
87
  getModelMetadata: () => undefined,
88
88
  mutationExecutor: {
89
- commit: () => Promise.resolve({ lastSyncId: 0, status: 'confirmed' as const }),
89
+ commit: () => Promise.resolve({
90
+ status: 'confirmed' as const,
91
+ statusAt: '1970-01-01T00:00:00.000Z',
92
+ lastSyncId: 0,
93
+ }),
90
94
  executeCreate: () => Promise.resolve(),
91
95
  executeUpdate: () => Promise.resolve(null),
92
96
  executeDelete: () => Promise.resolve(),
@@ -18,7 +18,7 @@ export type { CoordinationObservability } from '@abloatai/transaction/observabil
18
18
  // ─────────────────────────────────────────────
19
19
 
20
20
  // The logging port carries no framework and no local state, so it lives in the
21
- // settlement core (ADR 0016). Re-exported here so `interfaces` stays the single
21
+ // confirmation core (ADR 0016). Re-exported here so `interfaces` stays the single
22
22
  // place a consumer looks for the contracts it implements. The `import type` is
23
23
  // load-bearing: `SyncLogger = Logger` below needs the name bound in this module.
24
24
  export type { Logger } from '@abloatai/transaction/logger';
@@ -29,7 +29,7 @@ import type { Logger } from '@abloatai/transaction/logger';
29
29
  // ─────────────────────────────────────────────
30
30
 
31
31
  // The transport-facing slice — breadcrumbs and socket errors — moved to the
32
- // settlement core with the duplex transport (ADR 0016): a socket held for
32
+ // confirmation core with the duplex transport (ADR 0016): a socket held for
33
33
  // claim push must report its lifecycle with no store present. Re-exported
34
34
  // here so `interfaces` stays the single place a consumer looks; the `import
35
35
  // type` is load-bearing for `ObservabilityProvider extends` below.
@@ -143,7 +143,7 @@ export interface ObservabilityProvider
143
143
  captureSelfHealing(details: SelfHealingDetails): void;
144
144
 
145
145
  // `captureClaim` and `captureConflict` are inherited from
146
- // `CoordinationObservability` in the core — the settlement layer reports those
146
+ // `CoordinationObservability` in the core — the confirmation layer reports those
147
147
  // two on its own behalf, with no store present.
148
148
 
149
149
  /** Capture commit returning lastSyncId: 0 */
@@ -225,7 +225,7 @@ export type MutationCommitResult = MutationCommitResultInput;
225
225
  // `MutationOptions` describes how a write is issued — request identity, commit
226
226
  // disposition, and the premise it rests on (optimistic via `readAt`/`reads`, or
227
227
  // claim-protected via `claimRef`/`fenceToken`) — and holds no local row state,
228
- // so it lives in the settlement core (ADR 0016). Re-exported here so the
228
+ // so it lives in the confirmation core (ADR 0016). Re-exported here so the
229
229
  // existing `interfaces` import path keeps resolving.
230
230
  // The `import type` is load-bearing, not redundant: `export type { X } from`
231
231
  // re-exports without binding X in this module's scope, and `Pick<X, K>` on an
@@ -243,7 +243,7 @@ import type { MutationOptions } from '@abloatai/transaction/resources/mutationOp
243
243
  */
244
244
  export type WriteOptions = Pick<
245
245
  MutationOptions,
246
- 'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken'
246
+ 'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
247
247
  >;
248
248
 
249
249
  /** A single mutation within a batch. Its `options` travel with it so the server
@@ -265,6 +265,8 @@ export interface MutationOperation {
265
265
  * the wire at once.
266
266
  */
267
267
  transactionId?: string;
268
+ /** Server-issued claim identity the operation is attributed to. */
269
+ claimId?: string | null;
268
270
  readAt?: number | null;
269
271
  onStale?: OnStaleMode | null;
270
272
  /**
@@ -365,7 +365,7 @@ export class UndoScope<S extends Schema> {
365
365
  * Run `work` after every previously-enqueued scope operation has settled,
366
366
  * in invocation order. The internal `tail` always resolves (failures are
367
367
  * swallowed *for the chain only*) so one rejected mutator can't wedge the
368
- * queue; the original settlement is still surfaced to this call's caller.
368
+ * queue; the original confirmation is still surfaced to this call's caller.
369
369
  */
370
370
  private enqueue<T>(work: () => Promise<T>): Promise<T> {
371
371
  const result = this.tail.then(work, work);
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { z } from 'zod';
16
+ import { modelListEvidenceSchema } from '@abloatai/transaction/wire';
16
17
  import type { QueryBatch, QueryBatchResult } from './types.js';
17
18
  import { translateHttpError } from '@abloatai/transaction/errors';
18
19
  import { classifyRecovery, type RecoveryClass } from '@abloatai/transaction/errorCodes';
@@ -37,6 +38,16 @@ const QueryResultSchema = z
37
38
  const QueryBatchResultSchema = z
38
39
  .object({
39
40
  results: z.array(QueryResultSchema),
41
+ evidence: z.array(z.array(modelListEvidenceSchema)).optional(),
42
+ errors: z.array(z.object({
43
+ index: z.number().int().nonnegative(),
44
+ model: z.string(),
45
+ type: z.string(),
46
+ code: z.string(),
47
+ message: z.string(),
48
+ request_id: z.string().optional(),
49
+ event_id: z.string(),
50
+ })).optional(),
40
51
  })
41
52
  .loose();
42
53
 
@@ -189,6 +200,12 @@ export async function postQuery(
189
200
  });
190
201
  return { results: batch.queries.map(() => []) };
191
202
  }
203
+ for (const failure of parsed.data.errors ?? []) {
204
+ runtime.logger.warn(
205
+ `Could not load ${failure.model} — ${failure.message} (code: ${failure.code}, ref: ${failure.request_id ?? failure.event_id}). No results were returned for slot ${failure.index}.`,
206
+ );
207
+ runtime.logger.debug('[postQuery.error] query slot failure', failure);
208
+ }
192
209
  return parsed.data;
193
210
  } finally {
194
211
  clearTimeout(timer);
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  // The where grammar describes the *request*, not any local copy of the rows it
18
- // returns, so it lives in the settlement core (ADR 0016). Re-exported here so
18
+ // returns, so it lives in the confirmation core (ADR 0016). Re-exported here so
19
19
  // the existing `query/types` import path keeps resolving.
20
20
  export type {
21
21
  WherePrimitive,
@@ -24,6 +24,7 @@ export type {
24
24
  LoadWhere,
25
25
  } from '@abloatai/transaction/resources/where';
26
26
  import type { WhereClause } from '@abloatai/transaction/resources/where';
27
+ import type { ModelListEvidence } from '@abloatai/transaction/wire';
27
28
 
28
29
  /** A single structured fetch request. */
29
30
  export interface Query {
@@ -93,6 +94,18 @@ export interface QueryBatchResult {
93
94
  * before use.
94
95
  */
95
96
  results: unknown[];
97
+ /** Per-result-slot evidence for the primary rows in that slot. */
98
+ evidence?: readonly (readonly QueryReadEvidence[])[];
99
+ /** Per-slot failures that previously looked identical to an empty result. */
100
+ errors?: readonly {
101
+ index: number;
102
+ model: string;
103
+ type: string;
104
+ code: string;
105
+ message: string;
106
+ request_id?: string;
107
+ event_id: string;
108
+ }[];
96
109
  /**
97
110
  * The server watermark observed after the batch ran. Model reads expose this
98
111
  * as `stamp`, and callers thread it into `commits.create({ readAt })` so the
@@ -100,3 +113,5 @@ export interface QueryBatchResult {
100
113
  */
101
114
  lastSyncId?: number;
102
115
  }
116
+
117
+ export type QueryReadEvidence = ModelListEvidence;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The persisted local action record consumed by `SyncActionStore`. It is a
3
3
  * client materialization artifact, not a transaction-layer wire shape: the
4
- * authoritative broadcast vocabulary is the settlement core's delta wire
4
+ * authoritative broadcast vocabulary is the confirmation core's delta wire
5
5
  * schema, while this one adds the local `__class` discriminator IndexedDB uses.
6
6
  */
7
7
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Moved to the settlement core with the duplex transport (ADR 0016): the
2
+ * Moved to the confirmation core with the duplex transport (ADR 0016): the
3
3
  * connection state machine is plain, mobx-free plumbing an agent needs as much
4
4
  * as a browser does. This path re-exports it so existing importers stay
5
5
  * unchanged; the reactive store mirrors its transitions through
@@ -121,6 +121,7 @@ function snapshotDoesNotAdvanceModel(data: Record<string, unknown>, model: Model
121
121
  }
122
122
 
123
123
  export class OnDemandLoader {
124
+ private readonly readEvidence = new WeakMap<object, number>();
124
125
  private readonly inFlight = new Map<string, Promise<Model[]>>();
125
126
  /**
126
127
  * Query keys with a background confirm currently in flight. Distinct from
@@ -224,6 +225,11 @@ export class OnDemandLoader {
224
225
  return work;
225
226
  }
226
227
 
228
+ /** Exact evidence retained for a row returned by a server-confirmed fetch. */
229
+ getReadEvidence(row: object): number | undefined {
230
+ return this.readEvidence.get(row);
231
+ }
232
+
227
233
  private async runFetch(
228
234
  modelName: string,
229
235
  typename: string,
@@ -337,7 +343,8 @@ export class OnDemandLoader {
337
343
  clauses: readonly WhereClause[],
338
344
  options: FetchOptions<unknown> | undefined,
339
345
  ): Promise<Model[]> {
340
- const networkRows = await this.queryNetwork(modelName, clauses, options);
346
+ const network = await this.queryNetwork(modelName, clauses, options);
347
+ const networkRows = network.rows;
341
348
  const networkModels = networkRows
342
349
  // Strict: a row the server returned whose type name this client never
343
350
  // registered is a genuine schema collision (the pushed schema differs
@@ -346,6 +353,12 @@ export class OnDemandLoader {
346
353
  .map((raw) => this.hydrateOne(raw, typename, { strict: true }))
347
354
  .filter((m): m is Model => m !== null);
348
355
 
356
+ const evidenceById = new Map(network.evidence.map((entry) => [entry.id, entry.stamp]));
357
+ for (const model of networkModels) {
358
+ const stamp = evidenceById.get(model.id);
359
+ if (stamp !== undefined) this.readEvidence.set(model, stamp);
360
+ }
361
+
349
362
  if (networkModels.length > 0) {
350
363
  this.opts.objectPool.addBatch(networkModels, ModelScope.live);
351
364
  // Background IDB write — don't block the caller. Expanded children are
@@ -558,7 +571,10 @@ export class OnDemandLoader {
558
571
  modelName: string,
559
572
  clauses: readonly WhereClause[],
560
573
  options: FetchOptions<unknown> | undefined,
561
- ): Promise<unknown[]> {
574
+ ): Promise<{
575
+ rows: unknown[];
576
+ evidence: readonly { id: string; stamp: number }[];
577
+ }> {
562
578
  const typename = this.resolveTypename(modelName);
563
579
  const orderEntries = options?.orderBy ? Object.entries(options.orderBy) : [];
564
580
  const firstOrder = orderEntries[0];
@@ -586,6 +602,7 @@ export class OnDemandLoader {
586
602
  { queries: [query] },
587
603
  );
588
604
  const rows: unknown[] = Array.isArray(result.results[0]) ? result.results[0] : [];
605
+ const evidence = result.evidence?.[0] ?? [];
589
606
  // Normalize: wire rows lack `__typename` when the server elides it.
590
607
  const normalized = rows.map((row) => {
591
608
  if (row && typeof row === 'object' && !('__typename' in row)) {
@@ -605,7 +622,7 @@ export class OnDemandLoader {
605
622
  if (options?.expand && options.expand.length > 0) {
606
623
  this.hydrateExpanded(modelName, normalized, options.expand);
607
624
  }
608
- return normalized;
625
+ return { rows: normalized, evidence };
609
626
  }
610
627
 
611
628
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The reactive engine's connection to the sync server: the settlement core's
2
+ * The reactive engine's connection to the sync server: the confirmation core's
3
3
  * duplex transport ({@link WsTransport}, ADR 0016) plus everything that turns
4
4
  * the stream into a local, watchable copy — wire-delta validation, the resume
5
5
  * cursor and its persistence-gated ack discipline, incremental sync and
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Moved to the settlement core with the duplex transport (ADR 0016): the
2
+ * Moved to the confirmation core with the duplex transport (ADR 0016): the
3
3
  * commit-path frame builders are stateless wire helpers. This path re-exports
4
4
  * them so existing importers stay unchanged. The core's `buildCommitFrame`
5
5
  * takes the structural `CommitFrameOperation` slice, which this package's
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The runtime context, sliced into the ports the settlement core's transport
2
+ * The runtime context, sliced into the ports the confirmation core's transport
3
3
  * takes. The core is injected, never service-located (ADR 0016), while this
4
4
  * package resolves its shared dependencies through `getContext()` — so these
5
5
  * adapters bridge the two: each method reads the context at call time, which
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Moved to the settlement core with the duplex transport (ADR 0016): keeping
2
+ * Moved to the confirmation core with the duplex transport (ADR 0016): keeping
3
3
  * a long-lived socket's credential fresh is connection plumbing an agent needs
4
4
  * as much as a browser does. This path re-exports it so existing importers
5
5
  * stay unchanged.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Moved to the settlement core with the duplex transport (ADR 0016): the
2
+ * Moved to the confirmation core with the duplex transport (ADR 0016): the
3
3
  * inbound frame dispatch is the receiving half of the wire protocol, and the
4
4
  * handlers already worked against the minimal `WsSession` port rather than
5
5
  * the transport object. This path re-exports it so existing importers stay
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  DurableWriteStore,
3
3
  } from '@abloatai/transaction/durableWrites';
4
- import type { PendingWrite } from '@abloatai/transaction/transactions/settlement/pendingWrite';
4
+ import type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
5
5
 
6
6
  export interface CommitOutboxDatabase {
7
7
  sealTransactionRecord(
@@ -10,7 +10,7 @@ type EmitLocalMutation = (event: string, payload: unknown) => void;
10
10
  /**
11
11
  * Human-side optimistic-write adapter.
12
12
  *
13
- * Settlement retains ordering and receipts; this adapter owns the local
13
+ * Confirmation retains ordering and receipts; this adapter owns the local
14
14
  * pre-image ledger and announces apply/rollback work to the materialiser.
15
15
  */
16
16
  export function createLocalMutationPort(
@@ -87,7 +87,7 @@ import {
87
87
  type DurableCommitOperation,
88
88
  type DurableCommitOperationInput,
89
89
  type CommitOutboxScope,
90
- } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
90
+ } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
91
91
  import type { DurableWriteStore } from './durableWriteStore.js';
92
92
  import { stableStringify } from '@abloatai/transaction/utils/json';
93
93
  import {
@@ -126,7 +126,7 @@ import { handleConflict as resolveConflict, isPermanentError as classifyPermanen
126
126
  import { takeNextExecutionBatch as selectExecutionBatch, takePendingDrainBatch as selectPendingDrainBatch } from './executionSelection.js';
127
127
  import { scheduleProcessing as scheduleProcessingExternal, type ProcessingSchedulerContext } from './processingScheduler.js';
128
128
  import {
129
- drainPendingSettlements,
129
+ drainPendingConfirmations,
130
130
  type PendingDrainContext,
131
131
  } from './pendingDrain.js';
132
132
  import { restoreDurableCommits as restoreDurableCommitsExternal, type DurableCommitRestoreContext } from './durableCommitRestore.js';
@@ -286,7 +286,7 @@ export class MutationQueue extends EventEmitter {
286
286
  /**
287
287
  * Small race buffer for authoritative echoes that arrive before the queued
288
288
  * mutation receipt. The forward and WAL stream are independent channels, so
289
- * either can win without changing the settlement result.
289
+ * either can win without changing the confirmation result.
290
290
  */
291
291
  private recentDeltaCorrelations = new Map<string, number>();
292
292
  /**
@@ -1472,7 +1472,7 @@ export class MutationQueue extends EventEmitter {
1472
1472
  if (this.isProcessing) return;
1473
1473
  this.isProcessing = true;
1474
1474
  try {
1475
- await drainPendingSettlements(this.pendingDrainContext);
1475
+ await drainPendingConfirmations(this.pendingDrainContext);
1476
1476
  } finally {
1477
1477
  this.isProcessing = false;
1478
1478
  if (this.executionQueue.length > 0) this.scheduleProcessing(true);
@@ -6,7 +6,7 @@ import type { OptimisticUpdateEntry } from './localMutation.js';
6
6
  import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
7
7
  import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
8
8
  import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
9
- import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
9
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
10
10
  import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
11
11
  import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
12
12
  import type { MutationOperationType } from '@abloatai/transaction/types';
@@ -1,7 +1,7 @@
1
1
  import { AbloIdempotencyError } from '@abloatai/transaction/errors';
2
2
  import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
3
3
  import { stableStringify } from '@abloatai/transaction/utils/json';
4
- import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
4
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
5
5
  import type { CommitTransaction } from './commitLane.js';
6
6
  import type { SealDurableCommitInput } from './commitTransport.js';
7
7
 
@@ -4,7 +4,7 @@ import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
4
4
  import type {
5
5
  DurableCommitEnvelope,
6
6
  DurableCommitOperation,
7
- } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
7
+ } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
8
8
  import type { SealDurableCommitInput } from './commitTransport.js';
9
9
  import { transientRetryDelayMs } from './failureHandling.js';
10
10
 
@@ -15,7 +15,7 @@ import { MutationOperationType } from '@abloatai/transaction/types';
15
15
  import { snapshotJsonValue } from '@abloatai/transaction/utils/json';
16
16
  import type { OnStaleMode } from '@abloatai/transaction/coordination/schema';
17
17
  import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
18
- import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
18
+ import type { CommitEnvelopeMember } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
19
19
 
20
20
  export interface UserContext {
21
21
  userId: string;
@@ -208,6 +208,8 @@ export function hasStaleWriteOptions(options?: WriteOptions): boolean {
208
208
  export function hasCommitCoalescingBarrier(options?: WriteOptions): boolean {
209
209
  return (
210
210
  hasStaleWriteOptions(options) ||
211
+ options?.claimRef != null ||
212
+ options?.fenceToken != null ||
211
213
  typeof options?.idempotencyKey === 'string' ||
212
214
  typeof options?.label === 'string'
213
215
  );
@@ -217,6 +219,7 @@ export interface WriteOperationFields {
217
219
  readAt?: number | null;
218
220
  onStale?: OnStaleMode | null;
219
221
  fenceToken?: number | null;
222
+ claimId?: string;
220
223
  options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
221
224
  }
222
225
 
@@ -243,6 +246,12 @@ export function applyWriteOptions<T extends object>(
243
246
  if (writeOptions.fenceToken !== undefined) {
244
247
  operation.fenceToken = writeOptions.fenceToken;
245
248
  }
249
+ if (writeOptions.claimRef != null) {
250
+ operation.claimId =
251
+ typeof writeOptions.claimRef === 'string'
252
+ ? writeOptions.claimRef
253
+ : writeOptions.claimRef.id;
254
+ }
246
255
  if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
247
256
  operation.options = {
248
257
  ...(writeOptions.idempotencyKey != null
@@ -17,7 +17,7 @@ import {
17
17
  type CommitOutboxScope,
18
18
  type DurableCommitEnvelope,
19
19
  type DurableCommitOperation,
20
- } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
20
+ } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
21
21
  import type { DurableWriteStore } from './durableWriteStore.js';
22
22
 
23
23
  export interface CommitTransportContext {
@@ -1,9 +1,9 @@
1
1
  import { AbloIdempotencyError, AbloValidationError } from '@abloatai/transaction/errors';
2
2
  import type { RuntimeContext } from '../../RuntimeContext.js';
3
3
  import type { CommitTransaction } from './commitLane.js';
4
- import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
4
+ import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
5
5
  import type { DurableWriteStore } from './durableWriteStore.js';
6
- import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
6
+ import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
7
7
 
8
8
  export interface DurableCommitRestoreContext {
9
9
  readonly config: { enablePersistence: boolean };
@@ -1,12 +1,12 @@
1
1
  /**
2
- * The durable-write port moved to the settlement core (ADR 0016): it is a
2
+ * The durable-write port moved to the confirmation core (ADR 0016): it is a
3
3
  * contract over commit envelopes and holds no local rows. Re-exported here so
4
4
  * the existing `transactions/mutations/durableWriteStore` import path keeps
5
5
  * resolving for the queue, the outbox, and the client options.
6
6
  *
7
7
  * The port and its config live in the core's `durableWrites` module (a behavior
8
8
  * contract, not a persisted shape); the records that cross it are owned by
9
- * `transactions/settlement/pendingWrite`.
9
+ * `transactions/confirmation/pendingWrite`.
10
10
  */
11
11
 
12
12
  export {
@@ -17,5 +17,5 @@ export type {
17
17
  DurableWriteStore,
18
18
  DurableWritesConfig,
19
19
  } from '@abloatai/transaction/durableWrites';
20
- export { pendingWriteSchema } from '@abloatai/transaction/transactions/settlement/pendingWrite';
21
- export type { PendingWrite } from '@abloatai/transaction/transactions/settlement/pendingWrite';
20
+ export { pendingWriteSchema } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
21
+ export type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
@@ -30,7 +30,7 @@ export interface OptimisticEmitter {
30
30
  emit(event: string, payload: unknown): void;
31
31
  }
32
32
 
33
- /** Materializer capability consumed by settlement; implementation belongs to the local layer. */
33
+ /** Materializer capability consumed by confirmation; implementation belongs to the local layer. */
34
34
  export interface LocalMutationPort {
35
35
  readonly updates: Map<string, OptimisticUpdateEntry>;
36
36
  applyCreate(model: Model, transaction: QueuedMutation): void;
@@ -1,7 +1,7 @@
1
1
  import type { RuntimeContext } from '../../RuntimeContext.js';
2
2
  import type { MutationPersistencePort } from '../../mutationPersistence.js';
3
3
  import type { MutationQueueConfig } from './MutationQueue.js';
4
- import type { CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
4
+ import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
5
5
  import type { QueuedMutation } from './commitPayload.js';
6
6
  import { MutationStore } from './MutationStore.js';
7
7
  import { normalizeModelKey } from './commitPayload.js';
@@ -3,7 +3,7 @@ import type { QueuedMutation } from './commitPayload.js';
3
3
  import type { MutationStore } from './MutationStore.js';
4
4
  import type { OptimisticUpdateEntry } from './localMutation.js';
5
5
  import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
6
- import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
6
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
7
7
  import { applyWriteOptions, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
8
8
 
9
9
  export interface PendingDrainContext {
@@ -31,7 +31,7 @@ export interface PendingDrainContext {
31
31
  readonly emit: (event: string, payload: object) => boolean;
32
32
  }
33
33
 
34
- export async function drainPendingSettlements(ctx: PendingDrainContext): Promise<void> {
34
+ export async function drainPendingConfirmations(ctx: PendingDrainContext): Promise<void> {
35
35
  ctx.assertDurableReplayOpen();
36
36
  // Kick the commit lane too: atomic envelopes from `commits.create()` may
37
37
  // have been left at the head of the lane while the connection was down.