@abloatai/ablo 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/AGENTS.md +10 -4
  2. package/CHANGELOG.md +428 -10
  3. package/LICENSE +1 -1
  4. package/NOTICE +3 -3
  5. package/README.md +2 -1
  6. package/dist/ai-sdk.d.ts +1 -1
  7. package/dist/ai-sdk.d.ts.map +1 -1
  8. package/dist/context/evidence.d.ts +6 -8
  9. package/dist/context/evidence.d.ts.map +1 -1
  10. package/dist/context/evidence.js +6 -20
  11. package/dist/context/evidence.js.map +1 -1
  12. package/dist/context/index.d.ts +23 -0
  13. package/dist/context/index.d.ts.map +1 -0
  14. package/dist/context/index.js +26 -0
  15. package/dist/context/index.js.map +1 -0
  16. package/dist/context/onChange.d.ts +9 -0
  17. package/dist/context/onChange.d.ts.map +1 -0
  18. package/dist/context/onChange.js +37 -0
  19. package/dist/context/onChange.js.map +1 -0
  20. package/dist/source-conformance.d.ts +1 -1
  21. package/dist/source-conformance.d.ts.map +1 -1
  22. package/dist/source-conformance.js +1 -1
  23. package/dist/source-conformance.js.map +1 -1
  24. package/dist/source-drizzle.d.ts +1 -1
  25. package/dist/source-drizzle.d.ts.map +1 -1
  26. package/dist/source-drizzle.js +1 -1
  27. package/dist/source-drizzle.js.map +1 -1
  28. package/dist/source-kysely.d.ts +1 -1
  29. package/dist/source-kysely.d.ts.map +1 -1
  30. package/dist/source-kysely.js +1 -1
  31. package/dist/source-kysely.js.map +1 -1
  32. package/dist/source-next.d.ts +1 -1
  33. package/dist/source-next.d.ts.map +1 -1
  34. package/dist/source-next.js +1 -1
  35. package/dist/source-next.js.map +1 -1
  36. package/docs/agent-integration-decision-guide.md +123 -0
  37. package/docs/agents.md +74 -13
  38. package/docs/api-keys.md +6 -6
  39. package/docs/api.md +117 -43
  40. package/docs/branch-development.md +23 -4
  41. package/docs/cli.md +16 -9
  42. package/docs/client-behavior.md +21 -15
  43. package/docs/concurrency-convention.md +67 -77
  44. package/docs/context.md +56 -31
  45. package/docs/coordination.md +115 -36
  46. package/docs/customer-organizations.md +49 -31
  47. package/docs/data-sources.md +12 -6
  48. package/docs/debugging.md +1 -1
  49. package/docs/examples/agent-human.md +6 -18
  50. package/docs/examples/coordination-conformance.md +69 -0
  51. package/docs/examples/existing-document-pipeline.md +488 -0
  52. package/docs/examples/existing-python-backend.md +10 -13
  53. package/docs/examples/nextjs.md +49 -6
  54. package/docs/examples/scoped-agent.md +18 -1
  55. package/docs/examples/server-agent.md +2 -2
  56. package/docs/groups.md +19 -139
  57. package/docs/guarantees.md +5 -6
  58. package/docs/identity.md +2 -1
  59. package/docs/index.md +5 -0
  60. package/docs/integration-guide.md +46 -19
  61. package/docs/integrations/sandbox-runtime.md +148 -0
  62. package/docs/integrations.md +9 -0
  63. package/docs/operating-on-your-database.md +7 -0
  64. package/docs/quickstart.md +19 -13
  65. package/docs/react.md +9 -9
  66. package/docs/schema-contract.md +14 -13
  67. package/docs/session-settings.md +9 -0
  68. package/docs/sessions.md +1 -1
  69. package/examples/README.md +2 -2
  70. package/examples/agent-turn.ts +1 -1
  71. package/examples/data-source/customer-server.ts +12 -5
  72. package/examples/expensive-agent-turn.ts +1 -1
  73. package/llms.txt +72 -10
  74. package/package.json +6 -6
  75. package/dist/context/sources.d.ts +0 -21
  76. package/dist/context/sources.d.ts.map +0 -1
  77. package/dist/context/sources.js +0 -36
  78. package/dist/context/sources.js.map +0 -1
  79. package/dist/context.d.ts +0 -22
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -33
  82. package/dist/context.js.map +0 -1
package/AGENTS.md CHANGED
@@ -6,6 +6,12 @@ Claims don't lock. If another writer holds the row, `claim` waits for them and r
6
6
 
7
7
  ## Start here — scaffold with `ablo init`
8
8
 
9
+ Before choosing among identifier claims, row claims, captured reads, atomic
10
+ commits, existing database writes, and Ablo-routed writes, use the
11
+ [Agent Integration Decision Guide](./docs/agent-integration-decision-guide.md).
12
+ It routes existing applications to the smallest relevant example and names the
13
+ test layer that proves each guarantee.
14
+
9
15
  Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, the database connection (logical replication by default, or a signed Data Source endpoint as the fallback), and (for Next.js) the browser provider + session route:
10
16
 
11
17
  - **Read the docs for THIS version:** `npx ablo docs` lists every page, `npx ablo docs <page>` prints one. They ship inside the installed package, so they describe the code in `node_modules` and work with no network. Read them instead of a docs URL — a website describes the newest release, so against a pinned version it will hand you a call your package doesn't have (`retrieve`/`list` replaced `get`/`getAll`/`getCount` in 0.35.0).
@@ -21,7 +27,7 @@ Don't hand-write the integration. Run the CLI; it generates the current-API sche
21
27
 
22
28
  When you use the signed-endpoint fallback, the generated `ablo/data-source.ts` is the whole endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
23
29
 
24
- **Working on a real database?** Plain model writes are last-write-wins when no active claim applies. Use a functional update, a held claim, or `readAt` when a result depends on an earlier value. Reads are safe to inspect; raw application DDL (`ALTER TABLE …`) and a `--yes` connection cutover belong to a human. When you're unsure whether a write fits, `npx ablo check` reports the live column-by-column fit read-only, before anything runs. Full sorting rule: [Operating on Your Database](./docs/operating-on-your-database.md).
30
+ **Working on a real database?** Plain model writes are last-write-wins when no active claim applies. Use a functional update, a held claim, or `read` plus `reads` when a result depends on an earlier value. Reads are safe to inspect; raw application DDL (`ALTER TABLE …`) and a `--yes` connection cutover belong to a human. When you're unsure whether a write fits, `npx ablo check` reports the live column-by-column fit read-only, before anything runs. Full sorting rule: [Operating on Your Database](./docs/operating-on-your-database.md).
25
31
 
26
32
  ## Rule
27
33
 
@@ -35,10 +41,10 @@ The schema is the integration contract — it drives typed model clients, React
35
41
 
36
42
  Every model verb takes ONE options object. The common loop:
37
43
 
38
- 1. **Read** the row — `await ablo.<model>.get({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.local.get(id))`.
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)`.
39
45
  2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
40
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`).
41
- 4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
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.
42
48
 
43
49
  Keep coding assistants on this schema-backed path.
44
50
 
@@ -58,7 +64,7 @@ const schema = defineSchema({
58
64
 
59
65
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
60
66
 
61
- const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
67
+ const report = await ablo.weatherReports.read({ id: 'report_stockholm' });
62
68
  if (!report) throw new Error('Report not found');
63
69
 
64
70
  // If someone else holds the row, claim waits for them and re-reads the fresh
package/CHANGELOG.md CHANGED
@@ -1,19 +1,437 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.58.0
4
+
5
+ ### Reads now distinguish observation from decision input
6
+
7
+ `get({ id })` returns the current row for display or inspection. `read({ id })`
8
+ returns the same row while privately retaining its model, id, and watermark so a
9
+ later write can prove exactly which state it depended on:
10
+
11
+ ```ts
12
+ const report = await ablo.reports.read({ id: reportId });
13
+
14
+ await ablo.reports.update({
15
+ id: report.id,
16
+ data: { summary },
17
+ reads: [report],
18
+ });
19
+ ```
20
+
21
+ If `report` changed in between, the update does not land and rejects with
22
+ `AbloStaleContextError`. `get()` and `list()` remain observational, while
23
+ `onChange()` remains the live notification surface.
24
+
25
+ This replaces the older `retrieve()` and durable `track()` model surfaces. It is
26
+ a public-surface break with no compatibility aliases:
27
+
28
+ - replace `retrieve({ id })` with `get({ id })` for observation, or `read({ id })`
29
+ when a later mutation depends on the row;
30
+ - replace `track(...)`, `CommitContext.track`, and mutation `track` / `onStale`
31
+ options with captured rows passed through the mutation's `reads` option; and
32
+ - replace AI SDK `ToolModel.get` with `ToolModel.read`.
33
+
34
+ Schema conflict-policy configuration and the `agents*`, `humans*`, and
35
+ `system*` policy constants are also removed, together with the supporting
36
+ conflict, stale-notification, persisted-read-set, and internal read-set exports.
37
+ Use an active claim when work must exclude another participant. Use `read()` and
38
+ `reads` when work may run concurrently but must not commit from a stale premise.
39
+
40
+ The removals announced by earlier releases now take effect too:
41
+ `SourceRequestContext.requiredSyncGroups` is gone in favor of `syncGroups`, and
42
+ `DeltaPosition`, `deltaPositionSchema`, `ReadSetWatermark`, and
43
+ `readSetWatermarkSchema` are gone in favor of `LogPosition` and
44
+ `logPositionSchema`.
45
+
46
+ ### Context follows exact reads and can stop stale work early
47
+
48
+ `context()` now assembles ordinary application values and exact Ablo reads into
49
+ one result with `data`, `reads`, and `onChange`. It no longer publishes a
50
+ context-level cursor or source classification. `ContextResult.cursor`,
51
+ `ContextResult.sources`, `ContextChange`, `ContextSource`, and
52
+ `contextSourceSchema` leave with that older model.
53
+
54
+ The reactive client's cache-based `snapshot()` operation and `Snapshot` type are
55
+ removed. Call `read()` for every row an action depends on, assemble those values
56
+ with `context()`, and pass `ctx.reads` to the final write:
57
+
58
+ ```ts
59
+ const ctx = await context({
60
+ ablo,
61
+ data: {
62
+ report: ablo.reports.read({ id: reportId }),
63
+ documents: searchDocuments(reportId),
64
+ },
65
+ });
66
+
67
+ const stop = ctx.onChange((error) => controller.abort(error));
68
+
69
+ try {
70
+ const summary = await generateSummary(ctx.data, controller.signal);
71
+ await ablo.reports.update({
72
+ id: reportId,
73
+ data: { summary },
74
+ reads: ctx.reads,
75
+ });
76
+ } finally {
77
+ stop();
78
+ }
79
+ ```
80
+
81
+ `onChange` calls its listeners once when any captured row moves, which lets an
82
+ expensive model call stop early. The final write must still receive
83
+ `reads: ctx.reads`; that server-side check stays authoritative if notification
84
+ races the write or the connection drops.
85
+
86
+ Atomic `commits.create()` batches accept the same captured rows on both the
87
+ stateless HTTP and reactive WebSocket clients. Typed model claim handles can be
88
+ passed directly as a batch's `claim`, without losing their row type. Disposing a
89
+ reactive client now also disposes its mutation queue and commit-lane timers.
90
+
91
+ ### Claims are available from the shell
92
+
93
+ The CLI can now acquire, queue for, inspect, heartbeat, and release a row lease:
94
+
95
+ ```bash
96
+ npx ablo claims acquire reports report_123 --queue -- npm run reconcile
97
+ npx ablo claims list reports report_123
98
+ npx ablo claims release reports report_123
99
+ ```
100
+
101
+ The `-- <command>` form keeps the lease alive only while the child process runs
102
+ and releases it on success, failure, or interruption. This lets an operator or
103
+ coding agent participate in the same coordination boundary as SDK clients
104
+ without first adding application code.
105
+
106
+ ### An agent run can carry the person who started it
107
+
108
+ An agent session can now name the person it acts for. Pass `onBehalfOf` when you
109
+ mint, and every write that agent makes records both principals: the agent as the
110
+ actor, that user as the delegator.
111
+
112
+ ```ts
113
+ const { token } = await server.sessions.create({
114
+ agent: { id: agentId },
115
+ onBehalfOf: { user: { id: requestingUserId } },
116
+ can: { records: ['read', 'update'] },
117
+ syncGroups: [syncGroup('workspace', workspaceId)],
118
+ });
119
+ ```
120
+
121
+ Until now the delegation chain root came from whoever called the mint. A browser
122
+ session minting for its own user recorded that person; a backend minting with a
123
+ secret key recorded nobody. Background work is the second case, so a queued
124
+ job's writes arrived attributed to the agent alone, or to `system` where the
125
+ worker wrote around Ablo entirely. Ablo never sees your user directory, so it
126
+ cannot recover that identity afterwards.
127
+
128
+ The rule is about who may attest, not who may ask. A secret key already carries
129
+ organization authority, so it may name any user id, which is what lets a worker
130
+ resume a job somebody else started. A human session may name only its own user,
131
+ and naming another is refused. `onBehalfOf` on a capability that is not an agent
132
+ is refused as well.
133
+
134
+ Two habits make this hold in practice. Persist the user id on the job before you
135
+ enqueue it, because request context and in-memory arguments do not survive a
136
+ retry or a process boundary, and a durable job without its delegator can only
137
+ produce agent-only attribution. And do not quietly fall back to writing straight
138
+ to the database when minting is unavailable. Those writes are still observed,
139
+ but they carry no trusted correlation and are recorded as `system`.
140
+
141
+ Omit the field and sessions mint exactly as they did before.
142
+
143
+ ### The CLI has one credential input and a clearer project boundary
144
+
145
+ `ABLO_API_KEY` is now the CLI's single explicit credential input. Management,
146
+ branch-runtime, and restricted-agent authority follow from the credential kind
147
+ and its server-side grant; `ABLO_MANAGEMENT_KEY` is no longer a separate input.
148
+ Keep management credentials at the control-plane boundary and pass only a
149
+ delegated, per-run `rk_` credential into a sandbox or agent runtime.
150
+
151
+ After browser approval, `ablo login` now lets an organization with multiple
152
+ projects choose the project in the terminal. `ablo login --project <slug>` skips
153
+ the picker. Credentials remain fixed to the project that minted them, and the
154
+ CLI refuses to silently use another project's stored key.
155
+
156
+ Database connection setup now handles more provider-specific PostgreSQL role
157
+ constraints, repairs required role inheritance, registers local connectors
158
+ through the control-plane boundary, and reports a missing Data Source API key as
159
+ an authentication problem with a concrete fix. A branch still needs a database
160
+ connected before its schema can be pushed.
161
+
162
+ ### Hosted test branches are explicit and temporary
163
+
164
+ Live integration fixtures can create an expiring `test` branch backed by Ablo's
165
+ hosted log storage. Hosted storage must be requested explicitly and the branch
166
+ must expire within 24 hours. Ordinary customer branches remain unbound until
167
+ their own database is connected, so a test convenience cannot silently choose
168
+ where customer data lives.
169
+
170
+ The documentation now includes a coordination-conformance fixture, an
171
+ existing-document evidence pipeline, an existing Python backend path, a
172
+ GraphQL.js approach, and a sandbox-runtime integration guide. These examples
173
+ separate the guarantees Ablo enforces from application, provider, database, and
174
+ deployment responsibilities.
175
+
176
+ ### Pricing and plan limits moved
177
+
178
+ The rate card, the included allowances, and the plan ceilings all changed in
179
+ this release. The canonical page is published at
180
+ https://docs.abloatai.com/pricing and needs no sign-in.
181
+
182
+ ## 0.57.0
183
+
184
+ ### The endpoint outbox upgrades without a drain
185
+
186
+ Endpoint events now have an explicit envelope version. Existing rows and writes
187
+ from an older endpoint are version 1 and pass through the preserved pre-subject
188
+ routing decoder. New adapters write version 2, capturing `sync_groups` in the
189
+ same transaction as the row change. The database constraint requires every
190
+ version-2 event to carry those immutable routes.
191
+
192
+ Old and new endpoint versions may run together during rollout without stalling
193
+ the feed. A page served by an old reader necessarily uses version-1 semantics,
194
+ even when a new writer created the row, because that reader does not select the
195
+ new routing columns. Version-2 routing is therefore universal once every
196
+ endpoint reader has upgraded. There is still no pre-upgrade drain, write pause,
197
+ cursor inspection, or manual deletion.
198
+
199
+ Ablo sends `cursor` as the read position and `acknowledgedThrough` separately
200
+ after the event and consumer position are durable. Built-in adapters use that
201
+ explicit acknowledgement for bounded cleanup; custom event handlers may do the
202
+ same.
203
+
204
+ Version 1 remains a deliberately named compatibility decoder, not a
205
+ NULL value silently interpreted as an empty audience. Replication connections
206
+ have no endpoint outbox and require no action.
207
+
208
+ ### A row is authorized by the subject its schema declares
209
+
210
+ A model may now declare which field decides who a row belongs to, and that rule
211
+ is enforced on every path: reads, writes, claims, presence, and every storage
212
+ adapter, including the endpoint ones. A row is authorized exactly when the
213
+ request carries the sync group `${group}:${row[field]}`.
214
+
215
+ This is what 0.56.0's boundary change was heading towards. Sync groups routed
216
+ delivery and did not decide authorization, so a model that used them as though
217
+ they did was relying on something the guide told you not to rely on. A declared
218
+ subject is that rule made real, checked in one place and failing closed.
219
+
220
+ Two consequences worth knowing. A subject-scoped model stamps exactly one group
221
+ on a change, because delivery matching is OR-based and a second group would
222
+ widen the audience rather than narrow it. And a tombstone now reaches only the
223
+ row's authorized subject group, where before a delete could be announced more
224
+ widely than the row ever was.
225
+
226
+ A schema that routes by sync group without a matching row-access policy is
227
+ flagged. If the routing really is only routing, acknowledge it explicitly and
228
+ the flag goes quiet.
229
+
230
+ ### Creating many rows is one commit
231
+
232
+ `create` takes a list as well as a single row, under the same verb:
233
+
234
+ ```ts
235
+ const rows = await ablo.weatherReports.create({
236
+ data: [
237
+ { location: 'Stockholm', summary: 'Clear' },
238
+ { location: 'Oslo', summary: 'Rain' },
239
+ ],
240
+ });
241
+ ```
242
+
243
+ They are written as one atomic commit rather than one request each, so either
244
+ every row lands or none does. The result comes back in the order you gave it,
245
+ not the order the batch settled, and carries whatever defaults the server
246
+ stamped. An empty list writes nothing rather than opening an empty commit.
247
+
248
+ ### Reading a whole collection, in as many words
249
+
250
+ `listAll({ where, maxPages, signal })` reads a complete collection by walking the
251
+ same cursor `list` returns, so the common case stops being a hand-rolled loop:
252
+
253
+ ```ts
254
+ const open = await ablo.weatherReports.listAll({
255
+ where: { status: ['draft', 'review'] },
256
+ maxPages: 20,
257
+ });
258
+ ```
259
+
260
+ It is bounded on purpose. `maxPages` is how you say how much you are willing to
261
+ read, and `signal` cancels a walk that is taking longer than the work is worth.
262
+ A complete read that cannot say when it will stop is how a page turns into an
263
+ outage.
264
+
265
+ ### A claimed write carries its stale guard again
266
+
267
+ Holding a claim and then writing gave mutual exclusion but not lost-update
268
+ detection, on the stateless transport agents run. The claim handle carries the
269
+ position the row was read at, and the write defaults to rejecting on a change
270
+ since then. Unwrapping the handle cleared the claim before that default was
271
+ read, so the guard was unreachable and every model write through the public
272
+ surface lost it.
273
+
274
+ It read as though both protections were present: claim, read, decide, write. The
275
+ watermark now travels with the handle, and your own `readAt` or `onStale` still
276
+ win where you set them.
277
+
278
+ ### A create on an id that already exists is refused
279
+
280
+ It reported success and returned a row. A caller-selected id is a claim about
281
+ which row this is, so a create that finds one already there is a conflict rather
282
+ than an update, and it now says so.
283
+
284
+ ### CLI: a session route that revalidates before it mints
285
+
286
+ `ablo init` scaffolds a Next.js session route that re-checks membership at mint
287
+ time rather than trusting the caller, and puts secret clients behind the
288
+ framework's `server-only` boundary so a key cannot be imported into a component
289
+ that ships to a browser.
290
+
291
+ ### Filtering a server read by a reference field
292
+
293
+ `list({ where: { issueId } })` matched nothing on a replicated plane. It raised
294
+ no error and returned a well-formed empty array, so the read looked like a
295
+ question with no answers rather than a filter that never ran. Filtering on
296
+ `id`, `title` or `body` worked, which made the failure look like a property of
297
+ the data instead of a property of the field name.
298
+
299
+ The cause was a key space. A row served from the log is a snapshot in the wire
300
+ shape, so its fields are spelled the way your schema spells them, while the
301
+ filter looked them up by their database column. Those two agree exactly when a
302
+ column is a single word, and part ways on every `issueId`, `teamId` or
303
+ `assigneeId`. Ordering, relation expansion and any field declared with
304
+ `.from()` were reading the same wrong spelling: a `related` list came back
305
+ empty, and a `.from()` field was simply absent from the row.
306
+
307
+ If you page a collection and filter it in your own code to work around this,
308
+ that code can go.
309
+
310
+ A filter naming a field the model does not declare is now refused, with the
311
+ same error the direct-database plane already gave it. It used to return
312
+ nothing, which reads as an answer.
313
+
314
+ ### A list read walks its own pages
315
+
316
+ `list` returns a page, and a page of 20 looks exactly like a complete answer of 20. Every caller either checked `hasMore` or, more often, reasoned about a
317
+ truncated collection without knowing there was more.
318
+
319
+ Iterate the result for the page. Walk it for the collection:
320
+
321
+ ```ts
322
+ for await (const issue of await ablo.issues.list({ where: { teamId } })) {
323
+
324
+ }
325
+ ```
326
+
327
+ `hasMore` and `nextCursor` are unchanged, and taking the cursor yourself is
328
+ still the right thing when the pages go somewhere other than a loop.
329
+
330
+ ### Clearing a field
331
+
332
+ `null` clears a field, and the types now say so. They used to accept only the
333
+ field's own type or `undefined`, and `undefined` means "leave this alone": it
334
+ is dropped from the payload, so an unassign written that way kept the old
335
+ assignee and reported success. The only spelling that both compiled and worked
336
+ was one that cast the payload, which turned off type checking for the whole
337
+ write.
338
+
339
+ Only a field your schema declares optional accepts `null`. A required field has
340
+ no empty value to move to, and the type says that too.
341
+
342
+ ### A write that does not name its row is refused
343
+
344
+ `delete({ where: { id } })` reads like it should work, and `where` is what the
345
+ commit protocol takes one layer down. It used to spell the missing id into the
346
+ request as the literal text `undefined`, match no row, and return an ordinary
347
+ receipt. It now fails at the call, naming the model, the action, and `{ id }`.
348
+
349
+ The same guard covers `update`.
350
+
351
+ ### A create honours the id you gave it
352
+
353
+ An id passed inside `data`, which the create input has always allowed, was
354
+ never read: the row was written under a generated id and you were handed back
355
+ one you had not named. Both spellings now work, and the standalone `id` wins if
356
+ they disagree.
357
+
358
+ ### Every response says what your allowance is
359
+
360
+ The limiter knew the allowance and the refill and told you neither, so the only
361
+ strategy available was to retry and find the wall again.
362
+
363
+ ```
364
+ RateLimit-Policy: "secret";q=600;w=12
365
+ RateLimit: "secret";r=573;t=8
366
+ Retry-After: 3
367
+ ```
368
+
369
+ `RateLimit-Policy` is the standing allowance and is always present.
370
+ `RateLimit` reports what is left and when it refills, once a request is
371
+ attributed to a key. A 429 adds `Retry-After` in whole seconds. Pace against
372
+ these rather than retrying blind.
373
+
374
+ ### A route says when it is going away
375
+
376
+ Every response carries `Ablo-Version`, a date stamp for the contract being
377
+ served, so a caller can notice the contract moved under it.
378
+
379
+ A route being withdrawn now says so on itself for at least 180 days first.
380
+ `Deprecation` (RFC 9745) carries when the deprecation took effect, and the route
381
+ keeps answering; `Sunset` (RFC 8594) carries when it stops. The same operations
382
+ are marked `deprecated: true` in the OpenAPI document, so a generated client
383
+ sees it too.
384
+
385
+ Breaking changes still arrive as a new path segment beside `/v1`, never as a
386
+ change to it. Additive ones land in `/v1`, so ignore what you do not recognise.
387
+
388
+ ### The documentation answers a reader that is not a browser
389
+
390
+ The surfaces `llms.txt` names are routes now rather than a promise:
391
+ `/llms-full.txt` for the whole corpus in one fetch, `/openapi.json` for the REST
392
+ contract, `/developers` naming every developer surface on one page,
393
+ `/.well-known/mcp.json` for the MCP manifest.
394
+
395
+ Every page also answers from its own URL in Markdown. Send
396
+ `Accept: text/markdown`, or append `.md` where a client cannot set headers.
397
+ Responses carry `Vary: Accept`, a client that will take neither type gets a 406
398
+ listing what is available, and a path that does not exist answers a real 404
399
+ rather than a 200 carrying a sign-in page.
400
+
401
+ ### Renamed and removed
402
+
403
+ `SourceRequestContext.requiredSyncGroups` is now `syncGroups`. Ablo populates
404
+ both spellings this release, so a source adapter still reading the old name gets
405
+ the groups rather than `undefined`, which on a routing field would read as "no
406
+ groups" rather than as a field that moved. The old spelling is removed in
407
+ 0.58.0.
408
+
409
+ `DeltaPosition`, `deltaPositionSchema`, `ReadSetWatermark`, and
410
+ `readSetWatermarkSchema` are removed, as 0.56.0 announced. Use `LogPosition` and
411
+ `logPositionSchema`, which they have resolved to since then.
412
+
3
413
  ## 0.56.0
4
414
 
5
- ### One customer no longer sees another's live work
415
+ ### Coordination reads are scoped to the customer, not the organization
416
+
417
+ Coordination has always been scoped to the organization, and through 0.51.0 that
418
+ was the whole boundary: a platform gave each customer its own organization, and
419
+ the sessions guide was explicit that sync groups decide which changes travel
420
+ rather than what a session may read.
421
+
422
+ This release moves that line. A platform's customers are rows in its own schema,
423
+ reached by the sync groups on the session, so many customers share one
424
+ organization and the organization is no longer the finest boundary. The delivery
425
+ path already applied the finer cut. The claim listing and the presence read did
426
+ not, so under that newer arrangement one customer could see which rows another
427
+ had claimed, who held them, what the work was called, and who was online. Row
428
+ contents were never exposed; everything around them was.
6
429
 
7
- A platform's customers are rows in its own schema, so they share one
8
- organization and one plane. The claim listing and the presence read were scoped
9
- to exactly that pair and nothing finer. One customer could therefore see which
10
- rows another had claimed, who held them, what the work was called, and who was
11
- online. Row contents were never exposed; everything around them was.
430
+ Both reads now take the same cut, from the groups each side already carries.
12
431
 
13
- Both reads now apply the same cut the delivery path already applies, taken from
14
- the groups each side already carries. A customer sees the coordination for the
15
- rows it can see, and nothing else. If you serve many customers from one
16
- organization, this closes the gap without any change on your side.
432
+ If you give each customer its own organization, nothing changes for you and
433
+ nothing was reachable across customers. If you serve many customers from one
434
+ organization, this closes the gap with no change on your side.
17
435
 
18
436
  ### A client converges on the head it was measured against
19
437
 
package/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2025-2026 Lukas Andersson
189
+ Copyright 2025-2026 Ablo Inc.
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/NOTICE CHANGED
@@ -1,10 +1,10 @@
1
1
  @ablo/ablo
2
- Copyright 2025-2026 Lukas Andersson
2
+ Copyright 2025-2026 Ablo Inc.
3
3
 
4
- This product includes software developed by Lukas Andersson
4
+ This product includes software developed by Ablo Inc.
5
5
  (https://abloatai.com).
6
6
 
7
- "Ablo" is a trademark of Lukas Andersson. This license does not grant
7
+ "Ablo" is a trademark of Ablo Inc. This license does not grant
8
8
  permission to use the Ablo name, logo, or trademarks. Third parties
9
9
  may describe their use of or compatibility with Ablo factually (e.g.,
10
10
  "built with @ablo/ablo") but may not use the Ablo name in a way
package/README.md CHANGED
@@ -56,12 +56,13 @@ for changes.
56
56
  Read and write through one typed API:
57
57
 
58
58
  ```ts
59
- const order = await ablo.orders.get({ id: orderId });
59
+ const order = await ablo.orders.read({ id: orderId });
60
60
 
61
61
  if (!order) throw new Error('Order not found');
62
62
  await ablo.orders.update({
63
63
  id: order.id,
64
64
  data: { status: 'approved' },
65
+ reads: [order],
65
66
  });
66
67
  ```
67
68
 
package/dist/ai-sdk.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from '@abloatai/transaction/ai-sdk';
2
- import type { ContextResult } from './context.js';
2
+ import type { ContextResult } from './context/index.js';
3
3
  export interface ContextMessageOptions<TData extends Readonly<Record<string, unknown>>> {
4
4
  /** Top-level context keys to render. The default is every selected key. */
5
5
  readonly include?: readonly (keyof TData & string)[];
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk.d.ts","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAG7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAMlD,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpF,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC5E,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,qBAAqB,CAAC,KAAK,CAAM,GACzC,cAAc,CAehB"}
1
+ {"version":3,"file":"ai-sdk.d.ts","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAG7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAMxD,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpF,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC;CACtD;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC5E,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,EAC3B,OAAO,GAAE,qBAAqB,CAAC,KAAK,CAAM,GACzC,cAAc,CAehB"}
@@ -1,11 +1,9 @@
1
1
  import { type CapturedReadEvidence } from '@abloatai/transaction/internal/read-set';
2
- export interface ContextEvidenceSlice {
3
- readonly reads: readonly CapturedReadEvidence[];
4
- readonly includesInformational: boolean;
2
+ import type { ReadDependency } from '@abloatai/transaction/coordination';
3
+ import type { AbloStaleContextError } from '@abloatai/transaction';
4
+ export interface BoundContextEvidence {
5
+ readonly collect: (data: unknown) => readonly CapturedReadEvidence[];
6
+ readonly onChange?: (reads: readonly ReadDependency[], listener: (error: AbloStaleContextError) => void) => () => void;
5
7
  }
6
- export interface ContextEvidence {
7
- readonly all: readonly CapturedReadEvidence[];
8
- readonly inspect: (value: unknown) => ContextEvidenceSlice;
9
- }
10
- export declare function bindContextEvidence(client: object): (data: unknown) => ContextEvidence;
8
+ export declare function bindContextEvidence(client: object): BoundContextEvidence;
11
9
  //# sourceMappingURL=evidence.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,yCAAyC,CAAC;AAQjD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAChD,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;CACzC;AAsCD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,SAAS,oBAAoB,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,oBAAoB,CAAC;CAC5D;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,eAAe,CAWtF"}
1
+ {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAiCnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,SAAS,oBAAoB,EAAE,CAAC;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAClB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,KAC7C,MAAM,IAAI,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CAQxE"}
@@ -8,12 +8,9 @@ function isTraversable(value) {
8
8
  function inspectValue(binding, value) {
9
9
  const found = [];
10
10
  const seen = new WeakSet();
11
- let includesInformational = false;
12
11
  const visit = (current) => {
13
- if (typeof current !== 'object' || current === null) {
14
- includesInformational = true;
12
+ if (typeof current !== 'object' || current === null)
15
13
  return;
16
- }
17
14
  if (seen.has(current))
18
15
  return;
19
16
  seen.add(current);
@@ -23,31 +20,20 @@ function inspectValue(binding, value) {
23
20
  return;
24
21
  }
25
22
  if (isTraversable(current)) {
26
- const children = Object.values(current);
27
- if (children.length === 0)
28
- includesInformational = true;
29
- for (const child of children)
23
+ for (const child of Object.values(current))
30
24
  visit(child);
31
- return;
32
25
  }
33
- includesInformational = true;
34
26
  };
35
27
  visit(value);
36
- return {
37
- reads: [...new Map(found.map((item) => [item.row, item])).values()],
38
- includesInformational,
39
- };
28
+ return [...new Map(found.map((item) => [item.row, item])).values()];
40
29
  }
41
30
  export function bindContextEvidence(client) {
42
31
  const binding = readEvidenceBinding(client);
43
32
  if (!binding)
44
33
  throw new TypeError('context() requires an Ablo client in `ablo`.');
45
- return (data) => {
46
- const inspect = (value) => inspectValue(binding, value);
47
- return {
48
- all: inspect(data).reads,
49
- inspect,
50
- };
34
+ return {
35
+ collect: (data) => inspectValue(binding, data),
36
+ ...(binding.onChange ? { onChange: binding.onChange } : {}),
51
37
  };
52
38
  }
53
39
  //# sourceMappingURL=evidence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,mBAAmB,GAGpB,MAAM,yCAAyC,CAAC;AAEjD,SAAS,aAAa,CAAC,KAAa;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC;AAOD,SAAS,YAAY,CACnB,OAA4B,EAC5B,KAAc;IAEd,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IACnC,IAAI,qBAAqB,GAAG,KAAK,CAAC;IAElC,MAAM,KAAK,GAAG,CAAC,OAAgB,EAAQ,EAAE;QACvC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACpD,qBAAqB,GAAG,IAAI,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QAC9B,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClB,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,qBAAqB,GAAG,IAAI,CAAC;YACxD,KAAK,MAAM,KAAK,IAAI,QAAQ;gBAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,qBAAqB,GAAG,IAAI,CAAC;IAC/B,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO;QACL,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACnE,qBAAqB;KACtB,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAElF,OAAO,CAAC,IAAI,EAAE,EAAE;QACd,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACjE,OAAO;YACL,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK;YACxB,OAAO;SACR,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/context/evidence.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,mBAAmB,GAGpB,MAAM,yCAAyC,CAAC;AAIjD,SAAS,aAAa,CAAC,KAAa;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC;AAED,SAAS,YAAY,CACnB,OAA4B,EAC5B,KAAc;IAEd,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IAEnC,MAAM,KAAK,GAAG,CAAC,OAAgB,EAAQ,EAAE;QACvC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO;QAC5D,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO;QAC9B,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAClB,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,QAAQ,EAAE,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;gBAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;AACtE,CAAC;AAUD,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,OAAO,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAElF,OAAO;QACL,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC;QAC9C,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { CapturedRow } from '@abloatai/transaction';
2
+ import { type AwaitedDeep } from './await.js';
3
+ import { type ContextOnChange } from './onChange.js';
4
+ export type { AwaitedDeep } from './await.js';
5
+ export type { ContextChangeListener, ContextOnChange } from './onChange.js';
6
+ export interface ContextOptions<TData extends Readonly<Record<string, unknown>>> {
7
+ /** The client whose read evidence may guard a later create, update, or delete. */
8
+ readonly ablo: object;
9
+ /** Values selected by the application. Nested promises are accepted. */
10
+ readonly data: TData;
11
+ }
12
+ export interface ContextResult<TData extends Readonly<Record<string, unknown>>> {
13
+ readonly data: AwaitedDeep<TData>;
14
+ /**
15
+ * Exact returned Ablo rows, ready to pass to create, update, or delete
16
+ * through `reads`.
17
+ */
18
+ readonly reads: readonly CapturedRow[];
19
+ /** Called once when any exact read in this context becomes stale. */
20
+ readonly onChange: ContextOnChange;
21
+ }
22
+ export declare function context<const TData extends Readonly<Record<string, unknown>>>(options: ContextOptions<TData>): Promise<ContextResult<TData>>;
23
+ //# sourceMappingURL=index.d.ts.map