@abloatai/ablo 0.46.0 → 0.48.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 +1 -1
- package/CHANGELOG.md +160 -0
- package/LICENSE +1 -1
- package/NOTICE +3 -3
- package/README.md +20 -2
- package/docs/agent-messaging.md +0 -1
- package/docs/agents.md +1 -1
- package/docs/api.md +6 -5
- package/docs/cli.md +13 -5
- package/docs/client-behavior.md +8 -7
- package/docs/coordination.md +2 -2
- package/docs/data-sources.md +158 -15
- package/docs/debugging.md +4 -5
- package/docs/examples/agent-human.md +0 -2
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +1 -2
- package/docs/examples/server-agent.md +2 -3
- package/docs/groups.md +1 -1
- package/docs/guarantees.md +4 -7
- package/docs/how-it-works.md +6 -6
- package/docs/idempotency.md +0 -1
- package/docs/identity.md +1 -1
- package/docs/index.md +1 -2
- package/docs/integration-guide.md +7 -10
- package/docs/integrations/inngest.md +1 -4
- package/docs/integrations/temporal.md +1 -3
- package/docs/internal/serializable-schema.md +1 -1
- package/docs/internal/structure.md +4 -0
- package/docs/migration.md +3 -3
- package/docs/operating-on-your-database.md +2 -2
- package/docs/react.md +1 -3
- package/docs/schema-contract.md +2 -2
- package/examples/README.md +1 -1
- package/llms.txt +2 -3
- package/package.json +3 -3
- package/docs/internal/agent-orchestration.md +0 -58
package/docs/data-sources.md
CHANGED
|
@@ -2,6 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
> Keep the rows in your own Postgres while Ablo coordinates and confirms every write.
|
|
4
4
|
|
|
5
|
+
## Localhost development
|
|
6
|
+
|
|
7
|
+
Ablo Cloud cannot dial `localhost`: from a cloud server, that name means the
|
|
8
|
+
cloud server itself, not your Mac or development container. A development child
|
|
9
|
+
branch can still use Postgres that listens only on your machine by running a
|
|
10
|
+
signed Data Source over Ablo's outbound reverse channel:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx ablo migrate # once: models + ablo_idempotency + ablo_outbox
|
|
14
|
+
npx ablo dev --local
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The command loads `ablo/data-source.ts`, registers the current child branch as
|
|
18
|
+
connector-only, and dials out to Ablo over an authenticated WebSocket. Postgres
|
|
19
|
+
continues listening only on your machine; `DATABASE_URL` never leaves the
|
|
20
|
+
process. This is protocol-scoped, not a general-purpose tunnel: only signed
|
|
21
|
+
Data Source load, list, commit, and event requests traverse it. Use
|
|
22
|
+
`--source <path>` when the handler lives elsewhere.
|
|
23
|
+
|
|
24
|
+
Keep `ablo dev --local` running alongside the application. It pushes schema
|
|
25
|
+
changes and owns the database connector; stopping it deliberately makes the
|
|
26
|
+
branch's database unavailable instead of silently writing somewhere else.
|
|
27
|
+
|
|
28
|
+
### Is this full Ablo?
|
|
29
|
+
|
|
30
|
+
Yes for the Ablo application path: model reads and lists, coordinated writes,
|
|
31
|
+
claims, subscriptions, idempotency, confirmations, and transactional outbox
|
|
32
|
+
settlement all work against localhost Postgres. The browser, server code, and
|
|
33
|
+
agents still connect to Ablo Cloud; only database operations cross the narrow
|
|
34
|
+
signed connector to your machine.
|
|
35
|
+
|
|
36
|
+
It is not logical replication. Visibility depends on how a row is written:
|
|
37
|
+
|
|
38
|
+
| Write origin | Visible to Ablo in localhost mode? | Why |
|
|
39
|
+
|---|---:|---|
|
|
40
|
+
| `ablo.<model>.create/update/delete` | Yes | Ablo coordinates the write, the local adapter commits it with idempotency + outbox, and the outbox event confirms it. |
|
|
41
|
+
| Code using the signed Data Source adapter | Yes | The adapter records the row and authoritative event in one transaction. |
|
|
42
|
+
| A supported source push/outbox integration | Yes | It explicitly publishes the authoritative event to Ablo. |
|
|
43
|
+
| Raw SQL, `psql`, or an unrelated ORM write | No, not automatically | There is no WAL reader in signed-endpoint mode, and bypassing the adapter does not append `ablo_outbox`. |
|
|
44
|
+
|
|
45
|
+
If Ablo must observe every arbitrary SQL/ORM write, use the direct logical-WAL
|
|
46
|
+
path with a network-reachable Postgres endpoint, PrivateLink/peering/VPN, or a
|
|
47
|
+
database-capable secure tunnel. Do not expose Postgres without TLS,
|
|
48
|
+
authentication, and network restrictions.
|
|
49
|
+
|
|
50
|
+
### Local connector errors
|
|
51
|
+
|
|
52
|
+
Every stable code links to the generated [error reference](https://docs.abloatai.com/errors):
|
|
53
|
+
|
|
54
|
+
| Code | Meaning and fix |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `source_connector_not_attached` | The branch is connector-only but no process is attached. Start or restart `ablo dev --local`. |
|
|
57
|
+
| `source_connector_unauthenticated` | The temporary branch key is missing, expired, or rejected. Rerun `ablo dev --local` to mint a fresh key. |
|
|
58
|
+
| `source_connector_requires_secret_key` | The connector received the wrong key kind. Let `ablo dev` supply its branch-bound `sk_` key. |
|
|
59
|
+
| `source_connector_no_source_registered` | No endpoint source exists for this branch. Upgrade/rerun the CLI so registration happens before socket attachment. |
|
|
60
|
+
| `source_connector_localhost_required` | Connector-only registration used a non-local descriptor. Use `ablo dev --local`; deployed handlers use ordinary HTTPS endpoint registration. |
|
|
61
|
+
| `source_connector_timeout` | The handler or local Postgres exceeded the request deadline. Inspect the local process and database. |
|
|
62
|
+
| `source_connector_handler_error` | `ablo/data-source.ts` or its adapter threw. The local terminal contains the underlying error. |
|
|
63
|
+
| `source_connector_protocol_error` | CLI/SDK and service connector protocols disagree. Upgrade the CLI and SDK together. |
|
|
64
|
+
| `source_connector_production_not_enabled` | A root/production key attempted the development connector. Use a supported production route or explicitly enable production reverse-channel support. |
|
|
65
|
+
|
|
66
|
+
Disconnects, service restarts, and connector replacement are retryable. Keep the
|
|
67
|
+
same idempotency key: Ablo never falls back from this branch to hosted storage or
|
|
68
|
+
another database.
|
|
69
|
+
|
|
5
70
|
You write through Ablo, and Ablo writes to your Postgres. A call to
|
|
6
71
|
`ablo.<model>.create / update / delete` enters Ablo's commit chokepoint — where
|
|
7
72
|
claims, ordering, and idempotency are enforced — and Ablo applies the change to
|
|
@@ -36,6 +101,36 @@ npx ablo connect apply --env-file .env.local --yes
|
|
|
36
101
|
The explicit flag makes the credential choice visible and loads both the
|
|
37
102
|
branch-bound key and database URL. Shell environment variables take precedence.
|
|
38
103
|
|
|
104
|
+
### One database, several projects
|
|
105
|
+
|
|
106
|
+
Provider database URLs and Postgres schemas solve different isolation jobs:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
database URL = production, staging, or preview environment
|
|
110
|
+
schema = application/project inside that database
|
|
111
|
+
ABLO_API_KEY = exact Ablo project branch to bind
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It is safe to keep several apps in one production database when each app has its
|
|
115
|
+
own schema:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ABLO_API_KEY="$MAIL_KEY" DATABASE_URL="$PRODUCTION_URL" \
|
|
119
|
+
npx ablo connect apply --schema mail --yes
|
|
120
|
+
|
|
121
|
+
ABLO_API_KEY="$SLIDES_KEY" DATABASE_URL="$PRODUCTION_URL" \
|
|
122
|
+
npx ablo connect apply --schema slides --yes
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For a Neon or Supabase preview branch, use that branch's direct URL and keep the
|
|
126
|
+
schema name stable. Ablo binds one plane to `(database, schema)`: the same
|
|
127
|
+
database may add `billing`, but a second project cannot also claim `mail`.
|
|
128
|
+
Cross-organization conflicts reveal only that the binding is occupied.
|
|
129
|
+
|
|
130
|
+
Push the Ablo schema before connecting, or pass `--tables`. The publication is
|
|
131
|
+
an explicit list of schema-qualified mapped tables; Ablo never uses a
|
|
132
|
+
database-wide `FOR ALL TABLES` publication for this multi-project path.
|
|
133
|
+
|
|
39
134
|
If scoped roles already exist but their passwords are unavailable, do not drop
|
|
40
135
|
them or run `DROP OWNED`. Rotate them in place and re-register the fresh
|
|
41
136
|
credentials:
|
|
@@ -60,11 +155,12 @@ without printing the secret. Retire the old variable after the move.
|
|
|
60
155
|
## Connect in one command
|
|
61
156
|
|
|
62
157
|
```bash
|
|
63
|
-
npx ablo connect apply --url postgres://admin:...@host:5432/db
|
|
158
|
+
npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
|
|
64
159
|
```
|
|
65
160
|
|
|
66
|
-
Pass an admin connection string with `--url` and
|
|
67
|
-
|
|
161
|
+
Pass an admin connection string with `--url` and select the application namespace
|
|
162
|
+
with `--schema` (default `public`). It creates a per-binding publication, two
|
|
163
|
+
per-binding scoped roles, and the grants, turns on logical decoding where it can, registers
|
|
68
164
|
both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
|
|
69
165
|
The admin credential is used on this machine only and never persisted — nothing is
|
|
70
166
|
written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass `--show-sql`
|
|
@@ -78,7 +174,7 @@ to run it by hand or review exactly what changes.
|
|
|
78
174
|
|
|
79
175
|
When Ablo creates the replication slot, it takes a consistent initial snapshot of
|
|
80
176
|
every mapped table in the publication before following new changes. Rows that
|
|
81
|
-
predate `ablo connect` therefore become available to `
|
|
177
|
+
predate `ablo connect` therefore become available to `get`, `list`, and
|
|
82
178
|
reactive `local.*` reads without an application backfill.
|
|
83
179
|
|
|
84
180
|
Run `ablo connect check` before removing an existing HTTP/database read fallback.
|
|
@@ -87,6 +183,24 @@ write a script that updates every row to make it visible: an Ablo update require
|
|
|
87
183
|
the row to be visible already, and touching application rows is neither necessary
|
|
88
184
|
nor a safe bootstrap mechanism.
|
|
89
185
|
|
|
186
|
+
If a connection was snapshotted with an older replication role whose row-level
|
|
187
|
+
security hid historical rows, repair that role and request the load again without
|
|
188
|
+
deregistering or rotating credentials:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx ablo connect rotate # reasserts BYPASSRLS and safely re-registers both roles
|
|
192
|
+
npx ablo connect resnapshot # recreates only the slot; the load is asynchronous
|
|
193
|
+
npx ablo connect check # repeat until the existing-row load is complete
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Use the same `resnapshot` step after adding an existing populated table to the
|
|
197
|
+
publication. Following its future WAL changes is not enough to load rows written
|
|
198
|
+
before publication membership; the snapshot coverage guard therefore refuses to
|
|
199
|
+
record completion when even one mapped table is absent. Relation matching is
|
|
200
|
+
schema-qualified using the DataSource's configured `schema` (default `public`):
|
|
201
|
+
an identically named table in another Postgres schema neither counts as coverage
|
|
202
|
+
nor enters the snapshot or WAL stream for your model.
|
|
203
|
+
|
|
90
204
|
## The setup, step by step
|
|
91
205
|
|
|
92
206
|
### 1. Enable logical decoding
|
|
@@ -112,24 +226,34 @@ npx ablo connect
|
|
|
112
226
|
`ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres.
|
|
113
227
|
Run it against your database as a superuser or the DB owner. It creates:
|
|
114
228
|
|
|
115
|
-
- **A publication** naming the
|
|
116
|
-
|
|
229
|
+
- **A per-binding publication** naming only the schema-qualified mapped tables
|
|
230
|
+
Ablo reads and confirms against. Its suffix is derived from the authenticated
|
|
231
|
+
Ablo plane and is stable across re-runs:
|
|
117
232
|
|
|
118
233
|
```sql
|
|
119
|
-
CREATE PUBLICATION "
|
|
234
|
+
CREATE PUBLICATION "ablo_publication_<suffix>"
|
|
235
|
+
FOR TABLE "mail"."messages", "mail"."threads";
|
|
120
236
|
```
|
|
121
237
|
|
|
122
|
-
|
|
238
|
+
Override the pushed model set with `--tables a,b,c`.
|
|
123
239
|
|
|
124
240
|
- **A replication role:** it streams the WAL and `SELECT`s, nothing more. This is
|
|
125
241
|
the role Ablo reads and confirms through. You choose the password; it never
|
|
126
242
|
passes through Ablo's CLI or servers:
|
|
127
243
|
|
|
128
244
|
```sql
|
|
129
|
-
CREATE ROLE "
|
|
130
|
-
|
|
245
|
+
CREATE ROLE "ablo_replicator_<suffix>" WITH
|
|
246
|
+
NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT
|
|
247
|
+
LOGIN PASSWORD '<password>';
|
|
248
|
+
GRANT SELECT ON TABLE "mail"."messages", "mail"."threads"
|
|
249
|
+
TO "ablo_replicator_<suffix>";
|
|
131
250
|
```
|
|
132
251
|
|
|
252
|
+
`BYPASSRLS` is required because the initial load is an ordinary `SELECT`,
|
|
253
|
+
while logical replication already exposes every row in the publication
|
|
254
|
+
independently of row-level-security policies. Keep this role's `SELECT`
|
|
255
|
+
grants scoped to the published tables; it has no write or DDL privileges.
|
|
256
|
+
|
|
133
257
|
On Amazon RDS the `REPLICATION` attribute is granted, not set directly:
|
|
134
258
|
`GRANT rds_replication TO "ablo_replicator";`.
|
|
135
259
|
|
|
@@ -139,15 +263,23 @@ Run it against your database as a superuser or the DB owner. It creates:
|
|
|
139
263
|
can change rows in your tables; it cannot change your database:
|
|
140
264
|
|
|
141
265
|
```sql
|
|
142
|
-
CREATE ROLE "
|
|
266
|
+
CREATE ROLE "ablo_writer_<suffix>" WITH LOGIN PASSWORD '<write-password>'
|
|
143
267
|
NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;
|
|
144
|
-
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
268
|
+
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
269
|
+
ON TABLE "mail"."messages", "mail"."threads"
|
|
270
|
+
TO "ablo_writer_<suffix>";
|
|
145
271
|
```
|
|
146
272
|
|
|
147
273
|
Rename either role with `--role <name>` / `--write-role <name>`.
|
|
148
274
|
|
|
149
|
-
The
|
|
150
|
-
|
|
275
|
+
The schema-local `ablo_idempotency` ledger lives beside that app's tables. The
|
|
276
|
+
**replication slot** (`ablo_slot_<suffix>`) is created and owned by Ablo's runtime
|
|
277
|
+
when it first subscribes — you don't pre-create it. Registration checks
|
|
278
|
+
`max_replication_slots` first and explains how to free or add capacity.
|
|
279
|
+
|
|
280
|
+
`ablo connect --manual` retains the legacy canonical object names for
|
|
281
|
+
compatibility and is therefore single-binding within a physical database. Use
|
|
282
|
+
`connect apply --schema …` when several projects share that database.
|
|
151
283
|
|
|
152
284
|
### 3. Register the database with Ablo
|
|
153
285
|
|
|
@@ -197,6 +329,8 @@ Your **app** holds only the API key — never a connection string:
|
|
|
197
329
|
```bash
|
|
198
330
|
# .env — server runtime only, never the browser
|
|
199
331
|
ABLO_API_KEY=sk_...
|
|
332
|
+
ABLO_PROJECT_ID=proj_...
|
|
333
|
+
ABLO_BRANCH_ID=br_...
|
|
200
334
|
```
|
|
201
335
|
|
|
202
336
|
```ts
|
|
@@ -206,9 +340,18 @@ import { schema } from './ablo/schema';
|
|
|
206
340
|
export const ablo = Ablo({
|
|
207
341
|
schema,
|
|
208
342
|
apiKey: process.env.ABLO_API_KEY,
|
|
343
|
+
projectId: process.env.ABLO_PROJECT_ID,
|
|
344
|
+
branchId: process.env.ABLO_BRANCH_ID,
|
|
209
345
|
});
|
|
210
346
|
```
|
|
211
347
|
|
|
348
|
+
`ABLO_PROJECT_ID` and `ABLO_BRANCH_ID` are safety assertions, not routing inputs.
|
|
349
|
+
The API key still selects the project and branch; during `ready()` Ablo asks the
|
|
350
|
+
server what the key actually targets and refuses startup when either coordinate
|
|
351
|
+
differs. `ablo dev` writes all three values together, so accidentally exporting
|
|
352
|
+
a slides key into the mail app—or a mail development key into production—fails
|
|
353
|
+
before any read, write, or subscription begins.
|
|
354
|
+
|
|
212
355
|
The Ablo schema describes **only your synced, collaborative models** — the rows
|
|
213
356
|
Ablo coordinates and fans out in realtime. It is _not_ your whole-database schema
|
|
214
357
|
and does _not_ replace your `schema.prisma` (or Drizzle schema). Your auth,
|
|
@@ -227,7 +370,7 @@ landed:
|
|
|
227
370
|
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
|
|
228
371
|
|
|
229
372
|
// Block until your database has it and the WAL echo confirms.
|
|
230
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 }
|
|
373
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
|
|
231
374
|
|
|
232
375
|
// Reads are live off the same stream.
|
|
233
376
|
const report = ablo.weatherReports.local.get('report_stockholm');
|
package/docs/debugging.md
CHANGED
|
@@ -267,7 +267,6 @@ import { AbloError } from '@abloatai/ablo';
|
|
|
267
267
|
try {
|
|
268
268
|
await ablo.documents.create({
|
|
269
269
|
data,
|
|
270
|
-
wait: 'confirmed',
|
|
271
270
|
});
|
|
272
271
|
} catch (error) {
|
|
273
272
|
if (error instanceof AbloError) {
|
|
@@ -276,10 +275,10 @@ try {
|
|
|
276
275
|
}
|
|
277
276
|
```
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
278
|
+
An awaited model write rejects with that complete typed error.
|
|
279
|
+
`onMutationFailure` remains the notification channel for deliberately
|
|
280
|
+
unawaited optimistic writes; it is not required to recover details from an
|
|
281
|
+
awaited write.
|
|
283
282
|
|
|
284
283
|
### Local reads versus a confirmed server read
|
|
285
284
|
|
|
@@ -75,7 +75,6 @@ export async function markDone(taskId: string) {
|
|
|
75
75
|
// ablo.tasks.update({
|
|
76
76
|
// id: claim.data.id,
|
|
77
77
|
// data: { status: 'done' },
|
|
78
|
-
// wait: 'confirmed',
|
|
79
78
|
// readAt: <claim snapshot version>,
|
|
80
79
|
// onStale: 'reject',
|
|
81
80
|
// });
|
|
@@ -86,7 +85,6 @@ export async function markDone(taskId: string) {
|
|
|
86
85
|
const updated = await ablo.tasks.update({
|
|
87
86
|
id: claim.data.id,
|
|
88
87
|
data: { status: 'done' },
|
|
89
|
-
wait: 'confirmed',
|
|
90
88
|
});
|
|
91
89
|
|
|
92
90
|
return { status: 'done', task: updated };
|
|
@@ -255,7 +255,6 @@ await ablo.weatherReports.update({
|
|
|
255
255
|
data: { status: 'ready' },
|
|
256
256
|
readAt: snap.stamp,
|
|
257
257
|
onStale: 'reject',
|
|
258
|
-
wait: 'confirmed',
|
|
259
258
|
});
|
|
260
259
|
```
|
|
261
260
|
|
|
@@ -294,7 +293,6 @@ await ablo.weatherReports.update({
|
|
|
294
293
|
data: { status: 'ready' },
|
|
295
294
|
readAt: snap.stamp,
|
|
296
295
|
onStale: 'reject',
|
|
297
|
-
wait: 'confirmed',
|
|
298
296
|
});
|
|
299
297
|
```
|
|
300
298
|
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -23,7 +23,7 @@ app/
|
|
|
23
23
|
route.ts # mints a per-user ek_ token for the browser
|
|
24
24
|
tasks/
|
|
25
25
|
[id]/
|
|
26
|
-
page.tsx # RSC:
|
|
26
|
+
page.tsx # RSC: get + render
|
|
27
27
|
actions.ts # Server Action: claim, then write
|
|
28
28
|
TaskEditor.tsx # Client: live updates
|
|
29
29
|
lib/
|
|
@@ -168,7 +168,6 @@ export async function markDone(id: string) {
|
|
|
168
168
|
id,
|
|
169
169
|
data: { status: 'done' },
|
|
170
170
|
claim,
|
|
171
|
-
wait: 'confirmed',
|
|
172
171
|
});
|
|
173
172
|
|
|
174
173
|
return { status: 'done', task };
|
|
@@ -69,7 +69,6 @@ export async function completeTask(taskId: string, workerId: string) {
|
|
|
69
69
|
const updated = await ablo.tasks.update({
|
|
70
70
|
id: claim.data.id,
|
|
71
71
|
data: { status: 'done' },
|
|
72
|
-
wait: 'confirmed',
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
return { status: 'done', task: updated };
|
|
@@ -79,8 +78,8 @@ export async function completeTask(taskId: string, workerId: string) {
|
|
|
79
78
|
|
|
80
79
|
`get({ id })` is an async server read — it hits the server and returns the
|
|
81
80
|
row (or `undefined`, which the early `not_found` guard handles). The update runs
|
|
82
|
-
while the claim is held
|
|
83
|
-
|
|
81
|
+
while the claim is held; awaiting it resolves only once your database has
|
|
82
|
+
confirmed the row landed.
|
|
84
83
|
|
|
85
84
|
The two options on the claim:
|
|
86
85
|
|
package/docs/groups.md
CHANGED
|
@@ -266,7 +266,7 @@ try {
|
|
|
266
266
|
await ablo.tasks.update({ id, data });
|
|
267
267
|
} catch (err) {
|
|
268
268
|
if (err.code === 'stale_context') {
|
|
269
|
-
const fresh = await ablo.documents.
|
|
269
|
+
const fresh = await ablo.documents.get({ id: 's-1' }); // read
|
|
270
270
|
await ablo.documents.track({ id: 's-1', onStale: 'reject' }); // acknowledge
|
|
271
271
|
await ablo.tasks.update({ id, data: reconsider(fresh) }); // now it lands
|
|
272
272
|
}
|
package/docs/guarantees.md
CHANGED
|
@@ -14,14 +14,13 @@ of clobbering.
|
|
|
14
14
|
|
|
15
15
|
## Confirmed Writes
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
the
|
|
17
|
+
Awaiting a schema model write resolves only after authoritative confirmation
|
|
18
|
+
and returns the updated row.
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
const updated = await ablo.weatherReports.update({
|
|
22
22
|
id: 'report_stockholm',
|
|
23
23
|
data: { status: 'ready' },
|
|
24
|
-
wait: 'confirmed',
|
|
25
24
|
});
|
|
26
25
|
```
|
|
27
26
|
|
|
@@ -37,9 +36,8 @@ Schema model writes return the updated model row.
|
|
|
37
36
|
Schema model writes update local state optimistically. This keeps UI and agent
|
|
38
37
|
tools responsive while the commit is sent to the server.
|
|
39
38
|
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
- With `wait: 'confirmed'`, the promise waits for server confirmation.
|
|
39
|
+
- The local model changes immediately, before the promise settles.
|
|
40
|
+
- The promise always waits for authoritative confirmation.
|
|
43
41
|
- If the server rejects the write, the SDK rolls back the optimistic change and
|
|
44
42
|
raises a typed error.
|
|
45
43
|
|
|
@@ -58,7 +56,6 @@ await ablo.weatherReports.update({
|
|
|
58
56
|
data: { status: 'ready' },
|
|
59
57
|
readAt: snap.stamp,
|
|
60
58
|
onStale: 'reject',
|
|
61
|
-
wait: 'confirmed',
|
|
62
59
|
});
|
|
63
60
|
```
|
|
64
61
|
|
package/docs/how-it-works.md
CHANGED
|
@@ -7,7 +7,7 @@ whole model — everything below explains what it means and how to use it.
|
|
|
7
7
|
|
|
8
8
|
```ts
|
|
9
9
|
// You call Ablo. Ablo lands the change in your database and confirms it.
|
|
10
|
-
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }
|
|
10
|
+
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
|
|
11
11
|
|
|
12
12
|
// Reads come back live, kept current from your database.
|
|
13
13
|
const task = ablo.tasks.local.get('task_42');
|
|
@@ -42,10 +42,10 @@ echo confirms it → everyone connected sees it live.**
|
|
|
42
42
|
| Primitive | Plane | Purpose |
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
45
|
-
| `Model` | State | The generated `ablo.<model>` model. Use `
|
|
45
|
+
| `Model` | State | The generated `ablo.<model>` model. Use `get`/`list` (async reads), `local.get`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
|
|
46
46
|
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
|
|
47
47
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
48
|
-
| `Receipt` | Protocol | The lower-level durable result for custom runtimes.
|
|
48
|
+
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Awaiting a schema write waits for confirmation. |
|
|
49
49
|
|
|
50
50
|
### Why each primitive is separate
|
|
51
51
|
|
|
@@ -102,9 +102,9 @@ import Ablo from '@abloatai/ablo';
|
|
|
102
102
|
import { schema } from './ablo/schema';
|
|
103
103
|
export const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
104
104
|
|
|
105
|
-
// 5. Write through Ablo.
|
|
106
|
-
//
|
|
107
|
-
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }
|
|
105
|
+
// 5. Write through Ablo. Local state changes immediately; await blocks until
|
|
106
|
+
// the authoritative feed proves the row is there.
|
|
107
|
+
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
|
|
108
108
|
|
|
109
109
|
// 6. Read — live, no fetch loop.
|
|
110
110
|
const task = ablo.tasks.local.get('task_42');
|
package/docs/idempotency.md
CHANGED
package/docs/identity.md
CHANGED
|
@@ -517,7 +517,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
|
|
|
517
517
|
const ablo = Ablo({ schema, apiKey: session.token });
|
|
518
518
|
```
|
|
519
519
|
|
|
520
|
-
2. **Automatically, on read or claim.** Reading a row (`
|
|
520
|
+
2. **Automatically, on read or claim.** Reading a row (`get`/
|
|
521
521
|
`claim.state`) auto-enrolls the client in that row's entity group
|
|
522
522
|
(**read-interest**), and `claim`-ing it pins a **write-intent** subscription.
|
|
523
523
|
So an agent's reachable set **accretes** as it works — no extra subscribe call.
|
package/docs/index.md
CHANGED
|
@@ -14,7 +14,6 @@ await using claim = await ablo.reports.claim({ id: reportId });
|
|
|
14
14
|
await ablo.reports.update({
|
|
15
15
|
id: claim.data.id,
|
|
16
16
|
data: { forecast: await generateForecast(claim.data) },
|
|
17
|
-
wait: 'confirmed',
|
|
18
17
|
});
|
|
19
18
|
```
|
|
20
19
|
|
|
@@ -90,7 +89,7 @@ based on a row that has since changed is turned away rather than applied.
|
|
|
90
89
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http' });
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
Read with `list` / `
|
|
92
|
+
Read with `list` / `get`, coordinate with `claim`, write with `create` / `update` /
|
|
94
93
|
`delete`. See [Agents](./agents.md) for the loop and [API Reference](./api.md) for the shape.
|
|
95
94
|
</Step>
|
|
96
95
|
|
|
@@ -77,8 +77,7 @@ When handing this to a coding agent, give it a concrete target:
|
|
|
77
77
|
```txt
|
|
78
78
|
Add Ablo to this app for one model your agents edit.
|
|
79
79
|
Run npx ablo dev and use its branch-bound key. Declare schema, add the Ablo client, replace
|
|
80
|
-
one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
|
|
81
|
-
wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
80
|
+
one write with ablo.<model>.update(..., { readAt, onStale: 'reject' }), and add a smoke test for two concurrent writers.
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
## 1. Declare A Schema
|
|
@@ -261,9 +260,9 @@ Reads come in two flavors, and you pick based on whether you can wait.
|
|
|
261
260
|
store) — they're async, so you `await` them. `local.get(id)`,
|
|
262
261
|
`local.list({ where })`, and `local.count({ where })` read the already-synced local
|
|
263
262
|
graph synchronously, so they're the ones you call in render — and the ones you
|
|
264
|
-
use inside a `useAblo` selector, never the async `
|
|
263
|
+
use inside a `useAblo` selector, never the async `get`/`list`.
|
|
265
264
|
|
|
266
|
-
Use `
|
|
265
|
+
Use `get` when the row may not be local yet — it fetches from the server
|
|
267
266
|
and waits.
|
|
268
267
|
|
|
269
268
|
```ts
|
|
@@ -316,7 +315,7 @@ const ablo = useAblo();
|
|
|
316
315
|
For simple writes:
|
|
317
316
|
|
|
318
317
|
```ts
|
|
319
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }
|
|
318
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
320
319
|
```
|
|
321
320
|
|
|
322
321
|
For writes based on state the user or agent already read, snapshot first and
|
|
@@ -330,12 +329,12 @@ await ablo.weatherReports.update({
|
|
|
330
329
|
data: { status: 'ready' },
|
|
331
330
|
readAt: snap.stamp,
|
|
332
331
|
onStale: 'reject',
|
|
333
|
-
wait: 'confirmed',
|
|
334
332
|
});
|
|
335
333
|
```
|
|
336
334
|
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
The local row changes optimistically at once. Awaiting the model write waits for
|
|
336
|
+
authoritative confirmation; a rejection rolls the optimistic state back and
|
|
337
|
+
throws a typed `AbloError`.
|
|
339
338
|
|
|
340
339
|
## 5. Multiplayer Is Automatic
|
|
341
340
|
|
|
@@ -468,7 +467,6 @@ if (!claimed) return;
|
|
|
468
467
|
await ablo.weatherReports.update({
|
|
469
468
|
id: claimed.id,
|
|
470
469
|
data: { status: 'ready', forecast: await getForecast(claimed) },
|
|
471
|
-
wait: 'confirmed',
|
|
472
470
|
});
|
|
473
471
|
```
|
|
474
472
|
|
|
@@ -489,7 +487,6 @@ const completeReport = tool({
|
|
|
489
487
|
data: { status: 'ready', forecast },
|
|
490
488
|
readAt: snap.stamp,
|
|
491
489
|
onStale: 'reject',
|
|
492
|
-
wait: 'confirmed',
|
|
493
490
|
});
|
|
494
491
|
},
|
|
495
492
|
});
|
|
@@ -58,7 +58,6 @@ export const approveOrder = inngest.createFunction(
|
|
|
58
58
|
id: orderId,
|
|
59
59
|
data: { status: 'approved', approvalNote },
|
|
60
60
|
idempotencyKey: `${operationId}:approve-order:${orderId}`,
|
|
61
|
-
wait: 'confirmed',
|
|
62
61
|
}),
|
|
63
62
|
);
|
|
64
63
|
},
|
|
@@ -99,7 +98,7 @@ For every mutating step:
|
|
|
99
98
|
1. Accept a stable business operation ID in the triggering event.
|
|
100
99
|
2. Derive a separate key for each logical effect.
|
|
101
100
|
3. Reuse the key and mutation body for every retry.
|
|
102
|
-
4.
|
|
101
|
+
4. Await every model write before a later step depends on its authoritative result.
|
|
103
102
|
5. Treat reuse of a key with another body as an application bug.
|
|
104
103
|
6. Keep the complete retry horizon within Ablo's documented
|
|
105
104
|
[idempotency retention window](../idempotency.md).
|
|
@@ -133,7 +132,6 @@ await step.run('apply-order-review', () =>
|
|
|
133
132
|
id: orderId,
|
|
134
133
|
data: { status: 'approved', approvalNote },
|
|
135
134
|
idempotencyKey: `${operationId}:apply-order-review:${orderId}`,
|
|
136
|
-
wait: 'confirmed',
|
|
137
135
|
}),
|
|
138
136
|
);
|
|
139
137
|
```
|
|
@@ -167,7 +165,6 @@ await step.run('apply-review-with-claim', async () => {
|
|
|
167
165
|
data: { status: 'approved', approvalNote },
|
|
168
166
|
claim,
|
|
169
167
|
idempotencyKey: `${operationId}:apply-review:${orderId}`,
|
|
170
|
-
wait: 'confirmed',
|
|
171
168
|
});
|
|
172
169
|
} finally {
|
|
173
170
|
await claim.release();
|
|
@@ -46,7 +46,6 @@ export async function approveOrder(input: ApproveOrderInput) {
|
|
|
46
46
|
approvalNote: input.approvalNote,
|
|
47
47
|
},
|
|
48
48
|
idempotencyKey: input.idempotencyKey,
|
|
49
|
-
wait: 'confirmed',
|
|
50
49
|
});
|
|
51
50
|
}
|
|
52
51
|
```
|
|
@@ -135,7 +134,7 @@ For every mutating Activity:
|
|
|
135
134
|
1. Create the key in deterministic Workflow code or accept a stable operation
|
|
136
135
|
ID in the Workflow input.
|
|
137
136
|
2. Reuse the key and mutation body for every retry of the same logical effect.
|
|
138
|
-
3.
|
|
137
|
+
3. Await each model write before later Workflow steps depend on the
|
|
139
138
|
authoritative database result.
|
|
140
139
|
4. Treat reuse of a key with a different body as an application bug.
|
|
141
140
|
5. Keep the Temporal retry horizon within Ablo's documented
|
|
@@ -165,7 +164,6 @@ export async function applyReview(orderId: string, note: string) {
|
|
|
165
164
|
id: claim.data.id,
|
|
166
165
|
data: { approvalNote: note },
|
|
167
166
|
claim,
|
|
168
|
-
wait: 'confirmed',
|
|
169
167
|
});
|
|
170
168
|
}
|
|
171
169
|
```
|
|
@@ -69,7 +69,7 @@ FNV-1a content hash used for connect-time gating. Round-trip tested in
|
|
|
69
69
|
error, createdBy, createdAt, activatedAt)`, unique `(orgId, version)`. State
|
|
70
70
|
`pending|validated|active|overwritten|failed`, ≤1 active per tenant (Convex
|
|
71
71
|
`_schemas` machine; Zero's "row in the operational DB"). *Migration written,
|
|
72
|
-
not applied — 0 users
|
|
72
|
+
not applied — 0 users at the time.*
|
|
73
73
|
- **`pgSchemaStore` / `memorySchemaStore`** (`schemaStore.ts`) — mirrors
|
|
74
74
|
`pgApiKeyStore`. `insertPending` assigns `MAX(version)+1`; `activate` is a
|
|
75
75
|
transaction that demotes the current active → `overwritten` then promotes the
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Repository Structure
|
|
2
2
|
|
|
3
|
+
For a verb-by-verb guide to declarations and implementations—including exactly
|
|
4
|
+
where `create`, `update`, `delete`, and `claim` live—read the public
|
|
5
|
+
[`CODEMAP.md`](../../CODEMAP.md).
|
|
6
|
+
|
|
3
7
|
The public repository preserves the same ownership boundaries as the main
|
|
4
8
|
monorepo. `@abloatai/ablo` is the product package; the packages beneath it are
|
|
5
9
|
implementation owners and first-party extension surfaces.
|
package/docs/migration.md
CHANGED
|
@@ -293,7 +293,7 @@ helper, and the agent/task type family (`Agent`, `AgentOptions`,
|
|
|
293
293
|
+ const { token } = await server.sessions.create({ agent: { id: agentId } });
|
|
294
294
|
+ const agent = Ablo({ schema, apiKey: token });
|
|
295
295
|
+ await using claim = await agent.tasks.claim({ id });
|
|
296
|
-
+ await agent.tasks.update({ id, data: { status: 'done' }
|
|
296
|
+
+ await agent.tasks.update({ id, data: { status: 'done' } });
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
Per-run token/cost now lives in Langfuse, not an `agent_tasks` table. The only
|
|
@@ -324,10 +324,10 @@ modifier are named siblings. Reactive local reads stay on the synchronous
|
|
|
324
324
|
|
|
325
325
|
```diff
|
|
326
326
|
- await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
|
|
327
|
-
+ await ablo.tasks.update({ id, data: { status: 'done' }
|
|
327
|
+
+ await ablo.tasks.update({ id, data: { status: 'done' } })
|
|
328
328
|
|
|
329
329
|
- await ablo.tasks.retrieve(id)
|
|
330
|
-
+ await ablo.tasks.
|
|
330
|
+
+ await ablo.tasks.get({ id })
|
|
331
331
|
|
|
332
332
|
- useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
|
|
333
333
|
+ useAblo((ablo) => ablo.tasks.local.get(id)) ?? serverTask
|