@abloatai/ablo 0.5.1 → 0.6.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 (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +217 -122
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
package/docs/api.md CHANGED
@@ -11,30 +11,30 @@ import Ablo from '@abloatai/ablo';
11
11
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
12
12
 
13
13
  const schema = defineSchema({
14
- tasks: model({
15
- title: z.string(),
16
- status: z.enum(['todo', 'doing', 'done']),
14
+ weatherReports: model({
15
+ location: z.string(),
16
+ status: z.enum(['pending', 'ready']),
17
17
  }),
18
18
  });
19
19
 
20
20
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
21
21
 
22
22
  await ablo.ready();
23
- const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
24
- if (!task) throw new Error('Task not found');
23
+ const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
24
+ if (!report) throw new Error('Row not found');
25
25
 
26
- await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
26
+ await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
27
27
  ```
28
28
 
29
29
  ## Model Methods
30
30
 
31
- Each schema model becomes a typed resource on the client:
31
+ Each schema model becomes a typed model on the client:
32
32
 
33
- - `ablo.tasks.load({ where })` hydrates rows asynchronously.
34
- - `ablo.tasks.retrieve(id)` reads one already-loaded row synchronously.
35
- - `ablo.tasks.create(data)` creates a row.
36
- - `ablo.tasks.update(id, data, options?)` updates a row.
37
- - `ablo.tasks.delete(id, options?)` deletes a row.
33
+ - `ablo.weatherReports.load({ where })` hydrates rows asynchronously.
34
+ - `ablo.weatherReports.retrieve(id)` reads one already-loaded row synchronously.
35
+ - `ablo.weatherReports.create(data)` creates a row.
36
+ - `ablo.weatherReports.update(id, data, options?)` updates a row.
37
+ - `ablo.weatherReports.delete(id, options?)` deletes a row.
38
38
 
39
39
  `load` and `retrieve` are not aliases. Use `load` when the row may not be in the
40
40
  local pool yet. Use `retrieve` after `ready()` or `load()` when you want a cheap
@@ -53,9 +53,9 @@ local read.
53
53
  `list` and `count` read the local pool. They default to live rows and accept:
54
54
 
55
55
  ```ts
56
- const activeDoneTasks = ablo.tasks.list({
57
- where: { status: 'done' },
58
- filter: (task) => !task.title.startsWith('[archived]'),
56
+ const readyReports = ablo.weatherReports.list({
57
+ where: { status: 'ready' },
58
+ filter: (report) => !report.location.startsWith('[archived]'),
59
59
  orderBy: { updatedAt: 'desc' },
60
60
  limit: 20,
61
61
  scope: 'live', // 'live' | 'archived' | 'all'
@@ -67,11 +67,11 @@ const activeDoneTasks = ablo.tasks.list({
67
67
  Use `snapshot` when a write should reject if the row changed mid-flight:
68
68
 
69
69
  ```ts
70
- const snap = ablo.snapshot({ tasks: 'task_123' });
70
+ const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
71
71
 
72
- await ablo.tasks.update(
73
- 'task_123',
74
- { status: 'done' },
72
+ await ablo.weatherReports.update(
73
+ 'report_stockholm',
74
+ { status: 'ready' },
75
75
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
76
76
  );
77
77
  ```
@@ -82,62 +82,43 @@ Protected write options:
82
82
  |---|---|
83
83
  | `readAt` | The state cursor the write was based on. |
84
84
  | `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
85
- | `intent` | Active work claim associated with the write. |
86
85
  | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
87
86
  | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
88
87
  | `timeout` | Maximum time to wait for the write call. |
89
88
 
90
- ## Advanced Resource API
89
+ ## Claims
91
90
 
92
- Use `resource(name)` only when you intentionally need the raw protocol shape:
93
- generic server runtimes, MCP routes, batch tools, or code that has no schema.
91
+ A claim tells humans and agents who is working on a target before the write
92
+ lands. One self-describing object carries the lifecycle in a single `status`
93
+ field. It lives on the coordination plane: ephemeral, TTL'd, broadcast to peers
94
+ in real time, and never persisted as a row.
94
95
 
95
- ```ts
96
- const tasks = ablo.resource<{ status: string }>('tasks');
97
-
98
- const { data, stamp, intents } = await tasks.retrieve('task_123', {
99
- ifBusy: 'return',
100
- });
101
- ```
102
-
103
- `stamp` is the state watermark. Pass it as `readAt` to reject stale writes.
96
+ Coordinate one through flat verbs on the model, beside `create`/`update`/`retrieve`:
97
+ `ablo.<model>.claim(id, ...)` to claim a row, `ablo.<model>.claimState(id)` to read
98
+ who holds it (synchronous; never blocks), and `ablo.<model>.release(id)` to release
99
+ early. Claims are **advisory** they serialize on contention rather than locking.
104
100
 
105
- `intents` lists active work on the target. Set `ifBusy: 'wait'` to wait for
106
- that work to clear, or `ifBusy: 'fail'` to throw `AbloBusyError`.
107
-
108
- ## Intent
109
-
110
- Intent is the coordination object — it tells humans and agents who is working on
111
- a target before the write lands. Like Stripe's `PaymentIntent`, one
112
- self-describing object carries the whole lifecycle in a single `status` field.
113
- It lives on the **coordination plane**: ephemeral, TTL'd, broadcast to peers in
114
- real time, and never persisted as a row.
115
-
116
- Read or open one through the model accessor — `ablo.<model>.intent(id)` — which
117
- sits beside `create`/`update`/`retrieve` and returns a handle **synchronously**,
118
- so you can inspect who holds a target without awaiting.
119
-
120
- ### The Intent object
101
+ ### The Claim State Object
121
102
 
122
103
  | Field | Type | Description |
123
104
  |---|---|---|
124
- | `object` | `'intent'` | String representing the object's type. Always `'intent'`. |
125
- | `id` | string | Unique identifier for the intent. |
105
+ | `object` | `'claim'` | String representing the object's type. |
106
+ | `id` | string | Unique identifier for the claim. |
126
107
  | `status` | `'active' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. |
127
108
  | `target` | `{ type, id, field? }` | What is being coordinated. |
128
109
  | `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
129
- | `heldBy` | string | Participant id holding the intent. |
110
+ | `heldBy` | string | Participant id holding the claim. |
130
111
  | `participantKind` | `'human' \| 'agent'` | Whether a human session or an agent holds it. |
131
112
  | `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
132
113
 
133
114
  ```json
134
115
  {
135
- "object": "intent",
136
- "id": "int_3MtwBwLkdIwHu7ix",
116
+ "object": "claim",
117
+ "id": "claim_3MtwBwLkdIwHu7ix",
137
118
  "status": "active",
138
- "target": { "type": "tasks", "id": "task_123", "field": "status" },
119
+ "target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
139
120
  "action": "editing",
140
- "heldBy": "agent:task-writer",
121
+ "heldBy": "agent:report-writer",
141
122
  "participantKind": "agent",
142
123
  "expiresAt": "1716580000000"
143
124
  }
@@ -146,128 +127,57 @@ so you can inspect who holds a target without awaiting.
146
127
  ### Lifecycle
147
128
 
148
129
  ```
149
- claim() update() lands
130
+ claim(id) update(id) lands
150
131
  (free) ───────────▶ active ───────────────────────▶ committed
151
132
 
152
133
  ┌───────────┴───────────┐
153
134
  ▼ ▼
154
135
  canceled expired
155
- (finish w/o write) (TTL; holder died)
136
+ (release w/o write) (TTL; holder died)
156
137
  ```
157
138
 
158
- A target is free when `ablo.<model>.intent(id).current` is `null`. Terminal
159
- states drop out of the live stream a present intent is, by definition,
160
- `active`.
139
+ A target is free when `ablo.<model>.claimState(id)` is `null`. Terminal
140
+ states drop out of the live stream, so a present claim is active.
161
141
 
162
142
  ### Reading and claiming
163
143
 
164
- ```ts
165
- const task = ablo.tasks.intent('task_123');
144
+ `claimState(id)` is the read side for observers: synchronous, never blocks, and
145
+ returns the live claim state object (or `null`). `claim(id, ...)` is the write side:
146
+ it claims the row and returns the row. Because the claim is **advisory**, if
147
+ someone else already holds the row, `claim` waits for them to finish, then
148
+ re-reads the row before handing it back — so you always proceed from fresh state.
149
+ Default reads stay open; server/model reads can opt into `ifClaimed: 'wait'` or
150
+ `ifClaimed: 'fail'` when they should not read through active work.
166
151
 
152
+ ```ts
167
153
  // Read side — who is working on this target right now?
168
- if (task.current) {
169
- task.current.heldBy; // 'agent:task-writer'
170
- task.current.action; // 'editing'
171
- await task.whenFree(); // wait until they finish, then continue
154
+ const claim = ablo.weatherReports.claimState('report_stockholm');
155
+ if (claim) {
156
+ claim.heldBy; // 'agent:report-writer'
157
+ claim.action; // 'editing'
172
158
  }
173
159
 
174
- // Write side — claim, write, auto-release in one flow.
175
- await task.claim({ action: 'editing', field: 'status', ttl: '2m' });
176
- const updated = await task.update({ status: 'done' });
177
- updated.status; // 'done'
178
- ```
179
-
180
- `task.update(...)` carries the same stale-check as a plain update: it rejects
181
- with `AbloStaleContextError` if the row advanced past your claim point, so you
182
- re-read before retrying. The intent releases automatically when `update`
183
- resolves; call `task.finish()` if the work ends without a write.
184
-
185
- `task.whenFree({ timeout })` waits until the target is free. Pass `timeout` only
186
- when your product needs an upper bound.
187
-
188
- ### Cross-resource coordination
189
-
190
- For lower-level coordination that isn't scoped to a single model row, the
191
- top-level `ablo.intents` resource (`create`, `list`, `waitFor`) remains
192
- available. Most callers should prefer `ablo.<model>.intent(id)`.
193
-
194
- ## Advanced Commit API
195
-
196
- Most callers use `ablo.<model>.update(...)` or `resource.update(...)`. For atomic
197
- batches or custom runtimes, use `commits.create`.
198
-
199
- ```ts
200
- await ablo.commits.create({
201
- wait: 'confirmed',
202
- operations: [
203
- {
204
- action: 'update',
205
- resource: 'tasks',
206
- id: 'task_123',
207
- data: { status: 'done' },
208
- readAt: stamp,
209
- onStale: 'reject',
210
- },
211
- ],
212
- });
160
+ // Write side — claim for the duration of the callback.
161
+ const updated = await ablo.weatherReports.claim(
162
+ 'report_stockholm',
163
+ async (report) => ablo.weatherReports.update(report.id, { status: 'ready' }),
164
+ { action: 'editing', ttl: '2m' },
165
+ );
166
+ updated.status; // 'ready'
213
167
  ```
214
168
 
215
- Every state change becomes a commit. Commits check authorization, stale state,
216
- intent conflicts, and idempotency.
169
+ Writes go through the normal flat `ablo.<model>.update(id, data)`. While you hold
170
+ a claim on `id`, that `update` is automatically stale-guarded: it rejects with
171
+ `AbloStaleContextError` if the row advanced past your claim point, so you re-read
172
+ before retrying. The callback form releases automatically when the callback
173
+ returns or throws, or call `ablo.weatherReports.release(id)` if you claimed manually and
174
+ need to release early.
217
175
 
218
176
  ## Agent
219
177
 
220
178
  Most agents should import the same schema as the app and call
221
- `ablo.<model>.load(...)` plus `ablo.<model>.update(...)`. The schema-less
222
- `agent.run(...)` wrapper exists for advanced server workers that cannot import
223
- the app schema; it creates the capability and task internally and returns
224
- `done`, `failed`, or `cancelled`.
225
-
226
- ## Data Source
227
-
228
- Use `dataSource(...)` only when the customer's app database remains canonical
229
- and Ablo should call a signed endpoint instead of storing customer rows itself.
230
-
231
- ```ts
232
- import { dataSource } from '@abloatai/ablo';
233
- import { schema } from './ablo.schema';
234
-
235
- export const POST = dataSource({
236
- schema,
237
- apiKey: process.env.ABLO_API_KEY,
238
- async commit({ operations, clientTxId, context }) {
239
- // Write operations to the customer's database transaction.
240
- return { rows: [] };
241
- },
242
- });
243
- ```
244
-
245
- The SDK still uses `ablo.<model>.update(...)`. The Data Source endpoint is a
246
- server-to-server storage adapter. See [Connect Your Database](./data-sources.md).
247
-
248
- ## Capability
249
-
250
- Capabilities are the lower-level permission boundary. Most apps should let
251
- `agent.run(...)` create and revoke them.
252
-
253
- ```ts
254
- const capability = await api.capabilities.create({
255
- participantKind: 'agent',
256
- participantId: 'agent:task-writer',
257
- // Strings derive from the schema's `identityRoles` templates
258
- // (see integration-guide.md §1) or a model's `syncGroupFormat`.
259
- syncGroups: ['org:acme', 'user:agent:task-writer'],
260
- operations: ['tasks.retrieve', 'tasks.update'],
261
- lease: '10m',
262
- });
263
- ```
264
-
265
- Use `lease` as a crash cleanup window. Normal agent runs still close when the
266
- handler returns, fails, or is cancelled.
267
-
268
- For the design rationale — why capabilities instead of static API keys,
269
- why the lease + signature + revocation triple, and how this maps to AWS
270
- STS / Vault / Auth0 Token Vault — see [Capabilities](./capabilities.md#why-capabilities-not-api-keys).
179
+ `ablo.<model>.load(...)`, `ablo.<model>.claim(...)`, and
180
+ `ablo.<model>.update(...)`.
271
181
 
272
182
  ## Errors
273
183
 
@@ -283,6 +193,6 @@ All SDK errors extend `AbloError` and expose a stable `type` string.
283
193
  | `AbloValidationError` | Invalid input. |
284
194
  | `AbloServerError` | Server-side 5xx. |
285
195
  | `AbloStaleContextError` | `readAt` no longer matches current state. |
286
- | `AbloBusyError` | Active intent conflict or busy wait timeout. |
196
+ | `AbloClaimedError` | Active claim conflict or claim wait timeout. |
287
197
 
288
198
  See [Client Behavior](./client-behavior.md) for retry and timeout guidance.
package/docs/audit.md CHANGED
@@ -12,11 +12,11 @@ tamper-evident, queryable, exportable.
12
12
  actorId: string,
13
13
  onBehalfOfKind: 'user' | 'agent' | 'system' | null,
14
14
  onBehalfOfId: string | null,
15
- capabilityId: string | null,
16
- capabilityLabel: string | null,
15
+ credentialId: string | null,
16
+ credentialLabel: string | null,
17
17
  delegationChainRoot: string | null, // always points at a human
18
- causedByTaskId: string | null,
19
- actionType: string, // e.g. 'task.update'
18
+ causedByRunId: string | null,
19
+ actionType: string, // e.g. 'weatherReport.update'
20
20
  modelName: string | null, // e.g. 'claude-opus-4-7'
21
21
  diffSummary: unknown,
22
22
  // tamper-evident
@@ -37,7 +37,7 @@ a thing in this system; every chain starts with a person.
37
37
  ```bash
38
38
  curl https://<your-app>/api/orgs/<slug>/audit/verify-chain?\
39
39
  principalKind=agent\
40
- &principalId=task-writer-v3
40
+ &principalId=weather-agent-v3
41
41
  ```
42
42
 
43
43
  Returns either:
@@ -9,9 +9,9 @@ import Ablo from '@abloatai/ablo';
9
9
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
10
10
 
11
11
  const schema = defineSchema({
12
- tasks: model({
13
- title: z.string(),
14
- status: z.enum(['todo', 'doing', 'done']),
12
+ weatherReports: model({
13
+ location: z.string(),
14
+ status: z.enum(['pending', 'ready']),
15
15
  }),
16
16
  });
17
17
 
@@ -25,7 +25,7 @@ Common options:
25
25
 
26
26
  | Option | Purpose |
27
27
  |---|---|
28
- | `schema` | Required for typed model resources. Omit only for advanced schema-less runtimes. |
28
+ | `schema` | Required for typed model clients. |
29
29
  | `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
30
30
  | `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
31
31
  | `persistence` | `volatile` by default. Use `indexeddb` for browser durable cache and offline queueing. |
@@ -40,17 +40,17 @@ endpoint.
40
40
 
41
41
  ## Model Methods
42
42
 
43
- Each schema model becomes a typed resource:
43
+ Each schema model becomes a typed model:
44
44
 
45
45
  ```ts
46
46
  await ablo.ready();
47
47
 
48
- const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
49
- const local = ablo.tasks.retrieve('task_123');
48
+ const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
49
+ const local = ablo.weatherReports.retrieve('report_stockholm');
50
50
 
51
- await ablo.tasks.create({ title: 'Draft launch plan', status: 'todo' });
52
- await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
53
- await ablo.tasks.delete('task_123', { wait: 'confirmed' });
51
+ await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
52
+ await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
53
+ await ablo.weatherReports.delete('report_stockholm', { wait: 'confirmed' });
54
54
  ```
55
55
 
56
56
  `load` is async hydration from local store and server. `retrieve`, `list`, and
@@ -63,15 +63,15 @@ rows.
63
63
 
64
64
  ## Multiplayer Behavior
65
65
 
66
- Multiplayer works when every participant uses the same model resource path. A
66
+ Multiplayer works when every participant uses the same model client path. A
67
67
  human Server Action, a browser view, and an agent worker can all use
68
- `ablo.tasks`:
68
+ `ablo.weatherReports`:
69
69
 
70
70
  ```ts
71
- const [task] = await ablo.tasks.load({ where: { id } });
72
- const snap = ablo.snapshot({ tasks: id });
71
+ const [report] = await ablo.weatherReports.load({ where: { id } });
72
+ const snap = ablo.snapshot({ weatherReports: id });
73
73
 
74
- await ablo.tasks.update(id, patch, {
74
+ await ablo.weatherReports.update(id, patch, {
75
75
  readAt: snap.stamp,
76
76
  onStale: 'reject',
77
77
  wait: 'confirmed',
@@ -79,9 +79,9 @@ await ablo.tasks.update(id, patch, {
79
79
  ```
80
80
 
81
81
  The confirmed write fans out over realtime subscriptions. React clients that use
82
- `useAblo((ablo) => ablo.tasks.retrieve(id))` receive the new row, and selectors
83
- such as `useAblo((ablo) => ablo.intents.list({ resource: 'tasks', id }))`
84
- receive active intents. There is
82
+ `useAblo((ablo) => ablo.weatherReports.retrieve(id))` receive the new row, and selectors
83
+ such as `useAblo((ablo) => ablo.weatherReports.claimState(id))`
84
+ receive active claim state. There is
85
85
  no extra multiplayer setup beyond routing shared state through Ablo.
86
86
 
87
87
  If an app writes directly to its database, Ablo cannot coordinate that write
@@ -90,15 +90,14 @@ until the app reports it through Data Source events.
90
90
  ## Per-Write Options
91
91
 
92
92
  ```ts
93
- await ablo.tasks.update(
94
- 'task_123',
95
- { status: 'done' },
93
+ await ablo.weatherReports.update(
94
+ 'report_stockholm',
95
+ { status: 'ready' },
96
96
  {
97
97
  wait: 'confirmed',
98
98
  readAt: snap.stamp,
99
99
  onStale: 'reject',
100
- intent,
101
- idempotencyKey: 'task_123:mark-done:v1',
100
+ idempotencyKey: 'report_stockholm:mark-ready:v1',
102
101
  timeout: 20_000,
103
102
  },
104
103
  );
@@ -109,31 +108,31 @@ await ablo.tasks.update(
109
108
  | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
110
109
  | `readAt` | State cursor the write was based on. |
111
110
  | `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
112
- | `intent` | Active work claim associated with this write. |
113
111
  | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
114
112
  | `timeout` | Maximum time for the write call. |
115
113
 
116
- ## Busy Behavior
114
+ ## Claimed Behavior
117
115
 
118
116
  ```ts
119
- const busy = ablo.intents.list({ resource: 'tasks', id: 'task_123' });
117
+ const active = ablo.weatherReports.claimState('report_stockholm');
120
118
 
121
- if (busy.length > 0) {
122
- await ablo.intents.waitFor(
123
- { resource: 'tasks', id: 'task_123' },
124
- { timeout: 30_000 },
125
- );
119
+ if (active) {
120
+ return { status: 'claimed', active };
126
121
  }
122
+
123
+ await ablo.weatherReports.claim('report_stockholm', async (report) => {
124
+ await ablo.weatherReports.update(report.id, { status: 'ready' });
125
+ });
127
126
  ```
128
127
 
129
- Reads never silently block. For raw resource calls, use `ifBusy`:
128
+ Reads never silently block. For schema model calls, use `claimState(id)` to observe
129
+ current work and `claim(id, work)` to serialize a write across a slow step:
130
130
 
131
- - `return` returns active intents.
132
- - `wait` waits for matching intents to clear.
133
- - `fail` throws `AbloBusyError`.
131
+ - default `claim` waits in the fair queue and re-reads before invoking `work`;
132
+ - `{ wait: false }` rejects with `AbloClaimedError` instead of queuing;
133
+ - `{ maxQueueDepth }` rejects if the wait line is already too deep.
134
134
 
135
- Schema clients use the realtime stream for waits. Schema-less HTTP clients must
136
- provide `busyPollInterval` when using `ifBusy: 'wait'`.
135
+ Schema clients use the realtime stream for waits.
137
136
 
138
137
  ## Errors
139
138
 
@@ -149,16 +148,16 @@ All SDK errors extend `AbloError` and carry a stable `type`.
149
148
  | `AbloValidationError` | Invalid input or unsupported request shape. |
150
149
  | `AbloServerError` | Server-side 5xx. Retry with backoff if the operation is idempotent. |
151
150
  | `AbloStaleContextError` | Write was based on stale `readAt` state. Re-read and retry. |
152
- | `AbloBusyError` | Active intent conflicted with `ifBusy: 'fail'` or a busy wait timed out. |
151
+ | `AbloClaimedError` | An active claim conflicted with `{ wait: false }`, the queue was too deep, or a claim wait timed out. |
153
152
 
154
153
  ```ts
155
- import { AbloBusyError } from '@abloatai/ablo';
154
+ import { AbloClaimedError } from '@abloatai/ablo';
156
155
 
157
156
  try {
158
- await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
157
+ await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
159
158
  } catch (error) {
160
- if (error instanceof AbloBusyError) {
161
- return { status: 'busy', intents: error.intents };
159
+ if (error instanceof AbloClaimedError) {
160
+ return { status: 'claimed' };
162
161
  }
163
162
  throw error;
164
163
  }