@abloatai/ablo 0.47.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/migration.md CHANGED
@@ -1,461 +1,64 @@
1
- # Version History & Migration Guide
1
+ # Upgrade Guide
2
2
 
3
- > Every breaking change and the edit it requires, newest first.
3
+ > A safe workflow for upgrading the pre-1.0 SDK.
4
4
 
5
- The breaking-changes-first companion to the [Changelog](../CHANGELOG.md). The
6
- changelog tells the story of each release; this page tells you exactly what to
7
- change when you upgrade.
5
+ Ablo is pre-1.0, so a minor release may contain a breaking API change. Patch
6
+ releases remain compatible within the same minor line.
8
7
 
9
- > Ablo is pre-1.0, so minor versions (`0.x.0`) may carry breaking changes. Patch
10
- > versions (`0.x.y`) never do. Pin a minor and read this page before bumping it.
8
+ ## Upgrade safely
11
9
 
12
- ## Breaking changes at a glance
10
+ 1. Pin the version you run instead of depending on a floating range.
11
+ 2. Read the changelog entries between your installed and target versions.
12
+ 3. Use the documentation bundled with the target package while changing code.
13
+ 4. Run type-checks and tests before updating a production branch.
14
+ 5. Push schema changes deliberately after reviewing the generated diff.
13
15
 
14
- | Version | What changed | What to do |
15
- |---|---|---|
16
- | **Next** | New keys no longer encode `live`/`test`; branch binding is authoritative | Accept the new `sk_…`/`rk_…`/`pk_…`/`ek_…` spellings. Do not route or authorize from a key prefix. Old `_live_`/`_test_` keys continue to authenticate; rotate them to mint the current form |
17
- | **0.36.0** | `ttlSeconds` deprecated on the join surfaces in favour of `ttl` | `useJoin({ scope, ttlSeconds: '5m' })` → `useJoin({ scope, ttl: '5m' })`; same for `ParticipantJoinOptions`. Both spellings work until 0.37.0 |
18
- | **0.35.0** | Synchronous reads moved under `local`, mirroring the async verbs | `get(id)` → `local.get(id)`; `getAll(options)` → `local.list(options)`; `getCount(options)` → `local.count(options)` |
19
- | **0.35.0** | `causedByTaskId` write option + seven `turn_*` error codes removed | Delete the `causedByTaskId` argument from writes; a branch on `turn_validation_failed` was unreachable and can go with it |
20
- | **0.34.0** | Presence verb renamed `watch` → `join` | `ablo.<model>.watch(ids)` → `ablo.<model>.join(ids)`; `useWatch` → `useJoin`; the `WatchOptions` / `UseWatchOptions` / `UseWatchReturn` types → `JoinOptions` / `UseJoinOptions` / `UseJoinReturn`; error code `model_watch_not_configured` → `model_join_not_configured` |
21
- | **0.28.0** | Removed React placeholders that had no working runtime | `usePresence` → `usePeers` or `useJoin`; `useClaim` → `ablo.<model>.claim`; `SyncGroupProvider` / `useSyncGroup` → `useJoin({ scope })` |
22
- | **0.11.0** | Historical `intent` → `claim` rename | The hook renamed in that release was later removed in 0.28.0. Current code uses `ablo.<model>.claim` or `useJoin` |
23
- | **0.10.0** | Environment enum renamed `test`/`live` → `sandbox`/`production` | Historical: update code that branches on the environment (`'test'`→`'sandbox'`, `'live'`→`'production'`). That release retained `sk_test_`/`sk_live_`; current releases mint mode-free, branch-bound keys |
24
- | **0.9.2** | `turn` primitive + agent-work `tasks` resource removed | Coordinate with `claim`; mint a scoped session instead of `agent().run()` |
25
- | **0.9.2** | `intents` deprecated in favor of `claim` | Use `ablo.<model>.claim`; `ablo.intents` is now `@internal` |
26
- | **0.9.0** | One options object per verb | `update(id, data, opts)` → `update({ id, data, ...opts })` |
27
- | **0.9.0** | `claim` returns a disposable handle | `await using claim = await ablo.x.claim({ id })` |
28
- | **0.8.0** | Flat coordination methods removed | `ablo.x.claimState(id)` → `ablo.x.claim.state({ id })` |
29
- | **0.7.0** | Legacy React hooks removed | `useQuery`/`useOne`/`useMutate`/`useReader` → `useAblo()` + `ablo.<model>.*` |
30
- | **0.6.0** | `subscribe` → `onChange`; `Resource` → `Model` rename | Rename listeners and `ablo.resource()` → `ablo.model()` |
31
- | **0.5.0** | Intent-handle method renames | `acquire`→`claim`, `acquireOrAwait`→`claimOrWait`, … |
32
- | **0.3.0** | `<SyncProvider>` / `createAbloContext()` / `withSync` removed | Use the umbrella `<AbloProvider>` |
33
-
34
- ---
35
-
36
- ## Next: key spelling follows capability, branch binding follows authority
37
-
38
- New runtime credentials have one prefix per capability class:
39
-
40
- ```diff
41
- - sk_live_… / sk_test_…
42
- + sk_…
43
- ```
44
-
45
- The same applies to `rk_`, `pk_`, and `ek_`. This does **not** merge branch
46
- data or weaken production safety. Every key row is still immutably bound to one
47
- project and one branch, and the server-confirmed `branchRoot` flag distinguishes
48
- the protected production root from a development child.
49
-
50
- Remove code that chooses an endpoint, environment, or confirmation policy by
51
- matching `_live_` or `_test_`. Use the resolved identity (`ablo whoami` in the
52
- CLI) instead. Existing suffixed credentials remain valid compatibility inputs,
53
- so rollout does not require an immediate rotation.
54
-
55
- ---
56
-
57
- ## 0.36.0: one lease is spelled `ttl`
58
-
59
- ```diff
60
- - useJoin({ scope: { documents: [id] }, ttlSeconds: '5m' })
61
- + useJoin({ scope: { documents: [id] }, ttl: '5m' })
62
- ```
63
-
64
- `ablo.<model>.join(ids, { ttl })` has always said `ttl`, and so does every other
65
- lease in the SDK: `claim`'s `ttl`, `ClaimLeaseOptions.ttl`. The lower-level join
66
- surfaces said `ttlSeconds` while accepting exactly the same values, including
67
- duration strings, so a `ttl: '5m'` handed down from the model verb arrived as
68
- `ttlSeconds: '5m'`, a field asserting a unit its value did not carry.
69
-
70
- Both spellings work until 0.37.0, and `ttl` wins if you pass both. The wire is
71
- unchanged: it has always carried seconds and still does.
72
-
73
- ---
74
-
75
- ## 0.35.0: the synchronous reads move under `local`
76
-
77
- ```diff
78
- - const task = ablo.tasks.get(id);
79
- - const open = ablo.tasks.getAll({ where: { status: 'open' } });
80
- - const count = ablo.tasks.getCount({ where: { status: 'open' } });
81
- + const task = ablo.tasks.local.get(id);
82
- + const open = ablo.tasks.local.list({ where: { status: 'open' } });
83
- + const count = ablo.tasks.local.count({ where: { status: 'open' } });
84
- ```
85
-
86
- Options, return types, and reactivity inside `useAblo` selectors are unchanged.
87
- Every verb now matches its asynchronous sibling, and `local` narrows the read to
88
- what has already synced — which is what lets it return a value rather than a
89
- promise.
90
-
91
- `getAll` and `getCount` are distinctive enough to rename by search. `get` is not:
92
- in most codebases it is outnumbered many times over by `Map.get` and
93
- `headers.get`, and no search separates them. Upgrade the package first and let
94
- the compiler name the sites — each one is a type error at exactly the call that
95
- has to move.
96
-
97
- ## 0.35.0: `causedByTaskId` and the `turn_*` error codes removed
98
-
99
- 0.9.2 retired the `turn` primitive but left one field standing: `causedByTaskId`
100
- on the write options bag. It was never usable. The server validated it against a
101
- task record that nothing in the system has ever created, so supplying it had the
102
- whole batch rejected with `turn_validation_failed`, while leaving it null passed
103
- straight through. The safe way to use the option was to not use it.
104
-
105
- **Removed:** `MutationOptions.causedByTaskId`, the seven `turn_*` error codes
106
- (`turn_validation_failed`, `turn_open_failed`, `turn_close_failed`,
107
- `turn_not_found`, `turn_foreign_agent`, `parent_turn_not_found`,
108
- `parent_turn_foreign_agent`), and the stored row's provenance slice
109
- (`deltaProvenanceSchema` and the `DeltaProvenance` type). `syncDeltaRowSchema` is
110
- now the core and attribution slices composed.
111
-
112
- ```diff
113
- - await ablo.documents.update({ id, data, causedByTaskId: turnId });
114
- + await ablo.documents.update({ id, data });
115
- ```
116
-
117
- Attribution is unaffected. A delta still records the actor, the `onBehalfOf`
118
- principal behind a delegated write, the capability that authorized it, and the
119
- claim it was made under — which is what answers "who did this, and by what
120
- right." On the wire the field was optional and nullable, so a client that still
121
- sends it is accepted and ignored.
122
-
123
- The `caused_by_task_id` column stays, for the reason 0.9.2 gave when it kept it:
124
- the audit hash-chain signs its value into every row. Dropping it is a versioned
125
- migration of its own.
126
-
127
- ---
128
-
129
- ## 0.34.0: presence verb renamed `watch` → `join`
130
-
131
- The model-level presence verb read like a data subscription but delivered
132
- presence — who else is on a row and what they hold — so it now says what it
133
- does. `ablo.<model>.join(ids, { ttl })` opens the participant handle
134
- (`.peers`, `.claims`, `await using` disposal); the returned `status` was
135
- already `'joined'`, and the layer beneath always called itself `join`, so the
136
- verb now matches. `onChange` remains the way to hear row *values* change, and
137
- `track` remains the durable premise for actors.
138
-
139
- ```ts
140
- // before
141
- await using room = await ablo.documents.watch(documentIds, { ttl: '5m' });
142
- // after
143
- await using room = await ablo.documents.join(documentIds, { ttl: '5m' });
144
- ```
145
-
146
- The React hook follows: `useWatch({ scope })` → `useJoin({ scope })`. There is
147
- no compatibility alias — rename the call sites and the `WatchOptions` /
148
- `UseWatchOptions` / `UseWatchReturn` type imports.
149
-
150
- ---
151
-
152
- ## 0.28.0: dead React multiplayer placeholders removed
153
-
154
- Four React exports looked usable but had no live implementation:
155
-
156
- - `usePresence` returned no provider-backed presence value. Use `usePeers` for
157
- read-only presence or `useJoin` to join a scoped participant.
158
- - `useClaim` depended on a callback the provider never supplied and always
159
- threw. Use `ablo.<model>.claim({ id, ... })` for row claims or
160
- `useJoin({ scope, claim: true })` for a scoped participant claim.
161
- - `SyncGroupProvider` and `useSyncGroup` had no repository consumers. Pass the
162
- scope directly to `useJoin({ scope })`.
163
-
164
- There is no compatibility alias: the replacement APIs were already the only
165
- working paths.
166
-
167
- ## 0.11.0: `intent` → `claim` rename completed
168
-
169
- > **Historical note:** this section documents the 0.11.0 transition.
170
- > `useClaim` was subsequently removed in 0.28.0 because its provider callback
171
- > was never wired. Do not migrate new code to it; use the replacements above.
172
-
173
- The coordination primitive has been `claim` since 0.9.2, but a few `intent`-named
174
- surfaces lingered. 0.11.0 finishes the rename. There are three edits, all
175
- mechanical:
176
-
177
- **1. React hook.** `useIntent` is now `useClaim` (same signature):
178
-
179
- ```diff
180
- - import { useIntent } from '@abloatai/ablo/react';
181
- - const claimEditLayer = useIntent('editLayer');
182
- + import { useClaim } from '@abloatai/ablo/react';
183
- + const claimEditLayer = useClaim('editLayer');
16
+ ```bash
17
+ npm install @abloatai/ablo@0.48
18
+ npx ablo docs
19
+ npx ablo docs api
20
+ npx ablo check
184
21
  ```
185
22
 
186
- **2. Type registration.** The `Register` interface key is `Claims`, not `Intents`:
23
+ `npx ablo docs` is version-matched to the installed package. Prefer it during an
24
+ upgrade: the hosted website documents the newest release, which may expose a
25
+ method your pinned package does not yet contain.
187
26
 
188
- ```diff
189
- declare module '@abloatai/ablo' {
190
- interface Register {
191
- - Intents: { editLayer: { slideId: string; layerId: string } };
192
- + Claims: { editLayer: { slideId: string; layerId: string } };
193
- }
194
- }
195
- ```
196
-
197
- **3. Type namespace.** The `Ablo.Intent.*` helper types moved to `Ablo.Claim.*`.
198
- If you referenced them directly, rename the namespace; the shapes are unchanged.
199
-
200
- > **Coordinated deploy required.** The on-the-wire frames moved from `intent_*`
201
- > to `claim_*`. A `claim_*`-aware client cannot coordinate with an `intent_*`
202
- > server (and vice-versa), so ship the client and server together. If you run a
203
- > self-managed sync server, deploy it first.
204
-
205
- Two non-breaking improvements ride along: claim-rejection errors now surface the
206
- contending holders (`AbloClaimedError.claims` and a policy reason folded into the
207
- message), and `participantKind` is the canonical `'user' | 'agent' | 'system'`
208
- on presence and claim state.
209
-
210
- ## 0.10.0: environment enum `sandbox` / `production`; stateless HTTP transport
211
-
212
- > Historical migration note: current development uses immutable branches, not a
213
- > shared sandbox selector. The enum and compatibility fields below remain
214
- > relevant to integrations written against 0.10, but new applications let the
215
- > branch-bound credential select the plane.
216
-
217
- ### Environment enum rename (the only breaking change)
27
+ ## What to review
218
28
 
219
- In 0.10 the canonical environment values became **`production`** and
220
- **`sandbox`** (from `live` and `test`). At that release this was only a
221
- vocabulary change at the type/API layer: on-the-wire keys still used
222
- `sk_test_…` / `sk_live_…`. Current releases accept those legacy spellings but
223
- mint mode-free `sk_…` keys whose persisted branch binding is authoritative.
29
+ Pay particular attention when a release changes:
224
30
 
225
- You only need to act if your code branches on the environment value — most
226
- commonly a Data Source handler keyed on `mode`. The mapping is exactly
227
- `test sandbox`, `live production`:
31
+ - model method signatures or return values;
32
+ - claim acquisition, contention, or release behavior;
33
+ - credential scope or session minting;
34
+ - schema serialization and push validation;
35
+ - database connection or Data Source setup;
36
+ - error codes your application handles explicitly.
228
37
 
229
- ```diff
230
- const handler = createSourceHandler({
231
- read: async ({ mode }) => {
232
- - const db = mode === 'test' ? testDb : liveDb;
233
- + const db = mode === 'sandbox' ? sandboxDb : productionDb;
234
- // …
235
- },
236
- });
237
- ```
238
-
239
- `commit` now also forwards `projectId`, `accountScope`, and `environment` to
240
- source resolvers, so per-project and per-environment traffic can be routed to
241
- distinct stores.
242
-
243
- > **CLI note:** current login stores one mode-free `mk_` project management
244
- > credential. Legacy runtime key slots remain readable during the cutover, but
245
- > re-run `ablo login` before using branch/project commands so the CLI can store
246
- > the new credential.
247
-
248
- ### New (non-breaking): `transport: 'http'`
249
-
250
- `Ablo({ transport: 'http' })` returns a stateless `AbloHttpClient` for
251
- server-side actors (agents, workers, serverless): the same `ablo.<model>` surface
252
- and `claim` coordination, but each call is one HTTP round-trip with identity on
253
- the Bearer credential — no websocket, no local synced pool. The return type
254
- narrows, so stateful-only APIs (the `local` reads, `onChange`) become compile
255
- errors instead of latent runtime gaps. Existing code keeps the default
256
- `'websocket'` transport, unchanged.
257
-
258
- ```ts
259
- const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http' });
260
- await ablo.tasks.update({ id, data: { status: 'done' } });
261
- ```
38
+ The release changelog names required edits next to the feature that changed.
39
+ Avoid branching on credential prefixes or undocumented internals; use exported
40
+ types and server-confirmed identity instead.
262
41
 
263
- > **Minting still needs the stateful client.** `sessions.create(...)` is not on
264
- > the `transport: 'http'` surface. Keep a default-transport `server` client for
265
- > minting short-lived credentials (see the 0.9.2 example below), and use the
266
- > http client for the per-request reads and writes.
267
-
268
- ---
269
-
270
- ## 0.9.2: `turn` / agent-`tasks` removed; `intents` deprecated
271
-
272
- The SDK's coordination surface is now exactly two things: `ablo.<model>` writes
273
- and `claim`. The parallel `turn` / agent-`tasks` mechanism was redundant —
274
- `claim` already serializes writers **and** carries the causal link (its `intent`
275
- id rides on every guarded write), and the server stamps `actor` / `onBehalfOf` /
276
- `capabilityId` onto every delta from the auth context.
277
-
278
- **Removed:** `engine.beginTurn()`, the `Turn` handle and `Ablo.Turn` type,
279
- `AbloApi.beginTurn`, `CommitCreateOptions.causedByTaskId`, the `agent().run()`
280
- helper, and the agent/task type family (`Agent`, `AgentOptions`,
281
- `AgentRunResult`, `Task`, `TaskResource`, …).
282
-
283
- > **Note:** `ablo.tasks` is — and always was — the schema `tasks` **model**
284
- > proxy. Only the agent-work *resource* of the same name was removed. If you have
285
- > a `tasks` model in your schema, it is unaffected.
286
-
287
- ```diff
288
- - const turn = await engine.beginTurn();
289
- - await Ablo({ apiKey }).agent(agentId, opts).run(prompt, handler);
290
- + // Mint a scoped credential from a stateful (default-transport) server client —
291
- + // sessions.create lives on the stateful client, not on transport: 'http'.
292
- + const server = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
293
- + const { token } = await server.sessions.create({ agent: { id: agentId } });
294
- + const agent = Ablo({ schema, apiKey: token });
295
- + await using claim = await agent.tasks.claim({ id });
296
- + await agent.tasks.update({ id, data: { status: 'done' } });
297
- ```
298
-
299
- Per-run token/cost now lives in Langfuse, not an `agent_tasks` table. The only
300
- capability the client loses is the audit pane's "show everything this exact
301
- prompt produced" filter (it keyed off `caused_by_task_id`); new writes leave that
302
- column `null`. The server-side `agent_tasks` table, the `caused_by_task_id` delta
303
- column, and the `agent_actions_log` hash-chain are intentionally **kept but
304
- dormant** — they are load-bearing for the tamper-evident audit chain. The dead
305
- `/v1/tasks` and `/api/agent/turn` route handlers were removed.
306
-
307
- ### `intents` → `claim`
308
-
309
- ```diff
310
- - const lock = ablo.intents.editing(target);
311
- + await using claim = await ablo.documents.claim({ id });
312
- ```
313
-
314
- `ablo.intents` still exists but is marked `@internal`. Use `ablo.<model>.claim`
315
- everywhere you coordinate concurrent work.
316
-
317
- ---
318
-
319
- ## 0.9.0: one options object per verb; disposable `claim`
320
-
321
- Every model verb takes a single options object, so the id, the data, and every
322
- modifier are named siblings. Reactive local reads stay on the synchronous
323
- `get(id)`.
324
-
325
- ```diff
326
- - await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
327
- + await ablo.tasks.update({ id, data: { status: 'done' } })
328
-
329
- - await ablo.tasks.retrieve(id)
330
- + await ablo.tasks.get({ id })
331
-
332
- - useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
333
- + useAblo((ablo) => ablo.tasks.local.get(id)) ?? serverTask
334
- ```
335
-
336
- `claim` now returns a disposable handle instead of taking a callback. The handle
337
- exposes the fresh row on `.data` and releases on scope exit.
338
-
339
- ```diff
340
- - await ablo.tasks.claim(id, async (task) => {
341
- - await ablo.tasks.update(task.id, { status: 'in_review' })
342
- - })
343
- + await using claim = await ablo.tasks.claim({ id })
344
- + const task = claim.data
345
- + await ablo.tasks.update({ id: task.id, data: { status: 'in_review' } })
346
- ```
347
-
348
- `claim.state`, `claim.queue`, `claim.release`, and `claim.reorder` also take the
349
- options object.
350
-
351
- ---
352
-
353
- ## 0.8.0: callable `claim` namespace
354
-
355
- The flat coordination methods are gone; everything lives under `claim`.
356
-
357
- ```diff
358
- - await ablo.task.claimState(id)
359
- - await ablo.task.release(id)
360
- + await ablo.task.claim.state(id)
361
- + await ablo.task.claim.release(id)
362
- ```
363
-
364
- ---
365
-
366
- ## 0.7.0: legacy React hooks removed
367
-
368
- The query/mutation hooks were replaced by the single `useAblo()` accessor over
369
- typed model methods.
370
-
371
- ```diff
372
- - const { data } = useQuery('task', { where: { done: false } })
373
- + const ablo = useAblo()
374
- + const tasks = ablo.task.list({ where: { done: false } })
375
- ```
376
-
377
- Removed: `useQuery`, `useOne`, `useMutate`, `useReader`. The `MutateActions`,
378
- `ReaderActions`, and `ReaderFindOptions` types are still exported for callers
379
- that reference them. This release also replaced the `{ error, reason }` error
380
- shape with the canonical `{ type, code, message, doc_url, request_id }` envelope.
381
-
382
- > **Note:** This — not 0.9.x — is the release where `useAblo()` became the one
383
- > React read path. If you are coming from a 0.6.x or earlier app, this is your
384
- > biggest hook migration.
385
-
386
- ---
387
-
388
- ## 0.6.0: `onChange` and the Resource → Model rename
389
-
390
- ```diff
391
- - ablo.tasks.subscribe(cb)
392
- + ablo.tasks.onChange(cb)
393
-
394
- - ablo.resource('tasks')
395
- + ablo.model('tasks')
396
- ```
397
-
398
- Also renamed: `Ablo.Resource.*` → `Ablo.Model.*`, `ModelTarget.resource` →
399
- `ModelTarget.model`, and error code `resource_not_found` → `model_not_found`.
400
- (`subscribe` is reserved for an upcoming scope-grant verb.)
401
-
402
- ---
403
-
404
- ## 0.5.0: intent-handle method renames
405
-
406
- On the model intent handle (`ablo.<model>.intent(id)`):
407
-
408
- ```diff
409
- - handle.acquire() + handle.claim()
410
- - handle.acquireOrAwait() + handle.claimOrWait()
411
- - handle.settled() + handle.whenFree()
412
- - handle.release() + handle.finish()
413
- - handle.revoke() + handle.cancel()
414
- ```
415
-
416
- The lower-level `IntentHandle` / `IntentLeaseHandle` (`ablo.intents.*`) were
417
- unchanged at this release. (They were later folded under `claim` in 0.9.2.)
418
-
419
- ---
420
-
421
- ## 0.3.0: umbrella `<AbloProvider>`
422
-
423
- One provider component now owns the full React lifecycle. `<SyncProvider>`,
424
- `createAbloContext()`, and `withSync` were removed.
425
-
426
- ```diff
427
- - const { AbloProvider, useAblo } = createAbloContext<typeof schema>();
428
- - <SyncProvider store={sync._store} organizationId={orgId}>
429
- - <AbloProvider ablo={ablo}>{children}</AbloProvider>
430
- - </SyncProvider>
431
- + const ablo = Ablo({ schema, apiKey });
432
- + <AbloProvider client={ablo}>
433
- + {children}
434
- + </AbloProvider>
435
- ```
436
-
437
- `useSyncStatus()` changed from six booleans to a tagged union:
438
-
439
- ```diff
440
- - const { isReady } = useSyncStatus()
441
- + const isReady = useSyncStatus().name === 'connected'
442
- ```
42
+ ## Schema and database safety
443
43
 
444
- Import `observer` from `mobx-react-lite` directly if you used `withSync`.
44
+ An SDK upgrade and a database migration are separate operations.
445
45
 
446
- ---
46
+ - `ablo push` updates Ablo's typed schema contract. It does not run application
47
+ DDL or drop your tables.
48
+ - `ablo check` compares the contract with the connected database without
49
+ changing it.
50
+ - Your ORM or migration tool remains responsible for tables, columns,
51
+ constraints, and application data migrations.
447
52
 
448
- ## The `intent` `claim` evolution
53
+ When both need to change, deploy the database migration in a backwards-compatible
54
+ form first, push the compatible Ablo schema, then remove old application paths.
449
55
 
450
- Coordination has converged on one verb over several releases. If you are reading
451
- old code or old docs, this is the through-line:
56
+ ## If an upgrade fails
452
57
 
453
- | Release | State of coordination |
454
- |---|---|
455
- | 0.4.0 | `ablo.<model>.intent(id)` introduced: per-entity intent handle |
456
- | 0.5.0 | Intent-handle methods renamed to claim vocabulary (`acquire`→`claim`, …) |
457
- | 0.8.0 | Callable `claim` namespace (`claim(id)`, `claim.state`, `claim.queue`, …) |
458
- | 0.9.0 | `claim` returns an `await using` disposable handle |
459
- | 0.9.2 | `intents` deprecated and made `@internal`: **`claim` is the one coordination API** |
58
+ Use the typed error code and request ID rather than matching message text. The
59
+ [Errors](./errors.md) reference gives the recovery step for each public error.
60
+ If a failure only occurs on the new minor version, keep the previous pinned
61
+ version in production while reproducing it against an isolated branch.
460
62
 
461
- For the full chronological history, see the [Changelog](../CHANGELOG.md).
63
+ See the release changelog for version-specific changes and
64
+ [Deployment](./deployment.md) for the production rollout sequence.
@@ -1,14 +1,12 @@
1
1
  # Operating on Your Database
2
2
 
3
- > Which actions run freely, which to verify first, and which belong to a human.
3
+ > Which actions are read-only, which writes need a guard, and which changes belong to a human.
4
4
 
5
5
  Ablo sits over your database as a coordination layer, not an owner. It reads
6
- your Postgres replication stream and routes each write through a claim-checked
7
- commit that lands in your own tables. It never runs DDL, never migrates, never
8
- drops. That single boundary is why almost everything you do through Ablo is
9
- either read-only or reversible — and why the few actions that aren't are easy to
10
- name. This page is how to tell them apart, so you can work on a real database
11
- without guessing which move is the dangerous one.
6
+ your Postgres replication stream and routes model writes into your own tables.
7
+ The hosted service does not run application DDL, migrations, or drops. That
8
+ boundary keeps model operations attributable while your migration tool remains
9
+ responsible for the database shape.
12
10
 
13
11
  The habit that makes it easy is to look before you act. One command shows you
14
12
  the real shape of your database measured against your schema, and changes
@@ -32,26 +30,27 @@ radius of anything you do through the model API:
32
30
  action, run with your admin credential — never Ablo's.
33
31
  - It **never owns your rows.** Canonical data stays in your tables; Ablo hosts
34
32
  only the transaction log and the coordination state.
35
- - Every model write is **claim-checked and recorded.** A write based on a row
36
- that moved under you is rejected rather than applied, and the prior value is
37
- retained in the log, so a confirmed change is attributable and
38
- reconstructable.
33
+ - Every model write is **checked against active claims and recorded.** A plain
34
+ write is last-write-wins when no claim applies. A functional update, held
35
+ claim, or explicit `readAt` guard protects a write that depends on an earlier
36
+ value.
39
37
 
40
- So a normal `ablo.<model>.update(...)` cannot silently corrupt your database:
41
- the worst case is a clean rejection and a re-read, not a lost row.
38
+ Ablo prevents a non-holder from writing through another participant's claim by
39
+ default. It does not infer that every plain update is a read-modify-write
40
+ operation; use the guarded forms when lost-update protection matters.
42
41
 
43
42
  ## Three kinds of action
44
43
 
45
44
  Sort any action you're about to take into one of these, and the right move
46
45
  follows.
47
46
 
48
- **Run freely — read-only or reversible.**
47
+ **Normal application operations.**
49
48
  Reads (`get`, `list`), `ablo check`, and `ablo pull` observe and
50
49
  never change anything. Previews — `--show-sql`, `--dry-run` — print the exact
51
50
  SQL a command would run without executing it. Model writes through
52
- `ablo.<model>.create` / `update` are claim-checked, optimistic, rolled back if
53
- the server rejects them, and recorded in the log with the prior value. All of
54
- these are safe to run on your own initiative.
51
+ `ablo.<model>.create` / `update` are authorized, checked against active claims,
52
+ optimistic locally, rolled back locally if the server rejects them, and
53
+ recorded. Review their data effects as you would any application write.
55
54
 
56
55
  **Verify first — needs one look at the live database.**
57
56
  Routing an existing table's writes through a model requires the model to match
@@ -61,28 +60,23 @@ line in the report rather than a surprise at commit time. Decide the model shape
61
60
  from what `check` tells you, then proceed. Nothing here is risky — it just reads
62
61
  better after you've seen the ground truth.
63
62
 
64
- **Hand to a human — irreversible, outside the log's protection.**
63
+ **Hand to a human — database administration.**
65
64
  Raw DDL on the live database — `ALTER TABLE … OWNER TO`, adding or dropping a
66
- column, changing a constraint — changes the database itself and is not covered
67
- by the reversible log, so it belongs to a person with their hand on it. So does
65
+ column, changing a constraint — changes the database itself, so it belongs to a
66
+ person with their hand on it. So does
68
67
  a `connect` cutover run with its confirmation skipped (`--yes`): the prompt
69
68
  exists because the step provisions real roles and reconciles publication on a
70
69
  live database, and on a shared or production database that confirmation is the
71
- human's to give. Removing a model from your schema belongs here too — for the
72
- reason below.
70
+ human's to give. Removing a model from your pushed schema also deserves review
71
+ because clients will lose that typed API surface.
73
72
 
74
73
  ## The one action that isn't what it looks like
75
74
 
76
- Deleting a model from `ablo/schema.ts` reads like a code cleanup, but it is a
77
- schema change. Your schema is a desired-state declaration: `ablo push` diffs it
78
- against the server's copy, and a model that has vanished from the schema can be
79
- read as a table that should no longer exist. "Nothing imports it" answers a
80
- code question. The question that governs safety is *does removing this drop a
81
- real table on the next push* — and that one is answered against the database,
82
- not the codebase. Before removing a model that maps a live table, confirm the
83
- table is gone or empty and that your push path is additive; otherwise keep the
84
- model until the data is dealt with. A `load: 'lazy'` mapping is often present
85
- precisely to hold a table in place, so treat its comment as load-bearing.
75
+ Deleting a model from `ablo/schema.ts` removes it from the API contract the next
76
+ time you push the schema. Existing clients can no longer access that model
77
+ through Ablo. It does **not** drop the underlying table or its rows; database DDL
78
+ remains your migration tool's responsibility. Review client usage first, then
79
+ remove the model and push the schema as a normal application change.
86
80
 
87
81
  ## The verification loop
88
82
 
@@ -98,10 +92,9 @@ read-only checks:
98
92
  - Read the row and its claim state before you write — `get` / `list`, and
99
93
  `ablo.<model>.claim.state({ id })` for who is already working on it.
100
94
 
101
- The pattern underneath all of it is steady: reads and model writes flow freely
102
- because the boundary and the log make them safe, DDL and cutovers pause for a
103
- human because they change the database itself, and the space between the two is
104
- one `ablo check` away from certain.
95
+ The pattern underneath all of it is steady: observe first, use guarded writes
96
+ when a change depends on prior state, and review DDL and cutovers separately
97
+ because they change the database itself.
105
98
 
106
99
  ## See also
107
100
 
@@ -26,6 +26,28 @@ Then:
26
26
  - write with `ablo.weatherReports.update`
27
27
  - dispose the client when the worker finishes
28
28
 
29
+ For read-reason-write work, pass the exact returned rows that informed the
30
+ decision. Their watermarks stay opaque:
31
+
32
+ ```ts
33
+ const task = await ablo.tasks.get({ id: taskId });
34
+ const policy = await ablo.policies.get({ id: policyId });
35
+ const result = await model({ task, policy });
36
+ await ablo.tasks.update({
37
+ id: task.id,
38
+ data: result,
39
+ reads: [task, policy],
40
+ });
41
+ ```
42
+
43
+ This means: apply the update only if the rows used to produce it have not
44
+ changed. Incidental reads do nothing, and cloned or fabricated rows are
45
+ rejected locally.
46
+
47
+ `agent-turn.ts` is the cheap read/write path. `expensive-agent-turn.ts` adds a
48
+ heartbeating claim, post-grant model input, durable commit inspection, automatic
49
+ release, and a released-claim fencing check.
50
+
29
51
  Import the same schema in every runtime. Use `commits.create` only when several
30
52
  typed row operations must land atomically; ordinary writes stay on
31
53
  `ablo.<model>.create/update/delete`.
@@ -39,6 +61,8 @@ root and a bare `quickstart.ts` won't be found.
39
61
  ```bash
40
62
  cd packages/ablo
41
63
  ABLO_API_KEY=sk_... npx tsx examples/quickstart.ts
64
+ ABLO_API_KEY=sk_... TASK_ID=task_... npx tsx examples/agent-turn.ts
65
+ ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/expensive-agent-turn.ts
42
66
  ```
43
67
 
44
68
  ## Data Source (customer-owned database)