@abloatai/ablo 0.57.0 → 0.59.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/AGENTS.md +12 -5
- package/CHANGELOG.md +269 -13
- package/README.md +6 -5
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/context/evidence.d.ts +6 -8
- package/dist/context/evidence.d.ts.map +1 -1
- package/dist/context/evidence.js +6 -20
- package/dist/context/evidence.js.map +1 -1
- package/dist/context/index.d.ts +23 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/onChange.d.ts +9 -0
- package/dist/context/onChange.d.ts.map +1 -0
- package/dist/context/onChange.js +37 -0
- package/dist/context/onChange.js.map +1 -0
- package/dist/source-conformance.d.ts +1 -1
- package/dist/source-conformance.d.ts.map +1 -1
- package/dist/source-conformance.js +1 -1
- package/dist/source-conformance.js.map +1 -1
- package/dist/source-drizzle.d.ts +1 -1
- package/dist/source-drizzle.d.ts.map +1 -1
- package/dist/source-drizzle.js +1 -1
- package/dist/source-drizzle.js.map +1 -1
- package/dist/source-kysely.d.ts +1 -1
- package/dist/source-kysely.d.ts.map +1 -1
- package/dist/source-kysely.js +1 -1
- package/dist/source-kysely.js.map +1 -1
- package/dist/source-next.d.ts +1 -1
- package/dist/source-next.d.ts.map +1 -1
- package/dist/source-next.js +1 -1
- package/dist/source-next.js.map +1 -1
- package/docs/agents.md +38 -15
- package/docs/api-keys.md +6 -6
- package/docs/api.md +107 -34
- package/docs/basic-usage.md +84 -0
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +27 -29
- package/docs/comparison.md +63 -0
- package/docs/concurrency-convention.md +92 -75
- package/docs/context.md +76 -31
- package/docs/coordinate-existing-work.md +104 -0
- package/docs/coordination.md +93 -38
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/deployment.md +19 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/evidence-backed-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/faq.md +75 -0
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +8 -8
- package/docs/idempotency.md +3 -0
- package/docs/identity.md +2 -1
- package/docs/implement.md +61 -0
- package/docs/implementation-index.md +20 -0
- package/docs/index.md +59 -173
- package/docs/installation.md +77 -0
- package/docs/instrumentation.md +52 -0
- package/docs/integration-guide.md +20 -19
- package/docs/integrations/sandbox-runtime.md +157 -0
- package/docs/integrations.md +9 -0
- package/docs/migration.md +12 -7
- package/docs/operating-on-your-database.md +7 -0
- package/docs/options.md +172 -0
- package/docs/quickstart.md +25 -14
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/security.md +64 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +8 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/expensive-agent-turn.ts +1 -1
- package/examples/stale-context-agent-turn.ts +106 -0
- package/llms.txt +23 -12
- package/package.json +7 -6
- package/dist/context/sources.d.ts +0 -21
- package/dist/context/sources.d.ts.map +0 -1
- package/dist/context/sources.js +0 -36
- package/dist/context/sources.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
|
@@ -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.
|
package/docs/coordination.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
> Choose plain writes, functional updates, stale guards, or claims without losing concurrent work.
|
|
4
4
|
|
|
5
|
+
This page owns row coordination: acquire ownership, proceed from the granted
|
|
6
|
+
state, and release on every exit path.
|
|
7
|
+
|
|
8
|
+
## Choose the owner first
|
|
9
|
+
|
|
10
|
+
| Where the result lands | Start here |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Existing application path: API, GraphQL operation, Postgres transaction, filesystem write, or Git branch merge | [Coordinate existing work](./coordinate-existing-work.md) |
|
|
13
|
+
| Ablo model row, written through the Ablo client | Continue on this page |
|
|
14
|
+
|
|
15
|
+
If Ablo only decides who may start, use the first row and stop here. The claim
|
|
16
|
+
examples below own Ablo-row writes; they are not the starter for wrapping an
|
|
17
|
+
existing operation.
|
|
18
|
+
|
|
5
19
|
Ablo gives you several concurrency tools because not every write has the same
|
|
6
20
|
meaning. Choose the narrowest one that matches the operation.
|
|
7
21
|
|
|
@@ -9,7 +23,7 @@ meaning. Choose the narrowest one that matches the operation.
|
|
|
9
23
|
|---|---|---|
|
|
10
24
|
| Set an independent value | `update({ id, data })` | Last-write-wins when no claim applies. |
|
|
11
25
|
| Compute a value from the current row | `update(id, current => next)` | Re-reads and retries if the row changes concurrently. |
|
|
12
|
-
| Write only if earlier rows are still current | `reads: [record,
|
|
26
|
+
| Write only if earlier rows are still current | `reads: [record, rules]` | Rejects when an explicitly named dependency changed. |
|
|
13
27
|
| Read, call a model, then write | `claim({ id })` | Other participants cannot write the claimed target by default until your claim ends. |
|
|
14
28
|
|
|
15
29
|
**If a model call sits between the read and the write, take a claim.** A stale
|
|
@@ -25,8 +39,8 @@ does not carry a stale premise. It is intentionally last-write-wins.
|
|
|
25
39
|
Pass the exact rows that produced a decision on the write:
|
|
26
40
|
|
|
27
41
|
```ts
|
|
28
|
-
const record = await ablo.records.
|
|
29
|
-
const policy = await ablo.policies.
|
|
42
|
+
const record = await ablo.records.read({ id: recordId });
|
|
43
|
+
const policy = await ablo.policies.read({ id: policyId });
|
|
30
44
|
if (!record || !policy) throw new Error('required input is missing');
|
|
31
45
|
|
|
32
46
|
const result = await model({ record, policy });
|
|
@@ -91,7 +105,7 @@ Use explicit returned rows when application code reads first and writes later,
|
|
|
91
105
|
but does not need to reserve the row:
|
|
92
106
|
|
|
93
107
|
```ts
|
|
94
|
-
const report = await ablo.reports.
|
|
108
|
+
const report = await ablo.reports.read({ id: reportId });
|
|
95
109
|
if (!report) throw new Error('report missing');
|
|
96
110
|
|
|
97
111
|
await ablo.reports.update({
|
|
@@ -101,37 +115,13 @@ await ablo.reports.update({
|
|
|
101
115
|
});
|
|
102
116
|
```
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| `reject` | Reject the write if its premise is stale. |
|
|
109
|
-
| `notify` | Leave the row unchanged and return the current value for reconciliation. |
|
|
110
|
-
| `overwrite` | Apply the write without a stale check. |
|
|
118
|
+
There is no stale-mode option on the write. If a declared row changed, Ablo
|
|
119
|
+
rejects the whole mutation with `AbloStaleContextError`. Re-read and recompute,
|
|
120
|
+
or use the functional update form when the computation is pure and retryable.
|
|
121
|
+
To make an unconditional assignment, omit `reads` deliberately.
|
|
111
122
|
|
|
112
123
|
See [Concurrency Convention](./concurrency-convention.md) for guarded batches
|
|
113
|
-
and
|
|
114
|
-
|
|
115
|
-
### Decide the model's conflict policy
|
|
116
|
-
|
|
117
|
-
Who yields is a design decision about the model, not something to restate on
|
|
118
|
-
every write. Declare it once, in the schema, and it travels to the server with
|
|
119
|
-
the rest of the model:
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import { coordination, model, z } from '@abloatai/ablo/schema';
|
|
123
|
-
|
|
124
|
-
const cards = model(
|
|
125
|
-
{ title: z.string() },
|
|
126
|
-
{
|
|
127
|
-
conflict: coordination.humansOverwrite().agentsReject(),
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
An omitted participant kind uses the engine default, `reject`. A per-write
|
|
133
|
-
`onStale` states the disposition for that one write. Keep the policy simple, and
|
|
134
|
-
document any rule that lets a participant overwrite a held claim.
|
|
124
|
+
and the `get` / `read` boundary.
|
|
135
125
|
|
|
136
126
|
## Claims
|
|
137
127
|
|
|
@@ -149,19 +139,66 @@ const forecast = await generateForecast(claim.data.location);
|
|
|
149
139
|
await ablo.reports.update({
|
|
150
140
|
id: claim.data.id,
|
|
151
141
|
data: { forecast, status: 'ready' },
|
|
142
|
+
claim,
|
|
152
143
|
});
|
|
153
144
|
```
|
|
154
145
|
|
|
155
146
|
If another participant already holds the target, `claim` waits its turn and
|
|
156
|
-
then resolves with a fresh row in `claim.data`. Ordinary reads remain open.
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
then resolves with a fresh row in `claim.data`. Ordinary reads remain open. Pass
|
|
148
|
+
the handle as `claim` on the write so Ablo can verify that you still own the row
|
|
149
|
+
and that it has not changed since the claim was granted.
|
|
159
150
|
|
|
160
151
|
Bind claims with `await using` whenever possible. The claim then releases when
|
|
161
152
|
the scope exits, including when the external call or write throws. For runtimes
|
|
162
153
|
without explicit resource management, use `try/finally` and
|
|
163
154
|
`await claim.release()`.
|
|
164
155
|
|
|
156
|
+
### Handle an expired claim
|
|
157
|
+
|
|
158
|
+
When heartbeat is unset, the lease ends at its TTL. A delayed write that passes
|
|
159
|
+
the expired handle rejects with `AbloClaimedError` and code `claim_lost`. Do not
|
|
160
|
+
apply the prepared result elsewhere; clean up best-effort, then restart from a
|
|
161
|
+
new claim and its fresh `claim.data`.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { AbloClaimedError } from '@abloatai/ablo';
|
|
165
|
+
|
|
166
|
+
const claim = await ablo.tasks.claim({ id: taskId, ttl: '2s' });
|
|
167
|
+
try {
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 2300));
|
|
169
|
+
await ablo.tasks.update({
|
|
170
|
+
id: claim.data.id,
|
|
171
|
+
data: { status: 'done' },
|
|
172
|
+
claim,
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error instanceof AbloClaimedError && error.code === 'claim_lost') {
|
|
176
|
+
console.log(error.code);
|
|
177
|
+
} else {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
} finally {
|
|
181
|
+
try { await claim.release(); } catch { /* already expired */ }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Keep a claim alive
|
|
186
|
+
|
|
187
|
+
Set `ttl` to how quickly another worker should recover if this one stops. If the
|
|
188
|
+
work can take longer, set `heartbeat: true` so Ablo renews the claim:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const claim = await ablo.records.claim({
|
|
192
|
+
id: recordId,
|
|
193
|
+
ttl: '30s',
|
|
194
|
+
heartbeat: true,
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Leave `heartbeat` out when the claim should expire after the TTL; do not pass
|
|
199
|
+
`false`. If a write returns `claim_lost`, discard that result, claim the row
|
|
200
|
+
again, and restart from the new `claim.data`.
|
|
201
|
+
|
|
165
202
|
### One identity per participant
|
|
166
203
|
|
|
167
204
|
Explicit claims coordinate authenticated participants. Two clients using the
|
|
@@ -198,7 +235,7 @@ try {
|
|
|
198
235
|
}
|
|
199
236
|
```
|
|
200
237
|
|
|
201
|
-
To wait with limits, keep the
|
|
238
|
+
To wait with limits, keep the contention settings together:
|
|
202
239
|
|
|
203
240
|
```ts
|
|
204
241
|
const claim = await ablo.records.claim({
|
|
@@ -226,6 +263,19 @@ await using claim = await ablo.records.claim({
|
|
|
226
263
|
Claims on disjoint fields can coexist. A whole-row claim conflicts with every
|
|
227
264
|
field claim on that row.
|
|
228
265
|
|
|
266
|
+
### Relations do not create hierarchical claims
|
|
267
|
+
|
|
268
|
+
A `parent: true` relation controls ownership, access inheritance, and sync
|
|
269
|
+
routing. It does not make claims conflict across related rows. For example, a
|
|
270
|
+
claim on one document row and a claim on one of its page rows have different
|
|
271
|
+
model-and-ID targets and can coexist.
|
|
272
|
+
|
|
273
|
+
Choose the row that represents the actual unit of exclusive work. Page rows
|
|
274
|
+
allow different pages to process concurrently. If a whole-document operation
|
|
275
|
+
must exclude every page operation, enumerate the authoritative page manifest,
|
|
276
|
+
acquire page claims in one stable order, and guard the manifest against change.
|
|
277
|
+
Do not infer that exclusion from the schema relation alone.
|
|
278
|
+
|
|
229
279
|
The target options are:
|
|
230
280
|
|
|
231
281
|
| Option | Purpose |
|
|
@@ -251,12 +301,17 @@ The main methods are:
|
|
|
251
301
|
|
|
252
302
|
| Method | Purpose |
|
|
253
303
|
|---|---|
|
|
254
|
-
| `claim({ id })` |
|
|
304
|
+
| `claim({ id, ...options })` | Read and claim an existing model row; the handle includes fresh row data. |
|
|
255
305
|
| `claim.state({ id })` | Read the current holder without blocking. |
|
|
256
306
|
| `claim.queue({ id })` | Read the current wait order. |
|
|
257
307
|
| `claim.release({ id })` | Release early when you do not hold a handle. |
|
|
258
308
|
| `join({ scope })` | Observe presence for a broader scope. |
|
|
259
309
|
|
|
310
|
+
This page owns row-backed claims: `model.claim({ id })` reads and claims an Ablo
|
|
311
|
+
model row, and the handle carries fresh data. Identifier-only claims before an
|
|
312
|
+
existing authoritative service have a different persistence boundary; use the
|
|
313
|
+
[coordinate-existing-work guide](./coordinate-existing-work.md) for that form.
|
|
314
|
+
|
|
260
315
|
## Choosing correctly
|
|
261
316
|
|
|
262
317
|
- Prefer a plain update for values that do not depend on an earlier read.
|
package/docs/data-sources.md
CHANGED
|
@@ -110,12 +110,12 @@ migrations — your migration tool stays in charge of the shape of your database
|
|
|
110
110
|
Ablo only writes rows into tables you already have, through a role scoped to
|
|
111
111
|
exactly that.
|
|
112
112
|
|
|
113
|
-
> **Just trying Ablo?**
|
|
114
|
-
>
|
|
115
|
-
>
|
|
116
|
-
>
|
|
117
|
-
>
|
|
118
|
-
>
|
|
113
|
+
> **Just trying Ablo?** Start on a throwaway Postgres rather than your production
|
|
114
|
+
> one. `ablo dev` gives each Git branch its own isolated plane, so point that
|
|
115
|
+
> branch at a separate or local database, build against it, and connect your
|
|
116
|
+
> production root (below) when you're ready for its database to be the system of
|
|
117
|
+
> record. A branch with nothing connected refuses a schema push, which is the
|
|
118
|
+
> first thing you'll hit if you skip this.
|
|
119
119
|
|
|
120
120
|
Connecting sets up two capabilities on your Postgres: **logical replication**, so
|
|
121
121
|
Ablo can read and confirm, and a **scoped DML role**, so Ablo can write. `ablo
|
|
@@ -475,6 +475,12 @@ directly by other application code is visible only if that code writes the same
|
|
|
475
475
|
outbox record in its transaction. Native WAL observation sees both Ablo and
|
|
476
476
|
external writes.
|
|
477
477
|
|
|
478
|
+
Endpoint events use a versioned envelope. Version 2 freezes `syncGroups` in the
|
|
479
|
+
writing transaction; version 1 is retained only to decode events written by an
|
|
480
|
+
older adapter during a rolling upgrade. Poll requests keep `cursor` (where to
|
|
481
|
+
read) separate from `acknowledgedThrough` (what Ablo has durably accepted), and
|
|
482
|
+
the built-in adapters prune acknowledged rows in bounded batches.
|
|
483
|
+
|
|
478
484
|
## Next steps
|
|
479
485
|
|
|
480
486
|
- [Quickstart](./quickstart.md) — connect and write through `ablo.<model>`.
|
package/docs/debugging.md
CHANGED
|
@@ -86,7 +86,7 @@ Read it as the lifecycle of one claim:
|
|
|
86
86
|
- **`queued … position N of M`:** the row was held, so you're waiting in the FIFO line. This is the "an agent is waiting behind a claim" moment; it re-logs only when your position changes, so you can watch it advance.
|
|
87
87
|
- **`granted … your turn`:** you reached the head of the line; the lease is now yours and the row may have changed while you waited.
|
|
88
88
|
- **`rejected … held by <who>`:** your claim was refused because someone else holds it (and the model's policy didn't let you in).
|
|
89
|
-
- **`lost`:** you held the lease and it
|
|
89
|
+
- **`lost`:** you held the lease and it ended (the queue fairness ceiling advanced, or it expired).
|
|
90
90
|
- **`released`:** you (or `await using`'s scope exit) gave the lease back.
|
|
91
91
|
|
|
92
92
|
## Where the logs run
|
package/docs/deployment.md
CHANGED
|
@@ -229,6 +229,24 @@ ABLO_API_KEY=sk_… npx ablo webhooks list # endpoints + delivery health
|
|
|
229
229
|
place to look when a mirror falls behind. [Webhooks](./webhooks.md) covers the
|
|
230
230
|
handler, the Standard Webhooks signature, and rolling a secret.
|
|
231
231
|
|
|
232
|
+
## Multi-stage schema changes
|
|
233
|
+
|
|
234
|
+
`ablo plan` is the read-only front door for source, active artifact, and
|
|
235
|
+
PostgreSQL together. For a live rename, type transition, or required-field
|
|
236
|
+
change, commit a deployment manifest and pass the same file to plan and push.
|
|
237
|
+
Its gates are `expand`, `dual_write`, `backfill`, `verify`, `switch`, and
|
|
238
|
+
`contract`; each names an owner, resource, dependencies, status, and action.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx ablo plan --manifest ablo/deployment.json
|
|
242
|
+
npx ablo push --manifest ablo/deployment.json --yes
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
A live manifest cannot combine expand and contract for the same resource.
|
|
246
|
+
Contract belongs in a later manifest with its own recorded `approval`. A
|
|
247
|
+
backfill being finished is not contract evidence; verification and client-drain
|
|
248
|
+
gates must also be satisfied.
|
|
249
|
+
|
|
232
250
|
## What to watch once it is live
|
|
233
251
|
|
|
234
252
|
- **`ablo logs`:** commit activity as it happens, scoped by the key. A
|
|
@@ -263,7 +281,7 @@ and what each promises.
|
|
|
263
281
|
`ablo connect check` all green.
|
|
264
282
|
2. A secret `sk_` in the deploy environment as `ABLO_API_KEY` — never in a
|
|
265
283
|
browser bundle.
|
|
266
|
-
3. `ablo
|
|
284
|
+
3. `ablo plan` reviewed, followed by fingerprint-gated `ablo push --yes`.
|
|
267
285
|
4. `ablo status --json` gating the deploy on an empty `blockers` array.
|
|
268
286
|
5. Browser clients on a root-bound `pk_` or an `authEndpoint`, not a secret key.
|
|
269
287
|
6. Webhook endpoints registered at their deployed URLs, with the signing secret
|
|
@@ -25,7 +25,7 @@ a typed error if the row moved underneath you while the agent was busy.
|
|
|
25
25
|
## Schema-Backed Worker
|
|
26
26
|
|
|
27
27
|
The worker uses the same schema client the app uses. It reads the record from the
|
|
28
|
-
server with `
|
|
28
|
+
server with `read({ id })`, claims the row, and writes through
|
|
29
29
|
`ablo.records.update(...)` with a stale-check so a concurrent edit can't be
|
|
30
30
|
overwritten.
|
|
31
31
|
|
|
@@ -49,8 +49,8 @@ const ablo = Ablo({
|
|
|
49
49
|
export async function markDone(recordId: string) {
|
|
50
50
|
await ablo.ready();
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
const record = await ablo.records.
|
|
52
|
+
// read({ id }) is an async server read — await it.
|
|
53
|
+
const record = await ablo.records.read({ id: recordId });
|
|
54
54
|
if (!record) return { status: 'not_found' };
|
|
55
55
|
|
|
56
56
|
try {
|
|
@@ -67,21 +67,9 @@ export async function markDone(recordId: string) {
|
|
|
67
67
|
await using claim = acquired;
|
|
68
68
|
if (claim.data.status === 'done') return { status: 'noop' };
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// those options yourself:
|
|
74
|
-
//
|
|
75
|
-
// ablo.records.update({
|
|
76
|
-
// id: claim.data.id,
|
|
77
|
-
// data: { status: 'done' },
|
|
78
|
-
// readAt: <claim snapshot version>,
|
|
79
|
-
// onStale: 'reject',
|
|
80
|
-
// });
|
|
81
|
-
//
|
|
82
|
-
// If a newer version landed mid-run, the row no longer matches `readAt`, so
|
|
83
|
-
// the server rejects this commit with AbloStaleContextError (caught below)
|
|
84
|
-
// instead of clobbering that edit.
|
|
70
|
+
// The claim handle carries its acquisition snapshot. If a newer version
|
|
71
|
+
// somehow lands mid-run, the server rejects this commit with
|
|
72
|
+
// AbloStaleContextError instead of clobbering that edit.
|
|
85
73
|
const updated = await ablo.records.update({
|
|
86
74
|
id: claim.data.id,
|
|
87
75
|
data: { status: 'done' },
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Verify hosted coordination separately
|
|
2
|
+
|
|
3
|
+
> Prove claim behavior once, without coupling the proof to a document, workflow, or GraphQL schema.
|
|
4
|
+
|
|
5
|
+
Use two test layers when adopting Ablo behind an existing application:
|
|
6
|
+
|
|
7
|
+
| Proof | Responsibility |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Domain contract | State transitions, stale evidence, provenance, idempotency, and old/new path parity. |
|
|
10
|
+
| Hosted coordination conformance | Participant identity, exclusion, heartbeat, release, and lease-expiry recovery. |
|
|
11
|
+
|
|
12
|
+
The runnable hosted proof is
|
|
13
|
+
[`examples/coordination-conformance`](../../../../examples/coordination-conformance/README.md).
|
|
14
|
+
It creates a temporary test branch that inherits an existing non-production
|
|
15
|
+
schema. It does not push a schema or change domain rows.
|
|
16
|
+
|
|
17
|
+
## File structure
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
src/conformance/index.ts
|
|
21
|
+
-> src/conformance/claimExclusion.ts
|
|
22
|
+
-> src/conformance/contract.ts
|
|
23
|
+
src/runtime/index.ts
|
|
24
|
+
-> src/runtime/client.ts
|
|
25
|
+
-> src/runtime/config.ts
|
|
26
|
+
live/index.ts
|
|
27
|
+
-> live/claimAndExit.ts
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The conformance operation depends on a narrow structural claim interface. The
|
|
31
|
+
runtime child supplies the real Ablo model resource. Domain examples depend on
|
|
32
|
+
the same narrow behavior without inheriting this runner's branch or credential
|
|
33
|
+
setup.
|
|
34
|
+
|
|
35
|
+
## Run
|
|
36
|
+
|
|
37
|
+
First run the deterministic structure and configuration checks:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd examples/coordination-conformance
|
|
41
|
+
npm test
|
|
42
|
+
npm run typecheck
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then name an existing model on the CLI login's dedicated non-production
|
|
46
|
+
project:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ABLO_CONFORMANCE_MODEL=existingModel npm run test:live
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The model name supplies only a typed claim namespace. Every claim target is a
|
|
53
|
+
new random identifier; the proof creates no model row. The disposable branch
|
|
54
|
+
is deleted even when an assertion fails.
|
|
55
|
+
|
|
56
|
+
The runtime deliberately calls `model.claim(id, options)`. This identifier
|
|
57
|
+
overload is row-free. `model.claim({ id, ...options })` is the row-backed form:
|
|
58
|
+
it reads the model row and is not interchangeable in a coordination-only
|
|
59
|
+
rollout.
|
|
60
|
+
|
|
61
|
+
## What this lets domain examples omit
|
|
62
|
+
|
|
63
|
+
A document-processing example does not need its own branch provisioning,
|
|
64
|
+
session delegation, heartbeat, or process-death fixture. It must still test its
|
|
65
|
+
own behavior when a claim is won, skipped, released after failure, and combined
|
|
66
|
+
with changing evidence.
|
|
67
|
+
|
|
68
|
+
This separation prevents a vertical testcase from force-replacing an inherited
|
|
69
|
+
schema merely to re-prove generic lease behavior.
|