@abloatai/ablo 0.35.0 → 0.36.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 (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -15,6 +15,7 @@
15
15
  * no path carried one, so the fix looked complete and changed nothing. Widening
16
16
  * the locator again should touch this module and the schema, not every hop.
17
17
  */
18
+ import { partName } from './schema.js';
18
19
  // The one declared→wire conversion for claim metadata; `declaredMeta` is its
19
20
  // counterpart, called by the decodes that build a public claim.
20
21
  import { wireMeta } from './claimMeta.js';
@@ -41,8 +42,10 @@ source) {
41
42
  return {
42
43
  ...(source.path !== undefined ? { path: source.path } : {}),
43
44
  ...(source.range !== undefined ? { range: source.range } : {}),
44
- ...(source.field !== undefined ? { field: source.field } : {}),
45
- ...(source.fields !== undefined ? { fields: source.fields } : {}),
45
+ // Part names cross to their wire spelling here — the one declared→wire
46
+ // seam — so a `part('B2')` object never leaks into a frame or a URL.
47
+ ...(source.field !== undefined ? { field: partName(source.field) } : {}),
48
+ ...(source.fields !== undefined ? { fields: source.fields.map(partName) } : {}),
46
49
  ...(source.meta !== undefined ? { meta: wireMeta(source.meta) } : {}),
47
50
  };
48
51
  }
@@ -80,3 +83,20 @@ export function streamTarget(source) {
80
83
  const [type, id] = entityOf(source);
81
84
  return { type, id };
82
85
  }
86
+ /** The batch's grant, or nothing when there is no claim, or none with a token. */
87
+ export function batchFence(source, token) {
88
+ return source == null || token == null
89
+ ? null
90
+ : { ...modelTarget(source), token };
91
+ }
92
+ /**
93
+ * The token an operation carries: its own if it names one, the batch's when it
94
+ * writes the claimed row, otherwise none.
95
+ */
96
+ export function fenceTokenFor(fence, model, id) {
97
+ if (fence === null || id === null)
98
+ return null;
99
+ return model.toLowerCase() === fence.model.toLowerCase() && id === fence.id
100
+ ? fence.token
101
+ : null;
102
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { type FieldRef } from '../schema/fieldRef.js';
2
3
  import type { ParticipantKind } from '../types/participant.js';
3
4
  /**
4
5
  * The wire schemas for coordination — the shapes that keep agents and people
@@ -16,7 +17,15 @@ import type { ParticipantKind } from '../types/participant.js';
16
17
  * rather than re-declaring the shapes, and the server validates inbound frames
17
18
  * against them at runtime.
18
19
  */
19
- /** A line/column span within a text-bearing field (section body, doc, cell). */
20
+ /**
21
+ * A span within a text-bearing part of a row (section body, doc, cell).
22
+ *
23
+ * Overlap is judged on the line axis alone: two ranges conflict when their
24
+ * `[startLine, endLine]` intervals intersect, and columns ride along for
25
+ * display. An editor that addresses by integer position rather than by line
26
+ * maps that position axis straight onto `startLine`/`endLine` — the test is
27
+ * plain interval intersection and never assumes the units are lines of a file.
28
+ */
20
29
  export declare const targetRangeSchema: z.ZodObject<{
21
30
  startLine: z.ZodNumber;
22
31
  endLine: z.ZodNumber;
@@ -24,6 +33,42 @@ export declare const targetRangeSchema: z.ZodObject<{
24
33
  endColumn: z.ZodOptional<z.ZodNumber>;
25
34
  }, z.core.$strip>;
26
35
  export type TargetRange = z.infer<typeof targetRangeSchema>;
36
+ /**
37
+ * An app-defined claimable part name — a cell (`'B2'`), a section id, a
38
+ * block — made explicit with {@link part}. The schema's own field names need
39
+ * no marker; a name that is NOT a field does, so looseness is a visible
40
+ * decision at the call site rather than a silent absorber of typos.
41
+ *
42
+ * A small object rather than a branded string on purpose: a brand makes a
43
+ * concrete schema's claim params mutually unassignable with the erased
44
+ * `SchemaRecord` view, and the react context boundary erases and restores
45
+ * exactly that way. The object member stays pairwise comparable, so no
46
+ * boundary needs a cast through `unknown`.
47
+ */
48
+ export interface ClaimPart {
49
+ readonly part: string;
50
+ }
51
+ /**
52
+ * Name an app-defined part of a row for a claim target: `part('B2')` for a
53
+ * cell, `part('sec_intro')` for a section. The conflict rule compares part
54
+ * names as opaque case-insensitive strings, so any name is legal on the
55
+ * wire — this marker exists purely so the type surface stays definite about
56
+ * the model's own fields.
57
+ */
58
+ export declare function part(name: string): ClaimPart;
59
+ /**
60
+ * The wire spelling of a part name, from whichever spelling the caller used.
61
+ *
62
+ * Three, because they are three different promises. A {@link FieldRef} —
63
+ * `schema.fields.tasks.status` — is a field the schema declares, so a name that
64
+ * does not exist never compiles. `part('B2')` is a name the schema does not
65
+ * know and says so. A bare string is neither, and survives only because the
66
+ * erased `SchemaRecord` view and untyped callers still need it.
67
+ *
68
+ * All three become the same string here: the wire has always carried names, and
69
+ * what differs is how much was known before the crossing.
70
+ */
71
+ export declare function partName(value: string | ClaimPart | FieldRef): string;
27
72
  export declare const participantKindSchema: z.ZodEnum<{
28
73
  user: "user";
29
74
  agent: "agent";
@@ -174,24 +219,6 @@ export declare const staleNotificationSchema: z.ZodObject<{
174
219
  group: z.ZodOptional<z.ZodString>;
175
220
  }, z.core.$strip>;
176
221
  export type StaleNotification = z.infer<typeof staleNotificationSchema>;
177
- /**
178
- * One entry in a commit's batch premise — a read it was based on, so the
179
- * server can ask "did anything I looked at change?" — broader than the
180
- * write-target check, which only validates the rows being written. The server
181
- * re-runs stale detection against each entry at its `readAt`; a moved premise
182
- * fires the entry's `onStale` disposition (default `reject`) across the whole
183
- * batch (`notify` holds every write and notifies, `reject` aborts, `overwrite`
184
- * proceeds silently). An entry comes at one of two granularities:
185
- *
186
- * • Row — `{ model, id, readAt, fields? }`: did this specific row, or these
187
- * specific fields, change?
188
- * • Group — `{ group, readAt }`: did anything in this sync group change?
189
- * `group` is a sync-group key such as `report:abc` or `section:s1`, the
190
- * same unit a participant watches and claims.
191
- *
192
- * See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
193
- * governing convention and the receive → reconcile loop.
194
- */
195
222
  export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
196
223
  model: z.ZodString;
197
224
  id: z.ZodString;
@@ -212,21 +239,9 @@ export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
212
239
  }>>;
213
240
  }, z.core.$strip>]>;
214
241
  export type ReadDependency = z.infer<typeof readDependencySchema>;
215
- /**
216
- * A durable premise — what a participant is watching so that a later
217
- * change to it opens a {@link StaleNotification}. It is the persisted sibling of
218
- * a {@link ReadDependency}: the same reference shape, minus the disposition (a
219
- * track always notifies — that is what tracking is), with an optional `readAt`
220
- * that defaults to the watermark of the commit that registered it. The row form
221
- * watches one object; the group form watches a whole sync group ("anything in
222
- * `report:abc`"). Where a `ReadDependency` is checked once at commit and
223
- * discarded, a `TrackDependency` is kept and re-checked against every future
224
- * delta. See `packages/sync-engine/docs/groups.md` for how it drives change
225
- * propagation.
226
- */
227
242
  export declare const trackDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
228
- model: z.ZodString;
229
243
  id: z.ZodString;
244
+ model: z.ZodString;
230
245
  readAt: z.ZodOptional<z.ZodNumber>;
231
246
  }, z.core.$strip>, z.ZodObject<{
232
247
  group: z.ZodString;
@@ -351,6 +366,29 @@ export declare const claimErrorSchema: z.ZodObject<{
351
366
  policyReason: z.ZodOptional<z.ZodString>;
352
367
  }, z.core.$strip>;
353
368
  export type ClaimError = z.infer<typeof claimErrorSchema>;
369
+ /**
370
+ * Why a claim ended without its holder releasing it, or was refused.
371
+ *
372
+ * A closed set, because the two refusals are different guarantees and a reader
373
+ * has to be able to tell them apart: `conflict` means someone holds the row
374
+ * right now and you may queue behind them, while `coordination_unavailable`
375
+ * means the coordinator could not answer, so nothing is known about the row.
376
+ * `expired` and `preempted` are the two ways a lease you held ends.
377
+ *
378
+ * The rejection frame's `reason` stays a plain string on the wire — it is
379
+ * frozen, and an older server may send a word not listed here. This is the
380
+ * reader's side of it: a value that parses becomes the typed reason, and one
381
+ * that does not is simply absent rather than smuggled through as prose.
382
+ * {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
383
+ * reasons as enums; this brings the refusals into line.
384
+ */
385
+ export declare const claimEventReasonSchema: z.ZodEnum<{
386
+ conflict: "conflict";
387
+ expired: "expired";
388
+ coordination_unavailable: "coordination_unavailable";
389
+ preempted: "preempted";
390
+ }>;
391
+ export type ClaimEventReason = z.infer<typeof claimEventReasonSchema>;
354
392
  /**
355
393
  * A declared, pending-mutation claim — the unit broadcast inside a presence
356
394
  * frame's `activeClaims`. The client supplies the descriptive `targetRef`
@@ -428,7 +466,12 @@ export declare const wireClaimSchema: z.ZodObject<{
428
466
  export type WireClaim = z.infer<typeof wireClaimSchema>;
429
467
  export declare const claimRejectionSchema: z.ZodObject<{
430
468
  claimId: z.ZodString;
431
- reason: z.ZodString;
469
+ reason: z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{
470
+ conflict: "conflict";
471
+ expired: "expired";
472
+ coordination_unavailable: "coordination_unavailable";
473
+ preempted: "preempted";
474
+ }>>>;
432
475
  target: z.ZodOptional<z.ZodObject<{
433
476
  entityType: z.ZodString;
434
477
  entityId: z.ZodString;
@@ -626,7 +669,12 @@ export declare const claimQueuedSchema: z.ZodObject<{
626
669
  }, z.core.$strip>>;
627
670
  policyReason: z.ZodOptional<z.ZodString>;
628
671
  position: z.ZodNumber;
629
- reason: z.ZodOptional<z.ZodString>;
672
+ reason: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{
673
+ conflict: "conflict";
674
+ expired: "expired";
675
+ coordination_unavailable: "coordination_unavailable";
676
+ preempted: "preempted";
677
+ }>>>>;
630
678
  }, z.core.$strip>;
631
679
  export type ClaimQueued = z.infer<typeof claimQueuedSchema>;
632
680
  /**
@@ -721,29 +769,6 @@ export declare const claimExpiredSchema: z.ZodObject<{
721
769
  claimId: z.ZodString;
722
770
  }, z.core.$strip>;
723
771
  export type ClaimExpired = z.infer<typeof claimExpiredSchema>;
724
- /**
725
- * Why a claim ended without its holder releasing it, or was refused.
726
- *
727
- * A closed set, because the two refusals are different guarantees and a reader
728
- * has to be able to tell them apart: `conflict` means someone holds the row
729
- * right now and you may queue behind them, while `coordination_unavailable`
730
- * means the coordinator could not answer, so nothing is known about the row.
731
- * `expired` and `preempted` are the two ways a lease you held ends.
732
- *
733
- * The rejection frame's `reason` stays a plain string on the wire — it is
734
- * frozen, and an older server may send a word not listed here. This is the
735
- * reader's side of it: a value that parses becomes the typed reason, and one
736
- * that does not is simply absent rather than smuggled through as prose.
737
- * {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
738
- * reasons as enums; this brings the refusals into line.
739
- */
740
- export declare const claimEventReasonSchema: z.ZodEnum<{
741
- conflict: "conflict";
742
- expired: "expired";
743
- preempted: "preempted";
744
- coordination_unavailable: "coordination_unavailable";
745
- }>;
746
- export type ClaimEventReason = z.infer<typeof claimEventReasonSchema>;
747
772
  /**
748
773
  * What a {@link ModelClaim} points at — the target locator as SDK callers see
749
774
  * it, keyed by `model` and `id` rather than the wire schema's `entityType` and
@@ -1017,13 +1042,51 @@ export declare const claimHeartbeatBatchAckPayloadSchema: z.ZodObject<{
1017
1042
  }, z.core.$strip>>;
1018
1043
  }, z.core.$strip>;
1019
1044
  export type ClaimHeartbeatBatchAckPayload = z.infer<typeof claimHeartbeatBatchAckPayloadSchema>;
1045
+ /**
1046
+ * How many scopes one frame may name. A coarse abuse ceiling, not a business
1047
+ * limit: a connection legitimately watches a handful of entities, and a list
1048
+ * this long is an amplification attempt rather than a workload. Declared here,
1049
+ * beside the two frames it bounds, so neither can be given a different answer.
1050
+ */
1051
+ export declare const MAX_FRAME_SYNC_GROUPS = 200;
1052
+ /**
1053
+ * The `claim` payload a client sends — the frame behind `join`.
1054
+ *
1055
+ * It opens one participant claim: the connection is added to each named
1056
+ * scope's fan-out under `claimId`, announced into its presence roster, and
1057
+ * holds that interest until `release`, the TTL lapses, or the socket closes.
1058
+ * The handle is client-chosen because the client must be able to `release`
1059
+ * the exact claim it opened while others stay open.
1060
+ *
1061
+ * This shape was, for a long time, written three times — built as a literal in
1062
+ * the transport, restated as an interface on the server, and read back through
1063
+ * a cast in the frame handler — which is how the frame came to be the only
1064
+ * coordination message with no runtime check on the way in.
1065
+ */
1066
+ export declare const participantClaimPayloadSchema: z.ZodObject<{
1067
+ claimId: z.ZodString;
1068
+ syncGroups: z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"default">, z.core.$ZodBranded<z.ZodTemplateLiteral<`${string}:${string}`>, "SyncGroup", "out">]>>;
1069
+ capabilityToken: z.ZodOptional<z.ZodString>;
1070
+ ttlSeconds: z.ZodOptional<z.ZodNumber>;
1071
+ }, z.core.$strip>;
1072
+ export type ParticipantClaimPayload = z.infer<typeof participantClaimPayloadSchema>;
1073
+ /**
1074
+ * The `release` payload — drop one participant claim by its handle.
1075
+ *
1076
+ * A projection of the claim it releases rather than a second object, so the
1077
+ * handle cannot be spelled one way when opened and another when dropped.
1078
+ * Idempotent by contract: the server accepts an unknown handle silently, so a
1079
+ * client releasing everything at shutdown never has to check what is still open.
1080
+ */
1081
+ export declare const participantReleasePayloadSchema: z.ZodObject<{
1082
+ claimId: z.ZodString;
1083
+ }, z.core.$strip>;
1084
+ export type ParticipantReleasePayload = z.infer<typeof participantReleasePayloadSchema>;
1020
1085
  /**
1021
1086
  * The `update_subscription` payload a client sends. It replaces the
1022
- * connection's read interest with the complete set of sync groups — the read
1023
- * counterpart to a claim, with no write lock and no TTL. Each entry is a
1024
- * {@link syncGroupInputSchema} (`'default'` or a branded `kind:id`), so a
1025
- * malformed group is rejected on ingest rather than silently indexed. The
1026
- * element type is strict because this is untrusted client input.
1087
+ * connection's read interest with the complete set of sync groups — the
1088
+ * unleased counterpart to {@link participantClaimPayloadSchema}, with no
1089
+ * handle, no TTL, and no roster entry.
1027
1090
  */
1028
1091
  export declare const updateSubscriptionPayloadSchema: z.ZodObject<{
1029
1092
  syncGroups: z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"default">, z.core.$ZodBranded<z.ZodTemplateLiteral<`${string}:${string}`>, "SyncGroup", "out">]>>;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { syncGroupInputSchema } from '../schema/roles.js';
3
+ import { isFieldRef } from '../schema/fieldRef.js';
3
4
  /**
4
5
  * The wire schemas for coordination — the shapes that keep agents and people
5
6
  * from overwriting each other on a shared row. Coordination works in three
@@ -19,12 +20,71 @@ import { syncGroupInputSchema } from '../schema/roles.js';
19
20
  // ─────────────────────────────────────────────────────────────────────────
20
21
  // Shared primitives
21
22
  // ─────────────────────────────────────────────────────────────────────────
22
- /** A line/column span within a text-bearing field (section body, doc, cell). */
23
- export const targetRangeSchema = z.object({
23
+ /**
24
+ * A span within a text-bearing part of a row (section body, doc, cell).
25
+ *
26
+ * Overlap is judged on the line axis alone: two ranges conflict when their
27
+ * `[startLine, endLine]` intervals intersect, and columns ride along for
28
+ * display. An editor that addresses by integer position rather than by line
29
+ * maps that position axis straight onto `startLine`/`endLine` — the test is
30
+ * plain interval intersection and never assumes the units are lines of a file.
31
+ */
32
+ export const targetRangeSchema = z
33
+ .object({
24
34
  startLine: z.number(),
25
35
  endLine: z.number(),
26
36
  startColumn: z.number().optional(),
27
37
  endColumn: z.number().optional(),
38
+ })
39
+ // An inverted span claims nothing and collides with nothing — reject it at
40
+ // the boundary instead of granting a lease that never excludes anyone.
41
+ .refine((range) => range.startLine <= range.endLine, {
42
+ message: 'range.startLine must not be greater than range.endLine.',
43
+ });
44
+ /**
45
+ * Name an app-defined part of a row for a claim target: `part('B2')` for a
46
+ * cell, `part('sec_intro')` for a section. The conflict rule compares part
47
+ * names as opaque case-insensitive strings, so any name is legal on the
48
+ * wire — this marker exists purely so the type surface stays definite about
49
+ * the model's own fields.
50
+ */
51
+ export function part(name) {
52
+ return { part: name };
53
+ }
54
+ /**
55
+ * The wire spelling of a part name, from whichever spelling the caller used.
56
+ *
57
+ * Three, because they are three different promises. A {@link FieldRef} —
58
+ * `schema.fields.tasks.status` — is a field the schema declares, so a name that
59
+ * does not exist never compiles. `part('B2')` is a name the schema does not
60
+ * know and says so. A bare string is neither, and survives only because the
61
+ * erased `SchemaRecord` view and untyped callers still need it.
62
+ *
63
+ * All three become the same string here: the wire has always carried names, and
64
+ * what differs is how much was known before the crossing.
65
+ */
66
+ export function partName(value) {
67
+ if (typeof value === 'string')
68
+ return value;
69
+ return isFieldRef(value) ? value.field : value.part;
70
+ }
71
+ /**
72
+ * One claimable part name.
73
+ *
74
+ * Names compare as opaque strings, so any name is legal — except one that is
75
+ * plainly several. A caller who needed to claim two parts and had only `field`
76
+ * to say it in packed them into one delimited string, and because
77
+ * `blocks:b_1,b_2` and `blocks:b_1` are different names, both writers were
78
+ * granted a lease on `b_1` and one of their updates was lost with nothing
79
+ * raised. `fields` exists to say that, and refusing the packed spelling is what
80
+ * makes the mistake visible at the moment it is made rather than as a missing
81
+ * update later.
82
+ *
83
+ * Deliberately narrow: only the comma, because that is what a caller reaches
84
+ * for to join a list. A part name is otherwise free.
85
+ */
86
+ const partNameSchema = z.string().refine((name) => !name.includes(','), {
87
+ message: 'A part name cannot contain a comma. Claim several parts with `fields: [a, b]` — two names in one `field` compare as a single unrelated name, so both writers would be granted the same part.',
28
88
  });
29
89
  export const participantKindSchema = z.enum(['user', 'agent', 'system']);
30
90
  const _participantKindContract = true;
@@ -104,7 +164,7 @@ export const targetRefSchema = z.object({
104
164
  entityId: z.string(),
105
165
  path: z.string().optional(),
106
166
  range: targetRangeSchema.optional(),
107
- field: z.string().optional(),
167
+ field: partNameSchema.optional(),
108
168
  /**
109
169
  * Several named parts of one row, claimed together — three sections of a
110
170
  * document, two cells of a table.
@@ -120,7 +180,7 @@ export const targetRefSchema = z.object({
120
180
  * a claim naming `field` and a claim naming `fields` still compare correctly
121
181
  * against each other.
122
182
  */
123
- fields: z.array(z.string()).readonly().optional(),
183
+ fields: z.array(partNameSchema).readonly().optional(),
124
184
  meta: z.record(z.string(), z.unknown()).optional(),
125
185
  });
126
186
  /**
@@ -231,42 +291,54 @@ export const staleNotificationSchema = z.object({
231
291
  * See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
232
292
  * governing convention and the receive → reconcile loop.
233
293
  */
294
+ const readRowDependencySchema = z.object({
295
+ model: z.string(),
296
+ id: z.string(),
297
+ readAt: z.number(),
298
+ fields: z.array(z.string()).readonly().optional(),
299
+ onStale: onStaleModeSchema.optional(),
300
+ });
301
+ const readGroupDependencySchema = z.object({
302
+ group: z.string(),
303
+ readAt: z.number(),
304
+ onStale: onStaleModeSchema.optional(),
305
+ });
234
306
  export const readDependencySchema = z.union([
235
- z.object({
236
- model: z.string(),
237
- id: z.string(),
238
- readAt: z.number(),
239
- fields: z.array(z.string()).readonly().optional(),
240
- onStale: onStaleModeSchema.optional(),
241
- }),
242
- z.object({
243
- group: z.string(),
244
- readAt: z.number(),
245
- onStale: onStaleModeSchema.optional(),
246
- }),
307
+ readRowDependencySchema,
308
+ readGroupDependencySchema,
247
309
  ]);
248
310
  /**
249
311
  * A durable premise — what a participant is watching so that a later
250
- * change to it opens a {@link StaleNotification}. It is the persisted sibling of
251
- * a {@link ReadDependency}: the same reference shape, minus the disposition (a
252
- * track always notifies that is what tracking is), with an optional `readAt`
253
- * that defaults to the watermark of the commit that registered it. The row form
254
- * watches one object; the group form watches a whole sync group ("anything in
255
- * `report:abc`"). Where a `ReadDependency` is checked once at commit and
256
- * discarded, a `TrackDependency` is kept and re-checked against every future
257
- * delta. See `packages/sync-engine/docs/groups.md` for how it drives change
258
- * propagation.
312
+ * change to it opens a {@link StaleNotification}. Where a {@link ReadDependency}
313
+ * is checked once at commit and discarded, a `TrackDependency` is kept and
314
+ * re-checked against every future delta. The row form watches one object; the
315
+ * group form watches a whole sync group ("anything in `report:abc`"). See
316
+ * `packages/sync-engine/docs/groups.md` for how it drives change propagation.
317
+ *
318
+ * It is a PROJECTION of the ephemeral premise, not a second declaration of the
319
+ * same reference: a track names its target exactly as a read does, and the
320
+ * three ways it differs are stated here as omissions the compiler holds.
321
+ *
322
+ * • no `onStale` — a track always notifies; that is what tracking is;
323
+ * • no `fields` — a track fires at row grain, because the server keeps one
324
+ * row per tracked target and reports that the target moved, not which
325
+ * column did (`track_dependencies` has no field axis to store one in);
326
+ * • `readAt` optional — it defaults to the watermark of the commit that
327
+ * registered the track, so a caller with nothing to say about when it last
328
+ * looked gets "from here on".
329
+ *
330
+ * A field added to the premise reaches both halves; a field the durable half
331
+ * genuinely cannot carry has to be omitted here on purpose, in one line, rather
332
+ * than by being quietly left out of a copy.
259
333
  */
334
+ const trackReadAtSchema = { readAt: z.number().optional() };
260
335
  export const trackDependencySchema = z.union([
261
- z.object({
262
- model: z.string(),
263
- id: z.string(),
264
- readAt: z.number().optional(),
265
- }),
266
- z.object({
267
- group: z.string(),
268
- readAt: z.number().optional(),
269
- }),
336
+ readRowDependencySchema
337
+ .omit({ onStale: true, fields: true, readAt: true })
338
+ .extend(trackReadAtSchema),
339
+ readGroupDependencySchema
340
+ .omit({ onStale: true, readAt: true })
341
+ .extend(trackReadAtSchema),
270
342
  ]);
271
343
  // ─────────────────────────────────────────────────────────────────────────
272
344
  // Layer 2 — pessimistic claims and leases
@@ -391,6 +463,28 @@ export const claimErrorSchema = z.object({
391
463
  /** Optional conflict-policy explanation. Additive: older frames omit it. */
392
464
  policyReason: z.string().optional(),
393
465
  });
466
+ /**
467
+ * Why a claim ended without its holder releasing it, or was refused.
468
+ *
469
+ * A closed set, because the two refusals are different guarantees and a reader
470
+ * has to be able to tell them apart: `conflict` means someone holds the row
471
+ * right now and you may queue behind them, while `coordination_unavailable`
472
+ * means the coordinator could not answer, so nothing is known about the row.
473
+ * `expired` and `preempted` are the two ways a lease you held ends.
474
+ *
475
+ * The rejection frame's `reason` stays a plain string on the wire — it is
476
+ * frozen, and an older server may send a word not listed here. This is the
477
+ * reader's side of it: a value that parses becomes the typed reason, and one
478
+ * that does not is simply absent rather than smuggled through as prose.
479
+ * {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
480
+ * reasons as enums; this brings the refusals into line.
481
+ */
482
+ export const claimEventReasonSchema = z.enum([
483
+ 'conflict',
484
+ 'coordination_unavailable',
485
+ 'expired',
486
+ 'preempted',
487
+ ]);
394
488
  /**
395
489
  * A declared, pending-mutation claim — the unit broadcast inside a presence
396
490
  * frame's `activeClaims`. The client supplies the descriptive `targetRef`
@@ -404,7 +498,26 @@ export const wireClaimSchema = wireClaimBaseSchema.extend({
404
498
  });
405
499
  export const claimRejectionSchema = z.object({
406
500
  claimId: z.string(),
407
- reason: z.string(),
501
+ /**
502
+ * Why the claim was refused, as one of {@link claimEventReasonSchema}'s
503
+ * words — so a caller can branch on it. `conflict` means someone holds the
504
+ * row right now and you may queue behind them; `coordination_unavailable`
505
+ * means the coordinator could not answer, so nothing is known about it.
506
+ * Those are different decisions, and a free string made them one.
507
+ *
508
+ * The wire spelling is frozen and an older server may send a word not listed
509
+ * there, so this reads the way {@link wireParticipantKindSchema} reads its
510
+ * dialect: a value that parses becomes the typed reason, and one that does
511
+ * not is absent rather than smuggled through as prose. Prose has its own
512
+ * field — `policyReason` below — which is why nothing is lost by refusing to
513
+ * carry it here.
514
+ *
515
+ * The registry code a caller finally sees on `AbloClaimedError`
516
+ * (`claim_conflict`) stays a separate vocabulary, mapped at the throw. Two
517
+ * small vocabularies with one mapping point beat one vocabulary and a
518
+ * projection of it that has to be maintained as the registry grows.
519
+ */
520
+ reason: z.preprocess((value) => (claimEventReasonSchema.safeParse(value).success ? value : undefined), claimEventReasonSchema.optional()),
408
521
  target: targetRefSchema.optional(),
409
522
  heldBy: z.string().optional(),
410
523
  /**
@@ -523,28 +636,6 @@ export const claimQueueSchema = z.object({
523
636
  export const claimExpiredSchema = z.object({
524
637
  claimId: z.string(),
525
638
  });
526
- /**
527
- * Why a claim ended without its holder releasing it, or was refused.
528
- *
529
- * A closed set, because the two refusals are different guarantees and a reader
530
- * has to be able to tell them apart: `conflict` means someone holds the row
531
- * right now and you may queue behind them, while `coordination_unavailable`
532
- * means the coordinator could not answer, so nothing is known about the row.
533
- * `expired` and `preempted` are the two ways a lease you held ends.
534
- *
535
- * The rejection frame's `reason` stays a plain string on the wire — it is
536
- * frozen, and an older server may send a word not listed here. This is the
537
- * reader's side of it: a value that parses becomes the typed reason, and one
538
- * that does not is simply absent rather than smuggled through as prose.
539
- * {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
540
- * reasons as enums; this brings the refusals into line.
541
- */
542
- export const claimEventReasonSchema = z.enum([
543
- 'conflict',
544
- 'coordination_unavailable',
545
- 'expired',
546
- 'preempted',
547
- ]);
548
639
  /**
549
640
  * What a {@link ModelClaim} points at — the target locator as SDK callers see
550
641
  * it, keyed by `model` and `id` rather than the wire schema's `entityType` and
@@ -849,18 +940,91 @@ export const claimHeartbeatBatchAckPayloadSchema = z.object({
849
940
  results: z.array(claimHeartbeatAckPayloadSchema),
850
941
  });
851
942
  // ─────────────────────────────────────────────────────────────────────────
852
- // Read interest — area-of-interest navigation (update_subscription)
943
+ // Read interest — what a connection receives
944
+ //
945
+ // Two frames set it, and they differ in exactly one way: whether the
946
+ // interest is leased.
947
+ //
948
+ // • `claim` — a PARTICIPANT claim. Adds a scope under a handle, with a
949
+ // TTL and an optional capability token, and announces the sender into
950
+ // that scope's roster. This is the frame `ablo.<model>.join(...)` sends;
951
+ // `release` drops it. Several may be open on one connection at once.
952
+ // • `update_subscription` — REPLACES the connection's whole read set. No
953
+ // handle, no lease, no roster entry.
954
+ //
955
+ // Both are bounded by the connection credential's grant, and both name their
956
+ // groups the same way, so both parse their `syncGroups` through the same
957
+ // element schema. Neither is the row lease — that is `claim_begin`, in the
958
+ // pessimistic-claims block above, which shares only a word.
853
959
  // ─────────────────────────────────────────────────────────────────────────
854
960
  /**
855
- * The `update_subscription` payload a client sends. It replaces the
856
- * connection's read interest with the complete set of sync groups the read
857
- * counterpart to a claim, with no write lock and no TTL. Each entry is a
961
+ * How many scopes one frame may name. A coarse abuse ceiling, not a business
962
+ * limit: a connection legitimately watches a handful of entities, and a list
963
+ * this long is an amplification attempt rather than a workload. Declared here,
964
+ * beside the two frames it bounds, so neither can be given a different answer.
965
+ */
966
+ export const MAX_FRAME_SYNC_GROUPS = 200;
967
+ /**
968
+ * The sync groups a scope-subscription frame names. Each entry is a
858
969
  * {@link syncGroupInputSchema} (`'default'` or a branded `kind:id`), so a
859
- * malformed group is rejected on ingest rather than silently indexed. The
860
- * element type is strict because this is untrusted client input.
970
+ * malformed group is rejected on ingest rather than silently indexed — a group
971
+ * that does not parse matches nothing, and subscribing to nothing quietly is
972
+ * the failure this element type exists to prevent.
973
+ *
974
+ * Strict because this is untrusted client input, and shared because the two
975
+ * frames below carry the same value: when they disagreed, `claim` accepted a
976
+ * malformed group that `update_subscription` refused, and the connection
977
+ * ended up leased to a scope it could never receive.
978
+ */
979
+ const frameSyncGroupsSchema = z
980
+ .array(syncGroupInputSchema)
981
+ .max(MAX_FRAME_SYNC_GROUPS);
982
+ /**
983
+ * The `claim` payload a client sends — the frame behind `join`.
984
+ *
985
+ * It opens one participant claim: the connection is added to each named
986
+ * scope's fan-out under `claimId`, announced into its presence roster, and
987
+ * holds that interest until `release`, the TTL lapses, or the socket closes.
988
+ * The handle is client-chosen because the client must be able to `release`
989
+ * the exact claim it opened while others stay open.
990
+ *
991
+ * This shape was, for a long time, written three times — built as a literal in
992
+ * the transport, restated as an interface on the server, and read back through
993
+ * a cast in the frame handler — which is how the frame came to be the only
994
+ * coordination message with no runtime check on the way in.
995
+ */
996
+ export const participantClaimPayloadSchema = z.object({
997
+ /** Client-chosen handle. Echoed on `claim_ack`; names the claim to `release`. */
998
+ claimId: z.string().min(1),
999
+ syncGroups: frameSyncGroupsSchema,
1000
+ /**
1001
+ * A narrower capability to present for this claim than the connection's own.
1002
+ * Absent means the connection's credential governs it.
1003
+ */
1004
+ capabilityToken: z.string().optional(),
1005
+ /**
1006
+ * Crash cleanup, in seconds. The server caps it at the capability's own TTL;
1007
+ * absent means the claim lives until `release` or disconnect.
1008
+ */
1009
+ ttlSeconds: z.number().optional(),
1010
+ });
1011
+ /**
1012
+ * The `release` payload — drop one participant claim by its handle.
1013
+ *
1014
+ * A projection of the claim it releases rather than a second object, so the
1015
+ * handle cannot be spelled one way when opened and another when dropped.
1016
+ * Idempotent by contract: the server accepts an unknown handle silently, so a
1017
+ * client releasing everything at shutdown never has to check what is still open.
1018
+ */
1019
+ export const participantReleasePayloadSchema = participantClaimPayloadSchema.pick({ claimId: true });
1020
+ /**
1021
+ * The `update_subscription` payload a client sends. It replaces the
1022
+ * connection's read interest with the complete set of sync groups — the
1023
+ * unleased counterpart to {@link participantClaimPayloadSchema}, with no
1024
+ * handle, no TTL, and no roster entry.
861
1025
  */
862
1026
  export const updateSubscriptionPayloadSchema = z.object({
863
- syncGroups: z.array(syncGroupInputSchema),
1027
+ syncGroups: frameSyncGroupsSchema,
864
1028
  });
865
1029
  /**
866
1030
  * `subscription_ack` payload (server → client). Echoes the connection's