v0.58.0
Reads now distinguish observation from decision input
get({ id }) returns the current row for display or inspection. read({ id })
returns the same row while privately retaining its model, id, and watermark so a
later write can prove exactly which state it depended on:
const report = await ablo.reports.read({ id: reportId });
await ablo.reports.update({
id: report.id,
data: { summary },
reads: [report],
});
If report changed in between, the update does not land and rejects with
AbloStaleContextError. get() and list() remain observational, while
onChange() remains the live notification surface.
This replaces the older retrieve() and durable track() model surfaces. It is
a public-surface break with no compatibility aliases:
- replace
retrieve({ id })withget({ id })for observation, orread({ id })when a later mutation depends on the row; - replace
track(...),CommitContext.track, and mutationtrack/onStaleoptions with captured rows passed through the mutation’sreadsoption; and - replace AI SDK
ToolModel.getwithToolModel.read.
Schema conflict-policy configuration and the agents*, humans*, and
system* policy constants are also removed, together with the supporting
conflict, stale-notification, persisted-read-set, and internal read-set exports.
Use an active claim when work must exclude another participant. Use read() and
reads when work may run concurrently but must not commit from a stale premise.
The removals announced by earlier releases now take effect too:
SourceRequestContext.requiredSyncGroups is gone in favor of syncGroups, and
DeltaPosition, deltaPositionSchema, ReadSetWatermark, and
readSetWatermarkSchema are gone in favor of LogPosition and
logPositionSchema.
Context follows exact reads and can stop stale work early
context() now assembles ordinary application values and exact Ablo reads into
one result with data, reads, and onChange. It no longer publishes a
context-level cursor or source classification. ContextResult.cursor,
ContextResult.sources, ContextChange, ContextSource, and
contextSourceSchema leave with that older model.
The reactive client’s cache-based snapshot() operation and Snapshot type are
removed. Call read() for every row an action depends on, assemble those values
with context(), and pass ctx.reads to the final write:
const ctx = await context({
ablo,
data: {
report: ablo.reports.read({ id: reportId }),
documents: searchDocuments(reportId),
},
});
const stop = ctx.onChange((error) => controller.abort(error));
try {
const summary = await generateSummary(ctx.data, controller.signal);
await ablo.reports.update({
id: reportId,
data: { summary },
reads: ctx.reads,
});
} finally {
stop();
}
onChange calls its listeners once when any captured row moves, which lets an
expensive model call stop early. The final write must still receive
reads: ctx.reads; that server-side check stays authoritative if notification
races the write or the connection drops.
Atomic commits.create() batches accept the same captured rows on both the
stateless HTTP and reactive WebSocket clients. Typed model claim handles can be
passed directly as a batch’s claim, without losing their row type. Disposing a
reactive client now also disposes its mutation queue and commit-lane timers.
Claims are available from the shell
The CLI can now acquire, queue for, inspect, heartbeat, and release a row lease:
npx ablo claims acquire reports report_123 --queue -- npm run reconcile
npx ablo claims list reports report_123
npx ablo claims release reports report_123
The -- <command> form keeps the lease alive only while the child process runs
and releases it on success, failure, or interruption. This lets an operator or
coding agent participate in the same coordination boundary as SDK clients
without first adding application code.
An agent run can carry the person who started it
An agent session can now name the person it acts for. Pass onBehalfOf when you
mint, and every write that agent makes records both principals: the agent as the
actor, that user as the delegator.
const { token } = await server.sessions.create({
agent: { id: agentId },
onBehalfOf: { user: { id: requestingUserId } },
can: { records: ['read', 'update'] },
syncGroups: [syncGroup('workspace', workspaceId)],
});
Until now the delegation chain root came from whoever called the mint. A browser
session minting for its own user recorded that person; a backend minting with a
secret key recorded nobody. Background work is the second case, so a queued
job’s writes arrived attributed to the agent alone, or to system where the
worker wrote around Ablo entirely. Ablo never sees your user directory, so it
cannot recover that identity afterwards.
The rule is about who may attest, not who may ask. A secret key already carries
organization authority, so it may name any user id, which is what lets a worker
resume a job somebody else started. A human session may name only its own user,
and naming another is refused. onBehalfOf on a capability that is not an agent
is refused as well.
Two habits make this hold in practice. Persist the user id on the job before you
enqueue it, because request context and in-memory arguments do not survive a
retry or a process boundary, and a durable job without its delegator can only
produce agent-only attribution. And do not quietly fall back to writing straight
to the database when minting is unavailable. Those writes are still observed,
but they carry no trusted correlation and are recorded as system.
Omit the field and sessions mint exactly as they did before.
The CLI has one credential input and a clearer project boundary
ABLO_API_KEY is now the CLI’s single explicit credential input. Management,
branch-runtime, and restricted-agent authority follow from the credential kind
and its server-side grant; ABLO_MANAGEMENT_KEY is no longer a separate input.
Keep management credentials at the control-plane boundary and pass only a
delegated, per-run rk_ credential into a sandbox or agent runtime.
After browser approval, ablo login now lets an organization with multiple
projects choose the project in the terminal. ablo login --project <slug> skips
the picker. Credentials remain fixed to the project that minted them, and the
CLI refuses to silently use another project’s stored key.
Database connection setup now handles more provider-specific PostgreSQL role constraints, repairs required role inheritance, registers local connectors through the control-plane boundary, and reports a missing Data Source API key as an authentication problem with a concrete fix. A branch still needs a database connected before its schema can be pushed.
Hosted test branches are explicit and temporary
Live integration fixtures can create an expiring test branch backed by Ablo’s
hosted log storage. Hosted storage must be requested explicitly and the branch
must expire within 24 hours. Ordinary customer branches remain unbound until
their own database is connected, so a test convenience cannot silently choose
where customer data lives.
The documentation now includes a coordination-conformance fixture, an existing-document evidence pipeline, an existing Python backend path, a GraphQL.js approach, and a sandbox-runtime integration guide. These examples separate the guarantees Ablo enforces from application, provider, database, and deployment responsibilities.
Pricing and plan limits moved
The rate card, the included allowances, and the plan ceilings all changed in this release. The canonical page is published at https://docs.abloatai.com/pricing and needs no sign-in.