@abloatai/ablo 0.58.0 → 0.59.1

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/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. This file shows a coding assistant the one safe pattern: read a row, claim it, then write.
3
+ Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. For an existing application, first preserve its named operation and persistence boundary; use the schema-backed row pattern below only when that route owns the write.
4
4
 
5
5
  Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
6
6
 
@@ -8,7 +8,7 @@ Claims don't lock. If another writer holds the row, `claim` waits for them and r
8
8
 
9
9
  Before choosing among identifier claims, row claims, captured reads, atomic
10
10
  commits, existing database writes, and Ablo-routed writes, use the
11
- [Agent Integration Decision Guide](./docs/agent-integration-decision-guide.md).
11
+ [coordinate existing work guide](./docs/coordinate-existing-work.md).
12
12
  It routes existing applications to the smallest relevant example and names the
13
13
  test layer that proves each guarantee.
14
14
 
@@ -44,7 +44,7 @@ Every model verb takes ONE options object. The common loop:
44
44
  1. **Get or read** the row — `get({ id })` observes; `read({ id })` declares that a later mutation depends on this exact version. `list({ where })` is observational. In React render, use `local.get(id)`.
45
45
  2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
46
46
  3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, description?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
47
- 4. **Write** — pass `reads: [row]` when the decision used a row returned by `read`, or write through the held claim. If the declared read moved, the mutation does not land.
47
+ 4. **Write** — pass `reads: [row]` when the decision used a row returned by `read`, or pass `claim` when writing through a held claim. If the declared read moved or the claim was lost, the mutation does not land.
48
48
 
49
49
  Keep coding assistants on this schema-backed path.
50
50
 
@@ -80,6 +80,7 @@ const claimed = claim.data;
80
80
  await ablo.weatherReports.update({
81
81
  id: claimed.id,
82
82
  data: { status: 'ready', forecast: await getForecast(claimed.location) },
83
+ claim,
83
84
  });
84
85
  ```
85
86
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.59.1
4
+
5
+ ### Plans recognize completed database migrations
6
+
7
+ `ablo plan` now treats a connected PostgreSQL column as evidence that required
8
+ field migration work is complete when the mapped column exists, has the expected
9
+ type, and already enforces `NOT NULL`. The plan reports that verified work once
10
+ instead of demanding a synthetic backfill merely because the active Ablo
11
+ artifact predates the database migration.
12
+
13
+ The same reconciliation now recognizes a risky field-type correction as already
14
+ completed when PostgreSQL has the candidate type, so activation does not demand
15
+ `--force` merely because the active artifact still describes the old type. That
16
+ evidence remains a forward-only contract boundary: reactivating the old artifact
17
+ would disagree with the migrated database. Enum narrowing is not inferred from
18
+ a `TEXT` column because the observed shape does not prove existing values satisfy
19
+ the new constraint.
20
+
21
+ Removing a model from the served schema also no longer implies that Ablo will
22
+ drop its table when the connected database is application-owned and the table is
23
+ still present. The plan reports a compatibility warning, retains the physical
24
+ table, and allows the reviewed metadata activation without `--force`. Ablo-owned
25
+ tables, missing application tables, and unobserved database states remain
26
+ destructive errors.
27
+
28
+ Missing, nullable, incompatible, contradictory, and unobserved database states
29
+ remain blocked. Duplicate physical findings shared by the candidate and active
30
+ schemas are collapsed, while genuine three-state disagreements remain visible
31
+ in both directions.
32
+
33
+ ### Batched writes preserve their exact-read evidence
34
+
35
+ Reactive writes that carry captured rows through `reads` now retain those
36
+ dependencies when several writes are sealed into one durable commit, including
37
+ after an offline replay. A premise that became stale is therefore still rejected
38
+ instead of being lost at the batch boundary.
39
+
40
+ ## 0.59.0
41
+
42
+ ### Schema changes now have one ordered deployment plan
43
+
44
+ `ablo plan` compares the source schema, the active Ablo schema artifact, and the
45
+ connected PostgreSQL shape without changing any of them. It produces one
46
+ fingerprinted expand, dual-write, backfill, verify, switch, and contract
47
+ sequence, with explicit owners, blockers, and a rollback target:
48
+
49
+ ```sh
50
+ npx ablo plan
51
+ npx ablo plan --json
52
+ ```
53
+
54
+ `ablo check` is now the database-compatibility view of that same plan. `ablo
55
+ push` and `ablo migrate` consume it instead of maintaining separate migration
56
+ judgments, and `ablo rollback` plans or applies a reviewed reactivation of an
57
+ earlier schema artifact. The shared deployment contracts are available through
58
+ `@abloatai/ablo/schema` and `@abloatai/transaction/schema`.
59
+
60
+ Planning must observe all three states. `ablo plan`, `ablo check`, and `ablo
61
+ migrate` therefore require `ABLO_API_KEY` plus
62
+ `DATABASE_ADMIN_URL`/`DATABASE_URL`; a migration dry run is no longer a
63
+ source-only operation. `ablo push` now refuses a blocked plan and can accept an
64
+ explicit lifecycle manifest with `--manifest <path>`.
65
+
66
+ Runtime schema-drift warnings now name the affected fields and distinguish
67
+ client-only fields, active-only fields, and changes to type or optionality.
68
+
69
+ ### Declared timestamps no longer recurse during local edits
70
+
71
+ Schemas can declare `createdAt` and `updatedAt` for typed reads and ordering
72
+ without turning Ablo's automatic timestamp bookkeeping into another model
73
+ edit. Updating an observable field now advances `updatedAt` once, keeps the
74
+ timestamp observable, and excludes system-managed timestamps from the
75
+ user-authored change payload.
76
+
77
+ ### The public client configuration boundary is explicit
78
+
79
+ The supported `Ablo({ ... })` options are now machine-checked against the
80
+ published reference. The internal `onCommitReceipt` transport callback is no
81
+ longer accepted by the public factory type.
82
+
83
+ ### Connection capacity is selectable from the public pricing model
84
+
85
+ The pricing API can now select the first tier that accommodates a requested
86
+ connection count. The published Pro allowance increases from 1,000 to 5,000
87
+ concurrent connections.
88
+
89
+ ### CLI telemetry can reach authenticated ingestion
90
+
91
+ When an Ablo runtime key is available, the CLI uses it only in memory to
92
+ authenticate product-analytics delivery. The key is not written to the local
93
+ telemetry state, and existing telemetry opt-outs continue to apply.
94
+
95
+ ### Version-matched integration guidance is easier to enter
96
+
97
+ The documentation bundled with `@abloatai/ablo` now starts from installation,
98
+ the operation being coordinated, and whether an existing write boundary must
99
+ be preserved. New focused pages cover basic usage, implementation choices,
100
+ existing-operation coordination, every client option, security,
101
+ instrumentation, comparisons, common questions, and GraphQL.js. `npx ablo
102
+ docs` continues to read this package-local documentation, so the guidance
103
+ matches the installed version.
104
+
105
+ A new stale-context agent-turn example shows the complete long-running policy:
106
+ abort cancellable work when an exact read moves, rebuild context for a bounded
107
+ retry, retain the authoritative write guard, and reconcile rather than blindly
108
+ replay an irreversible external side effect.
109
+
3
110
  ## 0.58.0
4
111
 
5
112
  ### Reads now distinguish observation from decision input
package/README.md CHANGED
@@ -3,12 +3,12 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>Collaboration infrastructure for AI agents.</strong>
6
+ <strong>Coordination infrastructure for agents, applications, services, and people working on shared state.</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://docs.abloatai.com">Docs</a> &nbsp;|&nbsp;
11
- <a href="https://docs.abloatai.com/quickstart">Quickstart</a> &nbsp;|&nbsp;
11
+ <a href="https://docs.abloatai.com/installation">Installation</a> &nbsp;|&nbsp;
12
12
  <a href="https://docs.abloatai.com/api">API</a> &nbsp;|&nbsp;
13
13
  <a href="https://github.com/Abloatai/ablo">GitHub</a>
14
14
  </p>
@@ -22,8 +22,8 @@
22
22
 
23
23
  ---
24
24
 
25
- Ablo is collaboration infrastructure for AI agents: one API for agents, apps,
26
- and services to claim, change, and confirm the same rows.
25
+ Ablo is coordination infrastructure for agents, applications, services, and
26
+ people working on shared state.
27
27
 
28
28
  Every write goes through it, so authority, idempotency, conflicts, ordering,
29
29
  and confirmation are enforced in one place. Your Postgres remains the source of
package/docs/agents.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  > The stateless participant: wake on a trigger, read, claim, commit, go idle.
4
4
 
5
+ ## Stateless HTTP reads
6
+
7
+ Use `get` to read one task row by id and `list` to find matching rows. Agents
8
+ and other stateless workers use the HTTP client directly, without a
9
+ synchronization step or a `.data` wrapper.
10
+
11
+ ```ts
12
+ const task = await ablo.tasks.get({ id: taskId });
13
+ if (!task) throw new Error('task not found');
14
+ console.log(task.title);
15
+
16
+ const matching = await ablo.tasks.list({ where: { title } });
17
+ if (!matching[0]) throw new Error('task not found');
18
+ console.log(matching[0].title);
19
+ ```
20
+
21
+ These are observational reads. Use `read({ id })` only when a later Ablo write
22
+ depends on that exact version and will pass it through `reads`.
23
+
5
24
  An agent is a **reactive** participant: it wakes on something happening, reads
6
25
  what it needs, writes a result, and goes idle. That's a request/response
7
26
  workload — so agents talk to Ablo over **plain HTTP**, holding no WebSocket. The
@@ -16,7 +35,7 @@ other*.**
16
35
  <Note>
17
36
  Agents transact against your **pushed schema**, same as everyone — `ablo.records`
18
37
  exists because you defined a `record` model and ran `ablo push`. The key
19
- authenticates; the [schema](/quickstart) defines what you can call.
38
+ authenticates; the [schema](/installation) defines what you can call.
20
39
  </Note>
21
40
 
22
41
  ## The agent client
package/docs/api.md CHANGED
@@ -96,6 +96,49 @@ fallback removed — nothing to await, so they return a value.
96
96
  through the server. The `local` reads work off the rows a session has already
97
97
  synced, so a cheap re-read needs no round-trip.
98
98
 
99
+ ## Atomic commits
100
+
101
+ Use one `ablo.commits.create` when several Ablo model writes must all land or
102
+ none may land. Put every operation in `operations` and every exact row returned
103
+ by `read` that influenced the batch in the top-level `reads` array.
104
+
105
+ ```ts
106
+ import { AbloStaleContextError } from '@abloatai/ablo';
107
+
108
+ const task = await ablo.tasks.read({ id: taskId });
109
+ if (!task) throw new Error('task not found');
110
+
111
+ try {
112
+ await ablo.commits.create({
113
+ operations: [
114
+ {
115
+ action: 'update', model: 'tasks',
116
+ id: task.id,
117
+ data: { status: 'done' },
118
+ },
119
+ {
120
+ action: 'create', model: 'tasks',
121
+ id: markerId,
122
+ data: { title: 'atomic marker', status: 'done' },
123
+ },
124
+ ],
125
+ reads: [task],
126
+ });
127
+ } catch (error) {
128
+ if (error instanceof AbloStaleContextError && error.code === 'stale_context') {
129
+ console.log(error.code);
130
+ } else {
131
+ throw error;
132
+ }
133
+ }
134
+ ```
135
+
136
+ The server checks the premises and applies the operations in one transaction.
137
+ If any premise is stale or any operation fails, no operation lands. Independent
138
+ model calls are not an atomic batch. External effects and application-owned
139
+ Postgres writes cannot join this commit; keep those in their existing
140
+ transaction or outbox.
141
+
99
142
  ### Reading a whole collection
100
143
 
101
144
  Prefer a filtered `listAll` when the application truly needs one complete
@@ -252,13 +295,18 @@ const handle = await ablo.weatherReports.claim({
252
295
  description: 'editing',
253
296
  ttl: '2m',
254
297
  });
255
- await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
298
+ await ablo.weatherReports.update({
299
+ id: handle.data.id,
300
+ data: { status: 'ready' },
301
+ claim: handle,
302
+ });
256
303
  await handle.release();
257
304
  ```
258
305
 
259
- Writes go through the normal `ablo.<model>.update({ id, data })`. While you hold
260
- a claim on `id`, that `update` rejects with `AbloStaleContextError` if the row
261
- changed underneath you since you took the claim, so you re-read before retrying.
306
+ Writes go through the normal model mutation and pass the held handle as `claim`.
307
+ That explicit handle carries commit-time fencing. If the row changed underneath
308
+ you since you took the claim, the update rejects with `AbloStaleContextError`,
309
+ so you re-read before retrying.
262
310
  Call `handle.release()` (or `ablo.weatherReports.claim.release({ id })`) to release
263
311
  the claim when your work is done.
264
312
 
@@ -307,7 +355,9 @@ rendered as agent tools.
307
355
 
308
356
  ## Errors
309
357
 
310
- All SDK errors extend `AbloError` and expose a stable `type` string.
358
+ All SDK errors extend `AbloError`. `type` is the class-name discriminator, such
359
+ as `AbloStaleContextError`; `code` is the wire condition, such as
360
+ `stale_context`. Use `instanceof` in-process and `type` after serialization.
311
361
 
312
362
  | Error | Meaning |
313
363
  |---|---|
@@ -0,0 +1,84 @@
1
+ # Basic Usage
2
+
3
+ > Read one task row by id, list matching rows, create and update state, and coordinate work when needed.
4
+
5
+ Assume the configured client from [Installation](./installation.md) is exported
6
+ from `./ablo/client`.
7
+
8
+ ## Read rows
9
+
10
+ ```ts
11
+ import { ablo } from './ablo/client';
12
+
13
+ const task = await ablo.tasks.get({ id: taskId });
14
+ if (!task) throw new Error('task not found');
15
+
16
+ const open = await ablo.tasks.list({ where: { status: 'open' } });
17
+ ```
18
+
19
+ `get` observes one current row. Use `read` instead only when a later Ablo write
20
+ must be rejected if that exact premise changes.
21
+
22
+ ## Write rows
23
+
24
+ ```ts
25
+ const created = await ablo.tasks.create({
26
+ data: { title: 'Review return', status: 'open' },
27
+ });
28
+
29
+ await ablo.tasks.update({
30
+ id: created.id,
31
+ data: { status: 'done' },
32
+ });
33
+ ```
34
+
35
+ Writes return after Ablo confirms the authoritative result. Your PostgreSQL
36
+ constraints and schema remain in force.
37
+
38
+ ## Coordinate slow work
39
+
40
+ When the target is an Ablo model row, claim it before the expensive step and
41
+ pass the claim to the final write.
42
+
43
+ ```ts
44
+ await using claim = await ablo.tasks.claim({ id: taskId });
45
+
46
+ const result = await performExpensiveWork(claim.data);
47
+
48
+ await ablo.tasks.update({
49
+ id: claim.data.id,
50
+ data: { title: result.title, status: 'done' },
51
+ claim,
52
+ });
53
+ ```
54
+
55
+ A contender follows the chosen wait, skip, or fail policy. Disposal releases
56
+ the claim, and expiry lets another participant recover when the owner
57
+ disappears.
58
+
59
+ ## Preserve an existing write
60
+
61
+ The claimed target does not have to be an Ablo row. Claim a stable business id
62
+ and keep the final transaction in the application that already owns it.
63
+
64
+ ```ts
65
+ await using lease = await ablo.taskRuns.claim(taskId, {
66
+ contention: { mode: 'skip' },
67
+ });
68
+ if (!lease) return;
69
+
70
+ const prepared = await performExpensiveWork(taskId);
71
+ return existingTaskService.commitPrepared(taskId, prepared);
72
+ ```
73
+
74
+ That transaction must still re-read, validate, and commit authoritatively. An
75
+ Ablo claim does not join a transaction in another process.
76
+
77
+ ## Add stronger guarantees when required
78
+
79
+ - [Concurrency Convention](./concurrency-convention.md): reject a write when an
80
+ earlier premise changed.
81
+ - [Atomic commits](./api.md#atomic-commits): apply several Ablo writes together.
82
+ - [Idempotency](./idempotency.md): make retrying the same Ablo mutation safe.
83
+ - [Agents](./agents.md): configure a stateless HTTP worker.
84
+ - [React](./react.md): add live state and presence for a human interface.
@@ -28,20 +28,10 @@ const ablo = Ablo({
28
28
  });
29
29
  ```
30
30
 
31
- Common options:
32
-
33
- | Option | Purpose |
34
- |---|---|
35
- | `schema` | Required for typed model clients. |
36
- | `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
37
- | `baseURL` | Override the hosted sync endpoint for staging or private deployments. An HTTPS origin, optionally with a path prefix; plain HTTP is accepted for localhost. Your key travels here, so a URL carrying its own credentials, a query, or a fragment is refused at construction. |
38
- | `persistence` | `memory` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
39
- | `durableWrites` | Optional crash recovery for unacknowledged agent/worker writes. Independent of the default memory cache; accepts `{ store, namespace? }`. |
40
- | `transport` | `'websocket'` (default) is the live, stateful client: a persistent socket, a local synced pool, and model `onChange` subscriptions. `'http'` returns the **stateless** client for server-side actors (agents, workers, serverless): the same `ablo.<model>` read/write/claim surface, but ordinary calls are HTTP round trips with no socket. Stateful-only model methods (`local`, model `onChange`, and `join`) are compile errors. A listener added through `context().onChange` holds one HTTP response open only until its context changes or its last listener stops. |
41
- | `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
42
- | `defaultHeaders` | Extra headers attached to every HTTP request. |
43
- | `defaultQuery` | Extra query parameters attached to every HTTP request. |
44
- | `dangerouslyAllowBrowser` | Required before sending an API key from browser code. Prefer a server route instead. |
31
+ The package-root export is the stateless HTTP client for agents, workers, route
32
+ handlers, and other server operations. See [Options](./options.md) for its exact
33
+ constructor reference. Live state and local reads are added through the
34
+ [React client](./react.md).
45
35
 
46
36
  Your database connects out of band — through logical replication (`npx ablo
47
37
  connect`), or the signed [Data Source](./data-sources.md) endpoint as the
@@ -169,7 +159,9 @@ stream, so they never poll.
169
159
 
170
160
  ## Errors
171
161
 
172
- All SDK errors extend `AbloError` and carry a stable `type`.
162
+ All SDK errors extend `AbloError`. `type` is the class-name discriminator, such
163
+ as `AbloStaleContextError`; `code` is the wire condition, such as
164
+ `stale_context`. Use `instanceof` in-process and `type` after serialization.
173
165
 
174
166
  | Error | Typical cause |
175
167
  |---|---|
@@ -0,0 +1,63 @@
1
+ # Comparison
2
+
3
+ > How Ablo relates to the coordination tools an experienced team may already use.
4
+
5
+ Teams do not need Ablo to build a lock. Redis reservations and PostgreSQL locks
6
+ are proven, inexpensive primitives. Ablo is useful when the same team would
7
+ otherwise have to define ownership, expiry, recovery, waiting, participant
8
+ identity, stale-result handling, and visibility for each new workflow.
9
+
10
+ ## Versus PostgreSQL locks
11
+
12
+ - **Keep the lock.** PostgreSQL should continue to protect the short,
13
+ authoritative database transaction.
14
+ - **Coordinate before the transaction.** An Ablo claim can cover the model call,
15
+ document search, browser session, or tool run that happens before commit.
16
+ - **Keep the connection short-lived.** The application does not have to hold one
17
+ database session while an agent waits on external work.
18
+ - **Revalidate at commit.** The existing service still applies authorization,
19
+ constraints, version checks, and business rules.
20
+
21
+ ## Versus Redis reservations
22
+
23
+ - **The primitive is familiar.** Ablo uses expiring leases for live ownership
24
+ and waiting; it does not claim that temporary reservations are novel.
25
+ - **The lifecycle is defined.** Acquisition, skip, wait, heartbeat, release,
26
+ expiry, cancellation, and recovery share one client contract.
27
+ - **Ownership has identity.** A claim belongs to a scoped participant rather
28
+ than only an arbitrary worker string.
29
+ - **Correctness stays durable.** PostgreSQL and commit-time version checks remain
30
+ the backstop when an expired worker resumes late.
31
+ - **Contention is visible.** Owners, waiters, duration, and rejection reasons use
32
+ the same operational model across workflows.
33
+
34
+ ## Versus queues and workflow engines
35
+
36
+ - Queues decide who receives a job; claims decide who may act on a contested
37
+ business resource.
38
+ - Redelivery still needs idempotency, and delivery does not prove the rows behind
39
+ a decision are unchanged.
40
+ - Workflow engines remain the right owner for durable steps, timers, and retry
41
+ history. Ablo coordinates those workflows with other agents, services, and
42
+ people touching the same state.
43
+
44
+ ## Versus rolling your own
45
+
46
+ - Start without designing Redis key conventions, ownership tokens, renewal,
47
+ safe release, wait queues, and crash recovery for every call site.
48
+ - Reuse one participant and authorization model across workers and human
49
+ interfaces.
50
+ - Test against one documented failure contract instead of rebuilding delayed
51
+ worker, expiry, retry, and partial-failure tests per workflow.
52
+ - Add captured reads, guarded writes, and atomic Ablo commits only when the
53
+ operation needs them.
54
+
55
+ ## When Ablo is not necessary
56
+
57
+ PostgreSQL or a small internal reservation can be enough when one team controls
58
+ every writer, work is short, contention is rare, and stale or duplicate work is
59
+ cheap. Ablo becomes more valuable as slow agent work, independent participants,
60
+ shared resources, recovery, authority, and operational explanation matter.
61
+
62
+ The adoption boundary is intentionally small: keep the architecture that
63
+ already works and [coordinate one existing operation](./coordinate-existing-work.md).
@@ -2,6 +2,12 @@
2
2
 
3
3
  > A write either declares what it read or deliberately does not.
4
4
 
5
+ Use a captured read when a write must be rejected because an earlier premise
6
+ changed.
7
+
8
+ Read the authoritative premise, carry
9
+ that exact evidence into the write, and handle the documented failure code.
10
+
5
11
  Ablo does not put a configurable stale mode between your code and a
6
12
  commit. The public choice is visible at the call site:
7
13
 
@@ -38,6 +44,22 @@ The server validates every declared premise inside the write transaction. If
38
44
  one is stale, the entire mutation rejects before any write applies. Re-read,
39
45
  recompute, and submit a new mutation when that is the behavior you want.
40
46
 
47
+ ```ts
48
+ import { AbloStaleContextError } from '@abloatai/ablo';
49
+
50
+ try {
51
+ await submitGuardedWrite();
52
+ } catch (error) {
53
+ if (error instanceof AbloStaleContextError && error.code === 'stale_context') {
54
+ return rebuildFromFreshReads();
55
+ }
56
+ throw error;
57
+ }
58
+ ```
59
+
60
+ `error.type` is the class-name discriminator (`AbloStaleContextError`);
61
+ `error.code` is the wire condition (`stale_context`).
62
+
41
63
  ## Unguarded writes
42
64
 
43
65
  Use `get` or `list` when you only need to observe, and omit `reads` when the
@@ -71,6 +93,11 @@ A claim protects a target across a slower read → decide → write interval.
71
93
  Foreign writers are rejected while the claim is active; contenders that ask
72
94
  to queue wait in order. Ordinary reads stay open.
73
95
 
96
+ When the final effect remains in an existing application path—such as its API,
97
+ database transaction, filesystem, or Git merge—start with
98
+ [Coordinate existing work](./coordinate-existing-work.md). Use the
99
+ row-backed claim below when the target and final write belong to an Ablo model.
100
+
74
101
  Claims and stale reads answer different questions:
75
102
 
76
103
  | Mechanism | Lifetime | Question |
package/docs/context.md CHANGED
@@ -127,6 +127,26 @@ delivery closes. The final create, update, or delete must still receive
127
127
  `reads: ctx.reads`; that check remains authoritative if delivery races the
128
128
  write or is disconnected.
129
129
 
130
+ ## Retry stale agent work
131
+
132
+ Use this policy for a long-running turn:
133
+
134
+ 1. Create one operation key before the retry loop.
135
+ 2. Build a new context on every attempt.
136
+ 3. Use `onChange` to abort the model and cancellable tools.
137
+ 4. Still pass `reads: ctx.reads` to the final write.
138
+ 5. Retry a stale attempt at most a small fixed number of times.
139
+
140
+ Automatic retry is safe only before the first external action that cannot be
141
+ canceled. After sending an email, charging a card, or receiving an uncertain
142
+ tool response, look up that action by the same operation key. Do not run it
143
+ again unless that tool explicitly guarantees the retry is safe.
144
+
145
+ [`examples/stale-context-agent-turn.ts`](../examples/stale-context-agent-turn.ts)
146
+ is the complete copyable loop. Put that function in the application operation
147
+ that owns the write; GraphQL resolvers and route handlers should call it once,
148
+ not add another retry loop.
149
+
130
150
  ## External context
131
151
 
132
152
  Provider results pass through without an adapter or provider dependency. The
@@ -0,0 +1,104 @@
1
+ # Coordinate Existing Work
2
+
3
+ > Start here. Preserve the application, coordinate one operation, and read another page only when the routing table sends you there.
4
+
5
+ Use this guide to coordinate expensive work while its existing PostgreSQL
6
+ transaction remains authoritative.
7
+
8
+ Many production systems already reserve slow work in Redis and protect the
9
+ final write with a PostgreSQL transaction. That is a sound architecture. The
10
+ cost appears when every workflow must independently define ownership, expiry,
11
+ heartbeat, waiting, recovery, participant identity, and operational visibility.
12
+
13
+ Ablo standardizes that coordination lifecycle. It does not replace the
14
+ application's authoritative transaction.
15
+
16
+ ## Existing backend: copy this shape
17
+
18
+ For an application that already owns its API and Postgres transaction:
19
+
20
+ 1. Choose one named operation, such as `completeTask`.
21
+ 2. Keep its API, authorization, validation, transaction, locks, and constraints.
22
+ 3. Give each concurrent worker a distinct scoped credential.
23
+ 4. Claim the operation's stable business identifier before expensive work.
24
+ 5. While the claim is held, call the existing operation to commit.
25
+
26
+ ```ts
27
+ const ablo = Ablo({
28
+ schema,
29
+ apiKey: process.env.ABLO_API_KEY,
30
+ transport: 'http',
31
+ });
32
+
33
+ await using lease = await ablo.taskRuns.claim(taskId, {
34
+ contention: { mode: 'skip' },
35
+ ttl: '30s',
36
+ heartbeat: { every: '10s' },
37
+ });
38
+ if (!lease) return { outcome: 'skipped' };
39
+
40
+ const prepared = await performExpensiveWork(taskId);
41
+ return existingTaskService.commitPrepared(taskId, prepared);
42
+ ```
43
+
44
+ The string passed to `claim` is an identifier-only lease. It does not require an
45
+ Ablo row and returns no row data. `commitPrepared` must still re-read and validate
46
+ inside the application's Postgres transaction. An Ablo lease does not join a
47
+ transaction in another process.
48
+
49
+ If the operation only needs to inspect an Ablo row before calling the existing
50
+ write path, keep it this small:
51
+
52
+ ```ts
53
+ const task = await ablo.tasks.get({ id: taskId });
54
+ if (!task) throw new Error('task not found');
55
+
56
+ await completeTask({ id: task.id, expectedTitle: task.title });
57
+ return task.id;
58
+ ```
59
+
60
+ `get({ id })` observes a row. Use `read({ id })` only when its captured evidence
61
+ will be passed to an Ablo write through `reads`.
62
+
63
+ ## Change the shape only when required
64
+
65
+ | Your operation requires this | Use | Read |
66
+ |---|---|---|
67
+ | The claimed target is an Ablo model row and the final write goes through that model. | Row-backed claim; pass the returned `claim` to the write. | [Coordination](../coordination.md) |
68
+ | The result depends on an Ablo row that may change while work runs. | `read(...)` the premise and pass it through `reads`. | [Concurrency Convention](../concurrency-convention.md) |
69
+ | Several Ablo writes must all land or none may land. | One `commits.create(...)`. | [API Reference](../api.md) |
70
+ | A person needs live state, presence, or reactive local reads. | WebSocket client for that human interface. Workers stay on HTTP. | [React](../react.md) |
71
+ | The operation sends email, charges money, writes a file, or calls another provider. | That system's idempotency key or an application outbox. | [Idempotency](../idempotency.md) |
72
+
73
+ Do not add a mechanism unless its condition is true. In particular, do not
74
+ replace an existing database operation merely because Ablo coordinates it.
75
+
76
+ ## Check these boundaries before editing
77
+
78
+ - What existing operation and public result must remain unchanged?
79
+ - What stable identifier represents the contested work?
80
+ - Which process performs expensive work, and which process commits?
81
+ - Does Postgres or Ablo own each final write?
82
+ - What does the caller receive on contention, failure, and retry?
83
+
84
+ If an answer is unknown, preserve the existing write path. Do not copy an
85
+ advanced example or expand the schema to hide the missing decision.
86
+
87
+ ## Prove the implementation
88
+
89
+ For the adopted operation, test that:
90
+
91
+ 1. The coordinated and existing paths return the same public result.
92
+ 2. Two distinct participants do not both perform the expensive work.
93
+ 3. A contender follows the chosen skip or wait behavior.
94
+ 4. Failure or expiry allows a later attempt to proceed.
95
+ 5. The existing authorization and database transaction still run.
96
+
97
+ Local tests prove application behavior. Run
98
+ [`examples/coordination-conformance`](../../../../examples/coordination-conformance/README.md)
99
+ against hosted Ablo to prove participant identity, heartbeat, exclusion, release,
100
+ and expiry. Run staging against the real database and authorization to prove the
101
+ production boundary.
102
+
103
+ For setup, read the [Integration Guide](../integration-guide.md). The complete
104
+ implementation on this page is the starter route for an existing backend.