@abloatai/ablo 0.29.3 → 0.30.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 (118) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3 -2
  3. package/dist/BaseSyncedStore.js +3 -1
  4. package/dist/Database.js +29 -2
  5. package/dist/SyncClient.d.ts +5 -3
  6. package/dist/SyncClient.js +8 -4
  7. package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
  8. package/dist/ai-sdk/coordinatedTool.js +0 -1
  9. package/dist/ai-sdk/coordinationContext.js +1 -5
  10. package/dist/cli.cjs +377 -84
  11. package/dist/client/Ablo.d.ts +1 -1
  12. package/dist/client/Ablo.js +24 -36
  13. package/dist/client/auth.d.ts +8 -18
  14. package/dist/client/auth.js +16 -96
  15. package/dist/client/createModelProxy.d.ts +16 -11
  16. package/dist/client/createModelProxy.js +162 -20
  17. package/dist/client/httpClient.d.ts +1 -1
  18. package/dist/client/httpTransport.js +263 -89
  19. package/dist/client/options.d.ts +0 -15
  20. package/dist/client/resourceTypes.d.ts +13 -26
  21. package/dist/client/schemaConfig.js +5 -0
  22. package/dist/client/writeOptionsSchema.d.ts +1 -0
  23. package/dist/client/writeOptionsSchema.js +11 -2
  24. package/dist/client/wsMutationExecutor.d.ts +3 -5
  25. package/dist/coordination/schema.d.ts +17 -13
  26. package/dist/coordination/schema.js +36 -10
  27. package/dist/errorCodes.d.ts +9 -4
  28. package/dist/errorCodes.js +10 -5
  29. package/dist/errors.js +4 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/interfaces/index.d.ts +31 -19
  32. package/dist/schema/index.d.ts +1 -1
  33. package/dist/schema/index.js +4 -1
  34. package/dist/schema/schema.d.ts +13 -0
  35. package/dist/schema/select.js +7 -0
  36. package/dist/schema/syncDeltaRow.d.ts +31 -13
  37. package/dist/schema/syncDeltaRow.js +33 -15
  38. package/dist/schema/tenancy.d.ts +52 -16
  39. package/dist/schema/tenancy.js +86 -25
  40. package/dist/server/commit.d.ts +8 -24
  41. package/dist/server/index.d.ts +1 -0
  42. package/dist/server/index.js +1 -0
  43. package/dist/server/readConfig.d.ts +10 -0
  44. package/dist/source/adapter.d.ts +26 -15
  45. package/dist/source/adapter.js +5 -3
  46. package/dist/source/adapters/drizzle.js +35 -9
  47. package/dist/source/adapters/kysely.d.ts +33 -61
  48. package/dist/source/adapters/kysely.js +170 -177
  49. package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
  50. package/dist/source/adapters/kyselyMutationCore.js +125 -0
  51. package/dist/source/adapters/memory.d.ts +1 -1
  52. package/dist/source/adapters/memory.js +24 -10
  53. package/dist/source/adapters/prisma.js +35 -8
  54. package/dist/source/conformance.d.ts +26 -28
  55. package/dist/source/conformance.js +142 -69
  56. package/dist/source/contract.d.ts +46 -4
  57. package/dist/source/contract.js +66 -5
  58. package/dist/source/factory.d.ts +3 -3
  59. package/dist/source/factory.js +24 -3
  60. package/dist/source/idempotency.d.ts +46 -0
  61. package/dist/source/idempotency.js +129 -0
  62. package/dist/source/index.d.ts +6 -4
  63. package/dist/source/index.js +5 -3
  64. package/dist/source/migrations.d.ts +9 -5
  65. package/dist/source/migrations.js +39 -6
  66. package/dist/source/types.d.ts +88 -14
  67. package/dist/source/types.js +16 -0
  68. package/dist/surface.d.ts +1 -1
  69. package/dist/surface.js +0 -1
  70. package/dist/sync/BootstrapFetcher.js +7 -0
  71. package/dist/sync/SyncWebSocket.d.ts +5 -27
  72. package/dist/sync/awaitClaimGrant.d.ts +6 -0
  73. package/dist/sync/awaitClaimGrant.js +12 -2
  74. package/dist/sync/commitFrames.d.ts +2 -4
  75. package/dist/sync/commitFrames.js +1 -0
  76. package/dist/sync/createClaimStream.d.ts +1 -1
  77. package/dist/sync/createClaimStream.js +13 -15
  78. package/dist/sync/deltaPipeline.d.ts +1 -1
  79. package/dist/sync/deltaPipeline.js +1 -1
  80. package/dist/sync/participants.d.ts +2 -3
  81. package/dist/sync/participants.js +2 -2
  82. package/dist/sync/wsFrameHandlers.js +35 -15
  83. package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
  84. package/dist/testing/mocks/MockMutationExecutor.js +53 -1
  85. package/dist/transactions/TransactionQueue.d.ts +61 -9
  86. package/dist/transactions/TransactionQueue.js +502 -114
  87. package/dist/transactions/commitEnvelope.d.ts +4 -0
  88. package/dist/transactions/commitEnvelope.js +25 -0
  89. package/dist/transactions/commitPayload.d.ts +23 -5
  90. package/dist/transactions/commitPayload.js +3 -0
  91. package/dist/transactions/deltaConfirmation.d.ts +2 -1
  92. package/dist/transactions/deltaConfirmation.js +22 -4
  93. package/dist/transactions/durableWriteStore.d.ts +8 -1
  94. package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
  95. package/dist/transactions/httpCommitEnvelope.js +30 -0
  96. package/dist/transactions/replayValidation.d.ts +4 -0
  97. package/dist/transactions/replayValidation.js +6 -0
  98. package/dist/types/streams.d.ts +29 -17
  99. package/dist/wire/commit.d.ts +671 -0
  100. package/dist/wire/commit.js +300 -0
  101. package/dist/wire/delta.d.ts +4 -0
  102. package/dist/wire/delta.js +9 -0
  103. package/dist/wire/frames.d.ts +17 -45
  104. package/dist/wire/index.d.ts +2 -0
  105. package/dist/wire/index.js +4 -0
  106. package/docs/agent-messaging.md +4 -5
  107. package/docs/agents.md +1 -2
  108. package/docs/api-keys.md +1 -2
  109. package/docs/client-behavior.md +4 -6
  110. package/docs/coordination.md +64 -9
  111. package/docs/data-sources.md +4 -7
  112. package/docs/integration-guide.md +6 -8
  113. package/docs/migration.md +0 -3
  114. package/docs/quickstart.md +7 -8
  115. package/docs/schema-contract.md +7 -8
  116. package/package.json +4 -4
  117. package/dist/client/registerDataSource.d.ts +0 -19
  118. package/dist/client/registerDataSource.js +0 -66
@@ -33,6 +33,7 @@ export declare const durableCommitOperationSchema: z.ZodObject<{
33
33
  overwrite: "overwrite";
34
34
  notify: "notify";
35
35
  }>>>;
36
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
36
37
  model: z.ZodString;
37
38
  id: z.ZodString;
38
39
  input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -78,6 +79,7 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
78
79
  overwrite: "overwrite";
79
80
  notify: "notify";
80
81
  }>>>;
82
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
81
83
  model: z.ZodString;
82
84
  id: z.ZodString;
83
85
  input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -117,6 +119,8 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
117
119
  }, z.core.$strict>>;
118
120
  createdAt: z.ZodNumber;
119
121
  sealedAt: z.ZodNumber;
122
+ acceptedAt: z.ZodOptional<z.ZodNumber>;
123
+ correlationId: z.ZodOptional<z.ZodString>;
120
124
  sequence: z.ZodOptional<z.ZodNumber>;
121
125
  timestamp: z.ZodNumber;
122
126
  }, z.core.$strict>;
@@ -9,6 +9,7 @@
9
9
  import { z } from 'zod';
10
10
  import { readDependencySchema } from '../coordination/schema.js';
11
11
  import { commitOperationSchema } from '../wire/frames.js';
12
+ import { correlationIdSchema } from '../wire/commit.js';
12
13
  import { idempotencyKeySchema } from './idempotencyKey.js';
13
14
  export const COMMIT_ENVELOPE_VERSION = 1;
14
15
  export const COMMIT_ENVELOPE_RECORD_PREFIX = 'commit-envelope:';
@@ -32,6 +33,7 @@ export const durableCommitOperationSchema = commitOperationSchema
32
33
  transactionId: true,
33
34
  readAt: true,
34
35
  onStale: true,
36
+ fenceToken: true,
35
37
  })
36
38
  .extend({
37
39
  model: z.string().min(1),
@@ -74,6 +76,14 @@ export const durableCommitEnvelopeSchema = z
74
76
  scope: commitOutboxScopeSchema.optional(),
75
77
  createdAt: z.number().int().nonnegative(),
76
78
  sealedAt: z.number().int().nonnegative(),
79
+ /**
80
+ * Monotonic evidence that a connected source accepted this exact request.
81
+ * Its server and customer idempotency keys are permanent, so an accepted
82
+ * envelope remains replayable until the WAL echo confirms it.
83
+ */
84
+ acceptedAt: z.number().int().nonnegative().optional(),
85
+ /** Opaque server-derived source-batch identity paired with `acceptedAt`. */
86
+ correlationId: correlationIdSchema.optional(),
77
87
  /** Monotonic within one client; disambiguates writes sealed in the same ms. */
78
88
  sequence: z.number().int().nonnegative().optional(),
79
89
  timestamp: z.number().int().nonnegative(),
@@ -93,6 +103,21 @@ export const durableCommitEnvelopeSchema = z
93
103
  message: 'Source mutation ids must be unique',
94
104
  });
95
105
  }
106
+ if ((envelope.acceptedAt === undefined) !== (envelope.correlationId === undefined)) {
107
+ context.addIssue({
108
+ code: 'custom',
109
+ path: ['acceptedAt'],
110
+ message: 'Accepted commit envelopes require both acceptedAt and correlationId',
111
+ });
112
+ }
113
+ if (envelope.acceptedAt !== undefined &&
114
+ envelope.acceptedAt < envelope.sealedAt) {
115
+ context.addIssue({
116
+ code: 'custom',
117
+ path: ['acceptedAt'],
118
+ message: 'Commit envelope cannot be accepted before it is sealed',
119
+ });
120
+ }
96
121
  if (envelope.origin === 'model_batch') {
97
122
  const transactionIds = envelope.operations.map((operation) => operation.transactionId);
98
123
  if (transactionIds.some((id) => typeof id !== 'string' || id.length === 0)) {
@@ -72,16 +72,33 @@ export interface Transaction {
72
72
  localOnly?: boolean;
73
73
  /** Sync-id threshold: the transaction confirms once a delta with an id at least this value arrives. */
74
74
  syncIdNeededForCompletion?: number;
75
+ /**
76
+ * Opaque customer-side batch identity returned with a queued source receipt.
77
+ * It is retained durably and is the only identity a live WAL delta may use
78
+ * to settle this pending commit. Server-side replay additionally proves the
79
+ * complete stored operation-id set before returning `confirmed`.
80
+ */
81
+ correlationId?: string;
82
+ /**
83
+ * A queued forward has no authoritative sync-id watermark yet. It may only
84
+ * confirm when a WAL delta carries its opaque server-derived correlation;
85
+ * unrelated deltas must never satisfy it by advancing the global watermark.
86
+ */
87
+ requiresCorrelatedDelta?: boolean;
75
88
  /**
76
89
  * Resolves once the server has confirmed this transaction, whether by a delta
77
- * or an HTTP acknowledgement, and rejects with the originating error if the
78
- * transaction is permanently rolled back. It gives a caller one place to await
79
- * the answer to "did my write land?", matching the
90
+ * or an HTTP acknowledgement. It rejects with the originating error if the
91
+ * transaction is permanently rolled back; for a source-forwarded write it
92
+ * also rejects with `replication_lag_timeout` when the accepted write's WAL
93
+ * echo misses the confirmation deadline. That timeout does not roll back or
94
+ * fail the transaction: it remains pending and may confirm when replication
95
+ * recovers. This gives a caller one place to await the answer to "did my write
96
+ * land?", matching the
80
97
  * `commits.create({ wait: 'confirmed' })` and
81
98
  * {@link TransactionQueue.waitForConfirmation} vocabulary, so a failure
82
99
  * surfaces at the call site instead of only as a silent local rollback. The
83
- * rejection value is the same {@link AbloError} carried on the queue's
84
- * `transaction:failed` event.
100
+ * permanent-rejection value is the same {@link AbloError} carried on the
101
+ * queue's `transaction:failed` event.
85
102
  */
86
103
  confirmation?: Promise<void>;
87
104
  }
@@ -108,6 +125,7 @@ export declare function hasCommitCoalescingBarrier(options?: WriteOptions): bool
108
125
  export interface WriteOperationFields {
109
126
  readAt?: number | null;
110
127
  onStale?: 'reject' | 'overwrite' | 'notify' | null;
128
+ fenceToken?: number | null;
111
129
  options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
112
130
  }
113
131
  /**
@@ -131,6 +131,9 @@ export function applyWriteOptions(op, transaction) {
131
131
  if (writeOptions.onStale !== undefined) {
132
132
  operation.onStale = writeOptions.onStale;
133
133
  }
134
+ if (writeOptions.fenceToken !== undefined) {
135
+ operation.fenceToken = writeOptions.fenceToken;
136
+ }
134
137
  if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
135
138
  operation.options = {
136
139
  ...(writeOptions.idempotencyKey != null
@@ -40,13 +40,14 @@ export declare class DeltaConfirmationTracker {
40
40
  constructor(ctx: DeltaConfirmationContext);
41
41
  /** Applied-cursor alias, kept so the read sites below stay legible. */
42
42
  private get lastSeenSyncId();
43
+ private matchesSourceEcho;
43
44
  noteAck(lastSyncId: number | undefined): void;
44
45
  /**
45
46
  * Confirms every awaiting transaction whose sync-id threshold this delta
46
47
  * meets or exceeds.
47
48
  * @param syncId - The sync id of the received delta.
48
49
  */
49
- onDeltaReceived(syncId: number): void;
50
+ onDeltaReceived(syncId: number, correlationId?: string): void;
50
51
  scheduleDeltaConfirmationTimeout(tx: Transaction, timeoutMs: number): void;
51
52
  private cancelDeltaConfirmationTimeout;
52
53
  /**
@@ -27,6 +27,11 @@ export class DeltaConfirmationTracker {
27
27
  get lastSeenSyncId() {
28
28
  return this.ctx.position.applied;
29
29
  }
30
+ matchesSourceEcho(tx, correlationId) {
31
+ return (correlationId !== undefined &&
32
+ tx.correlationId !== undefined &&
33
+ correlationId === tx.correlationId);
34
+ }
30
35
  noteAck(lastSyncId) {
31
36
  this.ctx.position.noteAck(lastSyncId);
32
37
  }
@@ -35,7 +40,7 @@ export class DeltaConfirmationTracker {
35
40
  * meets or exceeds.
36
41
  * @param syncId - The sync id of the received delta.
37
42
  */
38
- onDeltaReceived(syncId) {
43
+ onDeltaReceived(syncId, correlationId) {
39
44
  // The cursor advances where the delta is applied (the store calls
40
45
  // position.advanceApplied / advancePersisted); this hook only resolves
41
46
  // confirmation thresholds against the incoming id.
@@ -52,7 +57,11 @@ export class DeltaConfirmationTracker {
52
57
  txId: tx.id.slice(0, 8),
53
58
  model: tx.modelName,
54
59
  needed: tx.syncIdNeededForCompletion,
55
- willConfirm: tx.syncIdNeededForCompletion !== undefined && syncId >= tx.syncIdNeededForCompletion,
60
+ requiresCorrelatedDelta: tx.requiresCorrelatedDelta === true,
61
+ willConfirm: tx.requiresCorrelatedDelta === true
62
+ ? this.matchesSourceEcho(tx, correlationId)
63
+ : tx.syncIdNeededForCompletion !== undefined &&
64
+ syncId >= tx.syncIdNeededForCompletion,
56
65
  })),
57
66
  });
58
67
  }
@@ -61,8 +70,16 @@ export class DeltaConfirmationTracker {
61
70
  return;
62
71
  let confirmedCount = 0;
63
72
  for (const tx of awaitingTxs) {
64
- // Confirm if this delta's ID meets or exceeds the threshold
65
- if (tx.syncIdNeededForCompletion !== undefined && syncId >= tx.syncIdNeededForCompletion) {
73
+ // Queued forward receipts deliberately have no watermark. They confirm
74
+ // only when the authoritative source echoes the receipt's opaque,
75
+ // authenticated-scope batch identity; the legacy anomaly path continues
76
+ // to use its sync-id threshold.
77
+ const confirmedByCorrelation = tx.requiresCorrelatedDelta === true &&
78
+ this.matchesSourceEcho(tx, correlationId);
79
+ const confirmedByThreshold = tx.requiresCorrelatedDelta !== true &&
80
+ tx.syncIdNeededForCompletion !== undefined &&
81
+ syncId >= tx.syncIdNeededForCompletion;
82
+ if (confirmedByCorrelation || confirmedByThreshold) {
66
83
  this.cancelDeltaConfirmationTimeout(tx.id);
67
84
  this.ctx.store.updateStatus(tx.id, 'completed');
68
85
  this.ctx.emit('transaction:completed', tx);
@@ -74,6 +91,7 @@ export class DeltaConfirmationTracker {
74
91
  model: tx.modelName,
75
92
  neededSyncId: tx.syncIdNeededForCompletion,
76
93
  receivedSyncId: syncId,
94
+ confirmation: confirmedByCorrelation ? 'source_correlation' : 'sync_id',
77
95
  });
78
96
  }
79
97
  }
@@ -30,6 +30,7 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
30
30
  overwrite: "overwrite";
31
31
  notify: "notify";
32
32
  }>>>;
33
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
33
34
  model: z.ZodString;
34
35
  id: z.ZodString;
35
36
  input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -69,6 +70,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
69
70
  }, z.core.$strict>>;
70
71
  createdAt: z.ZodNumber;
71
72
  sealedAt: z.ZodNumber;
73
+ acceptedAt: z.ZodOptional<z.ZodNumber>;
74
+ correlationId: z.ZodOptional<z.ZodString>;
72
75
  sequence: z.ZodOptional<z.ZodNumber>;
73
76
  timestamp: z.ZodNumber;
74
77
  }, z.core.$strict>, z.ZodObject<{
@@ -89,6 +92,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
89
92
  scopeNamespace: z.ZodString;
90
93
  createdAt: z.ZodNumber;
91
94
  sealedAt: z.ZodNumber;
95
+ acceptedAt: z.ZodOptional<z.ZodNumber>;
96
+ correlationId: z.ZodOptional<z.ZodString>;
92
97
  sequence: z.ZodOptional<z.ZodNumber>;
93
98
  timestamp: z.ZodNumber;
94
99
  }, z.core.$strict>]>;
@@ -105,7 +110,9 @@ export interface DurableWriteStore {
105
110
  /**
106
111
  * Atomically reserve a pending write and consume the staged records it owns.
107
112
  * The same id + same request is idempotent; the same id + a different request
108
- * must be rejected.
113
+ * must be rejected. For a source-accepted envelope, a re-seal may add the
114
+ * monotonic `acceptedAt`/`correlationId` evidence and the store must preserve
115
+ * that upgrade atomically rather than ignoring it.
109
116
  */
110
117
  seal(write: PendingWrite, consumedRecordIds: readonly string[]): Promise<void>;
111
118
  /** Load all unacknowledged writes. Stored data is treated as untrusted. */
@@ -24,6 +24,8 @@ export declare const durableHttpCommitEnvelopeSchema: z.ZodObject<{
24
24
  scopeNamespace: z.ZodString;
25
25
  createdAt: z.ZodNumber;
26
26
  sealedAt: z.ZodNumber;
27
+ acceptedAt: z.ZodOptional<z.ZodNumber>;
28
+ correlationId: z.ZodOptional<z.ZodString>;
27
29
  sequence: z.ZodOptional<z.ZodNumber>;
28
30
  timestamp: z.ZodNumber;
29
31
  }, z.core.$strict>;
@@ -41,4 +43,4 @@ export declare function createDurableHttpCommitEnvelope(input: {
41
43
  sealedAt?: number;
42
44
  sequence?: number;
43
45
  }): DurableHttpCommitEnvelope;
44
- export declare function isHttpCommitReplayExpired(envelope: Pick<DurableHttpCommitEnvelope, 'sealedAt'>, now?: number): boolean;
46
+ export declare function isHttpCommitReplayExpired(envelope: Pick<DurableHttpCommitEnvelope, 'sealedAt' | 'acceptedAt'>, now?: number): boolean;
@@ -2,6 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { v5 as uuidv5 } from 'uuid';
4
4
  import { stableStringify } from '../utils/json.js';
5
+ import { correlationIdSchema } from '../wire/commit.js';
5
6
  import { PROTOCOL_VERSION } from '../wire/protocolVersion.js';
6
7
  import { idempotencyKeySchema } from './idempotencyKey.js';
7
8
  export const HTTP_COMMIT_ENVELOPE_VERSION = 1;
@@ -67,6 +68,15 @@ export const durableHttpCommitEnvelopeSchema = z
67
68
  scopeNamespace: z.string().min(1),
68
69
  createdAt: z.number().int().nonnegative(),
69
70
  sealedAt: z.number().int().nonnegative(),
71
+ /**
72
+ * Monotonic evidence that the server accepted this exact request for a
73
+ * connected source. Such server keys are permanent, so this envelope must
74
+ * remain replayable past the ordinary hosted 24-hour window until its WAL
75
+ * echo confirms.
76
+ */
77
+ acceptedAt: z.number().int().nonnegative().optional(),
78
+ /** Opaque server-derived source-batch identity paired with `acceptedAt`. */
79
+ correlationId: correlationIdSchema.optional(),
70
80
  /** Monotonic within one client; disambiguates writes sealed in the same ms. */
71
81
  sequence: z.number().int().nonnegative().optional(),
72
82
  timestamp: z.number().int().nonnegative(),
@@ -88,6 +98,21 @@ export const durableHttpCommitEnvelopeSchema = z
88
98
  message: 'HTTP envelope cannot be sealed before it is created',
89
99
  });
90
100
  }
101
+ if ((envelope.acceptedAt === undefined) !== (envelope.correlationId === undefined)) {
102
+ context.addIssue({
103
+ code: 'custom',
104
+ path: ['acceptedAt'],
105
+ message: 'Accepted HTTP envelopes require both acceptedAt and correlationId',
106
+ });
107
+ }
108
+ if (envelope.acceptedAt !== undefined &&
109
+ envelope.acceptedAt < envelope.sealedAt) {
110
+ context.addIssue({
111
+ code: 'custom',
112
+ path: ['acceptedAt'],
113
+ message: 'HTTP envelope cannot be accepted before it is sealed',
114
+ });
115
+ }
91
116
  const { method, path } = envelope.request;
92
117
  const allowedPath = method === 'POST'
93
118
  ? commitPathSchema.safeParse(path).success ||
@@ -177,5 +202,10 @@ export function createDurableHttpCommitEnvelope(input) {
177
202
  });
178
203
  }
179
204
  export function isHttpCommitReplayExpired(envelope, now = Date.now()) {
205
+ // Connected-source mutation-log and customer idempotency keys are
206
+ // permanent. Once acceptance is durable, age can no longer make replay
207
+ // ambiguous; only the matching WAL echo may settle the envelope.
208
+ if (envelope.acceptedAt !== undefined)
209
+ return false;
180
210
  return now - envelope.sealedAt >= HTTP_COMMIT_REPLAY_WINDOW_MS;
181
211
  }
@@ -62,6 +62,7 @@ export declare const persistedTransactionSchema: z.ZodObject<{
62
62
  }>>>;
63
63
  idempotencyKey: z.ZodOptional<z.ZodString>;
64
64
  label: z.ZodOptional<z.ZodString>;
65
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
65
66
  }, z.core.$loose>>;
66
67
  localOnly: z.ZodOptional<z.ZodBoolean>;
67
68
  }, z.core.$loose>;
@@ -104,6 +105,7 @@ export declare const persistedMutationSchema: z.ZodObject<{
104
105
  }>>>;
105
106
  idempotencyKey: z.ZodOptional<z.ZodString>;
106
107
  label: z.ZodOptional<z.ZodString>;
108
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
107
109
  }, z.core.$loose>>;
108
110
  }, z.core.$loose>;
109
111
  export type PersistedQueuedMutation = z.infer<typeof persistedMutationSchema>;
@@ -139,6 +141,7 @@ export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
139
141
  }>>>;
140
142
  idempotencyKey: z.ZodOptional<z.ZodString>;
141
143
  label: z.ZodOptional<z.ZodString>;
144
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
142
145
  }, z.core.$loose>>;
143
146
  mutationId: z.ZodString;
144
147
  }, z.core.$loose>;
@@ -173,6 +176,7 @@ export declare const pendingMutationRecordSchema: z.ZodObject<{
173
176
  }>>>;
174
177
  idempotencyKey: z.ZodOptional<z.ZodString>;
175
178
  label: z.ZodOptional<z.ZodString>;
179
+ fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
176
180
  }, z.core.$loose>>;
177
181
  mutationId: z.ZodString;
178
182
  }, z.core.$loose>;
@@ -24,6 +24,12 @@ const persistedWriteOptionsSchema = z
24
24
  onStale: z.enum(['reject', 'overwrite', 'notify']).nullable().optional(),
25
25
  idempotencyKey: z.string().optional(),
26
26
  label: z.string().optional(),
27
+ // Aligned with the `WriteOptions` type: a claimed write persisted offline
28
+ // must replay carrying its fencing token (Option B), or a queued write that
29
+ // survives a reload would lose its fence and could land as a stale blind
30
+ // write. Declared (not just loose-passthrough) so it is validated as a
31
+ // number on rehydration.
32
+ fenceToken: z.number().nullable().optional(),
27
33
  })
28
34
  .loose();
29
35
  /**
@@ -289,16 +289,11 @@ export interface ClaimLeaseOptions {
289
289
  export type Duration = import('../utils/duration.js').Duration;
290
290
  export interface ClaimOptions extends ClaimLeaseOptions {
291
291
  /**
292
- * Free-form reason describing why you're claiming. Surfaces in conflict
293
- * messages and the activity overlay. Defaults to `'editing'`. Common
294
- * values: `'editing'`, `'writing'`, `'reviewing'`, custom strings for
295
- * app-specific phases.
296
- */
297
- readonly reason?: string;
298
- /**
299
- * Peer-visible explanation of the exact work being performed. This is more
300
- * specific than `reason`: `reason` is the phase (`'renaming'`), while
301
- * `description` is the instruction other agents should see.
292
+ * Peer-visible description of the work you're doing the sentence another
293
+ * participant reads to decide whether to wait, work elsewhere, or move on
294
+ * (`'rewriting the risk section to match Q3'`). Surfaces in conflict
295
+ * messages and the activity overlay, and rides back in the rejection a
296
+ * blocked writer receives. Defaults to `'editing'`.
302
297
  */
303
298
  readonly description?: string;
304
299
  /**
@@ -318,7 +313,7 @@ export interface ClaimStream {
318
313
  readonly others: readonly Claim[];
319
314
  /**
320
315
  * A reactive view of the wait queue on one target — the ordered line of
321
- * queued claims behind the current holder, each carrying its `reason`,
316
+ * queued claims behind the current holder, each carrying its `description`,
322
317
  * `heldBy`, and `position`. Empty when no one is waiting. Pair it with
323
318
  * `onChange(...)` for change notifications.
324
319
  */
@@ -410,13 +405,11 @@ export interface Claim<T = Record<string, unknown>> {
410
405
  /** What is being coordinated. */
411
406
  readonly target: ClaimTarget;
412
407
  /**
413
- * Human-readable phase`'editing'`, `'writing'`, `'reviewing'`. The same
414
- * field on every claim surface; distinct from the CRUD operation. Defaults to
415
- * `'editing'`.
408
+ * Peer-visible description of the work being done the same field on every
409
+ * claim surface. Always present: the SDK resolves it as it decodes the frame,
410
+ * defaulting to `'editing'` when a claim carries no description.
416
411
  */
417
- readonly reason: string;
418
- /** Peer-visible explanation of the work being performed. */
419
- readonly description?: string;
412
+ readonly description: string;
420
413
  /** Participant holding it. Absent on a handle you hold for yourself. */
421
414
  readonly heldBy?: string;
422
415
  /**
@@ -451,6 +444,15 @@ export interface Claim<T = Record<string, unknown>> {
451
444
  * uses it as the `readAt` staleness guard. Present on a claim you hold.
452
445
  */
453
446
  readonly readAt?: number;
447
+ /**
448
+ * The monotonic fencing token the server minted for this grant (Option B). A
449
+ * write made under this claim carries it, and the server rejects the write if
450
+ * a later holder already advanced this entity past the token — closing the
451
+ * "my claim lapsed and its successor came and went" lost-update window that
452
+ * `readAt` alone cannot (a blind write with no read basis has nothing to
453
+ * compare). Present on a claim you hold that was granted a token.
454
+ */
455
+ readonly fenceToken?: number;
454
456
  /** Row snapshot under the lease — present on a claim you hold. */
455
457
  readonly data?: T;
456
458
  /**
@@ -508,3 +510,13 @@ export interface ClaimHeartbeat {
508
510
  * `AsyncDisposable`.
509
511
  */
510
512
  export type HeldClaim<T = Record<string, unknown>> = Claim<T> & Required<Pick<Claim<T>, 'data' | 'release' | 'revoke' | 'heartbeat' | typeof Symbol.asyncDispose>>;
513
+ /**
514
+ * A held advisory lease you took on a key whose row Ablo does not hold — the
515
+ * resolved value of the row-free `ablo.<model>.claim(id)` overload. It is a
516
+ * {@link HeldClaim} minus `.data`: the same lease controls (`release`, `revoke`,
517
+ * `heartbeat`, and the `await using` disposer are all required and behave
518
+ * identically), only without a snapshot, because the row lives solely in the
519
+ * customer's own database and was never replicated into the local pool. Use it
520
+ * to serialize work against a key you know by id alone.
521
+ */
522
+ export type HeldLease = Omit<HeldClaim, 'data'>;