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 aModelIntentHandle. 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'. Anactiveintent is the lock.Added
ablo.<model>.intent(id)→ModelIntentHandle<T>, besidecreate/update/retrieve/loadon every model.- Read side (any participant, synchronous + reactive):
current(the holder’s intent, ornull),status('idle'when free),settled(). - Write side (the holder):
acquire(),acquireOrAwait(), lease-guardedupdate(),release(),revoke(). AsyncDisposable:await using lock = ablo.<model>.intent(id)auto-releases on scope exit.
- Read side (any participant, synchronous + reactive):
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 withacquireOrAwait()(wait) or readcurrentfor a reactive “who’s editing” badge, rather than catching a rejection.
Deprecated
- Participant-level
intents.claim()/onRejected()and theintent_rejectedwire 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 ondefineSchema(...). EachIdentityRoledeclares{ 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 registeredidentityRoles, 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 everydefineSchema(...)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 fromAuthProvider. 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 showsidentityRolesin the canonicaldefineSchemaexample plus a “Declaring scope on a model” subsection coveringorgScoped/scopedVia/syncGroupFormat.docs/capabilities.md,docs/api.md,docs/mcp.md, andAGENTS.mdcross-reference theidentityRolessection and use the renamed fields throughout.