Skip to main content
July 3, 2026
v0.26.0

Minor Changes

  • ablo connect --register works against any deployment. Registering your database as an Ablo data source over logical replication is now a first-class path everywhere — the gate that could refuse a new connection-string registration is gone. Point ablo connect --register at your Postgres and Ablo begins consuming its replication stream while your application keeps owning the write path; registering the source is the enable, with no tier or flag to choose. The signed dataSource() endpoint remains the explicit fallback for databases that can’t grant replication.
  • More engine failures surface as typed errors you can branch on, instead of opaque 500s. New AbloError codes: schema_provisioning_forbidden (403 — a push tried to create tables in a database where the engine isn’t permitted to run DDL), request_too_large (413 — a query or commit body exceeded the size limit), commit_operation_invalid (400 — a malformed commit operation), and upload_not_configured (503 — an upload was attempted with no blob store wired).
  • @abloatai/ablo/wire now exports the protocol schemas. commitOperationSchema, commitPayloadSchema, and the frame schemas let tooling and tests validate client↔engine frames against the same contract the engine enforces.
  • Protocol version negotiation is surfaced. @abloatai/ablo/wire exports PROTOCOL_VERSION, MIN_SUPPORTED_PROTOCOL_VERSION, WS_CLOSE_PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER, protocolVersionProblem, and ERROR_CONTRACT_VERSION — the single integer the client and server compare on connect to detect an incompatible peer and fail with a clear reason instead of a cryptic wire error.
  • @abloatai/ablo/coordination exposes the conflict-policy vocabularydefaultPolicy, capabilityPreemptPolicy, and interpretConflictAxis — for server-side consumers building on the claim model. @abloatai/ablo/schema gains the role primitives (identityRole, entityRole, and their types and schemas) for declaring identity- and entity-scoped roles.
  • Removed unused API surface. Dropped the @abloatai/ablo/server/next subpath export and the unused MutationDispatcher interface (plus its MockMutationDispatcher test double) and the offline-mutation-queue internals behind them — none had documented consumers. Every supported entry point (@abloatai/ablo, /ai-sdk, /react, /source, /coordination, /schema, /keys, /auth, /wire, …) is unchanged.
  • Internal: the SDK’s largest modules — the Ablo client, the data-source runtime, the transaction queue, and the WebSocket sync loop — were decomposed into cohesive leaf modules. No public runtime API or import path changed.
  • CLI & docs. ablo push prints a calmer, information-first deploy banner (a production push still requires typing the project name to confirm). The README and AGENTS.md now lead with the one-path logical-replication model and clarify that sandbox is test-mode only — in production your rows stay in your database and Ablo holds only the transaction log.
v0.25.0
v0.25.0

Minor Changes

  • ca30064: Logical replication is now the documented default storage path, with self-service data-source registration from the CLI.
    • ablo connect --register — registers your database as an Ablo data source over logical replication in one step: it runs the same pre-flight replication probe ablo connect uses (server reachable, REPLICATION privilege, wal_level=logical, publication/slot creatable), and on success POSTs the connection to the engine’s /v1/datasources. The ek_-authed call scopes the source to your org automatically; the password is stored decomposed as a secret, never echoed back. This is the “registration is the enable” path — there is no separate tier or flag to pick.
    • ablo init leads with logical replication — the default storage mode is now replication (was endpoint); the generated env + next-steps point at ablo connect / ablo connect --register. The signed-endpoint and direct modes remain as the explicit fallback / legacy options.
    • ablo status — a data-plane diagnostic that probes whether your registered source is reachable and replicating (failure-only reporting, so it never falsely reports healthy).
    • ablo push — minor guard/UX refinements (deploy-target clarity).
    • Docs (README, docs/data-sources.md, llms.txt) rewritten to the one-path model: Ablo consumes your database’s logical-replication stream and your application owns the write path. The security wording is precise — a logical-replication connection requires the REPLICATION privilege (it is not a read-only SQL account), so reviews are not misled by a “read-only” claim.
    No breaking changes to the SDK runtime API. The server-side WAL read cutover these CLI changes support ships by deploying the engine, not this package.
June 27, 2026
v0.24.0

Minor Changes

  • Multi-agent coordination for AI SDK tools + safer ablo push.
    • coordinatedTool (@abloatai/ablo/ai-sdk) — one call turns an Ablo model write into a Vercel AI SDK tool() with concurrency coordination handled, so an AI agent can contribute to shared state without clobbering concurrent writers. Three strategies: merge (default — delegates to the functional update’s compare-and-swap + backoff, self-healing accumulate), claim (mutual exclusion, returns a claimed signal the model retries on), and queue (SQS-style poll-acquire over HTTP). The ai-sdk entry now also documents the canonical multi-agent coordination model (surface-the-signal + back-off laws, strategy table).
    • ablo push guards--dry-run/--plan prints the deploy target, a model-level diff vs the deployed schema, and git state, then exits without applying. Production deploys now require a typed confirmation (and refuse an uncommitted schema unless --allow-dirty); sandbox confirms interactively. --yes/-y skips confirmation for CI.
June 26, 2026
v0.23.0

Minor Changes

  • 2807efb: create now returns the created row, not a CommitReceipt. The WebSocket client’s create already returned the row (T); the HTTP client and the .model(name) accessor returned a CommitReceipt, so “create returns the thing I created” only held on one transport. Both now return the confirmed, authoritative server row (framework defaults like createdAt/createdBy included). For an idempotent re-create of an existing caller-supplied id, the EXISTING row is returned (not the input). BREAKING (HTTP / .model() callers only): await ablo.<model>.create(...) now resolves to the row instead of { status, lastSyncId, ... }. Code that ignored the return value, or that read .id (the row carries id too), is unaffected; code that read lastSyncId / serverTxId / status off a typed model create should use the raw commits.create(...) resource, which still returns a CommitReceipt. WebSocket-client callers are unaffected (already returned T).
  • 2807efb: delete is idempotent — deleting an already-absent row is a no-op success, not an error. The WebSocket client’s delete threw entity_not_found when the row wasn’t in the local pool, while the HTTP client returned without error — so “delete this” was a hard edge on one transport. Both now agree: a row that isn’t present is already gone, so the delete succeeds with no effect. This is AIP-135’s recommended behavior for client-assigned-id / declarative APIs (Ablo is exactly that), and it makes delete safe to retry and to race (two actors deleting the same row). The deliberate “loud 0-row” assertion in @ablo/slides-sdk is unchanged (it keeps its own allowMissing opt-out).
  • 2807efb: retrieve reports a missing row as data: undefined instead of throwing. The HTTP client previously threw model_not_found for a missing row while the WebSocket client returned T | undefined — so the obvious read (“does this row exist?”) was a hard edge an agent had to wrap in try/catch on one transport only. Both transports now agree: an absent row is data-absence, not an error. ModelRead.data is now T | undefined (matching the documented .data?.x usage). Taking a claim on a row that doesn’t exist still throws AbloNotFoundError — a claim has nothing to hold.
June 26, 2026
v0.22.1

Patch Changes

  • Expose the functional update(id, current => next) overload on the stateless HTTP client type (HttpModelClient / AbloHttpClient). 0.22.0 wired the functional update at runtime on every transport and added the overload to ModelOperations (WebSocket) and ModelClient, but the Ablo({ transport: 'http' }) client resolves its models to HttpModelClient, whose update type still declared only the update({ id, data }) form. So server-side agents — the primary callers — saw a type error on update(id, fn) even though it worked. Add the overload to that type.
June 26, 2026
v0.22.0

Minor Changes

  • Add the functional update form: ablo.<model>.update(id, current => next). The setState(prev => next) of the data layer. Pass a function of the latest row and the SDK owns everything that used to be the caller’s problem under contention: it reads the freshest row, runs your updater, writes it as a compare-and-swap against the row’s watermark, and re-reads + re-runs on any concurrent write. No claim, no per-participant identity, and no stale_context / claim_* codes ever surface — correctness rides on the watermark, so concurrent writers reconcile instead of silently clobbering. The write either lands or throws a single AbloContentionError once its reconcile budget is spent. Identical guarantee on both transports (HTTP and WebSocket share one reconcile loop). Return null/undefined from the updater to skip the write. Tune with { retries, signal }. Exports: AbloContentionError, ModelUpdater, ContentionOptions, DEFAULT_CONTENTION_RETRIES. The classic update({ id, data }) form is unchanged.
June 26, 2026
v0.21.0

Minor Changes

  • Coordination observability now fires on BOTH transports. Previously captureClaim/captureConflict were emitted only by the WebSocket transport, so a ClaimLog (or any observability provider) handed to a stateless HTTP client — the transport server-side agents use via Ablo({ transport: 'http' }) — stayed empty, and even on WebSocket a hard commit rejection went unrecorded. Fixed:
    • HTTP transport now emits. Ablo({ transport: 'http', observability }) records claim acquisition (captureClaim) and coordination-conflict rejections (captureConflict, code stale_context / claim_conflict / entity_claimed) on BOTH HTTP write doors (commits.create and per-model ablo.<model>.update/create/delete). The conflict names the collided rows — from the server’s conflicts detail when present, otherwise the ops the write attempted. observability is now a documented option on the HTTP client.
    • WebSocket rejections now recorded. A commit rejected by the conflict policy (mutation_result success: false with a coordination code) now calls captureConflict, mirroring the existing notify-on-success path. So ClaimLog.collisions() no longer silently misses rejected writes.
    Net effect: a ClaimLog behaves identically regardless of transport — entries, collisions(), and onChange reflect the real coordination timeline for headless agent evals and live activity feeds alike.
June 25, 2026
v0.20.2

Patch Changes

  • Extend the HeldClaim return type to the HTTP transport. 0.20.1 fixed await using on the WebSocket client’s claim() but missed the stateless HTTP client (HttpClaimApi) used by server-side agents, which still returned the looser Claim<T>. Both transports’ claim() now return HeldClaim<T>, so await using held = await ablo.<model>.claim(...) typechecks regardless of transport.
June 25, 2026
v0.20.1

Patch Changes

  • Fix await using held = await ablo.<model>.claim(...) failing to typecheck. claim() now returns a HeldClaim<T> — a Claim<T> with data, release, revoke, and the async disposer made Required (they’re optional on the base Claim<T>, which also models observed peer claims that lack them). A held claim is therefore assignable to AsyncDisposable, so the await using auto-release pattern compiles. Observed claim surfaces still return the looser Claim<T>. HeldClaim is exported.
June 25, 2026
v0.20.0

Minor Changes

  • Reactive reads now work out of the box. A read like useAblo((a) => a.documents.get(id)) re-renders when a live delta updates the row — including in-place field updates (the common collaborative case), which previously fired no reaction and left the UI silently stale. Two changes make this work:
    • Models are reactive by default. Schema fields are now MobX-observable without opting in. json fields stay observable.ref (one atom for the whole blob, not a deep atom tree per node), so the default is cheap. Opt out per model with lazyObservable: false for very large read-only list models where the QueryView’s entry-replaced reactivity is enough.
    • useAblo returns a plain row snapshot (via the new Model.toReactiveSnapshot()) instead of the live model instance. Reading the fields inside the tracked function is what subscribes the reaction (MobX tracks property access, not values), and the fresh snapshot identity lets the hook detect the change. Consumers get plain row objects and never touch a MobX observable directly.
    Also new: deepEqual and stableStringify exports for comparing field.json() values. A jsonb-backed json field round-trips with reordered object keys (Postgres jsonb does not preserve key order), so a naive JSON.stringify(a) === JSON.stringify(b) comparison is unreliable when reconciling against external state (e.g. a rich-text editor). These helpers compare key-order-insensitively.
June 25, 2026
v0.19.0

Minor Changes

  • Claim observability — a ClaimLog you can print or assert on. A new observability provider hook lets you tap every claim event and stale-write collision the client sees. Hand new ClaimLog() to Ablo({ observability }) and it collects an ordered, readable log — formatClaim / formatConflict render one line per event, and collisions() returns the conflicts for eval assertions. New exports: ClaimLog, formatClaim, formatConflict, noopObservability, and the types ClaimLogEntry, ClaimEvent, ConflictEvent, SyncObservabilityProvider. Spread noopObservability to override only the hooks you care about. AWS-shaped CLI credential store + ablo config. Local CLI state is now split into two files, matching ~/.aws/config vs ~/.aws/credentials: config.json holds non-secret settings (active environment + active project) and is safe to print or let an agent read; credentials.json holds the keys (0600, never printed), keyed by project profile then environment. Per-project profiles follow Stripe’s model — ablo projects use <slug> selects the active profile, and a key’s project is fixed at mint so selecting a project never re-scopes an existing key. ablo status now reports the resolved profile and environment. Schema JSON-column reconciliation. generateJsonColumnReconciliation (new export) emits the DDL to reconcile JSON-backed columns when adopting or evolving an existing schema. Breaking (0.x):
    • The claim handle type ClaimHandle is renamed to Claim, and its identifier field is id (was claimId). Update type imports and any code reading .claimId.
    • The ai-sdk claimBroadcastMiddleware (and ./ai-sdk/claim-broadcast) is removed — coordination broadcast is handled by coordinationContextMiddleware. Import ClaimTarget from the package root or @abloatai/ablo ai-sdk’s coordination-context instead of claim-broadcast. The inline-claim option is reason (not the pre-0.12 action); the ai-sdk docs are corrected to match.
June 25, 2026
v0.18.0

Minor Changes

  • Client observability — debug / logLevel, off by default. The SDK used to emit a debug line per model and per property during schema registration (a firehose). It now defaults to a quiet warn threshold and exposes two new Ablo() options to opt back in:
    • logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent''info' surfaces coordination and connection events without the per-model registration noise; 'debug' is everything. Precedence: explicit logLeveldebug: trueABLO_LOG_LEVEL env → default warn. Supplying your own logger bypasses both.
    • debug: boolean — shorthand for logLevel: 'debug'.
    Coordination is now traceable at info: claims that are rejected or lost (preempted/expired), and your position advancing in a claim queue, each log once per change with a readable target (documents:abc.title) — quiet lowercase lines, no shouty tags. New: canonical wire-egress contract export. errorEnvelope, statusForType, and the ErrorEnvelope type are now exported from the package root. Server consumers (e.g. a self-hosted sync server) can assert against the one source of truth for the error-envelope shape and the AbloError-subclass→HTTP-status table instead of keeping a copy that silently drifts. Structured CLI error rendering. CLI failures render as a titled block with a reason code and per-code remediation (--verbose for the stack) instead of a console wall-of-text; AbloError.toString() produces a leak-proof one-liner. ABLO_API_KEY resolution + sandbox key scopes. The key is now resolved from .env.local / .env (not just the process env), and sandbox keys are granted schema:push by default so ablo push works out of the box in a fresh sandbox.
June 25, 2026
v0.17.0

Minor Changes

  • Bring-your-own database is now one model. Ablo connects to your Postgres and never operates it. There used to be two confusing BYO paths, and the connection-string one would create roles, force row-level security, transfer table ownership, and push you to run ablo migrate before anything worked. That cascade is gone. Ablo now follows the shape every serious “sync over your own Postgres” engine uses (ElectricSQL, PowerSync, Zero): it reads your database via Postgres logical replication and never runs DDL, creates roles, forces RLS, or rewrites your DATABASE_URL. You own your schema; Ablo reads it.
    • New: ablo connect. One command prints the exact, copy-pasteable setup for your own Postgres — enable wal_level=logical, create the ablo_publication publication and a least-privilege ablo_replicator role — and ablo connect --check validates readiness (wal level, publication, replication grant, replica identity). This is the single supported way to connect a real database.
    • ablo migrate left the happy path. It no longer creates roles, transfers ownership, or rewrites your connection string, and ablo dev no longer attempts a scoped-role creation on every watch loop. migrate is now an optional escape hatch for generating starter DDL (--dry-run prints the SQL).
    • Clearer failures. ablo push permission errors lead with the server’s actual reason code and per-code remediation instead of a generic “needs schema:push scope,” and the schema-conflict message names which environment/version a prior push came from and when.
    • Logical-replication runtime is in Preview. The setup (ablo connect) and the connection model are live; the server-side WAL consumer that streams your changes is implemented and journey-tested but not yet generally available.
    The previous connection-string-operate and adapter/outbox modes are demoted to a clearly-labeled Legacy / not recommended section — they still work, but new integrations should use logical replication.
June 24, 2026
v0.16.3

Patch Changes

  • Docs. The bundled SDK docs are now the single source for the documentation site, and several pages were expanded or corrected:
    • The sessions/identity model is reframed around projects — push one schema to a project, mint an ek_ per user (your users need no Ablo account), and all of them commit to that one schema. Per-customer org isolation (schemaProject) is presented as the add-on it is, not the default.
    • The declarative conflict schema axis (Axis 3) is now documented.
    • The agent docs were corrected to the current claim vocabulary (reason/queue, not the pre-0.12.0 action/wait).
    No code changes.
June 24, 2026
v0.16.2

Patch Changes

  • mintUserSessionKey: name the shared-schema binding around the project. The two flat options added in 0.16.0 (schemaOwnerOrgId + schemaProjectId) are replaced by one project-centric option — schemaProject: { organizationId, projectId } — naming “the project that owns the schema” as a single concept. The wire format is unchanged (the SDK still sends the same keys), so no server redeploy is needed. Released as a patch: the replaced options shipped in 0.16.0 and have no external consumers yet.
    // before
    mintUserSessionKey({ organizationId, schemaOwnerOrgId, schemaProjectId, ... });
    // after
    mintUserSessionKey({
      organizationId,                                  // data org
      schemaProject: { organizationId, projectId },    // the project that owns the schema
      ...
    });
    
June 24, 2026
v0.16.1

Patch Changes

  • Fix ablo login against the standalone auth server. The device flow now targets two origins instead of one: the RFC 8628 device endpoints (/api/auth/device/*) go to the identity server (auth.abloatai.com, override ABLO_AUTH_URL), while the human approval page (/cli), sign-up, and the key-handoff route (/api/cli/provision-key) go to the dashboard host (www.abloatai.com, new override ABLO_DASHBOARD_URL). Previously every step ran against www, where the device endpoints no longer resolve — producing “Couldn’t start login… Is the dashboard reachable?”. The CLI now also builds the approval URL itself rather than trusting the server’s verification_uri, which (being a relative /cli) resolved against the auth server’s origin to a 404.
June 24, 2026
v0.16.0

Minor Changes

  • Axis 3 — declare write-conflict behaviour in the schema (new). A model can now state what happens when a commit collides with a foreign claim or a stale snapshot — per committer kind (user / agent / system) — right next to its fields, using the same overwrite | reject | notify vocabulary as the onStale write guard. It is a third axis, orthogonal to policy (read access) and groups (delta routing).
    • conflict on model() — a plain, serializable disposition map. Pure data, so it round-trips through the schema registry to the server; the generic engine interprets it at the commit chokepoint (no per-model logic in the engine).
      // "a human's edit always wins (never blocked); an agent yields"
      conflict: { user: 'overwrite', agent: 'reject' }
      
    • Composable authoring helpers (new, from @abloatai/ablo/schema) — disposition functions plus a cn/cx-style combinator, so conflict policy reads like the rest of the DSL (relation.belongsTo()) and like modern config (plugins: [admin(), …]):
      import { coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
      
      conflict: coordination(humansOverwrite(), agentsReject());
      // → { user: 'overwrite', agent: 'reject' }
      
      Exports: coordination, humansOverwrite / humansReject / humansNotify, agentsOverwrite / agentsReject / agentsNotify, systemOverwrite / systemReject / systemNotify, and the ConflictRule type.
    • An omitted committer kind falls through to the engine default (reject; honor onStale: 'notify'), so this is fully additive — existing schemas are unchanged. New public types ConflictAxis (also Ablo.Conflict.Axis) and the interpretConflictAxis interpreter are exported for custom policy composition.
  • First-party shared schema for ephemeral keys (new). mintUserSessionKey now accepts schemaProjectId + schemaOwnerOrgId, binding the minted ek_ to a schema owner-org + project so schema resolves org-independently (one schema serves all of an integrator’s end-user orgs) while data stays scoped to organizationId. Requires the sk_ to carry ephemeral:mint-any-org; omit both for the existing per-org (BYO) behaviour.
June 23, 2026
v0.15.1

Patch Changes

  • Loud 0-row writes: surface unmatched UPDATE/DELETE ids and add AbloNotFoundError A commit now reports the ids of any UPDATE/DELETE that matched zero rows on CommitReceipt.missingIds, and the new exported AbloNotFoundError lets typed write wrappers throw instead of silently treating a missed write as success. Additive and back-compatible (the field is omitted when nothing missed). This unblocks the slides-sdk name-addressing / own-your-id work, which relies on a loud failure when a stale id is written.
June 22, 2026
v0.15.0

Minor Changes

  • Notify-instead-of-abort: non-coercive conflict handling + read-set (the “did anything I looked at change?” layer). The principle: on a stale-context conflict the engine now surfaces the current state and lets the actor — agent or human — resolve it, instead of forcing an outcome. See docs/concurrency-convention.md. onStale redesigned — Stripe-aligned values (BREAKING). The mode set is now 'reject' | 'overwrite' | 'notify'. Each value names its outcome:
    • notify (new, non-coercive) — the conflicting write is held (not applied) and the commit returns a StaleNotification carrying the conflicting field’s current value, so the actor reconciles and re-commits rather than losing work. The rest of the batch still commits.
    • overwrite (was force) — blind last-writer-wins, no signal.
    • reject (default, unchanged) — throws AbloStaleContextError.
    Migration:
    • onStale: 'force'onStale: 'overwrite'.
    • onStale: 'flag' / onStale: 'merge'onStale: 'notify' (both removed; notify is the single hold-and-surface mode).
    StaleNotification — the new advisory signal. New public type + staleNotificationSchema: { object: 'stale_notification', model, id, readAt, observedSyncId, conflictingFields, currentValues, writtenBy, group? }. Delivered two ways:
    • on the receipt — CommitReceipt.notifications (and CommitResult.notifications);
    • on a new SDK event — conflict:notified { clientTxId, notifications } (mirrors reconciliation:needed / sync:rollback).
    Read-set (reads[]) — declare what you looked at, not just what you write (new). A commit may carry batch-level read dependencies; a moved premise fires that entry’s onStale over the whole batch (notify holds every write + notifies, reject aborts, overwrite proceeds). Two granularities:
    • Row{ model, id, readAt, fields? }: did this row (optionally these fields) change?
    • Group{ group, readAt }: did anything in this sync group (deck:abc, org:X) change? — the same unit a participant watches and claims.
    New public type ReadDependency + readDependencySchema; available on ablo.commits.create({ operations, reads }) and the lower-level write options. This closes the gap the write-target check alone could not: a premise that changed without the written row changing. Conflict policy. ConflictDecision gains { action: 'notify' }; defaultPolicy maps onStale: 'notify' → notify-and-hold, everything else → reject. StaleContextConflict.requestedMode is added so custom policies can honor the caller’s declared intent.
  • Data Source reverse-channel connector (new). A customer Data Source can now dial out to the engine over a single outbound WebSocket (ablo.source.v1 subprotocol) instead of exposing an inbound HTTP endpoint — the deployment shape private/VPC stores need.
    • createSourceConnector({ apiKey, handler, baseURL? }) (new public API, exported from the root and /source) — opens one outbound socket (Node global WebSocket, no new dependency), with reconnect/backoff, and serves the customer’s existing Data Source handler.
    • Server side: a connector registry + /v1/source/listen upgrade route bridge requests down / responses up, teed into SourceClient through the storage resolver.
    • Trust model unchanged: the Standard-Webhooks HMAC is signed above the transport, so the socket carries the signed envelope byte-for-byte and the customer’s verifyAbloSourceRequest is untouched. Transport changes, trust model doesn’t.
    • Opt-in per source via reverse_channel_prod (migration 20260622150000); gated in authorizeUpgrade.
June 21, 2026
v0.14.0

Minor Changes

  • Claim API consistency + coordination docs
    • React: document useWatch (scoped presence + read-interest, with claim/hydrate/paused options) and usePeers (read-only presence) — previously exported but undocumented.
    • HTTP claim surface: HttpClaimApi is now a mechanically derived async projection of the reactive ClaimApi (AwaitedClaimMethod), so the two transports can never drift. No behavior change — the only difference remains the Promise wrapper that statelessness forces on state/queue/reorder.
    • Naming: unified the claim read verb to state across every layer (the internal ModelCollaboration.observe is now state, matching the public ablo.<model>.claim.state({ id })).
    • Docs: corrected the Claim object reference — the field is reason (serialized on the wire as action), and createdAt/expiresAt are number (epoch-ms), not strings; corrected the claim options to reason and queue.
June 17, 2026
v0.13.0

Minor Changes

  • Schema authoring: split model routing into two orthogonal axes — policy (row access) and groups (sync-group routing). Breaking (schema authoring). The flat, collision-prone model options are replaced by two namespaced ones:
    • policy — row-access / tenant isolation (named after Postgres/Supabase RLS policies: the rule that scopes which rows a tenant may read). A discriminated union on by replaces the old orgScoped / scopedVia / orgColumn trio:
      • { by: 'column' } — row-local tenancy column (the default when omitted; column name still overridable).
      • { by: 'parent', fk, parent } — inherit tenancy through a foreign key when the table has no tenancy column of its own (e.g. slide_layersslides).
      • Type TenancyInput is renamed PolicyInput; policyInputSchema / resolvePolicy are now exported.
    • groups: { root, grants, roles } — which delta channels a row fans into (orthogonal to policy, which governs read access). One namespaced object replaces the old flat scope / grants / entityRoles:
      • root (was scope) — mark a model a scope root; its records form the group <kind>:<id>. Renamed so it no longer collides with the old scopedVia tenancy sugar or the inner grants.scope relation name.
      • grants — a membership edge granting an identity access to a scope root.
      • roles (was entityRoles) — explicit non-relational record→group roles; accepts one role or an array.
      • groupsInputSchema / GroupsInput are now exported.
    CLI. config.json now stores per-project profile key pairs (profiles: Record<string, ProfileKeys>) instead of a single top-level pair; older flat layouts are folded into the active profile automatically on read, so existing logins keep working. login / projects updated to the profile model.
June 16, 2026
v0.12.0

Minor Changes

  • Canonicalize the claim API to one vocabulary, plus DX fixes (breaking).
    • BREAKING: claim phase field actionreason on every claim surface (Claim, ClaimHandle, ClaimCreateOptions, ModelClaim, …). The wire is unchanged (still action, healed on read) — no server redeploy needed.
    • BREAKING: claim contention flag waitqueue (one word everywhere).
    • BREAKING: React hook useParticipantuseWatch (aligns with ablo.<model>.watch).
    • ClaimDeclaration.ttlSeconds is now number (was a Duration).
    • Docs: retrieve HTTP envelope (.data/.stamp) called out; syncGroups reworded (provisional, not deprecated); orgScoped cross-tenant security warning; React error strings point at <AbloProvider>.
June 16, 2026
v0.11.2

Patch Changes

  • a35d935: Fix stream-recorded undo capturing the wrong “before” value for updates. A second update to the same field before the first sync-ack re-captured the original pre-session value (first-old-wins + clear-only-on-ack), so undo of a quick second edit jumped all the way back instead of one step. The queue now re-baselines a field’s tracked .old once its before-image is frozen into the committed transaction. Also close the create/update undo asymmetry: an update whose written key had no in-place mutation produced an empty previousData, which made the inverse un-revertible (a create’s delete inverse never is). Before-image capture now falls back to the last loaded/acked snapshot. Internally, the two undo paths (stream-recorded and manual RecordingTransaction) now share one before-image implementation via Model.capturePreviousValues / Model.consumeModifiedFields, so they can no longer drift.
  • One-correct-way consolidation (breaking; no external consumers yet, so released as a patch):
    • Credentials collapse to a single apiKey — a string, or a () => Promise<string | null> that fetches a per-user token. Removed getToken / authEndpoint / public authToken.
    • ablo.<model>.watch(ids, { ttl }) replaces the top-level ablo.participants.join({ scope }) — model-scoped read-interest + presence (WebSocket only).
    • Read claim-gating is ifClaimed: 'return' | 'fail' (removed 'wait'); waiting is the claim primitive’s job (ablo.<model>.claim).
    • The stateless client is Ablo({ transport: 'http' }); createAbloHttpClient is no longer a public export (the factory uses it internally).
    • Read-option types renamed: ServerReadOptions (server retrieve/list) and LocalReadOptions (local get/getAll).
    • defineSchema throws a clear error on a reserved-field collision; the MCP/docs API surface is now compile-time bound to the real exported types (can’t drift).
June 15, 2026
v0.11.1

Patch Changes

  • 7f91f6e: DX hardening from a real onboarding session — onboarding, CLI, coordination, types, and docs. Client behavior
    • databaseUrl is now an explicit, server-only option: Ablo(...) no longer auto-reads process.env.DATABASE_URL. A stray DATABASE_URL (common — Prisma/Drizzle/docker set it) no longer silently flips the client into connection-string mode; a one-time warning points at the explicit option. Passing databaseUrl: process.env.DATABASE_URL explicitly is unchanged.
    • Claims/presence are now observable from any client (including Node agents): reading a row enters its entity sync group (read-interest) and claiming pins it (write-intent), so ablo.<model>.claim.state({ id }) reports co-participants without any manual subscribe step — whether the observer arrives before the claim (live delta) or after it (subscribe-time backfill). The claim holder now also sees its own claim via claim.state. Requires a coordinated sync-server deploy (the subscribe-time claim backfill + the entity-scope subscription gate that lets an org-authority agent key narrow into a row’s group live server-side); the client package change alone does not deliver cross-client agent observation.
    CLI
    • ablo init detects the src/app layout (routes + the @/ablo import alias resolve correctly), writes the real stored sandbox key into .env.local instead of a placeholder, and scaffolds ablo/register.ts (a regular module, not a colliding ablo.d.ts).
    • ablo <command> --help / -h now prints usage instead of erroring with “unknown flag”, and migrate is listed in the top-level help.
    • ablo dev --no-watch now exits after one push instead of watching forever.
    Types
    • Name the client with typeof sync (the value-inferred idiom, like tRPC’s typeof appRouter / Drizzle’s typeof db) — ReturnType<typeof Ablo> collapses to the untyped client and should not be used. No bespoke client-type generic is needed.
    • model_claim_not_configured message clarified: claiming needs no per-model schema configuration; every model is claimable through the standard client.
    Docs
    • Reconciled the self-contradictory databaseUrl story (it is an explicit, server-only option, not auto-read from the environment; consistent casing), documented that the sandbox can host rows (apiKey only, no database), explained why a localhost Postgres can’t be the system of record, and led the connect-your-database flow with ablo pull/ablo check over ablo migrate. Fixed stale api.md vocabulary (object: 'claim', participantKind: 'user' | 'agent' | 'system').
  • 7f91f6e: Docs: document the completed intentclaim rename. Adds a 0.11.0 migration entry (useIntentuseClaim, Register.IntentsRegister.Claims, Ablo.Intent.*Ablo.Claim.*, and the coordinated client/server deploy for the claim_* wire frames), a useClaim section in the React reference, and fixes the stale participantKind union to the canonical 'user' | 'agent' | 'system'.
June 15, 2026
v0.11.0

Minor Changes

  • Canonical claim vocabulary, sync-group area-of-interest, and richer claim-rejection errors.
    • intentclaim everywhere. The coordination primitive is now a Claim across the public surface: useClaim replaces useIntent, the Ablo.Claim.* namespace replaces Ablo.Intent.*, and module augmentation registers Claims instead of Intents on the Register interface. The underlying wire frames moved from intent_* to claim_* — clients and servers must run a claim_*-aware build together.
    • Sync-group area of interest. A client’s read interest is no longer frozen at connect: the new update_subscription frame drives live re-indexing, and enterScope / leaveScope / pinScope / unpinScope let a store narrow or widen what it streams. AreaOfInterestManager adds hysteresis (warm-TTL), claim-pinning, reconcile coalescing, and an LRU cap so narrowing the view never shrinks the write allowlist.
    • Richer claim-rejection errors. Rejections (over WebSocket and HTTP) now carry heldByClaim and policyReason, and AbloClaimedError exposes a typed claims array so callers can see exactly who holds the contested rows.
    • Coordination vocabulary consolidation. Participant identity is canonical user | agent | system; the server stamps participantKind on every presence emit and clients read it, so non-human peers surface correctly.
June 12, 2026
v0.10.1

Patch Changes

  • Docs: add the 0.10.0 entry to the Version History & Migration Guide — the test/livesandbox/production environment enum rename (key prefixes unchanged) and the new transport: 'http' stateless client.
June 12, 2026
v0.10.0

Minor Changes

  • Rename environment enum values to production and sandbox while preserving the existing *_live_/*_test_ key prefix format.

Patch Changes

  • Stateless HTTP transport for server-side actors, and a canonical environment vocabulary.
    • Ablo({ transport: 'http' }) returns a stateless AbloHttpClient for agents, workers, and serverless — the same ablo.<model> surface and coordination plane with no websocket: each call is one HTTP round-trip and identity rides the Bearer credential. The return type narrows so stateful-only APIs (get/getAll/onChange) are compile errors instead of latent runtime gaps.
    • Canonical production / sandbox environments (new environment.ts, exported from the root): sk_test_ / sk_live_ remain the wire-level key prefixes but now map to production / sandbox everywhere — key parsing, source mode, and the CLI (which drops the legacy test/live config migration).
    • Source-mode commit scoping: commit now forwards projectId, accountScope, and environment to customer storage resolvers, so per-project and sandbox/production traffic can be routed to distinct stores.
    • Fixes: the WebSocket bearer credential is sent in the ablo.bearer.<token> subprotocol (never in the URL or proxy logs); Model no longer fabricates an updatedAt of “now” for records that arrive with only createdAt.
June 12, 2026
v0.9.15

Patch Changes

  • Package metadata: set the npm description to “The Collaboration Layer For AI Agents” (matching the GitHub repo About) so it stops reverting to the old “State control API…” text on publish.
June 11, 2026
v0.9.14

Patch Changes

  • README: replace the schema -> ablo.<model>... pseudo-diagram with a real typed snippet (create/retrieve/update/claim), and tidy the Get-started line.
June 11, 2026
v0.9.13

Patch Changes

  • Per-project axis: schemas, planes, routing, and enforcement scoped per project. Adds the control plane, per-project key scoping with identity threading, a remove_model gate, and the CLI/docs to drive it.
June 11, 2026
v0.9.12

Patch Changes

  • README: point the Docs / Quickstart / API header links at docs.abloatai.com (the real docs) instead of abloatai.com, which 307-redirects to the marketing site.
June 11, 2026
v0.9.11

Patch Changes

  • Model<'name'> type helper via the Register binding — name your model in one parameter (Model<'tasks'>) instead of restating typeof schema; Model<S, 'name'> is also supported and InferModel is deprecated. CLI: retire the stale dev wording from the login outro and push header. Docs: cover the Register binding end-to-end and document the pk_ publishable key + the /v1/commits HTTP path.
  • 3024593: Fix sessions.create({ user }) 403 — user sessions now mint via the sk_-gated ephemeral-key door
    • sessions.create({ user }) mints an ek_ user session via /auth/ephemeral-keys (was wrongly routed through /auth/capability, which rejects human participants — writes were being attributed to agents).
    • Control-plane calls always present your original sk_, never the client’s exchanged sync credential.
    • sessions.create({ agent, can }) no longer requires hand-built syncGroups — the org anchor is the server default — and the can allowlist is now honored at commit time (model-alias matching).
    • New: ablo.organizationId (resolved after ready()), ablo status --json, typed sync-group inputs (SyncGroupInput + invalid_sync_group rejection for malformed groups).
v0.9.10
v0.9.10

Patch Changes

  • README: add a centered brand header (Ablo banner, tagline, doc nav links, and status badges).
June 11, 2026
v0.9.9

Patch Changes

  • Docs: version history & migration guide refinements plus changelog, audit, and link fixes.
v0.9.8
v0.9.8

Patch Changes

  • Docs: add a Version History & Migration Guide, bring the changelog current, and sync doc trees. Drop the dormant causedByTaskId from the audit-row docs and fix the ablo mode argument vocabulary.
June 11, 2026
v0.9.7

Patch Changes

  • Docs: fix the commits.create operation shape to the public { action, model, data } form.
June 11, 2026
v0.9.6

Patch Changes

  • CLI quickstart simplification (3 commands). init now owns login, migrate is dropped from the direct-databaseUrl quickstart (dev handles it), and the dev command is renamed to push for honest naming with headless-safe login. Note: ablo dev is now ablo push — update any scripts. Also fixes 3 production bugs surfaced by the new end-to-end journey test harness.
June 10, 2026
v0.9.5

Patch Changes

  • Scoped-role automation + tenant-routing fix. ablo migrate now auto-creates the RLS-gated scoped role (zero SQL) with a log-safe SCRAM-SHA-256 password verifier, plus a Neon/Supabase scoped-role databaseUrl recipe. Fix a jsonb double-encode that corrupted per-tenant routing and silently fell back to the shared pool.
June 10, 2026
v0.9.4

Patch Changes

  • Sync-position correctness + CLI hardening. Consolidate five scattered sync cursors into one typed syncPosition (persisted/applied/acked with a derived readFloor), fixing a claim taken right after an ack-confirmed write reading stale against that write’s own delta. Add transaction ack-confirmation, schema DDL-first-push, and a reworked CLI (config/dev/login/mode/drizzle-pull).
June 9, 2026
v0.9.3

Patch Changes

  • Onboarding: quickstart leads with your-own-database (Drizzle Data Source), drop Ablo-managed mode, add ablo push step; context7 library-claim config.
June 9, 2026
v0.9.2

Patch Changes

  • Developer-onboarding overhaul so an LLM or a person gets a working integration on the first try.
    • ablo init scaffolds a project that builds and is current-API. The Next.js scaffold now ships app/providers.tsx + an app/api/ablo-session route, uses useAblo (the removed withSync is gone), object-param verbs, and never bundles your sk_ key into the browser. The webhook receiver moved off the [...all] catch-all.
    • Agent docs are accurate and ship. AGENTS.md, llms.txt, and llms-full.txt are on the 0.9.x API (object-param create/update/delete/retrieve, disposable await using claim, AbloProvider client prop), lead with ablo init, and AGENTS.md now ships in the package.
    • ablo push is self-documenting. Writing to a model the server hasn’t seen now fails with an error that tells you to run ablo push (the server_execute_unknown_model / unknown_model messages), instead of a cryptic “unknown model.”
    • intents is deprecated in favor of claim everywhere the docs and the MCP scaffold/prompts teach or generate coordination; the public ablo.intents accessor is marked @internal.
    • Docs say Node 24+, and the drizzle-orm peer floor is >=0.44.
  • a88747a: Remove the turn primitive and the agent-work tasks resource from the client surface — the SDK is now purely ablo.<model> + claim. Breaking
    • engine.beginTurn(), the Turn handle interface, and the Ablo.Turn type are removed. AbloApi.beginTurn and the HTTP client’s beginTurn are gone too.
    • CommitCreateOptions.causedByTaskId is removed. (Lineage is no longer stamped from the client.)
    • The engine no longer exposes a protocol accessor or a public tasks work-unit resource. ablo.tasks is, and always was, the schema tasks model proxy.
    • The agent().run() helper and the low-level agent/task type family are removed: AbloApi.agent(id, options) and AbloApi.tasks (the TaskResource), plus the exported types Agent, AgentOptions, AgentRunOptions, AgentRunResult/Done/Failed/Cancelled, AgentRunStatus, AgentRunContext, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentIntentOptions, AgentIntentInput, Task, TaskResource, TaskCreateOptions, TaskCloseOptions, TaskCloseResult (and the Ablo.* namespace aliases for all of them). The Ablo.Auth.Agent principal constructor and the schema-backed tasks model are unaffected.
    Why turn/agent_tasks was a second coordination-and-attribution mechanism living alongside claim. It is redundant on the client:
    • claim already serializes writers and carries the causal link — its intent id rides on every guarded write.
    • The server stamps actor / onBehalfOf / capabilityId onto each delta from the auth context.
    • Per-run token/cost is recorded in Langfuse, not the agent_tasks table.
    So the only thing the client lost is the audit pane’s “show everything this exact prompt produced” filter, which keyed off caused_by_task_id; new writes leave that column null. Migration Agents stop opening/closing tasks — just issue ablo.<model> writes (schema-backed) or ablo.commits.create(...) (schema-less) under a claim. Replace Ablo({ apiKey }).agent(id, opts).run(prompt, handler) with: mint a scoped credential via sessions.create({ agent }), then claim the row and update / commits.create. The server agent_tasks table, the caused_by_task_id delta column, the /api/sync/commit wire field, and the agent_actions_log compliance hash-chain remain in place but dormant (client writes leave the field null) — they are load-bearing for the tamper-evident audit chain and historical-row audit JOINs, so they are intentionally NOT dropped. The dead /v1/tasks + /api/agent/turn route handlers ARE removed (zero live callers).
June 8, 2026
v0.9.1

Patch Changes

  • 90b656c: drizzleDataSource now takes (db, schema) and derives snake_case columns from your schema, so it composes with ablo migrate with no parallel Drizzle table. Update calls from drizzleDataSource(db, tables)drizzleDataSource(db, schema). Also adds the snakeToCamel export and provisions the adapter’s ablo_outbox / ablo_idempotency tables via ablo migrate.
v0.9.0
v0.9.0
A single options object for every model verb, and a disposable claim handle.

Breaking Changes

  • One options object per verb. create, update, delete, and the async server retrieve each take a single options object instead of positional arguments, so the id, the data, and every modifier live as named siblings: create({ data, id? }), update({ id, data, ...options }), delete({ id, ...options }), retrieve({ id, ...options }). Reactive local reads stay on get(id) (synchronous) — useAblo((ablo) => ablo.tasks.get(id)).
    - await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
    + await ablo.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' })
    
    - await ablo.tasks.retrieve(id)
    + await ablo.tasks.retrieve({ id })
    
    - useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
    + useAblo((ablo) => ablo.tasks.get(id)) ?? serverTask
    
  • claim returns a disposable handle instead of taking a callback. The handle exposes the fresh row on .data and is released on scope exit (await using) or explicitly via .release(). claim.state, claim.queue, claim.release, and claim.reorder also take the options object.
    - await ablo.tasks.claim(id, async (task) => {
    -   await ablo.tasks.update(task.id, { status: 'in_review' })
    - })
    + await using claim = await ablo.tasks.claim({ id })
    + const task = claim.data
    + await ablo.tasks.update({ id: task.id, data: { status: 'in_review' } })
    
v0.8.0
v0.8.0
A callable claim coordination namespace and bring-your-own-database support via a new databaseUrl option.

Minor Changes

  • Callable claim coordination namespace. Taking a claim and inspecting its state now live under one accessor: claim(id, work) acquires a claim and runs work while it’s held, and claim.state(id), claim.queue(id), claim.release(id), and claim.reorder(id, order) cover the surrounding lifecycle. The README leads with the problem (who is allowed to act, and in what order) and the Quick Start now demonstrates claim directly.
  • Bring-your-own-database via databaseUrl. Point a project at your own Postgres with Ablo({ schema, apiKey, databaseUrl }). Ablo writes synced rows back into your database, so your data stays canonical. Server-side only; defaults to process.env.DATABASE_URL. See the data-sources guide for setup and role requirements.

Breaking

  • The flat coordination methods claimState, queue, release, and reorder are removed in favor of the claim namespace above.
    - await ablo.task.claimState(id)
    - await ablo.task.release(id)
    + await ablo.task.claim.state(id)
    + await ablo.task.claim.release(id)
    
v0.7.0
v0.7.0

Minor Changes

  • Structured error contract, schema/migration engine, and a full ablo CLI.
    • Structured error contract across HTTP + WS planes. A closed, canonical error-code registry is now the code tier of a Stripe-style error model. A single HTTP egress funnel converts every throw to a canonical { type, code, message, doc_url, request_id, ...details } envelope; the WS plane narrows mutation/claim error codes to the same union.
    • Versioned contract + drift guard. ERROR_CONTRACT_VERSION (date-based) ships in errors.json and on the Ablo-Version response header, so consumers detect contract changes without diffing docs. Generated errors.mdx / errors.json plus a CI drift guard keep the docs, OpenAPI spec, and SDK from silently diverging from the registry.
    • Always-on request correlation. Every response carries a req_… request id (honoring an inbound x-request-id), stamped into the envelope’s request_id.
    • OpenAPI parity. The stale { error, reason } schema is replaced by the canonical envelope plus a generated ErrorCode enum.
    CLI + schema:
    • Schema diff + migration planning engine (generateProvisionPlan / generateMigrationPlan in @abloatai/ablo/schema) — pure diff, classify, apply, and constant-value backfill for required-field migrations.
    • ablo generate — emit TypeScript types from the pushed schema.
    • Full ablo CLI suite, Stripe-CLI-shaped: init, login / logout / status, mode [test|live], dev (push schema to the test sandbox + watch), logs (tail your scope’s commit activity), and the data-source commands below. Authentication is the OAuth 2.0 device flow; login provisions and stores a test and a live key, and mode switches the active one.
    • Database-URL structure (bring-your-own-database). The CLI is split by where it writes:
      • ablo pull / ablo check / ablo migrate operate on your own DATABASE_URLpull introspects it to emit defineSchema(...) from existing tables (read-only, like prisma db pull), check verifies tables fit the schema with no DDL, and migrate applies DDL to DATABASE_URL.
      • ablo schema push / ablo dev target the hosted test/live sandbox; the server diffs, migrates, and activates the uploaded schema. dev never touches live data.
    BREAKING — removed the legacy React hooks useQuery / useOne / useMutate / useReader. Use useAblo() + ablo.<model>.* instead. The MutateActions, ReaderActions, and ReaderFindOptions types are still re-exported for callers that referenced them.
v0.6.0
v0.6.0

Minor Changes

  • 0f663e7: Coordination surface: fair queue, reactive wait-line, and lease renewal.
    • Claims acquire through a server FIFO queue. On contention a claim waits its turn and re-reads before proceeding; reads are never blocked. Writes blocked by another participant’s claim throw a typed AbloBusyError.
    • ablo.<model>.queue(id) — reactive read of the wait-line behind a row: who’s queued, their action, and FIFO position. Synced to peers like activity(id).
    • Backpressure on claim{ wait: false } skips instead of waiting if the row is already held (claim-or-skip dedup); { maxQueueDepth: n } bails with AbloBusyError('queue_too_deep') rather than joining a line already that deep.
    • Lease renewal — a held claim renews automatically while the holder’s connection is alive, so you never size a TTL; it lapses only after the holder goes silent. A queued claim that’s abandoned is dequeued (no ghost waiters).
    • Reads are never gated by a claim, including for agents.
    • Intent vocabulary cleanup: a waiting claim is an Intent with status: 'queued' (position carries its place in line). Removed the unbuilt whenFree.
  • BREAKING — API renames (apply when upgrading from 0.5.1):
    • Change-listeners renamed to .onChange(...): ablo.<model>.subscribe(cb), presence.subscribe(), intents.subscribe().onChange(...). (subscribe is reserved for an upcoming scope-grant verb.)
    • Row-access API renamed Resource → Model: Ablo.Resource.*Ablo.Model.*, ablo.resource(name)ablo.model(name), ModelTarget.resourceModelTarget.model, error code resource_not_foundmodel_not_found.
v0.5.1
v0.5.1

Patch Changes

  • Docs: add a React quick-start (provider + useAblo), plain-language rewrite, and a “Set up with Claude Code” section.
v0.5.0
v0.5.0

Minor Changes

  • 9154c1b: Rename intent handle methods to a clearer claim vocabulary; add AbloProvider bootstrapMode. BREAKING — on the model intent handle (ablo.<model>.intent(id)): acquireclaim, acquireOrAwaitclaimOrWait, settledwhenFree, releasefinish, revokecancel. The lower-level IntentHandle / IntentLeaseHandle (ablo.intents.*) are unchanged. Also: AbloProvider gains a bootstrapMode prop ('full' | 'none') to skip the baseline pull on read-light pages; StaleContextConflict gains an optional conflictingFields; README + JSDoc clarity pass and a new HTTP API section.
v0.4.0
v0.4.0

Minor Changes

  • Per-entity coordination intents on the model accessor. Coordinate writes to an entity through the same accessor you read it with — ablo.<model>.intent(id), returning a ModelIntentHandle. Intent state is one self-describing object ({ object: 'intent', id, status, target, action, heldBy, participantKind, createdAt?, expiresAt? }) with a single lifecycle: status: 'active' | 'committed' | 'expired' | 'canceled'. An active intent is the lock.

    Added

    • ablo.<model>.intent(id)ModelIntentHandle<T>, beside create / update / retrieve / load on every model.
      • Read side (any participant, synchronous + reactive): current (the holder’s intent, or null), status ('idle' when free), settled().
      • Write side (the holder): acquire(), acquireOrAwait(), lease-guarded update(), release(), revoke().
      • AsyncDisposable: await using lock = ablo.<model>.intent(id) auto-releases on scope exit.
    • acquireOrAwait() — serialize-on-contention: take the lease, or wait out the current holder, re-read the changed row, then take it. The caller never branches on who holds the target — it just gets the target safely. Bind it to an agent’s write-tool boundary so agents never reason about coordination.
    • New exports: ModelIntentHandle, ModelIntentAcquireOptions.

    Changed

    • acquire() is fire-and-forget over the socket — it does not throw on conflict. Resolve contention with acquireOrAwait() (wait) or read current for a reactive “who’s editing” badge, rather than catching a rejection.

    Deprecated

    • Participant-level intents.claim() / onRejected() and the intent_rejected wire frame still work but are superseded by the per-model handle. Their removal is a future breaking change.

Unreleased

Schema-driven identity sync-group composition, plus a terser capability surface.The convention for deriving a participant’s allowed sync-groups from its identity is now declared on the consumer’s schema as an open registration. Consumers with a { regionId, customerId } identity shape declare their own roles instead of receiving any built-in prefixes from the SDK.Capability fields shed their redundant allowed prefix to match the surrounding vocabulary — capability inputs always describe what the bearer can touch, so the prefix was doing no disambiguation work for the consumer.

Added

  • DefineSchemaOptions.identityRoles?: readonly IdentityRole[] — open registration of identity-anchored sync-group roles on defineSchema(...). Each IdentityRole declares { kind, template, extract }: a diagnostic label, a '<prefix>:{id}' template, and a pure extractor function from an opaque identity context to zero-or-more ids. No closed enum; consumers fully control both the template strings and the extraction logic.
  • composeIdentitySyncGroups(identity, schema) exported from @abloatai/ablo/schema — walks the schema’s registered identityRoles, calls each extractor, and substitutes ids into templates. Stable, deduped output. Returns [] when no roles are registered.
  • Schema.identityRoles: readonly IdentityRole[] — the registered list, accessible on every defineSchema(...) result.
  • New exported types: IdentityRole, IdentityContext.

Breaking

  • capabilities.create({ allowedSyncGroups, allowedOperations })capabilities.create({ syncGroups, operations }). Both fields renamed at every public surface — capability create input, capability retrieve response, capability record, Identity returned from AuthProvider. Hard rename, no alias. Update the call sites; the field semantics are unchanged.
    // Before
    await api.capabilities.create({
      allowedSyncGroups: ['org:acme'],
      allowedOperations: ['tasks.update'],
      lease: '10m',
    });
    
    // After
    await api.capabilities.create({
      syncGroups: ['org:acme'],
      operations: ['tasks.update'],
      lease: '10m',
    });
    

Changed

  • docs/integration-guide.md §1 now shows identityRoles in the canonical defineSchema example plus a “Declaring scope on a model” subsection covering orgScoped / scopedVia / syncGroupFormat. docs/capabilities.md, docs/api.md, docs/mcp.md, and AGENTS.md cross-reference the identityRoles section and use the renamed fields throughout.
v0.3.0
v0.3.0
Umbrella <AbloProvider> for React apps. One provider component now owns the full lifecycle — singleton rotation on auth change, Strict-Mode-safe bootstrap, beforeunload cleanup, session-expiry IndexedDB wipe, post-bootstrap hooks, mesh client construction. Replaces the ad-hoc provider glue every consumer had to write themselves.Declarative props absorb every class of lifecycle glue; the status hook returns a tagged union so impossible states are unrepresentable. The reference integration shrank from 515 LOC of hand-rolled singleton/AbortController/beforeunload/reaction-bridge wiring to a 60-LOC thin wrapper that just passes props through.

Added

  • <AbloProvider> — umbrella provider at @abloatai/ablo/react. Props include data config (schema, url, userId, organizationId), auth (capabilityToken / apiKey / session cookie fallback), declarative behavior (preventUnsavedChanges, lostConnectionTimeout, postBootstrap), callbacks (onSessionExpired, onError, resolveUsers), and DI escape hatches.
  • <SyncGroupProvider id="matter:..."> + useSyncGroup() — per-entity scope context.
  • <ClientSideSuspense fallback={...}> — gate renders until the engine reports connected. Phase-1 non-Suspense; phase-2 upgrades to real Suspense.
  • useSyncStatus() rewritten as a tagged union: { name: 'initial' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'needs-auth', ... }. Impossible states are unrepresentable.
  • useCurrentUserId() — returns the userId prop. Replaces downstream consumers’ defineProperty hacks on the store.
  • useErrorListener(cb) — imperative error callback (Sentry/Datadog).
  • useSync<R>() and useSyncStore<T>() accept generic parameters so consumers can widen to their concrete schema types without as unknown casts at call sites.
  • BaseSyncedStore.purge() / SyncEngine.purge() — disconnect + wipe every ablo_* / ablo-* IndexedDB. Called automatically on session expiry.
  • SyncEngine.onSessionError(listener) — subscribe to session-error events. Multiple subscribers supported.
  • Commit payload projection built into TransactionQueue. Mutations are automatically projected onto the model’s schema-declared fields (dropping framework internals __class / __typename / clientId / syncStatus and anything not declared), with field.json() values auto-stringified for TEXT columns and undefined dropped on updates. No config port, no consumer hook — the SDK derives correct wire payloads from the schema alone. Apps that previously maintained hand-rolled extractor tables can delete them entirely.

Breaking (continued)

  • Removed SyncEngineConfig.extractCreateInput and SyncEngineConfig.buildUpdateInput. The SDK’s built-in projection replaces them. Consumers who passed these in configOverrides should delete the override; the default now covers 100% of identity-column mutations. The configOverrides prop still exists but its remaining fields are all deprecated (see below) and scheduled for removal in v0.4.

Deprecated (vestigial — removal in v0.4)

  • SyncEngineConfig.modelCreatePriority, defaultCreatePriority, defaultNonCreatePriority — never read at runtime.
  • SyncEngineConfig.batchableModels — never read at runtime.
  • SyncEngineConfig.dedicatedDeleteModels — never read at runtime.
  • SyncEngineConfig.preserveCaseModels — never read at runtime.
  • SyncEngineConfig.essentialFields — used only in debug logging, no behavioral effect.
  • SyncEngineConfig.classNameFallbackMap — dead path; ModelRegistry.registerModelsFromSchema registers by constructor identity, bypassing the class-name fallback entirely.

Breaking

  • Removed <SyncProvider> — folded into <AbloProvider>. Migrate by swapping the provider and passing userId/organizationId/url instead of a pre-constructed store.
  • Removed createAbloContext() factory and its returned AbloProvider / useAblo / useParticipant triple. Mesh is now always-on inside <AbloProvider>; useAblo() and useParticipant(opts) are always available. Schema-typed mesh hooks are on the roadmap.
  • Removed withSync (no-op alias of observer). Import observer from mobx-react-lite directly if needed.
  • Removed useSyncContext from the public surface (never used outside the SDK’s test helpers).
  • useSyncStatus() return shape changed from six booleans to a tagged union. Migration: const { isReady } = useSyncStatus()const status = useSyncStatus(); const isReady = status.name === 'connected'.
  • SyncStoreContract gained six sync-status getters and a syncStatus field. Third-party classes implementing the contract must add these (additive for callers).

Migration

// Before (0.2.x)
const { AbloProvider, useAblo, useParticipant } = createAbloContext<typeof schema>();

function Root() {
  const sync = createSyncEngine({ url, schema, user });
  const ablo = new Ablo({ schema });
  return (
    <SyncProvider store={sync._store} organizationId={orgId}>
      <AbloProvider ablo={ablo}>
        <App />
      </AbloProvider>
    </SyncProvider>
  );
}

// After (0.3.0)
function Root() {
  return (
    <AbloProvider
      schema={schema}
      url={url}
      userId={userId}
      organizationId={orgId}
      preventUnsavedChanges
      onSessionExpired={() => router.replace('/signin')}
    >
      <ClientSideSuspense fallback={<Skeleton />}>
        <App />
      </ClientSideSuspense>
    </AbloProvider>
  );
}
No breaking change to useQuery / useOne / useMutate / useReader / useMutators / useUndoScope / usePresence / useIntent — call sites remain source-compatible.
v0.2.1
v0.2.1
React bindings hardening. Fixes two infinite-loop classes that surfaced in downstream apps as React error #185 (“Maximum update depth exceeded”), and exposes sync-status reactivity as a first-class observable + hook.

Fixed

  • useQuery / useOne no longer loop on getSnapshot. The useSyncExternalStore adapter was returning a fresh view.results.slice() on every call, which React’s post-commit consistency check interpreted as “store updated mid-render” — scheduling another render, another snapshot, another mismatch, ad infinitum. The snapshot is now cached in a ref and only refreshed inside the subscribe callback right before onChange() fires. Affected every tree with multiple simultaneous useQuery subscribers.

Added

  • BaseSyncedStore sync status is now properly observable. syncStatus and dataReady are annotated observable; isReady, isSyncing, isOffline, isReconnecting, isError, hasUnsyncedChanges are computed. Before, these were plain getters over plain fields — reaction(() => store.isReady, ...) silently never fired. Existing observer / reaction call sites that relied on the implicit pool.size trigger will continue to work; new call sites should read these observables directly.
  • useSyncStatus() React hook. Returns { isReady, isSyncing, isOffline, isReconnecting, isError, hasUnsyncedChanges } as a reactive snapshot, bridged via useSyncExternalStore with a correctly-cached snapshot. Replaces hand-rolled reaction bridges in consumer providers. See docs/react.md.
  • SyncStoreContract surfaces the status getters so TypeScript autocomplete works from the useSyncContext() return value without a cast.

Documentation

  • llms.txt and docs/react.md gained a “Common pitfalls” section covering the three traps this release addresses: don’t wrap providers in observer(), getSnapshot must return a cached reference, and sync-status fields are real observables (don’t watch pool.size as a proxy).

Migration

No breaking changes. Optional: replace any local reaction(() => store.isReady, setReady, { fireImmediately: true }) bridges in your own providers with const { isReady } = useSyncStatus() for consumers below the store provider.
v0.2.0
v0.2.0
Mesh SDK — the canonical agent-multiplayer surface. Locked at this release; further work is consolidation, not expansion.

What’s frozen

The SDK covers exactly three integration shapes. Each has a canonical example in examples/:
  1. Server agentnew Ablo({ schema }) reads ABLO_API_KEY, joins and works. (examples/server-agent.ts)
  2. Browser app — server mints a scoped capability, browser holds it via new Ablo({ schema, capabilityToken }). No API key in bundle, no session cookies, no allowed-origins registration required. Stripe client_secret shape. (examples/browser-app.ts)
  3. Sub-agentparent.join(child, opts) attenuates from the parent’s capability. (examples/sub-agent.ts)

Ergonomics (package-wide)

  • Ablo classimport Ablo from '@abloatai/ablo' / new Ablo({ schema }). Matches new Stripe() / new OpenAI() / new Anthropic() pattern. createMesh(opts) stays available as the functional alias.
  • Model-scoped joinsablo.matters.join(id, { label }) desugars to the generic join. Proxy-based so the namespace adapts to any schema. Collisions with reserved admin fields (roles, members, audit, capabilities) throw at construction time.
  • Flat scope formscope: { matters: id } alongside the array form.
  • as alias{ as: session({...}) } replaces the security-jargon onBehalfOf; both still accepted.
  • Auto-connectjoin() returns a connected participant. autoConnect: false to opt out.
  • Duration stringsttl: '3m', ttlSeconds: '24h' accepted alongside numbers.
  • Descriptive generics — every public type uses TSchema / TAgent / ModelName instead of S / A / K. Zero unknown in public types.

Coordination primitives

  • Presence verbsparticipant.presence.editing(target) / viewing(target) / idle(). Plus update({...}) escape hatch for custom actions.
  • Intent verbsparticipant.intents.editing(target, opts) / writing(target, opts). Returns an IntentHandle with Symbol.asyncDispose so await using work = ... auto-revokes.
  • Snapshotsconst snap = await participant.snapshot({ clauses: [id] }). Flat shape: snap.clauses[id] (typed from schema via InferModel, not unknown), snap.stamp, snap.signal (AbortSignal).
  • Async iterablesfor await (const peers of participant.presence), for await (const openIntents of participant.intents), for await (const delta of participant.deltas).

Env / config

  • ABLO_API_KEY — required for server-side use.
  • baseURL — optional override for private deployments / local-dev (defaults to wss://api.abloatai.com).
  • organizationIdno longer required in createMesh. The API key or session binds the caller to one org; the capability mint response echoes it back.
  • createMeshFromEnv — removed. new Ablo({ schema }) auto-reads env.

Test coverage

  • 53 mesh unit tests across 8 suites (__tests__/unit/mesh/)
  • New E2E test e2e-browser-capability-token.ts proves the server-mints / browser-holds flow end-to-end
  • Existing 12 mesh E2E tests (token refresh, watermark, chinese wall, etc.) still pass

v0.1.0
v0.1.0
Initial release.

Features

  • Schema DSL: Zero-codegen schema definition with full TypeScript inference (defineSchema, field, relation)
  • React Hooks: useModels, useModel, useMutations, withSync for reactive data binding
  • Consumer API: createSyncEngine() — one-liner setup that hides all internal wiring
  • Offline-first: IndexedDB persistence with automatic offline mutation queue and FK-safe flush
  • Real-time sync: WebSocket delta streaming with optimistic updates and rollback
  • AI Agent SDK: SyncAgent for backend/AI agent participation as first-class sync citizens
  • Pluggable auth: AuthProvider interface with built-in API key, JWT, and session providers
  • Security: IndexedDB cleanup on session expiry and sync group revocation
  • Testing utilities: @abloatai/ablo/testing subpath with mocks, fixtures, and harness

Test Coverage

  • 231 unit/integration/property/contract tests
  • 50 E2E tests against real Go server + PostgreSQL + Redis
  • Property-based testing via fast-check