@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
@@ -57,10 +57,10 @@ A **plane** is the isolation unit a credential acts on. `production` is the root
57
57
  plane; every sandbox sits beside it. Three things are per-plane, and knowing
58
58
  which three is most of what production readiness means:
59
59
 
60
- - **Rows** a sandbox write is invisible to production and to every other sandbox.
61
- - **The registered database** one per plane, so your production database and
60
+ - **Rows:** a sandbox write is invisible to production and to every other sandbox.
61
+ - **The registered database:** one per plane, so your production database and
62
62
  your dev database are separate registrations.
63
- - **The active schema artifact** the model shapes the engine actually routes on.
63
+ - **The active schema artifact:** the model shapes the engine actually routes on.
64
64
 
65
65
  A key's plane is fixed at mint and spelled in its prefix: `sk_live_` acts on
66
66
  production, `sk_test_` on a sandbox. There is no runtime override — the
@@ -219,14 +219,14 @@ handler, the Standard Webhooks signature, and rolling a secret.
219
219
 
220
220
  ## What to watch once it is live
221
221
 
222
- - **`ablo logs`** commit activity as it happens, scoped by the key, so a live
222
+ - **`ablo logs`:** commit activity as it happens, scoped by the key, so a live
223
223
  key streams the org and a test key streams only its sandbox. `--json` emits
224
224
  NDJSON for piping.
225
- - **`ablo status`** the readiness verdict. Cheap enough to run from a health
225
+ - **`ablo status`:** the readiness verdict. Cheap enough to run from a health
226
226
  check on your own side.
227
- - **The [audit log](./audit.md)** every confirmed write traced back to the key
227
+ - **The [audit log](./audit.md):** every confirmed write traced back to the key
228
228
  that made it and the person who authorized that key.
229
- - **Your own logger** pass `logger` to the client and SDK lifecycle, sync,
229
+ - **Your own logger:** pass `logger` to the client and SDK lifecycle, sync,
230
230
  retry, and rollback events join your existing pipeline.
231
231
 
232
232
  Writes carry receipts rather than being fire-and-forget: a commit is accepted the
@@ -242,7 +242,7 @@ and what each promises.
242
242
  | `password authentication failed` during connect | Often a pooled host refusing a session it cannot serve, in the words of a wrong password. | Register the direct database host. |
243
243
  | `server_execute_unknown_model` | The plane's active schema does not carry that model. | `ablo push` with a key for that plane. |
244
244
  | Clients rejected at connect | The deployed schema and the client's schema disagree. | Push this tree, or deploy the revision the server is running. |
245
- | `project_scope_denied` (403) | The model belongs to another project in your org. | Use a key minted for that project a push cannot cross projects. |
245
+ | `project_scope_denied` (403) | The model belongs to another project in your org. | Use a key minted for that project: a push cannot cross projects. |
246
246
  | 403 on `ablo push` | The key authenticated but cannot author schema. | A secret `sk_live_`; the `ablo login` live key is observe-only. |
247
247
 
248
248
  ## The checklist
@@ -15,7 +15,7 @@ The three steps below show how to declare it, scope the agent, and write.
15
15
 
16
16
  See [Identity & Sync Groups](../identity.md) for the full reference.
17
17
 
18
- ## 1. Schema declare the relationship, once
18
+ ## 1. Schema: declare the relationship, once
19
19
 
20
20
  ```ts
21
21
  import { defineSchema, identityRole, model, relation, z } from '@abloatai/ablo/schema';
@@ -44,7 +44,7 @@ export const schema = defineSchema(
44
44
  );
45
45
  ```
46
46
 
47
- ## 2. Dispatch narrow the agent to the workspace it's working on
47
+ ## 2. Dispatch: narrow the agent to the workspace it's working on
48
48
 
49
49
  An agent can never reach more than the user who triggered it — that's the upper
50
50
  limit. From there you narrow it to a single workspace by minting the agent's session
@@ -95,7 +95,7 @@ const ablo = Ablo({
95
95
  groups the session asks for with the groups the identity is actually allowed, so
96
96
  the agent can never reach a workspace its triggering user couldn't.
97
97
 
98
- ## 3. Write it fans out to everyone on that workspace
98
+ ## 3. Write: it fans out to everyone on that workspace
99
99
 
100
100
  Inside any component under the provider, grab the scoped client with `useAblo()`
101
101
  and write. The connection is already narrowed to `workspace:<workspaceId>` from Step 2.
package/docs/groups.md CHANGED
@@ -38,6 +38,56 @@ never persists work built on a premise it can no longer see.
38
38
 
39
39
  ---
40
40
 
41
+ ## How you hear about it
42
+
43
+ Four channels carry "something changed", and they answer four different
44
+ questions. Pick by the question you have.
45
+
46
+ ```ts
47
+ // A screen that stays current.
48
+ ablo.documents.onChange((docs) => render(docs));
49
+
50
+ // Who else is in here, and what are they holding.
51
+ await using room = await ablo.documents.join(documentIds, { ttl: '5m' });
52
+ room.peers;
53
+
54
+ // Stop this write if the thing I read moved while I composed it.
55
+ await ablo.blocks.update({ id, data, reads: [{ group: 'workspace:abc', readAt, onStale: 'notify' }] });
56
+
57
+ // Tell me later if this moves, even though I am not writing now.
58
+ await ablo.documents.track({ id: 's-1' });
59
+ ```
60
+
61
+ | Question | Channel | Arrives |
62
+ | --- | --- | --- |
63
+ | What do the rows say right now? | `onChange` | As deltas land, on the socket |
64
+ | Who else is working here? | `join`, then `room.peers` and `room.claims` | As participants come and go, on the socket |
65
+ | Did the premise for **this** write move? | `reads` on the write | On that write's receipt, before it applies |
66
+ | Has anything I read moved since? | `track` | On your next commit's receipt |
67
+
68
+ Two distinctions do most of the work here.
69
+
70
+ **`join` is about people; `track` is about data.** Both open a subscription and
71
+ both are scoped by sync group, which is why they look alike. `join` reports
72
+ participants: who is present, what they are doing, which rows they hold. `track`
73
+ reports the rows themselves: something you said you cared about moved, here is
74
+ the watermark to re-read it at. A tool that wants to avoid duplicating a peer's
75
+ work needs `join`. A tool whose output goes stale when its inputs change needs
76
+ `track`.
77
+
78
+ **`reads` guards one write; `track` outlives it.** They speak the same
79
+ vocabulary and produce the same `StaleNotification`. A `reads` entry is checked
80
+ once, at the commit that carried it, and discarded. A `track` is persisted and
81
+ re-checked against every delta after it, so a long-running actor hears about a
82
+ change that landed while it was thinking, on the next commit it makes.
83
+
84
+ `onChange` and `join` need a live socket, so they are available on the default
85
+ WebSocket client. `reads` and `track` ride the commit, so they reach a socketless
86
+ actor over HTTP too, which is what makes them the notification path for agents
87
+ and workers.
88
+
89
+ ---
90
+
41
91
  ## Three ways a change reaches other rows
42
92
 
43
93
  "A affects B and C" means three different things. The engine does the first two
@@ -206,8 +256,12 @@ them coarse everywhere else.
206
256
 
207
257
  ## Where this is defined
208
258
 
209
- - **Access** who may read or write a group is [`identity.md`](./identity.md).
210
- - **The convention** — non-coercion, the premise, and the notification — is
259
+ - **Access**, meaning who may read or write a group, is
260
+ [`identity.md`](./identity.md).
261
+ - **The convention** behind non-coercion, the premise, and the notification is
211
262
  [`concurrency-convention.md`](./concurrency-convention.md) (§4 and §5).
212
- - **The mechanics** the three coordination blocks underneath are
263
+ - **The mechanics**, the three coordination blocks underneath, are
213
264
  [`coordination.md`](./coordination.md).
265
+ - **`join` and presence**, the participant half of the table above, are
266
+ [`coordination.md`](./coordination.md) for the claim stream and
267
+ [`react.md`](./react.md) for `useJoin`.
@@ -65,11 +65,13 @@ await ablo.weatherReports.update({
65
65
  `onStale: 'reject'` prevents lost updates. If the target changed after the
66
66
  snapshot, the server rejects the write instead of applying stale reasoning.
67
67
 
68
- Advanced policies exist for controlled product flows:
68
+ Two other dispositions exist. `overwrite` applies the write with no stale check
69
+ at all. `notify` **holds** the write, so the row is left as it stands, and hands
70
+ back a `StaleNotification` carrying the current value for the actor to reconcile
71
+ and re-issue; the rest of the batch still commits.
69
72
 
70
- - `reject` fails the write when state moved.
71
- - `overwrite` applies the write without stale protection.
72
- - `notify` accepts the write and marks it for product review.
73
+ See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
74
+ what each disposition is checked against, and where the convention stops.
73
75
 
74
76
  ## Claim Coordination
75
77
 
@@ -99,14 +101,39 @@ Agents should import the same schema as the app and write through
99
101
 
100
102
  ## Audit Trail
101
103
 
102
- Accepted writes can be attributed to:
104
+ Attribution is not a separate log you opt into. It rides on the change itself.
105
+ Every broadcast delta names the actor, the authority it acted under, the
106
+ credential that authorized it, and the approval stage it was in:
103
107
 
104
- - the actor that wrote,
105
- - the human or system the actor worked on behalf of,
106
- - the model, operation, and state cursor.
108
+ ```ts
109
+ {
110
+ modelName: 'weatherReports',
111
+ modelId: 'report_stockholm',
112
+ actionType: 'U',
113
+ actor: { kind: 'agent', id: 'weather-agent-v3' },
114
+ onBehalfOf: { kind: 'user', id: 'user_8f2a' },
115
+ capabilityId: '…', // the key the write was authorized by
116
+ confirmationState: 'auto', // previewed | approved | required_human_approval
117
+ createdAt: '2026-05-14T14:22:01.034Z',
118
+ }
119
+ ```
120
+
121
+ `actor` and `onBehalfOf` are derived from the credential, not from the call site,
122
+ so an agent cannot name a different actor in its own write. `capabilityId` is
123
+ non-null for every agent and system commit, so a write can always be traced to
124
+ the key that made it, and from that key to the person it was issued to.
125
+
126
+ The stored history goes one step further than recording. Audit rows are chained
127
+ with a keyed hash, so the log is tamper-*evident*: `verify-chain` walks the chain
128
+ and, if it breaks, names the sequence number and the hashes that disagree. No
129
+ chain roots at an agent. The delegation root is always the person who set the
130
+ work in motion.
131
+
132
+ For agent work this is what answers, after the fact: what changed, who authorized
133
+ it, which run did it, and whether a human was in the loop.
107
134
 
108
- For agent work, this is what lets an audit surface answer: "what changed, who
109
- authorized it, which run did it, and what state was it based on?"
135
+ See [Audit Log](./audit.md) for the stored row shape, the filters, verification,
136
+ and export.
110
137
 
111
138
  ## Persistence
112
139
 
@@ -13,13 +13,13 @@ await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }, wait: 'confir
13
13
  const task = ablo.tasks.local.retrieve('task_42');
14
14
  ```
15
15
 
16
- ## The mental model read this once
16
+ ## The mental model: read this once
17
17
 
18
18
  Ablo is a **coordination layer in front of your Postgres**. Agents, background
19
19
  jobs, and the people alongside them all change the same application data through
20
20
  one API, and Ablo makes sure their writes don't clobber each other.
21
21
 
22
- - **Writes go through Ablo.** `ablo.<model>.create / update / delete` enter Ablo's
22
+ - **Writes go through Ablo:** `ablo.<model>.create / update / delete` enter Ablo's
23
23
  commit chokepoint — where claims, ordering, and idempotency are enforced — and
24
24
  Ablo applies the change to your Postgres through a scoped writer role. The commit
25
25
  is accepted (`queued`) the moment Ablo takes it.
@@ -37,17 +37,41 @@ one API, and Ablo makes sure their writes don't clobber each other.
37
37
  That's the shape: **you write through Ablo → it lands in your Postgres → the WAL
38
38
  echo confirms it → everyone connected sees it live.**
39
39
 
40
+ ## The primitives
41
+
42
+ | Primitive | Plane | Purpose |
43
+ |---|---|---|
44
+ | `Schema` | State | Declares typed models the app and agents can read and write. |
45
+ | `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async reads), `local.retrieve`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
46
+ | `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
47
+ | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
48
+ | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
49
+
50
+ ### Why each primitive is separate
51
+
52
+ Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
53
+ does a job the others cannot. If you are coming from Replicache or Yjs you would
54
+ expect just `Commit`. Here is what the other two buy you over that minimum:
55
+
56
+ - **`Claim` is not a read lock.** Reads stay open. Claims serialize
57
+ acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
58
+ from fresh state.
59
+ - **`Receipt` is not a `200 OK`.** It is the durable artifact a commit produced:
60
+ accepted commit id, server-assigned timestamps, stale-check outcome. It is
61
+ addressable after the fact and replayable into a different client. A status
62
+ code cannot be re-read by a sub-agent that was not on the original call.
63
+
40
64
  ## Where your data lives
41
65
 
42
66
  You point Ablo at a Postgres database, and that's where its rows live. Only *which*
43
67
  database differs by environment — the code is identical.
44
68
 
45
- - **Production** your Postgres. `ablo connect` sets up a scoped writer role and
69
+ - **Production:** your Postgres. `ablo connect` sets up a scoped writer role and
46
70
  logical replication; your rows live in your database, and Ablo writes to them
47
71
  through that role.
48
- - **Sandbox and local dev** a separate or local Postgres you can throw away. Same
72
+ - **Sandbox and local dev:** a separate or local Postgres you can throw away. Same
49
73
  models, same code, a different database behind them.
50
- - **Before you connect one** Ablo keeps state in its own log, so you can build the
74
+ - **Before you connect one.** Ablo keeps state in its own log, so you can build the
51
75
  whole app today and point it at a real database when you're ready.
52
76
 
53
77
  Registering the database is the whole switch. There is no tier or flag to choose.
@@ -35,10 +35,10 @@ The key is not a lookup that happens before the write — it is the **execution
35
35
  itself**. Ablo inserts a pending row keyed by the caller and the key inside the same transaction as
36
36
  the mutation, and a unique index makes that insert the lock:
37
37
 
38
- - **Insert wins** this transaction owns the execution and runs the write.
39
- - **Insert conflicts** someone else owns it. The second caller waits for the owner to finish and
38
+ - **Insert wins:** this transaction owns the execution and runs the write.
39
+ - **Insert conflicts:** someone else owns it. The second caller waits for the owner to finish and
40
40
  then replays its recorded result.
41
- - **Insert conflicts, different request** the key was reused to mean something else. Rejected.
41
+ - **Insert conflicts, different request:** the key was reused to mean something else. Rejected.
42
42
 
43
43
  Because the lock and the write share a transaction, there is no window in which a write has happened
44
44
  but its key has not been recorded.
@@ -52,7 +52,7 @@ same key string without colliding, and one agent can never replay another's resu
52
52
  |---|---|
53
53
  | A new key | Runs the write. |
54
54
  | The same key, the same request, already finished | Replays the recorded result. The write does not run again. |
55
- | The same key, the same request, still running | Waits for the in-flight attempt, then replays its result. If the original is still running after a short wait, rejects with `idempotency_conflict` (409) retry the same key. |
55
+ | The same key, the same request, still running | Waits for the in-flight attempt, then replays its result. If the original is still running after a short wait, rejects with `idempotency_conflict` (409): retry the same key. |
56
56
  | The same key, a **different** request | Rejects with `idempotency_conflict` (409). A key is bound to the request it first arrived with. |
57
57
 
58
58
  Both conflict cases return the same code, so tell them apart by what your own
@@ -60,7 +60,7 @@ client did. If you retried an identical request, the original is still in flight
60
60
  — wait and retry the same key. If you changed the request, that is a client bug:
61
61
  use a new key.
62
62
 
63
- ## Failures are not replayed they re-run
63
+ ## Failures are not replayed: they re-run
64
64
 
65
65
  This is where Ablo deliberately differs from Stripe and from most payment APIs, and it is the
66
66
  behaviour most likely to surprise you.
@@ -106,7 +106,7 @@ may already have committed. Restore the original route and retry the same key.
106
106
  | Timeout or dropped connection, no response | Retry with the **same** key, with backoff. You get the recorded success, or the write runs now. |
107
107
  | `source_unreachable` (503) | Retry with the **same** key once connectivity recovers. The write stays pinned to its route. |
108
108
  | `replication_lag_timeout` (504) | The write may have materialized. Retry with the **same** key, or wait for source ingestion to catch up. |
109
- | `AbloStaleContextError` | Re-read the row, regenerate, then write under a **new** key the new write is a new intention. |
109
+ | `AbloStaleContextError` | Re-read the row, regenerate, then write under a **new** key: the new write is a new intention. |
110
110
  | `AbloClaimedError` | Someone else holds the row. Wait or yield; the key is unused, so reuse it when you retry. |
111
111
  | `idempotency_conflict` (409) after an identical retry | The original is still in flight. Wait, then retry the **same** key. |
112
112
  | `idempotency_conflict` (409) after changing the request | A client bug: a key is bound to the first request sent under it. Use a **new** key. |
package/docs/identity.md CHANGED
@@ -24,9 +24,9 @@ that.
24
24
  A **sync group** is a named channel of shared state — a string like
25
25
  `org:acme` or `workspace:abc123`. It is simultaneously:
26
26
 
27
- - **the unit of fan-out** a confirmed write to a row publishes a delta to
27
+ - **the unit of fan-out:** a confirmed write to a row publishes a delta to
28
28
  every participant subscribed to that row's sync group(s), and
29
- - **the unit of access** a participant receives a row's deltas *only if* the
29
+ - **the unit of access:** a participant receives a row's deltas *only if* the
30
30
  row's sync group is in their allowed set.
31
31
 
32
32
  There is no built-in `org` / `team` / `user` concept in the engine. Those are
@@ -101,16 +101,16 @@ const ablo = Ablo({ schema, apiKey: session.token });
101
101
 
102
102
  That's the whole surface. The rest of this doc is the *why* behind each line.
103
103
 
104
- ## Two kinds of group the whole mental model
104
+ ## Two kinds of group: the whole mental model
105
105
 
106
106
  You just saw a human get `org` / `team` groups and an agent get one `workspace`
107
107
  group. That split is the model. Every sync group is named after one of two
108
108
  things:
109
109
 
110
- - **Membership groups** named after *who you are*: `org:{id}`, `team:{id}`,
110
+ - **Membership groups:** named after *who you are*: `org:{id}`, `team:{id}`,
111
111
  `user:{id}`. Produced from **identity** (`identityRoles`, Half 1). They're
112
112
  standing and durable — they don't change as you work.
113
- - **Entity groups** named after *a thing*: `dataroom:{id}`, `workspace:{id}`,
113
+ - **Entity groups:** named after *a thing*: `dataroom:{id}`, `workspace:{id}`,
114
114
  `document:{id}`. Produced from a **row's id** (a model's entity scope, Half 2).
115
115
  They're granular — one per record — and any participant can be pointed at a
116
116
  specific set of them.
@@ -122,8 +122,8 @@ they are, so you declare them once in the schema.
122
122
 
123
123
  | | Subscribed by | Declared where | Gets |
124
124
  | --- | --- | --- | --- |
125
- | **Human** | *who they are* membership | **the schema** (`identityRoles`) a rule, written once | every `org` / `team` / `user` group their identity implies their whole standing world |
126
- | **Agent** | *what it's been given* entities | **code, at the spawn site** chosen per run | a handful of entity groups: the dataroom it's in, the documents it has read never beyond what its user's membership could reach |
125
+ | **Human** | *who they are*: membership | **the schema** (`identityRoles`): a rule, written once | every `org` / `team` / `user` group their identity implies: their whole standing world |
126
+ | **Agent** | *what it's been given*: entities | **code, at the spawn site**: chosen per run | a handful of entity groups: the dataroom it's in, the documents it has read: never beyond what its user's membership could reach |
127
127
 
128
128
  > **One line:** humans subscribe by who they are; agents subscribe by what
129
129
  > they've been given.
@@ -197,7 +197,7 @@ Scoping is two declarations that meet in the middle. One describes the
197
197
  (which group does this row belong to?). A participant sees a row **iff** the
198
198
  row's sync group is in the participant's allowed set.
199
199
 
200
- ### Half 1 `identityRoles`: identity → allowed groups
200
+ ### Half 1 (`identityRoles`): identity → allowed groups
201
201
 
202
202
  Declared once, on the schema, via the `identityRole({ kind, source })` factory.
203
203
  Each role is **pure data**: a `kind` (the group's prefix — `org`, `user`, `team`)
@@ -238,7 +238,7 @@ in-process and on a hosted server that only ever sees the compiled JSON.
238
238
  > `user:{id}` role above already covers it — see
239
239
  > [Agents are participants too](#agents-are-participants-too).
240
240
 
241
- ### Half 2 per-model scope: row → group
241
+ ### Half 2 (per-model scope): row → group
242
242
 
243
243
  You never write a sync-group string for a row. You declare a model's *place* in
244
244
  the entity graph and the engine derives the groups its rows fan out on. Three
@@ -310,7 +310,7 @@ options as `tenancy-option-removed` errors and steers you to `policy: { by:
310
310
  explicit `policy: { by: 'none' }`. See
311
311
  `packages/sync-engine/src/schema/model.ts` for the full option set.
312
312
 
313
- ## How identity reaches Ablo the proxy model
313
+ ## How identity reaches Ablo: the proxy model
314
314
 
315
315
  This is the part the README's "authenticates with the signed-in user's
316
316
  session" glossed over. Concretely:
@@ -400,9 +400,9 @@ What carries identity — and just as importantly, what does *not* set the bound
400
400
 
401
401
  | Where | Purpose |
402
402
  | ------------ | ------------------------------------------------------------------------------------------------ |
403
- | `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary the server enforces scope from the authenticated request. |
403
+ | `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary: the server enforces scope from the authenticated request. |
404
404
  | `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
405
- | `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows it can never widen it. Passed to `sessions.create({ user \| agent, syncGroups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('workspace', 'abc123')]`. |
405
+ | `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows: it can never widen it. Passed to `sessions.create({ user \| agent, syncGroups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('workspace', 'abc123')]`. |
406
406
 
407
407
  Because the server is the boundary, a client that changes `userId` to another
408
408
  user's id does not gain their data — the server resolves and enforces the real
@@ -431,7 +431,7 @@ agent authority = (triggering user's allowed set) ← ceiling, inherited (on-
431
431
  ```
432
432
 
433
433
  Concretely: each model an agent edits declares a `scope`
434
- ([Half 2](#half-2--per-model-scope-row--group)), so each row forms its own
434
+ ([Half 2](#half-2-per-model-scope-row--group)), so each row forms its own
435
435
  group. The agent subscribes only to the groups for the rows it touches. Declare
436
436
  an entity anchor on the models an agent operates on:
437
437
 
@@ -475,22 +475,21 @@ not *what's reachable*.
475
475
  Three rules make agent access safe, and they fall out of the model above rather
476
476
  than needing a separate agent permission system:
477
477
 
478
- - **Inherit the user, and no more** the OAuth
478
+ - **Inherit the user, and no more:** the OAuth
479
479
  [on-behalf-of](https://workos.com/blog/oauth-on-behalf-of-ai-agents) model: the
480
480
  agent's reach is tied to the consenting user, never the org.
481
- - **Least privilege, just-in-time** scoped to the task's entities, not standing
481
+ - **Least privilege, just-in-time:** scoped to the task's entities, not standing
482
482
  org-wide access (the over-privilege pattern
483
483
  [OWASP's NHI Top 10](https://www.token.security/assets/the-ultimate-non-human-identity-security-guide)
484
484
  flags as the dominant agent risk).
485
- - **Dual-principal attribution** record both the executing agent and the
485
+ - **Dual-principal attribution:** record both the executing agent and the
486
486
  triggering human.
487
487
 
488
488
  Identity is 1:1 with a human participant; authority is narrowed to the work. That
489
489
  split is what lets Ablo keep *one model API for every actor* without ever
490
490
  granting an agent standing access to everything its user can see. The agent that
491
- runs the [Coordinating long agent work](../README.md#coordinating-long-agent-work)
492
- `claim` loop is, to the scoping layer, that same participant — scoped to the row
493
- it claimed.
491
+ runs the [Coordination](./coordination.md) `claim` loop is, to the scoping layer,
492
+ that same participant — scoped to the row it claimed.
494
493
 
495
494
  ## Narrowing to specific entities
496
495
 
@@ -530,7 +529,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
530
529
 
531
530
  > **`groups.root` is the schema model option, not a client setting.**
532
531
  > `groups: { root: 'workspace' }` in `model(...)` declares a scope root
533
- > ([Half 2](#half-2--per-model-scope-row--group)) — it names the group
532
+ > ([Half 2](#half-2-per-model-scope-row--group)) — it names the group
534
533
  > (`workspace:<id>`) that the mechanisms above then subscribe to.
535
534
  > There is no `Ablo({ scope })` constructor option. The lifecycle filter on
536
535
  > [`list()`](./api.md#model-methods) is a separate axis named **`state`**
@@ -543,7 +542,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
543
542
  > can't reach a workspace its capability doesn't already permit, no matter what it
544
543
  > passes. Smaller bootstrap, less fan-out, same server-enforced boundary.
545
544
 
546
- ## How this compares and the best practices it follows
545
+ ## How this compares, and the best practices it follows
547
546
 
548
547
  Ablo's identity model is not novel; it's the convergent answer every serious
549
548
  realtime / sync SDK arrived at. Knowing which industry pattern it *is* tells you
@@ -551,7 +550,7 @@ how to reason about it.
551
550
 
552
551
  **Realtime authorization splits into two shapes.** Ablo is firmly in the first:
553
552
 
554
- - **Server derives scope from authenticated identity** the server decides what
553
+ - **Server derives scope from authenticated identity:** the server decides what
555
554
  a participant may read/write and the client cannot override it. This is Ablo's
556
555
  proxy model. It's the same shape as
557
556
  [Supabase Realtime's RLS-on-connect](https://supabase.com/docs/guides/realtime/authorization)
@@ -560,7 +559,7 @@ how to reason about it.
560
559
  checks the permissions for you" — recommended for production), and
561
560
  [ElectricSQL **proxy auth**](https://electric-sql.com/docs/guides/auth) (a
562
561
  reverse-proxy sets shape params server-side before forwarding).
563
- - **Client proposes, server authorizes the exact request** the client names
562
+ - **Client proposes, server authorizes the exact request:** the client names
564
563
  the room/shape and the server signs off, as in
565
564
  [Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
566
565
  [ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
package/docs/index.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ablo Docs
2
2
 
3
- > The agentic coordination layer: one API for AI agents, apps, and services to claim, change, and confirm the same rows.
3
+ > Collaboration infrastructure for AI agents: one API for agents, apps, and services to claim, change, and confirm the same rows.
4
4
 
5
5
  Two agents reach for the same row. One claims it, does slow work — an LLM call,
6
6
  a fetch, a chain of tools — and commits. The second is neither rejected nor
@@ -110,12 +110,12 @@ Every surface reaches the same coordinated state. Pick by who is calling.
110
110
 
111
111
  | Surface | Use it for |
112
112
  |---|---|
113
- | **SDK** `@abloatai/ablo`, `transport: 'http'` | The agents themselves. Stateless, request/response, nothing held open. The main path. |
114
- | **Coordination MCP** `@abloatai/mcp` | An agent living inside an MCP host that needs claim and commit as tools. A data plane. |
115
- | **`humans()`** with `@abloatai/ablo/react` | The interfaces a person watches agent work arrive in: presence, live queries, a local copy. |
116
- | **CLI** `ablo` | Scaffolding, schema push, connecting a database. Terminals and CI. |
117
- | **REST** `/api/v1` | Runtimes with no SDK. |
118
- | **Integration-helper MCP** hosted `/api/mcp` | Teaching a coding assistant the SDK while you build. Docs, lint, and scaffolds only. |
113
+ | **SDK**: `@abloatai/ablo`, `transport: 'http'` | The agents themselves. Stateless, request/response, nothing held open. The main path. |
114
+ | **Coordination MCP**: `@abloatai/mcp` | An agent living inside an MCP host that needs claim and commit as tools. A data plane. |
115
+ | **`humans()`**: with `@abloatai/ablo/react` | The interfaces a person watches agent work arrive in: presence, live queries, a local copy. |
116
+ | **CLI**: `ablo` | Scaffolding, schema push, connecting a database. Terminals and CI. |
117
+ | **REST**: `/api/v1` | Runtimes with no SDK. |
118
+ | **Integration-helper MCP**: hosted `/api/mcp` | Teaching a coding assistant the SDK while you build. Docs, lint, and scaffolds only. |
119
119
 
120
120
  The two MCP servers are not interchangeable. The coordination server changes
121
121
  your data; the integration-helper server serves documentation and has no
@@ -136,6 +136,7 @@ default caller, not a special one.
136
136
 
137
137
  ## Concepts
138
138
 
139
+ - [How Ablo Works](./how-it-works.md) — the mental model in one page: you write through Ablo, it lands in your Postgres, the write-ahead log confirms it. **Read this first.**
139
140
  - [Coordination](./coordination.md) — `claim`, `claim.state`, and `claim.queue`: who holds a row, and who is waiting.
140
141
  - [Concurrency Convention](./concurrency-convention.md) — the governing rule for how concurrent writes resolve.
141
142
  - [Guarantees](./guarantees.md) — what a confirmed write, a stale-write rejection, and a claim each promise.
@@ -144,7 +145,6 @@ default caller, not a special one.
144
145
  - [Agents](./agents.md) — the stateless participant: wake, read, claim, commit, idle.
145
146
  - [Agent Messaging](./agent-messaging.md) — durable handoffs between agents, linked to the claim they discuss.
146
147
  - [Identity & Sync Groups](./identity.md) — who is connecting, and which slice of state they see.
147
- - [Interaction Model](./interaction-model.md) — the schema, claim, update, confirmation loop.
148
148
  - [Change Propagation](./groups.md) — how one row's change reaches the actors that depend on it.
149
149
  - [Client Behavior](./client-behavior.md) — options, errors, retries, timeouts, and imports.
150
150
 
@@ -11,7 +11,7 @@ backend and a database, one model at a time.
11
11
 
12
12
  Three things hold no matter which actor is writing:
13
13
 
14
- - **One model API for every actor.** `ablo.<model>.update(...)` is what
14
+ - **One model API for every actor:** `ablo.<model>.update(...)` is what
15
15
  React components, server actions, background workers, and AI agents
16
16
  all call. No separate "agent SDK," no parallel mutation path. The
17
17
  attribution comes from the credential, not the call site.
@@ -351,8 +351,19 @@ server -> ablo.weatherReports.update(...)
351
351
  Ablo coordinates those writes, fans out confirmed deltas, exposes active claims,
352
352
  and lets callers reject stale writes with `readAt`.
353
353
 
354
- Direct writes to your own database bypass that stream until your app reports the
355
- change through Data Source events.
354
+ A write that reaches your database some other way still reaches connected
355
+ clients. A `psql` session, a cron job, an admin tool, a legacy endpoint: Ablo
356
+ tails your write-ahead log, so a change it did not make is picked up and fanned
357
+ out like any other, attributed to the data source rather than to an agent.
358
+
359
+ What such a write does not get is the coordination. It never entered the commit
360
+ chokepoint, so no claim was checked, no `readAt` was compared, and no idempotency
361
+ key was honoured. It can land on top of a row an agent is holding. Route anything
362
+ that must respect a claim through `ablo.<model>`.
363
+
364
+ On the [Data Source endpoint fallback](./data-sources.md) there is no replication
365
+ stream to tail, and there the original caveat holds: a direct write stays
366
+ invisible until your app reports it through Data Source events.
356
367
 
357
368
  ## 6. Existing API Backend
358
369
 
package/docs/mcp.md CHANGED
@@ -6,7 +6,7 @@ Ablo publishes **two** MCP servers for two different jobs. Don't confuse them:
6
6
 
7
7
  | Server | Purpose | Auth | Tools |
8
8
  |---|---|---|---|
9
- | **Coordination** (`@abloatai/mcp`) | Manage your Ablo the way the CLI does, and let an agent safely read & mutate application data | API key (`sk_…` / `rk_…`) | projects, schema, logs, usage plus `get` / `list` / `create` / `update` / `delete` / `claim` / `release` over your rows |
9
+ | **Coordination** (`@abloatai/mcp`) | Manage your Ablo the way the CLI does, and let an agent safely read & mutate application data | API key (`sk_…` / `rk_…`) | projects, schema, logs, usage: plus `get` / `list` / `create` / `update` / `delete` / `claim` / `release` over your rows |
10
10
  | **Integration-helper** (hosted `/api/mcp`) | Help an AI coding assistant write SDK integration code that compiles | none (public docs) | doc search, export surface, schema lint, scaffold |
11
11
 
12
12
  The coordination server manages your account **and is the data plane** — it is
@@ -37,7 +37,7 @@ claude mcp add ablo -- npx -y @abloatai/mcp
37
37
  | `list_projects` | `ablo projects list` | the org's projects (needs `sk_`) |
38
38
  | `create_project` | `ablo projects create` | create one (needs `sk_`) |
39
39
  | `tail_logs` | `ablo logs` | recent commits and the actor behind each |
40
- | `get_usage` | | usage in daily buckets |
40
+ | `get_usage` |: | usage in daily buckets |
41
41
 
42
42
  There are no key-management tools. A mint returns the plaintext once — only a
43
43
  hash is kept — so no tool can hand it back later, and returning it at mint time
@@ -63,7 +63,7 @@ Each tool mirrors an SDK verb, scoped to a model + id. Model names come from
63
63
  | `update_model` | `ablo.<model>.update({ id, … })` | guarded update |
64
64
  | `delete_model` | `ablo.<model>.delete({ id })` | guarded delete |
65
65
  | `claim_model` | `ablo.<model>.claim({ id })` | acquire / queue a coordination lease |
66
- | `release_claim` | | release the lease so others proceed |
66
+ | `release_claim` |: | release the lease so others proceed |
67
67
 
68
68
  The agent-facing contract — the safe loop, the "derive idempotency keys from
69
69
  the business event" rule, and the error-code playbook — ships as a loadable
@@ -94,9 +94,9 @@ claude mcp add --transport http ablo https://<your-app>/api/mcp
94
94
 
95
95
  The endpoint is identical for every client — only the config surface differs:
96
96
 
97
- - **Claude Code** run the `claude mcp add` command above; verify with `/mcp list`, remove with `claude mcp remove ablo`.
98
- - **Cursor** add the server to `~/.cursor/mcp.json` (macOS / Linux), then restart.
99
- - **Windsurf** add the same JSON via Settings → Cascade → MCP, then restart.
97
+ - **Claude Code:** run the `claude mcp add` command above; verify with `/mcp list`, remove with `claude mcp remove ablo`.
98
+ - **Cursor:** add the server to `~/.cursor/mcp.json` (macOS / Linux), then restart.
99
+ - **Windsurf:** add the same JSON via Settings → Cascade → MCP, then restart.
100
100
 
101
101
  Cursor and Windsurf use the same config shape:
102
102
 
@@ -120,7 +120,7 @@ Each client then lists the Ablo tools (`search_ablo_docs`, `get_recipe`, `get_ap
120
120
  | `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
121
121
  | `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
122
122
  | `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
123
- | `scaffold_app` | Emits a starter file tree for a schema-first integration `next`, `node-agent`, or `plain`, with a `data-source` (your own database) endpoint. |
123
+ | `scaffold_app` | Emits a starter file tree for a schema-first integration: `next`, `node-agent`, or `plain`, with a `data-source` (your own database) endpoint. |
124
124
 
125
125
  #### Resources
126
126