@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 CHANGED
@@ -79,7 +79,7 @@ await ablo.weatherReports.update({
79
79
 
80
80
  ## Coordination surface
81
81
 
82
- Claims live on a callable namespace beside `create` / `update` / `retrieve`. Every member takes an options object:
82
+ Claims live on a callable namespace beside `create` / `update` / `get`. Every member takes an options object:
83
83
 
84
84
  - `await using claim = await ablo.<model>.claim({ id })` — acquire the row (waits if held); read it via `claim.data`; auto-releases on scope exit (or call `claim.release()`).
85
85
  - `ablo.<model>.claim.state({ id })` — who is currently working on the row (synchronous; never blocks).
package/CHANGELOG.md CHANGED
@@ -1,7 +1,154 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.48.0
4
+
5
+ ### A branch is unbound until you connect a database to it
6
+
7
+ A branch keeps its own storage, and it does not quietly get Ablo's. Until a
8
+ database is connected to that branch, a request needing one fails with
9
+ `no_data_source_registered` and says what to do:
10
+
11
+ ```
12
+ This branch is not connected to your database yet.
13
+ Run `ablo connect` for this branch, then retry.
14
+ ```
15
+
16
+ The old `test_database_not_registered` is gone. It described a sandbox that no
17
+ longer exists, it arrived on requests that had nothing to do with a test
18
+ database, and its advice pointed at options the SDK had already removed. A
19
+ branch created by `ablo dev` now reports its state plainly rather than looking
20
+ ready and then refusing the first schema push.
21
+
22
+ **Action required.** Replace any handler matching `test_database_not_registered`
23
+ with `no_data_source_registered`. The new code carries the same 4xx meaning and
24
+ a recovery path a caller can act on.
25
+
26
+ ### Failures say which branch they happened on
27
+
28
+ An unconnected branch previously returned its refusal with nothing written
29
+ server-side, so a support question about one customer's branch could not be
30
+ answered from logs at all. These now carry the request, organization, project,
31
+ branch, key kind and storage state, indexed in Sentry, so a failure can be
32
+ looked up by branch instead of reconstructed from database tables.
33
+
34
+ ### Replication uses your branch's own publication and slot
35
+
36
+ Registration, validation, writer checks, replication, drift detection and
37
+ `ablo connect` all require the branch-scoped names recorded when the source was
38
+ registered. Nothing falls back to a shared `ablo_publication` or `ablo_slot`
39
+ any more, so two branches on one database can never quietly share a stream.
40
+ `ablo connect scan` reports legacy unsuffixed objects as retired.
41
+
42
+ Once your engine is on this release and `ablo connect check` is clean, the
43
+ temporary alias can go:
44
+
45
+ ```sql
46
+ DROP PUBLICATION IF EXISTS "ablo_publication";
47
+ ```
48
+
49
+ ### Renamed
50
+
51
+ `FootprintPlane` is now `DataSourceIdentity`, with the same three fields. The
52
+ old name described an internal layout; the new one describes what it
53
+ identifies.
54
+
55
+ ## 0.47.0
56
+
57
+ ### Local Postgres works with Ablo Cloud
58
+
59
+ Run `npx ablo dev --local` to serve the generated signed Data Source handler
60
+ over an outbound, protocol-scoped connector. Postgres remains private on the
61
+ developer's machine and its connection string never leaves the app process.
62
+ The Data Source guide now explains exactly which writes are visible without
63
+ WAL, and the public error reference includes actionable `source_connector_*`
64
+ codes for every connector lifecycle failure.
65
+
66
+ ### Awaiting a model write now means it is confirmed
67
+
68
+ `create`, `update`, and `delete` change local reactive state immediately and
69
+ return a promise with a single meaning: the write reached authoritative
70
+ confirmation. An interface stays responsive without awaiting anything, and code
71
+ that needs to know a write survived can await the same call it already makes.
72
+
73
+ The `wait` option is gone from the client and from individual model calls.
74
+ Awaiting a model write always waits for confirmation, so there is nothing left
75
+ to configure. Explicit control over a queued versus confirmed receipt remains on
76
+ `commits.create`, which still hands back the receipt and its confirmation
77
+ separately.
78
+
79
+ **Action required.** Remove `wait` from `Ablo({ ... })` and from every
80
+ `create`, `update`, and `delete` call.
81
+
82
+ - `wait: 'confirmed'` behaves identically once removed.
83
+ - `wait: 'queued'` on a call you never awaited behaves identically once removed.
84
+ - `wait: 'queued'` on a call you did await now waits for confirmation. Move to
85
+ `commits.create` if the queued receipt was the reason for the option.
86
+
87
+ ### Customer branches connect before accepting a schema
88
+
89
+ A customer branch now remains in provisioning until it has an active Data
90
+ Source. Ablo does not invent internal storage for customer data: it reads the
91
+ customer's database through WAL and writes through the separately scoped DML
92
+ credential (or uses the explicitly registered signed endpoint fallback).
93
+
94
+ Database validation now uses the branch-scoped publication and replication slot
95
+ persisted with that Data Source. It no longer falls back to the shared
96
+ `ablo_publication` / `ablo_slot` names, so `ablo connect check` validates the
97
+ same objects that `ablo connect apply` created.
98
+
99
+ The sandbox-only `test_database_not_registered` error has been removed. An
100
+ unconnected customer branch now consistently returns `no_data_source_registered`
101
+ with the `ablo connect` recovery step.
102
+
103
+ **Action required for type imports.** `FootprintPlane` has been removed. Import
104
+ `DataSourceIdentity` from `@abloatai/ablo/source` instead; its fields remain
105
+ `organizationId`, optional `projectId`, and `branchId`.
106
+
107
+ ### The CLI names the problem it actually hit
108
+
109
+ A refused push no longer reports every failure as a missing `schema:push`
110
+ capability. That advice was wrong for most refusals: a database privilege error,
111
+ a row-level security misconfiguration, and an unregistered development database
112
+ each need a different fix, and none of them is a different API key. Each now
113
+ leads with the server's own message and the remedy for that specific cause.
114
+
115
+ Project names also resolve correctly under a branch-bound key. Listing projects
116
+ is a management operation that such a key is deliberately not allowed to
117
+ perform, so `ablo status` and `ablo push` reported a correctly minted key's
118
+ project as `unnamed` alongside a permission error. The name now comes from the
119
+ stored management credential.
120
+
121
+ ### Deprecations
122
+
123
+ `METER_EVENT_COUNTS` is deprecated in favour of its per-surface members, and the
124
+ `DatasourceResnapshotResponse` type and its schema are deprecated. All three
125
+ still ship and still work; they will be removed in a later release.
126
+
3
127
  ## 0.46.0
4
128
 
129
+ ### Provider branches isolate environments; schemas isolate projects
130
+
131
+ Several Ablo projects can now share one physical Postgres database safely when
132
+ each owns a separate schema. `ablo connect apply --schema mail` binds the
133
+ authenticated project branch to `(database, mail)` and derives an independent
134
+ slot, publication, replication role, and writer role. Publications enumerate
135
+ schema-qualified mapped tables and the idempotency ledger lives in the selected
136
+ schema. A provider branch's distinct direct URL remains the environment boundary
137
+ for Neon, Supabase, and similar hosts.
138
+
139
+ Registration enforces one owner per `(database, schema)` globally, without
140
+ revealing another organization's coordinates, and refuses a new binding when
141
+ `max_replication_slots` has no capacity. Runtime replication, rotate,
142
+ resnapshot, disconnect, and scan use the binding's stored/derived footprint;
143
+ legacy manual setups remain single-binding.
144
+
145
+ New clients can pin their intended project and branch with `projectId` /
146
+ `ABLO_PROJECT_ID` and `branchId` / `ABLO_BRANCH_ID`. `ablo dev` writes both
147
+ immutable coordinates beside its branch-bound key, and `ready()` compares them
148
+ with the key's server-resolved target before opening the sync connection. A mail
149
+ deployment carrying a slides key now fails with `project_scope_denied`; a
150
+ same-project key for the wrong environment fails with `branch_scope_denied`.
151
+
5
152
  ### Pre-existing rows arrive on their own
6
153
 
7
154
  Connecting a database that already holds data no longer leaves those rows
@@ -13,6 +160,19 @@ snapshot completes, and `ablo status --json` exposes the progress as
13
160
  `complete`. Row-touch backfill scripts are unnecessary; the snapshot is the
14
161
  engine's job.
15
162
 
163
+ The snapshot reader now detects row-level security that would filter its
164
+ ordinary `SELECT` even though WAL carries every published row. New setup plans
165
+ give the read-only replication role `BYPASSRLS` (with `SELECT` still restricted
166
+ to published tables), and readiness refuses to call an RLS-filtered snapshot
167
+ safe. Existing connections can run `ablo connect resnapshot` after repairing
168
+ the role; it recreates only the slot and keeps the DataSource and credentials.
169
+ The same completion guard rejects an empty or partial snapshot mapping: if a
170
+ pushed model's table is absent from the publication, completion stays pending
171
+ until coverage is repaired and a fresh snapshot is requested. Snapshot and WAL
172
+ mapping now use the DataSource's configured Postgres schema as part of relation
173
+ identity, so an identically named table in another schema cannot be loaded into
174
+ the model or falsely satisfy publication coverage.
175
+
16
176
  ## 0.45.0
17
177
 
18
178
  ### Claim admission is authoritative
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 Fablo Innovation AB
189
+ Copyright 2025-2026 Lukas Andersson
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 Fablo Innovation AB
2
+ Copyright 2025-2026 Lukas Andersson
3
3
 
4
- This product includes software developed by Fablo Innovation AB
4
+ This product includes software developed by Lukas Andersson
5
5
  (https://ablo.finance).
6
6
 
7
- "Ablo" is a trademark of Fablo Innovation AB. This license does not grant
7
+ "Ablo" is a trademark of Lukas Andersson. 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
@@ -22,6 +22,10 @@
22
22
 
23
23
  ---
24
24
 
25
+ > **Reading the implementation?** Start with the
26
+ > **[source code map](./CODEMAP.md)**. It shows which files own `create`,
27
+ > `update`, `delete`, `claim`, schemas, transports, and the reactive client.
28
+
25
29
  Safely coordinate AI agents, humans, workflows, and services writing to the
26
30
  same database.
27
31
 
@@ -63,7 +67,6 @@ if (!order) throw new Error('Order not found');
63
67
  await ablo.orders.update({
64
68
  id: order.id,
65
69
  data: { status: 'approved' },
66
- wait: 'confirmed',
67
70
  });
68
71
  ```
69
72
 
@@ -82,7 +85,6 @@ await ablo.orders.update({
82
85
  id: claim.data.id,
83
86
  data: { total: priced.total, status: 'repriced' },
84
87
  claim,
85
- wait: 'confirmed',
86
88
  });
87
89
  ```
88
90
 
@@ -116,6 +118,22 @@ authority, commits, claims, and ordered changes.
116
118
  Read the [Quickstart](https://docs.abloatai.com/quickstart), browse
117
119
  [docs.abloatai.com](https://docs.abloatai.com), or run `npx ablo docs`.
118
120
 
121
+ ## Navigating the source
122
+
123
+ This repository preserves the package ownership boundaries instead of
124
+ flattening the implementation into `packages/ablo`:
125
+
126
+ - `packages/ablo` is the branded public facade. Its files mostly re-export the
127
+ package that owns each API.
128
+ - `packages/transaction` owns the shared model-operation contracts and the
129
+ stateless HTTP implementation.
130
+ - `packages/humans` owns the reactive WebSocket/local/React implementation.
131
+
132
+ That means searching only inside `packages/ablo/src` will not find the
133
+ implementation of `create`, `update`, `delete`, or `claim`. Read the
134
+ **[source code map](./CODEMAP.md)** for a verb-by-verb ownership table and
135
+ guided call traces for both the default and reactive clients.
136
+
119
137
  ## Contributing
120
138
 
121
139
  Ablo is free and open source. You can help by
@@ -106,7 +106,6 @@ try {
106
106
  aboutEntityId: workItemId,
107
107
  aboutIntentId: claim.claimId,
108
108
  },
109
- wait: "confirmed",
110
109
  });
111
110
  } finally {
112
111
  await claim.release();
package/docs/agents.md CHANGED
@@ -147,7 +147,7 @@ default caller here, not a bolt-on.
147
147
 
148
148
  ```text
149
149
  something happens ──▶ your agent (HTTP, no socket)
150
- (a job, a webhook, read context (list/retrieve)
150
+ (a job, a webhook, read context (list/get)
151
151
  a queue message) claim → work → commit
152
152
  done — no held connection
153
153
  ```
package/docs/api.md CHANGED
@@ -41,7 +41,7 @@ await ablo.ready();
41
41
  const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
42
42
  if (!report) throw new Error('Row not found');
43
43
 
44
- await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
44
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
45
45
  ```
46
46
 
47
47
  For end-to-end app setup across React, existing backends, Data Source, and
@@ -75,7 +75,7 @@ fallback removed — nothing to await, so they return a value.
75
75
  | `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
76
76
  | `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
77
77
 
78
- `retrieve`, `list`, `create`, `update`, and `delete` are the main path — they go
78
+ `get`, `list`, `create`, `update`, and `delete` are the main path — they go
79
79
  through the server. The `local` reads work off the rows a session has already
80
80
  synced, so a cheap re-read needs no round-trip.
81
81
 
@@ -91,17 +91,18 @@ await ablo.weatherReports.update({
91
91
  data: { status: 'ready' },
92
92
  readAt: snap.stamp,
93
93
  onStale: 'reject',
94
- wait: 'confirmed',
95
94
  });
96
95
  ```
97
96
 
97
+ Reactive local state changes optimistically at call time; awaiting the model
98
+ write waits for authoritative confirmation.
99
+
98
100
  Protected write options:
99
101
 
100
102
  | Option | Purpose |
101
103
  |---|---|
102
104
  | `readAt` | The state cursor the write was based on. |
103
105
  | `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
104
- | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
105
106
  | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
106
107
  | `timeout` | Maximum time to wait for the write call. |
107
108
 
@@ -113,7 +114,7 @@ row, `claim` waits for them, re-reads the fresh row, then hands it to you — so
113
114
  two writers serialize instead of clobbering. A claim is temporary: it expires
114
115
  on its own if the holder stops, and is never saved as a row.
115
116
 
116
- You coordinate a row with calls on its model, beside `create`/`update`/`retrieve`:
117
+ You coordinate a row with calls on its model, beside `create`/`update`/`get`:
117
118
  `ablo.<model>.claim({ id })` takes the claim and returns a handle,
118
119
  `ablo.<model>.claim.state({ id })` reads who currently holds it (synchronous, never
119
120
  blocks), and `ablo.<model>.claim.release({ id })` releases it early. The full
package/docs/cli.md CHANGED
@@ -122,7 +122,7 @@ bypasses profiles for project/branch administration; the runtime key remains
122
122
  | `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. |: |
123
123
  | `ablo login` / `logout` / `whoami` / `status` | Authentication, exact credential identity, and readiness (above). | `whoami --key-env <NAME>`, `whoami --json`, `status --json` |
124
124
  | `ablo projects list\|create\|use\|rename` | Manage projects and the active one (see [Projects](#projects)). Each project's keys/schema/data are isolated. | `--name "<display>"` (create/rename) |
125
- | `ablo dev` | **Hosted**: ensure an isolated Git branch, wire its temporary key, push, then watch `ablo/schema.ts`. | `--branch <slug>`, `--branch-ttl-hours <1-168>`, `--no-watch`, `--schema`, `--export`, `--url` |
125
+ | `ablo dev` | Ensure an isolated Git branch, wire its temporary key, push, then watch `ablo/schema.ts`. `--local` also serves local Postgres over an outbound signed connector. | `--branch <slug>`, `--branch-ttl-hours <1-168>`, `--local`, `--source <path>`, `--no-watch`, `--schema`, `--export`, `--url` |
126
126
  | `ablo branch list\|status\|check\|create\|ensure\|credential\|delete` | Manage and diagnose immutable branch planes and expiring credentials. | Run `ablo branch --help`; use `--json` for automation. |
127
127
  | `ablo logs` | Tail the resolved runtime credential's branch activity. Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow` |
128
128
  | `ablo push` | **Hosted**: upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
@@ -145,9 +145,9 @@ npx ablo docs --json # the page list, machine-readable
145
145
 
146
146
  These pages ship inside the npm package, so they describe the code beside them
147
147
  and stay reachable with no network — isolated agent environments and CI runners
148
- often have none. That matters most when a project is pinned: `get` / `getAll` /
149
- `getCount` became `retrieve` / `list` in 0.35.0, and a website always describes
150
- the newest release, so an agent on an earlier version reads the new name and
148
+ often have none. That matters most when a project is pinned: `claim` took a
149
+ callback before it returned a disposable handle, and a website always describes
150
+ the newest release, so an agent on an earlier version reads the new shape and
151
151
  writes a call its own package doesn't have.
152
152
 
153
153
  Pass a slug (`coordination`), a path (`docs/coordination.md`), or a file name
@@ -166,8 +166,16 @@ npx ablo dev # discover from Git, push + watch
166
166
  npx ablo dev --branch preview-pr-482 # explicit branch
167
167
  npx ablo dev --no-watch # prepare, push once, exit
168
168
  npx ablo dev --branch-ttl-hours 24 # change temporary-key lifetime
169
+ npx ablo dev --local # keep Postgres private on localhost
169
170
  ```
170
171
 
172
+ `--local` loads `ablo/data-source.ts` (override with `--source <path>`), registers
173
+ the branch as connector-only, and opens an outbound authenticated WebSocket to
174
+ Ablo. Commit, load, list, and outbox-event requests run through the same signed
175
+ Data Source handler as production; no database credential leaves your process
176
+ and no public tunnel is opened. Because the connector is long-lived, `--local`
177
+ cannot be combined with `--no-watch`.
178
+
171
179
  It does not start your app, run migrations, create a database-provider branch,
172
180
  or copy production rows. Read [Branch-first development](./branch-development.md)
173
181
  for the exact discovery order, CI flow, database boundary, and troubleshooting.
@@ -330,5 +338,5 @@ migration can't leave clients gated against tables that don't match.
330
338
  | ------------------------------------- | ------------------------------------------------------------------------ | -------------------------- |
331
339
  | `ABLO_API_KEY` | Authenticate without `ablo login` (CI). Always overrides the stored key. |: |
332
340
  | `ABLO_API_URL` | Control-plane / API host (`push`, `dev`, `status`). | `https://api.abloatai.com` |
333
- | `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://abloatai.com` |
341
+ | `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://www.abloatai.com` |
334
342
  | `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives. | `~/.config/ablo` |
@@ -57,11 +57,15 @@ const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
57
57
  const local = ablo.weatherReports.local.get('report_stockholm');
58
58
 
59
59
  await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
60
- await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
61
- await ablo.weatherReports.delete({ id: 'report_stockholm', wait: 'confirmed' });
60
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
61
+ await ablo.weatherReports.delete({ id: 'report_stockholm' });
62
62
  ```
63
63
 
64
- Call `retrieve`/`list` first they fetch from the server and you `await` them.
64
+ On the reactive client, each model write changes local state optimistically
65
+ before the call returns. Its promise always waits for authoritative
66
+ confirmation, so `await update(...)` is the confirmation barrier.
67
+
68
+ Call `get`/`list` first — they fetch from the server and you `await` them.
65
69
  After that, `local.get`/`local.list`/`local.count` read the already-synced data instantly with
66
70
  no `await`, and stay reactive in render. Use the async pair to load, the sync trio
67
71
  to read.
@@ -87,7 +91,6 @@ await ablo.weatherReports.update({
87
91
  data: patch,
88
92
  readAt: snap.stamp,
89
93
  onStale: 'reject',
90
- wait: 'confirmed',
91
94
  });
92
95
  ```
93
96
 
@@ -109,7 +112,6 @@ it for reads, but it bypasses claims and ordering.
109
112
  await ablo.weatherReports.update({
110
113
  id: 'report_stockholm',
111
114
  data: { status: 'ready' },
112
- wait: 'confirmed',
113
115
  readAt: snap.stamp,
114
116
  onStale: 'reject',
115
117
  idempotencyKey: 'report_stockholm:mark-ready:v1',
@@ -118,7 +120,6 @@ await ablo.weatherReports.update({
118
120
 
119
121
  | Option | Purpose |
120
122
  |---|---|
121
- | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
122
123
  | `readAt` | State cursor the write was based on. |
123
124
  | `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
124
125
  | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
@@ -180,7 +181,7 @@ All SDK errors extend `AbloError` and carry a stable `type`.
180
181
  import { AbloClaimedError } from '@abloatai/ablo';
181
182
 
182
183
  try {
183
- await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
184
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
184
185
  } catch (error) {
185
186
  if (error instanceof AbloClaimedError) {
186
187
  return { status: 'claimed' };
@@ -313,7 +313,7 @@ four axes. `claim({ id })` alone is a complete call; each axis is opt-in.
313
313
 
314
314
  | name | type | required | description |
315
315
  |---|---|---|---|
316
- | `id` | `string` | yes | The row id: same id as `retrieve` / `update`. |
316
+ | `id` | `string` | yes | The row id: same id as `get` / `update`. |
317
317
  | `options.fields` | field selector | no | Claim fields declared by the model's Zod schema instead of the whole row: `fields: (task) => task.status`, or `fields: (task) => [task.status, task.title]` for several. The model supplies its own fields, so autocomplete is exact, a typo does not compile, and a schema rename is a compile error at every use. Two sets conflict where they intersect, so holders of disjoint fields do not wait for each other; see [claiming part of a row](#claiming-part-of-a-row). |
318
318
 
319
319
  *What others see* — the presence half:
@@ -510,7 +510,7 @@ summary into the same snapshot immediately.
510
510
 
511
511
  **You don't subscribe to anything first.** Reading or claiming a row
512
512
  automatically enrolls you in that row's sync group: reading it (including
513
- `retrieve`/`get`, or `claim.state` itself) gives you **read-interest**, and
513
+ `get`, or `claim.state` itself) gives you **read-interest**, and
514
514
  `claim`-ing it gives you a **pinned write-intent**. So `claim.state({ id })`
515
515
  observes co-participants on that row from **any** client — a browser, a Server
516
516
  Action, or a Node agent — and a holder sees its own claim, with no manual